资讯专栏INFORMATION COLUMN

“动静结合” 小白初探静态(词法)作用域,作用域链与执行环境(EC)

Drummor / 3563人阅读

摘要:图片中的作用域链,是全局执行环境中的作用域链。然后此活动对象被推入作用域链的最前端。在最后调用的时候,创建先构建作用域链,再创建执行环境,再创建执行环境的时候发现了一个变量标识符。

从图书馆翻过各种JS的书之后,对作用域/执行环境/闭包这些概念有了一个比较清晰的认识。

栗子说明一切 第一个栗子

来看一个来自ECMA-262的栗子:

var x = 10;

(function foo() {
  var y = 20;
  (function bar() {
    var z = 30;
    // "x" and "y" are "free variables"
    // and are found in the next (after
    // bar"s activation object) object
    // of the bar"s scope chain
    console.log(x + y + z);
  })();
})();

我们可以用下图展现上面的例子(父变量对象存储在函数的Scope属性内)

首先,可以很容易的理解到一个事实:在从控制台输出x+y+z的时候,xy是在bar()函数中的作用域链中bar()的活动对象之下找到的。实际上,foo()函数和bar()函数在执行的时候,他们的scope属性就已经确定了,他们的scope属性确定为他们外层的变量对象(VO)的集合。从图中可知,内存结构可能是这样的:

// foo的scope属性是global的VO
foo.["[[Scope]]"] = { global.["Variable Object"] }

// bar的scope属性是foo的AO和global的VO的集合
bar.["[[Scope]]"] = {foo.["Activation Object"], global.["Variable Object"]}
第二个栗子

这个例子来自《高性能Javascript》

// 全局范围定义
function add(num1, num2) {
  var sum = num1 + num2;
  return sum;
}

add()函数创建的时候,它的scope属性被确定为全局对象的VO,这个全局对象的VO可能包括window/navigator/document之类等等。关系如图:

这个scope属性很特别,他是静态的,在函数创建的时候便能确定。图片中的作用域链,是全局执行环境中的作用域链。而在函数执行的时候,书中说道:

每个执行上下文都有自己的作用域链,用于解析标识符。当执行上下文被创建的时候,它的作用域链初始化为当前运行函数的scope属性中的对象。这些值按照他们出现在函数中的顺序,被复制到执行上下文的作用域链中。这个过程一旦完成,一个被称为“活动对象(AO)”的新对象就为执行上下文创建好了。活动对象作为函数运行时的变量对象,包含了所有的局部变量,命名参数,参数集合以及this然后此活动对象被推入作用域链的最前端

可以了解到,作用域链是个链表,是在函数执行的时候才存在的,也就是函数创建执行环境的时候才开始存在的,它先把这个函数的静态属性scope属性中的所有变量对象按照顺序复制到作用域链(所以这样就不会担心作用域链嵌套的问题),然后创建AO放在作用域链顶部“0号位”。例如再执行代码:

var total = add(5, 10);

图片如下图:

所以,我们也可以得到一个惊人的结论:

函数作用域链 = 活动对象(AO) + scope属性

关键的来了

这个结论中:活动对象(AO)是临时的,动态的,独一无二的。scope属性是静态的,确定的。

所以说,函数的作用域链,是函数执行的时候动态创建的,但是它又是基于静态词法的环境(scope属性)。所谓“动态创建”,是指在函数执行的时候,先创建之前没有的作用域链,再创建活动对象,然后活动对象推入作用域链最前端;所谓“基于静态的词法环境”是指函数定义的时候,这个函数本是没有作用域链的,有的只有scope属性,而这个属性指向了这个函数外部的执行环境,而这个外部的执行环境拥有作用域链(因为这是外部创建外部的执行环境才拥有作用域链的,这样有一点递归的味道)。P.S.其实有的版本也说,作用域链的确定应该是在活动变量创建完成之后的,这个有待钻研。
P.S 在ES5规范文档中,进入函数代码的流程:

