Javascript Closures(javascript闭包概念)

原文:http://hi.baidu.com/ciici/blog/item/8a24a4584389b8d79c820490.html

Javascript Closures(javascript闭包概念)
2009年06月17日 星期三 11:57

1.简介

闭包(closure) 是 JS 最强大的特性之一,简单地说,闭包就是内部方法,即定义在方法内的方法,它们可以访问外部方法内的变量和参数,即使外部方法的执行已经终止。例如:

1
2
3
4
5
6
function example(arg1){
  var localVar = 2;
  return function inner(arg2){
    return arg1+localVar + arg2;
  }
}

要了解闭包的内部机制不是一件简单的事情,有许多准备工作要做。我们先来看一下有关对象的一些东西。

2.对象和对象属性

在 JS 中一切都是对象,包括 function 。对象可能会有一些属性,这些属性的值可能是另外一个对象,也可能是原生的数据类型:String, Number, Boolean, Null 或者 Undefined 。下面我们来看一下给对象属性赋值和读取对象属性的原理。

1
2
var o = new Object();
o.testNumber = 5;

上面的代码中我们先是创建了一个对象 o ,然后对其属性 testNumber 赋值。请注意,一开始的时候对象o是没有 testNumber 这个属性的,所以当赋值的时候 JS 会先检查,发现没有这个属性就先创建这个属性,然后再对其进行赋值。如果这个时候再执行下面的代码:

1
o.testNumber = 8;

JS 同样会先检查对象o ,发现它有testNumber这个属性之后,就不会再创建该属性,而是直接重新设置它的值。下面看一下读取对象属性:

1
2
o.testNumber = 8;
var val = o.testNumber;

读取对象属性的时候如果对象有这个属性就会返回它的值。很简单,不过有意思的地方在于它的内部机制 :) 首先我们先来了解一下原型链(prototype chain)的概念,所有的对象都会有一个原型(prototype),而它们的原型也是一个对象,这就是说它们的原型也可能有自己的原型,于是原型链就形成了。在JS中,Object 的默认原型是 null ,所以:

1
var o = new Object();

会创建一个原型为 Object.prototype 的对象 o,由于 Object.prototype 的prototype 为空 ,所以对象 o 的原型链就只有一个,就是最原始的 Object.prototype 。但是下面的代码就复杂一点:

1
2
3
4
5
6
7
8
9
10
function MyObject1(formalParameter){ 
 this.testNumber = formalParameter; 
} 
 
function MyObject2(formalParameter){ 
 this.testString = formalParameter; 
} 
 
MyObject2.prototype = new MyObject1( 8 );
var objectRef = new MyObject2( "String_Value" );

objectRef 所指向的 MyObject2 的实例有一个原型链,原型链中的第一个对象是 MyObject1 的prototype,而 MyObject1 的 prototype 的 prototype 是最原始的 Object.prototype ,而Object.prototype的prototype为空,所以原型链至此结束。

OK。下面我们来继续说对象属性的读取,其实读取一个对象属性的过程可能会涉及到该对象原型链中的所有对象。在上面那段代码中,我们来读取下面的属性:

1
var val = objectRef.testString;

由于objectRef 所指向的 MyObject2 的实例上有这个名为 testString 的属性,所以就会直接把这个属性的值”String_Value”赋给变量 val 。但是:

1
var val = objectRef.testNumber;

我们会发现 val 会被赋值为 8 ,可是objectRef 所指向的 MyObject2 的实例上没有这个名为 testNumber 的属性啊?为什么val 并不会被赋值为 undefined 呢?因为在对象自身上找不到该属性的时候,它就会检查对象的原型链,于是发现objectRef 指向的 MyObject2 的实例的prototype 是 MyObject1 的一个实例,而在这个实例中由一个值为 8 的 testNumber 属性,那么JS就会把这个值赋给 val 。我们看到,在 MyObject1 和 MyObject2 中都没有定义一个叫做 toString 的属性,但是当我们这样获取 objectRef 的 toString :

1
var val = objectRef.toString;

还是可以获取到。相信聪明的你已经想到了,因为 objectRef 的原型链的末端是 Object.prototype ,而在这个对象上面由一个名为 toString 的方法。最后:

1
var val = objectRef.madeUpProperty;

这次 val 终于是 undefined 了,因为检查完所有原型链都找不到名为 madeUpProperty 的属性,于是只好放弃。

综上,我们可以看到,在读取对象属性的时候,会先检查对象本身,然后依次检查它的原型链中的对象,在检查过程中发现符合要求的值就终止检查,然后返回。也就是说,它会返回第一个符合要求的值。了解了对象的赋值和读取过程之后,我们会发现一个有意思的事情,如果我们在上面代码的基础上执行:

1
objectRef.testNumber = 3;

由于objectRef 所指向的 MyObject2 的实例上没有 testNumber 这个属性,所以就会在该实例上创建一个名为 testNumber 的属性并赋值为 3 ,注意,这里并没有改变原型链上 MyObject1 的实例上的 testNumber 属性的值。而当我们再次获取 objectRef.testNumber 的值的时候,JS 会首先检查 objectRef 所指向的实例,然后就发现有这个属性,于是直接返回它的值:3 。这样一来,objectRef 原型链上的那个值为 8 的testNumber 属性就被隐藏了起来。

执行环境(Execution Context)

执行环境(Execution Context) 是 ECMAScript 定义中的一个抽象概念,用来定义 ECMAScript 执行时所需要的一些行为。所有的 JS 代码都是在一个执行环境中被执行的,全局的代码是在一个全局的执行环境中执行的,而对函数的每次调用都会产生一个相应的执行环境。

