前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >兄台: 作用域、执行上下文了解一下

兄台: 作用域、执行上下文了解一下

作者头像
前端柒八九
发布2022-08-25 14:12:16
5070
发布2022-08-25 14:12:16
举报
文章被收录于专栏:柒八九技术收纳盒
  1. 作用域控制着变量和函数的可见性和生命周期
  2. JS的作用域(scope)是「静态」的(static)
  3. ES6块级作用域和函数作用域属于同一大类(声明式作用域)
  4. ES6块级作用域是函数作用域的子集
  5. with会扩展作用域链
  6. 在全局作用域下,声明式(块级)ER优先级高
  7. 块级作用域中的(let/const)变量查找路径 1. 词法环境 2. 变量环境 3. OuterEnv对象
  8. 作用域链 是由环境记录(ER)的内部属性 OuterEnv串联起来的
  9. 作用域只是执行上下文有权访问的一组「有限」的变量/对象
  10. 同一个执行上下文上可能存在多个作用域
  11. 每个执行上下文都有「自己」的作用域
  12. 使用 setTimeout 来解决栈溢出:setTimeout 会将包装函数「封装成一个新的宏任务」,并将其添加到消息队列中

文章概要

  • 作用域(Scopes)
    • 词法环境(Lexical environments)
    • 作用域链
  • 执行上下文
  • 调用栈

作用域(Scopes)

变量的词法作用域(简称:作用域)是程序中可以访问变量的区域。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

JS的作用域是「静态」的(不会在运行时被改变)

作用域可以被嵌套。

代码语言:javascript
复制
function func() { // (A)
  const a = 1;
  if (true) { // (B)
    const b = 2;
  }
}

if语句引入的作用域(B行)嵌套在函数func()的作用域内(A行)。

词法环境(Lexical environments)

在ecma262(自备🪜)语言规范中定义:「作用域是通过词法环境实现的」

而词法环境由两个重要部分组成:

  1. 「环境记录」(environment record): 将变量名映射到变量值(类似于Map)。这是作用域变量的实际存储空间。记录中的「名称-值」条目称为绑定。
  2. OuterEnv内部属性:指向「外部环境」(outer environment)的引用

并且,由于 Environment Record(以下简称:ER) 是一个「抽象类」,它由三个具体的子类实现:

  1. 声明式ER 而声明式ER又派生出 1. 函数ER 2. module ER
  2. 对象ER 通过with扩展作用域链
  3. 全局ER

所以,我们可以这样认为:

❝作用域被分为3类 1. 声明式作用域(函数作用域/module作用域) 2. 对象作用域 3. 全局作用域 ❞

1. 声明式作用域

声明式ER可以通过 var/const/let/class/module/import/function生成。

也就是说我们常说的ES6块级作用域和函数作用域属于同一大类(声明式作用域)。

根据实现层级,还有一个更准确的结论:

❝ES6块级作用域是函数作用域的子集 ❞

2. 对象作用域

对于对象作用域(使用with)不常见,我们简单用代码描述一下。

代码语言:javascript
复制
function test(){
    let ob = { name:'789' };
    // 利用with扩展了作用域
    with(ob){
        console.log(`my name is ${name}`)
    }
}

test(); // my name is 789

这里多说一嘴:

HTML事件处理程序,作为事件处理程序执行的代码可以访问全局作用域中的一切。

代码语言:javascript
复制
<script> 
 function showMessage() { 
    // 可以访问全局作用域中的一切
 } 
</script> 
<input type="button" value="你过来啊!" onclick="showMessage()"/> 

以这种方式指定的事件处理程序,会创建一个函数来封装属性的值,这个函数有一个特殊的局部变量 event,其中保存的就是 event 对象。这个函数的「作用域链被扩展」了。这个函数中,document 和元素自身的成员都可以被当成局部变量来访问。

代码语言:javascript
复制
// 通过使用 with 实现的
function() { 
     with(document) { 
         with(this) { 
           // 属性值
          } 
      } 
} 

3.全局作用域

全局作用域是最外面的作用域,它没有外部作用域。

❝全局环境的OuterEnv为null。 ❞

全局ER使用两个ER来管理其变量:

  1. 对象ER : 将变量存储在全局对象中
  2. 声明式ER : 使用内部对象来存储变量
代码语言:javascript
复制
<script>
  const one = 1;
  var two = 2;
</script>
<script>
  // 所有脚本都共享顶层作用域
  console.log(one); // 1
  console.log(two); // 2
  
  // 并非所有的声明都被存入到全局对象中
  console.log(globalThis.one); // undefined
  console.log(globalThis.two); // 2
</script>
  1. 顶层作用域下,const/let/class声明的变量被绑定在声明ER里
  2. 顶层作用域下,varfunction声明的变量被绑定在对象ER里(在浏览器环境下, window指向全局对象)

当声明式ER和对象ER有共同的变量,声明式优先级高。

代码语言:javascript
复制
<script>
  let gv = 1; // declarative ER
  globalThis.gv = 2; // object ER

  console.log(gv); // 1 (声明式优先级高)
  console.log(globalThis.gv); // 2
</script>

变量查找是从下往上的。也就是声明式优先级高。

用一个图来收个尾

作用域链

在 JS 执行过程中,其作用域链是由词法作用域决定的。变量的可访问性在编译阶段(执行之前)已经确定了。所以,在函数进行变量查找时,我们只根据词法作用域(函数编码位置)来确定变量的可见性。

代码语言:javascript
复制
function tool(){
  console.log(myName)
}

