跳到主要内容

闭包

闭包的概念​

  • 闭包可以能够读取其他函数内部变量的函数
    • 使变量长期保存在内存当中

由于在JavaScript语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁

变量作用域​

要理解闭包,首先要理解javascript的特殊的变量作用域。

变量的作用域无非就两种:全局变量和局部变量。

javascript语言的特别之处就在于:函数内部可以直接读取全局变量,但是在函数外部无法读取函数内部的局部变量。

注意点:在函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明的是一个全局变量!

js
function f0(){
n=999;
}
f0();
alert(n); // 999
js
var n = 999;
function f1() {
console.log(n);
}
f1() // 999

上面代码中,函数f1可以读取全局变量n。但是,函数外部无法读取函数内部声明的变量。

如何从外部读取函数内部的局部变量?

如果有时需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。

那就是在函数内部,再定义一个函数,这就可以从外部读取函数内部的局部变量

函数内部内嵌一个函数,内嵌的这个函数就是个异步操作

js
function f1(){

    var n=999;

    function f2(){
      alert(n); // 999
    }

  }

在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。

这就是Javascript语言特有的"链式作用域"结构(chain scope),

子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

闭包是什么​

  • 把函数执行形成私有上下文,来保护和保存私有变量机制称为闭包。

  • 闭包是指有权访问另外一个函数作用域中的变量的函数

  • 可以访问函数内部的变量

  • 可以使变量长期保存在内存当中 (堆内存)

js
function f1(){
var a = 999;
function f2(){
console.log(a);
}
return f2; // f1返回了f2的引用
}
var result = f1(); // result就是f2函数了
result(); // 执行result,全局作用域下没有a的定义,
//但是函数闭包,能够把定义函数的时候的作用域一起记住,输出999

函数f1的返回值就是函数f2,由于f2可以读取f1的内部变量,所以就可以在外部获得f1的内部变量了。

那到底什么是闭包呢?​

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这就产生了闭包。 ----《你不知道的Javascript上卷》

我个人理解,闭包就是函数中的函数(其他语言不能函数再套函数),里面的函数可以访问外面函数的变量,外面的变量的是这个内部函数的一部分。

MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。 (其中自由变量,指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量。)

闭包形成的条件​

  • 函数嵌套
  • 内部函数引用外部函数的局部变量

闭包产生的原因​

闭包产生的本质就是,当前环境中存在指向父级作用域的引用

首先要明白作用域链的概念,其实很简单,在ES5中只存在两种作用域————全局作用域和函数作用域,当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链,值得注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。 比如:

text
var a = 1;
function f1() {
var a = 2
function f2() {
var a = 3;
console.log(a);//3
}
}

在这段代码中,f1的作用域指向有全局作用域(window)和它本身,而f2的作用域指向全局作用域(window)、f1和它本身。而且作用域是从最底层向上找,直到找到全局作用域window为止,如果全局还没有的话就会报错。就这么简单一件事情!

闭包产生的本质就是,当前环境中存在指向父级作用域的引用。还是举上面的例子:

text
function f1() {
var a = 2
function f2() {
console.log(a);//2
}
return f2;
}
var x = f1();
x();

这里x会拿到父级作用域中的变量,输出2。因为在当前环境中,含有对f2的引用,f2恰恰引用了window、f1和f2的作用域。因此f2可以访问到f1的作用域的变量。

闭包的原理​

  • 利用函数作用域链的特征
    • 作用域链:在当前执行环境作用域中没有查到值,去外部环境寻找变量。一层一层的找,直到找到全局执行上下文,这样就形成了一个链条 , (如果没有就报错。如果有,就输出结果)
  • 闭包的本质 其实就是 当前环境作用域中存在指向父级作用域的引用

函数执行完之后,为什么闭包不会被释放?​

  • 一般来说,函数执行开始的时候,会将其中的变量分配内存,等到函数执行完毕之后,函数的生命周期结束了,就会认为这些变量无用了,会被垃圾回收机制回收,但是对于闭包来说就会阻止这个过程,一个函数内部嵌套了函数,而且这个内部函数一直保留了对这父作用域变量的引用,所以不会被垃圾回收,只有当引用计数为0了,才会被回收

闭包的用途​

闭包可以用在许多地方。它的最大用处有两个,

一个是前面提到的可以读取函数内部的变量,

另一个就是让这些变量的值始终保持在内存中,不会在函数f1调用后被自动清除。

为什么会这样呢?原因就在于f1函数是f2的父函数,f1函数的执行也会被函数内部f2的私有作用域一直占用,发生了内存泄漏,

在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

text
function f1(){
var num=100;
return function f2(){
}
}
var f=f1(); //f1执行形成的这个私有的作用域就不能再销毁了

使用闭包的注意点​

