zoukankan      html  css  js  c++  java
  • ES5.1下的作用域链

    作用域链(ES5.1规格视角)

    javascript作用域类型

    javascript采用的是词法作用域(静态作用域) 与之相对的是动态作用域,以下的例子可以很好地说明

    let a = 3
    function foo() {
      console.log(a); 
    }
    function foowrapper() {
      let a = 4
      foo()
    }
    foowrapper() // 3而不是4
    

    静态作用域是与我们惯性思维冲突的,从上述例子可以看出,我们第一次看会习惯性往函数调用位置去找变量(动态作用域思维),但是javascript使用的是静态作用域,也就是代码写在哪里,作用域就从那里链接,而不是运行的位置进行作用域链接,这是整个作用域的核心概念

    作用域链定义

    函数调用上下文作用域链

    规格

    伪代码
    执行环境由词法环境、变量环境 this组成 ,今天我们主要关注点在变量环境(VO|AO),和词法环境[[scope]]

    activeExecutionContext = {
        VO: {...}, // or AO
        this: thisValue,
        Scope: [ // Scope chain
          // 所有变量对象的列表
          // 用于标识符查找
        ]
    };
    
    Scope定义
    // [[Scope]]表示内部使用的抽象操作,用于描述语言规范本身
    Scope = AO + [[Scope]]
    

    函数激活作用域链接过程

    在函数进入上下文(函数创建时)AO/VO,Scope定义如下:

    Scope = AO|VO + [[Scope]]
    

    解析
    活动对象是作用域数组的第一个对象,被添加到作用域的前端

    Scope = [AO].concat([[Scope]])
    

    函数激活链接实例

    var x = 10;
     
    function foo() {
      var y = 20;
     
      function bar() {
        var z = 30;
        alert(x +  y + z);
      }
     
      bar();
    }
     
    foo(); // 60
    

    对此,我们有如下的变量/活动对象,函数的的[[scope]]属性以及上下文的作用域链:

    全局上下文的变量对象是:

    globalContext.VO === Global = {
      x: 10
      foo: <reference to function>
    };
    

    在“foo”创建时,“foo”的[[scope]]属性是:

    foo.[[Scope]] = [
      globalContext.VO
    ];
    

    在“foo”激活时(进入上下文),“foo”上下文的活动对象是:

    fooContext.AO = {
      y: 20,
      bar: <reference to function>
    };
    

    “foo”上下文的作用域链为:

    fooContext.Scope = fooContext.AO + foo.[[Scope]] 
     
    fooContext.Scope = [
      fooContext.AO,
      globalContext.VO
    ];
    

    内部函数“bar”创建时,其[[scope]]为:

    bar.[[Scope]] = [
      fooContext.AO,
      globalContext.VO
    ];
    

    在“bar”激活时,“bar”上下文的活动对象为:

    barContext.AO = {
      z: 30
    };
    

    “bar”上下文的作用域链为:

    barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:
     
    barContext.Scope = [
      barContext.AO,
      fooContext.AO,
      globalContext.VO
    ];
    

    对“x”、“y”、“z”的标识符解析如下:

    - "x"
    -- barContext.AO // not found
    -- fooContext.AO // not found
    -- globalContext.VO // found - 10
    
    - "y"
    -- barContext.AO // not found
    -- fooContext.AO // found - 20
    
    - "z"
    -- barContext.AO // found - 30
    

    一个例外-使用函数构造函数生成函数

    var x = 10;
     
    function foo() {
     
      var y = 20;
     
      function barFD() { // 函数声明
        alert(x);
        alert(y);
      }
     
      var barFE = function () { // 函数表达式
        alert(x);
        alert(y);
      };
     
      var barFn = Function('alert(x); alert(y);');
     
      barFD(); // 10, 20
      barFE(); // 10, 20
      barFn(); // 10, "y" is not defined
     
    }
     
    foo();
    

    正常来说,在函数创建时获取函数的[[scope]]属性,通过该属性访问到所有父上下文的变量,但是由函数构造函数创建的的函数,其[[scope]]总是唯一的全局对象属性。

    动态改变作用域链

    eval

    eval能够动态生成函数,且eval上下文与当前调用上下文的作用域链一致
    示例

    function foo(str, a) {
    eval( str ); // 4
    var c = 4;
    console.log( a, b );
    }
    var b = 2;
    var c = 3;
    foo( "var b = 3; console.log(c)", 1 ); // 1, 3
    

    这里我们使用eval打印了c,可以看到结果是foo作用中的c,这里判断出eval不是像函数构造函数的[[Scope]]总是唯一的全局变量,而b变量能够在foo中访问,也说明了eval与foo是享受了同一个作用域,当然严格模式下,eval会创建自己的作用域,不会影响到当前调用上下文的作用域,如下例:

    function foo(str, a) {
      'use strict'
      eval(str) // 4
      var c = 4
      console.log(`foo-a ${a} foo-b ${b}`) // 1, 2 而不是 1 3
    }
    var b = 2
    var c = 3
    foo('var b = 3; console.log("eval-b",b,"eval-c",c)', 1) 
    

    这里访问b时,输出了2而不是eval中的3,说明严格模式下,eval创建了自己的作用域,不会影响到当前调用的上下文作用域

    with、catch

    它们将作用变量添加到作用域链最前端,作用域链修改如下:

    Scope = withObject | catchObject + AO | VO +[[Scope]]
    

    with实例

    var x = 10, y = 10;
     
    with ({x: 20}) {
     
      var x = 30, y = 30;
      console.log(x) // 30
      console.log(y) // 30
    }
     
    console.log(x) // 10
    console.log(y) // 30
    

    过程解析:

    1. x = 10, y = 10
    2. {x: 20} 添加到作用域顶端
    3. var x 、var y 在执行代码前以及被提升到with外面
    4. 修改{x: 20} 中的20 为 30
    5. 没在{x: 30}中找到y,在全局作用域中找到了y,将其修改为30
    6. with结束,将{x: 30} 从作用域链中移出。
    7. 此时x为全局作用域x,没有变化,而y在with声明时,变为30

    catch实例

    try {
      ...
    } catch (ex) {
      console.log(ex)
    }
    

    作用域链修改为:

    var catchObject = {
      ex: <exception object>
    };
     
    Scope = catchObject + AO|VO + [[Scope]]
    

    知识关联---二维作用域链(作用域链、原型链)查找

    为什么会有二维作用域链呢,因为一个属性在对象中没有直接找到,查询会在原型链中继续。也就是搜索属性

    过程
    1. 作用域链环节
    2. 深入到原型链环节
    可能的情况

    二维作用域链是指在作用域查找之后,再去原型链上查找,分为一下两种情况

    1. 基本属性查找,查找到了作用域链末端,全局变量节点,此时会在全局变量的原型链上展开搜索。
    2. 对象属性查找,先要在作用域链上找到对象本身,然后再去对象原型链上找到指定属性。

    情况1示例

    function foo() {
      console.log(x)
    }
     
    Object.prototype.x = 10;
     
    foo(); // 10
    

    过程解析:

    1. 在foo的VO对象没有找到x
    2. 向作用域链下一个节点查找,此时为window
    3. 无法在window上查找到属性x
    4. 此时已经是作用域顶端,便向着window其原型链找到x,辛酸过程如下图

      可以看到window原型链的末端节点为Object,终于在这里找了x,输出了10

    情况2示例

    function foo() {
      console.log(o.x)
    }
    
    let o = {
    }
     
    Object.prototype.x = 200
    foo(); 
    

    过程解析:

    1. 在foo的VO对象没有找到o
    2. 在window对象找到了o
    3. 在o自身查找x属性,没有找到
    4. 在o的原型上Object.prototype上找到了x = 200
    5. 输出200

    感谢

    参考了颜海镜大大翻译的ES5.1规格,感谢他和汤姆大叔的无私奉献,然后我才能发现一些东西别人几年前就看透了,自己好菜呀(划掉),这里是汤姆大叔的原文,我推荐大看一下他的深入js系列,虽然在2012写的,但是放在现在仍然是教科书级别的干货,例子出于他的文章和《你不知道的javascript》

  • 相关阅读:
    IDEA快捷的添加包名
    Spring源码(一)
    Java的理解
    剪绳子
    机器人的运动范围
    矩阵中的路径
    N皇后问题
    回溯法
    全排列
    反转链表
  • 原文地址:https://www.cnblogs.com/sefaultment/p/10756385.html
Copyright © 2011-2022 走看看