老司机也翻车的闭包

前置知识

es6之前,js中变量作用域分为两种:全局作用域、局部作用域
学习闭包之前需要先了解作用域及变量提升的概念。《JS变量作用域&作用域链》,《js变量提升》

通过了解变量作用域我们知道,js的变量作用域很特殊,采用的是“词法作用域”。
子作用域可以访问父作用域的变量。
但是父作用域无法访问到子作用域的变量。

调用栈:
我们在执行一个函数时,如果这个函数又调用了另外一个函数,而这个“另外一个函数”也调用了“另外一个函数”,便形成了一系列的调用栈

function fn1() {
    fn2()
}
function fn2() {
    fn3()
}
function fn3() {
    fn4()
}
function fn4() {
    console.log('fn4')
}
fn1()

调用栈的原则是先进后出,后进先出。
fn1 先入栈,fn1 调用fn2,fn2 入栈,……,直到 fn4 执行完成,fn4 先出栈,fn3,fn2,fn1 分别出栈。
正常来讲,函数执行完毕出栈时,函数内局部变量会在下一个垃圾回收节点被回收,该函数对应的执行上下文会被销毁。
重点:这也就是我们在外界无法访问函数内部定义的变量的原因。
也就是说,只有在函数执行时,相关函数可以访问该变量,该变量在预编译阶段进行创建,在执行阶段进行激活,在函数执行完毕后,相关上下文被销毁。

为何使用闭包

但是出于一些原因,有时候我们需要得到函数内部的局部变量,通过上面的解释知道常规的手段是不行的,
那就使用非常规手段,就是让无数人翻车的闭包

何为闭包

闭包的概念:闭包的概念也可以理解为函数的概念,即
函数对象可以通过作用域链关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中成为“闭包”。
这句话是犀牛书8.6节闭包中的一段定义,可能过于官方,很多人都不太理解,那我们把这句话再翻译一下:
一个函数内部的函数可以访问到外部函数的变量。
再换句话说就是:
函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局环境下可访问,就形成了闭包。
从技术角度来说,所有的JavaScript函数都是闭包。
注:闭包函数内不一定要有return,如没有 return 那么就要将一个内部函数赋值给一个全局变量,否则没有意义。当然也可以返回一个对象(见最后一个栗子)。

老司机来了,快上车

下面通过几个栗子,让大家快速了解闭包

  • 栗子1
function fn(){
    var a = 5;
}

a是函数fn的局部变量,在外部是无法访问到的,但是由于子作用域可以访问父作用域的变量,我们将代码简单修改代码↓

function fn(){
    var a = 5;
    function fn2(){
        console.log(a);
    }
}

在函数fn内部定义函数fn2,fn2内部可以访问到a变量,那是不是可以将函数fn2作为返回值,这样是不是就可以在函数fn外部获取到变量a了,再改写代码↓

function fn(){
    var a=5;
    return function(){
        console.log(a);
    }
}
var fn3 = fn();
fn3(); //5 

将fn函数内部函数作为返回值,然后在函数fn外部调用返回的函数,正确输出a。这样就实现了我们最开始的需求(在函数外部拿到函数的局部变量)。

为什么会这样?

首先再复习一遍闭包定义,“一个函数的内部函数可以拿到外部函数的变量”。
再具体一点就是:一个函数的内部函数可以拿到外部函数的变量,然后将这个内部函数作为返回值返回
这样在函数外部调用返回的函数时同样可以拿到函数内部的这个变量,这就是闭包

什么原理?

一个普通的函数在执行完后,上下文即被销毁,内部的变量都会被释放,但是这在个栗子中,js引擎发现返回的函数中使用了变量a,并且这个返回的函数在外部是有可能被执行的,所以变量a没有被释放,而是放到了一个只有这个返回的函数可以访问到的地方,此时a变量可以且只能被这个函数访问,每次调用fn()都会创建一个新的作用域链和一个新的私有变量。

到这你还是有点懵,没理解,不用怕,刚接触都会懵,将上面的栗子反复看几遍,总会有所收获的。
如果到这你都能看懂,那么恭喜你,你已经掌握了闭包的基础用法。系好安全带,开始飙车了。

  • 栗子2
function fn(){
    var a = 1;
    return  function(){
        a++;
        console.log(a);
    }
}
var fn2 = fn();
fn2(); //2
fn2(); //3
fn2(); //4

这里可以看到,我们不光可以获取到fn函数内的局部变量a,还可以对其进行修改。因为变量a是一直存放在内存中fn2函数可以访问到的地方
再升级下代码↓

  • 栗子3
function fn() {
    var a = 1;
    return function() {
        a ++;
        console.log(a);
    }
}
var fn1 = fn();
fn1(); //2
var fn2 = fn();
fn2(); //2
fn2(); //3
var fn3 = fn();
fn3(); //2

