一篇文章看懂JS闭包,都要2020年了,你怎么能还不懂闭包?

 壹 ❀ 引javascript

我以为每一位JavaScript工做者都没法避免与闭包打交道,就算在实际开发中不使用但面试中被问及也是常态了。就我而言对于闭包的理解仅止步于一些概念,看到相关代码我知道这是个闭包,但闭包能解决哪些问题场景我了解的并很少,这也是我想整理一篇闭包的缘由。咱们来看一段代码,很明显这是一个闭包,那么请问闭包指代的是下方代码中的哪一部分呢?本文开始。html

function outer() {
    let name = '听风是风';

    function insider() {
        console.log(`欢迎来到${name}的博客`);
    };
    return insider;
};
outer()(); //欢迎来到听风是风的博客

 贰 ❀ 什么是闭包?vue

若是在面试中被问及什么是闭包,大部分状况下获得的答复是(至少我之前是)A函数嵌套B函数,B函数使用了A函数的内部变量,且A函数返回B函数,这就是闭包java

这段描述固然没问题,那么为了让下次面试回答的更为漂亮,就让咱们从更专业的角度从新认识闭包。git

1.闭包起源angularjs

闭包翻译自英文单词 closure ([ˈkloʊʒər] 倒闭,关闭,停业),闭包的概念最先出如今1964 年的学术期刊《The Computer Journal》上,由 P. J. Landin The mechanical evaluation of expressions一文中说起。github

在这个JavaScript,Java甚至C语言都还没诞生的60年代,主流的编程语言是基于 lambda 演算的函数式编程语言。而在这个最先的闭包概念描述中使用了大量函数式术语,想传达的意思大概是带有一系列信息的λ表达式,对于函数式语言来讲λ表达式就是函数面试

早期的闭包由环境(执行环境、标识符列表)与表达式两部分组成,而将此组成对应到JavaScript中,环境部分正好对应了JS执行上下文中的函数词法环境与标识符列表表达式部分则对应了JS中的函数体express

因此到这里,咱们知道JavaScript中的闭包与最初闭包概念是高度吻合的,将带有一系列信息的λ表达式对应到JavaScript中来,所谓闭包其实就是一个自带了执行环境(由外层函数提供,即使外层函数销毁依旧能够访问)的特殊函数;那么回到文章开头的提问,这段代码中的闭包指代的就是内部函数 insider,而非外部函数outer所包含的范围,这一点必定得弄清楚。编程

2.闭包的特征

了解了JavaScript闭包的起源,咱们接着来看看其它文档对于闭包的解释,加深印象并汇总一下闭包有哪些特性。

百度百科:

闭包就是可以读取其余函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,因此闭包能够理解成“定义在一个函数内部的函数“。

《JavaScript高级编程指南》:

闭包是指有权访问另一个函数做用域中的变量的函数。

MDN(几年前的解释,现已更新):

闭包是指那些可以访问自由变量的函数。

MDN早期解释是比较有趣的,何为自由变量?自由变量是指在函数中使用的,但既不是函数arguments参数也不是函数局部变量的变量。看个例子:

let a = 1;//自由变量

function fn() {
    console.log(a);
};
fn(); //1

好比这个例子中,变量 a 不属于函数 fn,但函数 fn 由于做用域链的关系,仍是能够正常使用变量 a。

说到这里确定有同窗就疑惑了,MDN的描述不对吧,首先 fn 是一个函数,其次 fn 用到了自由变量 a,那岂不是 fn 也是个闭包?

事实就是如此,在《JavaScript权威指南》一书中明确提到,从理论角度来讲,JavaScript中全部的函数都是闭包....

是否是有点颠覆了你对于闭包的认知?上面说的是理论角度,站在技术实践角度来讲,闭包无非知足如下两点:

1、闭包首先得是一个函数

2、闭包能访问外部函数做用域中的自由变量即便外部函数上下文已销毁

因此MDN如今对于闭包的描述已修改成“闭包是由函数以及建立该函数的词法环境组合而成,这个环境包含了这个闭包建立时所能访问的全部局部变量”了,这不就符合了咱们在前面对于闭包特征的理解。咱们经过一个例子加深对闭包特征的印象:

let fn = function () {
    let num = 1; //自由变量
    return {
        a: function () {
            console.log(num);
        },
        b: function () {
            num++;
        }
    };
};

