声明

本系列文章内容全部梳理自以下几个来源:

作为一个前端小白,入门跟着这几个来源学习,感谢作者的分享,在其基础上,通过自己的理解,梳理出的知识点,或许有遗漏,或许有些理解是错误的,如有发现,欢迎指点下。

PS:梳理的内容以《JavaScript权威指南》这本书中的内容为主,因此接下去跟 JavaScript 语法相关的系列文章基本只介绍 ES5 标准规范的内容、ES6 等这系列梳理完再单独来讲讲。

正文-作用域链

作用域一节中,我们介绍了变量的作用域分两种:全局和函数内,且函数内部可以访问外部函数和全局的变量。

我们也介绍了,每个函数被调用时,会创建一个函数执行上下文 EC,EC 里有个变量对象 VO 属性,函数内部操作的局部变量就是来源于 VO,但 VO 只保存当前上下文的变量,那么函数内部又是如何可以访问到外部函数的变量以及全局变量的呢?

本篇就是来讲讲作用域链的原理,理清楚这些理所当然的基础知识的底层原理。

先来看个例子,再看些理论,最后结合理论再回过头分析例子。

var num = 0;
var sum = -1;
function a() {
console.log(num); //1. 输出什么
var b = function () {
console.log(num++);
}
var num = 1;
b(); //2. 输出什么
console.log(sum); //3. 输出什么
return b;
} var c = function(num) {
var d = a();
d(); //4. 输出什么
} c(10);

当执行了最后一行代码时,会有四次输出,每次都会输出什么,可以先想想,然后再继续看下去,对比下你的答案是否正确。

理论

作用域链的原理还是跟执行上下文 EC 有关,执行上下文 EC 有个作用域链属性(Scope chain),作用域链是个链表结构,链表中每个节点是一个 VO,在函数内部嵌套定义新函数就会多产生一个节点,节点越多,函数嵌套定义越深。

由于作用域链本质上类似于 VO,也是执行上下文的一个属性,那么,它的创建时机自然跟 EC 是一样的,即:全局代码执行时的解析阶段,或者函数代码执行时的解析阶段。

每调用一次函数执行函数体时,js 解释器会经过两个阶段:解析阶段和执行阶段;

调用函数进入解析阶段时主要负责下面的工作:

  1. 创建函数上下文
  2. 创建变量对象
  3. 创建作用域链

创建变量对象的过程在作用域一节中讲过了,主要就是解析函数体中的声明语句,创建一个活动对象 AO,并将函数的形参列表、局部变量、arguments、this、函数对象自身引用添加为活动对象 AO 的属性,以便函数体代码对这些变量的使用。

而创建作用域链的过程,主要做了两件事:

  1. 将当前函数执行上下文的 VO 放到链表头部
  2. 将函数的内部属性 [[Scope]] 存储的 VO 链表拼接到 VO 后面

ps:[[]] 表示 js 解释器为对象创建的内部属性,我们访问不了,也操作不了。

两个步骤创建了当前函数的作用域链,而当函数体的代码操作变量时,优先到作用域链的表头指向的 VO 寻找,找不到时,才到作用域链的每个节点的 VO 中寻找。

那么,函数的内部属性 [[Scope]] 存储的 VO 链表是哪里赋值的?

这部分工作也是在解析阶段进行的,只不过是外层函数被调用时的解析阶段。解析阶段会去解析当前上下文的代码,如果碰到是变量声明语句,那么将该变量添加到上下文的 VO 对象中,如果碰到的是函数声明语句,那么会将当前上下文的作用域链对象引用赋值给函数的内部属性 [[Scope]]。但如果碰到是函数表达式,那 [[Scope]] 的赋值操作需要等到执行阶段。

所以,函数的内部属性 [[Scope]] 存储着外层函数的作用域链,那么当每次调用函数时,创建函数执行上下文的作用域链属性时,直接拼接外层函数的作用域链和当前函数的 VO,就可以达到以函数内部变量优先,依照嵌套层次寻找外层函数变量的规则。

