当前位置: 代码迷 >> Web前端 >> 原形、作用域、闭包的完整解释(一)
  详细解决方案

原形、作用域、闭包的完整解释(一)

热度:237   发布时间:2012-10-29 10:03:53.0
原型、作用域、闭包的完整解释(一)

?

原文 Richard Cornford. March 2004.

翻译:大漠穷秋 2010-11-23

脚本娃娃 团队出品

介绍

闭包

??? 闭包是一个表达式( 通常是一个函数) ,可以有任意参数,连同绑定这些参数的环境( 它“封闭”了表达式) 一起构成。

??? 闭包是ECMAScript (javascript) 最强大的特性之一,但是不理解就无法正确地利用它。然而,创建它们相对容易,甚至无意间就可以创建,创建它们会在造成潜在的有害后果,尤其在一些相对通用的浏览器环境中。为了避免无意间遭遇这些弊端,并利用它们提供的便利,非常有必要理解其机制。这在很大成程度上取决于标识符解析过程中作用域链所扮演的角色,以及对象属性名解析方面的协议。

??? 闭包的简单解释是,ECMAScript 支持内部函数,函数定义和函数表达式位于其它函数体内部。这样就允许这些内部函数访问所有局部变量、( 外部) 函数的参数和外部函数中声明的( 其它) 内部函数。当这些内部函数之一可以在其所属的( 外部) 函数之外被访问时,就形成了一个闭包。这样,在外部函数返回之后它仍然可以执行。这时它仍然可以访问局部变量、参数和外部函数中声明的其它内部函数。当外部函数返回之后,这些局部变量、( 外部) 函数参数和函数定义( 最初) 仍然持有它们拥有的值,并且可以与内部函数交互。

??? 不幸的是,正确理解闭包需要理解它们背后的机制,以及相当多的技术细节。虽然在以下描述的开头部分,ECMA 262 指定的一些算法已经被废弃了,但是很多不能省略或者简单解释。熟悉对象属性名解析的个人可以跳过以下小节,并且可以停止阅读此文档然后回去运用它们。

对象属性名决议

??? CMAScript 承认两种类型的对象,“本地对象”和“宿主对象”("Native Object" and "Host Object") 以及一个本地对象的子集,叫做“内置对象”("Built-in Object")(ECMA262 第三版4.3) 。本地对象属于语言,宿主对象由环境提供,例如可以是document 对象、DOM 节点等等。

??? 本地对象是松散的,并且是命名属性的动态包( 当涉及到内置对象的子集时,一些实现不是那么动态,但是这通常无关紧要) 。对象定义的命名属性将会持有一个值,它可以是到另一个对象的引用( 从这个意义上说函数也是对象) ,或者是一个原始值:String,Number,Boolean,Null 或者Undefined 。原生类型Undefined 有点奇怪,利用它可以把一个对象的属性赋值为Undefined ,但是这么做不会从对象上删除这个属性;它保持为一个已定义的命名属性,仅仅持有了一个为undefined 的值。

??? 以下是一个简单描述,属性值是如何在对象上设置和读取的,尽可能最大限度地省略内部细节。

赋值

??? 通过给这个命名属性指定一个值,可以创建对象的命名属性,或者为已存在的命名属性赋值。例如给定:

??? var objectRef = new Object(); // 创建一个普通的javascript 对象。

?

??? 一个名为"testNumber" 的属性可以这样创建:

??? objectRef.testNumber = 5;

?

??? /* 或者*/

??? objectRef["testNumber"] = 5;

?

??? 在赋值之前,对象没有"testNumber" 属性,但是在赋值之后会创建一个。所有后续赋值都没有必要创建属性,仅仅重新设置它的值:

??? objectRef.testNumber = 8;

??? /* 或者*/

??? objectRef["testNumber"] = 8;

?

??? Javascript 对象的属性自身也可以是对象,稍后详述,并且原型(prototype) 也可以拥有命名属性。但是它在赋值的时候不起作用。如果赋值的时候,实际的对象没有相应名称的属性,一个拥有此名称的属性将会被创建,并且把值赋给它。如果它已经拥有此属性则重新赋值。

读取值

??? 从对象属性上读取值时原型开始起作用。如果一个对象拥有一个属性,与属性访问器( 译者注,指的就是属性访问操作符) 的属性名相同,则返回这个属性的值:

