【万字总结】带你一步步吃透做用域、词法做用域、变量提高、闭包

本文是对《你不知道的JavaScript(上卷)》的浓缩总结,已经看过的童鞋能够简单再过一遍,没看过的童鞋建议仔细阅读 javascript

前言

每当咱们看完各类面试宝典,知识点总结,觉得掌握了某些概念以后,有没有去想过,这种“掌握”是否真能学以至用。当你将经过面试做为学习的目的之后,你会频频体会到知其然不知其因此然的滋味。看完《你不知道的JavaScript(上卷)》以后,让我对不少知识点有了通透的理解,遂将本身的理解结合书本整理分享给各位可能有我上述所描述问题的童鞋,也欢迎大佬们补充指正。java

读完本文,但愿你能够对以下几个知识点,从编译原理的基础上,有一种通透的理解:面试

  • 做用域
  • 词法做用域
  • 变量提高
  • 做用域闭包

何为做用域

在js中(ES6版本后)通常会存在3种做用域性能优化

  • 全局做用域

做用于全部代码执行的环境(整个 script 标签内部),或者一个独立的 js 文件闭包

  • 函数做用域

做用于函数内的代码环境,仅在该函数内部能够访问app

  • 块做用域

let和const建立的做用域异步

  • eval做用域

仅在严格模式下存在,下一章会讨论函数

做用域的概念想必你们必定都能滚瓜烂熟,即:工具

做用域就是一套规则,用于肯定在何处以及如何查找变量(标识符)的规则性能

这里咱们注意到有两个关键词在何处以及如何查找,下面咱们就把js的执行拆开成编译时运行时两个维度,来看看这两个维度分别作了些什么,做用域在其中又是如何将在何处以及如何查找实现的

编译时

首先JavaScript究竟是解释型语言仍是编译型语言,这里不作过多讨论,由于它同时具备两种类型的特性,本文按照《你不知道的JavaScript(上卷)》中的定义,将其解释为一种特殊的编译形语言来理解便可,编译时也能够理解为预编译时

在传统编译语言的流程中,程序中的一段源代码在执行以前会经历三个步骤,统称为“编译”:

  1. 分词/词法分析(Tokenizing/Lexing)

例如,考虑程序var a = 1;。这段程序一般会被分解成为下面这些词法单元:vara=1;

  1. 解析/语法分析(Parsing)

这个过程是将步骤1获得的词法单元流转换成一个由元素逐级嵌套所组成的表明了程序语法结构的树,就是你们所熟知的AST抽象语法树

  1. 代码生成

将AST转换为可执行代码的过程,例如var a = 1;的AST转化为一组机器指令,用来建立一个叫做a的变量(包括分配内存等),并将一个值储存在a中。

引用原文里的总结就是:

任何JavaScript代码片断在执行前都要进行编译(一般就在执行前)。所以,JavaScript编译器首先会对var a = 1;这段程序进行编译,而后作好执行它的准备,而且一般立刻就会执行它。

这里咱们着重看一下var a这个声明,当编译器发现这类声明时,会询问做用域是否已经有一个该名称的变量存在于同一个做用域的集合中。若是是,编译器会忽略该声明,继续进行编译;不然它会要求做用域在当前做用域的集合中声明一个新的变量,并命名为a

看到这里是否你就能够理解,为何js在运行时,能够提早调用以后声明的变量(变量提高),由于变量声明在编译时就已经完成了,因此运行时能够经过做用域找到该变量

看到这里其实咱们能够知道,对变量的建立(包括分配内存等)实际上是在编译过程当中就完成了,而这个过程当中,咱们的做用域其实就已经参与了进来,他会记得这些变量在何处,那么记得之后要如何查找呢,咱们接着往下看

运行时

一样以var a = 1;为例,引擎运行时会首先询问做用域,在当前的做用域集合中(一段程序中可能会有多个做用域,例如代码在函数内执行就是当前函数做用域)是否存在一个叫做a的变量。若是有则直接使用这个变量;若是没有则会继续查找该变量(聪明的你应该已经猜到了这就须要用到做用域链了,这块咱们以后再分析)。若是引擎最终找到了a变量,就会将1赋值给它。不然引擎就会抛出一个ReferenceError异常(关于异常后面会专门讲)。

