JavaScript 执行上下文

JavaScript 引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个“准备工作”,比如变量提升、函数提升。 当 JavaScript 遇到一段可执行代码时,会创建对应的执行上下文。

可执行代码

JavaScript 可执行代码有3种:

  • 全局代码
  • 函数代码
  • eval代码
执行上下文栈

JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。

当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。

当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext

ECStack = [
    globalContext
]

举个例子:

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

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

执行上下文栈:

// 伪代码

// fun1()
ECStack.push(<fun1> functionContext);

// fun1中调用了fun2,还要创建fun2的执行上下文
ECStack.push(<fun2> functionContext);

// fun2调用了fun3
ECStack.push(<fun3> functionContext);

// fun3执行完毕
ECStack.pop();

// fun2执行完毕
ECStack.pop();

// fun1执行完毕
ECStack.pop();

// javascript接着执行下面的代码,但是ECStack底层永远有个globalContext
执行上下文分类

执行上下文分为:全局执行上下文和函数上下文。

执行上下文属性

对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO/AO)
  • 作用域链(Scope chain)
  • this

变量对象

变量对象存储了上下文中定义的变量和函数声明,不同执行上下文下的变量对象稍有不同。

全局上下文下的变量对象

全局上下文中的变量对象就是全局对象。

函数上下文下的变量对象

在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。

活动对象和变量对象其实是一个东西,变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,此时被称为AO,只有被激活的变量对象(也就是活动对象)上的各种属性才能被访问。

活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。

执行过程

执行上下文的代码会分成两个阶段进行处理:分析和执行:

  1. 进入执行上下文
  2. 代码执行

进入执行上下文

变量对象会包括:

  1. 函数的所有形参 (如果是函数上下文)

    • 由名称和对应值组成的一个变量对象的属性被创建
    • 没有实参,属性值设为 undefined
  2. 函数声明

    • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
  3. 变量声明

    • 由名称和对应值(undefined)组成一个变量对象的属性被创建;
    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

看个例子:

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"
}

总结

  1. 全局上下文的变量对象初始化是全局对象;
  2. 函数上下文的变量对象初始化只包括 Arguments 对象;
  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值;
  4. 在代码执行阶段,会再次修改变量对象的属性值。

作用域链

函数创建

JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。 这是因为函数有一个内部属性[[scope]],当函数创建的时候,就会保存所有父变量对象到其中,我们可以可以理解为[[scope]]就是所有父变量对象的作用域链。

比如说:

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

函数创建时,各自的[[scope]]为:

foo.[[scope]] = [
  globalContext.VO
];

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

函数激活

当函数激活时,进入函数上下文,创建 AO/VO 后,会将活动对象添加到作用域链的前端。 这时候执行上下文的作用域链,我们命名为 Scope:

Scope = [AO].concat([[Scope]]);

至此,作用域链创建完毕。

this

全局上下文

全局上下文中的 this 指代全局对象; + this等价于window对象 + var === this. === winodw.

console.log(window === this); // true
var a = 1;
this.b = 2;
window.c = 3;
console.log(a + b + c); // 6

函数上下文

函数上下文中 this 取决于函数被调用的方式。

  1. 直接调用,指向全局变量;
function foo(){
  return this;
}
console.log(foo() === window); // true
  1. call()、apply():this指向绑定的对象;
var person = {
  name: "Enjo"
};
function say(job){
  console.log(this === person)
  console.log(this.name+":"+job);
}
say.call(person,"FE"); // true Enjo:FE
say.apply(person,["FE"]); // true Enjo:FE
  1. bind():this将永久地被绑定到bind的第一个参数;
var person = {
  name: "Enjo"
};
function say(){
  console.log(this.name);
}
var f = say.bind(person);
console.log(f()) // Enjo
  1. 箭头函数:箭头函数没有自己的this,都指向外层;
  2. 作为对象的一个方法:this指向调用函数的对象;
var name = 'Alin'
var person = {
  name: "Enjo",
  getName: function(){
    console.log(this)
    return this.name;
  }
}
console.log(person.getName()); // person Enjo

var getName = person.getName;
console.log(getName()); // Window Alin
  1. 作为一个构造函数:this被绑定到正在构造的新对象;

    通过构造函数创建一个对象其实执行这样几个步骤:

    1. 创建了一个全新的对象;
    2. 这个对象会被执行[[Prototype]](也就是__proto__)链接;
    3. 生成的新对象会绑定到函数调用的this;
    4. 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上;
    5. 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。

    所以this就是指向创建的这个对象上

总结

要判断一个函数的this绑定,就需要找到这个函数的直接调用位置。然后可以按照下面四条规则来判断this的绑定对象:

  1. new调用:绑定到新创建的对象;
  2. callapplybind调用:绑定到指定的对象;
  3. 由上下文对象调用:绑定到上下文对象;
  4. 默认:全局对象。 注意:箭头函数不使用上面的绑定规则,根据外层作用域来决定this,继承外层函数调用的this绑定。

