JavaScript变量作用域探究

标签: javascript 变量 作用域 | 发表时间:2014-05-27 00:13 | 作者:zhangjiahao8961
出处:http://www.iteye.com

 

JavaScript变量作用域探究

前段时间,在coding的时候,碰到了当时感觉不可思议的问题。简化下问题,大体是这样的:

         if(false){

         var a = 12;

}

console.log(a);       // undefined

当时对这个问题很困惑,回去恶补了下JS变量的相关知识,发现还是自己的基础知识掌握的不够扎实,于是决定探究下JS变量的相关机制。如果你对上面的结果也有疑问,我相信看完下面的讲解之后,再看这问题,天空飘来五个字:“那都不叫事”。

一. 变量

在各种编程语言中我们都接触过变量的概念,什么是变量,它是在内存中分配的一段空间。在JavaScript中,在使用变量之前需要用var关键字进行声明。下面是各种变量声明的写法:

var a = 12;

var b;

var c,d,e=’hello’;

f = 520

console.log(a); //12

console.log(b); //undefined

console.log(c); // undefined

console.log(d); // undefined

 
 

 console.log(e); // hello

console.log(f); //520

你会发现,f没有使用var进行声明,就直接赋值使用了。在JS的非严格模式下,这样做是允许的。这样做和用var进行声明有什么区别呢?看下面的代码

    function test(){

             var a = 100;

             b = 200;

}

console.log(a); //error

console.log(a); //200

上述代码表明,在函数内部不用var声明,直接进行赋值,实际上相当于声明了一个全局变量,所以在函数外可以使用该变量。什么是全局变量和局部变量,这就牵扯到下面的变量作用域的知识。

二. 变量作用域

1. 函数作用域

变量都有它的作用范围,即作用域。全局变量拥有全局作用域,在JavaScript代码中的任何地方都可以使用。但在函数内部声明的变量只能在函数体内有定义,它们是局部变量,作用域是局部性的。函数的参数也是局部变量。

在一些类似C,JAVA的编程语言中,花括号内的每一段代码都具有各自的作用域,而且变量在声明它们的代码段之外是不可见的,我们称之为块级作用域(block scope)。而JavaScript中,变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。我们称之为函数作用域。

function test(o){

         var i = 0;              //i在整个函数体内均是有定义的

         if( typeof o == “object”){

var j = 0;//j在函数体内有定义的,不仅限于这个代码段

for(var k=0;k<10;k++){ //k在函数体内有定义,不限于循环

                                                     console.log(k);

}

console.log(k);

}

console.log(j);

}

2. 声明提前(Hoisting)

上面的例子中,如果if的条件一直是fasle,那if代码段中的变量声明了吗?回到文章开始的那个问题

         if(false){

         var a = 12;

}

console.log(a); // undefined

         事实证明,变量a 已经声明成功了,但是还没有赋值。这是为什么呢?JavaScript的函数作用域是指在函数内声明的所有变量在函数体内是可见的,有意思的是这些变量在声明之前就已经可用。JavaScript的这个特性称为声明提前(hoisting)。即JavaScript函数里声明的所有变量(但不涉及赋值)都被“提前”

至函数体的顶部。看下面的代码:

 

                                            var scope = “global”;

                                            function f(){

                                                     console.log(scope);  // undefined

                                                     var scope = “local”;

                                                     console.log(scope); // local

}

         由于JavaScript的声明提前机制,上面代码的实际执行情况如下:

                                                    

var scope = “global”;

                                            function f(){

var scope;// 函数体内的var声明被提前到函数体的顶部

                                                     console.log(scope);  // undefined

                                                     scope = “local”;

                                                     console.log(scope); // local

}

三. 执行上下文(Execution Context)

     1. 执行上下文栈(Execution Context Stack)

         当控制器转入 ECMA 脚 本的可执行代码时,控制器会进入一个执行环境。当前活动的多个执行环境在逻辑上形成一个栈结构。该逻辑栈的最顶层的执行环境称为当前运行的执行环境。任何 时候,当控制器从当前运行的执行环境相关的可执行代码转入与该执行环境无关的可执行代码时,会创建一个新的执行环境。新建的这个执行环境会推入栈中,成为 当前运行的执行环境。执行环境包含所有用于追踪与其相关的代码的执行进度的状态。

在ECMASscript中的代码有三种类型:global, function和eval。

每一种代码的执行都需要依赖自身的上下文。当然global的上下文可能涵盖了很多的function和eval的实例。函数的每一次调用,都会进入函数执行中的上下文,并且来计算函数中变量等的值。eval函数的每一次执行,也会进入eval执行中的上下文,判断应该从何处获取变量的值。

