从ECMAScript规范深度分析JavaScript(三):变量对象(下)

本文译自Dmitry Soshnikov的《ECMA-262-3 in detail》系列教程。其中会加入一些个人见解以及配图举例等等,来帮助读者更好的理解JavaScript。
声明:本文不涉及与ES6相关的知识。

前言

在本系列教程上一篇文章《从ECMAScript规范深度分析JavaScript(二):变量对象(上)》中我们讲述了变量对象的概念,以及在不同上下文中变量对象的区别,接下来让我们来深入探讨一下,在程序的不同阶段变量对象的行为、关于变量所要知道的细节以及特殊属性__parent__。

处理上下文代码的阶段

现在我们终于触及到本文的核心内容,执行上下文的代码被分成两个基本的阶段来处理:

  • 进入执行期上下文
  • 代码执行

变量对象的变化与这两个阶段紧密相关。

注意:这两个阶段不管是全局还是函数上下文的共同行为。

1、进入执行期上下文

在进入执行期上下文还没开始执行代码时,VO中有以下属性(在之前已经阐述过了):

  • 所有的形参(如果是在函数的执行期上下文中)
    这是变量对象的一个由形参的名和值创建的属性;如果没有对应传递实际参数,那么这个属性就由形式参数的名称和undefined值创建
  • 所有的函数声明
    这是变量对象的一个由函数对象的名和值创建的属性;如果变量对象已经包含了一个相同的属性名称,那么将会替换掉他的值和特性
  • 所有的变量声明
    这是变量对象的一个由变量名和值创建的一个属性;如果变量名已经和一个形参或者是一个函数名相同,则变量声明不会改变已经存在的属性

我们来看个例子:

    function test(a, b) {
      var c = 10;
      function d() {}
      var e = function _e() {};
      (function x() {});
    }    
    test(10); // 调用 

在传递10这个参数进入test函数上下文时,AO像下面这样

    AO(test) = {
      a: 10,
      b: undefined,
      c: undefined,
      d: <reference to FunctionDeclaration "d">
      e: undefined
    };

注意:AO并不包含函数x,这是因为x不是函数声明而是函数表达式(Function-Expression,缩写为FE),而函数表达式不会影响变量对象。

然而,函数_e也是一个函数表达式,但是我们接下来会发现,因为将它赋值给了变量e,我们可以通过变量名e来访问它(我们暂时先不讨论函数声明和函数表达式的区别)。

在这之后,就来到了上下文代码进行的第二阶段,代码执行期。

2、代码执行

此时,AO/VO已经充满了属性(但是,它们不是所有的属性都有真实值,其中的大多数仍然是初始值undefined)。

思考一下同样的例子,AO/VO在代码解释执行期间被修改:

    AO['c'] = 10;
    AO['e'] = <reference to FunctionExpression "_e">;

我们再次注意到,函数表达式_e仍然只存在于内存中,因为它被存储给了变量声明e,但是函数表达式x不存在于AO/VO中。如果我们尝试在定义前(甚至是定以后)调用x函数,我们将会得到一个错误:“x” is not defined

注意:没有保存的函数表达式只能在他定义的地方或者递归中调用。

一个经典的例子:

    alert(x); // 是个函数
     
    var x = 10;
    alert(x); // 10
     
    x = 20;
    function x() {}
     
    alert(x); // 20

为什么第一次打印x是函数并且还是在定义它之前访问的?为什么不是10或者20呢?

因为: 根据规则,VO对象在进入上下文的阶段填入函数声明了(这个常被称为函数声明提前);在同一阶段,还有一个变量声明“x”,那么正如我们之前提及的那样,变量声明在顺序上跟在函数声明和形式参数声明之后,而且,在这个阶段(指进入执行上下文阶段),变量声明不会干扰VO中已经存在的同名函数声明或形式参数声明,因此,在进入上下文时,VO的结构如下:

    VO = {};
      
    VO['x'] = <reference to FunctionDeclaration "x">
    
    // 发现var x = 10;如果函数x不是已经定义了,x将会是undefined,但在我们的示例中,变量声明没有影响到具有相同名称函数的值。
    VO['x'] = <the value is not disturbed, still function>

之后,我们进入到代码执行阶段,VO的变化如下:

    VO['x'] = 10;
    VO['x'] = 20;

我们将会在第二次和第三次打印中看到这些。

