JavaScript基础专题之深刻执行上下文(三)

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

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

这篇咱们来聊聊这三个重要属性面试

变量对象

变量对象做为执行上下文的一种属性,每次建立后,根据执行环境不一样上下文下的变量对象也稍有不一样,咱们比较熟悉的就是全局对象函数对象,因此咱们来聊聊全局上下文下的变量对象和函数上下文下的变量对象。数组

全局上下文

咱们先了解一个概念,什么叫全局对象。在 W3School 中:bash

全局对象是预约义的对象,做为 JavaScript 的全局函数和全局属性的占位符。经过使用全局对象,能够访问全部其余全部预约义的对象、函数和属性。dom

在顶层 JavaScript 代码中,能够用关键字 this 引用全局对象。由于全局对象是做用域链的头,这意味着全部非限定性的变量和函数名都会做为该对象的属性来查询。函数

例如,当JavaScript 代码引用 parseInt() 函数时,它引用的是全局对象的 parseInt 属性。全局对象是做用域链的头,还意味着在顶层 JavaScript 代码中声明的全部变量都将成为全局对象的属性。post

咱们能够根据代码理解ui

  1. 能够经过 this 引用,在客户端 JavaScript 中,全局对象就是 Window 对象。
console.log(this); //window
复制代码
  1. 全局对象是由 Object 构造函数实例化的一个对象。
console.log(this instanceof Object);//true
复制代码
  1. 咱们调用的一些方法都在window下。
console.log(Math.random());
console.log(this.Math.random());
复制代码

4.做为全局变量的宿主。this

var a = 1;
console.log(this.a);
复制代码

5.客户端 JavaScript 中,全局对象有 window 属性指向自身。spa

var a = 1;
console.log(window.a);//1

this.window.b = 2;
console.log(this.b);//2
复制代码

咱们发现全局上下文中的变量对象就是全局对象

函数上下文

在函数上下文中,不一样于全局上下文比较死板,咱们用活动对象(activation object, AO)来表示变量对象。

因此活动对象和变量对象实际上是一个东西,只是变量对象是规范上或者说是引擎实现上不可在 JavaScript 环境中直接访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,因此称为activation object,只有在激活状态才会对属性进行访问。

活动对象是在进入函数上下文时刻被建立的,它经过函数的 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. 在代码执行阶段,会再次修改变量对象的属性值

例子

function foo() {
    console.log(a);
    a = 1;
}

foo(); // ???

function bar() {
    a = 1;
    console.log(a);
}
bar(); // ???
复制代码

第一段会报错:Uncaught ReferenceError: a is not defined

第二段会打印:1

这是由于函数中的 "a" 并无经过 var 关键字声明,全部不会被存放在 AO 中。

第一段执行 console 的时候, AO 的值是:

AO = {
    arguments: {
        length: 0
    }
}
复制代码

没有 a 的值,而后就会到全局去找,全局也没有,因此会报错。

当第二段执行 console 的时候,全局对象已经被赋予了 a 属性,这时候就能够从全局找到 a 的值,因此会打印 1。

可是这个例子在非严格模式下才会成立,由于严格模式并不会主动帮你建立一个变量

再看看另外一个例子

console.log(foo);

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

var foo = 1;
复制代码

会打印函数,而不是 undefined 。

这是由于在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,若是若是变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。

做用域

在讲解做用域链以前,先说说做用域

做用域是指程序源代码中定义变量的区域。

做用域对如何查找变量进行了规定,也就是肯定当前执行代码对变量的访问权限。

JavaScript 采用词法做用域(lexical scoping),也就是静态做用域。

编译原理

咱们都知道JavaScript是一门动态语言或是解释性语言,但事实上它是一门编译语言。

程序中一段源码在执行前虎易经理三个步骤,统称为“编译”

  1. 分词/词法分析(Tokenizing/Lexing)

这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元,例如:var = 2;。这段代码会分解成var、a、=、二、;。若是词法单元生成器在判断a是一个独立的分词单元仍是其余词法单元的一部分时,调用的是有状态的解析规则,那么这个过程就称为词法分析。

  1. 解析/语法分析(Parsing)

这个过程是将词法单元流动(数组)转汉城一个由元素所组成的表明了程序语法结构的书。 这个书称为“抽象语法树(AST)”,var a = 2;的抽象语法树,可能会有一个叫作VariableDeclearation的顶级节点,接下来是一个叫做Identifier(它的值是 a)的子节点,以及一个叫做AssignmentExpresstion的子节点,AssignmentExpresstion节点有一个叫做NumericLiteral(它的值是2)的子节点。

  1. 代码生产

将AST转换为可执行代码的过程为代码生成

简单来讲,就是有某种方法将var a = 2; 的AST转换为一组机器指令,用来建立一个叫做a的变量(包括分配内存),并将一个值储存在a中。

赋值操做

JavaScript在引擎中,变量的赋值操做会执行两个动做,首先编译器会在当前做用域中声明一个变量(若是以前没声明过),而后在运行时引擎会在做用域中查找该变量,若是可以找到就会给它赋值