当一个函数被调用的时候,就会进入到一个执行环境中,当另外一个函数被调用(或者同一个函数被递归调用)的时候就会进入到一个新的执行环境,而且当这个函数返回的时候,就会回到原来的执行环境中。所以,运行中的 JS 代码会有一个执行环境堆栈。

当一个执行环境被创建的时候,会发生一系列的事情。首先,它会创建一个 “Activation” 对象,不过这个对象很特别,因为它没有 prototype 并且不能在代码中直接引用。然后就会创建一个叫做 “arguments” 类似数组的对象,里面保存了传过来的参数,同时”Activation” 对象上会创建一个叫做 “arguments” 的属性并指向刚刚创建的 “arguments”对象。

接下来,执行环境就会被赋予一个作用域(scope)。一个作用域一般是由一系列的对象组成的,每个函数对象都有一个内部的 [[scope]] 属性,该属性也是由一系列的对象组成的。执行环境被赋予的那个 scope 就会被相应函数对象中的[[scope]] 属性所引用,同时会把开始创建的 “Activation” 对象插入到 scope 包含对象的最前面。

然后,就会使用ECMA 262提到的一个 “Variable” 对象进行变量初始化。事实上,前面创建的 “Activation” 对象和这里的”Variable” 对象其实是同一个对象。初始化的时候会在 “Variable” 对象上创建一些和函数定义中的参数同名的属性,如果有传过来的参数,就会把传过来的值赋给 “Variable” 对象上相应的属性(不然就赋值undefined)。如果有内部函数,那么就会创建一个函数对象,然后在 “Variable” 对象上创建一个和内部函数名字相同的属性,并把刚创建的函数对象赋值给它。变量初始化的最后一步就是在 “Variable” 对象上创建和函数内部的声明的本地变量同名的所有属性。

其实在变量初始化的时候,所有本地变量对应的 “Variable” 对象上的属性值都是 undefined,它们并没有被真正初始化。只有当执行到函数体内对它们赋值的语句的时候,它们才算是真正初始化了。

正是由于 “Activation” 对象和 “Variable” 对象其实是同一个对象,所以在代码中,”Activation” 对象上的 “arguments” 属性就可以像本地变量那样被引用。

最后,会对this关键字赋一个值。如果this被赋值为一个对象,那么以this.为前缀的所有属性都是指该对象上的属性,如果this 被赋值为 null ,那么它就指向 global 对象。

全局的执行环境相对于函数的执行环境来说,有那么一点不同。因为全局执行环境不需要参数,所以它也就不需要创建一个 “Activation” 对象来引用它们。全局执行环境也有一个 scope,不过它的 scope只包含一个对象,就是 global对象。它也会经过变量初始化,而且 global 对象会充当其中的 “Variable” 对象 ,这就是为什么在全局作用域声明的函数和变量都会作为 global 对象的属性。全局的执行环境中,this关键字指向的也是 global 对象。

scope 链 和 [[scope]] 属性

要了解 scope 链,首先需要了解的就是内部的 [[scope]] 属性。在ECMAScript 中,所有的函数都是对象,在执行函数声明或者表达式的时候会创建函数对象,它们也可以通过构造函数 Function 被创建。

在通过构造函数Function 来创建函数对象的时候,这个函数对象内部的 [[scope]] 属性总是指向一个只包含了global对象的scope链。通过声明或者表达式创建的函数对象内部的[[scope]]属性则指向其所在的执行环境的scope 链。

比如下面两段代码:

1
2
3
function exampleFunction(formalParameter)
{ ... // function body code 
}
1
2
3
var exampleFuncRef = function(){ 
... // function body code 
}

虽然写法有点不同,函数对象创建的时间也不同,但是由于它们所处的都是全局的执行环境,所以它们内部的[[scope]] 属性指向的scope 链只包含一个 global 对象。

内部函数有一些不同,因为它们是在函数内部定义的,所以执行环境就不是全局执行环境,scope链上的对象就比较多。看一下下面的代码:

1
2
3
4
5
6
7
8
function exampleOuterFunction(formalParameter){ 
    function exampleInnerFuncitonDec(){ 
        ... // inner function body 
    } 
... // the rest of the outer function body. 
} 
 
exampleOuterFunction( 5 );

对于内部函数exampleInnerFuncitonDec()来说,当外部函数被调用的时候,就创建了一个新的执行环境和相应的Activation/Variable 对象,所以它内部的[[scope]]属性就指向了当前的scope链:包含了当前执行环境的Activation 对象以及外部函数的 [[scope]] 上的global对象。

由此我们看到,代码的逻辑和结构会自动控制 scope 链的创建。不过ECMAScript提供了一个 with 语句,使用它可以更改scope 链。

函数声明不会受到 with 语句的影响,因为它们在变量初始化的时候就已经创建相应的函数对象了,但是函数表达式可以,看下面的代码:

1
2
3
4
5
6
7
8
9
10
var y = {x:5};
function exampleFuncWith(){ 
var z; 
   with(y){ 
      z = function(){ 
          ... // inner function expression body; 
      } 
    } 
... 
}

由于使用了 with 语句,在函数执行的时候,内部函数 z 的 [[scope]] 属性指向的 scope 链的前面又会插入一个 y 对象,变成: y对象->当前执行环境的Activation对象->global对象。

接下来看一下标识符定位的问题。当代码运行的时候如果发现了某个标识符,就会在scope链中的对象上寻找与它对应的变量,从第一个开始依次找,如果找到就返回,不然就一直找到 scope 链的末尾。由于函数被调用的时候会创建新的执行环境,就会把相应的Activation/Variable 对象插入到 scope 链的前面,所以对于函数体中的标识符,总是会先检查是否有相对应的内部函数、参数或者本地变量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值