注意,一个function可能产生无限的上下文环境,因为一个函数的调用(甚至递归)都产生了一个新的上下文环境。

function foo(bar) {}

 

foo(10); // 调用相同的function,每次都会产生3个不同的上下文

foo(20); //(包含不同的状态,例如参数bar的值)

foo(30);

 

一个执行上下文可以激活另一个上下文,就好比一个函数调用了另一个函数(或者全局的上下文调用了一个全局函数),然后一层一层调用下去。逻辑上来说,这种实现方式是栈,我们可以称之为上下文堆栈。

激活其它上下文的某个上下文被称为 调用者(caller) 。被激活的上下文被称为被调用者(callee) 。被调用者同时也可能是调用者(比如一个在全局上下文中被调用的函数调用某些自身的内部方法)。

当一个caller激活了一个callee,那么这个caller就会暂停它自身的执行,然后将控制权交给这个callee. 于是这个callee被放入堆栈,称为进行中的上下文[running/active execution context]. 当这个callee的上下文结束之后,会把控制权再次交给它的caller,然后caller会在刚才暂停的地方继续执行。在这个caller结束之后,会继续触发其他的上下文。一个callee可以用返回(return)或者抛出异常(exception)来结束自身的上下文。

如下图,所有的ECMAScript的程序执行都可以看做是一个执行上下文堆栈[execution context (EC) stack]。堆栈的顶部就是处于激活状态的上下文。

   


                                    执行上下文栈

 

当一段程序开始时,会先进入全局执行上下文环境[global execution context], 这个也是堆栈中最底部的元素。此全局程序会开始初始化,初始化生成必要的对象[objects]和函数[functions]. 在此全局上下文执行的过程中,它可能会激活一些方法(当然是已经初始化过的),然后进入他们的上下文环境,然后将新的元素压入堆栈。在这些初始化都结束之后,这个系统会等待一些事件(例如用户的鼠标点击等),会触发一些方法,然后进入一个新的上下文环境。

有一个函数上下文“EC1″和一个全局上下文“Global EC”,下图展现了从“Global EC”进入和退出“EC1″时栈的变化:



  

执行上下文栈的变化

ECMAScript运行时系统就是这样管理代码的执行。如上所述,栈中每一个执行上下文可以表示为一个对象。让我们看看上下文对象的结构以及执行其代码所需的状态(state)。

 

     2. 执行上下文(Execution Context)

一个执行的上下文可以抽象的理解为object。每一个执行的上下文都有一系列的属性(我们称为上下文状态),他们用来追踪关联代码的执行进度。这个图示就是一个context的结构。


 

上下文结构

除了这3个所需要的属性(变量对象(variable object),this指针(this value),作用域链(scope chain) ),执行上下文根据具体实现还可以具有任意额外属性。接着,让我们仔细来看看这三个属性。

 

          2.1变量对象(Variable Object)

 

A variable object is a scope of data related with the execution context. It’s a special object associated with the context and which stores variables and function declarations are being defined within the context.

 

变量对象(variable object) 是与执行上下文相关的 数据作用域(scope of data) 。它是与上下文关联的特殊对象,用于存储被定义在上下文中的 变量(variables) 和 函数声明(function declarations) 。

变量对象(Variable Object)是一个抽象的概念,不同的上下文中,它表示使用不同的object。例如,在global全局上下文中,变量对象也是全局对象自身[global object]。(这就是我们可以通过全局对象的属性来指向全局变量)。

让我们看看下面例子中的全局执行上下文情况:

var foo = 10;

 

function bar() {} // // 函数声明

(function baz() {}); // 函数表达式

 console.log(

  this.foo == foo, // true

  window.bar == bar // true

);

 console.log(baz); // 引用错误,baz没有被定义

全局上下文中的变量对象(VO)会有如下属性:



  

                                   全局变量对象

 

如上所示,函数“baz”如果作为函数表达式则不被不被包含于变量对象。这就是在函数外部尝试访问产生引用错误(ReferenceError) 的原因。请注意,ECMAScript和其他语言相比(比如C/C++),仅有函数能够创建新的作用域。在函数内部定义的变量与内部函数,在外部非直接可见并且不污染全局对象。

那函数以及自身的变量对象又是怎样的呢?在一个函数上下文中,变量对象被表示为活动对象(activation object)。

      2.2活动对象(activation object)

 

当函数被调用者激活,这个特殊的活动对象(activation object) 就被创建了。它包含普通参数(formal parameters) 与特殊参数(arguments)对象(具有索引属性的参数映射表)。活动对象在函数上下文中作为变量对象使用。

