浏览器中的JavaScript执行机制

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

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

  2. JavaScript 代码的执行流程

    • 编译阶段
      输入一段代码,经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码,执行上下文是 JavaScript 执行一段代码时的运行环境。
    • 执行阶段
      JavaScript 引擎开始执行“可执行代码”,按照顺序一行一行地执行。
  3. 代码中出现相同的变量或者函数怎么办?
    一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数。

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

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

  1. 哪些情况下代码才算是“一段”代码,才会在执行之前就进行编译并创建执行上下文?

    • 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
    • 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
    • 当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。
  2. 什么是栈?
    栈就是类似于一端被堵住的单行线,车子类似于栈中的元素,栈中的元素满足后进先
    出的特点。

  3. 什么是 JavaScript 的调用栈?
    在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。
    如果在一个函数 A 中调用了另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。

  4. 栈溢出(Stack Overflow)
    调用栈是一种用来管理执行上下文的数据结构,符合后进先出的规则。调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出。

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

正是由于 JavaScript 存在变量提升这种特性,从而导致了很多与直觉不符的代码,这也是 JavaScript 的一个重要设计缺陷。

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

  2. 变量提升所带来的问题

    • 变量容易在不被察觉的情况下被覆盖掉
    • 本应销毁的变量没有被销毁
  3. ES6 是如何解决变量提升带来的缺陷
    引入了 let 和 const 关键字,作用块内声明的变量不影响块外面的变量。

  4. JavaScript 是如何支持块级作用域的
    块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。

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

  1. 作用域链
调用栈(call stack)

bar 函数和 foo 函数的 outer 都是指向全局上下文的,这也就意味着如果在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎会去全局执行上下文中查找。
我们把通过作用域查找变量的链条称为作用域链;作用域链是通过词法作用域来确定的,而词法作用域反映了代码的结构。

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

  2. 块级作用域中的变量查找

首先是在 bar 函数的执行上下文中查找,但因为 bar 函数的执行上下文中没有定义 test 变量,所以根据词法作用域的规则,下一步就在 bar 函数的外部作用域中查找,也就是全局作用域。

  1. 闭包(closure)
调用栈(call stack)

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

  1. 闭包使用原则
    如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

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

this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。
执行上下文主要分为三种——全局执行上下文、函数执行上下文和 eval 执行上下文,所以对应的 this 也只有这三种——全局执行上下文中的 this、函数中的 this 和 eval 中的 this。

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

  2. 函数执行上下文中的 this

    • 通过函数的 call、bind 和 apply 方法设置
      this 由参数决定。

    • 通过对象调用方法设置
      在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。
      通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。

    • 通过构造函数中设置
      首先 new 关键字会创建一个空的对象,然后会自动调用一个函数 apply 方法,将 this 指向这个空对象,这样的话函数内部的 this 就会被这个空的对象替代。
      (1)若无 return 语句,则默认返回 this 即构造函数的实例。
      (2)若有 return 语句,如果 return 了一个基本数据类型,则最终返回 this
      (3)若有 return 语句,如果 return 了一个对象,则最终返回这个对象

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

    • 嵌套函数中的 this 不会从外层函数中继承

      var myObj = {
          name: " 极客时间 ",
          showThis: function () {
              console.log(this) // myObj
              function bar() { console.log(this) }
              bar() // Window
          }
      }
      myObj.showThis()
      

      解决方案:
      第一种是把 this 保存为一个 self 变量,再利用变量的作用域机制传递给嵌套函数。
      第二种是继续使用 this,但是要把嵌套函数改为箭头函数,因为箭头函数没有自己的执行上下文,所以它会继承调用函数中的 this。

    • 普通函数中的 this 默认指向全局对象 window
      在严格模式下,默认执行一个函数,其函数的执行上下文中的 this 值是 undefined

你可能感兴趣的:(浏览器中的JavaScript执行机制)