前言
前些日子,在掘金上看到一片热门文章《在酷家乐做面试官的日子》。该文作者以面试官的角度,详细阐述了作为一名 web 应聘者应该具有哪些技能,才会更让人青睐。
在对比自身的过程中,发现有一些问题,或许了解,但不全面,这也是本系列文章诞生的缘由。
什么是闭包
闭包是函数和声明该函数的词法环境的组合。(源自 MDN)
官方解释通常来讲都非常拗口,通俗点来讲:
闭包就是创建一个了上下文环境,这个环境包含了创建时所能访问的所有局部变量。并且不受 GC 影响。
最简单的闭包
我们先来看一个最简单闭包声明:
function func1() {
// func1 函数的局部变量
var msg = 'hello world!'
// 依据变量作用域的定义, 在函数内部创建的函数, 根据函数的作用域链,使其可以访问其父函数的变量
// 这段代码中,func2 就是一个闭包。
function func2() {
console.log(msg)
}
func2()
}
func1() => hello world
看到这,很多同学有疑惑,这不就是一个普通的方法调用吗?在实际的代码中,经常这样写,并没发现特殊的地方啊。
实际上,函数 func2 就是一个闭包,它遵循闭包的定义和内部概念,在 func2 的上下文环境中中,包含了它创建时所有访问的所有局部变量 - msg,并且 msg 不受 GC 影响。
闭包的进阶表现
我们看另外一段进阶代码,来验证为什么说 func2 是一个闭包:
function func1() {
var msg = 'hello world!'
function func2() {
console.log(msg)
}
return func2
}
var func = new func1()
func()
相较于上一个例子,仅仅是将 func2() => return func
想想看,func() 会输出什么?实际也是 "hello world!"。
我们回到闭包的定义中:
闭包就是创建一个了上下文环境,这个环境包含了创建时所能访问的所有局部变量。并且不受 GC 影响。
我们尝试理解,func2是一个闭包,在 func2 创建时,这个环境可以访问当时创建的局部变量,当 func1 执行时,返回了这个封闭环境,并且这个封闭环境不受 GC 影响,仍然可以访问 msg 变量。
再稍微进阶一点
function func1(p1) {
return function func2(p2) {
return p1 + p2
}
}
var f1 = new func1(1)
var f2 = new func1(10)
f1(1) => 2
f2(10) => 20
上述代码中,f1、f2 各自为闭包,虽然拥有相同的函数定义,但实际上拥有各自不同的上下文环境:
函数 f1 => 1 + 1 => 2
函数 f2 => 10 + 10 => 20
闭包的作用
通过上面的例子,我们知道了闭包的概念和定义。那么闭包究竟有什么实际作用呢?
在我的理解中,闭包是为了解决函数作用域链上的变量值问题。
有几个经典应用:
函数柯里化
/*
* 函数柯里化可以有效对代码解耦
* 得益于闭包的封闭的上下文环境
* 在 f1、f2、f3 实例化对象中,都拥有不同的词法环境
*/
function f (x) {
return function (y) {
return function (z) {
return function (a) {
return function (b) {
return function (c) {
console.log(x + y + z + a + b + c);
};
};
};
};
};
}
var f1 = f(1)
var f2 = f(2)
var f3 = f(10)(10)
模拟私有方法
/*
* 得益于闭包的封闭的上下文环境,我们在每个 f1、f2 实例化对象中,都拥有不同的词法环境
* 不仅如此,该函数还扩展了私有方法
* changeIndex 则是私有方法,它对函数内部公开,对函数外部隐藏
*/
var func1 = function() {
var index = 0
// changeIndex 则是私有方法
function changeIndex(val) {
index = index + val
return index
}
return {
increment: function() {
return changeIndex(1);
},
decrement: function() {
return changeIndex(-1);
},
value: function() {
return index;
}
}
};
var f1 = new func1()
var f2 = new func1()
f1.value() => 0
f1.increment() => 1
f1.increment() => 2
f1.decrement() => 1
// f1 对比 f2
f1.value() => 1
f2.value() => 0
经典问题
闭包的循环引用问题
/*
* 期望循环输出 0 - 9
* 实际循环输出 10
*/
function f1(){
console.log('begin')
for (var index = 0; index < 10; index++) {
setTimeout(() => {
console.log(index)
}, 1000);
}
console.log('end')
}
f1()
在这个闭包循环的经典问题中,考察了我们对闭包、函数作用域、事件循环的理解。
我们尝试先通过事件循环理解一下该函数的运行:
- 主线程执行函数 f1 的定义并执行函数 f1。
- 主线程执行常规任务 => console.log('begin')
- 主线程执行 for 循环与 setTimeout 函数本身。
- 将 setTimeout 的回调函数的 Task 添加到 Task Queue。
- 主线程执行常规任务 => console.log('end')。
- 主线程的执行栈为空
- 定时器触发,主线程取出 setTimeout 的 Task 并执行相应的回调函数(此时,for 循环早已结束,index 值变成10)。
- 输出 10 次 10
那么如何改进呢?
使用自执行函数解决
/*
* 实际循环输出 0 - 9
*/
function f1() {
console.log('begin')
for (var index = 0; index < 10; index++) {
;(function(index) {
setTimeout(() => {
console.log(index)
}, 1000)
})(index)
}
console.log('end')
}
f1()
使用 ES6 的 Let 解决
/*
* 实际循环输出 0 - 9
*/
function f1() {
console.log('begin')
for (let index = 0; index < 10; index++) {
setTimeout(() => {
console.log(index)
}, 1000)
}
console.log('end')
}
f1()