js执行上下文/作用域(链)/闭包

300 阅读4分钟

执行上下文

执行上下文(Exceution Context),每次当控制器转到可执行代码的时候,就会进入一个执行上下文。执行上下文可以理解为当前代码的执行环境,js 中的运行环境大概包括三种情况:

  1. 全局环境:js 代码运行起来会首先进入该环境;
  2. 函数环境:当函数被调用执行时,会进入当前函数中执行代码;
  3. eval(不建议使用,可忽略)。

一个程序中一般有多个执行上下文,js 使用栈的方式管理这些执行上下文(call stack),栈底永远是全局,栈顶永远是当前的执行上下文。每当执行到一个函数时,会讲这个函数的执行上下文加入栈顶,执行完毕后再推出。浏览器关闭后,全局执行上下文也推出,栈被清空。

每个执行上下文都有一系列的属性,主要有如下 3 个:

  • 变量对象(VO
  • 作用域链(Scope Chain
  • this

变量对象(以下简称 VO)

以下例中 func 的执行上下文的生命周期为例:

function func(p1, p2, p3) {
    console.log(p1);
    console.log(p2);
    console.log(p3);
    console.log(a);
    console.log(b);
    console.log(p2);
    var a = 'a';
    var b = 'b';
    function p2() {};
    function b() {};
}
func('p1', 'p2');

1. 创建阶段

该阶段创建VOVO的属性不可访问。VO中有具体包含如下属性:

  1. 函数的所有形参为key,对应的实参(未传为undefined)为value;一个arguments对象;
// p3实参未传入,所以p3在VO中为undefined
VO = {
    arguments: {
        0: 'p1',
        length: 1
    },
    p1: 'p1',
    p2: 'p2',
    p3: undefined
}
  1. 函数声明,即function b() {}这种,函数名为key,方法作为value如果函数名与形参重复则覆盖形参
// 形参p2和函数声明p2重复,形参被覆盖
VO = {
    arguments: {
        0: 'p1',
        length: 1
    },
    p1: 'p1',
    p2: f p2(),
    p3: undefined
    b: f b(),
}
  1. 变量声明,即var定义的变量,变量名为keyundefinedvalue 如果和函数声明或者形参重复将被忽略
// 变量声明初始值都为undefined
VO = {
    arguments: {
        0: 'p1',
        length: 1
    },
    p1: 'p1',
    p2: f p2(),
    p3: undefined
    b: f b(),
    a: undefined
}

根据以上的分析,示例代码在上下文创建阶段将变为如下:

2. 代码执行阶段

该执行上下文的VO会变成活动对象(以下简称AO,当前执行上下文的VO就称为AO,同一个东西在两种环境下的不同叫法),里面的属性都可以访问,将会按顺序执行代码,完成变量赋值,以及执行其他代码;上述例子执行分析:

// 代码执行前AO:
VO = {
    arguments: {
        0: 'p1',
        length: 1
    },
    p1: 'p1',
    p2: f p2(),
    p3: undefined
    b: f b(),
    a: undefined
}
// 执行代码,
function func(p1, p2, p3) {
    // 以下打印的值都是从AO中读取的
    console.log(p1); // AO.p1 = 'p1'
    console.log(p2); // AO.p2 = f p2()
    console.log(p3); // AO.p3 = undefined
    console.log(a);  // AO.a = undefined
    console.log(b);  // AO.b = f b()
    var a = 'a'; // 重新赋值 AO.a = 'a'
    var b = 'b'; // 重新赋值 AO.b = 'b'
    function p2() {};
    function b() {};
    console.log(p1); // AO.p1 = 'p1'
    console.log(p2); // AO.p2 = f p2()
    console.log(p3); // AO.p3 = undefined
    console.log(a);  // AO.a = 'a'
    console.log(b);  // AO.b = 'b'
}
func('p1', 'p2');
// 代码执行后AO:
VO = {
    arguments: {
        0: 'p1',
        length: 1
    },
    p1: 'p1',
    p2: f p2(),
    p3: undefined
    b: f b(),
    a: 'a'
}

掌握了以上函数创建执行时的原理,再也不怕这种面试题啦!

3. 销毁阶段

可执行代码执行完毕之后,执行上下文出栈,对应的内存空间失去引用,等待被回收。

作用域和作用域链

作用域: 决定了代码区块中变量和其他资源的可见性。js 中采用的是词法作用域,又叫静态作用域,变量被创建时就确定好了,而非执行阶段确定的。也就是说我们写好代码时它的作用域就确定了,如下例:

var a = 2;
function foo(){
    console.log(a)
}
function bar(){
    var a = 3;
    foo(); // bar的VO不在foo的作用域链上,也证明了每个函数都有自己独立的作用域链
}
n(); // 输出的是2,不是3

作用域链: 函数被调用时,除了创建 VO,还创建了属性 [[scope]] 会保存所有的父级VO。一般情况下当前执行上下文的变量取值会到当前上下文的VO中取值。但是如果在当前VO中没有查到值,就会去[[scope]]中查找,直到查到全局作用域,这个查找过程形成的链条就叫做作用域链。

this

this 指向则主要看是谁调用的该函数(或者说函数调用时被哪个对象所拥有)。

闭包

闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包的常见方式,就是在一个函数内部创建另一个函数,如下例:

function outer() {
    const a = 1;
    return function () {
        console.log(a);
    }
}
const n = outer();

我们来分析一下,outer函数返回一个匿名函数,该函数的作用域链([[scope]])里包含了outer函数的VOouter函数执行完毕后,他的作用域链被销毁,但是他的VO还在变量n指向的匿名函数的作用链中,这也是为什么还能访问到数据a的原因,直到变量n被回收。

使用场景:节流防抖函数封装、vuejs响应式原理将Dep对象一直持有在属性的getset方法的作用域中...