深切明白JavaScript系列12:变量对象

引见

JavaScript编程的时候总避免不了声明函数和变量,以胜利构建我们的体系,然则诠释器是怎样而且在什么地方去查找这些函数和变量呢?我们援用这些对象的时候终究发生了什么?

原始宣布:Dmitry A. Soshnikov 宣布时候:2009-06-27 俄文地点:http://dmitrysoshnikov.com/ecmascript/ru-chapter-2-variable-object/

英文翻译:Dmitry A. Soshnikov 宣布时候:2010-03-15 英文地点:http://dmitrysoshnikov.com/ecmascript/chapter-2-variable-object/ 部份难以翻译的句子参考了justinw的中文翻译

大多数ECMAScript递次员应当都晓得变量与实行高低文有密切关系:

var a = 10; // 全局高低文中的变量
 
(function () {
  var b = 20; // function高低文中的部分变量
})();
 
console.log(a); // 10
console.log(b); // 全局变量 "b" 没有声明

而且,许多递次员也都晓得,当前 ECMAScript 范例指出自力作用域只能经由历程”函数(function)”代码范例的实行高低文建立。也就是说,相关于C/C++来讲,ECMAScript 里的 for 轮回并不能建立一个部分的高低文。

for (var k in {a: 1, b: 2}) {
  console.log(k);
}
 
console.log(k); // 只管轮回已完毕但变量k依旧在当前作用域

我们来看看一下,我们声明数据的时候究竟都发现了什么细节。

数据声明

假如变量与实行高低文相干,那变量本身应当晓得它的数据存储在那里,而且晓得怎样接见。这类机制称为变量对象(variable object)。

变量对象(缩写为VO)是一个与实行高低文相干的特别对象,它存储着在高低文中声明的以下内容: 变量 (var, 变量声明); 函数声明 (FunctionDeclaration, 缩写为FD); 函数的形参

举例来讲,我们能够用平常的 ECMAScript 对象来示意一个变量对象:

VO = {};

就像我们所说的, VO 就是实行高低文的属性( property ):

activeExecutionContext = {
  VO: {
    // 高低文数据(var, FD, function arguments)
  }
};

只要全局高低文的变量对象许可经由历程 VO 的属性称号来间接接见(由于在全局高低文里,全局对象本身就是变量对象,稍后会细致引见),在别的高低文中是不能直接接见 VO 对象的,由于它只是内部机制的一个完成。

当我们声明一个变量或一个函数的时候,和我们建立 VO 新属性的时候一样没有别的区分(即:有称号以及对应的值)。

比方:

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

对应的变量对象是:

// 全局高低文的变量对象
VO(globalContext) = {
  a: 10,
  test: <reference to function>
};
 
// test函数高低文的变量对象
VO(test functionContext) = {
  x: 30,
  b: 20
};

