今天是系列第二篇,主要讲一下闭包相关的问题。
我认为闭包的问题,实质是作用域的问题,所以我会先从作用域相关的问题讲起。

什么是作用域?

作用域就是变量可访问的管理范围(或者说变量的管理范围)。举个例子:班级里要准备大扫除了,班长给班级的值日生安排打扫任务。小明同学负责打扫厕所,小红和小丁同学负责打扫教室,小龙同学负责操场的卫生。那么我们可以说小明的管理范围就是厕所区域,小龙同学的管理范围是操场区域;换个说法就是小明的作用域是厕所,小龙的作用域是操场。

一般作用域可以分为词法作用域(也叫静态作用域)和动态作用域,而js中一般是词法作用域。

什么是词法作用域?

先来给个定义:词法作用域就是变量或者函数在定义的时候就确认了其管理范围,不会改变。<span style="color: red;">概括一下理解就是:只要函数看见的就能访问到。</span>干巴巴的概念看着没劲?来看个例子吧

function a(x){
    var y = x + 4
    function fn(z){
        console.log(z)
    }
    fn(10 * y)
}
a(4) //80
console.log(y) //y is not defined

上面是一个很常规的作用域的例子。a(4)输出的值是80,输出y的时候报错,报错信息是变量y没有找到。我们用作用域的知识分析一下这段代码:

  • 全局作用域:
  • 函数a作用域: x, y
  • 函数fn作用域: z

所以我们看到变量y的管理范围是在a函数中,而我们在全局作用域中输出y的值,显然是不在他的管理范围中,因此报错(找不到该变量)。
这里还有一个很重要的知识点,就是变量的生命周期。

什么是变量的生命周期?

我们把变量和人类比一下,比如说人的生命周期就是指人从出生到死亡的整个过程。所以说变量的生命周期就是从变量的创建到死亡的过程。从变量的生命周期上进行区分可以分成全局作用域和函数作用域。先给他们下个定义:

全局作用域

变量的作用域是全局的,称为全局作用域。

函数作用域

变量只在函数体中定义的,称为函数作用域

上面这个例子我们可以看到变量会在全局作用域和函数作用域下,那全局作用域中和函数作用域中的变量在程序运行过程中会经历一个怎样的过程呢?

  • 全局作用域中的变量一旦被定义,除非主动去销毁,否则在程序停止前将一直保存在内存中。
  • 函数作用域中的变量在函数运行中离开函数之后就会被销毁。

所以我们在全局作用域中找不到变量y,是因为在执行了a函数之后,变量y就不存在了。但是从技术实现上有一种方式能实现在离开函数之后还能函数作用域中的变量不被销毁,那就是闭包。

什么是闭包?

千呼万唤始出来,终于见到了本篇文章的主角了。上面说了要实现在离开函数之后还能访问函数作用域中的变量这样一个功能而引入了闭包这个东西。先不说闭包这个功能怎么实现,咱先给这个东西下个定义吧。

网上比较通俗的定义是:能够访问其他函数内部变量的函数。我对比了其他一些说法,还是这个靠谱一些。这个跟我们上面说的在离开函数之后还能访问函数作用域中的变量这个需求相关联。那它是怎么做到的呢?看栗子:

function fn(){
    var a = 10
    return function say(b){
        console.log(a + b)
    }
}
var getVal = fn()
getVal(5)

这是一个很典型的闭包写法:函数内部返回一个的声明函数。在执行fn之后将返回值赋值给getVal,然后执行getVal。我们可以看到,在执行getVal的时候其实执行的就是函数say,而函数say的作用域是在声明它的时候就确定的。我们可以看到它的词法作用域中有权限访问a,b变量,最后打印a + b的值,打印出15。

所以这里的闭包其实是在做这样的一件事:在执行完fn函数后,它还有能力把变量a保存在内存中,让变量a的生命周期跟全局变量一样永远存储在内存中。但是变量a又是私有的,它只有getVal这个函数才能访问到它,它又不像其他全局变量一样可以被随意访问,被随意修改。那么很自然的我们可以看到闭包有这样的优点。

闭包的优点

  • 能够在离开函数之后继续访问该函数的变量,变量一直保存在内存中。
  • 闭包中的变量是私有的,只有闭包函数才有权限访问它。不会被外面的变量和方法给污染。

闭包的常见用法

  • 使用模块化的封装

下面是index.js引用m.js模块的内容

// 文件:m.js
(function(){
    var name ='Lucy'
    var getName = function(){
        ...
    }
    exports.mode = {
        name,
        getName
    }
})()

// 文件index.js
var {mode} = require('./m.js')
mode.getName()
  • 模仿块级作用域
// 改造前:输出5个5
for(var i=0; i<5; i++){
    console.log(i)
}

// 改造后:输出0-4
for(var i=0; i<5; i++){
    (function(j){
        console.log(j)
    })(i)
}

闭包的缺陷

  • 会增加对内存的使用量,影响性能
  • 不正确的使用闭包会造成内存泄漏

总结

回顾一下本章的主要内容

  • 作用域:指变量可访问的管理范围,与生活中的区域管理结合起来理解就比较容易了。
  • 词法作用域:变量在定义的时候就确定了其管理范围,不会再发生变化。
  • 变量的生命周期:变量从出生到被销毁的过程

    • 全局作用域:作用域在全局的
    • 函数作用域:变量只在函数体中定义的
  • 闭包:能够访问其他函数内部变量的函数
  • 闭包的本质就是在执行函数返回内部的函数赋值给变量A,根据词法作用域,变量A跟内部函数一样有权限对其作用域中的变量进行访问,所以闭包内的变量没有被销毁,依然保存在内存中,从而实现了外部函数对内部变量的访问。

文中还有一些比如变量回收,作用域链,执行上下文,变量提升等知识点因为篇幅问题没有详细的展开,这个在后期会有相关主题的补充。

参考文章


摩根
11 声望2 粉丝

前端工程师