扯到变量提升

变量提升的本质就是函数在创建执行环境中的变量对象的时候,记录下了函数声明,变量和参数等等。具体参见深入理解Javascript之执行上下文(Execution Context),下面是片段:

建立Variable Object对象顺序:

建立arguments对象,检查当前上下文中的参数,建立该对象下的属性以及属性值

检查当前上下文中的函数声明: 每找到一个函数声明,就在variableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用。如果上述函数名已经存在于variableObject下,那么对应的属性值会被新的引用所覆盖。

检查当前上下文中的变量声明: 每找到一个变量的声明,就在variableObject下,用变量名建立一个属性,属性值为undefined。如果该变量名已经存在于variableObject属性中,直接跳过(防止指向函数的属性的值被变量属性覆盖为undefined),原属性值不会被修改。

扯到闭包

闭包,在离散数学中指的是满足性质A的一个最小关系集R,这可以理解这个关系集R,在性质A上封闭。闭包不是一种魔法,虽然可以通过闭包扯得很远很远,通过函数的作用域链的组成为AO+scope属性,为快速理解闭包中变量引用来自哪里提供了思路————没那么复杂,就直接再执行的函数定义处上看就行了。把函数定义的作用域看成是函数执行的作用域。这也是词法作用域迷人的地方。

Show Me the Code

说了那么多,有代码才是王道,毕竟“Talk is cheap”。

“面向对象”一般的编程:实现封装

这段代码来自MDN-用闭包模拟私有方法,有更改

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function(dis) {
      changeBy(dis);
    },
    decrement: function(dis) {
      changeBy(-dis);
    },
    value: function() {
      return privateCounter;
    }
  }
})();

console.log(Counter.value()); // 0
Counter.increment(1);
Counter.increment(2);
console.log(Counter.value()); // 3
Counter.decrement(5);
console.log(Counter.value()); // -2

返回的是一个对象,这个对象有三个属性,都是函数。而且这三个函数的scope属性都是指向一个集合,这个集合包括外层匿名函数的的AO,和全局变量的VO。分析一下Counter.value()这个调用:value这个属性对应的匿名函数定义的时候,它的scope属性确定,这个是词法作用域的特性,这个scope属性指向的是外部所有变量对象的集合(也就是上句说的那个集合)。在最后调用Counter.value()的时候,创建先构建作用域链,再创建执行环境,再创建执行环境的时候发现了一个变量标识符privateCounter。好,接下来在函数体内找找这个对应的值,找不到;到外层的函数,也就是那个Counter对应的匿名函数,诶找到了!好,将这个标识符和这个量“关联起来”。
结果,这样下来,返回的这个对象就类似于面向对象变成中的“外部接口”,而没有被返回的那部分(也就是代码中的var privateCounter function changeBy)则成了“私有的”,无法从外部直接访问。这样的闭包模拟了数据的封装和隐藏,一股熟悉而浓郁的C++味道袭来。当然,这样用的确不错,但是关乎性能方面,MDN这样推荐道:

如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用,方法都会被重新赋值一次(也就是说,为每一个对象的创建)。

执行环境到底是怎么建立的?

下面片段来自深入理解Javascript之执行上下文(Execution Context)

  function foo(i) {
   var a = "hello";
   var b = function privateB() {

   };
   function c() {

   }
}

foo(22);

在调用foo(22)的时候,建立阶段如下:

fooExecutionContext = {
   variableObject: {  // 变量对象
       arguments: {
           0: 22,
           length: 1
       },
       i: 22, // 形式参数声明在函数声明前
       c: pointer to function c() // 注意,函数声明在变量声明前
       a: undefined,
       b: undefined
   },
   // 作用链和变量对象顺序问题,有待钻研,T.T
   // 在官方文档中,貌似是作用域链先被创建(而且被称作词法环境组件)
   scopeChain: { ... },
   this: { ... }
}

