你觉得什么是闭包(适用于学习积累和面试)

写在前面

初次接触到闭包这个概念的时候,我尝试在互联网上找到可靠的描述,搜罗到的答案能够说是五花八门,每个帖子看完以后我都感受仿佛完全明了,可是本身张嘴却说不出个因此然,又一次重现了如下场景: 眼睛:已浏览。脑子:已过,稳了。嘴:啥?啥玩意儿? 无奈之下买了js原理的相关书籍,细细品味了一番,发现书里写的不少,也很详尽,可是概念终究太过抽象,有时候同一个概念,换一个表现形式,就容易失去联想。这也揭示一个猿们的通病,单纯看懂一个技术点并不难,难的是运用,以及同一个点的不一样形式的变换。javascript

别的观点

那么什么叫闭包?观点不少,出现频率最高的有如下两个观点:java

  1. 函数套函数。
  2. 在函数外获取函数内变量的技术。

单纯的评价这两个观点,显然都不算错,由于闭包不论是从形式上仍是表现上确实涵盖了以上特色,但这两个观点并不许确,属于闭包的必要不充分条件。咱们来尝试推翻这两个观点。编程

首先说第二点,这个观点在没有先决条件下,能够说是至关的不严谨,一个简单的例子:bash

function fun() {
    var innerVal = '内部变量'return innerVal;
}
var getInnerVal = fun ();
console.log(getInnerVal);
复制代码

根据以上例子,咱们确实得到了函数fun的内部变量,可是这跟闭包有关系么?毫无关系。闭包

从函数开始

下面咱们来讲上面的第一点,从这儿开始,咱们正式认识下闭包。 闭包这项技术离不开函数,由于函数是闭包的基本组成部分,因此咱们先谈谈函数,什么叫函数?咱们都知道js的全部运用和变化,都是基于做用域规则产生的,这个规则就是内部做用域拥有其所在的外部做用域的访问权,这意味着内部做用域老是能够拿到外部做用域声明的变量,而外部做用域却没有内部做用域的直接访问权。而每个函数被声明时,就至关于创造了本身的做用域,同时也遵循做用域的规则。异步

与此同时,函数自己也遵循软件开发的最小暴露原则,放在这里理解的话,就是说当一个做用域内声明了一个函数的时候,这个函数对于当前做用域来讲,就是一个小黑屋,做用域并不知道里面有什么,只有当这个函数被执行的时候,函数内部的逻辑对于做用域来讲才算是可见的。函数

以上指出的函数特色:工具

  1. 最小暴露原则。
  2. 创造做用域,并遵循做用域规则。

如今咱们来看看所谓的‘函数套函数’这个观点:post

//全局做用域下写了如下代码
function outFun (){
    var a = 0;
    function innerFun(b){
        a+=b;
        return a;
    }
}
复制代码

根据该观点,咱们如今就至关于产生了一个闭包,看起来好像是这样,但真的是么? 当全局做用域中的逻辑被执行的时候,咱们遇到了一个函数的声明,声明了一个名叫outFun的函数,并产生了一个属于outFun的局部做用域,很显然,此时只是声明了函数,而咱们没有作任何其余的事情,不要忘记刚说的最小暴露原则,那么如今对于全局做用域来讲,outFun就是一个小黑屋,里面有什么并不知道,也就是说对于全局做用域来讲,outFun就是个普通的函数,与其余的函数没什么差异,就更谈不上闭包了。因此‘函数套函数就是闭包’这个观点不是很靠谱。学习

闭包的产生时机

那么问题来了,怎么才算是产生了闭包?或者说,闭包产生的时机又是什么? 看这里:

//全局做用域下写了如下代码
function outFun (){
    var a = 0;
    function innerFun(b){
        a+=b;
        return a;
    }
}
outFun();
复制代码

根据《你不知道的javascript》第五章第44页中的定义--“当函数做用域能够记住并访问所在的词法做用域时,就产生了闭包,不论函数是在当前词法做用域外仍是内执行。”咱们来看上面这段代码,从概念上来讲产生了闭包(虽然是一个没什么做用的闭包),由于外部函数outFun的执行,声明了内部函数innerFun,而函数innerFun在声明的同时,记住并拥有了所在的词法做用域的访问权。

那么能够明确地一点是,闭包产生的时机是外部函数执行,内部函数声明的时候。

争议