那么做用域是如何找到变量a的呢,这就聊到了我面要讲的第二个关键词如何查找

做用域会协助引擎经过LHSRHS进行变量查找

LHS查询是找到变量的容器自己,RHS查询是取得变量的源值

例如var a = b;,首先经过LHS找到变量a,再经过RHS找到变量b的值,最后将b的值赋值给变量a。

对什么时候使用LHSRHS,一种比较好的理解方式就是:“赋值操做的目标是谁(LHS)”以及“谁是赋值操做的源头(RHS)

这里对于LHS或RHS只作一个概述,有兴趣的能够看看原文里的解读

做用域嵌套

当一个块或函数嵌套在另外一个块或函数中时,就发生了做用域的嵌套。

上一节提到了若是做用域当前做用域集合中没有找到要查找的变量会继续查找,其实就是在外层嵌套的做用域中继续查找,直到找到该变量,或抵达最外层的做用域(也就是全局做用域)为止。

例如:

function fn() {
    console.log(a)
}
var a = 1
复制代码

变量a嵌套在了函数fn的做用域中,当js引擎经过RHS(回忆一下为何是RHS)查找变量a进行输出时,当前做用域发现没有这个变量,就会去外层做用域查找,最后在全局做用域中找到了变量a

这种做用域一层层嵌套造成的链路,咱们就称之为做用域链

关于异常

咱们都知道,在非严格模式下,当咱们为一个未定义的变量赋值时,会隐式的建立一个全局变量

a = 1
console.log(a) //1
复制代码

而在严格模式时,会抛出ReferenceError异常

"use strict"
a = 1
console.log(a) //ReferenceError
复制代码

回忆一下变量的查询方式,不难知道这里使用的是LHS查询,在严格与非严格模式下表现是不一样的

那若是咱们是直接使用一个未声明的变量呢?

"use strict"
console.log(a) //ReferenceError
复制代码
console.log(a) //ReferenceError
复制代码

回忆一下变量的查询方式,不难知道这里使用的是RHS查询,在严格与非严格模式下表现是相同的

若是RHS查询找到了一个变量,可是你尝试对这个变量的值进行不合理的操做,好比试图对一个非函数类型的值进行函数调用,或者调用该变量不存在的方法等,就会抛出TypeError异常

var a = 1
a() //TypeError
a.sort() //TypeError
复制代码

引用原文里的总结就是:

ReferenceError同做用域判别失败相关,而TypeError则表明做用域判别成功了,可是对结果的操做是非法或不合理的。

总结

  1. 编译器在编译时对变量进行声明,做用域此时会记得这些变量在何处

  2. js引擎在运行时会在做用域的协助下,经过LHSRHS查询取得变量或其源值(所谓的如何查找)

  3. 查找时会经过做用域链嵌套做用域中一层层查找,直到找到全局做用域为止

  4. 不成功的RHS引用会抛出ReferenceError异常。不成功的LHS引用会隐式地建立一个全局变量(非严格模式下),或者抛出ReferenceError异常(严格模式下)

词法做用域

引用原文中的概念:

简单地说,词法做用域就是定义在词法阶段的做用域。换句话说,词法做用域是由你在写代码时将变量和函数写在哪里来决定的,所以当词法分析器处理代码时会保持做用域不变(大部分状况下是这样的)。

若是说做用域就是一套规则,词法做用域就是做用域的一种工做模型,一种在词法阶段的就定义的做用域

词法阶段

如图所示的代码片断有3个逐级嵌套的做用域 ABC。因为 词法做用域在写代码时就定义好了,所以在执行 C内部 console时,会首先在当前 做用域内寻找 abc三个变量,若是找不到再经过 做用域链逐级向上查找。所以首先在 C内找到了变量 c,再向上在 B内找到了变量 ab

这里咱们思考一个问题,若是我在C内从新定义了a会怎么样