由此可见,在建立阶段,除了arguments,函数的声明,以及参数被赋予了具体的属性值,其它的变量属性默认的都是undefined。一旦上述建立阶段结束,引擎就会进入代码执行阶段,这个阶段完成后,上述执行上下文对象如下:

fooExecutionContext = {
   variableObject: {
       arguments: {
           0: 22,
           length: 1
       },
       i: 22,
       c: pointer to function c()
       a: "hello",
       b: pointer to function privateB()
   },
   scopeChain: { ... },
   this: { ... }
}

我们看到,只有在代码执行阶段,变量属性才会被赋予具体的值

总结一下

分析代码的时候,务必回看函数的定义,毕竟人家函数是一等贵族

记住函数作用域链 = (动)活动对象(AO) + (静)scope属性

执行环境结构:

执行环境创建后,才开始执行代码,变量对象才开始被赋值

变量提升 ==> 变量对象的创建

闭包 ===> 作用域链中静态的部分,即scope属性

官方文档的补充


我的理解:词法环境组件 ≈ 作用域;变量环境组件 ≈ 变量对象;

以初始化全局代码的时候,貌似是创建变量对象在先。(这样有什么特殊的意义吗?)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79161.html

相关文章

  • js深入(三)作用链与闭包

    摘要:在之前我们根绝对象的原型说过了的原型链,那么同样的万物皆对象,函数也同样存在这么一个链式的关系,就是函数的作用域链作用域链首先先来回顾一下之前讲到的原型链的寻找机制,就是实例会先从本身开始找,没有的话会一级一级的网上翻,直到顶端没有就会报一 在之前我们根绝对象的原型说过了js的原型链,那么同样的js 万物皆对象,函数也同样存在这么一个链式的关系,就是函数的作用域链 作用域链 首先先来回...

    blair 评论0 收藏0
  • Javascript 函数、作用链与闭包

    摘要:而外层的函数不能访问内层的变量或函数,这样的层层嵌套就形成了作用域链。闭包闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量。 闭包是js中一个极为NB的武器,但也不折不扣的成了初学者的难点。因为学好闭包就要学好作用域,正确理解作用域链,然而想做到这一点就要深入的理解函数,所以我们从函数说起。 函数...

    ssshooter 评论0 收藏0
  • 前端基础进阶(四):详细图解作用链与闭包

    摘要:之前一篇文章我们详细说明了变量对象,而这里,我们将详细说明作用域链。而的作用域链,则同时包含了这三个变量对象,所以的执行上下文可如下表示。下图展示了闭包的作用域链。其中为当前的函数调用栈,为当前正在被执行的函数的作用域链,为当前的局部变量。 showImg(https://segmentfault.com/img/remote/1460000008329355);初学JavaScrip...

    aikin 评论0 收藏0
  • 「译文」JavaScript核心

    摘要:在这个情况下我们可能需要使用构造函数,其以指定的模式来创造对象。构造函数也有自己的,值为,也通过其属性关联到。从逻辑上来说,这是以栈的形式实现的,它叫作执行上下文栈。 原文:http://dmitrysoshnikov.com/ecmascript/javascript-the-core/ 对象 原型链 构造函数 执行上下文栈 执行上下文 变量对象 活动对象 作用域链 闭包 Thi...

    高璐 评论0 收藏0
  • JS基础——作用链与执行环境

    摘要:函数的作用域会在函数执行时用到,函数每次执行都会创建一个执行环境的内部对象,每个执行环境都有自己的作用域链。假设执行,其对应的作用域链如下函数执行过程中,变量的查找时从作用域头部开始查找,如果找到就是使用改变量的值。 每一个函数存在一个[[Scope]]内部属性,包含了一个函数被创建得作用域中对象得集合,这个集合为函数得作用域链。例如下面的全局函数: fucntion add(num1...

    Yi_Zhi_Yu 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<