在编译器中的过程

先引入两个名词

RHS:负责查找某个变量的值

LHS:找到变量的容器自己,从而对其赋值

如今咱们以console.log(a)为例,其中对a的引用进行是一个RHS引用,由于这里a并无赋予任何值。响应地,须要查找并取得a的值,这样值就传递给console.log()。

相比之下,例如:

a = 2;

这里对a的引用则是LHS的引用,由于实际上咱们并不关心当前的值是什么,只是想为= 2这个值操做找个一个目标或是容器

一个例子:

function foo(a){
  console.log(a + b)
}
var b = 2
foo(2)
复制代码

首先会对b进行RHS查询,没法在函数内部得到值,就会在上一级做用域查找,找到b以后再进行RHS查询。就是说,若是该变量若是在该做用域没有找到对应的赋值,就会向上查找,直到找到对应的赋值。

静态做用域与动态做用域

咱们大多使用的做用域是词法做用域, 而函数的做用域在函数定义的时候就决定了。

而与词法做用域相对的是动态做用域,函数的做用域是在函数调用的时候才决定的。

让咱们认真看个例子就能明白之间的区别:

var value = 1;

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

function bar() {
    var value = 2;
    foo();
}

bar();

复制代码

假设JavaScript采用静态做用域,让咱们分析下执行过程:

执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,若是没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,因此结果会打印 1。

假设JavaScript采用动态做用域,让咱们分析下执行过程:

执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。若是没有,就从调用函数的做用域,也就是 bar 函数内部查找 value 变量,因此结果会打印 2。

前面咱们已经说了,JavaScript采用的是静态做用域,因此这个例子的结果是 1。

动态做用域

bash 就是动态做用域 例如:

value=1
function foo () {
    echo $value;
}
function bar () {
    local value=2;
    foo;
}
bar
复制代码

做用域链

说完了做用域,终于到做用域链了。当查找变量的时候,会先从当前上下文的变量对象中查找,若是没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫作做用域链。

下面,让咱们以一个函数的建立和激活两个时期来说解做用域链是如何建立和变化的。

函数建立

函数的做用域在函数定义的时候就决定了。

这是由于函数有一个内部属性 [[scope]],当函数建立的时候,就会保存全部父变量对象到其中,你能够理解 [[scope]] 就是全部父变量对象的层级链,可是须要注意:[[scope]] 并不表明完整的做用域链

举个例子:

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

复制代码

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

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

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

复制代码

函数激活

当函数激活时,进入函数上下文,建立 VO/AO后,就会将活动对象添加到做用链的前端。

这时候执行上下文的做用域链,咱们命名为Scope

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

复制代码

这样咱们就建立了一个做用域链。

从新思考

如下面的例子为例,结合着以前讲的变量对象和执行上下文栈,咱们来总结一下函数执行上下文中做用域链和变量对象的建立过程:

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
];
复制代码

this

好吧,如今在说说this的问题,总结性的东西,面试题都会刷到,我就很少说了,下面我讲讲面试不考的知识,说说this究竟是什么

先看一段代码

function foo() {
  var a = 2;
  this.bar();
}
function bar() {
  console.log( this.a );
}
foo(); 
复制代码

聪明的同窗确定会发现会发现结果是undefined,在严格模式下会报错,首先,这段代码试图经过 this.bar() 来引用 bar() 函数。可是调用 bar() 最天然的方法是省略前面的 this,直接使用词法引用标识符。 此外,咱们发现咱们试图经过内部调用函数来改变词法做用域,从而让bar() 能够访问 foo() 做用域里的变量 a。这是不可能实现的。this 是在运行时进行绑定的,并非在编写时绑定,它的上下文取决于函数调用时的各类条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。 当一个函数被调用时,会建立一个活动对象。这个对象会包含函数在哪里被调用、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程当中用到。也就是说this在函数建立的时候,已经造成了。

这样执行上下文的三个属性就讲完了,大概过程如图所示:

回顾

上面咱们把三大属性就讲解了一遍,下面说说之前作过的例子:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
复制代码

两段代码都会打印'local scope'。虽然两段代码执行的结果同样,可是两段代码究竟有哪些不一样呢?

具体执行分析

咱们分析第一段代码:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
复制代码

执行过程以下:

  1. 执行全局代码,建立全局执行上下文,全局上下文被压入执行上下文栈
ECStack = [
        globalContext
    ];
复制代码
  1. 全局上下文初始化
globalContext = {
        VO: [global],
        Scope: [globalContext.VO],
        this: globalContext.VO
    }
复制代码
  1. 初始化的同时,checkscope 函数被建立,保存做用域链到函数的内部属性[[scope]]
checkscope.[[scope]] = [
      globalContext.VO
    ];
复制代码
  1. 执行 checkscope 函数,建立 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
        checkscopeContext,
        globalContext
    ];
复制代码
  1. checkscope 函数执行上下文初始化:
  • 复制函数 [[scope]] 属性建立做用域链,
  • 用 arguments 建立活动对象,
  • 初始化活动对象,即加入形参、函数声明、变量声明,
  • 将活动对象压入 checkscope 做用域链顶端。

