前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >理解JavaScript的闭包

理解JavaScript的闭包

原创
作者头像
伯爵
修改2019-10-17 10:41:39
6760
修改2019-10-17 10:41:39
举报
文章被收录于专栏:前端小菜鸟前端小菜鸟

闭包(Closure)又称为词法闭包和函数闭包,由函数创造的一个词法作用域,创建在词法作用域的变量被引用后,可以在这个词法环境之外使用。

词法作用域

在深入学习闭包之前,我们需要了解与闭包相关的基本知识,词法作用域。

JS的作用域的概念:引擎用来管理当前作用域和嵌套的子作用域中根据标识符名称进行变量查找的一套规则。

一般语言在编译的编译过程主要分为三步:

  1. 分词:将字符串组成的JavaScript代码分解成有意义的代码块(词法单元)。
  2. 解析:将词法单元流解析成抽象语法树(AST)。
  3. 代码生成:将AST转化成可执行的代码。

词法作用域是发生在编译第一阶段即词法阶段,词法作用域代码是由定义变量,函数和块作用域的位置所决定的。我们可以通过JavaScript函数实例理解词法作用域:

代码语言:txt
复制
function fun(a) {
   var b = a + 2;
   function secFun(c) {
      console.log(a, b , c)
   }
   secFun(b + 4)
}
fun(1) ; // 1, 3, 7

在函数执行过程中,函数创建了逐级嵌套的作用域:

  1. 首先是一个全局作用域,包含一个标识符:fun
  2. 执行fun函数,这时候我们在fun函数内部创建了新的作用域:包含三个标识符:secFun,a,b,
  3. 执行secFun函数,我们在secFun函数内部创建新的作用域:包含一个标识符:c

我们可以看到,JavaScript的词法作用域在编译之前已经确定了,由代码书写的位置决定。

什么是闭包

下面的函数是一个完整的闭包:

代码语言:txt
复制
function closureFun() {
   var name = "closureFun";
   function getName() {
      console.log(name)
   }
   return getName;
}
var nameFun = closureFun();
nameFun();

我们结合JS的词法作用域的内容,分析一下闭包的执行过程:

  1. 全局作用域,包含两个标识符:nameFun ,closureFun
  2. 执行closureFun创建新的作用域,包含两个标识符:name ,getName,通过scopeChain关联到全局作用域
  3. 以值的形式返回内部标识符getName的函数,赋值给变量nameFun
  4. 执行nameFun,查询执行标识符getName,实际上调用的是内部函数getName
  5. getName被执行,创建新的作用域,包含一个表示符:name,通过scopeChain关联到函数closureFun的作用域
  6. name变量在赋值的时候,通过scopeChain(作用域链)查询,当前作用域没有声明该变量,则沿着作用域链向上逐级查询,执行到closureFun函数作用域,查询到变量name,打印执行结果。

我们知道,我们在执行函数的时候,会创建一个新的作用域,称为私有作用域,当函数执行完毕之后为了节约内存JS引擎会将这个私有作用域会被销毁,定义在私有作用域的函数和变量都会被清除。

但是在定义函数词法作用域以外执行函数,可以保持函数内部定义的私有作用域,形成一个闭包。更直观的理解,我们可以在函数closureFun外面访问到函数内部定义的变量。

我们也可以这样理解闭包:访问并记住词法作用域的函数叫闭包。

闭包的应用

在前端开发过程中,我们经常使用的闭包应用包括:匿名立即执行函数,存储变量,封装私有变量。

  • 匿名执行函数
代码语言:txt
复制
<ul>
   <li>dom1</li>
   <li>dom2</li>
   <li>dom3</li>
</ul>

上面的html代码中,我们设定了一个常见的需要,我们需要当我们点击li元素的时候,获取当前li元素的下标,因为根据li元素的名称可以获取li元素的理解,所以我们的需求可以抽象:

  1. 获取li元素的集合
  2. 遍历集合给每个元素绑定click事件
  3. 获取当前的元素下标index即可

根据上面的需求转化,我们很容易写出来下面的解决方案:

代码语言:txt
复制
let doms = document.getElementsByTagName('li');
for (var i = 0; i < doms.length; i++) {
    doms[i].onclick = function() {
        alert(i)
    }
}

当时我们点击DOM元素的时候,发现这个是行不通的方案,我们每次获取到的下标都是i变量最后的值。我们获取到的下标i是一个引用值,执行循环运行完成的值。

我们可以使用闭包,完成上面的需求:

代码语言:txt
复制
let doms = document.getElementsByTagName('li');
for (var i = 0; i < doms.length; i++) {
    (function(index) {
        doms[index].onclick = function() {
            alert(index)
        }
    })(i)
}

我们通过匿名执行函数,每次遍历获取当前的下标i,匿名函数在内部的作用域获取标识符index,保存下标的副本到变量index,这样每个匿名函数都有一个内部的变量存储执行时的下标i的值。

  • 变量存储和管理

在我们开发过程中,我们可以使用闭包的特性创建常量:

代码语言:txt
复制
const person = () => {
    let name= "javaScript"
    return () => {
        return name;
    }
 }
let personName= person ();
personName() // javaScript
personName() // javaScript

这样我们无论如何去调用personName函数,始终获取到name的变量值,并且无法修改,这样我们就可以在JS开发过程中使用闭包来完成常量的封装。

代码语言:txt
复制
const personRun = () => {
    let stepCount = 0;
    return () => {
        return ++stepCount
    }
 }
let run = personRun();
run() //1
run() //2

我们可以使用personRun 函数词法作用域内的变量stepCount ,personRun函数运行,返回引用了stepCount变量的内部函数本身,赋值给外部run标识符,我们可以通过调用run函数完成对stepCount变量的管理。

  • 封装私有变量
代码语言:javascript
复制
const jsVersion = () => {
    let _version = 'ES5';

    _getVerson = () => _version;
    _setVerson = (version) => {
        _version = version
    }

    return {
        setVersion: _setVerson,
        getVerson: _getVerson
    }
}
let jsv = jsVersion();
jsv.getVerson() //ES5
jsv.setVersion('ES6')
jsv.getVerson() //ES6

封装私有变量是闭包的一个很实用的应用,也可以理解成闭包的对变量的一种管理,原理是在闭包创建的词法作用域内,外部无法直接访问词法作用域内部定义的变量,也就是说词法作用域定义的变量对外部是完全屏蔽的,相当于强语言类型的私有变量的概念,我们可以通过对外提供接口的方式操作内部封装的私有变量。

我们需要明白闭包使用是有代价的,因为闭包内变量的引用无法被自动释放,所以容易造成内存泄漏问题。

参考

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 词法作用域
  • 什么是闭包
  • 闭包的应用
  • 参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档