即:函数的变量对象保持不变,但除去存储变量与函数声明之外,还包含以及特殊对象arguments 。考虑下面的情况:

function foo(x, y) {

  var z = 30;

  function bar() {} // 函数声明

  (function baz() {}); // 函数表达式

}

 

foo(10, 20);

“foo”函数上下文的下一个激活对象(AO)如下图所示:



  

                                        激活对象

同样道理,function expression不在AO的行列。

我们接下去要讲到的是第三个主要对象。众所周知,在ECMAScript中,我们会用到内部函数[inner functions],在这些内部函数中,我们可能会引用它的父函数变量,或者全局的变量。我们把这些变量对象成为上下文作用域对象[scope object of the context]. 类似于之前讨论的原型链[prototype chain],我们在这里称为作用域链[scope chain]。

 

          2.3作用域链(Scope Chains)

 

A scope chain is a list of objects that are searched for identifiers appear in the code of the context.

作用域链是一个 对象列表(list of objects) ,用以检索上下文代码中出现的 标识符(identifiers) 。

作用域链的原理和原型链很类似,如果这个变量在自己的作用域中没有,那么它会寻找父级的,直到最顶层。

标示符[Identifiers]可以理解为变量名称、函数声明和普通参数。例如,当一个函数在自身函数体内需要引用一个变量,但是这个变量并没有在函数内部声明(或者也不是某个参数名),那么这个变量就可以称为自由变量[free variable]。那么我们搜寻这些自由变量就需要用到作用域链。

在一般情况下,一个作用域链包括父级变量对象(variable object)(作用域链的顶部)、函数自身变量VO和活动对象(activation object)。不过,有些情况下也会包含其它的对象,例如在执行期间,动态加入作用域链中的—例如with或者catch语句。[译注:with-objects指的是with语句,产生的临时作用域对象;catch-clauses指的是catch从句,如catch(e),这会产生异常对象,导致作用域变更]。

当查找标识符的时候,会从作用域链的活动对象部分开始查找,然后(如果标识符没有在活动对象中找到)查找作用域链的顶部,循环往复,就像作用域链那样。

var x = 10;

 

(function foo() {

  var y = 20;

  (function bar() {

    var z = 30;

    // "x"和"y"是自由变量

    // 会在作用域链的下一个对象中找到(函数”bar”的互动对象之后)

    console.log(x + y + z);

  })();

})();

我们假设作用域链的对象联动是通过一个叫做__parent__的属性,它是指向作用域链的下一个对象。这可以在Rhino Code中测试一下这种流程,这种技术也确实在ES5环境中实现了(有一个称为outer链接).当然也可以用一个简单的数据来模拟这个模型。使用__parent__的概念,我们可以把上面的代码演示成如下的情况。(因此,父级变量是被存在函数的[[Scope]]属性中的)。



  

                                   作用域链

在代码执行过程中,如果使用with或者catch语句就会改变作用域链。with语句用于临时扩展作用域链,

with(object)

statement

这条语句将object添加到作用域链的头部,然后执行statement,最后把作用域链恢复到原始状态。

而这些object都是一些简单对象,他们也会有原型链。这样的话,作用域链会从两个维度来搜寻。

  1. 首先在原本的作用域链
  2. 每一个链接点的作用域的链(如果这个链接点是有prototype的话)

我们再看下面这个例子:

Object.prototype.x = 10;

var w = 20;

var y = 30;

          console.log(x); // 10

          (function foo() {

                var w = 40;

                 var x = 100;  // "x" 可以从"Object.prototype"得到,注意值是10哦

     with ({z: 50}) {  // 因为{z: 50}是从它那里继承的

    console.log(w, x, y , z); // 40, 10, 30, 50

  }   // 在"with"对象从作用域链删除之后

 

 

  // x又可以从foo的上下文中得到了,注意这次值又回到了100哦

  // "w" 也是局部变量

  console.log(x, w); // 100, 40

 

//在浏览器里我们可以通过如下语句来得到全局的w值

  console.log(window.w); // 20

 

})();

我们就会有如下结构图示。这表示,在我们去搜寻__parent__之前,首先会去__proto__的链接中。



  

with增大的作用域链

注意,不是所有的全局对象都是由Object.prototype继承而来的。上述图示的情况可以在SpiderMonkey中测试。

         与with语句类似,当try块发生错误时,程序流程自动转入catch块,并将异常对象推入作用域前端的一个变量对象。在catch块中,函数的所有局部变量都被放在第二个作用域链中。

四. 最佳实践

  1. 尽量不要使用with语句,使用with语句会使变量的作用域链增大,使得搜寻变量的时间变长,影响程序的效率。在Ecmascript5严格模式下,已经禁用了with语句。
  2. 使用变量的时候尽量使用var进行声明。在函数体内,将声明的局部变量尽量都放在函数的顶部。


