逻辑与交互 | 浏览器中的 JavaScript 执行机制

108 阅读6分钟

变量提升

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

变量提升带来的问题

  1. 变量容易被覆盖掉
  2. 本应销毁的变量没有被销毁,例如:
function foo(){
  for (var i = 0; i < 7; i++) {
  }
  console.log(i); 
}
foo()

在 for 循环跑完后,i 变量并没有被销毁

如何解决变量提升

利用块级作用域关键字 let 或者 const 关键字替换 var。JavaScript 中有三种作用域,全局作用域、函数作用域以及块级作用域。

  • 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期
  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的内容只能在函数内部被访问。函数执行结束后,函数内容就被销毁。
  • 块级作用域就是使用一对大括号包裹的一段代码,具体如下
//if块
if(1){}

//while块
while(1){}

//函数块
function foo(){}
 
//for循环块
for(let i = 0; i<100; i++){}

//单独一个块
{}

JavaScript 代码的执行流程

  1. 编译阶段
  • 将代码编译成两部分内容:
    • 执行上下文:JavaScript 执行一段代码时的运行环境,一般来说,有全局执行上下文、函数执行上下文、eval 函数执行上下文三种。执行上下文的内容有 this、方法内置变量、class 内置函数等。
    • 可执行代码
  1. 执行阶段:按照代码顺序与逻辑执行代码

每调用一个函数,JavaScript 引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后 JavaScript 引擎开始执行函数代码。那么什么是调用栈呢?

调用栈

调用栈是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈可以追踪到哪个函数正在被执行,以及各函数间的调用关系。

如果在一个函数 A 中调用了另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。

当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。

栈溢出(Stack Overflow)

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

栈溢出一般都是通过多次递归调用,或者函数间相互调用造成的。

如何解决栈溢出

修改逻辑降低递归次数,改写递归为循环等

作用域链

每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部执行上下文。当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量,当前的执行上下文查找不到变量再去外部执行上下文中查找。这个查找的链条就被称为作用域链。

注意,作用域链遵照词法作用域,词法作用域是指,作用域由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能预测代码在执行过程中如何查找标识符。

function foo() {
    console.log(a)
}
function bar() {
    let a = 1
    foo()
}
let a = 2
bar() // 2

上面的代码中 foo 是在全局作用域下声明的,不管在哪里执行 foo 函数,它的外部引用都是全局作用域。

闭包

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

如何回收闭包

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

如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。

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

this 指针

JavaScript 中的 this 是和执行上下文绑定的,每个执行上下文中都有一个 this,this还可以分为两种:

  • 全局执行上下文中的 this:指向 window 对象,不可修改。
  • 函数执行上下文中的 this:默认情况下指向 window 对象,但是可以通过以下三种方式来设置 this 值。
    1. 通过函数的 call、bind 和 apply 方法
    let bar = {
      myName : "极客邦",
      test1 : 1
    }
    function foo(){
      this.myName = "极客时间"
    }
    foo.call(bar)
    console.log(bar)
    console.log(myName)
    
    2.通过对象调用方法,使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的
    var myObj = {
      name : "极客时间", 
      showThis: function(){
        console.log(this)
      }
    }
    myObj.showThis()
    
    3.通过构造函数来设置
    function CreateObj(){
      this.name = "极客时间"
    }
    var myObj = new CreateObj()
    
    当执行 new CreateObj() 的时候,JavaScript 引擎做了如下四件事:
    1. 首先创建了一个空对象 tempObj;
    2. 接着调用 CreateObj.call 方法,并将 tempObj 作为 call 方法的参数,这样当 CreateObj 的执行上下文创建时,它的 this 就指向了 tempObj 对象;
    3. 然后执行 CreateObj 函数,此时的 CreateObj 函数执行上下文中的 this 指向了 tempObj 对象;
    4. 最后返回 tempObj 对象。

this 的设计缺陷以及应对办法

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

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

这段代码中 bar 的打印结果是 window,并没有继承外层函数设置的 myObj

可以使用 ES6 中的箭头函数来解决这个问题。

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