??? /*

??? 给一个命名属性赋值

??? 如果在赋值之前对象没有一个对应名称的属性

??? 在赋值之后我们会获得一个

??? */

??? objectRef.testNumber = 8;

?

??? /* 从属性上读回这个值*/

??? var val = objectRef.testNumber;

?

??? /* 然后,val 现在持有了个值为8 ,就是我们刚才赋给对象命名属性的值。*/

???

??? 但是所有对象都可以有原型,并且原型也是对象,所以接着它们也可以有原型,原型又可以有原型,如此就形成了所谓的原型链。当链条中的对象有一个值为null 的原型时,原型链终止。Object 构造器的默认原型值为null ,所以:

??? var objectRef = new Object(); // 创建一个通用的JavaScript 对象。

?

??? 创建一个对象的原型为Object.prototype ,这个原型自身的属性为null 。所以objectRef 的原型链仅仅包含一个对象:Object.prototype 。然而:

?

/*

一个构造函数,用来创建MyObject1 型的对象。

*/

function MyObject1(formalParameter){

/*

给创建的对象赋一个名为testNumber 的属性

并且将传给构造函数的第一个参数值赋给它

*/

??? this.testNumber = formalParameter;

}

/*

一个构造函数,用来创建MyObject2 型的对象

*/

function MyObject2(formalParameter){

?? /*

??? 给创建的对象一个名为testString 的属性

??? 并且将传给构造函数的第一个参数值赋给它

??? */

??? this.testString = formalParameter;

}

/*

?? 下一步操作,把关联到MyObject2 实例上的默认prototype

?? 替换成MyObject1 的实例,给MyObject1 的构造函数传递一个值8

?? 这样它的testNumber 属性将会被设置为这个值:

*/

MyObject2.prototype = new MyObject1( 8 );

/*

最后,创建MyObject2 的实例,然后将这个对象的引用赋值给变量objRef

??? 给构造函数传递一个字符串作为第一个参数

*/

var objectRef = new MyObject2( "String_Value" );

?

??? 被变量objectRef 引用的MyObject2 实例拥有一个原型链。链中的第一个对象是MyObject1 的实例,他被创建并赋值给MyObject2 构造函数的prorotype 属性。MyObject1 的实例有一个原型,此对象由( 引擎) 实现分配给函数MyObject1prototype 属性。这个对象( 译者注:指分配给MyObject1prototype 属性的对象) 有一个原型,就是默认Object 的原型,对应Object.prototype 所引用的对象。Object.prototype 是一个值为null 的原型,所以原型链到这一点结束。

??? 当一个属性访问器试图从变量objectRef 所引用的对象上读取一个命名属性时,整个原型链都会加入处理过程。在简单的情况下:

??? var val = objectRef.testString;

?

??? objectRef 所引用的MyObject2 的实例有一个名为”testString” 的属性,所以它就是这个属性的值,设置为”String_Value” ,它被赋值给变量val 。然而:

??? var val = objectRef.testNumber;

?

??? 不能从MyObject2 实例自身读取命名属性,因为它没有这个属性,但是变量val 的值被设置为8 而不是undefined ,因为在对象自身上查找对应的属性失败之后,解释器会检查对象的原型。它的原型是MyObject1 的实例,它被创建时有一个属性名为”testNumber” ,并且把值8 赋给了这个属性,所以属性访问器算出的值为8MyObject1MyObject2 都没有定义toString 方法,但是如果一个属性访问器试图从objectRef 上读取toString 属性的值:

??? var val = objectRef.toString;

?

??? 变量val 被赋值为一个函数的引用。这个函数是Object.ptorotypetoString 属性,它被返回是因为检查了objectRef 的原型,当发现objectRef 没有”toString” 属性,而是引用一个对象,所以当发现原型中也缺少此属性时,则继续查找原型的原型。它的原型是Object.prorotype ,确实有一个toString 方法,所以val 就被赋值为一个引用,指向返回的函数对象。

??? 最后:

??? var val = objectRef.madeUpProperty;

?

??? 返回undefined ,因为处理过程检查了整个原型链,发现没有哪个对象有一个名为"madeUpPeoperty" 的属性,它最终获得了Object.prototype 的值,这是一个null 值,然后处理过程返回一个undefined

