JavaScript闭包的理解

JavaScript闭包的理解

  • 零、参考文章
  • 一、准备知识
    • 1.1 作用域
      • 作用域链
      • 闭包里的作用域链
      • 块级作用域
        • 暂时性死区
    • 1.2 执行上下文
      • 代码执行的两个阶段
        • 预编译阶段
        • 执行阶段
        • 总结
    • 1.3 调用栈
  • 二、闭包
    • 2.1 闭包的理解
    • 2.2 闭包中作用域链的理解

零、参考文章

https://gitbook.cn/gitchat/column/5c91c813968b1d64b1e08fde/topic/5c99a9a3ccb24267c1d01960

一、准备知识

1.1 作用域

在 JavaScript 中, 作用域为可访问变量,对象,函数的集合以及在局部变量中对外层作用域的引用。

作用域其实就是一套规则:这个规则用于确定在特定场景下如何查找变量,这套规则定义了引擎如何在当前作用域以及嵌套作用域根据标识符来查询变量。反过来说N个作用域组成的作用域链决定了函数作用域内标识符查找到的值。

作用域是定义的时候就决定的。

作用域链

在函数执行过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取和存储数据。该过程从作用域链头部,也就是当前执行函数的作用域开始(下图中从左向右),查找同名的标识符,如果找到了就返回这个标识符对应的值,如果没找到继续搜索作用域链中的下一个作用域,如果搜索完所有作用域都未找到,则认为该标识符未定义。函数执行过程中,每个标识符值得解析都要经历这样的搜索过程。

  1. 当执行函数时,总是先从函数内部找寻局部变量
  2. 如果内部找不到(函数的局部作用域没有),则会向创建函数的作用域(声明函数的作用域)寻找,依次向上

所以作用域链是声明时候决定的

PS:所以Jquery会把 window传到函数中,避免寻找window时反复遍历作用域链

闭包里的作用域链

let a = 1
function foo() {
  let a = 2
  function too() {
    console.log(a)
  }
  return too
}
foo()() // 2

too定义在 foo 作用域,too定义的时候 a=2的在作用域链上离函数更近。所以输出 2

块级作用域

ES6 增加了 let 和 const 声明变量的块级作用域
之前只有全局和函数作用域
在function(){}中 let和var 作用域没有区别依然是函数作用域

let区别在于他是块级作用域,在{}中产生了区别,例如

// i虽然在全局作用域声明,但是在for循环体局部作用域中使用的时候,变量会被固定,不受外界干扰。
for (let i = 0; i < 10; i++) { 
  setTimeout(function() {
    console.log(i);    //  i 是循环体内局部作用域,不受外界影响。
  }, 0);
}
// 输出结果:
0  1  2  3  4  5  6  7  8 9

暂时性死区

let和const没有变量提升。

暂时性死区:如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

1.2 执行上下文

代码执行的两个阶段

  1. 代码预编译阶段
  2. 代码执行阶段

预编译阶段

这个时候由编译器将 JavaScript 代码编译成可执行的代码。 注意,这里的预编译和传统的编译并不一样,传统的编译非常复杂,涉及分词、解析、代码生成等过程 。这里的预编译是 JavaScript 中独特的概念,虽然 JavaScript 是解释型语言,编译一行,执行一行。但是在代码执行前,JavaScript 引擎确实会做一些“预先准备工作”。

需要注意的地方,预编译阶段会进行:

  • 预编译阶段进行变量声明;
  • 预编译阶段变量声明进行提升,但是值为 undefined;
  • 预编译阶段所有非表达式的函数声明进行提升。

执行阶段

执行阶段主要任务是执行代码,执行上下文在这个阶段全部创建完成。

总结

作用域在预编译阶段确定,但是作用域链是在执行上下文的创建阶段完全生成的。因为函数在调用时,才会开始创建对应的执行上下文。执行上下文包括了:变量对象、作用域链以及 this 的指向
JavaScript闭包的理解_第1张图片

代码执行的整个过程说起来就像一条生产流水线。

  • 第一道工序是在预编译阶段创建变量对象(Variable Object),此时只是创建,而未赋值。
  • 到了下一道工序代码执行阶段,变量对象转为激活对象(Active Object),即完成 VO → AO。此时,作用域链也将被确定,它由当前执行环境的变量对象和所有外层已经完成的激活对象组成。这道工序保证了变量和函数的有序访问,即如果当前作用域中未找到变量,则继续向上查找直到全局作用域。

这样的工序在流水线上串成一个整体,这便是 JavaScript 引擎执行机制的最基本道理。

ES2018对上下文进行了扩充

  • lexical environment:词法环境,当获取变量或者this值时使用。(作用域链+this)
  • variable environment:变量环境,当声明变量时使用。(变量对象)
  • code evaluation state:用于恢复代码执行位置。
  • Function:执行的任务是函数时使用,表示正在被执行的函数。
  • ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码。Realm:使用的基础库和内置对象实例。
  • Generator:仅生成器上下文有这个属性,表示当前生成器。

1.3 调用栈

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

function foo1() {
  foo2()
}
function foo2() {
  foo3()
}
function foo3() {
  foo4()
}
function foo4() {
  console.log('foo4')
}
foo1()

调用关系:foo1 → foo2 → foo3 → foo4。这个过程是 foo1 先入栈,紧接着 foo1 调用 foo2,foo2入栈,以此类推,foo3、foo4,直到 foo4 执行完 —— foo4 先出栈,foo3 再出栈,接着是 foo2 出栈,最后是 foo1 出栈。这个过程“先进后出”(“后进先出”),因此称为调用栈。

[外链图片转存失败(img-cLBskFVz-1566290597776)(18191EC398884FAEBD8A69F25BD3C3A4)]

正常来讲,在函数执行完毕并出栈时,函数内局部变量在下一个垃圾回收节点会被回收,该函数对应的执行上下文将会被销毁,这也正是我们在外界无法访问函数内定义的变量的原因。也就是说,只有在函数执行时,相关函数可以访问该变量,该变量在预编译阶段进行创建,在执行阶段进行激活,在函数执行完毕后,相关上下文被销毁。

二、闭包

函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局环境下可访问,就形成了闭包。

2.1 闭包的理解

闭包是函数和声明该函数的词法环境的组合。
从技术角度上看所以函数都是闭包,他们都是对象,都关联到了作用域链。

function init() {
    var name = 1; // name 是一个被 init 创建的局部变量
    function displayName() { // displayName() 是内部函数,一个闭包
    	console.log(name++)
    }
    displayName();
}
init();//1
init();//1
init();//1

displayName产生了闭包但是因为在全局作用域上没有持有这个闭包的引用所以函数执行完毕就回收了这个闭包也消失了。

function init() {
    var name = 1; // name 是一个被 init 创建的局部变量
    function displayName() { // displayName() 是内部函数,一个闭包
    	console.log(name++)
    }
    return  displayName;
}
var initFn = init()
initFn();//1
initFn();//2
initFn();//3

initFn持有了displayName这个闭包的引用,没有被回收保留了作用域链。函数定义时的作用域到了函数执行时依然有效。

2.2 闭包中作用域链的理解

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()

作用域链是连接到定义函数当时的作用域,声明时候就确定了,声明innerFoo时作用域中没有c,ReferenceError: c is not defined。

你可能感兴趣的:(学习笔记)