在详细完成层面(以及范例中)变量对象只是一个笼统观点。(从实质上说,在详细实行高低文中,VO 称号是不一样的,而且初始构造也不一样。

差别实行高低文中的变量对象

关于一切范例的实行高低文来讲,变量对象的一些操纵(如变量初始化)和行动都是共通的。从这个角度来看,把变量对象作为笼统的基础事物来明白更加轻易。一样在函数高低文中也定义和变量对象相干的分外内容。

笼统变量对象VO (变量初始化历程的平常行动) ║ ╠══> 全局高低文变量对象GlobalContextVO ║ (VO === this === global) ║ ╚══> 函数高低文变量对象FunctionContextVO (VO === AO, 而且添加了<arguments>和<formal parameters>)

我们来细致看一下:

全局高低文中的变量对象

起首,我们要给全局对象一个明白的定义:

全局对象( Global object ) 是在进入任何实行高低文之前就已建立了的对象; 这个对象只存在一份,它的属性在递次中任何地方都能够接见,全局对象的生命周期终止于递次退出那一刻。

全局对象初始建立阶段将Math、String、Date、parseInt作为本身属性,等属性初始化,一样也能够有分外建立的别的对象作为属性(其能够指向到全局对象本身)。比方,在DOM中,全局对象的window属性就能够援用全局对象本身(固然,并非一切的详细完成都是如许):

global = {
  Math: <...>,
  String: <...>
  ...
  ...
  window: global //援用本身
};

当接见全局对象的属性时一般会疏忽掉前缀,这是由于全局对象是不能经由历程称号直接接见的。不过我们依旧能够经由历程全局高低文的this来接见全局对象,一样也能够递归援用本身。比方,DOM中的window。综上所述,代码能够简写为:

String(10); // 就是global.String(10);
 
// 带有前缀
window.a = 10; // === global.window.a = 10 === global.a = 10;
this.b = 20; // global.b = 20;

因而,回到全局高低文中的变量对象—-在这里,变量对象就是全局对象本身:

VO(globalContext) === global;

异常有必要要明白上述结论,基于这个道理,在全局高低文中声明的对应,我们才够间接经由历程全局对象的属性来接见它(比方,事前不晓得变量称号)。

var a = new String('test');
 
console.log(a); // 直接接见,在VO(globalContext)里找到:"test"
 
console.log(window['a']); // 间接经由历程global接见:global === VO(globalContext): "test"
console.log(a === this.a); // true
 
var aKey = 'a';
console.log(window[aKey]); // 间接经由历程动态属性称号接见:"test"

函数高低文中的变量对象

在函数实行高低文中,VO是不能直接接见的,此时由运动对象(activation object,缩写为AO)饰演VO的角色。

VO(functionContext) === AO;

运动对象是在进入函数高低文时候被建立的,它经由历程函数的arguments属性初始化。arguments属性的值是Arguments对象:

AO = {
  arguments: <ArgO>
};

Arguments对象是运动对象的一个属性,它包括以下属性:

  1. callee — 指向当前函数的援用

  2. length — 真正通报的参数个数

  3. properties-indexes (字符串范例的整数) 属性的值就是函数的参数值(按参数列表从左到右分列)。 properties-indexes内部元素的个数即是arguments.length. properties-indexes 的值和现实通报进来的参数之间是同享的。

比方:

function foo(x, y, z) {
 
  // 声明的函数参数数目arguments (x, y, z)
  console.log(foo.length); // 3
 
  // 真正传进来的参数个数(only x, y)
  console.log(arguments.length); // 2
 
  // 参数的callee是函数本身
  console.log(arguments.callee === foo); // true
 
  // 参数同享
 
  console.log(x === arguments[0]); // true
  console.log(x); // 10
 
  arguments[0] = 20;
  console.log(x); // 20
 
  x = 30;
  console.log(arguments[0]); // 30
 
  // 不过,没有传进来的参数z,和参数的第3个索引值是不同享的
 
  z = 40;
  console.log(arguments[2]); // undefined
 
  arguments[2] = 50;
  console.log(z); // 40
 
}
 
foo(10, 20);

这个例子的代码,在当前版本的Google Chrome浏览器里有一个bug — 纵然没有通报参数z,z和arguments[2]依然是同享的。

处置惩罚高低文代码的2个阶段

如今我们终究到了本文的中心点了。实行高低文的代码被分红两个基础的阶段来处置惩罚:

  1. 进入实行高低文

  2. 实行代码

变量对象的修正变化与这两个阶段严密相干。

注:这2个阶段的处置惩罚是平常行动,和高低文的范例无关(也就是说,在全局高低文和函数高低文中的表现是一样的)。

进入实行高低文

当进入实行高低文(代码实行之前)时,VO里已包括了以下属性(前面已说了): 函数的一切形参(假如我们是在函数实行高低文中) — 由称号和对应值构成的一个变量对象的属性被建立;没有通报对应参数的话,那末由称号和undefined值构成的一种变量对象的属性也将被建立。 一切函数声明(FunctionDeclaration, FD) –由称号和对应值(函数对象(function-object))构成一个变量对象的属性被建立;假如变量对象已存在雷同称号的属性,则完整替代这个属性。 一切变量声明(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”来接见。 函数声明FunctionDeclaration与函数表达式FunctionExpression 的差别,将在第15章Functions举行细致的讨论,也能够参考本系列第2章揭秘定名函数表达式来相识。

这以后,将进入处置惩罚高低文代码的第二个阶段 — 实行代码。

代码实行

这个周期内,AO/VO已具有了属性(不过,并非一切的属性都有值,大部份属性的值照样体系默许的初始值undefined )。

照样前面谁人例子, AO/VO在代码诠释时期被修正以下:

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

再次注重,由于FunctionExpression”_e”保留到了已声明的变量”e”上,所以它依然存在于内存中。而FunctionExpression “x”却不存在于AO/VO中,也就是说假如我们想尝试挪用”x”函数,不论在函数定义之前照样以后,都邑涌现一个毛病”x is not defined”,未保留的函数表达式只要在它本身的定义或递归中才被挪用。

另一个典范例子:

console.log(x); // function
 
var x = 10;
console.log(x); // 10
 
x = 20;
 
function x() {};
 
console.log(x); // 20

为何第一个console.log “x” 的返回值是function,而且它照样在”x” 声明之前接见的”x” 的?为何不是10或20呢?由于,依据范例函数声明是在当进入高低文时填入的; 赞同周期,在进入高低文的时候另有一个变量声明”x”,那末正如我们在上一个阶段所说,变量声明在递次上跟在函数声明和形式参数声明以后,而且在这个进入高低文阶段,变量声明不会滋扰VO中已存在的同名函数声明或形式参数声明,因而,在进入高低文时,VO的构造以下:

VO = {};
 
VO['x'] = <reference to FunctionDeclaration "x">
 
// 找到var x = 10;
// 假如function "x"没有已声明的话
// 这时候"x"的值应当是undefined
// 然则这个case里变量声明没有影响同名的function的值
 
VO['x'] = <the value is not disturbed, still function>

紧接着,在实行代码阶段,VO做以下修正:

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

我们能够在第二、三个console.log看到这个结果。

鄙人面的例子里我们能够再次看到,变量是在进入高低文阶段放入VO中的。(由于,虽然else部份代码永久不会实行,然则不论怎样,变量”b”依然存在于VO中。)

if (true) {
  var a = 1;
} else {
  var b = 2;
}
 
console.log(a); // 1
console.log(b); // undefined,不是b没有声明,而是b的值是undefined

关于变量

一般,各种文章和JavaScript相干的书本都宣称:”不论是运用var关键字(在全局高低文)照样不运用var关键字(在任何地方),都能够声明一个变量”。请记着,这是 毛病 的观点: 任何时候,变量只能经由历程运用var关键字才声明。

上面的赋值语句:

a = 10;

这仅仅是给全局对象建立了一个新属性(但它不是变量)。”不是变量”并非说它不能被转变,而是指它不相符ECMAScript范例中的变量观点,所以它”不是变量”(它之所以能成为全局对象的属性,完整是由于VO(globalContext) === global,人人还记得这个吧?)。

让我们经由历程下面的实例看看详细的区分吧:

console.log(a); // undefined
console.log(b); // "b" 没有声明
 
b = 10;
var a = 20;

一切泉源依然是VO和进入高低文阶段和代码实行阶段:

进入高低文阶段:

VO = {
  a: undefined
};

我们能够看到,由于”b”不是一个变量,所以在这个阶段根本就没有”b”,”b”将只在代码实行阶段才会涌现(然则在我们这个例子里,还没有到那就已出错了)。

让我们转变一下例子代码:

console.log(a); // undefined, 这个人人都晓得,
 
b = 10;
console.log(b); // 10, 代码实行阶段建立
 
var a = 20;
console.log(a); // 20, 代码实行阶段修正

关于变量,另有一个主要的学问点。变量相关于简朴属性来讲,变量有一个特征(attribute):{DontDelete},这个特征的寄义就是不能用delete操纵符直接删除变量属性。

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

然则这个划定规矩在有个高低文里不起走样,那就是eval高低文,变量没有{DontDelete}特征。

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

运用一些调试东西(比方:Firebug)的控制台测试该实例时,请注重,Firebug一样是运用eval来实行控制台里你的代码。因而,变量属性一样没有{<span style=”color: #ff6600;”>DontDelete</span>}特征,能够被删除。

特别完成: parent 属性

前面已提到过,按标准范例,运动对象是不可能被直接接见到的。然则,一些详细完成并没有完整恪守这个划定,比方SpiderMonkey和Rhino;的完成中,函数有一个特别的属性 parent,经由历程这个属性能够直接援用到运动对象(或全局变量对象),在此对象里建立了函数。

比方 (SpiderMonkey, Rhino):

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

在上面的例子中我们能够看到,函数foo是在全局高低文中建立的,所以属性parent 指向全局高低文的变量对象,即全局对象。

但是,在SpiderMonkey顶用一样的体式格局接见运动对象是不可能的:在差别版本的SpiderMonkey中,内部函数的parent 偶然指向null ,偶然指向全局对象。

在Rhino中,用一样的体式格局接见运动对象是完整能够的。

比方 (Rhino):

var global = this;
var x = 10;
 
(function foo() {
 
  var y = 20;
 
  // "foo"高低文里的运动对象
  var AO = (function () {}).__parent__;
 
  print(AO.y); // 20
 
  // 当前运动对象的__parent__ 是已存在的全局对象
  // 变量对象的特别链形成了
  // 所以我们叫做作用域链
  print(AO.__parent__ === global); // true
 
  print(AO.__parent__.x); // 10
 
})();

总结

在这篇文章里,我们深切进修了跟实行高低文相干的对象。我愿望这些学问对您来讲能有所协助,能处理一些您曾碰到的题目或疑心。根据设计,在后续的章节中,我们将讨论作用域链,标识符剖析,闭包。

有任何题目,我很愉快鄙人面批评中能帮你解答。

别的参考

关于本文

本文转自TOM大叔深切明白JavaScript系列。声明一下,本人一切整顿的文章均不是照搬全抄,到场本身的明白和细致的注解,以及修正了一些语病错字等。

【深切明白JavaScript系列】文章,包括了原创,翻译,转载,整顿等各范例文章,原文是TOM大叔的一个异常不错的专题,现将其重新整顿宣布。感谢大叔。假如你以为本文不错,请帮助点个引荐,支撑一把,感激涕零。

更多优异文章迎接关注我的专栏

    原文作者:hiyangguo
    原文地址: https://segmentfault.com/a/1190000004963415
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