0%

浏览器工作原理学习笔记(四)-浏览器中的JS执行机制

本系列为极客时间上的《浏览器工作原理与实践》专栏的学习笔记

变量提升

JS 引擎在执行一段 JS 代码的时候,流程如下:
Markdown

在编译阶段,主要做了以下这些事情:

  • 产生可执行代码和执行上下文
  • 执行上下文包含:变量环境和词法环境,变量环境里面保存着该段代码需要的变量和函数
  • 遇到声明语句,将对应的变量或函数保存在执行上下文的变量环境中,并赋予变量默认值,函数的值为指针,指向堆中保存函数体的地址
  • 将可执行代码编译成字节码

编译阶段在遇到同名变量或函数的时候,后者会覆盖前者。
执行阶段就是按顺序,一行一行执行可执行代码。

调用栈

调用栈是 JS 引擎用来管理执行上下文的数据结构。
在一个页面中,会存在一个全局上下文和若干个函数执行上下文。在执行代码的过程中,遇到函数调用,会先对函数代码进行编译,形成函数执行上下文,并将其压入栈中。待函数执行完成之后,会有一个出栈的操作,用来销毁掉当前的函数执行上下文。

栈溢出

调用栈是有容量大小的,当入栈的执行上下文超过一定数量时,JS 引擎就会报错,这种错误就是栈溢出。在一些函数递归调用的场景中,如果没有设置合理的退出条件,就会发生栈溢出的错误。可以通过使用加入定时器的方法,来把当前任务拆分成许多小任务,来解决这种问题。

块级作用域

在 ES6 之前,JS 的作用域只有两种:全局作用域和函数作用域。由于没有块级作用域,JS 引擎在编译代码的时候,会执行变量提升,由此带来的问题是: - 变量覆盖 - 变量没有被及时销毁
ES6 通过引入 let 和 const 关键字,支持了块级作用域,同时还向下兼容着变量提升的特性。在编译阶段,let 和 const 关键字声明的变量会被存放在执行上下文中的词法环境里,而且块与块之间互不影响(是一个栈的结构)。
值得注意的是,在块级作用域中,在 let 或 const 声明语句之前去使用它们定义的变量,会报错,因为这些变量即使存放在词法环境里,但是并没有被初始化,也就形成了所谓的”暂时性死区“。所以一个比较好的习惯时,如果逻辑不是很复杂,建议将变量声明都放在最前面。

作用域链和闭包

JS 引擎在执行过程中,为了查找某个变量的值,是依照作用域链来查找的。作用域链的形成由词法作用域所决定,后者是由代码的书写结构所决定的,也就是说,词法作用域是静态的作用域。
在每个执行上下文的变量环境中,都存在一个指向外部执行上下文的引用,称为 outer。在查找变量的过程中,outer 起到了路径引导作用。
示例代码:

1
2
3
4
5
6
7
8
9
function bar() {
console.log(myName)
}
function foo() {
var myName = "极客邦"
bar()
}
var myName = "极客时间"
foo()

执行 bar 函数时的调用栈:
Markdown

闭包

闭包的形成:一个函数最终返回了一个内部函数,并且该内部函数引用到了外部函数里面的变量。即使外部函数执行完之后被销毁了,但是被内部函数引用到的变量保存下来了,存在的结构就称为“闭包”,可以理解为是内部函数的“专属背包”。
在之后的内部函数执行过程中,查找变量的作用链中就会包含闭包这个节点。

JS 中的 this

首先需要明确的是 this 保存在执行上下文中。
JS 中存在三种执行上下文:全局执行上下文、函数执行上下文、eval 执行上下文。eval 不经常用,可以忽略,所以只需要关注在全局和函数执行上下文中,this 指向何处就好了。

  1. 全局执行上下文的 this 指向 window (浏览器环境,非严格模式)。严格模式下指向的是 undefined。
  2. 函数执行上下文的 this 指向
    • 默认指向,指向 window (非严格模式),严格模式下指向 undefined
    • 作为对象的方法被执行,指向该对象
    • 作为构造函数被执行,指向新 new 出来的对象
    • 使用 call/apply/bind 方法时,指向指定的参数

this 的设计缺陷以及应对方案

  1. 嵌套函数中的 this 不会从外部继承

    解决方案:
    将外部函数的 this 传入给内部函数;
    使用箭头函数;

  2. 普通函数中的 this 默认指向全局对象

    解决方案:
    通过 call/apply/bind 函数调用,避免打破数据的边界;
    设置严格模式,将 bug 的出现时机前置;