本文深入解析 JavaScript 的作用域机制,涵盖函数作用域与全局作用域的区别,以及通过作用域链访问变量的规则。重点讲解闭包的形成条件、内部状态保持原理,并通过经典循环引用案例演示如何利用闭包解决变量共享问题,帮助开发者理解函数嵌套与变量访问权限。
JavaScript 作用域与闭包概述
作用域的嵌套会构建出作用域链,而函数的嵌套则会生成闭包。这两者是 JavaScript 语言区别于其他编程语言的核心特性之一。
JavaScript 作用域类型
在 JavaScript 中,主要存在两种作用域类型:函数作用域和全局作用域。
在函数内部声明的变量以及该函数的参数共享同一个作用域,这被称为函数作用域。以下是一个展示函数作用域的基础示例:
代码如下:
function foo() {
var bar = 1;
{
var bar = 2;
}
return bar; // 2
}
与 C 等具备块级作用域的语言不同,此处代码将始终返回数值 2。
全局作用域在浏览器环境中对应 window 对象(在 Node.js 环境中则对应 global 对象):
代码如下:
var bar = 1;
function foo() {}
alert(window.bar); // 1
alert(window.foo); // "function foo() {}"
变量 bar 和函数 foo 均归属于全局作用域,且均为 window 对象的属性。
作用域链的访问机制
在 JavaScript 中查询变量时,引擎会从当前本地变量和参数开始,逐级向上遍历作用域链,直至到达全局作用域。
代码如下:
var scope = 0, zero = "global-scope";
(function(){
var scope = 1, one = "scope-1";
(function(){
var scope = 2, two = "scope-2";
(function(){
var scope = 3, three = "scope-3";
// scope-3 scope-2 scope-1 global-scope
console.log([three, two, one, zero].join(" "));
console.log(scope); // 3
})();
console.log(typeof three); // undefined
console.log(scope); // 2
})();
console.log(typeof two); // undefined
console.log(scope); // 1
})();
console.log(typeof one); // undefined
console.log(scope); // 0
在最内层的函数中,各层级的变量均可被逐级访问并输出。而在倒数第二层的函数中,由于无法访问到变量 three,因此输出结果为 undefined。
可以通俗地理解这一过程:当你需要花钱购物时,首先会检查自己的钱包;如果钱不够,会向父亲索取;若父亲也没有,再向祖父求助。反之,当父亲缺钱时,他并不会反向向你索取。
闭包的形成与特性
在函数内部定义另一个函数,这种结构称为函数嵌套。函数的嵌套操作会形成闭包。
闭包与作用域链相互依存,函数嵌套在建立多层级作用域链的同时,也创建了闭包结构。
代码如下:
function bind(func, target) {
return function() {
func.apply(target, arguments);
};
}
如何理解闭包的概念?
外部函数无法访问内嵌函数的内部逻辑。同时,外部函数也不能访问内嵌函数的参数及局部变量。相反,内嵌函数可以访问外部函数的参数和变量。换言之,内嵌函数包含了外部函数的作用域。结合前文的作用域链示例,我们可以从闭包的角度重新审视:
代码如下:
var scope = 0, zero = "global-scope";
(function(){
var scope = 1, one = "scope-1";
(function(){
var scope = 2, two = "scope-2";
(function(){
var scope = 3, three = "scope-3";
// scope-3 scope-2 scope-1 global-scope
console.log([three, two, one, zero].join(" "));
console.log(scope); // 3
})();
console.log(typeof three); // undefined
console.log(scope); // 2
})();
console.log(typeof two); // undefined
console.log(scope); // 1
})();
console.log(typeof one); // undefined
console.log(scope); // 0
最内层的函数能够访问其内部以及外部定义的所有变量。然而,倒数第二层的函数无法访问最内层的变量。此外,最内层对 scope 赋值为 3 的操作,并未对外部同名变量产生任何影响。
闭包的状态保持与应用
我们可以从另一个角度来理解闭包:
每次调用外部函数时,都会创建一个新的内嵌函数实例。在创建时刻,外部函数的作用域(包含本地变量、参数等上下文信息)会成为该内嵌函数对象内部状态的一部分。即使外部函数执行完毕并退出,这种状态依然保持。请看以下示例:
代码如下:
var i, list = [];
for (i = 0; i < 2; i += 1) {
list.push(function(){
console.log(i);
});
}
list.forEach(function(func){
func();
});
执行结果将输出两次 "2",而非预期的 "1" 和 "2"。这是因为 list 数组中的两个函数所访问的变量 i,均指向其外层作用域的同一个变量实例。
我们可以通过修改代码,利用闭包机制来解决此问题:
代码如下:
var i, list = [];
for (i = 0; i < 2; i += 1) {
list.push((function(j){
return function(){
console.log(j);
};
})(i));
}
list.forEach(function(func){
func();
});
外层的“立即执行函数”接收参数 i,并在内部以参数 j 的形式存在。该参数 j 与返回的内层函数中使用的名称 j 指向同一个引用。当外层函数执行并退出后,参数 j(此时保存了 i 的当前值)作为内层函数状态的一部分被保留下来。