[面试整理]执行上下文

195 阅读6分钟

词法作用域

js采用的是词法作用域,函数在定义的时候,函数的作用域就已经确定了

执行上下文栈

因为JavaScript代码的执行顺序:顺序执行但是他的是,一段一段执行。

例子

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

我们假定,执行上下文栈是一个栈,假定为ECStrack = []

  • 首先我们最先遇到的一定是全局代码,那么我们就会将全局执行上下文globalStrack压入到ECStrack里面

  • 这是我们首先遇到的是fun1,压入ECStrack里面

    ECStrack = [globalStrack,fun1Strack]
    
  • 发现fun1里面调用了fun2,那么我们也要将fun2也压入

    ECStrack = [globalStrack,fun1Strack,fun2Strack]
    
  • 发现fun2里面调用了fun3,那么我们也要将fun3也压入

    ECStrack = [globalStrack,fun1Strack,fun2Strack,fun3Strack]
    
  • fun3执行完毕了,那么我们就pop

    ECStrack = [globalStrack,fun1Strack,fun2Strack]
    
  • 因为fun2只调用fun3,那么其实fun2也计算好了,pop

    ECStrack = [globalStrack,fun1Strack]
    
  • fun1也是如此

    ECStrack = [globalStrack]
    
  • 最后只剩下了全局执行上下文

函数完成以后,他就会从执行栈里面pop掉

执行上下文

执行上下文栈会创建执行上下文,执行上下文里面又含有三个重要的属性

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

全局上下文

就是定义在全局的那些东西,全局上下文里的变量对象其实就是全局对象

函数上下文

函数上下文,在函数上下文里面我们会涉及到,活动对象AO

活动对象在进入函数上下文的时候被创建当函数代码执行时他会进行修改

执行过程

  • 进入执行上下文
  • 代码执行

进入执行上下文

一般他的AO有几个方便

  • arguments(形参)
    • 没有实参,属性值为undefined
    • 有的话,就是那个传入的值
  • 函数声明
    • 由名称和对应值(函数对象function-object)组成
    • 如果有其他变量和函数变量同名,那么函数直接覆盖
  • 变量声明
    • 值全部是undefind

举例:

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};
  b = 3;
}
foo(1);

我们可以分析AO

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}

因为我是存在参数的,所以,参数传过来的时候,我就已经知道了,那个值是多少

代码执行

代码执行的时候,我们会将初始化的AO进行改变

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression 'd'
}

例子

console.log(foo);

function foo(){
    console.log("foo");
}

var foo = 1;

其实我们打印的是这个函数

因为当函数和其他变量声明同名时,函数,先进行函数声明,然后再是变量声明

作用域链

javascript在使用一个变量的时候,,他会现在当前的作用域中找,如果没找到,那么他回去上层的作用域找,一直到全局作用域为止,这样就构成了作用域链

函数内部有一个属性,[[scope]],当函数创建时,他就会保存在对应的父变量的对象中

举例

function foo() {
    function bar() {
        ...
    }
}

那么对应的[[scoped]]就是

foo.[[scope]] = [
  globalContext.VO
];
bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];

原因:

  1. 因为foo是定义在全局的,所以他的scoped里面只有global.vo
  2. 但是bar是在foo,里面的,所以他还有一个foo,因为foo是一个函数,所以我们使用AO来表示变量对象fooContext.AO

当函数进入执行上下文时,就会将活动对象AO添加到作用链前端

例子

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();
  1. 首先是checkscope函数被创建,保存作用域链到内部属性[[scoped]]

    checkscope.[[scope]] = [
        global.vo
    ]
    
  2. 执行checkscope函数,创建checkscope函数执行上下文

    ECStack = [
        checkscopeContext,
        globalContext
    ];
    
  3. checkscope进入执行上下文

    • 复制scope到作用域链Scope
    checkscopeContext = {
        Scope: checkscope.[[scope]],
    }
    
    • 创建AO
    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope2: undefined
        },
        Scope: checkscope.[[scope]],
    }
    
    • 将活动对象压入到checkscope作用域链Scope顶端
    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope2: undefined
        },
        Scope: [AO, [[Scope]]]
    }
    
  4. 执行函数,将AO进行更新

    checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope2: 'local scope'
        },
        Scope: [AO, [[Scope]]]
    }
    
  5. 函数执行完毕,那么pop

例题分析

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
  1. 执行全局上下文,创建全局上下文,并将全局执行上下文压入栈中

        ECStack = [
            globalContext
        ];
    
  2. 全局上下文初始化

        globalContext = {
            VO: [global],
            Scope: [globalContext.VO],
            this: globalContext.VO
        }
    
  3. checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]]

        checkscope.[[scope]] = [
          globalContext.VO
        ];
    
  4. 执行checkScope那么压入执行上下文栈

        ECStack = [
            checkscopeContext,
            globalContext
        ];
    
  5. checkScope进行初始化,包括复制[[scope]]到作用域链,AO

        checkscopeContext = {
            AO: {
                arguments: {
                    length: 0
                },
                scope: undefined,
                f: reference to function f(){}
            },
            Scope: [AO, globalContext.VO],//将活动对象压入到作用域链顶部
            this: undefined
        }
    
  6. f进行初始化

        fContext = {
            AO: {
                arguments: {
                    length: 0
                }
            },
            Scope: [AO, checkscopeContext.AO, globalContext.VO],
            this: undefined
        }
    
  7. 函数f执行,顺带着寻找scope,f结束,pop出去

  8. 然后checkScope也执行完成,pop

  9. 最后只剩下了globalContext

所以到最后,所说的作用域链,其实就是那个Scope,就是那个数组

闭包

定义:

  1. 即使他的上下文已经被销毁了,但是他依然存在了
  2. 在代码中存在自由变量

回答:

引入自做了一份前端面试复习计划,保熟~ - 掘金 (juejin.cn)

在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的 [[scope]] 中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。

常见题:

var data = [];

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

data[0]();
data[1]();
data[2]();

输出答案全部是3

分析:

这里为了方便,我只写出作用域链

global:

vo:{
    data:[],
    i:3
}

data[0]:

data[0].Scoped = [AO,global.VO]
data[0].AO = {
  function () {}
}

同理,data[1]data[2]

data[1].Scoped = [A0,data[0].AO,global.VO]
data[2].Scoped = [AO,data[1].A0,data[0].AO,global.VO]
data[1].AO = {
  function () {}
}
data[2].AO = {
  function () {}
}

所以一直到i = 3,就会从全局找,一直找到3

闭包在处理速度和内存消耗方面性能具有负面影响

例如:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

其实可以避免这种写法,可以写为

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

参考链接:

做了一份前端面试复习计划,保熟~ - 掘金 (juejin.cn)

JavaScript深入之参数按值传递 · Issue #10 · mqyqingfeng/Blog (github.com)

面试官:说说你对闭包的理解?闭包使用场景 | web前端面试 - 面试官系列 (vue3js.cn)