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

栏目: JavaScript · 发布时间: 5年前

内容简介:变量对象作为执行上下文的一种属性,每次创建后,根据执行环境不同上下文下的变量对象也稍有不同,我们比较熟悉的就是我们先了解一个概念,什么叫全局对象。在W3School中:全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。

变量对象作为执行上下文的一种属性,每次创建后,根据执行环境不同上下文下的变量对象也稍有不同,我们比较熟悉的就是 全局对象函数对象 ,所以我们来聊聊全局上下文下的变量对象和函数上下文下的变量对象。

全局上下文

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

全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。

在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。

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

我们可以根据代码理解

  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.作为全局变量的宿主。

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

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

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、=、2、;。如果词法单元生成器在判断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在函数创建的时候,已经形成了。

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

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

回顾

上面我们把三大属性就讲解了一遍,下面说说以前做过的例子:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
复制代码
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
    ];
复制代码

总结

本篇对执行上下文的三个属性做了一些总结,深入了理解了执行过程,出于兴趣去了解,意义在于学习一些底层知识。说了那么多也写不好代码,知道个大概就好了。


以上所述就是小编给大家介绍的《JavaScript基础专题之深入执行上下文(三)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

产品型社群

产品型社群

李善友 / 机械工业出版社 / 2015-3-1 / CNY 69.00

传统模式企业正在直面一场空前的“降维战争”, 结局惨烈,或生或死。 传统模式很难避免悲惨下场, 诺基亚等昔日庞然大物轰然倒塌, 柯达发明了数码成像技术却依然破产, 新商业的兴起到底遵循的是什么模式? 微信轻而易举干掉了运营商的短信业务, “好未来”为何让传统教育不明觉厉? 花间堂为什么不是酒店,而是入口? 将来不会有互联网企业与传统企业之分, ......一起来看看 《产品型社群》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具