JavaScript的一些认识
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript的一些认识相关的知识,希望对你有一定的参考价值。
最近看了一下《nodejs开发指南》发现nodejs在某些特定的领域由他自己的长处,适合密集计算但是业务逻辑比较简单的场景,如果做网站还是选择php吧,呵呵,这本书我除了第5章《用nodejs开发web》没有看,其他章节都大概看完了,了解了nodejs的简单用法,感觉对我作用最大的还是附录A《javascript的高级特性》,这里的内容让我对js的高级特性有了深一步的认识,以下做个记录:
一、作用域
和C、C++、Java 等常见语言不同,JavaScript 的作用域不是以花括号包围的块级作用域(block scope),这个特性经常被大多数人忽视,因而导致莫名其妙的错误。例如下面代码,在大多数类C 的语言中会出现变量未定义的错误,而在JavaScript 中却完全合法:
if(true) { var somevar = ‘value‘; } console.log(somevar); // 输出value
这是因为JavaScript 的作用域完全是由函数来决定的,if、for 语句中的花括号不是独立的作用域。
1、函数作用域
不同于大多数类C 的语言,由一对花括号封闭的代码块就是一个作用域,JavaScript 的作用域是通过函数来定义的,在一个函数中定义的变量只对这个函数内部可见,我们称为函数作用域。在函数中引用一个变量时,JavaScript 会先搜索当前函数作用域,或者称为“局部作用域”,如果没有找到则搜索其上层作用域,一直到全局作用域。我们看一个简单的例子:
var v1 = ‘v1‘;
var f1 = function() { console.log(v1); // 输出v1 }; f1();
var f2 = function() { var v1 = ‘local‘; console.log(v1); // 输出local }; f2();
以上示例十分明了,JavaScript 的函数定义是可以嵌套的,每一层是一个作用域,变量搜索顺序是从内到外。下面这个例子可能就有些令人困惑:
var scope = ‘global‘; var f = function() { console.log(scope); // 输出undefined var scope = ‘f‘; } f();
这是JavaScript 的一个特性,按照作用域搜索顺序,在console.log 函数访问 scope 变量时,JavaScript 会先搜索函数f 的作用域,恰巧在f 作用域里面搜索到scope 变量,所以上层作用域中定义的scope 就被屏蔽了,但执行到 console.log 语句时,scope 还没被定义,或者说初始化,所以得到的就是 undefined 值了。
函数作用域的嵌套关系是定义时决定的,而不是调用时决定的,也就是说,JavaScript 的作用域是静态作用域,又叫词法作用域,这是因为作用域的嵌套关系可以在语法分析时确定,而不必等到运行时确定。下面的例子说明了这一切:
var scope = ‘top‘; var f1 = function() { console.log(scope); };
f1(); // 输出top
var f2 = function() { var scope = ‘f2‘; f1(); }; f2(); // 输出to
这个例子中,通过f2 调用的f1 在查找 scope 定义时,找到的是父作用域中定义的scope 变量,而不是 f2 中定义的 scope 变量。这说明了作用域的嵌套关系不是在调用
时确定的,而是在定义时确定的。
2、全局作用域
在JavaScript 中有一种特殊的对象称为 全局对象。这个对象在Node.js 对应的是 global对象,在浏览器中对应的是 window 对象。由于全局对象的所有属性在任何地方都是可见的,所以这个对象又称为 全局作用域。全局作用域中的变量不论在什么函数中都可以被直接引用,而不必通过全局对象。
满足以下条件的变量属于全局作用域:
? 在最外层定义的变量;
? 全局对象的属性;
? 任何地方隐式定义的变量(未定义直接赋值的变量)。
需要格外注意的是第三点,在任何地方隐式定义的变量都会定义在全局作用域中,即不通过var 声明直接赋值的变量。这一点经常被人遗忘,而模块化编程的一个重要原则就是避免使用全局变量,所以我们在任何地方都不应该隐式定义变量。
二、闭包
1、啥叫闭包
闭包的严格定义是“由函数(环境)及其封闭的自由变量组成的集合体。”,通俗地讲,JavaScript 中每个的函数都是一个闭包
var generateClosure = function() { var count = 0; var get = function() { count ++; return count; }; return get; }; var counter = generateClosure(); console.log(counter()); // 输出1 console.log(counter()); // 输出2 console.log(counter()); // 输出3
当一个函数返回它内部定义的一个函数时,就产生了一个闭包,闭包不但包括被返回的函数,还包括这个函数的定义环境。上面例子中,当函数generateClosure() 的内部函数get 被一个外部变量counter 引用时,counter 和generateClosure() 的局部变量就是一个闭包
var generateClosure = function() { var count = 0; var get = function() { count ++; return count; }; return get; }; var counter1 = generateClosure(); var counter2 = generateClosure(); console.log(counter1()); // 输出1 console.log(counter2()); // 输出1 console.log(counter1()); // 输出2 console.log(counter1()); // 输出3 console.log(counter2()); // 输出2
counter1 和 counter2 分别调用了 generate-Closure() 函数,生成了两个闭包的实例,它们内部引用的 count 变量分别属于各自的运行环境。我们可以理解为,在generateClosure() 返回get 函数时,私下将 get 可能引用到的 generateClosure() 函数的内部变量(也就是 count 变量)也返回了,并在内存中生成了一个副本,之后generateClosure() 返回的函数的两个实例counter1和counter2 就是相互独立的了。
2、闭包的用途
1)、嵌套的回调函数
如下代码是在Node.js 中使用MongoDB 实现一个简单的增加用户的功能:
exports.add_user = function(user_info, callback) { varuid = parseInt(user_info[‘uid‘]); mongodb.open(function(err, db) { if(err) {callback(err); return;} db.collection(‘users‘, function(err, collection) { if(err) {callback(err); return;} collection.ensureIndex("uid", function(err) { if(err) {callback(err); return;} collection.ensureIndex("username", function(err) { if(err) {callback(err); return;} collection.findOne({uid: uid}, function(err) { if(err) {callback(err); return;} if(doc) { callback(‘occupied‘); } else{ varuser = { uid: uid, user: user_info, }; collection.insert(user, function(err) { callback(err); }); } }); }); }); }); }); };
这段代码中用到了闭包的层层嵌套,每一层的嵌套都是一个回调函数。回调函数不会立即执行,而是等待相应请求处理完后由请求的函数回调。我们可以看到,在嵌套的每一层中都有对 callback 的引用,而且最里层还用到了外层定义的 uid 变量。由于闭包机制的存在,即使外层函数已经执行完毕,其作用域内申请的变量也不会释放,因为里层的函数还有可能引用到这些变量,这样就完美地实现了嵌套的异步回调。
2)、实现私有成员
JavaScript 的对象没有私有属性,也就是说对象的每一个属性都是曝露给外部的。这样可能会有安全隐患,譬如对象的使用者直接修改了某个属性,导致对象内部数据的一致性受到破坏等。JavaScript通过约定在所有私有属性前加上下划线(例如_myPrivateProp),表示这个属性是私有的,外部对象不应该直接读写它。但这只是个非正式的约定,假设对象的使用者不这么做,有没有更严格的机制呢?答案是有的,通过闭包可以实现。
var generateClosure = function() { var count = 0; var get = function() { count ++; return count; }; return get; }; var counter = generateClosure(); console.log(counter()); // 输出1 console.log(counter()); // 输出2 console.log(counter()); // 输出3
只有调用counter() 才能访问到闭包内的 count 变量,并按照规则对其增加1,除此之外决无可能用其他方式找到count 变量。受到这个简单例子的启发,我们可以把一个对象用闭包封装起来,只返回一个“访问器”的对象,即可实现对细节隐藏
三、对象
1、对象的创建和访问
JavaScript 中的对象实际上就是一个由属性组成的关联数组,属性由名称和值组成,值的类型可以是任何数据类型,或者函数和其他对象
在JavaScript 中,你可以用以下方法创建一个简单的对象:
var foo = {}; //也可以用这种 var foo = new Object() 来显式地创建一个对象。
foo.prop_1 = ‘bar‘; foo.prop_2 = false; foo.prop_3 = function() { return‘hello world‘; } console.log(foo.prop_3());
我们还可以用关联数组的模式来创建对象,以上代码修改为:
var foo = {}; foo[‘prop1‘] = ‘bar‘; foo[‘prop2‘] = false; foo[‘prop3‘] = function() { return‘hello world‘; }
2、call 和 apply
call 和 apply 的功能是以不同的对象作为上下文来调用某个函数。简而言之,就是允许一个对象去调用另一个对象的成员函数
call 和 apply 的功能是一致的,两者细微的差别在于 call 以参数表来接受被调用函数的参数,而 apply 以数组来接受被调用函数的参数。call 和 apply 的语法分别是:
func.call(thisArg[, arg1[, arg2[, ...]]])
func.apply(thisArg[, argsArray])
var someuser = { name: ‘byvoid‘, display: function(words) { console.log(this.name + ‘ says ‘ + words); } };
var foo = { name: ‘foobar‘ }; someuser.display.call(foo, ‘hello‘); // 输出foobar says hello
3、原型
functionPerson() { }
Person.prototype.name = ‘BYVoid‘; Person.prototype.showName = function() { console.log(this.name); };
varperson = newPerson(); person.showName();
上面这段代码使用了原型而不是构造函数初始化对象。这样做与直接在构造函数内定义属性有什么不同呢?
? 构造函数内定义的属性继承方式与原型不同,子对象需要显式调用父对象才能继承构造函数内定义的属性。
? 构造函数内定义的任何属性,包括函数在内都会被重复创建,同一个构造函数产生的两个对象不共享实例。
? 构造函数内定义的函数有运行时闭包的开销,因为构造函数内的局部变量对其中定义的函数来说也是可见的。
function Foo() { var innerVar = ‘hello‘; this.prop1 = ‘BYVoid‘; this.func1 = function(){ innerVar = ‘‘; }; }
Foo.prototype.prop2 = ‘Carbo‘; Foo.prototype.func2 = function() { console.log(this.prop2); };
var foo1 = newFoo(); var foo2 = newFoo(); console.log(foo1.func1 == foo2.func1); // 输出false console.log(foo1.func2 == foo2.func2); // 输出true
那么我们什么时候使用原型,什么时候使用构造函数内定义来创建属性呢?
? 除非必须用构造函数闭包,否则尽量用原型定义成员函数,因为这样可以减少开销。
? 尽量在构造函数内定义一般成员,尤其是对象或数组,因为用原型定义的成员是多个实例共享的。
4、原型链
JavaScript 中有两个特殊的对象:Object 与Function,它们都是构造函数,用于生成对象。Object.prototype 是所有对象的祖先,Function.prototype 是所有函数的原
型,包括构造函数,我把JavaScript 中的对象分为三类,一类是用户创建的对象,一类是构造函数对象,一类是原型对象。
用户创建的对象,即一般意义上用new 语句显式构造的对象。构造函数对象指的是普通的构造函数,即通过 new 调用生成普通对象的函数。原型对象特指构造函数prototype 属性指向的对象。这三类对象中每一类都有一个__proto__ 属性,它指向该对象的原型,从任何对象沿着它开始遍历都可以追溯到 Object.prototype。构造函数对象有prototype 属性,指向一个原型对象,通过该构造函数创建对象时,被创建对象的 __proto__ 属性将会指向构造函数的 prototype 属性。原型对象有 constructor属性,指向它对应的构造函数。
functionFoo() { } Object.prototype.name = ‘My Object‘; Foo.prototype.name = ‘Bar‘;
var obj = new Object(); var foo = newFoo();
console.log(obj.name); // 输出My Object console.log(foo.name); // 输出Bar console.log(foo.__proto__.name); // 输出Bar console.log(foo.__proto__.__proto__.name); // 输出My Object console.log(foo. __proto__.constructor.prototype.name); // 输出Bar
在JavaScript 中,继承是依靠一套叫做原型链(prototype chain)的机制实现的。属性继承的本质就是一个对象可以访问到它的原型链上任何一个原型对象的属性。例如上例的foo 对象,它拥有foo. __proto__ 和 foo. __proto__.__proto__ 所有属性的浅拷贝(只复制基本数据类型,不复制对象)。所以可以直接访问foo.constructor(来自foo.
__proto__,即Foo.prototype),foo.toString(来自foo. __proto__.__proto__,即Object.prototype)。
5、对象的复制
JavaScript 和Java 一样都没有像C语言中一样的指针,所有对象类型的变量都是指向对象的引用,两个变量之间赋值传递一个对象并不会对这个对象进行复制,而只是传递引用,有些时候我们需要完整地复制一个对象,这该如何做呢?Java 语言中有 clone 方法可以实现对象复制,但JavaScript 中没有这样的函数。因此我们需要手动实现这样一个函数,一个简单的做法是复制对象的所有属性:
//自己写的clone方法
Object.prototype.clone = function() { var newObj = {}; for(var i in this) { newObj[i] = this[i]; } return newObj; }
//定义一个obj对象 var obj = { name: ‘byvoid‘, likes: [‘node‘] };
//clone一个对象 var newObj = obj.clone(); obj.likes.push(‘python‘);
console.log(obj.likes); // 输出[ ‘node‘, ‘python‘ ] console.log(newObj.likes); // 输出[ ‘node‘, ‘python‘ ]
上面的代码是一个对象浅拷贝(shallow copy)的实现,即只复制基本类型的属性,而共享对象类型的属性。浅拷贝的问题是两个对象共享对象类型的属性,浅拷贝的问题是两个对象共享对象类型的属性,例如上例中 likes 属性指向的是同一个数组。
实现一个完全的复制,或深拷贝(deep copy)并不是一件容易的事,因为除了基本数据类型,还有多种不同的对象,对象内部还有复杂的结构,因此需要用递归的方式来实现:
Object.prototype.clone = function() { var newObj = {}; for(var i in this) { if(typeof(this[i]) == ‘object‘ || typeof(this[i]) == ‘function‘) { newObj[i] = this[i].clone(); } else{ newObj[i] = this[i]; } } return newObj; };
Array.prototype.clone = function() { var newArray = []; for(var i = 0; i < this.length; i++) { if(typeof(this[i]) == ‘object‘ || typeof(this[i]) == ‘function‘) { newArray[i] = this[i].clone(); } else{ newArray[i] = this[i]; } } return newArray; };
Function.prototype.clone = function() { var that = this; var newFunc = function() { returnthat.apply(this, arguments); }; for(var i in this) { newFunc[i] = this[i]; } returnnewFunc; };
var obj = { name: ‘byvoid‘, likes: [‘node‘], display: function() { console.log(this.name); }, };
var newObj = obj.clone(); newObj.likes.push(‘python‘); console.log(obj.likes); // 输出[ ‘node‘ ] console.log(newObj.likes); // 输出[ ‘node‘, ‘python‘ ] console.log(newObj.display == obj.display); // 输出false
以上是关于JavaScript的一些认识的主要内容,如果未能解决你的问题,请参考以下文章
JavaScript中作用域和作用域链的简单理解(变量提升)