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

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


简单的欲望,只需要放纵就可以实现;而高级的欲望,需要自律和克制来实现

一语中的

  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的作用域是静态的(不会在运行时被改变)

作用域可以被嵌套。

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)不常见,我们简单用代码描述一下。

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

这里多说一嘴:

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

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

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

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

3.全局作用域

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

全局环境的OuterEnv为null。

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

  1. 对象ER : 将变量存储在全局对象
  2. 声明式ER : 使用内部对象来存储变量
<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有共同的变量,声明式优先级高

<script>
  let gv = 1; // declarative ER
  globalThis.gv = 2; // object ER
  console.log(gv); // 1 (声明式优先级高)
  console.log(globalThis.gv); // 2
</script>
复制代码

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

用一个图来收个尾

作用域链

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

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

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 引擎追踪函数执行的一个机制。

function bar() {
}
function foo(fun){
  fun()
}
foo(bar)
复制代码

V8 准备执行这段代码时,先将全局执行上下文压入到调用栈。 V8 在主线程上执行 foo 函数,创建 foo 函数的执行上下文,并将其压入栈中。 V8 执行 bar 函数时,创建 bar 函数的执行上下文,并将其压入栈中。 bar 函数执行结束,V8 就会从栈中弹出 bar 函数的执行上下文。 foo 函数执行结束,V8 会将 foo 函数的执行上下文从栈中弹出。

堆栈溢出

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

function foo(){
  foo()
}
foo()
复制代码

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

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

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

function foo() {
  setTimeout(foo, 0)
}
foo()
复制代码

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

主线程会从消息队列中取出需要执行的宏任务。 V8 就要执行 foo 函数,并创建 foo 函数的执行上下文,将其压入栈中。 V8 执行执行 foo 函数中的 setTimeout 时,setTimeout 会将 foo 函数封装成一个新的宏任务,并将其添加到消息队列中。 foo 函数执行结束,V8 就会结束当前的宏任务,调用栈也会被清空。

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

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


相关文章
|
6月前
|
JavaScript 前端开发
作用域链的理解
作用域链的理解
56 0
|
13天前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。
|
1月前
|
JavaScript 前端开发
作用域和作用域链及预解析
作用域和作用域链及预解析
20 4
|
6月前
|
存储 程序员 C++
C++程序局部变量:生命周期与作用域的探讨
C++程序局部变量:生命周期与作用域的探讨
118 1
|
6月前
|
自然语言处理 JavaScript 前端开发
深入理解作用域、作用域链和闭包
在 JavaScript 中,作用域是指变量在代码中可访问的范围。理解 JavaScript 的作用域和作用域链对于编写高质量的代码至关重要。本文将详细介绍 JavaScript 中的词法作用域、作用域链和闭包的概念,并探讨它们在实际开发中的应用场景。
|
6月前
|
自然语言处理 JavaScript 前端开发
对作用域链的理解
对作用域链的理解
58 0
|
存储 JavaScript 前端开发
从执行上下文和作用域链理解闭包
从执行上下文和作用域链理解闭包
105 0
从执行上下文和作用域链理解闭包
|
设计模式 自然语言处理 JavaScript
一篇文章帮你真正理解javascsript作用域闭包
一篇文章帮你真正理解javascsript作用域闭包
85 0
|
存储 缓存 JavaScript
深入理解作用域和闭包(下)
深入理解作用域和闭包(下)
深入理解作用域和闭包(下)
|
存储 JavaScript 前端开发
深入理解作用域和闭包(上)
深入理解作用域和闭包(上)
深入理解作用域和闭包(上)