在下面这个例子中,我们会再次发现,变量是在进入上下文阶段放入VO的(else代码块从未执行,但是没用,变量b依然存在于VO当中):

    if (true) {
      var a = 1;
    } else {
      var b = 2;
    }
     
    alert(a); // 1
    alert(b); // undefined, 而不是"b is not defined",证明b是声明过了的

注:这是因为在ES6之前,javascript没有块级作用域的概念,ES6中情况会有其他情况(比如let声明),我们在这里不作讨论。

关于变量

通常,各类文章甚至JavaScript书籍都声称:“可以在全局作用域中使用var关键字或者在所有地方不使用var关键字来声明全局变量”。事实上并不是这样,一定要记住:

任何时候,变量只能通过使用var关键字才能声明。

向下面这样赋值:

    a = 10;

只是创建了全局对象的一个属性,而不是变量。“不是变量”不是说它不能够被改变,而是它不符合ECMAScript的概念(他们也变成了全局对象的属性,因为 VO(globalContext) === global,这个我们在之前就有提及)。

我们来看一下这个例子来了解他们的区别:

    alert(a); // undefined
    alert(b); // "b" is not defined
     
    b = 10;
    var a = 20;

所有这些都取决于VO和他的修改阶段(进入上下文阶段和代码执行阶段)
进入上下文时:

    VO = {
      a: undefined
    };

我们发现在这个阶段没有b,因为它不是变量,b将只会在代码执行阶段出现(但是在我们的代码中没有,因为会有报错)
改一下代码:

    alert(a); // undefined, 我们知道这种情况
     
    b = 10;
    alert(b); // 10, 在代码执行阶段被创建
     
    var a = 20;
    alert(a); // 20, 在代码执行阶段被修改

这里关于变量还有一个更重要的点,和普通简单属性不同,变量有{DontDelete}特性,这意味着没法通过delete运算符删除变量:

    a = 10;
    alert(window.a); // 10
     
    alert(delete a); // true
     
    alert(window.a); // undefined
     
    var b = 20;
    alert(window.b); // 20
     
    alert(delete b); // false
     
    alert(window.b); // still 20

但是这条规则在一种执行期上下文的情况下不生效,即eval上下文中{DontDelete}特性不会被设置给变量:

    eval('var a = 10;');
    alert(window.a); // 10
     
    alert(delete a); // true
     
    alert(window.a); // undefined

所以有些人使用某些debug调试工具的控制台测试这个例子时会出现问题,比如Firebug:

注意:Firebug使用的是也是eval来执行你控制台的代码的,所以这里的变量没有{DontDelete}特性,进而能够被删除。

特殊实现:__parent__属性

我们之前已经提到,根据标准,是没法直接获取激活对象的。但是,在一些实现上没有遵守这个标准,比如SpiderMonkey 和Rhino中,函数拥有一个特殊的**parent**属性,用来指向创建这个函数的激活对象(或者是全局变量对象)。

示例(SpiderMonkey,Rhino):

    var global = this;
    var a = 10;
      
    function foo() {}
      
    alert(foo.__parent__); // global
      
    var VO = foo.__parent__;
      
    alert(VO.a); // 10
    alert(VO === global); // true

在上面的例子中,foo函数在全局上下文中被创建,因此,他的__parent__属性被设置为全局上下文的变量对象(也就是全局对象)。

然而,没法用相同的方式从SpiderMonkey中访问激活对象:根据版本的不同,内部函数的__parent__属性返回null或者全局对象。

在Rhino中是允许通过同样的方式来来访问激活对象的,比如:

    var global = this;
    var x = 10;
      
    (function foo() {
	      var y = 20;
	      
	      // the activation object of the "foo" context
	      var AO = (function () {}).__parent__;
	      
	      print(AO.y); // 20
	      // 当前激活对象的__parent__已经存在是全局对象
	      // 这样特殊的变量对象链就形成了,就是作用域链
	      print(AO.__parent__ === global); // true
	      
	      print(AO.__parent__.x); // 10
      
    })();

结语

在本章中我们进一步学习了和执行期上下文相关的对象,此篇是很重要的一章,明白了这其中的机制,我们才能够对后续的作用域链,闭包等知识有更好的理解。

希望此文能够解决大家工作和学习中的一些疑问,避免不必要的时间浪费,有不严谨的地方,也请大家批评指正,共同进步!
转载请注明出处,谢谢!

交流方式:QQ1670765991

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值