function fn(a) {
    var b = a * 2
    function inner(c) {
        var a = 2
        console.log(a,b,c)
    }
    inner(b*2)
}
fn(1)
复制代码

答案是会直接使用C内的变量a,缘由是做用域查找始终从运行时所处的最内部做用域开始,直到碰见第一个匹配的标识符为止。这里就产生了遮蔽效应(内部的标识符“遮蔽”了外部的标识符)。也就意味着在C内永远没法访问到B内的变量a。除非被遮蔽的变量在全局做用域内,则能够经过window.xxx来访问。

欺骗词法

前面讲到词法做用域是在词法阶段就肯定了的,那么有没有可能在运行时来“修改”(也能够说欺骗)词法做用域呢?答案是能够,可是彻底不推荐!

eval

如下讨论仅在非严格模式内

引用原文里的描述:

eval(..)函数能够接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。换句话说,能够在你写的代码中用程序生成代码并运行,就好像代码是写在那个位置的同样。

咱们经过一段代码来理解一下:

function fn(str,a) {
    console.log(a,b)
}
var b = 2
fn(eval("var b = 3"), 1) //1,3
复制代码

因为eval内从新声明了变量b,经过遮蔽效应致使输出的b变成了3

with

with一般被看成重复引用同一个对象中的多个属性的快捷方式,能够不须要重复引用对象自己。他有一个反作用,会将变量泄漏到全局做用域。这里具体不作展开,由于with其实很是冷门且不推荐使用,并且在严格模式已经被彻底禁止了,有兴趣了解的童鞋能够看看原文中的解释。

性能影响

evalwith为何不建议使用,很大缘由就是对性能有影响,这里原文解释的比较清楚:

JavaScript引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于可以根据代码的词法进行静态分析,并预先肯定全部变量和函数的定义位置,才能在执行过程当中快速找到标识符。但若是引擎在代码中发现了eval(..)with,它只能简单地假设关于标识符位置的判断都是无效的,由于没法在词法分析阶段明确知道eval(..)会接收到什么代码,这些代码会如何对做用域进行修改,也没法知道传递给with用来建立新词法做用域的对象的内容究竟是什么。最悲观的状况是若是出现了eval(..)with,全部的优化可能都是无心义的,所以最简单的作法就是彻底不作任何优化。若是代码中大量使用eval(..)with,那么运行起来必定会变得很是慢。不管引擎多聪明,试图将这些悲观状况的反作用限制在最小范围内,也没法避免若是没有这些优化,代码会运行得更慢这个事实。

总结

  1. 词法做用域是由函数及变量声明的位置来决定的,在执行过程当中也会以此为做用域基准进行LHSRHS

  2. 能够经过eval(..)with词法做用域进行“欺骗”(非严格模式)

  3. 词法欺骗的反作用是致使js引擎性能优化失效,使程序运行变慢,所以不建议使用

函数做用域和块做用域

函数做用域

借用上一节的图片

咱们了解了 词法做用域的概念以后,就知道函数fn在声明时就确认了本身的 做用域B,该 做用域就是 函数做用域

函数做用域由函数在声明时所处的位置决定,与其在哪里被调用以及如何被调用无关

属于这个函数的所有变量均可以在整个函数的范围内使用及复用,而在函数以外是没法被访问的,这个特性有一个很是好的做用。这里咱们再引伸出一个概念:最小特权原则

最小特权原则,也叫最小受权或最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其余内容都“隐藏”起来,好比某个模块或对象的API设计

设想一个这样的场景,咱们须要向小明借钱

var money = 100
function borrowMoney(money) {
    return money
}
function xiaoming() {
    return borrowMoney(money)
}
xiaoming() //100
money = 1000000
xiaoming() //1000000
borrowMoney(5000000) //5000000
复制代码

不难发现咱们只要修改money的值即可以像小明借任意的钱,甚至咱们能够不通过小明容许直接borrowMoney(money)拿钱,显然这是不合理的。咱们但愿的是外部没有对moneyborrowMoney的访问权限,他们应该是属于小明的私有变量,这时就须要函数做用域出马了,按照最小特权原则,将moneyborrowMoney“隐藏”起来

