1.前言
众所周知,JS中的三座大山一直是初学者绕不过去的坎,本文将为初学者讲讲到底什么是闭包。
2.作用域
要想知道闭包是什么,我们需要知道一个概念,叫作用域。所谓的作用域就是定义的变量可以发生作用范围,JS通过区分作用域来确定变量是否可以访问和赋值。我们可以用一个套娃盒子来简单解释作用域的概念
我们可以看到 作用域 A 只能访问到 作用域 A 自己作用域下的定义的变量A,而不能访问到其作用域下面 作用域 B 作用域 C 作用域 D 中的变量。而子作用域却可以访问到上级作用域中的变量。
我们可以根据下面这段代码更深入的了解作用域
js
var a = 1;
function child() {
var b = 2;
console.log('我是子作用域中的a:' + a) // 我是子作用域中的a:1
console.log('我是子作用域中的b:' + b) // 我是子作用域中的b:2
}
console.log('我是父作用域中的a:' + a) // 我是父作用域中的a:1
console.log('我是父作用域中的b:' + b) // 执行报错 b is not defined
child();
当我们了解到父作用域中不能访问子作用域时,不禁会感到疑惑,为什么需要设计出这么奇怪的规则,定义的变量在所有作用域中都可以访问到难道不好吗?
确实如果我们所有作用域都可以访问到变量的时候,我们再也不需要考虑到变量是否可以被访问到了,我们可以再任意地方使用我们定义的变量,只要你想的话。不过我们很快就会遇到另外一个烦人的问题。想象一下
js
function genRedCar () {
var car = '我是一辆红颜色的车子';
}
function genBlueCar () {
var car = '我是一辆蓝色的车子';
}
genRedCar();
genBlueCar();
假如我们没有作用域这个概念,那么当上面两个函数运行时,由于我们都是赋值的同一个变量car,那么最终我们也只能得到一辆蓝色的车子。这显然不是我们的初衷。这就是没有作用域的坏处,它会将我们所有定义的变量都暴露在全局。
当我们的代码复杂起来的时候,那么就不得不考虑不同函数中有可能使用到同一个变量的情况。A 函数 和 B 函数 和 C 函数 ...函数 共同操作同一个变量的情况。
所以我们需要一个机制来确定变量所能作用到的范围,这就是作用域。****
说完为什么需要作用域后,你最大的疑问可能是我该如何定义一个作用域呢? 在 ES6 之前,我们有且只有函数作用域,也就是通过定义一个函数来声明一个作用域。
js
function child () {
var a = 1;
console.log(a); // 1
console.log(b); // undefined
if (a == 1) {
var b = 2;
console.log(b) // 2
}
}
console.log(a); // 执行报错 a is not defined
console.log(b); // 执行报错 b is not defined
child();
可以看到在父级作用域中 a 变量 和 b 变量 都是 not defined
没有被定义,但是在 子作用域中 b变量 却没有报错而是打印出 undefined
变量尚未赋值。
这时你可能会感到疑惑,为什么b变量明明定义在 console.log(b)
下面还是能成功打印,但是输出的值不是2,而是undefined呢?我们先不说为什么,我们先把作用域讲完,因为这涉及到另一个补充的知识点,作用域的提升。
从上面代码我们可以看到虽然我们定义的 if(a == 1) {}
像一个作用域,但是却不是一个作用域,父级作用域 child 能访问到子作用中定义的 变量b。也就是说 在 ES6 之前,我们只能通过定义一个函数的方式来定义一个作用域。
那么ES6发生了什么变化呢?ES6 引入了 let 和 const 关键字,来帮助程序员创建一个块级作用域
js
if (true) {
let x = 5; // x 只在 if 块内部可见
console.log(x); // 5
}
console.log(x); // 报错,x 不在函数作用域或全局作用域中可见
如果没有 ES6 的情况下我们是不是也能实现类似的效果呢?
js
(() => {
if (true) {
var x = 5; // x 只在 if 块内部可见
console.log(x); // 5
}
})() // 立即执行函数
console.log(x); // 报错,x 不在函数作用域或全局作用域中可见
可以看到在 ES5 中我们也可以通过函数作用域来实现类似的效果,但是增加了大量不必要的代码。
3. 作用域提升
作用域提升其实就是 js 将变量申明和赋值分为两步,表面上看我们定义了一个 var a = 1;
实现上 js 将这段代码分开为了 var a; a = 1;
两行代码。就让我拿上面作用域的例子来说明
js
// 编译前
function child () {
var a = 1;
console.log(a); // 1
console.log(b); // undefined
if (a == 1) {
var b = 2;
console.log(b) // 2
}
}
// 编译后
function child () {
var a;
var b;
a = 1; // 这里原来是 var a = 1;
console.log(a); // 1
console.log(b); // undefined
if (a == 1) {
b = 2; // 这里原来是 var b = 2;
console.log(b) // 2
}
}
可以看到var b = 2; var a = 1;
都被拆分成了两段,定义变量的声明被提升到了作用域的最前面,这就是js的作用域提升机制。
4. 闭包
说了这么多,我们终于来到最想要理解的闭包了,那么到底什么是闭包呢?上面说过作用域最大的作用就是确定变量所能作用到的范围,为此父级作用域是无法访问到自己作用域的,那么父级作用域中有没有可能访问到子级作用域中的变量呢?
你可能想到我直接将变量 return
返回给父级不就好了吗?的确这样做是一种方式。但是它并不能涵盖所有的场景,好在我们还有另外一种方式,也就是返回一个函数。
前面我们说过子作用域可以访问父级作用域的变量,那么如果我们不直接返回变量,而是返回一个函数给父级作用域,那会变成什么样的情况呢?
js
// 使用变量直接返回的方式
function createCounter() {
var count = 0;
return count;
}
function increment (count) {
return count + 1;
}
function decrement (count) {
return count - 1;
}
function getCount (count) {
return count;
}
var counter1 = createCounter();
console.log(getCount(counter1)); // 输出 0
counter1 = increment(counter1);
console.log(getCount(counter1)); // 输出 1
counter1 = decrement(counter1);
console.log(getCount(counter1)); // 输出 0
var counter2 = createCounter();
console.log(getCount(counter2)); // 输出 0
counter2 = increment(counter2);
console.log(getCount(counter2)); // 输出 1
counter2 = decrement(counter2);
console.log(getCount(counter2)); // 输出 0
// 使用函数的方式
function createCounter() {
var count = 0;
return {
increment: function() { count++; },
decrement: function() { count--; },
getCount: function() { return count; }
};
}
var counter1 = createCounter();
console.log(counter1.getCount()); // 输出 0
counter1.increment();
console.log(counter1.getCount()); // 输出 1
counter.decrement();
console.log(counter1.getCount()); // 输出 0
var counter2 = createCounter();
console.log(counter2.getCount()); // 输出 0
counter2.increment();
console.log(counter2.getCount()); // 输出 1
counter2.decrement();
console.log(counter2.getCount()); // 输出 0
我们依然可以通过作用域盒子来看一看具体的变量的引用方式
不错,你会发现,父级作用域不能再直接访问count变量,而是通过函数调用的方式来访问子作用域中的变量。这样做有什么好处呢?
- 这样做变量和函数不会污染全局,我们将变量和函数放在了作用域内,不会污染外部作用域。
- 变量不再需要多次传递,通过函数的方式执行多个计算操作而不再需要重复声明或传递变量。
- 这样做可以封装数据, 我们可以隐藏数据细节,防止外部环境直接修改变量的值。
通过返回函数的方式,让外部作用域可以访问到内部作用域的的变量的方式就是闭包。
其实所谓的闭包可以理解为前端界的一种黑话,其实概念非常简单,并没有什么难懂的东西。只是创建了闭包、作用域的名词让很多初学者摸不着头脑。
语言其实就是一个工具,创造出来的概念和名词也就是一种解决问题的方法,很多工作了很多年的前端也许也无法用一句话来直接解释闭包,虽然他们可能日常工作中经常使用到闭包,这很正常,因为很多人并没有去了解该语言的一些黑话,你知道它是怎么工作的,但是可能并不知道如何来描述它。
对于初学者来说知道闭包的概念是什么,和熟练的使用闭包可能还有一段距离,希望这篇文章能在了解什么是闭包这个黑话和面试如何解释闭包上面能帮助到大家。