Javascript作用域链和闭包
闭包指有权访问另一个函数作用域中变量的函数,通常是一个函数和以静态方式存储的所有父级作用域的一个集合体。
创建闭包常见方式,就是在一个函数内部创建另一个函数。因为内部函数的作用域链包含了外部函数的作用域,所以可以访问外部函数内定义的变量。注意,外部函数的AO和VO中,不会出现闭包的任何内容。
多个函数可能拥有相同的父级作用域,常见的例子就是拥有多个内部函数(比如return了一个包含多个函数的对象)。此时,一个闭包对变量的修改会体现在另一个闭包对变量的读取上。
先看一个例子:
var t = 0, arr=[];
for(var i=0;i<=3;i++) {
t++;
arr.push(function(){console.log(t)});
}
arr[0]();
大部分人对这个问题了如指掌,输出结果也能立即说出:4。
修改方法也简单,用闭包:
var t = 0, arr=[];
for(var i=0;i<=3;i++) {
t++;
arr.push(
(function(t){
return function(){
console.log(t);
}
})(t)
);
}
arr[0](); // 输出1
因为立即执行函数本身和闭包没有什么关系,可以用命名函数来取代,然后循环展开,所以第一段代码等同于下面这个:
var t = 0, arr = [],
func = function(){
console.log(t);
};
t++;
arr.push(func);
t++;
arr.push(func);
t++;
arr.push(func);
arr[0]();
闭包修改之后等同于下面:
var t = 0, arr=[],
func = function(t){
return function(){
console.log(t);
}
};
arr.push(func(++t));
arr.push(func(++t));
arr.push(func(++t));
arr[0](); // 输出1
下面开始看闭包是什么,又做了什么操作。
闭包的相关概念
想从作用域链看懂闭包,首先要知道几个概念:
1、执行环境(Execution Context
):执行环境对应了变量或函数有权访问的数据,决定了它们各自的行为。执行环境可以看作一个对象,其中包括了三个属性:变量对象(variable object
),this指针(this value
),作用域链(scope chain
)。
EC={
VO:{/* 函数声明和形参(arguments对象),内部的变量 */},
Scope:{ /* VO以及所有父执行上下文中的VO(scope chain) */}, //函数调用时确定
this:{} //函数调用时确定
}
2、执行环境栈(Execution Context Stack
):由于函数的每次调用,都会生成上述具有全新状态的函数执行环境,而在函数内部调用另一个函数,又会触发进入另一个执行环境,控制权会转给新的执行环境。从逻辑上来说,这个情况以栈的形式实现,因此叫做执行环境栈。换句话说,所有程序运行时,都可以用执行环境栈来表示:栈顶是当前活跃的执行环境,栈底是全局执行环境。当栈中某个执行环境运行完毕后,会出栈,直到只剩下全局执行环境。
3、变量对象(Variable Object
-- VO):每个执行环境都有一个与之关联的变量对象,环境里定义的所有变量和函数(及其形参)都保存在这个对象中(函数表达式不包含在变量对象中)。分为全局环境VO和函数环境VO。
4、活动对象(Activation Object
):当一个函数被调用,活动对象将被创建,其中包含了变量和函数(及其形参)以及arguments对象
。活动对象之后会作为函数执行环境的变量对象来使用,即多了一个arguments
(与VO相比)。
5、this对象:this对象是在运行时,基于函数执行环境绑定的,任何对象都可以作为this的值,它是执行环境的一个属性。所以,代码中访问this时,直接从执行环境中获取,而不是通过作用域链查找。
全局函数时,等于window;被作为某个对象的方法调用时,等于那个对象。
函数在被调用时,默认会取得两个特殊变量this
和arguments
。匿名函数的执行环境具有全局性(未绑定到任何对象),所以this对象
通常指向window
。例子:
var name = 'window name';
var object = {
name: 'obj name',
getName: function(){
return function(){
console.log(this.name);
}
}
}
object.getName()()
以上代码执行时,内部函数未绑定到任何对象(以object.getName()
表达式来调用,相当于在全局环境中执行),所以this
指向window
,因此永远不可能直接访问外部函数中的name
变量。
6、作用域链(Scope Chain
):内容是执行环境中的变量对象的有序链表。用途是保证对执行环境有权访问的所有变量和函数的有序访问,即标识符解析。
注意,[[Scope]]属性和作用域链(Scope Chain
)是不同的,前者是函数的属性,是静态的,保存了所有上层变量对象的分层链,在函数创建的时候保存在函数中。后者是执行环境的属性,有了函数调用,作用域链(和this
)才会确定。
作用域链的前端始终都是当前执行的代码所在环境的变量对象。作用域链下一个变量来自包含(外部)环境,再下一个来自下一个包含环境,直至全局执行环境。而全局执行环境始终都是作用域链里最后一个对象。
标识符解析是沿着作用域链一层一层解析的,从最前端开始,直至全局环境的变量对象,搜到为止,搜不到则说明变量未声明。这个规则和原型链相似,均为向上查找。
var fn = null;
function foo() {
var a = 2;
function innnerFoo() {
console.log(c);
console.log(a);
}
fn = innnerFoo;
}
function bar() {
var c = 100;
fn();
}
foo();
bar();
上面代码执行时,foo函数给fn赋值为引用innerFoo。当bar函数执行时,实际调用了innerFoo函数。由于innerFoo是foo函数的内部函数,在执行时,其作用域链包含AO-闭包的AO(fooAO)-globalVO
(函数调用时,活动对象作为执行环境的变量对象来使用),而变量也会按这个顺序在执行环境中取得。所以搜索c会报未声明的错误,a正常输出2。
回到最初的问题,第一个例子arr[0]()为什么输出了4?
首先,arr添加了4个匿名函数作为值。当作为函数调用时,需要输出变量t,匿名函数开始在作用域链中寻找t,首先自身作用域中没有,再往上就是全局作用域,找到了t,此时全局作用域里的t已经通过循环累加到4,所以直接输出4。
闭包
闭包指有权访问另一个函数作用域中变量的函数,通常是一个函数和以静态方式存储的所有父级作用域的一个集合体。
创建闭包常见方式,就是在一个函数内部创建另一个函数。因为内部函数的作用域链包含了外部函数的作用域,所以可以访问外部函数内定义的变量。注意,外部函数的AO和VO中,不会出现闭包的任何内容。
多个函数可能拥有相同的父级作用域,常见的例子就是拥有多个内部函数(比如return了一个包含多个函数的对象)。此时,一个闭包对变量的修改会体现在另一个闭包对变量的读取上。
function foo(){
var x = 1;
return { // funcA与funcB拥有相同的父级作用域
funcA: function(){return ++x;},
funcB: function(){return x;}
}
}
var bar = foo();
bar.funcA();
bar.funcB();
更重要的是,外部函数执行完毕后,其活动对象也不会被销毁(但是其执行环境的作用域链会被销毁),因为内部函数作用域链仍在引用这个活动对象。直到内部函数被销毁后,外部函数的活动对象才会被销毁。
function outer(){
var name = 'outer';
return function(){
// stuff
console.log('outer var: ' + name);
}
}
var inner = outer();
var result = inner();
// 解除对匿名函数的引用,以便释放内存
inner = null;
闭包的使用
模仿块级作用域:js没有块级作用域的概念。所有变量,都是在包含函数中创建的。可以使用立即执行函数声明并立即调用一个函数(因为function开始的为函数声明,后面不能跟括号,需要用括号将整个function声明转为函数表达式),来模仿块级作用域。
用于限制向全局作用域中添加过多变量或者函数,避免多人维护时命名冲突。
(function(){
var x = 1;
// stuff
})();
console.log(x); // Uncaught ReferenceError: x is not defined
私有变量:任何在函数中定义的变量,都可以认为是私有变量。包括函数的参数、局部变量和内部定义的函数。可以在对象的构造函数内部定义私有变量,并定义特权方法(接口)以访问这些私有变量。
function Foo(val){
var x = val; //对象构造函数内定义私有变量
this.getVal = function(){ //特权方法(接口)
return x;
};
}
var f = new Foo();
f.getVal();
利用私有变量,隐藏了不应该被直接修改的数据。但是,由于在构造函数中定义的特权方法,所以针对每个实例都会创建同样的新方法,代码复用性较差。
静态私有变量:在私有作用域(立即执行函数)中定义私有变量或者方法。将对象的构造函数(使用函数表达式而不是函数声明)升为全局变量(不使用var
),以便在外部访问对象。可以通过prototype
在原型上定义共享方法。
(function(){
var name = '';
Foo = function(value){
name = value;
};
Foo.prototype.getName = function(){
return name;
}
})();
var f = new Foo('Al');
f.getName(); // Al
var f2 = new Foo('Eric');
f2.getName(); // Eric
f.getName(); // Eric
私有变量name成为一个静态的、由所有实例共享的属性。每个实例对name的修改,都会影响到其他实例。但是由于使用了原型方法,增进了代码复用。
模块模式:为单例对象(只有一个实例的对象,js里用字面量创建即可)添加私有变量和接口。具体方式为,函数内部定义私有变量和函数,返回一个包含公共接口的对象字面量。在需要对单例进行初始化,同时又需要维护其私有变量时使用。示例:
var singleton = function(){
var privateVal = true;
function privateFunc(){
console.log('here is the privateFunc');
}
return {
publicVal: false,
publicMethod: function(){
console.log(privateVal);
privateFunc();
}
}
}
var instance = singleton();
console.log(instance.publicVal);
instance.publicMethod();
增强的模块模式:与模块模式类似,不直接返回对象,而是创建特定类型的对象,然后为该对象添加变量或者接口,最后返回。
最后,如果把最开始的例子改一下:
var obj = {
a:0,
b: 1
};
for(var i=0,arr=[];i<=3;i++) {
obj.a++;
arr.push(
(function(o){
return function(){
console.log(o);
}
})(obj)
);
}
arr[0](); //???
那么输出的答案是{a:1, b:1}
吗?
学习了,却细看不下,最后那里感觉绕
@zwwooooo 后面部分是先把书上的内容记下来了
@zwwooooo 烧脑。
直接浏览器里看答案!
第一个例子跑起来就是undefine …