Javascript 执行上下文引发的作用域,闭包思考

201 阅读5分钟

一.执行上下文

1. 执行上下文栈

先看下执行上下文生命周期

js中执行上下文是一个基本概念,**执行上下文就是当前代码运行环境,运行环境主要包含全局环境和函数环境,**深入理解执行上下文对理解函数作用域链,变量提升,闭包有很大好处

执行上下文栈(Execution Context Stack),遵循后进先出原则,管理执行上下文,在js代码执行时,首先进入全局环境,这个时候全局上下文被创建并入栈,当遇到函数调用时候进入相应函数环境,此时函数上下文被创建并入栈,当函数代码执行完毕后会出栈

function A() {
   B()
}
function B() {

}
A()

我们可以用伪代码看下山下文栈

// 全局上下文创建并入栈
ECStack.push(global_EC)
// A 被调用  A的函数上下文创建并入栈
ECStack.push(A_EC)
// B 被调用, B的函数上下文创建并入栈
ECStack.push(B_EC)
// B 调用完毕,出栈
ECStack.pop()
// A 执行完毕出栈
ECStack.pop()
// 全局上下文出栈
ECStack.pop()

2.执行上下文的组成

执行上下文中有三个重要的属性,变量对象(Variable Object),作用域链(scope chain),this(this binding),我们可以表示为

EC = {
    VO,
    SC,
    this
}

执行上下文的生命周期分别为创建执行阶段,

  • 创建阶段主要是生成变量对象,建立作用域链,确定this指向
  • 执行阶段主要是变量赋值和执行代码

1.变量对象(Variable Object)

创建上下文分为三个过程

  1. 检索当前上下文的参数:生成 Arguments 对象,并把形参变量名变成属性名,属性值为形参

  2. 检索当前上下文的函数声明,函数名为属性名,函数的引用地址为属性值

  3. 检索当前上下文的变量声明,变量名为属性名,undefined 为属性值

    VO = { Arguments: {}, ParamVariable: 形参具体值, Function: , Variable: undefined }

当上下文到执行阶段后,变量对象会变成活动对象(Active Object),声明的变量会赋值

我们以实际例子说明

function A(p) {
    var a = 1
    function B() {}
    var c = function () {}
}
A(0)

这样一段代码生成的上下文的创建阶段是

A_EC = {
    VO = {
        arguments: {
            '0': 0,
            length: 1
        },
        p: 0,
        a: undefined,
        B: <function B reference>,
        c: undefined
    }
}

在A 执行阶段

A_EC = {
    VO = {
        Arguments: {
            '0': 0,
            length: 1
        },
        p: 0,
        a: 1,
        B: <function B reference>,
        c: <function express c reference>
    }
}

这就是函数提升变量提升的内在机制了

var a = 1
function A() {
}

实际上js的创建顺序可以理解为

function A() {}
var a = undefined
a = 1

2.作用域链

作用域链是指由当前上下文和上层上下文的一系列变量对象组成的层级链,它保证了当前执行环境能访问哪些变量和函数,当需要查找某个变量或者函数时候,会在当前上下文的变量对象中查找,要是没找到,就会沿着上层上下文的变量对象进行查找,知道找到全局上下文

js作用域包含了全局作用域和函数作用域,函数作用域是在函数声明时候确定的, 每一个函数都会包含一个 [[scope]]内部属性,函数声明时候,该属性会保存它上层上下文的变量对象,形成层级链

function A() {
    function B() {
    }
    B()
}
A()

// B 的[[scope]]创建阶段
B.[[scope]] = [A_EC.VO, globalObj]

当B 被调用时候,它执行上下文被创建并入栈,这个时候会将生成的变量对象加到作用域链的顶端

B.[[scope]] = [B_EC.VO, A_EV.VO, globalObj]

3. this指向

this的指向,是函数在调用时候确定的

可以看我之前的文章

juejin.cn/post/693903…
this的几种情况都做了说明

4.闭包

上面提到了上下文出栈后,对应的变量对象就会被垃圾回收机制回收,这里提一下,js垃圾回收常用有两种方式

  1. 引用计数:这种判断很简单,就是看一个对象是否指向其他引用,没有的话就会回收,有的话不会回收,但是循环引用可能会导致内存泄露
  2. 标记清除:从根部对象出发定时扫描内存的对象,不在使用的对象标记成无法到达的对象,可以静心垃圾回收

但是闭包能够阻止变量对象被回收

闭包是可以访问内部变量的函数,用执行上下文方式理解就是,当前执行上下文已经出栈,但是还可以访问当前的变量对象

function A() {
    var a = 1
    function B() {
        var b = a + 1
    }
    return B
}
A()()

上面可以看出

// B的[[scope]]可以表示为
B.[[scope]] = [B_EC.VO, A_EC.VO, global.VO]

所以B上下文的作用域链中包含了A的上下文的变量对象,并且B 访问了 A的变量,所以阻止了A的变量对象的垃圾回收

那拿一个经典的例子

var arr = []
for(var i=0; i < 3; i++ ) {
    arr[i] = function() {
        console.log(i)
    }
}
arr[0]()
arr[1]()
arr[2]()

来分析一下这个例子, 当全局上下文执行完之后 全局上下文可以表示为

global_EC = {
    VO: {
        arr,
        i: 3
    }
}

这是时候 arr[0] 调用时候的 上下文是

arr[0]_EC: {
    VO: {
    },
    scope: [arr[0]_EC.VO, global_EC.VO]
}

arr[0]_EC中是没有  i 的变量的,所以只能去global_EC的变量对象中去找,i = 3 ,所以 arr[1](),arr[2] 中的 i 都是global 的,在let 出现之前一个经典的解决方式就是闭包

var arr = []
for(var i=0;i<3;i++) {
    arr[i] = (function(i) {
            return function() {
                console.log(i)
            }
        }
    )(i)
}

我们再分析一下,此时arr[0]_EC

arr[0]_EC.scope = [ arr[0]_EC.VO, 匿名函数_EC.VO, golbal_EC.VO]

而匿名函数的VO是

匿名函数_EC.VO = {
    Arguments: {
        '0': 0,
        length: 1
    },
    i: 0
}

这个时候 arr[0] 的VO里是没有 i 的,但是根据作用域链找到了  匿名函数里的 VO 的  i ,所以可以返回预期的结果

当然还可以用 ES6 中的 let 来解决

var data = []
for( let i = 0; i<3; i++) {
    data[i] = function() {
        console.log(i)
    }
}

可以把let 声明的块级作用域看作是一个执行上下文,可以看做

{
    let i = 0
    data[0] = fucntion() {
        console.log(i)
    }
}

data[0]就可以看作是 let 生成的 作用域的闭包,引用了 块级作用域的i,所以可以拿到 i的值