JavaScript详细解析之作用域链
作用域链(Scope Chain)是JavaScript的一种执行机制,他的存在决定了变量和函数的可见性和访问规则。了解作用域链的构成和作用将有助于我们更好的理解JavaScript的执行机制和变量作用域,更准确的编写代码。
1.全局作用域和局部作用域
在JavaScript中,每个变量和函数都有自己的作用域。作用域指变量或者函数的可访问范围。我们可以将作用域分为两种:全局作用域和局部作用域。
全局作用域是在代码的顶层声明的变量和函数,它可以被程序的任何部分访问。具有全局作用域的变量和函数在程序的任何地方都可以访问,而不需要在其他地方声明。
// 全局作用域
var a = 1;
function test(){
console.log(a); // 访问全局变量a
}
test(); // 1
而局部作用域是指在函数或者块级作用域中声明的变量和函数,它所定义的变量和函数仅在局部作用域内起作用,无法从外部直接访问。
function test(){
// 局部作用域
var b = 2;
console.log(b);
}
test(); // 2
console.log(b); // 报错:b未定义
2.作用域链的构成
作用域链是指一组对象的列表,每个对象都是当前执行上下文的一部分。JavaScript中每次执行函数时都会创建一个新的执行上下文,并建立一个新的作用域链,这个链表中第一个对象始终是当前函数的变量对象,第二个对象是是外部函数的变量对象,以此类推,最后一个为全局对象,称为作用域链的“顶端”。
作用域链中的变量和函数按照它们在代码中出现的顺序排列。当访问变量时,JavaScript引擎从当前的变量对象开始向上遍历作用域链,直到找到变量名匹配的对象。如果一直到全局对象都没有找到匹配的变量,那么这个变量就被视为未定义的。
下面的例子中,让我们看看作用域链的构成:
var a = 1;
function outer(){
var b = 2;
function inner(){
var c = 3;
console.log(a + b + c); // 6
}
inner();
}
outer();
在这个例子中,当函数inner访问变量a的时候,它会首先查找内部执行上下文中的变量对象,如果没有找到就向上遍历作用域链并查找。由于变量对象是按照定义顺序排列的,所以inner的变量对象中并没有找到a,接下来是outer的变量对象,这个对象中有变量b,因此内部函数可以使用b。最后到全局变量对象,找到了a变量。因此内部函数中的 a + b + c 语句将输出6。
3.闭包
闭包(Closure)是指函数与其相关的引用环境组合的包装体,它实现了JavaScript中的诸如函数式编程等高级概念。闭包可以访问到其定义的外部函数中的变量,并保留该变量的值,使其即使在外部函数调用完毕后,依然能够被内部函数访问和使用。
function outer(){
var b = 2;
function inner(){
var c = 3;
console.log(a + b + c); // 6
}
return inner;
}
var foo = outer();
foo();
在这个例子中,调用outer函数返回了inner函数,然后将其保存在变量foo中。当执行foo()时,闭包中的环境存储在内存中,并且在变量b的值被保留过来。因此,在内部函数中,变量a、b、c都可以访问且值为1、2和3。这就是内部函数通过闭包访问外部变量的原理。我们可以看到,闭包可以很好地解决函数封装和数据保护问题,用于创建模块化代码。
3.1 循环中的闭包
有时候我们需要在循环中创建多个闭包。例如:
var funcs = [];
for(var i = 0; i < 3; i++){
funcs[i] = function(){
console.log("i is " + i);
}
}
for(var j = 0; j < 3; j++){
funcs[j]();
}
在这个例子中,我们期望输出0、1和2,但实际上会输出3个2。这是为什么呢?在循环中,我们创建了3个函数并将其保存在数组中,这些函数都是闭包,可以访问循环内部的变量i。但由于闭包的特殊性质,它可以访问的变量在它外部成为一个整体。因此,当遍历一次循环后,i的值被赋值为3,而所有的闭包都共享这个变量,在三次调用中最终都输出了3。
解决方法之一是使用一些可以创建函数副本的技术。例如:
var funcs = [];
for(var i = 0; i < 3; i++){
funcs[i] = (function(index){
return function(){
console.log("i is " + index);
}
})(i);
}
for(var j = 0; j < 3; j++){
funcs[j]();
}
使用立即执行函数创建了函数的副本,并将i的值传递给函数。这个立即执行函数既返回了一个新的函数对象,也为index参数保存了当前循环的迭代值,确保了在调用闭包时其值是正确的。这样,解决了在一个循环中创建多个闭包的问题。
4.总结
作用域链是JavaScript的一个关键概念,它非常重要。在编写JavaScript代码时正确使用作用域链是至关重要的,可以避免代码错误和内存泄漏。同时,理解闭包的运作原理也为模块化和高级编程控制提供了强大的支持。
在应用程序的不同部分使用适当的作用域链设置和闭包可以确保所有变量和函数都能正确地使用,并保证其在应用程序的生命周期内正确工作。