返回列表

转载:JavaScript变量作用域

默认分类 2014/07/07 00:23

原文地址:http://heroicyang.com/2013/07/22/javascript-scope-sequel/

JavaScript变量作用域

所谓作用域,也就是变量和函数起作用的区域,不同的语言有着不同的实现。而在 JavaScript 中,这也是往往让人迷糊的地方,也是 JavaScript 中必须理解的特性之一。

首先来看看下面的代码:

for (var i = 0; i < 10; i++) {
    var foo = "bar";
}

function test() {
  console.log(i); // 10
  console.log(foo); // "bar"
}
test();

从运行结果不难看出,变量i在循环体结束之后仍然可以访问。而诸如 Java、C# 等语言中,循环结束之后便不能再访问到循环体中的变量了。继续看下面代码:

var name = "heroic";

function foo() {
  var name = "foo";
  console.log(name); // "foo"
}

foo();
console.log(name); // "heroic"

结合两段代码可以知道,在 JavaScript 中是不存在块级作用域的,只存在函数作用域(也称“本地作用域”)和全局作用域。
而作用域中的变量我们分别称其为:局部变量和全局变量。

前辈们(前端的长辈们,嚯嚯)常常反复在说:
"全局变量是魔鬼啊,魔鬼啊。。。 " (回声): "那到底是嘛原因呢?"
咱接着往下说。

_ JavaScript 中如果不使用var关键字来声明变量,则该变量会成为全局变量。
_ 在整个作用域中,都可以访问并修改这些全局变量,不可控,BUG 随之而来。 _ 局部变量的访问优先级高于全局变量,所以在某种特殊情况下(JavaScript 变量声明提前特性),可能得不到我们想要的结果。

function foo() {
  name = "foo";
}

function bar() {
  console.log(name); // "foo"
  name = "bar";
}

foo();
bar();
console.log(name); // "bar"

上面这个例子很好理解,由于在foo中没有使用var关键字来声明name变量,所以可以在整个全局作用域中访问并修改该变量的值,在实际项目中必然会混乱不堪,引入不可控的 BUG 等等。接下来看看这个可能对于初学者来说有点迷糊的例子。

var name = "heroic";

function foo() {
  console.log(name);
  var name = "foo";
  console.log(name);
}

foo();
// undefined
// "foo"

这就是前面提到的 "声明提前",JavaScript 中变量和函数以及函数的参数的申明都是在一个类似于预编译的时期就做了,而在运行时期才是创建变量赋值表达式和函数表达式等。所以上面的代码等同于:

var name = "heroic";

function foo() {
  var name;
  console.log(name);
  name = "foo";
  console.log(name);
}

foo();

JavaScript变量作用域(续)

上篇,已经大致明确了以下几点:

  1. JavaScript 没有块级作用域,只有函数 (局部) 作用域和全局作用域
  2. 函数中未使用var关键字声明的变量会成为全局变量
  3. 同名时局部变量访问优先级高于全局变量4. JavaScript 具有变量声明提前的特性

接下来根据上篇留下的最后一段代码,继续谈谈变量作用域。

var name = 'global';

function foo() {
  var name = 'foo';
  bar();
}

function bar() {
  console.log(name);
}

foo();

这段代码最终会在控制台打印出"global",而并非"foo"。可以看出,函数运行时能访问到的作用域是它被定义时的作用域,不是被调用时的作用域。

每当谈及 JavaScript 作用域的时候,基本上都会提到“词法作用域”、“执行环境”、“活动对象”、“作用域链”这几个概念,而了解这些概念将有助于理解 JavaScript 中的闭包。我也谈谈我对此的理解,如误欢迎指正,不胜感激。

词法作用域

词法作用域,也称静态作用域,也即是说函数的作用域是定义时决定的,而非运行时。最开始的代码就阐明了这一点,所以在源码时期就可以通过分析得出一个函数的作用域。JavaScript 正是基于词法作用域的语言。

执行环境

JavaScript 需要一个环境来运行,比如客户端的浏览器和服务端的 Node.js。而执行环境有分为全局执行环境局部执行环境

全局执行环境

在浏览器中,全局执行环境为window对象。因此当 JavaScript 代码运行时,所有的全局变量和函数都作为了window对象的属性和方法创建。全局执行环境关联了一个作用域链,包含了全局执行环境中的变量对象。

局部执行环境、活动对象

每个函数在定义时,都会关联一个初始的作用域链,将当前执行环境作用域链上的变量对象附加到这个关联的作用域链上。每个函数都是有自己的执行环境的,也就是局部执行环境。当一个函数被调用,进入局部执行环境。随之创建了活动对象,包含arguments和局部变量,然后将其附加到作用域链的前端(即下标为 0)。在函数执行之后,退出局部执行环境,把控制权交给之前的执行环境(可能是全局执行环境,也有可能是另一个局部执行环境)。

作用域链

前面已经反复提到了作用域链,也基本解释了作用域链,它差不多等同于一个对象列表或链表。为了更好的理解作用域链,还是结合最开始的那段代码来进行解释,不然有点不是很好理清楚。

var name = 'global';

function foo() {
  var name = 'foo';
  bar();
}

function bar() {
  console.log(name);
}

foo();

1. 代码运行开始,在全局执行环境中,全局环境关联了一个作用域链,变量对象包含namefoobar。然后foo函数和bar函数也各自关联了自己作用域链。

globalScopeChain = {
    name: 'global',
    foo: [Function],
    bar: [Function]
};

fooScopeChain = [globalScopeChain]
barScopeChain = [globalScopeChain]

2. 调用foo函数,进入foo函数的局部执行环境,创建活动对象包含argumentsname,并将活动对象添加到作用域链的前端。

fooScopeChain = [
  {
    arguments: [],
    name: 'foo'
  },
  {
    name: 'global',
    foo: [Function],
    bar: [Function]
  }
]

3. 调用bar函数,进入bar函数的局部执行环境,创建活动对象包含arguments,然后也将活动对象添加到作用域链的前端。

barScopeChain = [
  {
    arguments: []
  },
  {
    name: 'global',
    foo: [Function],
    bar: [Function]
  }
]

而变量的查找,就是在整个作用域链上进行,从第一个对象开始,直到作用域链的顶端(即全局)。这也就是实例代码中打印出"global"的原因。