资讯专栏INFORMATION COLUMN

JavaScript-作用域-执行上下文-变量对象-作用域链

liangzai_cool / 3481人阅读

摘要:变量对象作用域链因为变量对象在执行上下文进入执行阶段时,就变成了活动对象,因此图中使用了来表示。

作用域

作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在 JavaScript 中,变量的作用域有全局作用域和局部作用域两种。JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。

静态作用域

函数的作用域在函数定义的时候就决定了。
js函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,下文会详细描述

动态作用域

函数的作用域是在函数调用的时候才决定的。

实例

静态作用域的语言下面的代码会打出1,因为在foo定义的时候,他的作用域就确定了在全局(后面讲变量对象的时候也会说foo是注册在全局的而不是在bar里面才注册)

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

var value = 1;

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

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

bar();
执行上下文 执行上下文(Execution Context)

就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。

JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。

编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。

执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。

执行上下文创建和执行:

执行上下文有以下三个属性

变量对象(Variable object,VO)

作用域链(Scope chain)

this

执行上下文总共有三种类型:

全局执行上下文: 这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:1. 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。2. 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。

函数执行上下文: 每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤,具体过程将在本文后面讨论。

Eval 函数执行上下文: 运行在 eval 函数中的代码也获得了自己的执行上下文(不常用)

执行上下文栈

JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文
当 JavaScript 引擎首次读取你的脚本时,它会创建一个全局执行上下文并将其推入当前的执行栈。每当发生一个函数调用,引擎都会为该函数创建一个新的执行上下文并将其推到当前执行栈的顶端。
引擎会运行执行上下文在执行栈顶端的函数,当此函数运行完成后,其对应的执行上下文将会从执行栈中弹出,上下文控制权将移到当前执行栈的下一个执行上下文。

let a = "Hello World!";

function first() {  
  console.log("Inside first function");  
  second();  
  console.log("Again inside first function");  
}

function second() {  
  console.log("Inside second function");  
}

first();  
console.log("Inside Global Execution Context");

浏览器中加载时,JavaScript 引擎会创建一个全局执行上下文并且将它推入当前的执行栈。

当调用 first() 函数时,JavaScript 引擎为该函数创建了一个新的执行上下文并将其推到当前执行栈的顶端。

当在 first() 函数中调用 second() 函数时,创建了一个新的执行上下文并将其推到当前执行栈的顶端。

当 second() 函数执行完成后,它的执行上下文从当前执行栈中弹出,上下文控制权将移到当前执行栈的下一个执行上下文,即 first() 函数的执行上下文。

当 first() 函数执行完成后,它的执行上下文从当前执行栈中弹出,上下文控制权将移到全局执行上下文。

一旦所有代码执行完毕,Javascript 引擎把全局执行上下文从执行栈中移除。

// 伪代码

ECStack = [
    globalContext
];

// first()
ECStack.push( functionContext);

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


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

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


// 当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext:
变量对象 什么是变量对象

变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

什么是全局对象

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

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

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

可以通过 this 引用,在客户端 JavaScript 中,全局对象就是 Window 对象。

console.log(this);// this 引用,在客户端 JavaScript 中,全局对象就是 Window 对象。

console.log(this instanceof Object);//全局对象是由 Object 构造函数实例化的一个对象。

console.log(Math.random());//.预定义了一堆,嗯,一大堆函数和属性。
console.log(this.Math.random());

var a = 1;//作为全局变量的宿主。
console.log(this.a);
函数上下文

在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。
变量对象VO和活动对象AO是同一个对象在不同阶段的表现形式。当进入执行环境的创捷阶段时,变量对象被创建,这时变量对象的属性无法被访问。进入执行阶段后,变量对象被激活变成活动对象,此时活动对象的属性可以被访问。

函数执行过程 进入执行上下文,创建阶段

当进入执行上下文时,这时候还没有执行代码,在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。
变量对象会包括:

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

函数声明

变量声明

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

  b = 3;

}

foo(1);

根据函数参数,创建并初始化arguments对象,及形参属性

检查上下文中的函数声明,将函数名作为变量对象的属性,函数引用作为值。如果该函数名在变量对象中已存在,则覆盖已存在的函数引用。

检查上下文的变量声明,将变量名作为变量对象的属性,值设置为undefined。如果该变量名在变量对象中已存在,为防止与函数名冲突,则跳过,不进行任何操作。

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,//注意a已经初始化了
    b: undefined,
    c: reference to function c(){},//如果重名后调过了变量c只有函数c
    d: undefined
}
代码执行阶段

上下文创建完成之后,就会开始执行代码,这个时候,会完成变量赋值,函数引用,以及执行其他代码。

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: 3,//执行阶段c又会重新被赋值
    d: reference to FunctionExpression "d"
}
上下文总结

全局上下文的变量对象初始化是全局对象