(1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

闭包使用完了后,要立即释放资源,将引用变量指向null。

使用闭包的注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值

所有闭包都会导致内存泄漏吗?​

  • 不一定。闭包所引用的变量如果没有被清除,垃圾回收无法回收,从而导致内存泄漏。

如何识别的内存泄漏?​

  • Chrome 浏览器查看内存占用
    • 打开开发者工具,选择 Performance 面板
    • 顶部默认勾选了 Screenshots ,把它去掉,勾选 Memory
    • 点击 Start profiling and reload page
    • 然后页面就会开始刷新,同时进行录制
    • 等待页面加载完成后,点击 stop 按钮,面板上就会显示内存占用情况

看下有关于内存泄漏的一道经典面试题​

js
function outer(){
var num=0;//内部变量
return function add(){//通过return返回add函数,就可以在outer函数外访问了
num++;//内部函数有引用,作为add函数的一部分了
console.log(num);
};
}
var func1=outer();
func1();//实际上是调用add函数, 输出1
func1();//输出2 因为outer函数内部的私有作用域会一直被占用
var func2=outer();
func2();// 输出1  每次重新引用函数的时候,闭包是全新的。
func2();// 输出2  

如何解决下面的循环输出问题?​

为什么会全部输出6?如何改进,让它输出1,2,3,4,5?

js
for(var i = 1; i <= 5; i ++){
setTimeout(function timer(){
console.log(i)
}, 0)
}
  • 因为setTimeout为宏任务,由于JS中单线程eventLoop机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后setTimeout中的回调才依次执行,但输出i的时候当前作用域没有,往上一级再找,发现了i,此时循环已经结束,i变成了6。因此会全部输出6。

解决方法:

1、利用IIFE(立即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中

js
for(var i = 1;i <= 5;i++){
(function(j){
setTimeout(function timer(){
console.log(j)
}, 0)
})(i)
}

2、给定时器传入第三个参数, 作为timer函数的第一个函数参数

  • 加了第三个参数产生了闭包
  • 这个闭包可以获取函数作用域的变量
  • 使每次循环的变量的作用域的都保留了下来
js
for(var i=1;i<=5;i++){
setTimeout(function timer(j){
console.log(j)
}, 0, i)
}

3、使用ES6中的let

  • let具有块级作用域
js
for(let i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i)
},0)
}

解决闭包代码的一个漏洞题目​

题目主要利用了闭包创建私有空间, 让外界只能通过提供的get方法来读取obj对象里的属性

  • js属性的访问方式,会先从自身对象查找,如果找不到,就从原型上查找
  • 对象的原型是Object.prototype
    • valueOf这个方法可以访问自身获取当前对象,但是会造成this指向,vaueOf方法的this的指向 取决于函数如何调用,指向调用它的对象 【因此不能使用valueOf这个方法】

解决方式:

  • 1、通过拦截对象原型, 使用Object.defineProperty或者proxy,通过get方法来返回当前this

    • 拿到这个对象之后,就可以对这个对象进行操作了

    • 但是这种方式也是有缺陷,万一这个对象的原型设置为null空的时候,就无法对原型进行操作了

  • 2、可完善get这个方法,通过**hasOwnProperty**这个方法来获取自身属性。而不是通过原型,这样外部就无法通过原型来操作这个对象

js
const o = (function () {
let obj = { a: 1, b: 2 };
return {
get(k) {
return obj[k];
},
};
})();
// 如何在不改变上面代码的情况下 修改obj对象

闭包的作用​

闭包的两大作用:保存/保护

(1)保护:划分一个独立的代码执行区域,在这个区域中有自己私有变量存储的空间,保护自己的私有变量不受外界干扰(操作自己的私有变量和外界没有关系);

(2)保存:如果当前上下文不被释放【只要上下文中的某个东西被外部占用即可】,则存储的这些私有变量也不会被释放,可以供其下级上下文中调取使用,相当于把一些值保存起来了;

闭包的使用场景​

  • 防抖、节流函数中可使用闭包,防止作用域发生变化,保存当前作用域对象

  • 柯里化:接受多个参数的函数转变为单一参数的函数,并返回接受剩余参数的函数去处理剩下的参数

    好处:延迟计算、动态创建函数、灵活、参数复用

  • 模块封装,为了防止变量污染全局

    • 将所有的数据和功能都封装在一个函数内部(私有的)
    • JS模块:具有特定功能的js文件,将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包信n个方法的对象或函数,模块的使用者,只需要通过模块暴露的对象调用方法来实现对应的功能。
  • 可以延长局部变量的生命周期

  • 封装私有化变量 【保存/保护这个变量】

  • 通常用于第三方库、组件库的封装,防止外部来修改内部对象,否则会造成问题

  • react-hooks实现也是用到了闭包原理