彻底明白js的作用域、执行上下文

简介: 彻底明白js的作用域、执行上下文

一、执行上下文

JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建

上面提到的可执行代码,那么什么是可执行代码呢?

其实很简单,就三种,全局代码、函数代码、eval代码。

其中eval代码大家可以忽略,毕竟实际开发中处于性能考虑基本不会用到,所以接下来我们重点关注的就是全局代码、函数代码

在庞大的代码里必然不会只有一两个函数,那么如何管理每次执行函数时候创建的上下文呢

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

为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组:

ECStack = [];

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

ECStack = [

   globalContext

];

举个?:

function out(){

   function inner(){}

   inner()

}

out()

那么这个函数的执行上下文栈会经历以下过程:

ECStack.push(globalContext)

ECStack.push(outContext)

ECStack.push(innerContext)

ECStack.pop(innerContext)

ECStack.pop(outContext)

ECStack.pop(globalContext)

再来看一个闭包的?:

function f1(){

   var n=999;

   function f2(){

       console.log(n)

   }

   return f2;

}

var result=f1();

result(); // 999

该函数的执行上下文栈会经历以下过程:

ECStack.push(globalContext)

ECStack.push(f1Context)

ECStack.pop(f1Context)

ECStack.push(resultContext)

ECStack.pop(resultContext)

ECStack.pop(globalContext)

大家自行感受一下对比,一定要记住上下文是在函数调用的时候才会生产

既然调用一个函数时一个新的执行上下文会被创建。那执行上下文的生命周期同样可以分为两个阶段。

  • 创建阶段
    在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。
  • 代码执行阶段
    在这个阶段会生成三个重要的东西
    a.变量对象(Variable object,VO)
    b.作用域链(Scope chain)
    c.this

变量对象

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

活动对象其实就是被激活的变量对象,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object,而只有活动对象上的各种属性才能被访问。

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

1.进入执行上下文

当进入执行上下文时,这时候还没有执行代码,

变量对象会包括:

  • 函数的所有形参 (如果是函数上下文)a.由名称和对应值组成的一个变量对象的属性被创建
    b.没有实参,属性值设为 undefined
  • 函数声明a.由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
    b.如果变量对象已经存在相同名称的属性,则完全替换这个属性
  • 变量声明a.由名称和对应值(undefined)组成一个变量对象的属性被创建;
    b.如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

根据这个规则,理解变量提升就变得十分简单了

举个?分析下,看下面的代码:

function foo(a) {
  console.log(b)
  console.log(c)
  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
}

2.代码执行

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值

还是上面的例子,当代码执行完后,这时候的 AO 是:

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

因此,这个例子代码执行顺序就是这样的

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

二、作用域

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

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

因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。

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

经典的一道面试题

var a = 1
function out(){
    var a = 2
    inner()
}
function inner(){
    console.log(a)
}
out()  //====>  1

三、作用域链

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

下面,让我们以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的。

上面讲到函数作用域是在创建的阶段确定

这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!

举个?

function out() {
    function inner() {
        ...
    }
}

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

out.[[scope]] = [
  globalContext.VO
];
 
inner.[[scope]] = [
    outContext.AO,
    globalContext.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
];

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

ECStack = [
    checkscopeContext,
    globalContext
];

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

checkscopeContext = {
    Scope: checkscope.[[scope]],
}

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

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: checkscope.[[scope]],
}

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

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}

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

ECStack = [
    globalContext
];

至此,关于作用域和执行上下文的介绍就到这里,希望大家多消化,有问题请在评论中及时指出

