一文带你掌握JavaScript中的执行上下文和作用域

 更新时间:2023年02月08日 11:18:59   作者:mick  
作为一名前端工作人员,我们必须知道JavaScript内部是如何执行的。那对于执行上下文和作用域的理解至关重要,无论是工作还是面试都是无法跳跃的一步,本文就来带大家深入了解一下

执行上下文

我们先来看段代码

var foo = function () {
  console.log("foo1")
}

foo() // foo1

var foo = function () {
  console.log("foo2")
}
foo() // foo2

那这段代码呢?

function foo() {
  console.log("foo1")
}
foo() // foo2

function foo() {
  console.log("foo2")
}
foo()// foo2

是不是有点懵逼了呢?第一段代码比较好理解,但是第二段代码为什么会打印两个"foo2"呢?

这是因为JavaScript引擎并非一行一行分析和执行程序的。当执行一段代码的时候,会有一些准备工作。那JavaScript引擎到底准备了哪些工作?

下面我们来一点点分析

console.log(a) // undefined
var a = 10

这段代码我们在定义a之前打印了a,但是并没有报错,说明在执行console.log(a)的时候,a就已经被声明了,也就是我们常说的变量提升,这就是准备工作。

var a
console.log(a)
a = 10

首先会把a的定义提前声明,而不是赋值。

下面我们看下对于函数声明和函数表达式,JavaScript引擎是如何做准备的。

console.log(add2(1, 2)) // 3
function add2(a, b) {
  return a + b
}

console.log(add1(1, 2)) // 报错:add1 is not a function
var add1 = function (a, b) {
  return a + b
}

我们发现,用函数语句创建的add2,函数名称和函数体都被提前,在声明它之前使用它。而函数表达式只是变量声明提前了,变量赋值仍然在之前的位置。现在回到刚开始那段代码是不是就理解了呢?

所以JavaScript引擎都做好了哪些准备工作呢?

  • 变量、函数表达式——变量提前声明,默认为undefined
  • 函数声明——提前声明并赋值

其实还有一个this也是提前就准备好了,并且也赋值了。

当执行一个函数的时候,就会进行准备工作,这里的“准备工作”,就是“执行上下文”

执行上下文栈

执行上下文栈管理执行上下文。JavaScript代码有两种执行上下文:全局执行上下文和函数执行上下文,还有一个是eval(我们先不考虑)。全局执行上下文只有一个,函数执行上下文是在每次函数执行调用的时候,就会创建一个新的。

每个执行上下文都有三个属性:

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

变量对象

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

不同执行上下文的变量对象不同,下面来看看全局上下文的变量对象和函数上下文的变量对象

全局上下文

  • 全局对象是预定义的对象,作为JavaScript的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义对象、函数和属性
  • 在顶层的JavaScript代码中,可以用关键字this引用全局对象。因为全局对象是作用域链的头,意味着所有非限定性的变量和函数名都会作为该对象的属性来查询
  • 例如,当JavaScript代码引用parseInt()函数时,它引用的是全局对象的parseInt属性。

函数上下文

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

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

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

执行过程

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

  • 进入执行上下文
  • 代码执行

进入执行上下文

当调用函数后,进入执行上下文,在执行代码之前,变量对象会包含:

函数的所有形参

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

函数声明

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

