前言

上一章节我们讲了VO。

我们回顾一下之前的内容。

进入执行上下文会创建VO对象、建立作用域链、确定this指向。执行上下文的数据(函数形参、变量声明、函数声明)是作为属性存储在VO中的。

我们也知道变量对象在每次进入上下文时创建,并填入初始值,值的更新出现在代码执行阶段。

这一章节我们继续深入了解执行上下文,我们来认识作用域链。

作用域链

这里引用ECMA-262-3的定义:

每一个执行上下文都与一个作用域链相关联。作用域链是一个对象组成的链表,求值标识 符的时候会搜索它。当控制进入执行上下文时,就根据代码类型创建一个作用域链,并用初始化对象填充。执行一个上下文的时候,其作用域链只会被 with 声明和 catch 语句所影响。

不能一下子看明白没关系,我们接着往下看,待会儿回过来自己思考思考。

一个demo:

这个例子的执行上下文创建和弹出的过程不明白的参见执行上下文。

根据上边ECMA-262-3的定义,作用域链是一个变量对象组成的链表,用来进行变量查询。 比如上面的'bar'上下文的作用域链依次是 AO(bar)、AO(foo)、VO(global)。

我们这样模拟全局上下文:

其中:Scope = AO|VO + [[Scope]] 也就是当前变量对象加上所有父级变量对象的列表。

讲AO的时候我们说过了有两个阶段,进入上下文(初始化)、代码执行阶段(update值)。

我们还知道JS是词法作用域规则。直白的说就是你写代码的时候就确定了作用域。不考虑eval环境的话,[[scope]]与函数紧紧搂抱(有关)。

函数的生命周期

函数的生命周期分为两个阶段:创建、调用阶段。

  1. 函数创建

    思考:

    我们会得到预期的结果,没毛病。我们可能是这样分析的:

    在foo的作用域内只有b的声明,执行alert的时候对a进行RHS查询,没找到,就去外层作用域查找,ok,我们找到了。

    现在我们得用更底层的思路去分析。虽然上边的分析没毛病。

    首先我们可以确定 foo 的 AO 对象、全局的 VO 对象:

    那么它是怎么找到 a 的呢?

    联系上前边我们说过的作用域链,是不是有一点点触碰到了。

    总结:

    1. [[scope]]是所有 父级变量对象的层级链,处于当前函数上下文之上,在函数创建时存于其中。当然对a的查找是顺着层级往上的(可以理解为从数组的头开始往右一层一层的找)。

    2. [[scope]]在函数创建时被存储,静态(不变的),永远永远,直到函数被销毁。也就是说函数一旦创建, [[scope]]属性已经写入,并存储在函数对象中,无论函数是否调用。

    3. [[scope]]和作用域链不是一个概念哦,[[scope]]是函数的一个属性而已。

  2. 函数调用阶段

    前边提到了:Scope = AO|VO + [[Scope]]

    其实更容易理解的形式是这样:

    在函数调用(进入上下文)阶段,会把当前的VO|AO加入到当前执行函数[[scope]]属性的前边。

    这时我们再回到文章开头提到的ECMA262-3对于作用域链的定义。定义中提到了标识符。

    标识符是干什么呢?

    标识符的作用就是确定一个变量(或函数声明)属于哪个变量对象。标识符解析算法在ECMA262-3中也有定义:

    执行过程中,使用下面的算法进行标识符解析查找:

    1. 获取作用域链中的下一个对象。如果没有,转到步骤5。
    2. 调用 Result(1) 的 [[HasProperty]] 方法,把标识符作为属性名传递。
    3. 如果 Result(2) 为 true,返回一个引用类型的值,其基对象是 Result(1),属性名为标识符。
    4. 转到步骤1。
    5. 返回引用类型的值,基对象为 null,属性名为标识符。
    6. 求值标识符的结果总是一个引用类型的值,其成员名字组件与标识符字符串相等

    多读几遍。

    大体上说明标识符解析总是会返回一个引用类型,这个引用类型的 getBase() 结果是对应的变量对象(或若未找到则为null)。属性名是向上查找的标识符的名称。 在向上查找中,一个上下文中的局部变量较之于父作用域的变量拥有较高的优先级。可以理解为由内向外查找,如果找到了就会返回引用类型,外层有重名的也不会找到那儿去。

    引用类型在this的章节会详细说明。

    我们通过一个copy的复杂demo来熟悉熟悉:

    在上面代码的执行阶段:

    1. 首先全局上下文

    2. 对于foo

    3. 对于 bar

      对'x', 'y', 'z'标识符的解析过程:

      1. 对'x'进行查找:

      2. 对'y'进行查找:

      3. 对'z'进行查找:

      就是这样。虽然没有完全的解释清楚ECMA262-3中对于标识符解析的算法规则,但是这样容易理解,也就是这样找的。