相关文章
|
11天前
|
JavaScript 前端开发
js的作用域作用域链
【10月更文挑战第29天】理解JavaScript的作用域和作用域链对于正确理解变量的访问和生命周期、避免变量命名冲突以及编写高质量的JavaScript代码都具有重要意义。在实际开发中,需要合理地利用作用域和作用域链来组织代码结构,提高代码的可读性和可维护性。
|
11天前
|
自然语言处理 JavaScript 前端开发
[JS]作用域的“生产者”——词法作用域
本文介绍了JavaScript中的作用域模型与作用域,包括词法作用域和动态作用域的区别,以及全局作用域、函数作用域和块级作用域的特点。通过具体示例详细解析了变量提升、块级作用域中的暂时性死区等问题,并探讨了如何在循环中使用`var`和`let`的不同效果。最后,介绍了两种可以“欺骗”词法作用域的方法:`eval(str)`和`with(obj)`。文章结合了多位博主的总结,帮助读者更快速、便捷地掌握这些知识点。
22 2
[JS]作用域的“生产者”——词法作用域
|
3月前
|
JavaScript 前端开发
浅谈js作用域
浅谈js作用域
32 0
|
12天前
|
前端开发 JavaScript 数据处理
CSS 变量的作用域和 JavaScript 变量的作用域有什么不同?
【10月更文挑战第28天】CSS变量和JavaScript变量虽然都有各自的作用域概念,但由于它们所属的语言和应用场景不同,其作用域的定义、范围、覆盖规则以及与其他语言特性的交互方式等方面都存在明显的差异。理解这些差异有助于更好地在Web开发中分别运用它们来实现预期的页面效果和功能逻辑。
|
29天前
|
存储 JavaScript 前端开发
深入理解 JavaScript 执行上下文与 this 绑定机制
JavaScript 代码执行时,会为每段可执行代码创建对应的执行上下文,其中包含三个重要属性:变量对象、作用域链、和 this。本文深入剖析了执行上下文的生命周期以及 this 在不同情况下的指向规则。通过解析全局上下文和函数上下文中的 this,我们详细讲解了 this 的运行期绑定特性,并展示了如何通过调用方式影响 this 的绑定对象。同时,文中对箭头函数 this 的特殊性以及四条判断 this 绑定的规则进行了总结,帮助开发者更清晰地理解 JavaScript 中的 this 行为。
71 8
深入理解 JavaScript 执行上下文与 this 绑定机制
|
11天前
|
JavaScript 前端开发
如何在 JavaScript 中实现块级作用域?
【10月更文挑战第29天】通过使用 `let`、`const` 关键字、立即执行函数表达式以及模块模式等方法,可以在JavaScript中有效地实现块级作用域,更好地控制变量的生命周期和访问权限,提高代码的可维护性和可读性。
|
19天前
|
JavaScript 前端开发
javascript的作用域
【10月更文挑战第19天javascript的作用域
|
11天前
|
自然语言处理 JavaScript 前端开发
如何在 JavaScript 中创建执行上下文
在JavaScript中,每当执行一段代码时,都会创建一个执行上下文。它首先进行变量、函数声明的创建和内存分配(即变量环境和词法环境的建立),接着进入代码执行阶段,处理具体逻辑。
|
25天前
|
JavaScript 前端开发
JavaScript 作用域
JavaScript 作用域是指程序中可访问的变量、对象和函数的集合。它分为函数作用域和局部作用域。函数作用域内的变量仅在函数内部可见,而全局作用域的变量在整个网页中均可访问。局部变量在函数执行完毕后会被销毁,而全局变量则在整个脚本生命周期中都存在。未使用 `var` 关键字声明的变量默认为全局变量。
|
11天前
|
存储 自然语言处理 JavaScript
如何在 JavaScript 中创建执行上下文
在JavaScript中,作用域链是一套用于查找变量和函数的机制,由当前执行上下文的变量对象和所有外层执行上下文的变量对象组成。它包括全局作用域、函数作用域和块级作用域。作用域链的工作原理是从内向外逐层查找变量,直至全局作用域。闭包通过作用域链记住其词法作用域,即使在外部作用域之外执行也能访问内部变量。作用域链有助于变量隔离、模块化和数据隐藏,提高代码的可维护性和可读性。