上面代码将fn的返回函数分别赋给3个对象,fn1、fn2、fn3,
三次赋值相当于初始化3个a变量放到内存中,分别只供fn1、fn2、fn3使用。
fn1、fn2、fn3函数在执行的时候,分别访问的是各自区域内的a变量,3个区域不共享。
原理:每次调用fn()都会创建一个新的作用域链和一个新的私有变量。

  • 栗子4
//第一题
function q1() {
    var a = {};
    ruturn function() {
        return a;
    }
}
var t1 = q1();
var o1 = t1();
var o2 = t1();
console.log(o1 == o2);//true

//第二题
function q2() {
    var a = {};
    ruturn function() {
        return a;
    }
}
var t1 = q2();
var t2 = q2();
var o1 = t1();
var o2 = t2();
console.log(o1 == o2);//false

分别输出true和false,不需要解释了吧。

  • 栗子5
    一些情况下,需要返回多个函数,这时候就用到返回对象
function fn() {
    var a = 10;
    return {
        add:function(addNum) {
            a += addNum;
            console.log(a);
        },
        sub:function(subNum) {
            a -= subNum;
            console.log(a);
        }
    }
}

var obj1 = fn();
obj1.add(5); // 15
obj1.add(20); // 35
obj1.sub(3); // 32

var obj2 = fn();
obj2.add(2); // 12
obj2.add(6); // 18

返回对象和返回函数用法基本一致,变量在不同对象间依然不共享。

  • 栗子6
const foo = () => {
    var arr = []
    var i
    
    for (i = 0; i < 10; i++) {
        arr[i] = function () {
        console.log(i)
        }
    }

    return arr[0]
}

foo()()

输出10。

  • 栗子7
var fn = null
const foo = () => {
    var a = 2
    function innerFoo() {
        console.log(a)
    }
    fn = innerFoo
}

const bar = () => {
    fn()
}

foo()
bar()

输出2
在 foo 函数内,将 innerFoo 函数赋值给 fn,fn 是全局变量,这就导致了 foo 的变量对象 a 也被保留了下来。
这个栗子就说明了,闭包函数可以没有显式的 return 。

  • 栗子8
var fn = null
const foo = () => {
    var a = 2
    function innerFoo() {
        console.log(c)
        console.log(a)
    }
    fn = innerFoo
}

const bar = () => {
    var c = 100
    fn()
}

foo()
bar()

栗子8是栗子7的改版。
执行结果为:报错 ReferenceError: c is not defined。
变量 c 并不在其作用域链上,c 只是 bar 函数的内部变量。

说翻车就翻车——内存管理

内存管理就是:对内存生命周期的管理。包含分配内存空间、读写内存、释放内存空间。

var foo = 'bar' // 在栈内存中给变量分配空间
alert(foo)  // 使用内存
foo = null // 释放内存空间

JavaScript依赖宿主浏览器的垃圾回收机制
如内存管理不当极易造成内存泄漏,指内存空间明明已经不再被使用,但由于某种原因并没有被释放的现象。

  • 栗子9
var element = document.getElementById('element')
element.innerHTML = '<button id="btn1">按钮</button>'

var btn = document.getElementById('btn1')
btn.addEventListener('click', function () {
    // ...
})

element.innerHTML = ''

栗子9中,button元素已经从dom中移除,但是其事件处理句柄还在,所以依然无法被回收,需要手动removeEventListener。需要注意的是,addEventListener()添加的匿名函数无法移除,所以要尽量传入具名函数。

另外闭包使用不当,极易造成内存泄漏,如果不再使用,需要手动清除。
之前说到闭包中的变量在函数执行完后不会被释放,还是存放在内存中,势必会造成内存浪了。严重可导致内存泄漏。
没办法直接释放这个变量,如需释放变量就释放访问变量的函数

  • 栗子10
function foo() {
    let a = 123
    
    function bar() { alert(a) }
    
    return bar
}

let bar = foo()

此时 a 变量会被保存在内存中,如果需要释放则执行

bar = null

释放掉对闭包函数的引用后,垃圾回收机制就会回收变量a。

总结

很多人学完闭包都会有一个这样的问题,“我知道什么是闭包,可是闭包是做什么的呢?”

  • 闭包的应用场景
    • 模块化
    • 防止变量被破坏
    • Redux中间件实现机制

设计模式中的单例模式就可以依托闭包来实现

function Person() {}

const getSingleInstance = (function () {
    var singleInstance
    return function () {
        if (singleInstance) {
            return singleInstance
        }
        return singleInstance = new Person()
    }
})()

const p1 = new getSingleInstance()
const p2 = new getSingleInstance()
console.log(p1 === p2) // true

singleInstance 为闭包变量 ,这正是单例模式的体现。

一个闭包引申出了内存、执行上下文、作用域、作用域链等概念。虽说都是基础,但是每个概念都能衍生出很多知识点。难怪老司机也爱翻车。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容