闭包

闭包这玩意儿可以单独写一章节。这里只是说说其与[[scope]]属性的联系。

闭包,几乎所有的JS书籍上的介绍都略有不同,每个人都有每个人的理解。

在<<你不知道的JavaScript>>中,闭包定义是:

函数拥有对其词法作用域的访问,哪怕是在当前作用域之外执行

红宝书中是:

闭包是指有权访问另一个函数作用域中的变量的函数。

我们通过一个demo来解读:

我提几点,然后自己思考。得到的结论就是你对闭包的初步认识了。

从本章节的内容来看,闭包与[[scope]]属性息息相关。首先之前提到了函数的[[scope]]属性是静态属性,函数创建的时候就被存储到函数对象中,函数销毁才会销毁。

闭包的特点恰好就是持久的保有对其定义的词法作用域的访问权限。

再来一点,[[scope]]中保存的是当前函数的所有上层变量对象。上面的demo中foo()持久访问的正是其上层匿名立即执行函数的AO对象中的属性。

所以,闭包是函数代码和其[[scope]]的结合?

Function构建的函数[[scope]]中只有全局的VO

这个Function是很有意思的,之前在一些讲解原型的高热度的文章中,发现作者很多会说所有的函数都有prototype属性。其实是错误的,比如Function.prototype.bind创建的函数就是没有prototype属性的。

同样Function构建的函数,其[[scope]]也比较特殊,里面只有全局的VO对象。

f3只能够访问全局VO中的属性,不能访问VO(foo);印证了[[scope]]里面只有全局的VO对象。

with & catch & eval

eval不建议使用,就简单提一提。代码eval的上下文与当前的调用上下文(calling context)拥有同样的作用域链:

文章开始部分引用的ECMA-262-3关于作用域的定义中提到了with & catch这一点。

事实上在代码执行阶段,可以通过with声明和catch语句修改作用域链。

它们将会被添加到作用域链的最前端:

很多资料对这个过程解释的很绕,我们通过一段代码具体分析:

我们一步一步分析。

  1. 代码开始执行

  2. 执行with语句

  3. with结束

OK,catch的过程是一样的,就两个核心: 第一是withObject|catchObject会被添加到作用域链前端,其实就是标识符解析从withObj|catchObj先开始,因为它在头上嘛。这也是我们说with、catch可以挟持作用域的原因。

第二就是声明完成之后会移除这些状态,回到最初的美好。

留一个思考题:

作用域链与原型链

二维作用域链查找,就是说在对象中没找到就去原型链上查找:

这个其实大家很熟悉,原型链末尾都是 -> Object.prototype -> null。

顺着原型链逐级的委托,最终会成功输出'jack ma'。

好的,接着:

这里输出了10,而不是顶层原型定义的50。上一个例子明明是输出的原型委托继承的值啊?

我们分析:

现在写出这个流程大家应该驾轻就熟。我们能够得到一些什么呢?

执行阶段会顺着原型链的层级进行标识符解析工作。而活动对象(AO)是没有原型的。 所以inner的AO中没有x属性,就去下一链级的outer中找。找到了,如果没有就去全局找了,全局中的x是可以通过原型委托继承得到的。

所以,假如你删除var x = 10;得到的是原型委托继承的50。

下一章

执行上下文 -- this

参考

ECMA-262-3
dmitrysoshnikov.com

原文发布时间:2017-11-30

本文来源掘金如需转载请紧急联系作者