function xiaoming() {
    var money = 100
    function borrowMoney(money) {
        return money
    }
    return borrowMoney(money)
}
xiaoming() //100
money = 1000000 //严格模式会ReferenceError
xiaoming() //100
borrowMoney(5000000) //ReferenceError
复制代码

当即执行函数表达式

假设咱们如今的场景是,咱们的程序运行过程当中只须要借一次钱,且不care究竟是问谁借,那么原来的写法其实有两点没有必要,一个是建立了一个具名函数xiaoming,该名称自己会“污染”所在做用域(即在全局做用域中声明了一个不须要往后再被调用的函数名xiaoming),另外还须要显示的调用函数xiaoming

若是函数不须要函数名(至少不“污染”所在做用域),而且可以自动运行,将会更加理想,好在js提供了可以同时解决这两个问题的方案:

(function xiaoming() {
    var money = 100
    function borrowMoney(money) {
        return money
    }
    return borrowMoney(money)
})() //100
复制代码

上述代码经过括号包裹住函数xiaoming建立了函数表达式,再经过()执行该表达式,且虽然咱们依旧声明了函数名xiaoming(为了体现不会污染所在做用域,实际能够直接写匿名函数),但该名称并未“污染”全局做用域,而是绑定在函数表达式自身函数中,仅在function内部能够被访问,也就是说:

(function xiaoming() {
    ···
    xiaoming()  //能够访问
})() //100
xiaoming()  //ReferenceError
复制代码

区分函数声明函数表达式最简单的方法是看function关键字出如今声明中的位置(不只仅是一行代码,而是整个声明中的位置)。若是function是声明中的第一个词,那么就是一个函数声明,不然就是一个函数表达式

从概念咱们就能够知道,为何以下写法,均可以建立函数表达式并当即执行(某些非主流写法不建议使用):

~function xiaoming() {···}()
!function xiaoming() {···}()
+function xiaoming() {···}()
复制代码

块做用域

实际在ES6出现以前,JavaScript里并无严格的块做用域,可是有些语法也会建立块做用域,后面会讲到

下面这段代码你们必定不陌生

for(var i=0;i < 10;i++) {
    console.log(i)  //0,1,2,3,4,5,6,7,8,9
}
console.log(i)  //10
复制代码

此时变量i其实是声明在了全局做用域中,但其实这是彻底没有必要的,反而会对全局形成“污染”

因而在ES6推出了letconst关键字,完全解决这了一问题

let关键字能够将变量绑定到所在的任意做用域中(一般是{ .. }内部)。换句话说,let为其声明的变量隐式地劫持了所在的块做用域。

for(let i=0;i < 10;i++) {
    console.log(i)  //0,1,2,3,4,5,6,7,8,9
}
console.log(i)  //ReferenceError
复制代码

还记得一道常见的面试题吗?

for(var i=0;i < 10;i++) {
    setTimeout(function() {
        console.log(i)  //输出10个10
    })
}
复制代码

如今是否就很容易理解,为何是10个10了吧。当setTimeout宏任务执行的时候,for循环在主线程已经执行完毕(不理解的须要去补习下事件循环机制),所以在执行console.logi的值已是10了,而后在全局做用域中找到i以后进行输出

若是使用let造成块级做用域,就能获得理想中的输出结果

for(let i=0;i < 10;i++) {
    setTimeout(function() {
        console.log(i)  //输出0,1,2,3,4,5,6,7,8,9
    })
}
复制代码

这段代码可能不方便理解,那么换一种写法你们就能看懂块级做用域是如何运做的:

{
    let j
    for(j=0;j < 10;j++) {
        let i = j
        setTimeout(function() {
            console.log(i)
        })
    }
}
复制代码

从代码能够看出,在每一次for循环内,其实都隐式的绑定了一次变量i,且该变量仅在块做用域内部可访问,在执行console.log时访问的是块做用域内部该次循环所绑定的i

