逃不出的作用域
一个变量的生命由始而终,
{ 始终逃不出它的作用域 }。
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、局部作用域
for 和 if 等语法块无法隔离作用域
// 代码开始,全局作用域开始
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 中有详述。
参考
- Everything you wanted to know about JavaScript scope
- 《JavaScript 函数式编程》