事实上从概念的角度对闭包进行阐述是存在争议的,此处感谢 @茹挺进 的不一样思考。大部分观点认为下文中技术角度阐述的闭包才算是真正的闭包,好比如下观点:

  1. 简单的说就是函数内部保存的变量不随这个函数调用结束而被销毁就算是产生了闭包。
  2. 可以完成信息隐藏,并进而应用于须要状态表达的某些编程范型中的才算是闭包。
  3. 闭包是由函数和与其相关的引用环境组合而成的实体。

可是彻底从技术角度来阐述闭包的话,就意味着:

  1. 闭包与内存泄露的产生绑定在了一块儿(先不考虑后续对闭包的清理)。
  2. 同时,也再一次模糊了闭包产生的时机。

出于以上两点的考虑,我进行了区分理解,与此同时,还存在一种状况致使我决定将其区分,IIFE函数的存在,与概念上的闭包同样,你说他是,他有不一样,你说他不是,他有具有一部分特色,因此这里你们能够有更多的探讨。

技术上的闭包

咱们上面说到示例代码是一个没什么做用的闭包,由于闭包是一项用来解决实际问题的技术,那么尽管上面的写法在概念上来讲算是闭包,可是从技术的角度来讲,它又不是,由于它不能解决任何实际问题,那么怎么才能让它变得有用呢,这就须要咱们想办法让内部函数变得可观察,咱们来看下面的例子:

//全局做用域下写了如下代码
function outFun (){
    var a = 0;
    return function innerFun(b){
        a+=b;
        return a;
    }
}
var newFun = outFun();
var res1 = newFun(1);
console.log(res1);   //1
var res2 = newFun(2);
console.log(res2);   //3
复制代码

咱们经过return的方式,将内部函数的引用传递到了外面,同时又在外部函数所在的做用域中声明了一个变量去引用它,使得内部函数变得可操做,也可观察,从而制造了一个实用的闭包。

关于return的误解

此时可能会有另外一些声音出现,他们将目光聚焦到了return这个操做上,认为闭包的标志就是是否使用了return,将内部函数的引用传递出去,这是一个误解,咱们用三个新的例子,来证实return的使用并非闭包所依赖的关键,它只是观察和操做的手段之一。

example1:
//全局做用域下写了如下代码
const obj = {};
function outFun (){
    var a = 0;
    obj.newFun = function innerFun(b){
        a+=b;
        return a;
    }
}
outFun();
var res1 = obj.newFun(1);
console.log(res1);   //1
var res2 = obj.newFun(2);
console.log(res2);   //3
复制代码
example2:
//全局做用域下写了如下代码
let newFun;
function outFun (){
    var a = 0;
    newFun = function innerFun(b){
        a+=b;
        return a;
    }
}
outFun();
var res1 = newFun(1);
console.log(res1);   //1
var res2 = newFun(2);
console.log(res2);   //3
复制代码
example3:
//全局做用域下写了如下代码
let arr = [];
function outFun (){
    var a = 0;
    function innerFun(b){
        a+=b;
        return a;
    }
    arr.push(innerFun);
}
outFun();
var res1 = arr[0](1);
console.log(res1);   //1
var res2 = arr[0](2);
console.log(res2);   //3
复制代码

例子中并无使用return,因此return跟闭包没有任何关系,事实上将内部函数变为可观察、可操做的核心并不拘泥于某种形式,而是在于将函数的引用传递出去便可。

从这里不难看出,闭包也包含了一种对引用关系的阐述。

真的内存泄露了吗?

既然咱们已经明了了闭包的概念、产生及其意义,那么不可或缺的也要面对它的一些其余的特性,或者说问题---内存泄漏, 直白点说,内存泄漏就是指某一块内存由于某种缘由没法被释放回收,从而形成可用内存出现缺失的情况。 至关一部分人习惯把闭包与内存泄漏捆绑在一块儿,这个观点是笼统的,咱们只能说,闭包可能形成内存泄漏。

前置说明---检查内存泄漏的工具及使用方法:

传送门:你觉得内存泄露怎么侦测

咱们来看下面对比的两个个例子:

//全局做用域下写了如下代码
//发生内存泄漏的例子
var obj = {};
function outFun (){
    var a = 0;
    return function innerFun(b){
        a+=b;
        return a;
    }
}
setTimeout(function (){
    obj.newFunc = outFun();
    console.log(obj.newFunc(1)); //1
},3000);
setTimeout(function (){
    console.log(obj.newFunc(2)); // 3
},6000);
setTimeout(function (){
    obj.newFunc = null;
    console.log('clean'); //clean
},9000);
复制代码
  1. 初始时内存快照,无变化,因此无内容
  2. 第一个settimeout以后,本次建立了闭包并占用了内存
  3. 本次变化未处理闭包,因此闭包内存无变化,即该闭包内存未释放
  4. 本次变化将引用闭包的对象属性置空,闭包占用的内存被释放