for循环头部的let不只将i绑定到了for循环的块中,事实上它将其从新绑定到了循环的每个迭代中,确保使用上一个循环迭代结束时的值从新进行赋值。

除了let之外,ES6还引入了const,一样能够用来建立块做用域变量,惟一的区别是const的值不容许被修改

总结

  1. 函数做用域是JavaScript中最多见的做用域单元,能够经过最小特权原则将内部变量“隐藏”起来

  2. 能够经过括号包裹等方式建立函数表达式当即执行,在特定场合下减小全局“污染”和代码量

  3. ES6引入和letconst关键字,能够在所在代码块{···}内部建立块级做用域

提高

关于变量提高,也是一个老生常谈的问题,想必你们都知道有这么一种机制,可是至于为何,不必定十分理解,不要紧,看完本章你会明白透彻

var提高

考虑如下代码:

a = 1
var a
console.log(a)  //1
复制代码

输出的结果并不是语义上理解的undefined,而是1

console.log(a)  //undefined
var a = 2
复制代码

输出的结果并不是语义上理解的报错,而是undefined

为何会出现上面的结果呢,咱们就要从编译阶段来理解其中的本质缘由了

经过前面的学习咱们已经知道,任何JavaScript代码片断在执行前都要进行编译(一般就在执行前)。编译阶段中的一部分工做就是找到全部的声明,并用合适的做用域将它们关联起来。引用原文的描述就是:

包括变量和函数在内的全部声明都会在任何代码被执行前首先被处理。

所以第一段代码按照编译执行的正确处理顺序以下:

var a   //编译阶段先处理变量a的声明
a = 1
console.log(a)  //1
复制代码

代码按照编译执行的正确处理顺序以下:

var a   //编译阶段先处理变量a的声明
console.log(a)  //undefined
a = 2   //赋值操做不会被提高
复制代码

函数提高

对于函数来讲也一样如此,且函数做用域内部的声明的变量一样会在该做用域内部被首先处理:

fn()    //正常执行
function fn() {
    console.log(a)  //undefined
    var a = 2
}
复制代码

代码按照编译执行的正确处理顺序以下:

function fn() {   //编译阶段先处理fn的声明
    var a   //编译阶段先处理变量a的声明
    console.log(a)  //undefined
    a = 2   //赋值操做不会被提高
}
fn()    //正常执行
复制代码

变量和函数声明从它们在代码中出现的位置被“移动”到了最上面。这个过程就叫做提高

同名时的提高

这里咱们引伸一下,若是同时声明了同名的变量和函数会怎么样呢?好比以下代码:

console.log(a)  //输出函数a
var a = 2
function a() {···}
复制代码

编译阶段,函数的优先级比普通变量要高,所以会被优先声明,代码按照编译执行的正确处理顺序以下:

function a(){···}   //编译阶段先处理a的声明
var a   //回忆咱们第一章讲到的,遇到已声明过的变量a,忽略该声明,继续进行编译
console.log(a)  //输出函数a
a = 2   //赋值操做不会被提高
复制代码

若是是重复声明同名函数呢:

a()  //输出2,后声明的函数a会覆盖以前的,尽管a已经被声明过
function a() {
    console.log(1)
}
function a() {
    console.log(2)
}
复制代码

函数表达式的提高

咱们再作另外一个引伸,有时候咱们会经过函数表达式的方式来定义一个function,那若是提早调用会怎样呢:

fn()    //TypeError
a()     //ReferenceError
var fn = function a() {···}
复制代码

形成这种结果是因为函数函数表达式中并不会被提高,只是相似一个赋值语句,能够类比成函数a就是一个值,这个值不须要提早声明,所以a也没法提早执行,而变量fn声明了可是尚未赋值,因此也没法经过()来执行,代码按照编译执行的正确处理顺序以下:

var fn   //编译阶段先处理fn的声明
fn()   //至关于undefined(),从以前对异常的介绍能够知道,属于非法操做所以会报TypeError
a()    //未找到a的声明,所以会报ReferenceError
fn = function () {  //赋值操做不会被提高
    var a = ...self...
    ···
}
复制代码

