一个变量的生命由始而终, { 始终逃不出它的作用域 }。 JavaScript作用域在初学阶段显得十分棘手, 尤其对有别的编程语言经验的人甚然, 他们很容易被别的编程经验带入, 下意识地期望在 JavaScript 中得到与别的语言一致的作用域体验, 然而结果却常常出乎意料。

JavaScript 从它被潦草地设计出来伊始,便踏上了一条不归路。 虽然新的 ECMAScript 标准正在快速演进, 但放眼当下,着眼于遍布市场的各式浏览器, 它们对新标准支持参差不齐。 我们仍旧需要解决作用域这个历史遗留问题。

只需要掌握一些简单的规则, 便可正确地控制作用域JavaScript 中的行为表现。

仅凭人们喜欢吃汉堡并不能断定他们就想看见牛。
当你真正喜欢一样东西,日思夜盼……不理会你的那个人就是上帝。
—— 逃出克隆岛

词汇表

  • 作用域
  • 命名空间
  • 函数作用域
  • 闭包、词法作用域
  • this、动态作用域
  • 全局、公共作用域
  • 局部、私有作用域

1、全局作用域

在代码顶层声明的变量都是全局变量

// 在顶层声明的变量
var g_value = 'hello, kitty';
// 从这里开始,任何位置都可以获取和修改 g_value
// 在 if 语句中修改 g_value
if(true) {
  g_value.replace('kitty', 'world');
}
console.log(g_value); // OK!
// 在 function 中修改 g_value
!function() {
  g_value = 'where is kitty?';
}();
console.log(g_value); // OK!

附注:在 JavaScript 中,所有全局变量都是 windows 对象的属性

// 声明一个全局变量
var g_value = 'hello, kitty';
console.log(g_value === window.g_value); // OK! -> true

// 定义一个全局函数
function g_func() {
  // ...
}
console.log(g_func === window.g_func); // OK! -> true

无论何处,只要没有使用 var 关键词声明变量, 都将得到全局变量,你没有任何理由拒绝使用 var

// 在函数中没有用 var 关键字声明变量,并立即执行这个函数
!function() {
  none_var_value = 'this is dangerous!!!';
}();
console.log(none_var_value); // Buggy, but OK!

2、局部作用域

forif 等语法块无法隔离作用域

  // 代码开始,全局作用域开始
  for(int i = 0; i < 10; i++) {
    // 仍然处于全局作用域
    console.log(i);
  }
  console.log(i); // OK! -> 10

  if(true) {
    // 依旧处于全局作用域
    var inner_if_value = 'I\'m in `if` statement';
  }
  console.log(inner_if_value); // OK!

  {
    // 啊哈,还是全局作用域
    var i_am = 'crazy';
  }
  console.log(i_am); // OK!
  // 代码结束,全局作用域结束

只有 function 内部可以隔离函数作用域

// 代码开始,全局作用域开始
!function() {
  // 局部作用域开始
  var l_value = 'I\'m from local';
  console.log(l_value); // OK!
  // 局部作用域结束
}();
console.log(l_value); // Referrence Error!
// 代码结束,全局作用域结束

示例一

// 在 `jQuery` 项目代码中,我们常看到类似的代码:
$(function() {
  // 变量缓存
  var $app = $('#app');
  // ...

  // 逻辑开始
  // ...
});
// 便是为了利用匿名函数隔离内部所有变量的作用域

示例二

// 我们还经常看到一些自调用匿名函数(SIAF)的写法:
!function() {
  // ...
}();

// 等价写法有很多种,比如下面这种:
(function() {
  // ...
})();
// 也是为了利用匿名函数隔离内部所有变量的作用域

3、函数作用域

函数的定义可以嵌套,内部作用域如下:

// 代码开始,全局作用域开始
function AAA() {
  // 函数 AAA 作用域开始
  function BBB() {
    // 函数 BBB 作用域开始
    function CCC() {
      // 函数 CCC 作用域开始
      // ...
      // 函数 CCC 作用域结束
    }
    // 函数 BBB 作用域结束
  }
  // 函数 AAA 作用域结束
}
// 代码结束,全局作用域结束

4、词法作用域