//全局做用域下写了如下代码
//未发生内存泄漏的例子
function outFun (){
    var a = 0;
    return function innerFun(b){
        a+=b;
        return a;
    }
}
setTimeout(function (){
    outFun();
    console.log('3秒后');
},3000);
复制代码
  1. 初始时内存快照,无变化,因此无内容
  2. 第一个settimeout以后,outFun执行,本次建立了闭包,但执行完毕后内存当即被回收了,并无占用内存

内存泄漏的真正缘由

从这里咱们不难看出,闭包产生内存泄漏的一个必须的条件是被传递出去的内部函数的引用地址,是否被别的做用域的变量所引用。 只有知足这个条件,才会发生内存泄漏,由于当函数outFun执行完毕后,js解释器根据垃圾回收机制,要回收内存的时候,发现内部函数被其余做用域所引用,而js解释器并不知道这个被引用出去的内部函数啥时候执行,因此只能保持内存不释放,以备随时的使用。 因此,结论跃然纸上,闭包不表明就必定会发生内存泄漏,仅仅是可能发生闭包,好比开发者创造了闭包后,没有及时清理。 与此同时,咱们能够看出,闭包技术的特性是一把双刃剑,因为它能将做用域内存保持住,那么开发者就能够在后续对该做用域中能访问到的那些个变量作进一步处理,但同时,若是不能合理处理闭包,那么严重的内存泄漏将致使内存溢出,程序崩溃。

闭包的实用例子

简单的再说两个闭包的实用例子,

  1. bind方法已然是耳熟能详,当咱们使用js去shim它的时候,就能够用到闭包,而这个过程,也能够被称之为柯理化,又或者称之为预参数,它们都是闭包的一种运用方式。

//这段代码能够类比一种场景,一个ul中有5个li,开发者收集到了5个li的节点后,for循环,经过addeventlistener分别给每个li绑定click事件,打印当前遍历的索引,效果雷同。
for(var i=1;i<6;i++){
    setTimeout(function(){
        console.log(i);
    },i*1000);
}
复制代码

以上代码咱们指望的效果是过1秒输出1,过2秒输出2,过3秒输出3,过4秒输出4,过5秒输出5,然而结果是都输出的6,由于var声明的变量,在for循环所在的做用域中只有一个,后面的每一次赋值都会覆盖前面的值(这个状况自己跟异步不要紧,只是异步了以后,凸显出了这个特色),当第五次循环完毕后,会再次自增1,变为6,只是6不知足循环条件,循环中断,而后打印出来的就都是6。 如今咱们利用闭包改造下,

for(var i=1;i<6;i++){
    (function(j){
        setTimeout(function(){
            console.log(j);
        },j*1000);
    })(i)
}
复制代码

如今就获得了咱们想要的结果,过1秒输出1,过2秒输出2,过3秒输出3,过4秒输出4,过5秒输出5。 至于缘由,上面关于闭包的分析里已经涵盖。

解决问题的方法永远不仅一个

事实上解决这个问题的方式不必定非要使用闭包,

for(let i=1;i<6;i++){
    setTimeout(function(){
        console.log(i);
    },i*1000);
}
复制代码

let以代码块{}为做用域边界,劫持做用域,使得每个块内的i都是惟一的,互不干涉。

除此以外,还有别的方法解决该问题,

for(var i=1;i<6;i++){
    setTimeout(function(j){
        console.log(j);
    },i*1000,i);
}
复制代码

这个缘由详见settimeout的使用方法,不作赘述。

关于IIFE函数

有必要补充一点,关于自执行函数,即IIFE函数的一些说明。 IIFE函数,虽然的确创造了闭包,可是因为它自己并无建立外部做用域,因此从严格上来说,它不像是闭包。 (虽然IIFE函数也可以引用到括号所在做用域的变量,但那是因为词法做用域查找规则存在的缘由,这个规则只是闭包的一部分。) (刚说了,闭包产生于其声明的时候和声明的位置,而IIFE在其声明的做用域内是被隐藏的、是找不到的,这也是其不像闭包的另外一个缘由。)但它能很多解决问题,这是最重要的

写在最后

须要声明的一点是,我不是一个教授者,我只是一个分享者、一个讨论者、一个学习者,有不一样的意见或新的想法,提出来,咱们一块儿研究。分享的同时,并不仅是被分享者在学习进步,分享者亦是。

知识遍地,拾到了就是你的。

既然有用,不妨点赞,让更多的人了解、学习并提高。

相关文章
相关标签/搜索