深入理解 JavaScript 执行机制和闭包

5,050 阅读6分钟

一:前置知识

二:js的执行机制

2.1:执行机制中的基础

先来一段简单的代码,感受一下v8在js中的执行机制

showName()
console.log(myName)//undefined,变量提升

var myName = 'midsummer'
function showName() {
  console.log('函数showName被执行')
}//函数showName被执行 

原始类型数据存放在栈里

引用类型数据存放在堆里

1.png

  • 变量提升: js引擎在执行js的过程中把变量声明部分和函数声明部分提升到代码的头部,默认赋值为undefined,声明提升发生在编译阶段,执行阶段不会发生
  • 预编译完成之后,开始执行:第二行会打印出undefined,之后myName被赋值为‘midsummer’,然后函数showName被执行,打印'函数showName被执行'

2.2:执行机制中的栈结构

  • 栈结构: 特殊的数组,先进后出
  • 调用栈: JS引擎用来追踪函数调用关系的
  • 栈溢出: 调用栈的内存超过限制

函数执行完毕后,该函数的执行上下文会销毁(出栈)

var a = 2
function add(b,c){
    return b+c
}
function addAll(b,c){
    var d=10
    var result = add(b,c)
    return a + result +d
}
addAll(3,6)

以这份代码为例,在执行时,调用栈的变化如下:

1. 全局预编译之后,全局执行上下文入栈,进行全局的执行

2.png

2. addAll函数内部预编译之后,addAll执行上下文入栈,进行局部的执行

3.png

3. add函数内部预编译之后,add执行上下文入栈,进行局部的执行

4.png

4. add函数执行完成,add执行上下文出栈

5.png

5. addAll函数执行完成,addAll执行上下文出栈

6.png

6. 整份代码执行完毕,全局执行上下文出栈

7.png

2.3:执行机制中的作用域链

先上一个简单的例子引入一下

function foo(){
    var a =1
    let b =2
    {
        let b =3
        var c =4
        let d =5
        console.log(a)//1
        console.log(b)//3
    }
    console.log(b)//2
    console.log(c)//4
    console.log(d)//error
}
foo()

8.png

  • 词法环境中也是一个栈结构

  • 先在词法环境中找值,再到变量环境中找值(如图中箭头走向),故console.log(b)为3

  • 外层作用域不可以访问内层作用域,故console.log(d)为error

以上是一个关于块级作用域链的例子,接下来为大家展示一个关于函数作用域链的例子,让读者更深入了解以下:

作用域链:并不是在调用栈中从上往下查找,而是看当前执行上下文变量环境中的outer指向来定,而outer指向的规则是,词法作用域在哪里,outer就指向哪里

词法作用域:在函数定义时所在的作用域,由函数声明的位置来决定,跟函数在哪里调用没有关系

function bar () {
    console.log(myName);
}

function foo(){
    var myName = 'Tom';
    bar();
}

var myName = 'Jerry';

foo();

9.png

所以,打印结果是根据作用域链得到的“jerry”,而非依据栈结构得到的“Tom”

三:闭包

3.1:作用域与词法作用域

词法作用域:由函数声明的位置来决定,跟函数在哪里调用没有关系

10.jpg

3.2:闭包初识

请读者仔细阅读实例代码,试着自己画出调用栈的动态变化

function foo(){
    function bar(){
        var age = 18
        console.log(myName)
    }
    var myName = 'midsummer'
    return bar
}

var myName = 'Jerry'
var fn = foo()
fn()

11.png

读者们思路是不是这样:foo函数执行完成后,出栈,然后fn()的调用带来了bar()的执行,bar执行上下文入栈。可是这样有一个问题,此时在调用栈中的bar执行上下文的outer是指向foo执行上下文,foo执行上下文已经执行完毕出栈了,那不是会报错?其实不然,接下来为大家介绍闭包

在javaScript中,根据词法作用域的规则,内部函数一定能访问外部函数中的变量,当内部函数被拿到外部函数之外调用时,即使外部函数执行完毕,但是内部函数对外部函数中的变量依然存在引用,那么这些被引用的变量会以一个集合的方式保存下来,这个集合就是闭包

在foo执行上下文出栈时,留下bar执行上下文需要的变量集合就是闭包

12.png

3.3:闭包的运用

阅读下面代码,请读者们思考是否可以实现count的累加,输出1,2,3?

function add(){
    let count = 0
    count++
    return count
}
console.log(add())
console.log(add())
console.log(add())

显然是不可以的,会输出1,1,1。因为调用一次add之后,执行完毕后,执行上下文会被销毁,再次调用add,会重新预编译和执行,所以还是会输出1。想要达到预期效果,可以将count声明在全局,add执行上下文的销毁不会影响全局变量,但这种做法会带来命名重复,项目维护成本大,这时候闭包就是很好的封装手段,读者可以自行尝试用闭包修改代码,并画出调用栈

function add(){
    let count = 0
    function fn (){
        count++
        return count
    }
    return fn
}
var res = add()
console.log(res())
console.log(res())
console.log(res())

13.jpg

那么,闭包的作用也就不言而喻了

闭包作用:实现变量私有化

四:知识点get

  • 变量提升:js引擎在执行js的过程中把变量声明部分和函数声明部分提升到代码的头部,默认赋值为undefined,声明提升发生在编译阶段,执行阶段不会发生

  • 执行上下文

    • 变量环境:var声明的变量
    • 词法环境:let,const声明的变量
  • 调用栈:

    • 栈结构:特殊的数组,先进后出
    • 调用栈:JS引擎用来追踪函数调用关系的
    • 栈溢出:调用栈的内存超过限制
  • 作用域链: 并不是在调用栈中从上往下查找,而是看当前执行上下文变量环境中的outer指向来定,而outer指向的规则是,我的词法作用域在哪里,outer就指向哪里

  • 词法作用域:在函数定义时所在的作用域,由函数声明的位置来决定,跟函数在哪里调用没有关系

  • 闭包: 在javaScript中,根据词法作用域的规则,内部函数一定能访问外部函数中的变量,当内部函数被拿到外部函数之外调用时,即使外部函数执行完毕,但是内部函数对外部函数中的变量依然存在引用,那么这些被引用的变量会以一个集合的方式保存下来,这个集合就是闭包

  • 闭包作用: 实现变量私有化