一个变量在他被声明的作用域链中都是可见的,其值由最近一层的定义所决定

var outer_value = 'outside';

!function() {
  var outer_value = 'inside';
  console.log(outer_value); // OK! -> inside

  !function() {
    var outer_value = 'inner';
    console.log(outer_value); // OK! -> inner

    outer_value = 'modified';
  }();
  console.log(outer_value); // OK! -> inside

}();
console.log(outer_value); // OK! -> outside

5、闭包

闭包与词法作用域的关系十分紧密

function create_factory(device) {
  // 1. 此处声明 product 变量
  var product = 'I created a(n) ' + device;

  // 4. 返回值是一个函数,而且该函数需要依赖 product 才能执行
  return function(model) {
    // 5. 所以 product 变量的生命会延长
    // 6. 具体延长到什么时候?你猜:)
    return product + model;
  };

  // 2. 此时 create_factory 执行完毕
  // 3. 局部变量 product 理应被销毁
}

// 执行两次 create_factory 函数,各得到一个函数作为返回值
var apple_creator = create_factory('iPhone');
var sangsung_creator = create_factory('Galaxy');

// 试想如果每次执行完毕
// product 变量都被销毁了
// 那么接下来这两行代码会得到什么结果
// 应该是 Refference Error 吧?

// 那么我们来执行一下试试看:
console.log(apple_creator('4s')); // OK! -> I created a(n) iPhone4s!
console.log(sangsung_creator('S6')); // OK! -> I created a(n) GalaxyS6!
// 事实证明,身为局部变量的 product 的确没有被销毁

当内层函数引用了外层的变量,就被称为闭包,被引用的变量就叫闭包变量闭包变量不会被立即销毁,所以不要滥用闭包

6、变量提升(Hoisting)

变量提升与词法作用域的关系也十分紧密, 只要一个变量被声明, 那么在它所处的整个词法作用域中都是可见的

// 直接访问一个从未声明的变量
console.log(none_defined); // Referrence Error!
// 先声明后访问
var defined_value = 'hello, kitty!';
console.log(defined_value); // OK!
// 先访问后声明
console.log(will_define); // undefined!
// ...
var will_define = 'hello, kitty!';
// 先访问后声明变形一
!function() {
  console.log(will_define); // undefined!
}();
var will_define = 'hello, kitty!';
// 先访问后声明变形二
// 在当前作用域声明 defined_value
var defined_value = 'hello, kitty!';

!function() {
  // 当前词法作用域找到了两个 defined_value
  // 一个来上一层,另一个在本层稍后面几行
  // 以就近原则为准,因此 defined_value 的值由本层级的定义决定
  // 但是 defined_value 的赋值,还要等到后面才被执行
  // 所以此时,defined_value 相当于已被声明,但没有赋值
  // 所以直接输出 defined_value 得到的是 undefined 而不是 Referrence Error!
  console.log(defined_value); // undefined!

  var defined_value = 'hello, world!';
  // 赋值完毕,可以正常访问 defined_value 的值了
  console.log(defined_value); // OK! -> hello, world!
}();

// 函数执行结束,退回到全局作用域,defined_value 的值并没有变化
console.log(defined_value); // OK! ->  hello, kitty!

变量提升有一个特例,就是当声明的变量是函数的时候

// 方式一:使用 var 声明函数

// 先访问后声明
console.log(func === undefined); // OK! -> true
func(); // TypeError, func is not a function.

var func = function() {
  // ...
}
// 方式二:不使用 var 声明函数

// 先访问后声明
console.log(func === undefined); // OK! -> false
func(); // OK!

function func() {
  // ...
}

// 上述两种情况是由 `ECMAScript` 标准决定的
// 使用时要注意区别

上面的一切是因为 JavaScript 在定义它们的作用域里运行, 而不是在执行它们的作用域里运行。

7、动态作用域

JavaScript 中,this 指针的指向是动态绑定的, 较之 C++/Java 中的 this 稍有出入

// 直接在全局作用域中访问 `this`
console.log(this === window); // OK! -> true
// 直接在全局作用域中访问 `this`
console.log(this === window); // OK! -> true

此处简述动态作用域和 this 的用法,在另一篇关于 prototype-new-constructor 中有详述。

参考