词法作用域的概念
知道 JavaScript 词法作用域概念将有助于我们理解闭包。
我们都知道,JavaScript 使用词法作用域,在函数内定义的变量不能在函数之外的任何地方访问,因为变量仅仅在该函数的域的内部有定义。相对应的,一个函数可以访问定义在其范围内的任何变量和函数。
换言之,定义在全局域中的函数可以访问所有定义在全局域中的变量。在另一个函数中定义的函数也可以访问在其父函数中定义的所有变量和父函数有权访问的任何其他变量。
举个例子 🌰:
// 下面的变量定义在全局作用域 (global scope) 中
var num1 = 20,
num2 = 3,
name = "Chamahk";
// 本函数定义在全局作用域
function multiply() {
return num1 * num2;
}
multiply(); // 返回 60
// 嵌套函数的例子
function getScore() {
var num1 = 2,
num2 = 3;
function add() {
return name + " scored " + (num1 + num2);
}
return add();
}
getScore(); // 返回 "Chamahk scored 5"
从代码中可以看到:
- 1.全局域中的函数
multiply
可以访问所有定义在全局域中的变量num1、num2
,故而执行函数后输出 60. - 2.内部函数
add
可以访问父函数及其定义在全局域中变量,执行后输出 "Chamahk scored 5"。
了解了 JavaScript 使用词法作用域后我们知道,在通常情况下我们访问不了其他函数内定义的变量。但是!假如,我们就是需要访问其他函数内定义的变量,又要怎么办呢?
答案是:可以通过闭包做到。
那么,什么是闭包呢?
闭包就是能够读取其他函数内部变量的函数。
你可以在一个函数里面嵌套另外一个函数。嵌套(内部)函数对其容器(外部)函数是私有的。它自身形成一个闭包。
下面的例子展示了闭包函数 square
:
function addSquares(a, b) {
function square(x) {
return x * x;
}
return square(a) + square(b);
}
a = addSquares(2, 3); // returns 13
b = addSquares(3, 4); // returns 25
c = addSquares(4, 5); // returns 41
由于内部函数形成了闭包,因此你可以调用外部函数并为外部函数和内部函数指定参数。我们再看一个闭包的应用场景,代码如下:
function outside(x) {
function inside(y) {
return x + y;
}
return inside;
}
fn_inside = outside(3); // 可以这样想:给一个函数,使它的值加 3
result = fn_inside(5); // returns 8
result1 = outside(3)(5); // returns 8
上面示例中,闭包的作用就是保存变量。注意到上例中 inside
被返回时 x
是怎么被保留下来的。一个闭包必须保存它可见作用域中所有参数和变量。因为每一次调用传入的参数都可能不同,每一次对外部函数的调用实际上重新创建了一遍这个闭包。只有当返回的 inside
没有再被引用时,内存才会被释放。
常见的面试题型:用闭包实现输出数字 0~10 ?
function printNumbers() {
for (var i = 0; i <= 10; i++) {
(function (num) {
setTimeout(function () {
console.log(num);
}, i * 1000);
})(i);
}
}
printNumbers();
在上面的代码中,我们使用了一个立即执行函数表达式(IIFE),来创建一个闭包,将 i 的当前值传递给 setTimeout() 函数,以便在指定的时间后打印出该数字。因为 setTimeout() 是异步函数,所以我们可以使用闭包来确保在每次循环中的正确值被传递给 setTimeout(),并且它们能够按照预期的顺序被打印出来。
需要注意的是,在这个例子中,我们将 setTimeout() 的延迟时间设置为 i * 1000 毫秒,以便每个数字之间有一个一秒的延迟。
如果不想使用过多的闭包,你可以用 ES2015 引入的 let 或 const 关键词:
for (let i = 0; i <= 10; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}