这也是为什么,明明函数的作用域链是当函数调用时才创建,但却依赖于函数定义的位置的原因。因为函数调用时,创建的只是当前函数执行上下文的 VO。而函数即使没被调用,只要它的外层函数被调用,那么外层函数创建执行上下文的阶段就会顺便将其作用域链赋值给在它内部定义的函数。

分析

var num = 0;
var sum = -1;
function a() {
console.log(num); //1. 输出:undefined
var b = function () {
console.log(num++);
}
var num = 1;
b(); //2. 输出:1
console.log(sum); //3.输出:-1
return b;
} var c = function(num) {
var d = a();
d(); //4. 输出:2
} c(10);

1.当第一次执行全局代码时,首先创建全局执行上下文EC

所以,当进入执行阶段,开始执行全局代码时,全局变量已经全部添加到全局 EC 的 VO 里的,这也就是变量的提前声明行为,而且对于全局 EC 来说,它的作用域链就是它的 VO,同时,因为解析过程中遇到了函数声明语句,所以在解析阶段就创建了函数 a 对象(a:<function> 表示 a 是一个函数对象),也为函数 a 的内部属性 [[Scope]] 赋值了全局 EC 的作用域对象。

2.全局代码执行到 var c = function(num) 语句时

相应的全局变量在执行阶段进行了赋值操作,那么,赋值操作实际操作的变量就是对全局 EC 的 VO 里的相对应变量的操作。

3.当全局代码执行到 c(10),调用了函数 c 时

也就是说,在 c 函数内部代码执行之前,就为 c 函数的执行创建了 c 函数执行上下文 EC,这个过程中,会将形参变量,函数体声明的变量都添加到 AO 中(在函数执行上下文中,VO 的具体表现为 AO),同时创建 arguments 对象,确定函数内 this 的指向,由于这里的普通函数调用,所以 this 为全局对象。

最后,会创建作用域链,赋值逻辑用伪代码表示:

Scope chain = c函数EC.VO -> c函数内部属性[[Scope]]

           = c函数EC.VO -> 全局EC.VO

图中用数组形式来表示作用域链,实际数据结构并非数组,所以,对于函数 c 内部代码来说,变量的来源依照优先级在作用域链中寻找。

4.当函数 c 内部执行到 var d = a(); 调用了 a 函数时

同样,调用 a 函数时,也会为函数 a 的执行创建一个函数执行上下文,a 函数跟 c 函数一样定义在全局代码中,所以在全局 EC 的创建过程中,已经为 a 函数的内部属性 [[Scope]] 赋值了全局 EC.VO,所以 a 函数 EC 的作用域链同样是:a函数EC.VO -> 全局EC.VO。

也就是作用域链跟函数在哪被调用无关,只与函数被定义的地方有关。

5.执行 a 函数内部代码

接下去开始执行 a 函数内部代码,所以第一行执行 console.log(num) 时,需要访问到 num 变量,去作用域链中依次寻找,首先在 a函数EC.VO 中找到 num:undefined,所以直接使用这个变量,输出就是 undefined。

6.执行 var b = function()

接下去执行了 var b = function (),创建了一个函数对象赋值给 b,同时对 b 函数的内部属性 [[Scope]] 赋值为当前执行上下文的作用域链,所以 b 函数的内部属性 [[Scope]]值为:a函数EC.VO -> 全局EC.VO

7.接下去执行到 b(),调用了b函数,所以此时

同样,也为 b 函数的执行创建了函数执行上下文,而作用域链的取值为当前上下文的 VO 拼接上当前函数的内部属性 [[Scope]] 值,这个值在第 6 步中计算出来。所以,最终 b 函数 EC 的作用域:

b函数EC.VO -> a函数EC.VO -> 全局EC.VO

8.接下去开始执行函数b的内部代码:console.log(num++);