暂时死区

想必你们对于暂时死区都不陌生,实际上就是因为letconst不会被提高致使的,所以就会有以下代码的输出:

console.log(a)  //ReferenceError
let a = 1
复制代码

ES6标准中对let/const声明中的解释,第13章中有以下一段文字:

关于暂时死区的描述大体意思就是:

当程序的控制流程在新的做用域(module function 或 block 做用域)进行实例化时,在此做用域中用let/const声明的变量会先在做用域中被建立出来,但所以时还未进行词法绑定,因此是不能被访问的,若是访问就会抛出错误。所以,在运行流程进入做用域建立变量,到变量能够被访问之间的这一段时间,就称之为暂时死区。

从我我的的理解就是,let声明的变量,在编译阶段是会被建立的,可是不会绑定在词法做用域中,相似造成了一个隐形的块做用域,没法被外部访问,只有当运行时代码执行到了变量声明的位置时,才会放开访问权限

上述代码能够描述成相似下面这样一段代码:

{
    let a   //提早建立变量a,但没法被访问
}
console.log(a)  //ReferenceError
a = 1   //放开对a的访问权限,并赋值
复制代码

看到这里想必你们已经对变量提高有了透彻的理解,达到了所谓的通透。再随便给你一段代码,也能按照编译执行的阶段分析出正确的处理顺序了吧

总结

  1. 一个简单的赋值语句,被拆开成两部分进行,第一部分在编译阶段完成对变量函数的声明,就是所谓的提高,第二部分在执行阶段完成对变量的赋值

  2. 声明自己会被提高,而包括函数表达式的赋值在内的赋值操做并不会提高

  3. 要注意避免重复声明!不然会引发不少意想不到的问题

做用域闭包

本文的重头戏闭包终于隆重登场了,想必这个近乎神话的概念,曾经也让很多同窗痛苦不堪。今天就让咱们在对做用域有了透彻的理解以后,再来揭开闭包的神秘面纱。引用原文的一段话:

闭包是基于词法做用域书写代码时所产生的天然结果,你甚至不须要为了利用它们而有意识地建立闭包。闭包的建立和使用在你的代码中随处可见。你缺乏的是根据你本身的意愿来识别、拥抱和影响闭包的思惟环境。

闭包

首先回顾一下闭包的定义:

当函数能够记住并访问所在的词法做用域时,就产生了闭包,即便函数是在当前词法做用域以外执行。 咱们用一段代码来解释一下:

function fn() {
    var a = 1
    function bar() {
        console.log(a)
    }
    bar()
}
复制代码

嵌套做用域的知识咱们能够了解到,基于词法做用域的查找规则,函数bar能够访问外部做用域中的变量a。而这种基于词法做用域的规则,就是闭包最核心的一部分。从学术的角度说,函数bar其实已经拥有了一个涵盖fn做用域的闭包,也能够理解成bar封闭在了fn的做用域中,可是经过这种方式定义的闭包并不能直接进行观察,咱们换一种写法:

function fn() {
    var a = 1
    function bar() {
        console.log(a)
    }
    return bar
}
var baz = fn()
baz()   //输出2,这就是闭包的效果
复制代码

fn执行后,由于咱们知道因为看上去fn的内容不会再被使用,因此很天然地会期待引擎的垃圾回收器来释放再也不使用的内存空间,从而销毁fn的整个内部做用域,对其进行回收。而闭包的“神奇”之处正是能够阻止这件事情的发生。事实上内部做用域依然存在,所以没有被回收。谁在使用这个内部做用域呢,正是bar自己。在这个例子中,var baz = fn()建立了bazbar的引用,所以执行baz就至关于执行了内部的函数bar,也就至关于bar在自身词法做用域以外执行了(本例在全局做用域进行了执行),并访问到了自身所在词法做用域中的变量a,这就彻底跟概念吻合上了。

再看另外两个例子:

