标签:c style class blog code java
闭包和作用域链是JavaScript中比较重要的概念,首先,看看几段简单的代码。
代码1:
1 var name = "stephenchan"; 2 var age = 23; 3 function myFunc() { 4 alert(name); 5 var name = "endlesscode"; 6 alert(name); 7 alert(age); 8 alert(weight); 9 } 10 myFunc(); 11 myFunc();
上述代码1中,两次调用myFunc()的输出是一致的。可能你会认为输出是:
但是结果却是:
代码2:
1 var i = 10; 2 function myFunc() { 3 var i = 20; 4 function innerFunc() { 5 alert(i); 6 } 7 return innerFunc; 8 } 9 var func = myFunc(); 10 func();
上面的代码2会输出20,但为什么不输出10或者是输出undefined?
代码3:
1 var name = "stephenchan"; 2 function callMePlz() { 3 alert(name); 4 } 5 6 function myFunc() { 7 var name = "endlesscode"; 8 callMePlz(); 9 } 10 11 myFunc();
上面的代码3输出的会是endlesscode、stephenchan还是undefined?
代码4:
1 function callMePlz() { 2 var name = "stephenchan"; 3 var intro = function() { 4 alert("I am " + name); 5 } 6 return intro; 7 } 8 9 function showMe(arg) { 10 var name = arg; 11 var func = callMePlz(); 12 func(); 13 } 14 showMe("endlesscode");
上面的代码4与代码3不同的是,从callMePlz返回的函数引用,然后再执行函数。
代码5:
1 var name = "stephenchan"; 2 function callMePlz() { 3 var intro = function() { 4 alert("I am " + name); 5 } 6 return intro; 7 } 8 9 function showMe(arg) { 10 var name = arg; 11 var func = callMePlz(); 12 func(); 13 } 14 showMe("endlesscode");
上面的代码5与代码4不同的是原来在callMePlz函数中的变量name在全局环境中声明了,但输出的结果是:
先不对上面的代码进行说明,讲述一下闭包和作用域链的概念。
闭包(closure)是什么?闭包与函数有着紧密的关系。“在JavaScript中,一个函数只是一段静态的代码、脚本文件,因此函数是一个代码书写时,以及编译期的、静态的概念;而闭包则是函数的代码在运行过程中的一个动态环境,是一个运行期的、动态的概念”。这是《JavaScript语言精髓和编程实践》中对函数和闭包的描述,实际上我们常说的闭包倒是可以表现为如上面代码2中的innerFunc一样,在myFunc的函数执行后返回的是一个在其内部定义的、外部可调用的函数引用,这个函数语言的特性在C和C++是没有的。为什么在myFunc结束之后innerFunc还能正常访问到myFunc里面的数据呢?这就涉及到函数执行环境与闭包的相关概念,闭包中所保留着函数运行的实例,环境以及作用域链等等,并在myFunc调用之后没有将函数实例直接丢弃,因此在调用innerFunc的时候能够引用到myFunc中声明的i。
作用域链(scope chain)是什么?顾名思义,就是由作用域组成的链,是一个类似链状的数据结构。作用域就是对上下文环境的数据描述。闭包和作用域链是紧密关系的,函数实例执行时的闭包是构成作用域链的基本元素。JavaScript代码在执行前会进行语法分析,在语法分析阶段,会记录全局环境中的变量声明和函数定义,构造函数的调用对象(Call Oject、Activation Object、Activate Object、活动对象,不同称呼罢了)和在全局环境下的作用域链。
图1是《JavaScript语言精髓和编程实践》一书中对闭包相关元素的内部数据结构的描述。我们主要关注其中的ScriptObject,ScriptObject是对调用对象的一种描述。ScriptObject在语法分析阶段就已经构造好了,其中的varDecls是保存着函数中的变量声明,funcDecls保存着内部的函数声明。
在语法分析阶段,varDecls保存在函数中用var进行显示声明的局部变量,并且置默认值为undefined,这里就是在代码1中"alert(name)"输出为undefined的原因,由于myFunc在语法分析阶段就已经保留了标记符name在varDecls,在赋值语句"var name=‘endlesscode‘"执行之前name的值都是undefined,因此在"alert(name)"的时候就显示为undefined了。
而函数定义在语法分析阶段工作就稍微有点不同。在语法分析阶段,发现有函数定义的时候,除了记录函数的声明外,还会创建一个函数对象,并将当前的作用域链赋值给此函数对象的[[scope]]属性(这个属性是JavaScript引擎内部维护的,但是Firefox却是可以通过私有属性__parent__来访问它),这里要注意的是在语法分析阶段将作用域链赋值给[[scope]]属性,而不是在执行阶段。如果是在全局环境下,但当前的作用域链为只有一个元素,就是全局的调用对象(Windows Object, Global Object,不同称呼罢了)。这就是为什么在《JavaScript权威指南》中提到“JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。”
下面以一段代码的大概处理流程来进行说明:
1 var outerVar1 = "var in global code"; 2 function outerFunc(arg1, arg2) { 3 var innerVar1 = "var in function code"; 4 function innerFunc() { return outerVar1 + "-" + innerVar1 + "-" + (arg1 + arg2); } 5 return innerFunc(); 6 } 7 var outerVar2 = outerFunc(10, 20);
执行处理过程大致如下:(精华1)
我们再拿前面代码4的例子对作用域链进行简单的分析:(精华2)
代码4:
1 function callMePlz() { 2 var name = "stephenchan"; 3 var intro = function() { 4 alert("I am " + name); 5 } 6 return intro; 7 } 8 9 function showMe(arg) { 10 var name = arg; 11 var func = callMePlz(); 12 func(); 13 } 14 showMe("endlesscode");
假如全局的语法分析已经结束,已经开始执行"showMe(‘endlesscode‘)"了。在进入执行showMe的执行上下文时,我们可以看到"showMe"函数中[[scope]]属性记录中的作用域链为:
创建showMe的调用对象后,则新的作用域链为showMe的调用对象和全局调用对象组成:
也就是"showMe调用对象->Global调用对象"这样的链式关系。接着,忽略showMe函数中语法分析等过程,到执行callMePlz()函数时,callMePlz函数的[[scope]]属性为
callMePlz函数的[[scope]]属性指示的作用域链也只包括了全局调用对象,因为callMePlz也是在全局环境下定义的。创建callMePlz的调用对象后,则新的作用域链为callMePlz的调用对象和全局调用对象组成:
也就是"callMePlz调用对象->Global调用对象"这样的链式关系。可以看到,在callMePlz的作用域链中,并没有包括showMe的调用对象。当callMePlz进行语法分析的时候,找到intro函数时,将intro函数的[[scope]]属性赋值为(上例分析中第2条中的第2条):
callMePlz返回的是intro函数对象的引用,当在showMe函数中执行intro函数时,创建intro函数的调用对象,此时intro函数的作用域链为:
由于在intro函数中没有声明变量和函数,所以看到的也只是一些内置的属性成员,此时intro函数的作用域链则为:"intro调用对象->callMePlz调用对象->Global调用对象",因此,当执行intro函数时,则以"intro调用对象->callMePlz调用对象->Global调用对象"的顺序去搜索"name"变量,发现在callMePlz调用对象上找到了,因此在代码4中输出的是"I am stephenchan"而不是"I am endlesscode"。
以上面的分析方法来分析上述的其他代码,就容易理解其输出了。
另外,函数闭包内的标识符系统有优先顺序,其优先级从高到低为:this > 局部变量(varDecls) > 函数形式参数名(argsName) > arguments关键字 > 函数名(funcNames)。
1 //输出‘hi‘,说明argsName > funcNames。 2 function foo(foo) { 3 alert(foo); 4 } 5 foo(‘hi‘); 6 7 //输出100的类型"number",说明argsName > arguments。 8 function foo2(arguments) { 9 alert(typeof arguments); 10 } 11 foo2(100); 12 13 //输出arguments的类型为‘object‘,说明arguments > funcNames。 14 function arguments() { 15 alert(typeof arguments); 16 } 17 arguments(); 18 19 //输出‘test‘,形式参数名与未赋值局部变量重复时,取形式参数值。 20 function foo3(str) { 21 var str; 22 alert(str); 23 } 24 foo3(‘test‘); 25 26 //输出‘member‘,形式参数与有值的局部变量重复时,取局部变量。 27 function foo4(str) { 28 var str = ‘member‘; 29 alert(str); 30 } 31 foo4(‘test‘);
原文链接:http://blog.endlesscode.com/2010/01/20/javascript-closure-scope-chain/
推荐阅读:http://www.cnblogs.com/lhb25/archive/2011/09/06/javascript-scope-chain.html
(好文推荐)一篇文章看懂JavaScript作用域链,布布扣,bubuko.com
标签:c style class blog code java
原文地址:http://www.cnblogs.com/ttcc/p/3764129.html