闭包

一、执行上下文

先看一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
function test(o){
console.log(i); //undefined
var i = 0;
if (typeof o == 'object') {
var j = 0;
for (var k = 0; k < 10; k++){
console.log(k); //0~9
}
console.log(k); //10
}
function log(){ console.log(j); }
console.log(j); //0 | undefined
}

我们知道,JavaScript的作用域只有全局作用域和函数作用域,而且JavaScript的函数作用域是在函数内声明的所有变量在函数体内始终可见。并且其中的变量在函数内var 声明之前已经可用,即存在声明提前的现象。

上述这个函数在具体执行的时候,用到了变量i, j, k, log以及o,并且从函数开始一直到函数执行结束的不同阶段,各个变量的值也不同。我们可以将执行上下文将理解为某段代码执行时可用的各种变量和函数的具体赋值情况。还有一个隐藏的变量this也需要注意。(如果是函数内,还有一个arguments变量也可使用) 结合起来即

1.变量(包括函数内定义的变量,形参变量,函数定义式变量)
2.this值
3.函数声明

这三类变量或函数的具体的值的情况决定了某段代码的执行上下文。比如上述函数放在全局环境下执行,test({name: 'steven'}) 函数初始时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<table>
<tr>
<th rowspan="4">变量</th>
<td>o</td>
<td>{name:steven}</td>
</tr>
<tr>
<td>i</td>
<td>undefined</td>
</tr>
<tr>
<td>j</td>
<td>undefined</td>
</tr>
<tr>
<td>k</td>
<td>undefined</td>
</tr>
<tr>
<th>this</th>
<td>window</td>
<td>window</td>
</tr>
<tr>
<th>函数声明</th>
<td>log</td>
<td>function</td>
</tr>
</table>

执行到最后一句console.log(j); 时为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<table>
<tr>
<th rowspan="4">变量</th>
<td>o</td>
<td>{name:steven}</td>
</tr>
<tr>
<td>i</td>
<td>0</td>
</tr>
<tr>
<td>j</td>
<td>0</td>
</tr>
<tr>
<td>k</td>
<td>10</td>
</tr>
<tr>
<th>this</th>
<td>window</td>
<td>window</td>
</tr>
<tr>
<th>函数声明</th>
<td>log</td>
<td>function</td>
</tr>
</table>

二、作用域链

JavaScript是基于词法作用域的语言,通过阅读包含变量定义在内的数行源码就能知道变量的作用域。
全局变量在程序中始终都有定义。局部变量在声明它的函数体以及其所嵌套的函数内始终是有定义的。

每一段JavaScript代码都有一个与之关联的作用域链(scope chain),这个作用域链是一个对象列表或者链表,这组对象定义了这段代码”作用域中”的变量。假如有如下函数

1
2
3
4
5
6
7
8
9
10
var t0 = 0;
function outer(t1){
var t2 = 2;
function inner(){
console.log(t1); //1
console.log(t2); //2
}
inner();
}
outer(1);

在函数inner中的代码,其可访问的作用域,一个是自身,再一个即是外层的outer函数的作用域,因此可以访问到t1, t2变量。作用域链可以看做这一个一个作用域从内到外的链表或者列表,内层代码需找某个变量(解析变量)时即从最内层作用域找起,一直找到最底层全局作用域。

执行上下文中的变量或函数可以认为都包含了整个作用域链(一直到全局环境)中的变量和函数,可以简单认为,作用域链是静态的范围,执行上下文是代码具体执行时的带有具体值的变量和具体定义的函数。

三、自由变量

在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量。如下代码

1
2
3
4
5
var a = 10;
function fn(){
var b = 20;
console.log(a+b);
}

fn函数中使用的变量a,并不是来≠自于函数fn作用域内部定义,而是来自外部(可能是全局作用域,也可能是另一个函数作用域),则a就是一个自由变量。但是定义是否一定来自于父作用域呢,也未必,其实父作用域这个函数带有一定的迷惑性,不建议使用,看下述代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var z = 10;var i = 11;

function foo(i) {
console.log(i);
console.log(z);
}
function test(){
var z = 20;
var i = 10;
(function () {
foo(i);
})();
}
test();

foo函数中的z打印时应该为什么值呢?