function fn() {
    var a = 1
    function bar() {
        console.log(a)
    }
    baz(bar)
}
function baz(fn) {
    fn()    //这里一样也是闭包
}
复制代码

咱们能够发现,不管使用何种方式对函数类型的值进行传递,当函数在别处被调用时均可以观察到闭包

不管经过何种手段将内部函数传递到所在的词法做用域之外,它都会持有对原始定义做用域的引用,不管在何处执行这个函数都会使用闭包

上述代码可能有点刻意而为之,但我保证看了下面的例子,你会发现闭包其实在你的编码过程当中无处不在:

function wait(msg) {
    setTimeout(function timer() {
        console.log(msg)
    }, 1000)
}
wait('hello')  //1s后执行timer输出hello
复制代码

函数timer具备涵盖wait做用域闭包,所以还保留有对msg的引用

//使用jQuery
function clickHandler(el, name) {
    $(el).click(function active() {
        console.log('clicked:' + name)
    })
}
clickHandler('#btn1', 'btn1')   //单击btn1,至关于执行active,输出‘clicked btn1’
clickHandler('#btn2', 'btn2')   //单击btn2,至关于执行active,输出‘clicked btn2’
复制代码

函数active具备涵盖clickHandler做用域闭包,所以还保留有对name的引用

引用原文的表述:

本质上不管什么时候何地,若是将(访问它们各自词法做用域的)函数看成第一级的值类型并处处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax请求、跨窗口通讯、Web Workers或者任何其余的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包

当即执行函数与闭包

关于当即执行函数

var msg = 'hello'
(function() {
    console.log(msg)    //hello
})()
复制代码

严格来说它并非闭包。由于函数并非在它自己的词法做用域之外执行,而是在全局做用域执行。msg是经过普通的词法做用域查找而非闭包被发现的。

当即执行函数是最经常使用来建立能够被封闭起来的闭包的工具,例如:

var sayHello = (function() {
    var msg = 'hello'
    return function() {
        console.log(msg)
    }
})()
sayHello()  //hello
复制代码

循环与闭包

回忆以前讲let时提到,能够经过块做用域解决循环内部输出理想值的问题,其实也能够经过闭包来解决:

for(var i=0;i < 10;i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j)  //依次输出0,1,2,3,4,5,6,7,8,9
        })
    })(i)
}
复制代码

就如同let建立块做用域同样,每次循环都经过当即执行函数建立了封闭的做用域,而每个定时器中的回调函数都保持着对这个做用域中变量j的引用,这个变量j实际就是咱们传入的值i,所以每次绑定的j值都是不同的,最终才会依次输出0到9

模块

模块也是一种利用闭包强大威力的编码方式,一种最简单的写法就是:

function Module() {
    var food = 'apple'
    var water = 'water'
    function eat() {
        console.log('eat ' + food)
    }
    function drink() {
        console.log('drink ' + water)
    }
    return {
        eat: eat,
        drink: drink
    }
}
var fn = Module()
fn.eat()    //eat apple
fn.drink()  //drink water
复制代码

eatdrink函数具备涵盖模块实例内部做用域闭包(经过调用Module实现)。当经过返回一个含有属性引用的对象的方式来将函数传递到词法做用域外部时,咱们已经创造了能够观察和实践闭包的条件。

ES6中为模块增长了一级语法支持。在经过模块系统进行加载时,ES6会将文件看成独立的模块来处理。每一个模块均可以导入其余模块或特定的API成员,一样也能够导出本身的API成员。 关于ES6中的模块本文不作展开。

总结

  1. 函数能够记住并访问所在的词法做用域,即便函数是在当前词法做用域以外执行,这时就产生了闭包

  2. 当即执行函数并不等于闭包,可是是最经常使用来建立能够被封闭起来的闭包的工具

  3. 利用闭包的特性能够在循环中实现块做用域的效果

  4. 能够利用闭包的特性来实现模块

  5. 闭包无处不在

结语

下篇文章将会把《你不知道的JavaScript(上卷)》后半部分梳理完,涉及的知识点包括:

  • this
  • 对象
  • 原型
  • 委托

敬请期待···