let closure = fn();
//到这里outer函数已执行完毕,执行上下文被释放
closure.a(); // 1

在上方的例子中,外层函数fn执行返回了两个闭包 a,b。咱们知道函数每次被调用执行都会建立一个新的执行上下文,当函数执行完毕函数执行上下文被弹出执行栈并销毁,因此在 let closure = fn() 执行完毕时函数fn的执行上下文已不复存在,但咱们执行closure.a()能够看到依旧能访问到外层函数的局部变量num。

为了让这种感受更为强烈,咱们直接销毁掉函数fn再次调用闭包函数,能够看到闭包不只是访问甚至还能操做外层函数中的变量。

fn = null;
closure.b();
closure.a(); // 2

是否是很神奇?那为何外层函数上下文都销毁了,闭包还能访问到自由变量呢,这就得说说闭包做用域链的特别之处了。

 叁 ❀ 用奇妙的执行上下文看闭包

JavaScript中的做用域是指变量与函数的做用范围。当在某做用域使用某变量时,首先会在本做用域的标识符中查找有没有,若是没有就会去父级找,尚未就一直找到源头window为止(window也没有就报错),这个查找的过程便造成了咱们所说的做用域链。

那么在JavaScript中这个过程具体是怎么样的呢,我在 一篇文章看懂JS执行上下文 一文中有详细描述执行上下文的执行过程,因此这里我就简单描述下,咱们先来看个例子:

let scope = "global scope";

function checkscope() {
    //这是一个自由变量
    let scope = "local scope";
    //这是一个闭包
    function f() {
        console.log(scope);
    };
    return f;
};

let foo = checkscope();
foo();

咱们使用伪代码分别表示执行栈中上下文的变化,以及上下文建立的过程,首先执行栈中永远都会存在一个全局执行上下文

//建立全局上下文
ECStack = [GlobalExectionContext];

此时全局上下文中存在两个变量scope、foo与一个函数checkscope,上下文用伪代码表示具体是这样:

//全局上下文建立
GlobalExectionContext = {
    // this指向全局对象
    ThisBinding: < Global Object > ,
    // 词法环境
    LexicalEnvironment: {
        //环境记录
        EnvironmentRecord: {
            Type: "Object", // 对象环境记录
            // 标识符绑定在这里 函数,let const建立的变量在这
            scope: < uninitialized > ,
            foo: < uninitialized > ,
            checkscope: < func >
        }
        // 全局环境外部环境引入为null
        outer: < null >
    }
}

全局上下文建立阶段结束,进入执行阶段,全局执行上下文的标识符中像scope、foo之类的变量被赋值,而后开始执行checkscope函数,因而一个新的函数执行上下文被建立,按照执行栈前进后出的特色,执行栈如今是这样:

ECStack = [checkscopeExectionContext,GlobalExectionContext];

那么checkscope函数执行上下文也进入建立阶段,它的上下文咱们一样用伪代码表示:

// 函数执行上下文
checkscopeExectionContext = {
    //因为函数是默认调用 this绑定一样是全局对象
    ThisBinding: < Global Object > ,
    // 词法环境
    LexicalEnvironment: {
        EnvironmentRecord: {
            Type: "Declarative", // 声明性环境记录
            // 标识符绑定在这里  arguments与局部变量在这
            Arguments: {},
            scope: < uninitialized > ,
            f: < func >
        },
        // 外部环境引入记录为</Global>
        outer: < GlobalLexicalEnvironment >
    }
}

因为 checkscope() 等同于 window.checkscope() ,因此在 checkExectionContext 中this指向全局,并且外部环境引用outer也指向了全局(做用域链),其次在标识符中咱们能够看到记录了形参arguments对象以及一个变量scope与一个函数 f 。

函数 checkscope 执行到返回返回函数 f 时,函数执行完毕,checkscope 的执行上下文被弹出执行栈,因此此时执行栈中又只剩下全局执行上下文:

ECStack = [GlobalExectionContext];

代码执行又走到了foo(),foo函数被执行,因而foo的执行上下文被建立,执行栈中如今是这样:

ECStack = [fooExectionContext, GlobalExectionContext];

foo的执行上下文是这样:

fooExectionContext = {
    //因为函数是默认调用 this绑定一样是全局对象
    ThisBinding: < Global Object > ,
    // 词法环境
    LexicalEnvironment: {
        EnvironmentRecord: {
            Type: "Declarative", // 声明性环境记录
            // 标识符绑定在这里  arguments与局部变量在这
            Arguments: {},
        },
        // 外部环境引入记录为</checkscope>
        outer: < checkscopeEnvironment >
    }
}