由于使用到 num 变量,开始从作用域链中寻找,首先在 b函数EC.VO 中寻找,没找到;接着到下个作用域节点 a函数EC.VO 中寻找,发现存在 num 这个变量,所以 b 函数内使用的 num 变量是来自于 a 函数内部,而这个变量的取值在上述介绍的第 7 步时已经被赋值为 1 了,所以这里输出1。

同时,它还对 num 进行累加1操作,所以当这行代码执行结束,a 函数 EC.VO 中的 num 变量已经被赋值为 2 了。

9.b 函数执行结束,将 b 函数 EC 移出 ECS 栈,继续执行栈顶a函数的代码:console.log(sum);

所以这里需要使用 sum 变量,同样去作用域链中寻找,首先在 a函数EC.VO 中并没有找到,继续去 全局EC.VO 中寻找,发现 sum 变量取值为 -1,所以这里输出-1.

10.a 函数也执行结束,将 a 函数 EC 移出 ECS 栈,继续执行 c 函数内的代码:d()

由于 a 函数将函数 b 作为返回值,所以 d() 实际上是调用的 b 函数。此时:

这里又为 d 函数创建了执行上下文,所以到执行阶段执行代码:console.log(num++); 用到的 num 变量沿着作用域链寻找,最后发现是在 a函数EC.VO 中找到,且此时 num 的值为第 8 步结束后的值 2,这里就输出 2.

到这里你可能会疑惑,此时 ECS 栈内,a函数EC 不是被移出掉了吗,为何 d 函数创建 EC 的作用域链中还包括了 a函数EC

这里就涉及到闭包的概念了,留待下节闭包讲解。

总结

如果要从原理角度理解:

  • 变量的作用域机制依赖于执行上下文,全局代码对应全局执行上下文,函数代码对应函数执行上下文
  • 每调用一次函数,会创建一次函数执行上下文,这过程中,会解析函数代码,创建活动对象 AO,将函数内声明的变量、形参、arguments、this、函数自身引用都添加到AO中
  • 函数内对各变量的操作实际上是对上个步骤添加到 AO 对象内的这些属性的操作
  • 创建执行上下文阶段中,还会创建上下文的另一个属性:作用域链。对于函数执行上下文,其值为当前上下文的 VO 拼接上当前函数的内部属性 [[Scope]],对于全局执行上下文,其值为上下文的 VO。
  • 函数内部属性 [[Scope]] 存储着它外层函数的作用域链,是在外层函数创建函数对象时,从外层函数的执行上下文的作用域链复制过来的值。
  • 总之,JavaScript 中的变量之所以可以在定义后被使用,是因为定义的这些变量都被添加到当前执行上下文 EC 的变量对象 VO 中了,而之所以有全局和函数内两种作用域,是因为当前执行上下文 EC 的作用域链属性的支持。也可以说一切都依赖于执行上下文机制。

那么,如果想通俗的理解:

  • 函数内操作的变量,如果在其内部没定义,那么在其外层函数内寻找,如果还没有找到,继续往外层的外层函数内寻找,直到外层是全局对象为止。
  • 这里的外层函数,指的是针对于函数声明位置的外层函数,而不是函数调用位置的外层函数。作用域链只与函数声明的位置有关系。

大家好,我是 dasu,欢迎关注我的公众号(dasuAndroidTv),公众号中有我的联系方式,欢迎有事没事来唠嗑一下,如果你觉得本篇内容有帮助到你,可以转载但记得要关注,要标明原文哦,谢谢支持~

