闭包是什么?从为什么会有闭包讲起。
闭包的起源
维基百科:闭包的概念是在1960年代为采用lambda演算的表达式的机器求值而开发的,它首次在1970年于PAL编程语言中完全实现,用来支持词法作用域的头等函数。
关键词:
词法作用域:函数的作用域在函数定义(就是代码书写的位置)的时候就决定了。
头等函数(函数是一等公民):函数可以作为函数的参数、函数的返回值,赋值给变量或存储在数据结构中。
举例
写一个JS函数,来说明
function parent() {
let n = 1
function son() {
// 词法作用域: 可以访问到n
console.log(n);
}
return son // 函数是一等公民: 可以被返回
}
let son = parent()
son()// 函数是一等公民: 返回的函数可以执行。
- 函数是一等公民 决定了:函数
son
可以返回,且任意时刻执行。 - 词法作用域 决定了:函数
son
可以访问到函数parent
的 变量n
。
现在不要想其它概念。
我们直观的认为:函数 son
执行,访问函数 parent
作用域的 n
执行 console.log
。
是对的吗?对的。
那闭包这个概念呢?
发现了吗?我们不知道闭包这个概念,但是完全不影响,我们认识代码的执行!
为什么有闭包这个概念?
先讲一个概念:函数调用栈(Function Call Stack)
简单讲一下函数在函数调用栈上的执行流程。
- 函数在栈上运行,且会使用栈内存。
- 函数在栈内存上,保存局部变量,**基本类型数据、**函数执行信息等数据。
- 函数执行完后,出栈。
出栈后,栈上保存的局部变量,基本类型数据,都无法访问了,数据不存在了。
举例说明
如果没有闭包,执行是什么情况。
// 在没有闭包的调用栈中执行
// 下面有图,看图理解
function parent() {
let n = 1 // 局部变量n,基本数据类型1
function son() { // 局部变量son
// son执行时,parent已经出栈,变量n访问不到了
// 报错!
console.log(n);
}
return son
// parent出栈
}
let son = parent() // parent入栈
son()// son入栈,
要让 son
可以在 parent
出栈后依然能访问到 n
。
需要个新技术来解决这个问题。
闭包诞生
为了解决上述问题( 词法作用域 + 函数是一等公民 + 函数调用栈 正常运行)。诞生了闭包这个概念。
闭包其实是一种技术:在函数调用栈的基础上,同时实现函数是一等公民、词法作用域。
闭包的具体实现
核心原理:将闭包所需的数据,都存储到堆(Heap)上。
堆(Heap)的数据,是不会随着函数调用结束而回收的。
上面的举例函数,在 chrome
中的闭包信息:
v8
将函数闭包所需的数据,构成一个Closure
对象放在堆上,然后函数引用这个对象。
再次调用时,函数会去访问Closure
对象里的数据。
(深入原理请看一文颠覆大众对闭包的认知)
闭包的特性:保存、保护变量
根据闭包的实现,我们可以看出,Closure
是一个私有的、不影响全局的对象。
从而有了以下特性:
- 保存:闭包将局部变量的生命周期拉长,不在随着函数调用结束而回收。
- 保护:不会成为全局变量、也不会收到外部影响。
- (我认为是因为词法作用域的作用,也算闭包的一部分)
私有变量,很重要。
闭包的应用
模块
我们要提供一个模块供别人使用,要解决命名空间污染的问题。
命名空间污染:模块要用多个变量,我们希望变量不影响全局,全局也不影响我们的变量。
IIFE + 闭包 即可实现。
// 闭包实现模块化。
var moduleA = (function (global, doc) {
var methodA = function() {};
var dataA = {};
return {
methodA: methodA,
dataA: dataA
};
})(this, document);
现代JS提供ESM
,原生支持模块。
模拟私有属性
// 模拟私有属性
function Test(name) {
let _name = name;
return {
getName: function () {return _name;},
};
}
let obj = new Test('z');
obj.getName(); // z
obj._name // undefined
现代JS,class
支持私有属性(操作符#
)。
高阶函数、有状态的函数
例如:高阶函数、科里化、节流防抖(等要判断状态的函数)
// 节流
function throttle(fn, timeout) {
let timer = null
return function (...arg) {
if(timer) return
timer = setTimeout(() => {
fn.apply(this, arg)
timer = null
}, timeout)
}
}
其实都应该算函数式编程的高阶函数。
闭包的缺点
内存泄漏
数据被闭包对象 Closure
引用,无法被释放回收。
闭包函数又在多个地方被引用,导致数据引用复杂,容易发生内存泄漏问题。
什么是闭包?
闭包(多种说法):
- MDN:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被用包围),这样的组合就是闭包(closure)。
- 闭包让你可以在一个内层函数中访问到其外层函数的作用域。
- 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
- Rust语言:闭包允许捕获调用者作用域中的值。
- 维基百科:闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。
- 闭包其实是一种技术:在函数调用栈的基础上,同时实现函数是一等公民、词法作用域。
闭包的实现有多种方式,所以解释就有差异。
面试就回答MDN的说法就好。
总结
- 闭包解决了什么问题?
- 在函数调用栈的基础上,同时实现函数是一等公民、词法作用域。
- JS中的闭包是什么?(MDN的解释)
- 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被用包围),这样的组合就是闭包(closure)。
- 闭包让你可以在一个内层函数中访问到其外层函数的作用域。
- 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
- 闭包的实现:
- 原理:将闭包所需的数据,都存储到堆(Heap)上。
V8
会将闭包所需的数据,存在函数的[[Scope]]
的Closure
对象上,这个对象在堆(Heap)上。
- 闭包的特性:
- 保存:闭包将局部变量的生命周期拉长,不在随着函数调用结束而回收。
- 保护:不会成为全局变量、也不会收到外部影响。
- 可以构建私有变量。
- 闭包的应用
- 模块
- 私有属性
- 高阶函数、有状态的函数
- 闭包的缺点
- 内存泄漏