本系列为极客时间上的《浏览器工作原理与实践》专栏的学习笔记
变量提升
JS 引擎在执行一段 JS 代码的时候,流程如下:
在编译阶段,主要做了以下这些事情:
- 产生可执行代码和执行上下文
- 执行上下文包含:变量环境和词法环境,变量环境里面保存着该段代码需要的变量和函数
- 遇到声明语句,将对应的变量或函数保存在执行上下文的变量环境中,并赋予变量默认值,函数的值为指针,指向堆中保存函数体的地址
- 将可执行代码编译成字节码
编译阶段在遇到同名变量或函数的时候,后者会覆盖前者。
执行阶段就是按顺序,一行一行执行可执行代码。
调用栈
调用栈是 JS 引擎用来管理执行上下文的数据结构。
在一个页面中,会存在一个全局上下文和若干个函数执行上下文。在执行代码的过程中,遇到函数调用,会先对函数代码进行编译,形成函数执行上下文,并将其压入栈中。待函数执行完成之后,会有一个出栈的操作,用来销毁掉当前的函数执行上下文。
栈溢出
调用栈是有容量大小的,当入栈的执行上下文超过一定数量时,JS 引擎就会报错,这种错误就是栈溢出。在一些函数递归调用的场景中,如果没有设置合理的退出条件,就会发生栈溢出的错误。可以通过使用加入定时器的方法,来把当前任务拆分成许多小任务,来解决这种问题。
块级作用域
在 ES6 之前,JS 的作用域只有两种:全局作用域和函数作用域。由于没有块级作用域,JS 引擎在编译代码的时候,会执行变量提升,由此带来的问题是: - 变量覆盖 - 变量没有被及时销毁
ES6 通过引入 let 和 const 关键字,支持了块级作用域,同时还向下兼容着变量提升的特性。在编译阶段,let 和 const 关键字声明的变量会被存放在执行上下文中的词法环境里,而且块与块之间互不影响(是一个栈的结构)。
值得注意的是,在块级作用域中,在 let 或 const 声明语句之前去使用它们定义的变量,会报错,因为这些变量即使存放在词法环境里,但是并没有被初始化,也就形成了所谓的”暂时性死区“。所以一个比较好的习惯时,如果逻辑不是很复杂,建议将变量声明都放在最前面。
作用域链和闭包
JS 引擎在执行过程中,为了查找某个变量的值,是依照作用域链来查找的。作用域链的形成由词法作用域所决定,后者是由代码的书写结构所决定的,也就是说,词法作用域是静态的作用域。
在每个执行上下文的变量环境中,都存在一个指向外部执行上下文的引用,称为 outer。在查找变量的过程中,outer 起到了路径引导作用。
示例代码:
1 | function bar() { |
执行 bar 函数时的调用栈:
闭包
闭包的形成:一个函数最终返回了一个内部函数,并且该内部函数引用到了外部函数里面的变量。即使外部函数执行完之后被销毁了,但是被内部函数引用到的变量保存下来了,存在的结构就称为“闭包”,可以理解为是内部函数的“专属背包”。
在之后的内部函数执行过程中,查找变量的作用链中就会包含闭包这个节点。
JS 中的 this
首先需要明确的是 this 保存在执行上下文中。
JS 中存在三种执行上下文:全局执行上下文、函数执行上下文、eval 执行上下文。eval 不经常用,可以忽略,所以只需要关注在全局和函数执行上下文中,this 指向何处就好了。
- 全局执行上下文的 this 指向 window (浏览器环境,非严格模式)。严格模式下指向的是 undefined。
- 函数执行上下文的 this 指向
- 默认指向,指向 window (非严格模式),严格模式下指向 undefined
- 作为对象的方法被执行,指向该对象
- 作为构造函数被执行,指向新 new 出来的对象
- 使用 call/apply/bind 方法时,指向指定的参数
this 的设计缺陷以及应对方案
- 嵌套函数中的 this 不会从外部继承
解决方案:
将外部函数的 this 传入给内部函数;
使用箭头函数; - 普通函数中的 this 默认指向全局对象
解决方案:
通过 call/apply/bind 函数调用,避免打破数据的边界;
设置严格模式,将 bug 的出现时机前置;