变量声明

  • 由名称和对应值(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={
    arguments: {
        0:1,
        length:1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

小小总结一下变量对象:

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

下面我们看下执行上下文栈是如何工作的

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

function fun2() {
  fun3()
}

function fun1() {
  fun2()
}

fun1()

我们用数组模拟执行上下文栈,最先遇到的是全局代码,初始化的时候,会向执行上下文栈中压入全局执行上下文globalContext

Stack=[
    globalContext
]

当执行一个函数时候,就会创建一个执行上下文,并且压入执行上下文栈中,当函数执行完毕后,就会将函数的执行上下文从栈中弹出。上下文所在其所有的代码执行完毕后会被销毁。

// 执行fun1
Stack.push(<fun1>functionContext);

// fun1中调用了fun2
Stack.push(<fun2>functionContext);

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

//fun3执行完毕 弹出
Stack.pop()

//fun2执行完毕 弹出
Stack.pop()

//fun1执行完毕 弹出
Stack.pop()

最后Stack底层永远有个全局执行上下文globalContext。

作用域

作用域是指程序源代码中定义变量的区域。作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。JavaScript采用词法作用域,也就是静态作用域。

静态作用域和动态作用域

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

作用域链

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

函数创建

上面提到,函数的作用域在函数定义的时候就已经决定了。这是因为函数有一个内部属性[[scope]],当函数创建的时候,就会保存所有父变量对象到其中,可以理解[[scope]]就是所有父变量对象的层级链,但是[[scope]]并不代表完整的作用域链。我们来看个代码:

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

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

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

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

当函数激活,进入函数体,创建VO/AO后,就会将活动对象添加到作用链的前端。

总结

执行上下文和作用域的区别:

1.全局作用域除外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了,而不是在函数调用时。

全局执行上下文环境是在全局作用域确定之后,js代码马上执行之前创建的。

函数执行上下文是在调用函数时,执行函数体代码之前创建的。

2.作用域是静态的,只要函数定义好了就一直存在,且不会再变化。

执行上下文环境是动态的,调用函数时创建,函数调用结束上下文环境就会被释放。

以上就是一文带你掌握JavaScript中的执行上下文和作用域的详细内容,更多关于JavaScript执行上下文 作用域的资料请关注脚本之家其它相关文章!

相关文章

  • JavaScript使用setTimeout实现倒计时效果

    JavaScript使用setTimeout实现倒计时效果

    这篇文章主要为大家详细介绍了JavaScript使用setTimeout实现倒计时效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-02-02
  • 微信小程序checkbox组件使用详解

    微信小程序checkbox组件使用详解

    这篇文章主要介绍了微信小程序checkbox组件的使用,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-01-01
  • Element-plus安装及基础组件使用详解

    Element-plus安装及基础组件使用详解

    ElementPlus是一个基于Vue3的UI组件库,旨在提供丰富的HTML元素封装,以简化前端开发,主要特点包括预定义样式、事件处理、易用性等,为开发者提供了一致且美观的用户界面,同时支持按需导入,提高项目效率,感兴趣的朋友一起看看吧
    2024-09-09
  • 如何用CocosCreator实现射击小游戏

    如何用CocosCreator实现射击小游戏

    这篇文章主要介绍了如何用CocosCreator实现射击小游戏,此游戏难度不大,仅作为入门的练手小游戏,一小时就能完成,里面用到的知识很常用,喜欢游戏的同学,可以参考下
    2021-04-04
  • 浅谈javascript六种数据类型以及特殊注意点

    浅谈javascript六种数据类型以及特殊注意点

    这篇文章主要介绍了javascript六种数据类型以及特殊注意点,有需要的朋友可以参考一下
    2013-12-12
  • js脚本获取webform服务器控件的方法

    js脚本获取webform服务器控件的方法

    asp.net webform中获取服务器控件,js脚本获取服务器控件需要使用ClientID,下面有个示例,大家可以参考下
    2014-05-05
  • JavaScript实现简单图片切换

    JavaScript实现简单图片切换

    这篇文章主要为大家详细介绍了JavaScript实现简单图片切换,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-04-04
  • JS访问SWF的函数用法实例

    JS访问SWF的函数用法实例

    这篇文章主要介绍了JS访问SWF的函数用法,实例分析了javascript访问swf文件的方法及易错点的处理技巧,需要的朋友可以参考下
    2015-07-07
  • 原生JS和jQuery操作DOM对比总结

    原生JS和jQuery操作DOM对比总结

    这篇文章主要给大家介绍了原生JS和jQuery操作DOM的一些对比总结,文中总结了很多的对比,相信对大家的学习或者工作能带来一定的帮助,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-01-01
  • js实现带圆角的多级下拉菜单效果

    js实现带圆角的多级下拉菜单效果

    这篇文章主要介绍了js实现带圆角的多级下拉菜单效果,通过调用封装的js库ocscript.js实现圆角下拉菜单功能,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-08-08

最新评论