foo执行也等同因而window调用,因此this一样指向全局window,但outer外部环境引入有点不一样,这里指向了外层函数 checkscope,为啥是checkscope?

咱们知道JavaScript采用的是词法做用域,也就是静态做用域,函数的做用域在定义时就肯定了,而不是执行时肯定。看个小例子来巩固下静态做用域:

var a = 1;

function fn1() {
    console.log(a);
};

function fn2() {
    var a = 2;
    fn1(a);
};

fn2(); //1

这里输出1,这是由于 fn1 定义在全局做用域中,它能访问的做用域就是全局,即使咱们在 fn2中 调用,它依旧只能访问定义它地方的做用域。

明白了这个概念,这下总能理解foo执行上下文outer外部环境引入为啥是 checkscopeExectionContext 了吧。

那也不对啊,如今执行栈中一共就 fooExectionContext 与 GlobalExectionContext 这两个,checkscopeExectionContext 早被释放了啊,怎么还能访问到 checkscope 中的变量。

正常来讲确实是不能够,可是JavaScript骚就骚在这里,即便 checkscope 执行上下文被释放,由于闭包 foo 外部环境 outer 的引用,从而让 checkscope做用域中的变量依旧存活在内存中,没法被释放。

这也是为何谈到闭包咱们老是强调手动释放自由变量

这也是为何文章开头咱们说闭包是自带了执行环境的函数

那么闭包的理解就点到这里,让咱们总结一句,闭包是指能使用其它做用域自由变量的函数,即便做用域已销毁。

若是你在阅读上下文这段有疑惑,若是你好奇为何var存在变量声明提高而let没有,仍是强烈阅读博主这篇文章 一篇文章看懂JS执行上下文 

 肆 ❀ 闭包有什么用?

说闭包聊闭包,结果闭包有啥用都不知道,甚至遇到了一个闭包第一时间都没反应过来这是闭包,这就是我之前的常态。那么咱们专门说说闭包有啥用,无论用不用得上,做为了解也没坏处。

1.模拟私有属性、方法

在Java这类编程语言中是支持建立私有属性与方法的,所谓私有属性方法其实就是这些属性方法只能被同一个类中的其它方法所调用,可是JavaScript中并未提供专门用于建立私有属性的方法,但咱们能够经过闭包模拟它,好比:

let fn = (function () {
    var privateCounter = 0;

    function changeBy(val) {
        privateCounter += val;
    };
    return {
        increment: function () {
            changeBy(1);
        },
        decrement: function () {
            changeBy(-1);
        },
        value: function () {
            console.log(privateCounter);
        }
    };
})();
Counter.value(); //0
Counter.increment();
Counter.increment();
Counter.value(); //2
Counter.decrement();
Counter.value(); //1

这个例子中咱们经过自执行函数返回了一个对象,这个对象中包含了三个闭包方法,除了这三个方法能访问变量privateCounter与 changeBy函数外,你没法再经过其它手段操做它们。

构造函数你们不陌生吧,构造函数中也有闭包,直接上例子:

function Echo(name) {
    //这是一个私有属性
    var age = 26;
    //这些是构造器属性
    this.name = name;
    this.hello = function () {
        console.log(`个人名字是${this.name},我今年${age}了`);
    };
};
var person = new Echo('听风是风');
person.hello();//个人名字是听风是风,我今年26了

若是你们对于我说构造函数中使用了闭包有疑问,能够阅读博主这篇文章 js new一个对象的过程,实现一个简单的new方法 这篇文章,其实new过程都会隐性返回一个对象,这个对象中也包含了构造函数中构造器属性中的方法。

若是某个属性方法在全部实例中都须要使用,咱们通常推荐加在构造函数的prototype原型链上,还有种作法就是利用私有属性。好比这个例子中全部实例均可以正常使用变量 age。同时咱们将age称为私有属性的同时,咱们也会将this.hello称为特权方法,由于你只有经过这个方法才能访问被保护的私有属性age啊。

我在JavaScript模式 精读JavaScript模式(七),命名空间模式,私有成员与静态成员 这篇文章中有介绍私有属性方法,静态属性法,特权方法,有兴趣也能够读读看(内链推的飞起...)。

2.工厂函数

