JavaScript:变量对象(Variable Object)
引言:在使用JavaScript编程的时候,避免不了声明函数和变量,但是我们很少知道解释器是如何并且在什么地方找到这些函数和变量的,我们在引用这些对象的时候究竟发生了什么?
对ECMAScript程序员而言,应该都知道变量和执行上下文有密切关系:
var a = 10; //全局上下文的变量
(function () {
var b = 20; //function上下文中的局部变量
})
alert(a); // 10
alert(b); //全局变量"b"没有声明
当前ECMAScript规范指出独立作用域只能通过"函数(function)"代码类型的执行上下文创建,相对于c/c++来说,ECMAScript里的for循环并不能创建一个局部上下文:
for ( var k in {a:1,b:2} ) {
alert(k);
}
alert(k); //尽管循环已经结束但变量k依然在当前作用域
既然变量和执行上下文相关,那变量自己应该知道它数据存储在哪里(不同上下文,可能有名字相同变量),并且知道如何访问。这种机制称为变量对象(variable object)。
变量对象(缩写为VO)是一个与执行上下文相关的特殊对象,它存储着上下文中声明的一下内容:
变量 (var , 变量声明);
函数声明 (FunctionDeclaration,缩写为FD);
函数的形参
通俗一点,我们可以用普通的ECMAScript对象来表示一个变量对象:
VO = {};
我们可以这样说:VO就是执行上下文的属性(property):
activeExecutionContext = {
VO: {
//上下文数据 (var , FD, function arguments)
}
}
当我们声明一个变量或函数的时候,就是给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 (变量初始化过程的一般行为)
║
╠══> 全局上下文变量对象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');
alert(a); // 直接访问,在VO(globalContext)里找到:"test"
alert(window['a']); // 间接通过global访问:global === VO(globalContext): "test"
alert(a === this.a); // true
var aKey = 'a';
alert(window[aKey]); // 间接通过动态属性名称访问:"test"
函数上下文变量对象
在函数上下文中,VO是不能直接访问的,此时由活动对象(activation object,缩写为AO)扮演VO的角色。
活动对象是在进入函数上下文时被创建的,它通过函数的arguments属性初始化。arguments属性的值是
Arguments对象:
AO = {
arguments: <ArgO>
};
Arguments对象是活动对象的一个属性,它包括如下属性:
- callee — 指向当前函数的引用
- length — 真正传递的参数个数
- properties-indexes (字符串类型的整数) 属性的值就是函数的参数值(按参数列表从左到右排列)。 properties-indexes内部元素的个数等于arguments.length. properties-indexes 的值和实际传递进来的参数之间是共享的。
例如:
function foo(x, y, z) {
// 声明的函数参数数量arguments (x, y, z)
alert(foo.length); // 3
// 真正传进来的参数个数(only x, y)
alert(arguments.length); // 2
// 参数的callee是函数自身
alert(arguments.callee === foo); // true
// 参数共享
alert(x === arguments[0]); // true
alert(x); // 10
arguments[0] = 20;
alert(x); // 20
x = 30;
alert(arguments[0]); // 30
// 不过,没有传进来的参数z,和参数的第3个索引值是不共享的
z = 40;
alert(arguments[2]); // undefined
arguments[2] = 50;
alert(z); // 40
}
foo(10, 20);
这个例子的代码,在当前版本的Google Chrome浏览器里有一个bug — 即使没有传递参数z,z和arguments[2]仍然是共享的。
处理上下文代码的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”,未保存的函数表达式只有在它自己的定义或递归中才能被调用。
另一个经典例子:
alert(x); // function var x = 10;
alert(x); // 10 x = 20; function x() {}; alert(x); // 20
为什么第一个alert “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;
我们可以在第二、三个alert看到这个效果。
在下面的例子里我们可以再次看到,变量是在进入上下文阶段放入VO中的。(因为,虽然else部分代码永远不会执行,但是不管怎样,变量“b”仍然存在于VO中。)
if (true) {
var a = 1;
} else {
var b = 2;
}
alert(a); // 1
alert(b); // undefined,不是b没有声明,而是b的值是undefined
关于变量
通常,各类文章和JavaScript相关的书籍都声称:“不管是使用var关键字(在全局上下文)还是不使用var关键字(在任何地方),都可以声明一个变量”。请记住,这是错误的概念:
任何时候,变量只能通过使用var关键字才能声明。
上面的赋值语句:
a = 10;
这仅仅是给全局对象创建了一个新属性(但它不是变量)。“不是变量”并不是说它不能被改变,而是指它不符合ECMAScript规范中的变量概念,所以它“不是变量”(它之所以能成为全局对象的属性,完全是因为VO(globalContext) === global,大家还记得这个吧?)。
让我们通过下面的实例看看具体的区别吧:
alert(a); // undefined
alert(b); // "b" 没有声明 b = 10;
var a = 20;
所有根源仍然是VO和进入上下文阶段和代码执行阶段:
进入上下文阶段:
VO = {
a: undefined
};
我们可以看到,因为“b”不是一个变量,所以在这个阶段根本就没有“b”,“b”将只在代码执行阶段才会出现(但是在我们这个例子里,还没有到那就已经出错了)。
让我们改变一下例子代码:
alert(a); // undefined, 这个大家都知道, b = 10;
alert(b); // 10, 代码执行阶段创建 var a = 20;
alert(a); // 20, 代码执行阶段修改
关于变量,还有一个重要的知识点。变量相对于简单属性来说,变量有一个特性(attribute):{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
使用一些调试工具(例如: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中用同样的方式访问活动对象是不可能的:在不同版本的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 })();
JavaScript:变量对象(Variable Object)的更多相关文章
- 读汤姆大叔《JavaScript变量对象》笔记
一段简单的JavaScript代码思考 先看一段简单的代码,打印结果是??为什么why?? 从上述打印结果不难看出,在打印基本变量num.函数表达式fn.函数声明fun时,就已经知道变量num.函数表 ...
- JavaScript 复制对象【Object.assign方法无法实现深复制】
在JavaScript这门语言中,数据类型分为两大类:基本数据类型和复杂数据类型.基本数据类型包括Number.Boolean.String.Null.String.Symbol(ES6 新增),而复 ...
- 图解Javascript——变量对象和活动对象
span { line-height: 1.5 } 这是由一段代码引发的思考: var laterDeclaredVar = 'I am a global variable ...'; (functi ...
- JavaScript变量作用域(Variable Scope)和闭包(closure)的基础知识
在这篇文章中,我会试图讲解JavaScript变量的作用域和声明提升,以及许多隐隐藏的陷阱.为了确保我们不会碰到不可预见的问题,我们必须真正理解这些概念. 基本定义 作用范围是个“木桶”,里面装着变量 ...
- 深入理解JavaScript系列(12):变量对象(Variable Object)
介绍 JavaScript编程的时候总避免不了声明函数和变量,以成功构建我们的系统,但是解释器是如何并且在什么地方去查找这些函数和变量呢?我们引用这些对象的时候究竟发生了什么? 原始发布:Dmitry ...
- JavaScript 执行环境(执行上下文) 变量对象 作用域链 上下文 块级作用域 私有变量和特权方法
总结自<高程三>第四章 理解Javascript_12_执行模型浅析 JS的执行环境与作用域 javascript高级程序第三版学习笔记[执行环境.作用域] 在javascript ...
- javascript 之变量对象-09
变量对象 变量对象:每个执行环境(执行上下文)都有一个对应的变量对象(variable object),环境中(执行上下文中)定义的所有变量.函数都保存在这个对象中. 在上篇中说到,当执行流执行一个函 ...
- JavaScript深入之变量对象
前言 在上篇<javascript深入之执行上下文栈>中讲到,当javascript代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution ...
- JavaScript深入之变量对象(转载)
前言 在上篇<JavaScript深入之执行上下文栈>中讲到,当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(executio ...
随机推荐
- phalcon:数据库分库,读写分离,负载均衡 系统方法执行顺序
phalcon:数据库分库,读写分离,负载均衡 系统方法执行顺序 用命名空间区分不同的数据库实例,对应代码结构上是不同的目录区分,在同一目录下基类负责初始化连接.连接来自初始化时注入的多个db服务 隐 ...
- GATK原理及流程
用GATK跑了很久的流程,但还是不知道它的原理,现在项目要深入,没有流程可用,一切都要手动,所以必须开始着手了解GATK运行的原理,必须要知道需要输入什么,基本的算法,输出是什么. 参见: GATK使 ...
- as与c++的反射机制对比
所谓反射机制(Reflection),简单来说,就是可以根据class的名称获取这个class以及其对应的实例.具体来说, 指的是我们可以于运行时加载.探知.使用编译期间完全未知的classes.换句 ...
- 【bzoj1037】生日聚会
bzoj1037 题意 \(n\)个男孩,\(m\)个女孩,共\(n+m\)个排成一排. 要求对于任意连续的一段,男孩与女孩的数目之差不超过\(k\). 求排列的方案数. \(1\leq n,m\le ...
- 使用mybatis操作mysql数据库SUM方法返回NULL解决
使用SQL语句用函数SUM叠加的时候,默认查询没有值的情况下返回的是NULL,而实际可能我们要用的是返回0 解决: SELECT SUM(total) FROM test_table 改成: SE ...
- Laravel多对多简析
首先生成两张数据表,一般要实现两张数据表之间的联系要建立第三张表,如下 数据表生成之后,生成一些测试数据,接下来就对表article_tag表进行操作 在模型文件中声明两张表之间的关系: 测试数据:
- spring来了-01-概述
思考 对象创建能否写死? 对象创建细节 对象数量 action 多个 [需要维护成员变量] service 一个 [不需要维护成员变量] dao ...
- css -- 题目汇总
1.描述下浮动和它的工作原理.模块浮动,使其脱离文档流,并且生成一个块级框.(所以父级撑不开就得到了很好的解释) 2.清除浮动的方法有那些,分别适用于什么情形.clear , 父级元素overfl ...
- Innodb中的事务隔离级别和锁的关系
前言: 我们都知道事务的几种性质,数据库为了维护这些性质,尤其是一致性和隔离性,一般使用加锁这种方式.同时数据库又是个高并发的应用,同一时间会有大量的并发访问,如果加锁过度,会极大的降低并发处理能力. ...
- Remove Duplicates from Sorted List II [LeetCode]
Given a sorted linked list, delete all nodes that have duplicate numbers, leaving only distinct numb ...