??? 对命名属性的读取操作会返回第一个找到的值,这个值可以在对象上或者在它的原型链上。给对象的命名属性赋值时,如果对象上没有对应的属性存在,将会在对象自身创建一个属性。

??? 这意味着,如果一个值被分配为objectRef.testNumber=3 ,一个”testNumber” 属性将会在MyObject2 的实例自身上被创建,随后所有读取这个属性的操作将会获得在对象上设置的值。不再需要检查原型链来解析属性访问器,但是MyObject1 的实例,”testNumber” 属性被赋值为8 不会变。objectRef 对象上分配的对应属性遮盖了其原型链上的对应属性。

??? 注意:ECMAScript 为内部Object 类型定义了一个内部[[prototype]] 属性。这个属性不能直接通过脚本访问,但是它是一个引用对象原型链的对象链,同时内部[[prototype]] 属性在属性访问解析中被使用。存在一个公共的prototype 属性与内部[[prototype]] 属性相关联,允许对它进行赋值、定义和操作。这两者之间关系的细节在ECMA 262 ( 第三版) 中有描述,这已经超越了本文的讨论范围。

?

标识符解析,执行环境和作用域链

执行环境

??? 执行环境是ECMSScript 规范使用的一个抽象概念(ECMA 262 第三版) ,用来定义ECMAScript 实现所必须的行为。规范中没有提到关于执行环境应该如何实现的任何事情,但是执行环境有关联的属性,它引用了规范所定义的结构,所以它们可以被设想( 甚至实现) 为拥有属性的对象,虽然不是公共属性。

??? 所有JavaScript 代码都是在一个执行环境中被执行的。全局代码( 内嵌代码,一般作为js 文件或者HTML 页面加载) 在全局执行环境中执行,每次函数调用( 可以是构造器) 都有一个分配的执行环境。用eval 函数执行的代码也有一个独特的执行环境,但是因为eval 从来不会被JavaScript 程序员经常使用,所以这里不讨论它。执行环境的特定的细节可以在ECMA 26210.2( 第三版) 节找到。

??? 当一个JavaScript 函数被调用的时候,它进入一个执行环境,如果其它函数被调用( 或者在相同的函数上递归) ,一个新的执行环境被创建,在函数调用过程中进入此环境。当这个被调用的函数返回之后会返回先前的执行环境。这样,运行中的javascript 代码就形成了一个执行环境栈

??? 当一个执行环境被创建时,很多事情按照规定的顺序发生。首先,在一个函数的执行环境中,一个“活动”对象被创建。活动对象是另一个规范机制。它可以被看做一个对象,因为它最终拥有可访问的命名属性,但是它不是一个普通对象,因为它没有原型(prototype)( 至少不是一个已定义的prototype) ,并且它不能直接被javascript 代码引用。

??? 为调用函数创建执行环境的下一步是创建一个arguments 对象,这是一个类似数组的对象,拥有以整数值为下标的成员,它和调用函数时传递的参数按次序对应。arguments 对象还有lengthcallee 两个属性( 和这里的讨论无关,参见细节描述)活动对象 会被创建一个名为”arguments” 的属性,它会被赋值为一个引用,指向arguments 对象。

??? 然后为执行环境分配一个作用域。作用域由一组对象列表( 或链) 组成。每个函数对象都有一个内部[[scope]] 属性( 稍后我们将涉及更多细节) ,它也由一组对象列表( 或链) 组成。调用函数时分配给执行环境的作用域(scope) 由一个列表组成,这个列表就是被对应函数对象的[[scope]] 属性所引用的列表,并且把活动对象添加到链( 或者列表顶端) 的前端而组成。

??? 然后,使用一个ECMA262 所指的 可变”(Variable) 对象执行“变量初始化”的过程。然而,活动对象被用作可变对象( 注意,很重要:它们是同一个对象 ) 。可变对象的命名参数是为函数的每一个形参创建的,并且,如果调用函数的参数与这些参数对应,这些参数的值会被赋到属性上( 否则,赋值为undefined) 。内部函数定义也被用来创建函数对象,它们也被设置到可变对象的属性上,属性名和定义函数时的函数名对应。变量初始化的最后一步是创建可变对象的命名属性,对应函数中声明的所有局部变量。

