JS中闭包的定义
这里先来看一下闭包的定义,分成两个:在计算机科学中和在JavaScript中。在计算机科学中对闭包的定义(维基百科):
闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures)。是在支持 头等函数 的编程语言中,实现词法绑定的一种技术;闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表); 闭包跟函数最大的区别在于,当捕捉闭包的时候,它的 自由变量 会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行;闭包的概念出现于60年代,最早实现闭包的程序是 Scheme,那么我们就可以理解为什么JavaScript中有闭包: 因为JavaScript中有大量的设计是来源于Scheme的;
我们再来看一下MDN对JavaScript闭包的解释:
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure); 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域;在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来;
那么我的理解和总结:
闭包是指有权访问另一个函数作用域中变量的函数。
那闭包的作用是什么?我们为什么要使用闭包呢?
闭包的两大作用:保存/保护
- 保护:划分一个独立的代码执行区域,在这个区域中有自己私有变量存储的空间,保护自己的私有变量不受外界干扰(操作自己的私有变量和外界没有关系);
- 保存:如果当前上下文不被释放【只要上下文中的某个东西被外部占用即可】,则存储的这些私有变量也不会被释放,可以供其下级上下文中调取使用,相当于把一些值保存起来了;
闭包的特点:
- 内部函数可以访问定义他们外部函数的参数和变量。(作用域链的向上查找,把外围的作用域中的变量值存储在内存中而不是在函数调用完毕后销毁)设计私有的方法和变量,避免全局变量的污染。
a. 闭包是密闭的容器,,类似于set、map容器,存储数据的
b. 闭包是一个对象,存放数据的格式为 key-value 形式 - 函数嵌套函数
- 本质是将函数内部和外部连接起来。优点是可以读取函数内部的变量,让这些变量的值始终保存在内存中,不会在函数被调用之后自动清除
例子
那么要什么条件才能形成闭包
- 函数的嵌套
- 内部函数引用外部函数的局部变量,延长外部函数的变量生命周期
闭包的用途:
- 模仿块级作用域
- 保护外部函数的变量 能够访问函数定义时所在的词法作用域(阻止其被回收)
- 封装私有化变量
- 创建模块
例子:
function foo() {
// AO: 销毁
var name = "foo"
function bar() {
//函数bar访问了外层作用域的自由变量name,那么这个函数就是一个闭包;
console.log("bar", name)
}
return bar
}
var fn = foo()
fn()
闭包的经典使用场景:
//函数作为参数
var a = 'zayyo'
function foo(){
var a = 'foo'
function fo(){
console.log(a)
}
return fo
}
function f(p){
var a = 'f'
p()
}
f(foo())
/* 输出
* foo
/
循环赋值----闭包的运用
修改一个函数,使它依次输出1-10
//原函数-未使用闭包前
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i)
}, 1000)
}
//输出10个10
使用闭包的原理,使代码依次输出1-10
//使用闭包后
for(var i = 0; i<10; i++){
(function(j){
setTimeout(function(){
console.log(j)
}, 1000)
})(i)
}
依次输出1~10
节流防抖
// 节流
function throttle(fn, timeout) {
let timer = null
return function (...arg) {
if(timer) return
timer = setTimeout(() => {
fn.apply(this, arg)
timer = null
}, timeout)
}
}
// 防抖
function debounce(fn, timeout){
let timer = null
return function(...arg){
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arg)
}, timeout)
}
}
柯里化实现
function curry(fn, len = fn.length) {
return _curry(fn, len)
}
function _curry(fn, len, ...arg) {
return function (...params) {
let _arg = [...arg, ...params]
if (_arg.length >= len) {
return fn.apply(this, _arg)
} else {
return _curry.call(this, fn, len, ..._arg)
}
}
}
let fn = curry(function (a, b, c, d, e) {
console.log(a + b + c + d + e)
})
fn(1, 2, 3, 4, 5) // 15
fn(1, 2)(3, 4, 5)
fn(1, 2)(3)(4)(5)
fn(1)(2)(3)(4)(5)
因为存在闭包的原因上面能依次输出1~10,闭包形成了10个互不干扰的私有作用域。将外层的自执行函数去掉后就不存在外部作用域的引用了,输出的结果就是连续的 10。为什么会连续输出10,因为 JS 是单线程的遇到异步的代码不会先执行(会入栈),等到同步的代码执行完 i++ 到 10时,异步代码才开始执行此时的 i=10 输出的都是 10。
那这里又会又一个问题产生,因为我们的JavaScript的内存是通过标签清除法来实现内存回收的(简单说,也就是引用从根节点开始是否可达来判定是否是垃圾)。而闭包的使用会导致内存无法被回收,并且闭包会携带包含其它的函数作用域,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃的后果。
那我们怎么解决闭包导致的内存泄漏问题呢?
内存泄漏是指:用动态存储分配函数内存空间,在使用完毕后未释放,导致一直占据该内存单元。直到程序结束。指任何对象在你不再拥有或需要它之后仍然存在。
demo代码:
function foo() {
// AO: 销毁
var name = "foo"
function bar() {
//函数bar访问了外层作用域的自由变量name,那么这个函数就是一个闭包;
console.log("bar", name)
}
return bar
}
var fn = foo()
fn()
fn函数在调用完毕之后,foo函数会自动销毁,但foo函数中的变量name不会被销毁,因为在bar函数内部进行了访问,因为在JavaScript的内存回收机制中规定,被另一个作用域引用的变量不会被回收。除非bar函数解除调用才能销毁。
如果该函数使用的次数很少,不进行销毁的话就会变为闭包产生的内存泄漏。
那我们怎么解决闭包导致的内存泄漏问题呢?
只需将该函数赋值为null即可。
fn = null // 阻止内存泄漏
闭包可能会造成内存泄漏,但不是一定会造成。
常见面试题
for 循环和闭包
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]()
/* 输出
3
3
3
/
这里的 i 是全局下的 i,共用一个作用域,当函数被执行的时候这时的 i=3,导致输出的结构都是3。
使用闭包改善上面的写法达到预期效果,写法1:自执行函数和闭包
var data = [];
for (var i = 0; i < 3; i++) {
(function (j) {
data[j] = function () {
console.log(j);
}
})(i)
}
data[0]();
data[1]();
data[2]()
写法2:使用 let
var data = [];
for (let i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]()
let 具有块级作用域,形成的3个私有作用域都是互不干扰的。