闭包
定义
MDN定义:
闭包是指那些能够访问自由变量的函数。
自由变量:
自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的的变量。
《你不知道的JavaScript》里的定义:
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
解释
观察下面的代码:
var a = 1
function foo() {
console.log(a)
}
foo() // 1
这是闭包吗?
按照我们原来的理解,这不就是作用域的查找规则嘛!我们根据对定义的理解,foo
函数访问了a
变量,但是a
变量是自由变量,于是构成闭包。换一种说法,foo
函数记住了全局作用域并进行了访问,于是产生了闭包。
从上面的代码很难理解闭包,因为我们很容易通过作用域的查找规则去理解它。
观察以下代码:
function foo() {
var a = 1
function bar() {
console.log(a)
}
bar()
}
foo() // 1
这个代码是不是稍微容易理解了一点点,但它本质上和上面的代码相同,还是不容易观察到闭包。
于是我们再改一下:
function foo() {
var a = 1
function bar() {
console.log(a)
}
return bar
}
var baz = foo()
baz() // 1
这次的代码要认真分析一下了:
foo
函数里声明了一个变量a
并赋值为1
和一个函数bar
,bar
函数输出变量a
,最后foo
函数返回bar
函数- 在全局中声明了一个变量
a
并赋值为2
和另一个变量baz
并将foo
函数的返回值赋给它,然后执行baz
函数
我们惊讶地发现baz
函数竟然输出了foo
函数作用域内的变量a
,按照我们对作用域和执行上下文的理解,foo
函数执行完毕后不是应该销毁其作用域的吗,此时我们观察到了闭包。
bar
函数访问了自由变量a
,构成闭包。这个定义感觉和上面两个例子没什么区别,太笼统了,我们用《你不知道的JavaScript》里的定义去理解:bar
函数记住并访问了它所在的词法作用域foo
函数作用域,即使bar
函数在全局作用域执行(实际上baz
变量引用了bar
函数),这时产生了闭包。
或许我们可以换一种形式观察闭包:
function foo() {
var a = 1
function bar() {
console.log(a)
}
baz(bar)
}
function baz(fn) {
fn()
}
foo() // 1
或者再换一种形式:
var fn
function foo() {
var a = 1
function bar() {
console.log(a)
}
fn = bar
}
function baz() {
fn()
}
foo()
baz() // 1
观察上面两例代码,我们可以再次通过作用域去理解闭包:
无论通过何种手段将内部函数传递到所在词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
关于闭包,冴羽大大在它的深入系列之闭包中提到了闭包可以从两个角度定义:
- 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
- 从实践角度:以下函数才算是闭包:
- 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
- 在代码中引用了自由变量
因此,MDN对于闭包的定义是如此的笼统以至于难以观察到。
如果你还是对MDN的定义耿耿于怀,那么我推荐你去看看这篇文章:探索闭包(原文:a closure)
身影
回调函数
function wait(message) {
setTimeout(function timer() {
console.log(message)
}, 1000)
}
wait('Hello, closure!')
将一个内部函数(timer
)传递给setTimeou(..)
。timer
具有涵盖wait(..)
作用域的闭包,因此还保有对变量message
的引用。
无论何时何地,如果将函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。
在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其它的异步(或同步)任务中,只要使用了回调函数,实际上就是在使用闭包。
IIFE——立即执行函数表达式
var a = 1
(function () {
console.log(a) // 1
})()
尽管IIFE本身并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建可以被封闭起来的闭包的工具。
循环
观察下面代码:
var arr = []
for(var i = 0; i < 5; i++) {
arr[i] = function () {
console.log(i)
}
}
for(var j = 0; j < 5; j++) {
arr[j]()
}
上面代码会输出什么?
我们想要得到是5个不同的函数,每个函数分别输出0、1、2、3、4,然而结果却是输出5个5
,我们分析造成这个结果的原因:
- 首先是输出的5是哪来的,我们可以在全局中
console.log(i)
,发现结果为5
,由此知道了5
是循环结束后i
的值,我们产生疑问:为什么在循环过程中i
的值没有被保存下来呢? - 实际上,我们陷入了一个误区:我们试图假设在循环中每个迭代在运行时都会给自己”捕获“一个
i
的副本。但是根据作用域的工作原理,实际情况是尽管循环中的5个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i
- 因此所有函数都是共享一个
i
的引用,而在函数调用时,i
的值已经是5
了,所以会输出5个5
怎么解决这个问题呢?我们需要使用闭包,在循环过程中每个迭代都需要一个闭包作用域。
我们使用IIFE来尝试一下这个例子:
var arr = []
for(var i = 0; i < 5; i++) {
arr[i] = (function (j) {
console.log(j)
})(i)
}
上面的代码中,循环内部使用IIFE会为每个迭代都生成一个新的作用域,使得每个函数可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
块作用域也能解决这个问题,但和闭包关系不大,此处略去。
应用
单例模式
单例模式是一种常见的设计模式,它保证了一个类只有一个实例。实现方法一般是先判断实例是否存在,如果存在就直接返回,否则就创建了再返回。单例模式的好处就是避免了重复实例化带来的内存开销.
function Singleton(){
this.data = 'singleton'
}
Singleton.getInstance = (function () {
var instance
return function(){
if (instance) {
return instance
} else {
instance = new Singleton()
return instance
}
}
})();
var sa = Singleton.getInstance()
var sb = Singleton.getInstance()
console.log(sa === sb); // true
console.log(sa.data); // 'singleton'
模拟私有属性
let fn = (function () {
let privateCounter = 0
function changeBy(val) {
privateCounter += val
}
return {
increment() {
changeBy(1)
},
decrement() {
changeBy(-1)
},
getValue() {
console.log(privateCounter)
}
}
})()
fn.getValue() // 0
fn.increment()
fn.getValue() // 1
fn.decrement()
fn.decrement()
fn.decrement()
fn.getValue() // -2
在这个例子中,fn
函数返回了3个闭包方法,除了这3个方法能够访问privateCounter
变量和changeBy
函数外,你无法通过其他手段操作它们。
函数柯里化
柯里化(
currying
),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
- 工厂函数
function makeAdder(x) {
return function (y) {
console.log(x + y)
}
}
var a = makeAdder(5)
var b = makeAdder(10)
a(2) // 7
b(2) // 12
在这个例子中,我们利用了闭包自带执行环境的特性(即使外层作用域已销毁),仅仅使用一个形参完成了两个形参求和的操作。
- bind方法的实现
Function.prototype.myBind = function(context = window) {
if (typeof this !== 'function') throw new Error('Error')
let selfFunc = this
let args = [...arguments].slice(1)
return function F () {
// 因为返回了一个函数,可以 new F(),所以需要判断
if (this instanceof F) {
return new selfFunc(...args, arguments)
} else {
// bind 可以实现类似这样的代码 f.bind(obj, 1)(2),所以需要将两边的参数拼接起来
return selfFunc.apply(context, args.concat(arguments))
}
}
}
- 类型判断函数
function typeOf (value) {
return function (obj) {
const toString = Object.prototype.toString;
const map = {
'[object Boolean]' : 'boolean',
'[object Number]' : 'number',
'[object String]' : 'string',
'[object Function]' : 'function',
'[object Array]' : 'array',
'[object Date]' : 'date',
'[object RegExp]' : 'regExp',
'[object Undefined]' : 'undefined',
'[object Null]' : 'null',
'[object Object]' : 'object'
};
return map[toString.call(obj)] === value;
}
}
var isNumber = typeOf('number');
var isFunction = typeOf('function');
var isRegExp = typeOf('regExp');
isNumber(0); // => true
isFunction(function () {}); // true
isRegExp({}); // => false
通过向 typeOf
里传入不同的类型字符串参数,就可以生成对应的类型判断函数,作为语法糖在业务代码里重复使用。
问题
内存泄露
由于闭包使用过度而导致的内存占用无法释放的情况,我们称之为:内存泄露。
内存泄露 是指当一块内存不再被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或者内存池的现象。内存泄漏可能会导致应用程序卡顿或者崩溃。
相比普通函数,闭包对于内存的占用比普通函数大,毕竟外层函数的自由变量无法释放。
function bindEvent(){
let ele = document.querySelector('.ele')
ele.onclick = function () {
console.log(ele.style.color)
}
}
bindEvent()
在上面的例子中点击事件函数中使用到了外层函数中的DOM ele
,导致 ele
始终无法释放,造成大量的内存占用。
解决:
function bindEvent(){
let ele = document.querySelector('.ele')
let color = ele
ele.onclick = function () {
console.log(color)
}
ele = null
}
bindEvent()
闭包中的this值
var name = '听风是风'
var obj = {
name: '行星飞行',
sayName: function () {
return function () {
console.log(this.name)
}
}
}
obj.sayName()() // '听风是风'
上面的代码可以理解为以下代码:
var name = '听风是风'
var obj = {
name: '行星飞行',
sayName: function () {
return function () {
console.log(this.name)
}
}
}
var fn = obj.sayName()
fn() // '听风是风'
因此在闭包中的this
一般指向全局对象window
。
如果想要使用外层函数的this
,可以:
- 保存
this
值
var name = '听风是风'
var obj = {
name: '行星飞行',
sayName: function () {
var that = this
return function () {
console.log(that.name)
}
}
}
obj.sayName()() // '行星飞行'
- 使用箭头函数
var name = '听风是风'
var obj = {
name: '行星飞行',
sayName: function () {
return () => {
console.log(this.name)
}
}
}
obj.sayName()() // '行星飞行'
参考
《你不知道的JavaScript上卷》