你还要我怎样的JS系列(4) -- 作用域链相关推荐

  1. 深入理解js系列一作用域是什么

    title: 深入理解js系列一作用域是什么? date: 2018-02-06 02:15:59 tags: 深入理解js系列 作用域 categories: JavaScript 作用域是什么? ...

  2. JS - 自由变量与作用域链

    先解释一下什么是"自由变量". 在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量.如下图 如上程序中,在调用fn() ...

  3. 【JS】原始值与引用值、执行上下文与作用域链、作用域链增强、变量声明、标识符查找

    1.原始值与引用值 EcmaScript变量可以包含两种不同类型的数据:原始值.引用值 原始值:最简单的数据 引用值:由多个值构成的对象 六种原始值: Undefined Null Boolean N ...

  4. 温故js系列(11)-BOM

    前端学习:教程&开发模块化/规范化/工程化/优化&工具/调试&值得关注的博客/Git&面试-前端资源汇总 欢迎提issues斧正:BOM JavaScript-BOM ...

  5. js系列教程5-数据结构和算法全解

    全栈工程师开发手册 (作者:栾鹏) 快捷链接: js系列教程1-数组操作全解 js系列教程2-对象和属性全解 js系列教程3-字符串和正则全解 js系列教程4-函数与参数全解 js系列教程5-容器和算 ...

  6. 【深入浅出Node.js系列十一】Node.js开发框架Express4.x

    为什么80%的码农都做不了架构师?>>>    #0 系列目录# 深入浅出Node.js系列 [深入浅出Node.js系列一]什么是Node.js [深入浅出Node.js系列二]N ...

  7. Ember.js系列文章

    JS前端框架之Ember.js系列文章 本文为文章索引,主要是罗列Ember.js的相关文章便于阅读. 相关演示代码:github for free. 基础篇 1. EmberJs之What|Why| ...

  8. 【D3.V3.js系列教程】--(十四)有路径的文字

    [D3.V3.js系列教程]--(十四)有路径的文字 1. 在 svg 中插入一個 text // 在 body 中插入一個 svg var svg = d3.select('body').appen ...

  9. js带开关的时钟_昌利JS系列混凝土搅拌机衬板配件

    搅拌机衬板一般有扇形衬板.弧形衬板.异形板等组成,它安装在搅拌罐的内壁上,形成曲面形,起到对搅拌机内壁的保护作用.强制式混凝土搅拌机因为在搅拌物料的过程中搅拌筒内壁会受到大量的矿石.水泥粉料等物料的强 ...

最新文章

  1. python的语法类似php_PHP实现类似python__mian__=__name__来单独执行类文件
  2. Ubuntu terminal路径太深,名字太长
  3. asterisk1.8 for mipsel mysql
  4. [转载] java synchronized静态同步方法与非静态同步方法,同步语句块
  5. python的help怎么用_python的help函数如何使用
  6. python串口模块_Python使用pip安装pySerial串口通讯模块
  7. boot入门思想 spring_什么是Spring boot?Spring Boot快速入门以及Spring Boot实例教程
  8. redis、mysql、和php原生array数组效率对比
  9. 什么是串口协议转换器?串口转换器有哪些特点?
  10. win10关闭触摸板自动开启
  11. 可用于近红外光谱数据分析的网上公开数据集
  12. C基础:45道练习题汇总(初学者加油)
  13. 苹果笔记本怎么查看计算机基本信息,如何查询苹果电脑型号_查询苹果电脑型号的方法...
  14. 基于51单片机的DS12C887电子钟万年历带农历温度
  15. 【Educoder】Python学习记录(二)
  16. 基于浙大MO平台的开发机器学习算法
  17. 设置vim 显示行号
  18. 2022一建四色笔记
  19. 专治不会看源码的毛病--spring源码解析AOP篇(2017版)
  20. AI+安防,视频监控的6大技术趋势

热门文章

  1. 找不到php fpm.pid,找不到文件nginx php-fpm
  2. linux已使用线程,在Linux中使用线程
  3. android studio 中怎么写aspectj代码,AndroidStudio中 AspectJ 基础使用 简介
  4. chromium禁用ajax,页面加载时,jQuery AJAX不会在Chrome / Chromium中启动
  5. 鼠标、键盘键值对应表
  6. 74hc595级联c语言程序,74hc595级联程序范例
  7. day03:关于惯性导航工具箱的学习与使用:use of the progen
  8. linux c语言 udp 接收和发送数据用同一个端口_【Python学习笔记】80、UDP编程
  9. python爬微信头像_使用python itchat包爬取微信好友头像形成矩形头像集的方法
  10. [转]VB中资源文件.res的使用方法详解