深入理解JS | 青训营笔记

87 阅读4分钟

image.png

JS的基本概念

Q:为什么说GUI线程和JS线程是互斥的?

A:JS线程会改变页面中的元素属性(如位置、背景),此时如果GUI线程正在同时渲染页面,那JS线程会把DOM进行修改导致GUI线程的渲染并不是最新的。因此,如果JS代码中有很大的计算任务(如循环),则会导致页面卡顿。

Q:什么是静态作用域和动态作用域?它们的区别是什么?

A:可以通过一个生动形象的例子来介绍它们:

假设我们有一个人名为小明,他有一个朋友名为小张,还有一个朋友名为小李。

在静态作用域下,小明和小张都住在同一栋楼房里面,而小李住在另一栋楼房里面。假设小明给小张打电话,询问他现在住在哪里。小张回答说:“我在我自己家里”。由于小张和小明住在同一栋楼房里,所以小明可以根据静态作用域的规则得出结论,即小张现在在和小明住在同一栋楼房里。

在动态作用域下,小明和小张都在同一栋楼房里面,而小李不在这栋楼房里面。假设小明给小张打电话,询问他现在住在哪里。小张回答说:“我在小李家里”。由于动态作用域的规则是根据当前的执行上下文动态生成作用域链,所以小明可以得出结论,即小张现在在小李的家里,尽管小张的作用域在函数定义时并不包括小李的作用域。

换句话说,静态作用域是在函数定义时就已经确定的,而动态作用域是在函数调用时根据执行上下文动态生成的。

Q:老师在一上课就提出了这个问题:JS真的是单纯解释性语言嘛,解释一行执行一行吗?

A:并不是,变量提升规则就是最好的证据,如果JS真的是读一行解释一行,怎么可能会有变量提升出现呢。因此JS的执行其实也是经历了编译的过程的。下面举一个关于变量提升的例子:

先看这段代码

func()
const func = showname;

function showname(){
  console.log("wlc")
}

image.png 报错的原因很简单,此段代码根据老师上课所讲的变量提升原理等价于下面这段代码:

var func = undefined;

func()

func = showname;

function showname(){
  console.log("wlc")
}

自然,无法正常调用函数。

附上几条变量提升的性质:

image.png

JS是怎么执行的

Q:为什么AST不能直接转化为机器码,而是要先经过转化为字节码再到机器码呢?

A:根据老师上课所举例子,高级语言中的1+1在汇编语言中会变成多行代码。如果AST直接转化为机器码,必定会占据很大的内存,因此V8在执行解释JS时会先转换为字节码从而节省内存开销,再在随后逐行解释执行生成机器码。此外,字节码也会通过编译执行的方式进行代码优化,比如一段代码出现过两次及以上,V8便会认为其为热代码,将其直接转化为机器码存储起来,下次遇到便直接当机器码执行即可,极大的提升了速度,这个过程便是JIT。

image.png

Q:如何理解执行上下文?

A:先看这个上课的例子,我将分过程解释执行上下文的步骤:

image.png 在这个例子中,JS的执行过程为:

  1. 先压入全局执行上下文
  2. 随后依次压入testCompany1和testCompany2两个函数执行上下文
  3. testCompany2会利用Outer指针沿着作用域链向外层找直至在testCompany1上下文中找到company
  4. 函数执行完毕,ESP指针向栈底下移

JS进阶

Q:怎么理解闭包?

A:闭包是指一个函数在运行时可以访问到其定义时的作用域。在 JavaScript 中,每个函数都会形成一个执行上下文,并在创建时创建一个作用域链。作用域链包含了该函数定义时所在的作用域以及所有外层作用域的变量和函数。当一个函数内部定义了一个函数并返回它时,内部函数会包含对外部函数所有变量和函数的引用。这些引用被保存在闭包中的引用环境中,因此即使外部函数执行完毕并退出了执行上下文栈,这些引用仍然可以被内部函数访问。

image.png 在这个例子中,showName已从栈中弹出,company变量会被标记为可回收,然而由于闭包内部的函数function引用了外部函数showName的变量dep和name,这两个变量会伴随着getName结束才会回收。