前端入门18-JavaScript进阶之作用域链的更多相关文章

  1. javascript的关键所在---作用域链

    javascript的关键所在---作用域链 javascript里的作用域是理解javascript语言的关键所在,正确使用作用域原理才能写出高效的javascript代码,很多javascript ...

  2. JavaScript深入之作用域链

    前言 在 <javascript深入之执行上下文栈> 中讲到,当javascript代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution ...

  3. javascript闭包和作用域链

    最近在学习前端知识,看到javascript闭包这里总是云里雾里.于是翻阅了好多资料记录下来本人对闭包的理解. 首先,什么是闭包?看了各位大牛的定义和描述各式各样,我个人认为最容易一种说法: 外部函数 ...

  4. javascript笔记:javascript的关键所在---作用域链

    javascript里的作用域是理解javascript语言的关键所在,正确使用作用域原理才能写出高效的javascript代码,很多javascript技巧也是围绕作用域进行的,今天我要总结一下关于 ...

  5. [译]JavaScript:函数的作用域链

    原文:http://blogs.msdn.com/b/jscript/archive/2007/07/26/scope-chain-of-jscript-functions.aspx 在JavaScr ...

  6. 认识javascript范围和作用域链

    范围 作用域就是变量和函数的可訪问范围.控制着变量和函数的可见性与生命周期,在JavaScript中变量的作用域有全局作用域和局部作用域. 全局和局部作用域以下用一张图来解释: 单纯的JavaScri ...

  7. javascript深入浅出图解作用域链和闭包

    一.概要 对于闭包的定义(红宝书P178):闭包就是指有权访问另外一个函数的作用域中的变量的函数. 关键点: 1.闭包是一个函数 2.能够访问另外一个函数作用域中的变量 文章首发地址于sau交流学习社 ...

  8. JavaScript面向对象的作用域链(转载)

    JavaScript的作用域一直以来是前端开发中比较难以理解的知识点,对于JavaScript的作用域主要记住几句话,走遍天下都不怕... 一.“JavaScript中无块级作用域” 在Java或C# ...

  9. Javascript——闭包、作用域链

    1.闭包:是指有权访问另一个函数作用域中的变量的函数.创建闭包的常见方式:在一个函数内部创建另一个函数. function f(name){ return function(object){ var ...

随机推荐

  1. python3 stack/ queue和deque模块

    '''栈stack 先进后出FILO (first in last out)'''lst = []lst.append("张一山")lst.append("杨紫" ...

  2. Android OpenGL ES 开发(七): OpenGL ES 响应触摸事件

    像旋转三角形一样,通过预设程序来让对象移动对于吸引注意是很有用的,但是如果你想让你的OpenGL图形有用户交互呢?让你的OpenGL ES应用有触摸交互的关键是,扩展你的GLSurfaceView的实 ...

  3. [Swift]LeetCode789. 逃脱阻碍者 | Escape The Ghosts

    You are playing a simplified Pacman game. You start at the point (0, 0), and your destination is (ta ...

  4. Primitive Assembly

    I found something in the Specification of OpenGL Version 4.6 (Core Profile): The output of Vertex Sh ...

  5. B+树的Copy-on-Write设计

    本文主要介绍B+树的Copy-On-Write,包括由来.设计思路和核心源码实现(以Xapian源码为例).中文的互联网世界里,对B树.B+树的科普介绍很丰富,但对它们在工业界的实际使用却几乎没有相关 ...

  6. hexo配置自己的博客站点

    最近业余时间利用hexo为自己搭建一个高度自定义的个人站点,站点发布在github上,访问地址为:https://cqhaibin.github.io/.本博客简单介绍实现此站点的过程.效果图如下 构 ...

  7. C# for Python(Nugut Iron包)

    cInronPython是一种在.NET和Mono上实现的Python语言,使用InronPython就可以在.NET环境中调用Python代码 安装InronPython Python: port ...

  8. C++ gui程序附加dos输出窗口

    C++ gui程序附加console qtcreator 1:在.pro文件中加入一句: CONFIG+= console 2:在运行设置里勾选在终端运行的选项 vs 1.新建gui项目 2.连接器( ...

  9. spring cloud + .net core实现微服务架构

    1.新建spring boot项目 2.添加spring-cloud-starter-eureka-server依赖(需提供版本信息) <!-- https://mvnrepository.co ...

  10. 使用ML.NET实现情感分析[新手篇]后补

    在<使用ML.NET实现情感分析[新手篇]>完成后,有热心的朋友建议说,为何例子不用中文的呢,其实大家是需要知道怎么预处理中文的数据集的.想想确实有道理,于是略微调整一些代码,权作示范. ...