已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [javascript 变量 作用域] 推荐:

JavaScript变量作用域

- ~Wing~ - 博客园-首页原创精华区
变量作用域是程序中定义这个变量的区域. 先贴一段代码,如果读者对代码的输出并不感到困惑就不用往下面读了. /* 代码1 */ var scope = "global "; function checkScope() {. document.write(scope); //输出"local". //输出"childLocal".

[译]Javascript 作用域和变量提升

- - justjavac(迷渡)
下面的程序是什么结 果. 这可能是陌生的,危险的,迷惑的,同样事实上也是非常有用和印象深刻的javascript语言特性. 对于这种表现行为,我不知道有没有一个标准的称呼,但是我喜欢这个术语:“Hoisting (变量提升)”. 这篇文章将对这种机制做一个抛砖引玉式的讲解,但是,首先让我们对javascript的作用域有一些必要的理解.

JavaScript变量作用域探究

- - JavaScript - Web前端 - ITeye博客
JavaScript变量作用域探究. 前段时间,在coding的时候,碰到了当时感觉不可思议的问题. 当时对这个问题很困惑,回去恶补了下JS变量的相关知识,发现还是自己的基础知识掌握的不够扎实,于是决定探究下JS变量的相关机制. 如果你对上面的结果也有疑问,我相信看完下面的讲解之后,再看这问题,天空飘来五个字:“那都不叫事”.

javascript的词法作用域

- 恋上女人香 - 断桥残雪部落格
大家应该写过下面类似的代码吧,其实这里我想要表达的是有时候一个方法定义的地方和使用的地方会相隔十万八千里,那方法执行时,它能访问哪些变量,不能访问哪些变量,这个怎么判断呢. 这个就是我们这次需要分析的问题——词法作用域. 词法作用域:变量的作用域是在定义时决定而不是执行时决定,也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域.

JavaScript 函数、作用域和继承

- - 幸福收藏夹
关于函数、作用域和继承,可以写的非常多. 不过和 JavaScript 类型浅解 一样,是写给初学者看的,我们着重从简单的来. 当然,即使用「简单」来描述,这也是 JavaScript 中最不容易懂的点之一. 如你所见, function fn(){},像这个声明式函数. 虽然初学看起来有点乱,不过我想说的是,你总会知道如何用的,现在知道就可以了.

Javascript诞生记

- Milido - 阮一峰的网络日志
二周前,我谈了一点Javascript的历史. 今天把这部分补全,从历史的角度,说明Javascript到底是如何设计出来的. 只有了解这段历史,才能明白Javascript为什么是现在的样子. 我依据的资料,主要是Brendan Eich的自述. "1994年,网景公司(Netscape)发布了Navigator浏览器0.9版.

JavaScript,你懂的

- dylan - keakon的涂鸦馆
经常有人问我,JavaScript应该怎么学. 先学基本语法,如果曾学过C等语言,应该1小时内就能掌握了. 再去使用内置的函数、方法和DOM API,熟悉它能干什么;而在学习DOM API的过程中,你还不得不与HTML和CSS打交道. 然后弄懂匿名函数和闭包,学会至少一个常用的JavaScript库(例如jQuery).

Javascript 里跑Linux

- rockmaple - Shellex&#39;s Blog
牛逼到暴的大拿 Fabrice Bellard,用Javascript实现了一个x86 PC 模拟器,然后成功在这个模拟器里面跑Linux(请用Firefox 4 / Google Chrome 11打开,Chome 12有BUG). 关于这个东西… 伊说 “I did it for fun“,大大啊大大啊….

高效 JavaScript

- xtps - ITeye论坛最新讨论
传统上,网页中不会有大量的脚本,至少脚本很少会影响网页的性能. 但随着网页越来越像 Web 应用程序,脚本的效率对网页性能影响越来越大. 而且使用 Web 技术开发的应用程序现在越来越多,因此提高脚本的性能变得很重要. 对于桌面应用程序,通常使用编译器将源代码转换为二进制程序. 编译器可以花费大量时间优化最终二进制程序的效率.

你得学JavaScript

- 蒋冰 - 伯乐在线 -博客
  注:本文由 敏捷翻译 - 蒋少雄 翻译自 Kenny Meyers 的博文.   如果三年前你问我应该学什么语言,我会告诉你是Ruby.   如果你现在想学一门语言的话,你应该学习JavaScript..   我认为,每一位Web开发人员都应该学习JavaScript. 目前推出的许多新技术都支持这个观点.