Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JavaScript] 变量对象 Variable Object #4

Open
andy2046 opened this issue Feb 11, 2018 · 0 comments
Open

[JavaScript] 变量对象 Variable Object #4

andy2046 opened this issue Feb 11, 2018 · 0 comments

Comments

@andy2046
Copy link
Owner

andy2046 commented Feb 11, 2018

介绍

本文中,我们将分析与ECMAScript执行上下文相关的概念变量对象 Variable Object

先举个栗子🌰,为什么a b x表现大不相同,当引用一个函数或者变量时,解释器是如何以及从哪里找到它们的呢

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

alert(x); // function x() {}
var x = 10;
alert(x); // 10
x = 20;
function x() {};
alert(x); // 20

定义

然变量和执行上下文有关,那它应该知道数据存放在哪以及如何获取,这种机制称为变量对象

变量对象(缩写VO)是与执行上下文相关的特殊对象,它存储在上下文中声明的:

  • 变量(var,VariableDeclaration)
  • 函数声明(FunctionDeclaration,简写为FD)
  • 函数形式参数

示意地示例,可以用ECMAScript的对象来表示变量对象:

VO = {};

变量对象同时也是执行上下文EC的一个属性:

activeExecutionContext = {
  VO: {
    // context data (var, FD, function arguments)
  }
};

对变量的间接引用(通过VO的属性名)只允许发生在全局上下文中的变量对象上(全局对象本身就是变量对象), 对于其他的上下文,是无法直接引用VO的,因为VO是实现层机制

声明新的变量或函数的过程其实就是用变量或函数的名称和值在VO中创建新的属性的过程:

var a = 10;
function test(x) {
  var b = 20;
};
test(30);

上面👆代码对应的变量对象如下:

// Variable object of the global context
VO(globalContext) = {
  a: 10,
  test: <reference to function>
};

// Variable object of the "test" function context
VO(test functionContext) = {
  x: 30,
  b: 20
};

但是,但是,在实现层(标准中定义的),变量对象只是一个抽象概念

不同执行上下文中的变量对象

变量对象上的一些操作(比如:变量的初始化)和行为对于所有的执行上下文类型来说都一样,从这点来说,将变量对象表示成抽象的概念更加便捷

AbstractVO (generic behavior of the variable instantiation process)
 
  ║
  ╠══> GlobalContextVO
  ║        (VO === this === global)
  ║
  ╚══> FunctionContextVO
           (VO === AO, <arguments> object and <formal parameters> are added)

全局上下文中的变量对象

首先对全局对象(Global object)作个定义

全局对象是一个在进入任何执行上下文前就创建出来的对象,此对象以单例形式存在,它的属性在程序的任何地方都可以直接访问,它的生命周期随着程序的结束而终止

全局对象在创建时,Math,String,Date,parseInt等属性也会被初始化,同时,其中一些对象会指向全局对象本身,如BOM中,全局对象上的window属性就指向全局对象

global = {
  Math: <...>,
  String: <...>
  ...
  ...
  window: global
};

在引用全局对象的属性时,前缀通常可以省略,因为全局对象是不能通过名字直接访问的,然而,通过全局对象上的this值,以及通过BOM中的window对象这样递归引用的方式都可以访问到全局对象

String(10); // means global.String(10);
// with prefixes
window.a = 10; // === global.window.a = 10 === global.a = 10;
this.b = 20; // global.b = 20;

回到全局上下文的变量对象上,这里变量对象就是全局对象本身

VO(globalContext) === global;

准确地理解这个事实是非常必要的,正是由于这个原因,当在全局上下文中声明一个变量时,可以通过全局对象上的属性来间接地引用该变量(比如,当变量名不能提前预知的情况下)

var a = new String('test');
alert(a); // directly, is found in VO(globalContext): "test"
alert(window['a']); // indirectly via global === VO(globalContext): "test"
alert(a === this.a); // true
var aKey = 'a';
alert(window[aKey]); // indirectly, with dynamic property name: "test"

函数上下文中的变量对象

在函数的执行上下文中,VO是不能直接访问的,它主要扮演活跃对象(activation object)(简称AO)的角色

VO(functionContext) === AO;

活动对象在进入函数上下文时创建,并通过属性arguments进行初始化,其值就是Arguments对象

AO = {
  arguments: <ArgO>
};

Arguments对象是活动对象的属性,它包含的属性如下:

  • callee 对当前函数的引用
  • length 实际参数的数量
  • properties-indexes(整数,转换为字符串)其值是函数参数的值(在参数列表中从左到右),properties-indexes的个数 == arguments.length,arguments对象的properties-indexes的值和当前(真正传递的)形式参数是共享的
