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

版权声明:转载需注明出处 https://blog.csdn.net/zf2014122891/article/details/85926955

本文译自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__ of the current activation
      // object is already the global object,
      // i.e. the special chain of variable objects is formed,
      // so-called, a scope chain
      print(AO.__parent__ === global); // true
      
      print(AO.__parent__.x); // 10
  
})();

结语

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

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

交流方式:QQ1670765991

猜你喜欢

转载自blog.csdn.net/zf2014122891/article/details/85926955
今日推荐