你不知道的JavaScript_上卷_第1部分_做用域和闭包学习笔记

第一部分:做用域和闭包闭包

  第一章:做用域是什么异步

  第二章:词法做用域分布式

  第三章:函数做用域和块做用域函数

  第四章:提高性能

  第五章:做用域闭包spa

 

 

一、简单的归纳图3d

 

二、3个简单的demoblog

下面这几个 demo 是考察你是否了解 JS引擎 在编译和执行时的工做机制队列

 

 

 

   

 

三、JS是编译语言事件

第一章原文节选

  尽管一般将 JavaScript 归类为‘动态’或‘解释执行语言’,但事实上,它是一门编译语言。

  但与传统编译语言不一样,它不是提早编译的,编译结果也不能在分布式系统中进行移植。

  对于 JavaScript 来讲,大部分状况下编译发生在代码执行前的几微秒(甚至更短!)

编译时 JS引擎 会作哪些事

  简单归纳:词法分析 -> 语法分析(抽象语法树 AST)-> 代码生成(将 AST 转换为可执行代码,供 JS引擎 执行阶段使用)

  编译的词法分析阶段基本可以知道所有标识符在哪里以及是如何声明的,从而可以预测在执行过程当中如何对它们进行查找,其中具体的细节是这样的:

    3.1 若是遇到一个变量的声明例如 var a,JS引擎会询问做用域是否已经有一个该名称的变量存在于同一个做用域的集合中?

       若是是,JS引擎会忽略该声明,继续进行编译,不然它会要求做用域在当前做用域的集合中声明一个新的变量,并命名为 a

    3.2 若是变量(含函数表达式)是以 var 关键字声明的,该变量的声明会被提高到当前做用域的最顶部,变量的赋值语句则留在原先位置

    3.3 若是是函数声明,整个函数定义都会被提高到当前做用域的最顶部

    3.4 若是当前做用域出现了同名的函数声明和变量声明,函数声明优先被提高到做用域的最顶部,同名的变量声明则会被忽略

    3.5 示例分析

      

      

      上面的代码段会被引擎理解为以下形式:

      

      注意,var foo 尽管出如今 function foo() ...... 的声明以前,但它是重复的声明(所以被忽略了),由于函数声明会被提高到普通变量以前

      尽管重复的 var 声明会被忽略掉,但出如今后面的对 foo 的赋值操做仍是能覆盖前面的函数定义

扩展阅读

这篇文章来自知乎,里面一些优秀的回答能帮助你加深对 JavaScript 是编译语言的理解

《V8引擎本用了什么编译技术,使得 JavaScript 与用 C 写出来的代码性能相差无几?》

 

四、JS是词法做用域、JS引擎在编译和执行阶段都要和做用域打交道

做用域是一套规则,这套规则用来管理引擎如何在当前做用域以及嵌套的子做用域中根据标识符名称进行变量查找

词法做用域最重要的特征是它的定义过程发生在代码的书写阶段( 假设你没有使用 eval() 或 with )

词法做用域的造成阶段主要发生在编译阶段,JS引擎在执行阶段主要是进行做用域查询:

  以 var a = 2 为例

  JS引擎会在解释 JavaScript 代码以前首先对其进行编译,JS是先编译后执行

  因此在 JS引擎 看来,var a = 2 不是一个声明,而是两个单独的声明 var a 和 a = 2

  第一个声明是编译阶段的任务,若是当前做用域下没有 a 变量就建立,若是有,就忽略该声明

  第二个声明是执行阶段的任务,若是当前做用域下有 a 这个变量就将2赋值给a,若是没有就去当前做用域的上一级做用域查找,以此类推,若是到了全局做用域仍是没有找到a变量,引擎会抛出 ReferenceError

 

五、函数做用域、块做用域