##四、闭包
Javascript权威指南中提到一段:

JavaScript采用词法作用域(lexical scoping),也就是说,函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。为了实现这种词法作用域,JavaScript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。

函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性称为闭包。
(这个术语非常古老,是指函数变量可以被隐藏于作用域链之内,因此看起来是函数将变量包裹了起来)

第二条是有关闭包的定义,简单来讲,函数会将其定义处上下的变量都带上。来看两个例子

1
2
3
4
5
6
7
8
// 代码1
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() { return scope; }
return f();
}
checkscope();
1
2
3
4
5
6
7
8
// 代码2
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() { return scope; }
return f;
}
checkscope()();

代码1中,判断return的scope比较容易,是局部变量 scope,代码2中则带有一点迷惑性。回想上述理论中函数执行依赖变量作用域,这个作用域在定义时决定,就明白答案了。(local scope)

结论1:函数执行用到的作用域取决于函数定义的位置。
1
2
3
4
5
6
7
8
// 代码3
function checkNum() {
var num = 1;
function f() { return console.log(num); }
num++;
return f;
}
checkNum()();

代码3 与代码2相比,差别在于 函数f定义的位置 和返回的位置中 插入了一段有关局部变量的操作。执行结果为多少呢?(2)

结论2:(内部)函数的执行上下文取决于函数执行或者return的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
// 代码4
function checkNum() {
var num = 1;
function f1() { return console.log(num++); }
function f2() { return console.log(num+=2);}
return {f1: f1, f2: f2}
}
var tmp1 = checkNum();
tmp1.f1();
tmp1.f2();
var tmp2 = checkNum();
tmp2.f1();
tmp2.f2();

代码4中,返回的对象包含了两个函数,均带有同一个内部变量num,在checkNum外调用时。tmp1.f1() 和 tmp2.f1()返回的值相同 均为1,而 tmp1.f2() 和 tmp2.f2() 均为4

结论3:同一个上下文中创建的闭包是共用一个[[Scope]]属性的。 也就是说,某个闭包对其中的变量做修改会影响到其他闭包对其变量的读取。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 代码5
function log(i){
return function inner() { console.log(i);}
}
var dataList = [];
for(var i = 0;i < 3;i++){
dataList[i] = log(i);
}
dataList[0]();
dataList[1]();
dataList[2]();

//代码6
var dataList = [];
for(var i = 0;i < 3;i++){
dataList[i] = (function(){
return function inner(){
console.log(i);
}
})();
}
dataList[0]();
dataList[1]();
dataList[2]();

代码5和代码6的差别不仅仅在于把log函数由外部提到了内部,而是inner函数的上下文([[scope]])有所变化。代码5中inner函数的i取自外层i,传递参数不同,则i不同,而代码6中的i直接引用了外层的i,当外层的i变化时,势必引起inner中的i变化。

再来看ECMAScript中的定义,ECMAScript中,闭包指的是:

  • 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

  • 从实践角度:以下函数才算是闭包:

    • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    • 在代码中引用了自由变量

理论上它将闭包看做是某一类函数,实践中将某类特性看成闭包,虽然有些差别,但是这种现象以及实际使用中产生的效果都是相同的。

##四、特例this

在函数中this到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了。

上述提到函数的变量作用域和定义有关,但是在函数作用域内有一个特殊情况,即this,虽然它可以和变量一样使用,但是this的指向,却是在函数执行时决定的。看下述例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 代码7
var name = "The Window";
var object = {
  name : "My Object",
  getNameFunc : function(){
   return function(){
      return this.name;
   };
  }
};
alert(object.getNameFunc()());

//代码8
var name = "The Window";
var object = {
 name : "My Object",
 getNameFunc : function(){
   var that = this;
   return function(){
      return that.name;
   };
 }
};
alert(object.getNameFunc()());

代码8中虽然返回了函数 that.name,但是其上下文也一并返回,这时候设定this,可以拿到正确的值。

##延伸

参考资料:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
http://dmitrysoshnikov.com/ecmascript/chapter-6-closures/#introduction
http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
http://www.cnblogs.com/skylor/p/4721816.html
http://www.cnblogs.com/wangfupeng1988/p/3977924.html

坚持原创技术分享,您的支持将鼓励我继续创作!