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机制还有后面的作用域链相关知识。

  感谢大家阅读,有什么疑问记得留言,我会及时回复与大家讨论。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。