同时 f 函数被建立,保存做用域链到 f 函数的内部属性[[scope]]

checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }
复制代码
  1. 执行 f 函数,建立 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈
ECStack = [
        fContext,
        checkscopeContext,
        globalContext
    ];
复制代码
  1. f 函数执行上下文初始化, 如下跟第 4 步相同:
  • 复制函数 [[scope]] 属性建立做用域链
  • 用 arguments 建立活动对象
  • 初始化活动对象,即加入形参、函数声明、变量声明
  • 将活动对象压入 f 做用域链顶端
fContext = {
        AO: {
            arguments: {
                length: 0
            }
        },
        Scope: [AO, checkscopeContext.AO, globalContext.VO],
        this: undefined
    }
复制代码
  1. f 函数执行,沿着做用域链查找 scope 值,返回 scope 值

  2. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

ECStack = [
        checkscopeContext,
        globalContext
    ];
复制代码
  1. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
ECStack = [
        globalContext
    ];
复制代码

ES5标准

ES5中在 咱们改进了命名方式

  • 词法环境(lexical environment)
  • 变量环境(variable environment)
  • this (this value)

因此执行上下文在概念上表示以下:

ExecutionContext = {
  ThisBinding = <this value>,
  LexicalEnvironment = { ... },
  VariableEnvironment = { ... },
}
复制代码

词法环境

官方的 ES5 文档把词法环境定义为

词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。

简单来讲词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。 如今,在词法环境的内部有两个组件:(1) 环境记录器和 (2) 一个外部环境的引用。

环境记录器是存储变量和函数声明的实际位置。 外部环境的引用意味着它能够访问其父级词法环境(做用域)。

词法环境有两种类型:

全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,好比 window 对象)还有任何用户定义的全局变量,而且 this的值指向全局对象。 在函数环境中,函数内部用户定义的变量存储在环境记录器中。而且引用的外部环境多是全局环境,或者任何包含此内部函数的外部函数。

环境记录器也有两种类型:

声明式环境记录器存储变量、函数和参数。 对象环境记录器用来定义出如今全局上下文中的变量和函数的关系。

简而言之,

在全局环境中,环境记录器是对象环境记录器。 在函数环境中,环境记录器是声明式环境记录器。

对于函数环境,声明式环境记录器还包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。 抽象地讲,词法环境在伪代码中看起来像这样:

GlobalExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
    }
    outer: <null>
  }
}

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
    }
    outer: <Global or outer function environment reference>
  }
}
复制代码

变量环境

它一样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中建立的绑定关系。 如上所述,变量环境也是一个词法环境,因此它有着上面定义的词法环境的全部属性。 在 ES6 中,词法环境组件和变量环境的一个不一样就是前者被用来存储函数声明和变量(let 和 const)绑定,然后者只用来存储 var 变量绑定。 咱们看点样例代码来理解上面的概念:

let a = 20;
const b = 30;
var c;

function multiply(e, f) {
 var g = 20;
 return e * f * g;
}

c = multiply(20, 30);
执行上下文看起来像这样:

GlobalExectionContext = {

  ThisBinding: <Global Object>,

  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      a: < uninitialized >,
      b: < uninitialized >,
      multiply: < func >
    }
    outer: <null>
  },

  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 在这里绑定标识符
      c: undefined,
    }
    outer: <null>
  }
}

FunctionExectionContext = {
  ThisBinding: <Global Object>,

  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 在这里绑定标识符
      g: undefined
    },
    outer: <GlobalLexicalEnvironment>
  }
}
复制代码

只有遇到调用函数 multiply 时,函数执行上下文才会被建立。 可能你已经注意到 let 和 const 定义的变量并无关联任何值,但 var 定义的变量被设成了 undefined。 这是由于在建立阶段时,引擎检查代码找出变量和函数声明,虽然函数声明彻底存储在环境中,可是变量最初设置为 undefined(var 状况下),或者未初始化(let 和 const 状况下)。 这就是为何你能够在声明以前访问 var 定义的变量(虽然是 undefined),可是在声明以前访问 let 和 const 的变量会获得一个引用错误。 这就是咱们说的变量声明提高。 执行阶段 这是整篇文章中最简单的部分。在此阶段,完成对全部这些变量的分配,最后执行代码。 注意 — 在执行阶段,若是 JavaScript 引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为 undefined。

总结

本篇文章对执行上下文进行了深刻的讨论,也对不一样的标准进行了大体的分析,意义在于略懂一些底层知识。说了那么多也写很差代码,知道个大概就行了。

JavaScript基础专题系列

JavaScript基础系列目录地址:

JavaScript基础专题之原型与原型链(一)

JavaScript基础专题之执行上下文和执行栈(二)

新手写做,若是有错误或者不严谨的地方,请大伙给予指正。若是这片文章对你有所帮助或者有所启发,还请给一个赞,鼓励一下做者,在此谢过。

相关文章
相关标签/搜索