浏览器中的JS执行机制

107 阅读7分钟

浏览器原理与实践系列文章内容来自极客时间李兵老师《浏览器工作原理与实践》,主要是记录自己的学习过程,基于自己的理解对内容做的一些总结,包括《宏观视角下的浏览器》《浏览器中的JS执行机制》《V8引擎工作原理》《事件循环系统》《浏览器中的页面》《网络协议》《浏览器安全》共七篇,此为第二篇

变量提升

所谓的变量提升,是指在JavaScript代码执行过程中,JavaScript引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,即undefined。

JS代码执行流程:先编译、再执行

  • JavaScript代码执行过程中,需要先做变量提升,而之所以需要实现变量提升,是因为JavaScript代码在执行之前需要先编译
  • 编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为undefined;在代码执行阶段,JavaScript引擎会从变量环境中去查找自定义的变量和函数。
  • 如果在编译阶段,存在两个相同的函数,那么最终存放在变量环境中的是最后定义的那个,这是因为后定义的会覆盖掉之前定义的。

调用栈

  • 每调用一个函数,JavaScript引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后JavaScript引擎开始执行函数代码。
  • 如果在一个函数A中调用了另外一个函数B,那么JavaScript引擎会为B函数创建执行上下文,并将B函数的执行上下文压入栈顶。
  • 当前函数执行完毕后,JavaScript引擎会将该函数的执行上下文弹出栈。
  • 当分配的调用栈空间被占满时,会引发“堆栈溢出”问题。

块级作用域

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

作用域有三种:

  • 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
  • 块级作用域就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。作用域块内声明的变量不影响块外面的变量

由于JavaScript的变量提升存在着变量覆盖、变量污染等设计缺陷,所以ES6引入了块级作用域关键字,即let 和 const。

作用域链和闭包

作用域链:

每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,把这个外部引用称为outer。当一段代码使用了一个变量时,JavaScript引擎首先会在“当前的执行上下文”中查找该变量,如果在当前的变量环境中没有查找到,那么JavaScript引擎会继续在outer所指向的执行上下文中查找

function bar() {
    console.log(myName)
}
function foo() {
    var myName = "极客邦"
    bar()
}
var myName = "极客时间"
foo()

image.jpeg转存失败,建议直接上传图片文件

bar函数和foo函数的outer都是指向全局上下文的,这也就意味着如果在bar函数或者foo函数中使用了外部变量,那么JavaScript引擎会去全局执行上下文中查找。这个查找的链条就称为作用域链。

词法作用域:指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。是代码编译阶段就决定好的,和函数是怎么调用的没有关系

闭包

在JavaScript中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,把这些变量的集合称为闭包。比如外部函数是foo,那么这些变量的集合就称为foo函数的闭包。

如果闭包使用不正确,会很容易造成内存泄漏的。通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。

如果引用闭包的函数是个局部变量,等函数销毁后,在下次JavaScript引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么JavaScript引擎的垃圾回收器就会回收这块内存。所以在使用闭包时尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,就尽量让它成为一个局部变量。

浏览器是如何识别并实现闭包的:

  1. JS引擎执行到一个函数时,首先会编译,创建一个空的执行上下文
  2. 在编译过程中,如果遇到函数,JS引擎还要对遇到的内部函数进行一次快速的词法扫描,如果发现内部函数中引用了该函数中的变量,由于内部函数引用了外部函数的变量,所以JS引擎判断这是一个闭包,于是在堆空间创建一个【closure】的对象(这是一个内部变量,JS是无法访问的),用来保存引用的变量。
  3. 接着继续扫描当前主函数,如果扫描到还有内部函数引用外部函数变量,继续添加到已创建的【closure】对象中。
  4. 当主函数执行结束后,由于主函数返回的内部函数引用了【closure】对象,所以即使主函数退出了,【closure】依然被其内部的方法引用着,在下次调用该主函数返回的内部方法时,创建的执行上下文中就包含了【closure】对象

this

this是和执行上下文绑定的,也就是说每个执行上下文中都有一个this。

全局执行上下文中的this: 全局执行上下文中的this是指向window对象的。这也是this和作用域链的唯一交点,作用域链的最底端包含了window对象,全局执行上下文中的this也是指向window对象。

函数执行上下文中的this

  • 使用对象来调用其内部的一个方法,该方法的this是指向对象本身的。
  • 在全局环境中调用一个函数,函数内部的this指向的是全局变量window。
  • 嵌套函数中的this不会从外层函数中继承