No2.浏览器中的JavaScript执行机制

前段时间在《极客时间》上学了一个专栏,通篇略过,干货不少,但理解相当不够透彻,于是计划用几周的时间,对本专栏内容用作者的总结以及自己的相对逐字理解,来个通篇的文字记录学习,书读百遍,其义自现。
本篇是这个专栏的第二章:《浏览器中的JavaScript执行机制》。本章分为五节。

07|变量提升:JavaScript代码是按顺序执行的吗?


本节主要讲解执行上下文相关的内容。

通过一些代码的执行顺序与经验我们知道:

  • 在执行过程中,若使用了未声明的变量,那么 JavaScript 执行会报错。
  • 在一个变量定义之前使用它,不会出错,但是该变量的值会为 undefined,而不是定义时的值。
  • 在一个函数定义之前使用它,不会出错,且函数能正确执行。
变量提升

所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined.
之所以会发生变量提升,是因为一段JavaScript代码在执行之前,需要被JavaScript引擎编译,编译完成之后,才会进入执行阶段。也就是说在编译阶段,变量和函数的声明提升到了开头。

08 |调用栈:为什么JavaScript代码会出现栈溢出?


一般有三种情况,当一段代码执行的时候JS引擎对其进行编译并创建执行上下文:

  1. 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份.
  2. 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
  3. 当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。
小结
  • 每调用一个函数,JavaScript 引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后 JavaScript 引擎开始执行函数代码。
  • 如果在一个函数 A 中调用了另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。
  • 当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。
  • 当分配的调用栈空间被占满时,会引发“堆栈溢出”问题。

09 | 块级作用域:var缺陷以及为什么要引入let和const


作用域

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
ES6出现之前,JS的作用域只有两种:全局作用域函数作用域。 ES6出现,引入了块级作用域

在同一段代码中,ES6 是如何做到既要支持变量提升的特性,又要支持块级作用域的呢?

当一段代码里面既有var声明的变量也有let声明的变量的时候:

  • 函数内部通过var声明的变量,在编译阶段全都被存放到变量环境里面.
  • 通过let声明的变量,在编译阶段会被存放到词法环境中。
  • 在函数作用域内部,通过let声明的变量并没有被存放到词法环境中。
    也就是说:通过理解词法环境的结构和工作机制,块级作用域是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现的,通过两者的结合,JavaScript引擎也就同时支持了变量 提升和块级作用域了。

10 | 作用域和闭包:代码中出现相同的变量,JavaScript引擎是如何选择的


作用域链

理解作用域链是理解闭包的基础,而闭包在JavaScript中无处不在,同时作用域和作用域链还是作用语言的基础,所以我们先来学习一下作用域链
理解了调用栈、执行上下文、词法环境、变量环境等概念,那么你理解起来作用域链也会很容易,看下面一段代码:


function bar() {
   console.log(myName)
}
function foo() {
   var myName = "局部变量"
   bar()
}
var myName = "全局变量"
foo()

通过上面的代码,我们知道最终打印出来的结果是:”全局变量“。
这是因为,当一段代码使用了一个变量后,JavaScript引擎会首先在“当前的执行上下文”中去查找该变量。若没有找到,由于每个执行上下文都包含一个外部引用指向外部执行上下文,所以bar函数中的变量会去全局上下文中区域查找。我们把这个查找的链条就称为作用域链。

词法作用域

foo 函数调用的 bar 函数,那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?了解这个问题我们继续来学习词法作用域:
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
然后,根据词法作用域,foo 和 bar 的上级作用域都是全局作用域,所以如果 foo 或者 bar 函数使用了一个它们没有定义的变量,那么它们会到全局作用域去查找。也就是说,词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系。

闭包

在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。
在使用闭包的时候,要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

11 | this:从JavaScript执行上下文的视角讲清楚this


首先我们要知道,在对象内部的方法中使用对象内部的属性是一个非常普遍的需求,但是JavaScript作用域机制并不支持这一点,基于这个需求,JavaScript搞出了一套this机制。

在前几节中,我们提到执行上下文中包含了:变量环境词法环境外部环境、还有一个没有提及的this,this是和执行上下文绑定的,每个执行上下文都有一个this。
在08节我们总结了执行上下文主要分三种:全局执行上下文、函数执行上下文和eval执行上下文。
对应的this也只有这三种:全局执行上下文中的this、函数执行上下中的this和eval中的this(不做讨论)。

  • 全局执行上下文中的this:全局执行上下文中的this指向window对象。
  • 函数执行上下文中的this:
    1. 默认情况下调用一个函数,其执行上下文中的 this 也是指向 window 对象的.
    2. 通过函数的call方法设置其this指向其他对象(还可以使用bind和apply方法来设置函数执行上下文中的this)。
    3. 通过对象调用方法设置。(使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的。在全局环境中调用一个函数,函数内部的this指向的是全局变量window)。
    4. 通过构造函数中设置。
this的设计缺陷以及应对方案
  1. 嵌套函数的this不会从外层函数中继承。==> 1⃣️、将this保存一个self变量,利用变量作用域机制传递给嵌套函数。2⃣️、将乔套函数改为箭头函数。
  2. 普通函数中的this默认指向全局对象window。==>可以通过设置JavaScript的“严格模式”来解决。
liugezhou wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
Enjoy Yourself EveryDay!