1. 基本概念
JavaScript闭包是一种特殊的JavaScript对象。它由函数及其创建时可访问的所有变量组成,即使在该函数已经执行完毕后,这些变量仍然可以被访问。
为了更好地了解JavaScript闭包,有必要回顾一下JavaScript的作用域和作用域链。
1.1 作用域
JavaScript中的作用域分为全局作用域和函数作用域。在全局作用域中定义的变量可以在任何地方访问,而在函数作用域中定义的变量只能在该函数内部访问。
var global = "I'm a global variable";
function foo() {
var local = "I'm a local variable";
}
console.log(global); // "I'm a global variable"
console.log(local); // ReferenceError: local is not defined
1.2 作用域链
JavaScript采用一种叫做作用域链的机制来解析变量,它是函数作用域的一个重要特征。
每当函数被调用时,都会创建一个新的执行上下文,该执行上下文中包括一个作用域链。作用域链由当前执行上下文的变量对象和外部环境的活动对象构成,后者是指调用该函数时创建的执行上下文。
当访问一个变量时,JavaScript会先从当前执行上下文的变量对象中查找,如果没有找到,就会向上遍历作用域链,直到找到该变量或者遍历完整个作用域链。
var global = "I'm a global variable";
function foo() {
var local = "I'm a local variable";
console.log(global); // "I'm a global variable"
console.log(local); // "I'm a local variable"
}
foo();
在上面的例子中,当函数foo被调用时,会创建一个新的执行上下文,其中包括一个作用域链。当访问变量global时,JavaScript首先从foo的变量对象中查找,没有找到,就会向上遍历作用域链,找到了全局变量global。当访问变量local时,JavaScript会从foo的变量对象中查找,找到了local变量,就不会再向上遍历作用域链。
2. 实战演练
下面通过几个实例来进一步了解JavaScript闭包。
2.1 实例1:基本用法
function outer() {
var count = 0;
function inner() {
count++;
console.log(count);
}
return inner;
}
var closure = outer();
closure(); // 1
closure(); // 2
closure(); // 3
在上面的例子中,函数outer返回了一个函数inner,并且inner可以访问outer中的变量count。当outer被调用时,会创建一个新的执行上下文,其中包括一个作用域链。当inner被调用时,会创建一个新的执行上下文,其中包括一个作用域链,该作用域链包括了outer的变量对象。
由于闭包可以访问outer中的变量count,因此每次调用闭包会使count加1,并输出加1后的结果。
2.2 实例2:计数器
function counter() {
var count = 0;
function increment() {
count++;
console.log(count);
}
function decrement() {
count--;
console.log(count);
}
return {increment: increment, decrement: decrement};
}
var c = counter();
c.increment(); // 1
c.increment(); // 2
c.decrement(); // 1
c.decrement(); // 0
在上面的例子中,函数counter返回了一个包含increment和decrement方法的对象,并且这两个方法都可以访问变量count。每次调用increment方法会使count加1,并输出加1后的结果;每次调用decrement方法会使count减1,并输出减1后的结果。
由于increment和decrement都形成了闭包,因此它们可以访问counter中的变量count,并且每次调用它们都可以保留count的状态,实现了一个计数器的功能。
3. 闭包的用途
闭包可以用于多种场景,如隐藏数据、模拟私有变量、缓存数据等。
3.1 隐藏数据
闭包可以将数据隐藏起来,从而保护数据的安全性。
function person(name) {
return function() {
console.log("My name is " + name);
}
}
var sayHi = person("Alice");
sayHi(); // "My name is Alice"
在上面的例子中,函数person返回了一个函数,这个函数可以访问外部的变量name。由于返回的函数形成了闭包,可以保留name的状态。
通过这种方式,我们可以在程序中隐藏一些敏感数据,从而保护这些数据的安全性。
3.2 模拟私有变量
JavaScript没有提供真正的私有变量,但是可以通过闭包模拟私有变量。
function person(name, age) {
var _name = name;
var _age = age;
return {
getName: function() {
return _name;
},
getAge: function() {
return _age;
},
setAge: function(age) {
_age = age;
}
}
}
var alice = person("Alice", 20);
console.log(alice.getName()); // "Alice"
console.log(alice.getAge()); // 20
alice.setAge(21);
console.log(alice.getAge()); // 21
在上面的例子中,我们使用闭包模拟了两个私有变量_name和_age,分别用于存储姓名和年龄。然后返回一个包含getName、getAge和setAge方法的对象,这些方法都可以访问_name和_age。
由于_name和_age都被定义在person函数内部,因此它们是私有变量,外部不能直接访问它们。但是通过返回包含访问和修改这些变量的方法的对象,外部可以间接访问和修改它们。
3.3 缓存数据
闭包可以用于缓存函数的结果,从而提高程序的性能。
function fibonacci() {
var cache = {};
return function(num) {
if (num in cache) {
return cache[num];
} else {
if (num <= 1) {
return num;
} else {
var result = fibonacci(num - 1) + fibonacci(num - 2);
cache[num] = result;
return result;
}
}
}
}
var fib = fibonacci();
console.log(fib(5)); // 5
console.log(fib(10)); // 55
在上面的例子中,我们定义了一个fibonacci函数,用于计算斐波那契数列的第n项。由于这个函数计算结果很耗时,我们使用一个缓存对象cache来记录已经计算过的结果,以提高程序的性能。
由于斐波那契数列的定义具有递归性质,因此我们使用一个匿名函数来实现递归,从而形成了一个闭包。每次调用该匿名函数时,如果在cache中已经有num的计算结果,则直接返回该结果,否则计算结果并存入cache中。
4. 总结
JavaScript闭包是一种特殊的JavaScript对象,可以访问外层函数的变量,并且可以将这些变量保留在内存中。闭包可以用于隐藏数据、模拟私有变量、缓存数据等多种场景。但是需要注意,过多地使用闭包会导致内存泄漏,因此需要谨慎使用。