??? ? 在可变对象上创建的,与申明的局部变量对应的属性,在变量初始化时被初始为undefined ,局部变量的初始化实际不会发生,直到执行函数体中代码对相应的表达式时才进行计算。

??? 事实上,拥有arguments 属性的“活动对象”,和拥有对应到函数局部变量的命名属性的“可变对象”,是同一个对象,这就允许把arguments 标识符当作函数的局部变量看待。

??? 最后,为使用this 关键字分配一个值。如果分配的值指向一个对象,那么使用this 关键为前缀的属性访问器将会引用这个对象的属性。如果分配( 内部) 的值为null ,那么this 关键字将会指向全局对象。

??? 全局执行环境做了一些细微不同的处理,因为它没有参数,所以它没有必要定义活动对象去引用它们。全局执行环境确实需要一个作用域,并且它的作用域链仅仅由一个对象构成,就是全局对象。全局执行环境也需要经历变量初始化,它的内部函数是所声明的普通顶级函数,它们组成了javascript 代码的绝大部分。全局对象被用作可变对象,这就是为什么声明的全局函数成为了全局对象的属性。全局范围内声明的变量也一样。

??? 全局执行环境也使用一个到全局对象的引用作为this 对象。

作用域链和[[scope]]

??? 调用函数时执行环境中的作用域链,是通过把执行环境中的活动对象/ 可变对象添加到作用域链顶部而构成,作用域链由函数对象的[[scope]] 属性持有,所以,理解[[scope]] 属性是如何定义的非常重要。

??? ECMAScript 中,函数是对象,它们在变量初始化时被创建:函数申明、执行函数表达式或者通过调用Function 构造器。

??? 使用Function 构造器创建的函数对象,总是把[[scope]] 属性指向一个作用域链,其中仅仅包含全局对象。

??? 通过函数声明或者函数表达式创建的函数对象,拥有一个执行环境中的作用域链,被赋值给它们内部的[[scope]] 属性,它们是在这个执行环境中被创建的。

??? 全局函数声明最简单的情况如:

?

??? function exampleFunction(formalParameter){

??? ??? ...?? // function body code

??? }

?

??? 在为全局执行环境进行变量初始化时,对应的函数对象被创建。全局执行环境有一个作用域链,它仅仅由全局对象组成。这样,这个被创建的函数对象被全局对象以”exampleFunction ”为名称引用,并且被分配了一个内部[[scope]] 属性,它引用一个作用域链,其中只包含全局对象。

??? 当一个函数表达式在全局环境中被执行时,分配一个类似的作用域链:

??? var exampleFuncRef = function(){

??? ??? ...?? // function body code

??? }

?

??? 在这种情况下,在为全局执行环境进行变量初始化的过程中,全局对象的一个命名属性被创建,并且它的一个引用被赋值给全局对象的一个命名属性,但是函数对象没有被创建,直到对赋值表达式求值为止。但是在全局环境中创建函数对象的操作还是发生了,所以创建的函数对象的[[scope]] 属性仍然指向了只包含一个全局对象的作用域链。

??? 内部函数声明和以函数对象为结果的表达式在函数内部的执行环境中被创建,所以它们拥有更精细的作用域链。考虑以下代码,定义了一个有内部函数声明的函数,然后执行外部函数:

function exampleOuterFunction(formalParameter){

??? function exampleInnerFuncitonDec(){

??????? ... // inner function body

??? }

??? ...? // the rest of the outer function body.

}

exampleOuterFunction( 5 );

?

??? 对应着外部函数声明的函数对象在全局执行环境的变量初始化过程中被创建,所以它的[[scope]] 属性包含只有一个元素的作用域链,其中只有一个全局对象。

??? 当外部代码调用exampleOuterFunction 时,会为这个函数调用创建一个新的执行环境,同时还有和它一起的活动对象/ 可变对象。新执行环境中的作用域构成变成了:新的活动对象加上外部函数对象[[scope]] 属性( 仅包含全剧对象) 所引用的作用域链。为新执行环境进行变量初始化导致创建了一个函数对象,它对应内部函数定义,并且这个函数对象的[[scope]] 属性被分配为它被创建时执行环境中的作用域值。作用域链包含了活动对象,紧接着是全局对象。

1 楼 mr.lili 2011-12-29  
MM                     
  相关解决方案