摘要:极客时间《浏览器工作原理与实践》浏览器中的 JavaScript 执行机制学习笔记,变量提升、调用栈、块级作用域、作用域链和闭包、this
变量提升
变量提升是指在 JavaScript 代码编译阶段,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值 undefined。变量和函数声明在代码里的位置是不会改变的,但在编译阶段被 JavaScript 引擎放入内存中,编译完成之后,才会进入执行阶段
编译阶段
1 | showName() |
经过编译阶段后生成两部分代码:
1、提升部分:函数的声明部分和变量的声明部分
1 | // 把函数 showName 提升到开头 |
2、可执行代码部分
1 | showName() |
函数和变量在执行之前都提升到了代码开头
实际上,一段代码经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码。执行上下文是 JavaScript 执行一段代码时的运行环境,执行上下文中的变量环境对象保存了提升的内容,this 的指向也保存在变量环境中
如果代码中有同名的函数,后面的函数会覆盖前面的函数,如果有与函数同名的变量,变量会被忽略
执行阶段
编译完后就会按照“可执行代码”的顺序一行一行的执行。变量的赋值是在执行时进行的
调用栈
用来管理执行上下文的栈称为执行上下文栈,又称调用栈,它是 JavaScript 引擎追踪函数执行的一个机制。在浏览器控制台打断点,点开 Call Stack 即可查看调用栈情况
一般代码的执行逻辑:
1、创建全局上下文,并将其压入栈底;
2、调用函数时创建函数执行执行上下文,并压入栈
3、函数执行完,该函数的执行上下文出栈
调用栈是有大小的,如果递归时没有退出,会造成栈溢出
块级作用域
作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
ES6 之前只有全局作用域和函数作用域:
1、全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期,在控制台 Sources 面板 Scope 信息中以 Global 显示
2、函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。在控制台 Scope 信息中以 Local 显示
ES6 开始支持块级作用域,要产生块级作用域必须有大括号,再配合使用 let 和 const 关键字可以避免变量提升特性带来的问题。块级作用域在控制台 Scope 中以 Block 显示
从执行上下文的角度来理解,第一步在函数编译阶段创建执行上下文时:
1、函数内部通过 var 声明的变量,无论是否在块级作用域内,在编译阶段全都被存放到变量环境里面,创建和初始化(赋值 undefined)被提升
2、函数内部非块级作用域,通过 let、const 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中,创建和初始化(赋值 undefined)被提升,但在声明位置之前会形成一个暂时性死区(temporal dead zone,简称 TDZ)不能被访问。在函数中的块级作用域内部,通过 let、const 声明的变量暂时没有操作
3、函数内部非块级作用域声明的函数,在编译阶段会被存放到变量环境里面,整体被提升。函数内部块级作用域声明的函数会像 var 声明的变量类似,仅名字会提升到变量环境里并初始化为 undefined,且与非块级作用域的函数重名时会被忽略(浏览器实现)。
第二部开始执行代码,变量开始赋值。在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部通过 let 或者 const 声明的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出。块级作用域中声明的函数会在块级作用域内部提升,当块级作用域从栈顶弹出后,该函数声明会再提升到变量环境里(再次提升相当于一次赋值操作)。
变量查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。
1 | function foo() { |
可以将注释掉的代码都打开,然后查看 Scope 中通过 var、let、const 和函数声明的值的变化
作用域链和闭包
作用域链
在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,这个外部引用被称为 outer。当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量。如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找。这个查找的链条就被称为作用域链,而作用域链是由词法作用域决定的。词法作用域即静态作用域,其在代码编译阶段就已经确定,由代码中函数声明的位置来决定,和函数是怎么调用的没有关系
1 | function fna() { // 作用域由函数声明的位置决定 |
闭包
根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数作用域引用外部函数作用域的变量依然保存在内存中,这些变量的集合称就被为闭包。
1 | function fna() { |
JavaScript 引擎会沿着“当前执行上下文–>fna 函数闭包–>全局执行上下文”的顺序来查找 test 变量。在 getTest 函数内打个断点,在控制台可以看到 Scope 调用栈从上到下(栈顶到栈底)依次是 Local -> Closure (fna) -> Global
如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。为了避免内存泄漏,在使用闭包的时候,要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。
this
请查看this