function test(){
  var myName = 'inner';
  tool();
}
var myName = 'outer';
test(); // outer

❝词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系 ❞

这里再多说一嘴,ES6 是支持块级作用域的,当执行到代码块时,如果代码块中有 let 或者 const 声明的变量,针对变量的查询路径为: 1. 词法环境 2. 变量环境 3. OuterEnv对象(上一层作用域继续先1后2)

执行上下文

❝1.作用域只是执行上下文有权访问的一组「有限」的变量/对象 2.同一个执行上下文上可能存在多个作用域 ❞

执行上下文是执行其代码的函数的环境。「每个函数都有自己的执行上下文」

我们通过一个例子,来窥探下,执行上下文的内部结构。

话不多说,上菜。

小提示:存在两个用let定义的b

代码语言:javascript
复制
function foo(){
    var a = 1
    let b = 2
    {
      let b = 3
      var c = 4
      let d = 5
      console.log(a)  // {A}
      console.log(b)
    }
    console.log(b) 
    console.log(c)
    console.log(d)
}   
foo()  // 1,3,2,4,undefined

我们在v8如何处理JS中讲过,执行JS代码核心流程 1. 先编译 2. 后执行。

针对如上代码,先对其进行编译并创建执行上下文,然后再按照顺序执行代码。

函数内部通过 var 声明的变量在编译阶段全都被存放到变量环境里面了。通过 let 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。

「继续执行代码」,当执行到代码块里面时,变量环境中 a 的值已经被设置成了 1,词法环境中 b 的值已经被设置成了 2。

当进入函数的作用域块,作用域块中通过 let 声明的变量会被存放在词法环境的一个单独的区域中。这个区域中的变量并不影响作用域块外面的变量。

❝在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶。当作用域执行完成之后,该作用域的信息就会「从栈顶弹出」。 ❞

当执行到作用域块中的{A}这行代码时就需要在词法环境和变量环境中查找变量 a 的值。

具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎。如果没有查找到,那么继续在变量环境中查找。

作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出。

其实,在ECMA262规范定义中,针对执行上下文还有更多的属性和方法。其中,最重要的就是我们上文讲到的词法环境和语法环境(变量环境)。

还有用于描述该执行上下文代码状态的 Code State:是处于执行(perform)、挂起(suspend)还是继续执行(resume)。(盲猜这里是不是和协程、生成器有关系)


调用栈

执行上下文都都介绍了,那调用栈还远吗?

调用栈就是用来管理函数调用关系的一种数据结构,是 JavaScript 引擎追踪函数执行的一个机制。

代码语言:javascript
复制
function bar() {
}
function foo(fun){
  fun()
}
foo(bar)

V8 准备执行这段代码时,先将全局执行上下文压入到调用栈。

V8 在主线程上执行 foo 函数,创建 foo 函数的执行上下文,并将其压入栈中。

V8 执行 bar 函数时,创建 bar 函数的执行上下文,并将其压入栈中。

bar 函数执行结束,V8 就会从栈中弹出 bar 函数的执行上下文。

foo 函数执行结束,V8 会将 foo 函数的执行上下文从栈中弹出。

堆栈溢出

过多的执行上下文堆积在栈中便会导致栈溢出。

代码语言:javascript
复制
function foo(){
  foo()
}
foo()

foo 函数内部嵌套调用它自己,调用栈会一直向上增长。最后会爆栈,把主线程给阻塞。

使用 setTimeout 来解决栈溢出的问题

我们可以利用setTimeout来解决栈溢出问题。setTimeout 的本质是将同步函数调用改成异步函数调用。

代码语言:javascript
复制
function foo() {
  setTimeout(foo, 0)
}
foo()

异步调用是将 foo 封装成事件,并将其添加进消息队列中,主线程再按照一定规则循环地从消息队列中读取下一个任务。

主线程会从消息队列中取出需要执行的宏任务。

V8 就要执行 foo 函数,并创建 foo 函数的执行上下文,将其压入栈中。

V8 执行执行 foo 函数中的 setTimeout 时,setTimeout 会将 foo 函数「封装成一个新的宏任务」,并将其添加到消息队列中。

foo 函数执行结束,V8 就会结束当前的宏任务,调用栈也会被清空。

当一个宏任务执行结束之后,主线程会一直重复取宏任务、执行宏任务的过程,通过 setTimeout 封装的回调宏任务,会在某一时刻被主线取出并执行。

foo 函数并不是在当前的父函数内部被执行的,而是封装成了宏任务,并丢进了消息队列中,等待主线程从消息队列中取出该任务,再执行该回调函数 foo。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-01-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端柒八九 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章概要
  • 作用域(Scopes)
    • 词法环境(Lexical environments)
      • 1. 声明式作用域
      • 2. 对象作用域
      • 3.全局作用域
    • 作用域链
    • 执行上下文
    • 调用栈
      • 堆栈溢出
        • 使用 setTimeout 来解决栈溢出的问题
    相关产品与服务
    消息队列 CMQ 版
    消息队列 CMQ 版(TDMQ for CMQ,简称 TDMQ CMQ 版)是一款分布式高可用的消息队列服务,它能够提供可靠的,基于消息的异步通信机制,能够将分布式部署的不同应用(或同一应用的不同组件)中的信息传递,存储在可靠有效的 CMQ 队列中,防止消息丢失。TDMQ CMQ 版支持多进程同时读写,收发互不干扰,无需各应用或组件始终处于运行状态。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档