实现call/apply

例如:

var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

其实分为2步:

  1. callbarthis指向了 foo
  2. 执行函数 bar

实现思路:

  1. 将函数设为对象的属性:foo.bar = bar;
  2. 执行该函数:foo.bar();
  3. 删除该函数。

第一版:

Function.prototype.selfCall = function(context) {
    // 调用call的函数,用this可以获取
    context.fn = this
    context.fn()
    delete context.fn
}

第二版:call 函数可以给定参数执行函数

Function.prototype.selfCall = function(context) {
    context.fn = this
    var args = []
    for (let i = 1; i <arguments.length; i++) {
        args.push(`arguments[${i}]`)
    }
    eval(`context.fn(${args})`)
    delete context.fn
}

Function.prototype.selfCall = function(context) {
    context.fn = this
    var args = [].slice.call(arguments, 1) // [...arguments].splice(1)
    context.fn(...args)
    delete context.fn
}
var foo = {
    value: 1
};

第三版:如果上下文传空,this 指向 window

Function.prototype.selfCall = function(context) {
    var ctx = context || window
    ctx.fn = this
    var args = [].slice.call(arguments, 1) // [...arguments].splice(1)
    ctx.fn(...args)
    delete ctx.fn
}

第四版:函数是可以有返回值的

Function.prototype.selfCall = function(context) {
    var ctx = context || window
    ctx.fn = this
    var args = [].slice.call(arguments, 1)  // [...arguments].splice(1)
    var result = ctx.fn(...args)
    delete ctx.fn
    return result
}

实现bind

第一版:

Function.prototype.selfBind = function(context) {
    var args = [].slice.call(arguments, 1) // [...arguments].splice(1)
    var self = this
    var bound = function() {
        self.apply(context, args.concat([].slice.call(arguments)))
        // self.apply(context, args.concat(...arguments))
    }
    return bound
}

第二版:bind返回的函数如果作为构造函数,搭配new关键字出现的话,我们的绑定this就需要“被忽略”。

var obj = {
    name: '若川',
};
function original(a, b){
    console.log('this', this); // {name: "若川"}
    console.log('typeof this', typeof this); // object
    this.name = b;
    console.log('name', this.name); // 2
    console.log('this', this);  // {name: 2}
    console.log([a, b]); // [1, 2]
}
var bound = original.bind(obj, 1);
var newBoundResult = new bound(2);
console.log(newBoundResult, 'newBoundResult'); // bound{} "newBoundResult"

执行这段代码之后,会发现this指向了new bound()生成的新对象。

new做了什么:

  1. 创建了一个全新的对象;
  2. 这个对象会被执行[[Prototype]](也就是__proto__)链接;
  3. 生成的新对象会绑定到函数调用的this;
  4. 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上;
  5. 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。
Function.prototype.selfBind = function(context) {
    var args = [].slice.call(arguments, 1) // [...arguments].splice(1)
    var self = this

    var F = function() {}
    F.prototype = this.prototype

    var bound = function() {
        self.apply(this instanceof self ? this : context, args.concat([].slice.call(arguments)))
        // self.apply(this instanceof self ? this : context, args.concat(...arguments))
    }
    bound.prototype = new F()
    return bound
}
var obj = {
    name: '若川',
};
function original(a, b){
    console.log('this', this); // original {name: 2}
    console.log('typeof this', typeof this); // object
    this.name = b;
    console.log('name', this.name); // 2
    console.log('this', this);  // original {name: 2}
    console.log([a, b]); // 1, 2
}
var bound = original.bind(obj, 1);
var newBoundResult = new bound(2);
console.log(newBoundResult, 'newBoundResult'); // original {name: 2} "newBoundResult"

第三版:细节完善:调用bind方法的一定是一个函数

Function.prototype.selfBind = function(context) {
    if (typeof this !== "function") {
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable")
    }
    var args = [].slice.call(arguments, 1) // [...arguments].splice(1)
    var self = this

    var F = function() {}
    F.prototype = this.prototype

    var bound = function() {
        self.apply(this instanceof self ? this : context, args.concat([].slice.call(arguments)))
        // self.apply(this instanceof self ? this : context, args.concat(...arguments))
    }
    bound.prototype = new F()
    return bound
}
举例
var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();

执行过程如下:

  1. checkscope 函数被创建时,保存作用域链到内部属性[[Scope]]
checkscope.[[scope]] = [
    globalContext.VO
]
  1. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
    checkscopeContext,
    globalContext
]
  1. checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {
    Scope: checkscope.[[scope]],
}
  1. 第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: checkscope.[[scope]],
}
  1. 第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}
  1. 准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}
  1. 查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
    globalContext
]
原文链接:juejin.im

上一篇:Vue深入响应式原理-隐式添加响应式
下一篇:设计模式(五)创建型设计模式

相关推荐

官方社区

扫码加入 JavaScript 社区