(好文推荐)一篇文章看懂JavaScript作用域链
闭包和作用域链是JavaScript中比较重要的概念,首先,看看几段简单的代码。
代码1:
var name = "stephenchan";
var age = 23;
function myFunc() {
alert(name);
var name = "endlesscode";
alert(name);
alert(age);
alert(weight);
}
myFunc();
myFunc();
上述代码1中,两次调用myFunc()的输出是一致的。可能你会认为输出是:
endlesscode
23
[Reference Error]
但是结果却是:
undefined
endlesscode
23
[Reference Error]
代码2:
var i = 10;
function myFunc() {
var i = 20;
function innerFunc() {
alert(i);
}
return innerFunc;
}
var func = myFunc();
func();
上面的代码2会输出20,但为什么不输出10或者是输出undefined?
代码3:
var name = "stephenchan";
function callMePlz() {
alert(name);
} function myFunc() {
var name = "endlesscode";
callMePlz();
} myFunc();
上面的代码3输出的会是endlesscode、stephenchan还是undefined?
stephenchan
代码4:
function callMePlz() {
var name = "stephenchan";
var intro = function() {
alert("I am " + name);
}
return intro;
} function showMe(arg) {
var name = arg;
var func = callMePlz();
func();
}
showMe("endlesscode");
上面的代码4与代码3不同的是,从callMePlz返回的函数引用,然后再执行函数。
I am stephenchan
代码5:
var name = "stephenchan";
function callMePlz() {
var intro = function() {
alert("I am " + name);
}
return intro;
} function showMe(arg) {
var name = arg;
var func = callMePlz();
func();
}
showMe("endlesscode");
上面的代码5与代码4不同的是原来在callMePlz函数中的变量name在全局环境中声明了,但输出的结果是:
I am stephenchan
先不对上面的代码进行说明,讲述一下闭包和作用域链的概念。
闭包(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中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。”
下面以一段代码的大概处理流程来进行说明:
var outerVar1 = "var in global code";
function outerFunc(arg1, arg2) {
var innerVar1 = "var in function code";
function innerFunc() { return outerVar1 + "-" + innerVar1 + "-" + (arg1 + arg2); }
return innerFunc();
}
var outerVar2 = outerFunc(10, 20);
执行处理过程大致如下:(精华1)
- 引擎启动,初始化Global Object,即window对象,全局的调用对象,建立作用域链,假设为scope_1,作用域链中只包含全局的上下文环境,即Global Object。
- 语法分析阶段,扫描JavaScript代码,获取代码中变量和函数定义,其扫描过程如下:全局环境下语法分析结束,执行outerVar1赋值语句,赋值为"var in global code"。
- 发现变量outerVar1,在Global Object的varDecls中添加outerVar1属性,值为undefined。
- 发现函数outerFunc的定义,在Global Object的funcDecls中添加outerFunc,并创建函数对象(这里应该创建的是函数的原型对象),将scope_1传递给outerFunc的函数对象,即outerFunc内部的[[scope]]属性。另外注意,创建过程并不会对函数体中的JavaScript代码做特殊处理,可以理解为只是将函数JavaScript代码保存中函数对象的内部属性上,在函数执行时再做进一步处理。也就是说,这一步大概处理的就是记录函数定义,赋值[[scope]]属性记录当前定义的作用域,而没有进一步对outerFunc里面的代码进行进一步的语法分析。
- 发现变量outerVar2,在Global Object中的varDecls中添加属性,值为undefined。
- 全局环境下语法分析结束,执行outerVar1赋值语句,赋值为"var in global code"。
- 执行outerFunc,获取返回值。将outerFunc的返回结果赋值给outerVar2。
- 创建调用对象,假设为act_obj_1。同时将act_obj_1链接起outerFunc的[[scope]]属性,构成一个新的作用域链,假设为scope_2,scope_2中的第一个对象为act_obj_1,act_obj_1指向scope_1。
- 处理参数列表,在act_obj_1中设置属性arg1、arg2,值分别为10和20。创建arguments对象并进行设置,将arguments设置为act_obj_1的属性。
- 对outerFunc函数体进行语法分析,注意这里在全局语法分析的时候并没有对outerFunc函数体进行语法分析:
- 发现变量innerVar1,在act_obj_1中的varDecls添加innerVar1属性,值为undefined。
- < 发现函数innerFunc的定义,使用这个定义创建函数对象,并将scope_2传递给innerFunc,作为innerFunc的[[scope]]属性,在act_obj_1的funcDecls添加innerFunc。
- outerFunc函数语法分析结束,执行innerVar1赋值语句,赋值为"var in function code"。
- 执行innerFunc,执行函数的处理流程是一致的:
- 创建调用对象,假设为act_obj_2;同时将avt_obj_2链接起innerFunc[[scope]]属性,构成一个新的作用域链,假设为scope_3,scope_3中的第一个对象为act_obj_2,act_obj_2指向scope_2。
- 处理参数列表,因为innerFunc没有参数,所以只需要创建arguments对象并设置为act_obj_2的属性。
- 对innerFunc进行语法分析,识别变量和函数,但没有发现变量定义和函数声明。
- 执行innerFunc函数。对任何一个变量引用,从scope_3开始进行链式搜索,以scope_3->scope_2->scope_1的顺序进行搜索,结果发现outerVar1在scope_1中的Global Object发现;innerVar1、arg1、arg2在scope_2中的act_obj_1中找到。
- 检查scope_3和act_obj_2的引用,发现没有其他引用,则丢弃,让引擎进行垃圾回收。
- 返回innerFunc执行的值。
- 检查没有对act_obj_1和scope_2的引用,则丢弃act_obj_1和scope_2。
- 返回结果。
- 将outerFunc的返回结果赋值给outerVar2。
我们再拿前面代码4的例子对作用域链进行简单的分析:(精华2)
代码4:
function callMePlz() {
var name = "stephenchan";
var intro = function() {
alert("I am " + name);
}
return intro;
} function showMe(arg) {
var name = arg;
var func = callMePlz();
func();
}
showMe("endlesscode");
假如全局的语法分析已经结束,已经开始执行"showMe('endlesscode')"了。在进入执行showMe的执行上下文时,我们可以看到"showMe"函数中[[scope]]属性记录中的作用域链为:
2 {//Global Object,因为在全局没有var声明的变量,因此就没有列出来
3 document : ...,
4 location : ...
5 }
6 ]
创建showMe的调用对象后,则新的作用域链为showMe的调用对象和全局调用对象组成:
2 {//showme_activation_obj
3 name : undefined,
4 func : undefined,
5 arg : "endlesscode",
6 arguments : ...
7 },
8 {//Global Object,因为在全局没有var声明的变量,因此就没有列出来
9 document : ...,
10 location : ...
11 }
12 ]
也就是"showMe调用对象->Global调用对象"这样的链式关系。接着,忽略showMe函数中语法分析等过程,到执行callMePlz()函数时,callMePlz函数的[[scope]]属性为
2 {//Global Object,因为在全局没有var声明的变量,因此就没有列出来
3 document : ...,
4 location : ...
5 }
6 ]
callMePlz函数的[[scope]]属性指示的作用域链也只包括了全局调用对象,因为callMePlz也是在全局环境下定义的。创建callMePlz的调用对象后,则新的作用域链为callMePlz的调用对象和全局调用对象组成:
2 {//callmeplz_activation_obj
3 name : undefined,
4 intro : undefined,
5 arguments : ...
6 },
7 {//Global Object,因为在全局没有var声明的变量,因此就没有列出来
8 document : ...,
9 location : ...
10 }
11 ]
也就是"callMePlz调用对象->Global调用对象"这样的链式关系。可以看到,在callMePlz的作用域链中,并没有包括showMe的调用对象。当callMePlz进行语法分析的时候,找到intro函数时,将intro函数的[[scope]]属性赋值为(上例分析中第2条中的第2条):
2 {//callmeplz_activation_obj
3 name : undefined, //这里还是undefined,当语法分析结束,执行callMePlz时,这里就赋值为"stephenchan"
4 intro : undefined,
5 arguments : ...
6 },
7 {//Global Object,因为在全局没有var声明的变量,因此就没有列出来
8 document : ...,
9 location : ...
10 }
11 ]
callMePlz返回的是intro函数对象的引用,当在showMe函数中执行intro函数时,创建intro函数的调用对象,此时intro函数的作用域链为:
2 {//intro_activation_obj
3 arguments : ...
4 },
5 {//callmeplz_activation_obj
6 name : undefined, //这里还是undefined,当语法分析结束,执行callMePlz时,这里就赋值为"stephenchan"
7 intro : undefined,
8 arguments : ...
9 },
10 {//Global Object,因为在全局没有var声明的变量,因此就没有列出来
11 document : ...,
12 location : ...
13 }
14 ]
由于在intro函数中没有声明变量和函数,所以看到的也只是一些内置的属性成员,此时intro函数的作用域链则为:"intro调用对象->callMePlz调用对象->Global调用对象",因此,当执行intro函数时,则以"intro调用对象->callMePlz调用对象->Global调用对象"的顺序去搜索"name"变量,发现在callMePlz调用对象上找到了,因此在代码4中输出的是"I am stephenchan"而不是"I am endlesscode"。
以上面的分析方法来分析上述的其他代码,就容易理解其输出了。
另外,函数闭包内的标识符系统有优先顺序,其优先级从高到低为:this > 局部变量(varDecls) > 函数形式参数名(argsName) > arguments关键字 > 函数名(funcNames)。
//输出'hi',说明argsName > funcNames。
function foo(foo) {
alert(foo);
}
foo('hi'); //输出100的类型"number",说明argsName > arguments。
function foo2(arguments) {
alert(typeof arguments);
}
foo2(100); //输出arguments的类型为'object‘,说明arguments > funcNames。
function arguments() {
alert(typeof arguments);
}
arguments(); //输出'test',形式参数名与未赋值局部变量重复时,取形式参数值。
function foo3(str) {
var str;
alert(str);
}
foo3('test'); //输出'member',形式参数与有值的局部变量重复时,取局部变量。
function foo4(str) {
var str = 'member';
alert(str);
}
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作用域链的更多相关文章
- 一篇文章看懂JS闭包,都要2020年了,你怎么能还不懂闭包?
壹 ❀ 引 我觉得每一位JavaScript工作者都无法避免与闭包打交道,就算在实际开发中不使用但面试中被问及也是常态了.就我而言对于闭包的理解仅止步于一些概念,看到相关代码我知道这是个闭包,但闭包 ...
- 一篇文章看懂angularjs component组件
壹 ❀ 引 我在 angularjs 一篇文章看懂自定义指令directive 一文中详细介绍了directive基本用法与完整属性介绍.directive是个很神奇的存在,你可以不设置templa ...
- angularjs 一篇文章看懂自定义指令directive
壹 ❀ 引 在angularjs开发中,指令的使用是无处无在的,我们习惯使用指令来拓展HTML:那么如何理解指令呢,你可以把它理解成在DOM元素上运行的函数,它可以帮助我们拓展DOM元素的功能.比如 ...
- 一篇文章看懂spark 1.3+各版本特性
Spark 1.6.x的新特性Spark-1.6是Spark-2.0之前的最后一个版本.主要是三个大方面的改进:性能提升,新的 Dataset API 和数据科学功能的扩展.这是社区开发非常重要的一个 ...
- 一篇文章看懂JS执行上下文
壹 ❀ 引 我们都知道,JS代码的执行顺序总是与代码先后顺序有所差异,当先抛开异步问题你会发现就算是同步代码,它的执行也与你的预期不一致,比如: function f1() { console.lo ...
- 一篇文章看懂Facebook和新浪微博的智能FEED
本文来自网易云社区 作者:孙镍波 众所周知,新浪微博的首页动态流不像微信朋友圈是按照时间顺序排列的,而是按照一种所谓的"智能排序"的方式.这种违背了用户习惯的排序方式一直被用户骂, ...
- rabbitMQ教程(二)一篇文章看懂rabbitMQ
一.rabbitMQ是什么: RabbitMQ,遵循AMQP协议,由内在高并发的erlanng语言开发,用在实时的对可靠性要求比较高的消息传递上. 学过websocket的来理解rabbitMQ应该是 ...
- 一篇文章看懂Java并发和线程安全
一.前言 长久以来,一直想剖析一下Java线程安全的本质,但是苦于有些微观的点想不明白,便搁置了下来,前段时间慢慢想明白了,便把所有的点串联起来,趁着思路清晰,整理成这样一篇文章. 二.导读 1.为什 ...
- rabbitMQ教程(三)一篇文章看懂rabbitMQ
一.rabbitMQ是什么: RabbitMQ,遵循AMQP协议,由内在高并发的erlanng语言开发,用在实时的对可靠性要求比较高的消息传递上. 学过websocket的来理解rabbitMQ应该是 ...
随机推荐
- 初识zookeeper(一)之zookeeper的安装及配置
1.简要介绍 zookeeper是一个分布式的应用程序协调服务,是Hadoop和Hbase的重要组件,是一个树型的目录服务,支持变更推送.除此还可以用作dubbo服务的注册中心. 2.安装 2.1 下 ...
- 用VB实现点名程序
用vb实现点名程序主要是随机变量的产生和数据的读取和存储以及计时器程序的设计,读取的文件命名为data.txt,书写格式为第一行为总人数下面的每行为一个人名,在应用时最好把data文件和程序文件放在一 ...
- TEZ安装试用
下载地址:http://pan.baidu.com/s/1ZNpyI 第一次使用maven编译 tez的时候到tez ui部分报错,google后发现有人遇到类似问题是因为maven版本的问题, 当时 ...
- [转]Ionic Datepicker
本文转自:https://market.ionic.io/plugins/ionicdatepicker ##Introduction: This is an ionic-datepicker bow ...
- JavaScript“尽快失败”的原则
我第一次听说编码原则中有"尽快失败"这一条时,觉得很奇怪,为什么代码要失败?应该成功才对呀.但事实上,当代码在遇到错误的时候应该尽快的终止.为了检测各种状态,我们需要频繁的创建if ...
- 用pygame学习初级python(一) 15.4.19
最近有计划要学一下python,主要是要用flask.django一些框架进行后端的学习工作,但是在web应用之前希望进行一些基础的项目进行一些语法的练习,熟悉一下写法, 这个时候我就想先做几个小游戏 ...
- 求次短路 codevs 1269 匈牙利游戏
codevs 1269 匈牙利游戏 2012年CCC加拿大高中生信息学奥赛 时间限制: 1 s 空间限制: 128000 KB 题目等级 : 钻石 Diamond 题目描述 Descriptio ...
- HDU 5102 The K-th Distance
题意:给你n-1条边,然后没两个节点的距离按照递增的顺序,求出前k项的和. 官方题解: 把所有边(u,v) 以及(v,u)放入一个队列,队列每弹出一个元素(u,v),对于所有与u相邻的点w,如果w!= ...
- 3xian之所在(转)
最后一天,漫天飘起了雪花,假装欢送我离去. 这次WF之战不太顺利,早期的C题大概花了1秒钟构思,然而由于输出格式多了一个空格直到两个半小时才逃脱Wrong Answer的纠缠.还好lynncui在期间 ...
- 第16章 Windows线程栈
16.1 线程栈及工作原理 (1)线程栈简介 ①系统在创建线程时,会为线程预订一块地址空间(即每个线程私有的栈空间),并调拨一些物理存储器.默认情况下,预订1MB的地址空间并调拨两个页面的存储器. ② ...