本文主要是对JavaScript作用域以及闭包(一)的补充,主要补充作用域基本概念以及闭包用途两个方面。
词法作用域
在 JavaScript 中函数的嵌套关系是定义的时候决定的,而不是调用的时候决定的,也就是说,JavaScript 的作用域是静态作用域,又叫词法作用域。词法作用域取决于源码,通过静态分析就能决定,而不必等到运行时确定(with和eval除外)。
JavaScript 中的动态作用域:相对于静态作用域,动态作用域在执行时确定,其生命周期到代码片段执行完为止。在代码执行时,对应的作用域链通常是保持静态的,不过当遇到 with 语句,call、apply 方法和 try-catch 中的 catch 时,会改变作用域链。
JavaScript 中的任何函数都是被某个对象调用的,包括全局对象,所以 this 指针,也就是上下文对象是一个非常重要的东西。this 指针永远是这个引用所属的对象,引用的是动态作用域。可以理解为是对 JavaScript 静态作用域的补充。
没有块级作用域?
现在这种说法可能不太严谨了。更准确的说是,JavaScript 没有严格意义上的块级作用域。在 ES6 中,增加了一个关键字 let,可以在 {}、if、for 里声明。用法同关键字 var,但作用域限定在块级,let的变量不存在变量提升。(具体可百度 ES6 let关键字)
作用域链
在 JavaScript 中,执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中(我们无权访问,但解析器会在处理数据时在后台使用它)。
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。
当代码在一个执行环境中执行时,会创建变量对象的一个作用域链。
作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问,也就是说决定了你这个执行环境只能访问作用域链上的变量(包括函数),不能访问作用域链外的。作用域链的最开始一端始终是当前执行的代码所在的环境的变量对象。
标识符解析是沿着作用域链一级一级搜索标识符的过程,搜索过程始终从作用域的前端开始,然后逐级地向后回溯,直到找到标识符为止。(如果找不到标识符,就导致错误发生)
再说说闭包
关于闭包,下面有一个非常经典的问题:
var test = function() {
var ret = [];
for(var i=0; i<5; i++) {
ret[i] = function() {
return i;
}
}
return ret;
};
var testa = test()[0]();
console.log(testa); //输出:5
var testb = test()[1]();
console.log(testb); //输出:5
按照直觉, test()[0]() , test()[1]().. 应该分别输出1,2,3,4,5才对。不过实际上它们的结果都是5,这是由于闭包特性造成的。外部函数在执行后其执行环境已经销毁了,但是返回的函数还绑定着变量 i,所以变量 i 仍在内存中,当外部的函数执行完后,才执行内部函数,而此时内部函数所捕获到的变量已经是外部函数执行后的最终值,也就是5了,所以这些函数都引用的是同一个变量5。
闭包的用途
主要有两个用途:1实现嵌套的回调函数,2隐藏对象细节。
1.实现回调函数嵌套
先来看一段代码:
module.exports = function (param, callback) {
asyncFun1(param, function (error, data) {
if (error) return callback(error);
asyncFun2(data,function (error,data) {
if (error) return callback(error);
asyncFun3(data, function (error, data) {
if (error) return callback(error);
callback(data);
})
})
})
}
代码中用到了闭包的层层嵌套,每一层的嵌套都是一个回调函数。而回调函数都不会立即执行,而是等相应的请求处理完后由请求的函数回调。由于闭包机制的存在,即使外层函数已经执行完毕,其作用域申请的变量也不会被释放,因为里层的函数还有可能引用到这些变量,这样就能实现了嵌套的异步回调。
2.创建私有成员
还是先来看一个例子:
var closure = function() {
var count = 0;
var get = function() {
count++;
return count;
};
return get;
}
var counter = closure();
console.log(counter()); //输出1
console.log(counter()); //输出2
这样,只有调用 counter(),才能访问到闭包内的 count 变量。按照这种思路,我们就可以把一个对象用闭包封装起来,只返回一个“访问器”(getter),就可以对其实现细节进行隐藏。