Javascript浅谈之变量及变量对象
一、变量介绍
JavaScript编程的时候总避免不了声明变量和函数,这是构成JS代码的必不可少的基本元素,但是解释器是如何声明并且在什么地方查找这些函数和变量?引用这些对象的时候究竟发生了什么?
1.变量的声明
JavaScript中任何时候,变量只能通过使用var关键字才能声明。
//下面都是正确的变量声明 var iNum = 12; var sName = "萨菲罗斯"; var foo = function(){ console.log("简单的函数");}; //下面这种忽略关键字“var”的赋值方式其实并不是声明变量,只是简单的给全局变量window添加了一个属性而已 count = 12;
这里要纠正一个错误,就是如果不使用var关键字,则声明的是全局变量,其实这种说发是错误的,它其实这仅仅是给全局对象添加了一个新的属性而已。
分辨是变量还是属性有个非常建议的方法,变量相对于简单属性来说,变量有一个特性(attribute):{DontDelete},这个特性的含义就是不能用delete操作符直接删除变量属性。
var sVar = "这是一个变量"; sAttr = "这是一个属性";
console.log(window.sVar); //>>这是一个变量 console.log(window.sAttr); //>>这是一个属性
delete window.sVar; // >>false delete window.sAttr; //>>true console.log(sVar); //>>这是一个变量 console.log(sAttr); //>>ReferenceError: sAttr is not defined
从上面例子中可以清楚看出,sAttr只是全局变量的一个属性而已。至于为什么sVar和sAttr都能通过作为window的属性进行访问,这点接下来变量对象中会详加解释。
2. hoisting 机制(变量提升)
变量声明有个很重要特点: hoisting 机制 —— 变量声明永远都会被提升至作用域的最顶端
var name = "jink"; (function(){ console.log("name :", name); //>>name : undefined var name = "fn inner jink"; console.log("name :", name); //>>name : fn inner jink })();
是不是挺疑惑为什么前一个输出,name的值居然不是“jink”。这里涉及到两个非常重要的知识点:1.变量声明的hoisting机制;2.变量的寻找原理,这需要涉及到作用域链的知识。我们在这里暂时只分析前者,对于作用域链这个,我后面将会有文章专门介绍。
其实hoisting机制非常简单,就是把变量的声明提到作用域的顶端。
var name = "jink"; (function(){ var name; //由于作用域链缘故,最顶端变量值优先被使用 console.log("name :", name); //>>name : undefined name = "fn inner jink"; console.log("name :", name); //>>name : fn inner jink })();
二、变量对象
其实通过前面例子我们突然发现,变量和执行上下文好像有着非常密切的关系。其实在ECMAScript的内部实现中,它们两者的确存在非常紧密的关系:变量自己应该清楚它的数据存储在哪,并且知道如何访问它。这种机制被称为“变量对象(variable object)”。 当程序进入某个执行上下文的伊始,都会创建一个对象变量,用来管理执行上下文中所有变量。
变量对象(缩写为VO)是一个与执行上下文相关的特殊对象,它存储着在上下文中声明的以下内容:
(1)变量 (var, 变量声明);
(2)函数声明 (FunctionDeclaration, 缩写为FD);
(3)函数的形参
举例来说,我们可以用普通的ECMAScript对象来表示一个变量对象:
VO = {}; //VO就是执行上下文的属性(property): activeExecutionContext = { VO: { // 上下文数据(var, FD, function arguments) } };
只有全局上下文的变量对象允许通过VO的属性名称来间接访问(因为在全局上下文里,全局对象自身就是变量对象,稍后会详细介绍),在其它上下文中是不能直接访问VO对象的,因为它只是内部机制的一个实现。
当我们声明一个变量或一个函数的时候,和我们创建VO新属性的时候一样没有别的区别(即:有名称以及对应的值)。
由于变量对象和执行上下文相关,且对象变量在不同执行上下文中表现的方式也不一样。
1.全局上下文
说道全局上下文,有个概念不得不提,那就是全局对象。 全局对象(Global object) 是在进入任何执行上下文之前就已经创建了的对象。这个对象只存在一份,它的属性在程序中任何地方都可以访问,全局对象的生命周期终止于程序退出那一刻。
全局对象初始创建阶段将Math、String、Date、parseInt作为自身属性,等属性初始化,同样也可以有额外创建的其它对象作为属性(其可以指向到全局对象自身)。例如,在DOM中,全局对象的window属性就可以引用全局对象自身(当然,并不是所有的具体实现都是这样):
global = { Math: <...>, String: <...> ... ... window: global //引用自身 };
当访问全局对象的属性时通常会忽略掉前缀,这是因为全局对象是不能通过名称直接访问的。不过我们依然可以通过全局上下文的this来访问全局对象,同样也可以递归引用自身。例如,DOM中的window。综上所述,代码可以简写为:
var sVar = "这是一个变量"; sAttr = "这是一个属性"; //window.sVar === global.window.sVar === global.sVar === sVar console.log(window.sVar); //>>这是一个变量 //至于sAttr,仅仅是全局变量的一个属性而已 console.log(window.sAttr); //>>这是一个属性
由上得到一个结论:在全局上下文中变量对象就是全局对象自己。
2.函数上下文
在函数执行上下文中,VO是不能直接访问的,此时由活动对象(activation object,缩写为AO)扮演VO的角色。
VO(functionContext) === AO;
活动对象是在进入函数上下文时刻被创建的,它通过函数的arguments属性初始化。arguments属性的值是Arguments对象,它是活动对象的一个属性。
AO = { arguments: <ArgO>, other//函数内部声明的变量和函数 };
由于执行上下文中的变量在内部会有一个变量统一管理,这样只要知道这个VO,我们就可以像对象一样引用这个变量,很遗憾,这个VO在函数上下文中是不可见的,所以函数内部声明的变量对外面而已是不可见的。我只需要知道,在函数上下文中,变量对象就是活动对象,活动对象中包括函数参数和函数内部声明的变量和函数即可。
三、总结
还记得前面的hoisting机制吧,因为变量对象在进入上下文后立马创建,它会将整个上下文中申明的变量统统记录下来,无论变量在上下文中的何处,然后给默认初始值undefined。当代码执行时,发现一个变量,立马到变量对象中寻找,如果是赋值,就简单的赋值;如果是引用,则看看变量存在不,不存在报错,存在则返回变量值。这个过程对于我们而言是不可见的,但给我们印象好像所有变量声明都通同提升到上下文顶端了。
这章中涉及到一个比较重要的概念:变量对象。其实我们在实际情况中根本用不到这个对象,仅仅只是借助它来理解JavaScript中的一些概念。结合它你就能很容易理解hoisting机制还有后面的作用域链相关知识。
感谢大家阅读,有什么疑问记得留言,我会及时回复与大家讨论。