ES6以前,每建立一个函数就会产生一个新的基于函数的做用域,ES6出现后,JS开始拥有基于代码块( 一般指 { .. } 内部)的做用域

  块做用域

    特色:编译时,块做用域里的变量的声明不会被提高

    优势

      5.一、垃圾收集 ( 原书3.4节 )

      5.二、let 循环

        5.2.1 var 循环

          

          

          咱们指望的结果多是这样的

            先执行 for 循环,由于循环2次,因此第一次循环打印 i 其值为0,第二次循环打印 i 其值为1,循环结束后打印 i 其值为 2

          分析为何指望和运行的结果不一致

            当既有同步代码也有异步代码时,引擎会先执行同步代码,全部的同步代码都执行完毕后,再回过头去执行异步代码

            第一次 for 循环时,循环体是一个定时器它是异步的,因此引擎暂时不会执行它,会将它放到事件队列中。

            此时循环计数 i 由 0 变为 1,知足循环条件,能够进行第二次循环

            由于此处的 for循环 的循环体不管循环多少次,循环体都不变仍是定时器,因此第二次循环时,引擎又将一个定时器放到事件队列中

            此时循环计数 i 由 1 变为 2,不知足循环条件,循环结束

            for循环后是一条console语句,它是同步的,因此引擎会直接执行它,引擎会在当前做用域即全局做用域查找变量 i,找到后打印其值为2

            这段程序的全部同步代码都执行完毕了,引擎开始去处理哪些存在事件队列中而且知足当下执行条件的异步代码,找到符合要求的两个定时器

            这两个定时器都要打印变量 i 的值,从词法做用域能够看出,它们要打印的就是全局变量 i 的值,而全局变量 i 的值如今是2

            因此运行的结果是,先执行了 for循环 以后的console语句,打印2,最后执行两个定时器且打印的结果都是2

        5.2.2 let 循环   

          

          

          为何 let 循环运行后的结果就能符合咱们的预期呢?

          分析:

            和 var 循环同样,每次循环体里的定时器都会被放到事件队列中,等程序全部的同步代码都执行完毕后,才会去执行事件队列中的异步代码

            循环结束后没有同步代码可执行,并且定时器的延迟时间咱们设置的是 0ms,因此循环一结束,引擎就会立马执行那两个定时器的回调函数

            这两个定时器的回调函数都要打印一个变量 i 的值,说明这个变量 i 的值必定不是全局变量,若是是全局变量两次打印的值确定是同样的

            但 let循环 实际运行两次打印的值都不同,因此这个变量 i 究竟是什么做用域呢,既不是全局做用域,也不是函数做用域(for循环代码外面没有函数包裹)

            是块做用域

            for 循环头部的 let 声明还会有一个特殊的行为

              每次迭代循环计数 i 都会被从新声明一次,因此 let 循环每次迭代都会生成一个全新的封闭的块做用域,实际效果相似下方代码

                

                以第一个定时器回调函数为例,它须要在做用域里找到变量 j 才能完成 console 语句。

                JS是词法做用域,做用域在代码书写的时候就定义好了

                以 let 关键字声明的变量会生成块做用域,在 { ... } 块内的确有一个变量 j,其值被赋值为每次迭代开始的循环计数

                每循环一次就会执行一次 let j,就会生成一个新的块做用域,因此两个回调函数都打印了 j 变量,但这两个 j 变量不是同一个变量

              循环结束后这些声明的 j 变量对应的内存空间其实就会被销毁,但由于在同一个块做用域里,定时器的回调函数使用了这个应该随后会被销毁的 j 变量,

              因此每次循环产生的块做用域里的变量都还保留着,因此当引擎开始执行定时器的回调时,每一个回调都能找到本身的 j 变量

              此处的 let循环 除了涉及块做用域仍是涉及到闭包,下方会有闭包的介绍

      5.3 改造 var 循环

        明白了 let循环 为何能达到预期结果后,咱们不妨试着改造以前的 var 循环,不是没有块做用域就完成不了需求

        分析:

          5.3.1 由于指望每循环一次打印的都是当前的循环计数,即每次循环打印的值都不同

          5.3.2 因此变量 i 必定不是全局变量,不是全局变量只能是局部变量,即变量 i 属于函数做用域

          5.3.3 因此循环体外层要包裹一个函数

            

          5.3.4 也许是考虑到函数声明会污染全局变量且手动调用很麻烦,因此下面这种形式更常见

            

 

六、闭包

闭包的2个示例

  示例1:

    

  示例2:

    

  比较这两个示例的共同点

    示例1中,foo() 函数执行完毕后,按理说 foo() 整个内部做用域都应该被销毁

    示例2中,wait() 函数执行完毕后,其产生的函数做用域也应该被销毁

    但2个示例中,本来在执行完毕后应该被释放的内存空间都没有被释放

    由于有特殊状况出现,阻止了JS引擎的垃圾回收工做

  什么特殊状况

    以示例1为例:

      foo() 函数内部有一个函数 bar(),bar() 函数显然能访问它的父做用域 foo() 函数

      而 foo() 函数的返回值刚好就是其内部函数 bar()

      咱们在全局做用域中调用 foo() 函数 并将其返回值赋值给了一个全局变量 baz

      本来foo的内部函数bar只有一个引用计数,通过 var baz = foo() 后,如今bar函数的引用计数为2

      因此在 foo函数 执行完毕后,引擎没有回收 bar函数 的内存空间,而 bar函数 里又访问了其父级做用域 foo函数 里的变量,因此 foo函数 的内存空间暂时也不能释放

      全局变量baz是个函数表达式,baz() 后,咱们能打印 foo函数 的局部变量 a

闭包的定义

  原书定义:

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

    闭包是基于词法做用域书写代码时所产生的天然结果

  我本身的话:

    这种在某个具备封闭性质的词法做用域以外由于一些缘由在外部还能访问该做用域的能力就叫闭包

      这些缘由有:

        一个函数的返回值是其内部的一个子函数

        在定时器、事件监听器、Ajax请求、跨窗口通讯、Web Workers或者任何其余的异步(或者同步)任务中使用了回调函数

IIFE模式是闭包吗?

  

  按照上述对闭包的定义,它不是闭包。

  由于函数( 示例代码中的IIFE )并非在它自己的词法做用域之外执行的。

  它在定义时所在的做用域而非外部做用域中执行

  a 也是经过普通的词法做用域查找而非闭包被发现的

相关文章
相关标签/搜索