什么是工厂函数?工厂函数给个人感受与构造函数或者class相似,调用工厂函数就会生产该类(构造函数)的实例,咱们举一个MDN的简单例子:

function makeAdder(x) {
    return function (y) {
        console.log(x + y);
    };
};

var a = makeAdder(5);
var b = makeAdder(10);
a(2); // 7
b(2); // 12

在这个例子中,咱们利用了闭包自带执行环境的特性(即便外层做用域已销毁),仅仅使用一个形参完成了两个形参求和的骚操做,是否是很奈斯。

3.其它应用

闭包其实在不少框架中都是随处可见的,好比angularjs中能够自定义过滤器,而自定义过滤器的方式一样也是一个闭包,好比这样:

angular.module('myApp',[])
    .filter('filterName',function () {
        return function () {
            //do something
        };
    })

若是我没记错,vue建立过滤器的方式貌似也是闭包....

 伍 ❀ 闭包使用注意

说了这么多,闭包总给咱们一种高逼格的感受,其实说到底也就是自带执行环境的函数而已,若是你要使用闭包有些地方还真的注意一下。

1.闭包的性能与内存占用

咱们已经知道了闭包是自带执行环境的函数,相比普通函数,闭包对于内存的占用还真就比普通函数大,毕竟外层函数的自由变量没法释放。

function bindEvent(){
    let ele = document.querySelector('.ele');
    ele.onclick = function () {
        console.log(ele.style.color);
    };
};
bindEvent();

好比这个例子中,因为点击事件中使用到了外层函数中的DOM ele,致使 ele 始终没法释放,你们都知道操做DOM原本是件不太友好的事情,你如今操道别人不说,还抓着不放了,你良心不会痛?

好比这个例子你要获取color属性,那就单独复制一份color属性,在外层函数执行完毕后手动释放ele,像这样:

function bindEvent() {
    let ele = document.querySelector('.ele');
    let color = ele.style.color;
    ele.onclick = function () {
        console.log(color);
    };
    ele = null;
};
bindEvent();

2.闭包中的this

闭包中的this也会让人产生误解,咱们在前面说了静态做用域的概念,即函数做用域在定义时就已经肯定了,而不是调用时肯定。this这个东西咱们也知道,this在最终调用时才肯定,而不是定义时肯定,跟静态做用域有点相反。

var name = "听风是风";
var obj = {
    name: "行星飞行",
    sayName: function () {
        return function () {
            console.log(this.name);
        };
    }
};

obj.sayName()(); //

猜猜这里输出什么,很遗憾这里输出外层的听风是风,具体为何其实在上文中经过执行上下文看闭包就解释了,下面的解释看不懂就回去从新读一遍。

函数每次执行都会建立执行上下文,而上下文又由this、词法环境、变量环境以及外部环境引用等组成,咱们只说做用域是能够继承的,没人说this指向也能够继承吧。咱们上面的代码改改:

var a = obj.sayName()
a(); //等同于window.a()

this指向是不能像做用域同样存在链式的,执行第二个方法时实际上是window在调用,这下明白没?

那么有同窗就要问了,那我要用在闭包中使用外层函数的this咋办,这还不简单,保存this呗:

var name = "听风是风";
var obj = {
    name: "行星飞行",
    sayName: function () {
        var that = this;
        return function () {
            console.log(that.name);
        };
    }
};
obj.sayName()();//行星飞行

 陆 ❀ 总

那么到这里,咱们从闭包的起源解释了JavaScript闭包的来源,了解到闭包其实就是自带了执行环境的函数,若是在之后的面试中有面试官问你闭包,我但愿你能经过在这里学到的知识秀的对方头皮发麻。

除了知道闭包的概念,咱们还从执行上下文的角度解释了为什么闭包还能使用已销毁父级函数的自由变量,并复习了做用域,做用域链以及静态做用域的概念。

说闭包用闭包,咱们介绍了几种常规的闭包用法,以及在实际使用中咱们应该注意的点。

那么到这里闭包文章就算写完了,下一篇写this。

若是你对于本文描述存在疑惑或者本文存在描述错误,欢迎留言讨论,我会在第一时间回复你,毕竟对于一个孤独的人来讲,收到陌生人的评论也是件开心的事。

 参考

 JavaScript深刻之从做用域链理解闭包

JavaScript深刻之闭包

深刻javascript——做用域和闭包

MDN

相关文章
相关标签/搜索