函数上下文创建阶段函数先注册重名覆盖,变量后注册重名跳过

函数上下文的变量对象初始化只包括 Arguments 对象

在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值,也就是初始化变量对象

在代码执行阶段,会再次修改变量对象的属性值

作用域链 什么是作用域链 定义

作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。

形成

上文的作用域中讲到过函数的作用域在函数定义的时候就决定了,因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从自己的scope中保存的父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

区分作用域与作用域链 作用域

在JavaScript中,我们可以将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。

两者的区别

作用域是一套规则,那么作用域链是什么呢?是这套规则的具体实现。

作用域规则在代码编译阶段就确定了,而作用域链是在执行上下文的创建阶段生成的

举个例子
var a = 20;

function test() {
    var b = 10;
   //function innerTest() {
   //     var c = 10;
   //     return b + c;
   //}
    return b;
}

test();

执行过程
1.test 函数在全局上下文中被创建,保存全局上下文的变量对象组成的作用域链到内部属性[[scope]]

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

2.创建 test 函数执行上下文,test函数执行上下文被压入执行上下文栈

ECStack = [
    testContext,
    globalContext
];

3.test 函数并不立刻执行,开始做准备工作,第一步:复制[[scope]]属性到函数上下文,创建了作用域链

testContext = {
    Scope: testscope.[[scope]],
}

4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明

testscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        b: undefined
    },
    Scope: testscope.[[scope]],
}

5.第三步:将活动对象压入 testscope 作用域链顶端

testscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        b: undefined
    },
    Scope: [AO, [[Scope]]]//用Scope简写testscope.[[scope]]
}

6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值

testscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        b: 10
    },
    Scope: [AO, [[Scope]]]
}

7.查找到 b 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出

ECStack = [
    globalContext
];

8.如果test内部含有innerTest函数,则在该innerTest函数创建时将test上下文中的作用域链传入(testscopeContext.Scope)
然后后循环执行和test相同的步骤

var a = 20;

function test() {
    var b = 10;
   function innerTest() {
        var c = 10;
        return b + c;
   }
    return b;
}

test();

全局,函数test,函数innerTest的执行上下文先后创建。我们设定他们的变量对象分别为VO(global),VO(test), VO(innerTest)。而innerTest的作用域链,则同时包含了这三个变量对象,所以innerTest的执行上下文可如下表示。

innerTestContext  = {
    AO: {...},  // 变量对象
    Scope: [VO(innerTest), VO(test), VO(global)], // 作用域链
}

因为变量对象在执行上下文进入执行阶段时,就变成了活动对象,因此图中使用了AO来表示。Active Object
作用域链是由一系列变量对象组成,我们可以在这个单向通道中,查询变量对象中的标识符,这样就可以访问到上一层作用域中的变量了。

参考文章

https://github.com/mqyqingfen...

https://www.jianshu.com/p/21a...

https://juejin.im/post/5bdfd3...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/54225.html

相关文章

  • JavaScript-作用-执行下文-变量对象-作用

    摘要:变量对象作用域链因为变量对象在执行上下文进入执行阶段时,就变成了活动对象,因此图中使用了来表示。 作用域 作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在 JavaScript 中,变量的作用域有全局作用域和局部作用域两种。JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。 静态作用域 函数的作用域在函数定义的时候...

    MonoLog 评论0 收藏0
  • 理解JavaScript中的作用作用

    摘要:示例当一个函数创建后,它的作用域链会被创建此函数的作用域中可访问的数据对象填充。每一个运行期上下文都和一个作用域链关联。此时,作用域链中函数的所有局部变量所在的作用域对象会被推后,访问代价变高了。 作用域 作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在JavaScript中,变量的作用域有全局作用域和局部作用域两种。 作用域链 函数对象有一个内部属性[...

    XanaHopper 评论0 收藏0
  • JavaScript深入之作用

    摘要:下面,让我们以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的。这时候执行上下文的作用域链,我们命名为至此,作用域链创建完毕。 JavaScript深入系列第五篇,讲述作用链的创建过程,最后结合着变量对象,执行上下文栈,让我们一起捋一捋函数创建和执行的过程中到底发生了什么? 前言 在《JavaScript深入之执行上下文栈》中讲到,当JavaScript代码执行一段可执行代...

    waltr 评论0 收藏0
  • 讲清楚之javascript作用

    摘要:并且作用域链也确定了在当前上下文中查找标识符后返回的值。为了具象化分析问题,我们可以假设作用域链是一个数组,数组成员有一系列变量对象组成。注意,所有作用域链的最末端都为全局变量对象。所以作用域作用域链都是在当前运行环境内代码执行前就确定了。 什么是作用域(Scope)? 作用域产生于程序源代码中定义变量的区域,在程序编码阶段就确定了。javascript 中分为全局作用域(Global...

    whidy 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<