function foo(x, y, z) {
  
  // quantity of defined function arguments (x, y, z)
  alert(foo.length); // 3
 
  // quantity of really passed arguments (only x, y)
  alert(arguments.length); // 2
 
  // reference of a function to itself
  alert(arguments.callee === foo); // true
  
  // parameters sharing
 
  alert(x === arguments[0]); // true
  alert(x); // 10
  
  arguments[0] = 20;
  alert(x); // 20
  
  x = 30;
  alert(arguments[0]); // 30
  
  // however, for not passed argument z,
  // related index-property of the arguments
  // object is not shared
  
  z = 40;
  alert(arguments[2]); // undefined
  
  arguments[2] = 50;
  alert(z); // 40
}

foo(10, 20);

处理上下文代码的阶段

处理执行上下文代码分为两个阶段:

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

对变量对象的修改和这两个阶段密切相关

要注意的是,这两个处理阶段的行为是通用的,与上下文类型无关(不管是全局上下文还是函数上下文)

进入执行上下文

一旦进入执行上下文(但是在执行代码之前),VO就会被填充如下的一些属性:

  • 函数的形参(当进入函数执行上下文时)
    变量对象的属性,其属性名就是形参的名字,其值就是实参的值,对于没有传递的参数,其值为undefined
  • 函数声明(FunctionDeclaration,FD)
    变量对象的属性,其属性名和值就是函数对象的名称和值,如果变量对象已经包含具有相同名称的属性,则替换它的值
  • 变量声明(var,VariableDeclaration)
    变量对象的属性,其属性名即为变量名,其值为undefined,如果变量名和已经声明的函数名或函数参数名相同,则不会影响已经存在的属性

看个栗子🌰:

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

当以10为参数进入test函数上下文时,对应的AO如下:

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

注意,上面的AO并不包含函数x,因为这里的x不是函数声明而是函数表达式(FunctionExpression,简称FE),函数表达式不会影响VO, 尽管函数_e也是函数表达式,然而,由于它被赋值给变量e,因此它可以通过e来访问到

至此,处理上下文代码的第一阶段介绍完了,接下来介绍第二阶段:执行代码阶段

执行代码

此时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); // function
var x = 10;
alert(x); // 10
x = 20;
function x() {}
alert(x); // 20

上例中,为何x打印出来是函数呢,为何在声明前就可以访问到,为何10或者20不是这样呢,原因在于,根据规则,在进入上下文时,VO会被函数声明填充,同时还有变量声明x,但是,变量声明是在函数声明和函数形参之后,并且变量声明不会与已经存在的同名的函数声明和函数形参冲突, 因此在进入上下文的阶段,VO填充为如下形式:

VO = {};

VO['x'] = <reference to FunctionDeclaration "x">

// found var x = 10;
// if function "x" would not be already defined 
// then "x" be undefined, but in our case
// variable declaration does not disturb
// the value of the function with the same name

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, but not "b is not defined"

关于变量

一些JavaScript文章甚至是JavaScript书籍经常会说:声明全局变量的方式有两种,一种是使用var关键字(在全局上下文中),另外一种是不用var关键字(在任何位置),这样的描述是错误的,记住:

使用var关键字是声明变量的唯一方式

像下面👇的赋值语句:

a = 10;

仅仅是在全局对象上创建了新的属性(而不是变量),不是变量并不意味着它无法改变,而是按照ECMAScript中变量的概念不是变量

不同之处如下:

alert(a); // undefined
alert(b); // "b" is not defined

b = 10;
var a = 20;

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

进入上下文阶段:

VO = {
  a: undefined
};

这个阶段并没有任何b,因为它不是变量,b在执行代码阶段才出现

将上述代码改动一下:

alert(a); // undefined, we know why
b = 10;
alert(b); // 10, created at code execution
var a = 20;
alert(a); // 20, modified at code execution

关于变量还有一点非常重要:与简单属性不同,变量是不能删除的{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

注意,在ES5中{DontDelete}被重命名为[[Configurable]],并且可以通过Object.defineProperty方法手动管理

然而,有一个执行上下文不会被这个规则影响到,这就是eval上下文,其中没有为变量设置{DontDelete}属性

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

引用

ECMA-262-3 in detail. Chapter 2. Variable object.

Repository owner locked and limited conversation to collaborators Feb 11, 2018
@andy2046 andy2046 changed the title [JavaScript] 变量对象 Variable object [JavaScript] 变量对象 Variable Object Feb 19, 2018
Repository owner unlocked this conversation Feb 20, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant