【浏览器工作原理】4. 浏览器中的JavaScript是如何执行机制的

223 阅读3分钟

引言

JavaScript代码是按照顺序执行的吗?为什么有些代码不是按照预期执行的?本来将会详细说明浏览器是如何运行JavaScript的。

变量提升

先来看个例子,先想下会输出什么?

showName()
console.log(myname)
var myname = 'hello world!'
function showName() {
    console.log('函数showName被执行');
}

输出:

函数showName被执行和undefined

如果不了解JavaScript的执行机制中的变量提升,一定不清楚为什么会这样。

为什么要提升?

因为JavaScript代码在执行之前需要先编译,在编译阶段变量和函数会被存到变量环境中。编译阶段为什么这么处理,其实跟JavaScript作用域有关系。

在es6之前作用域有全局作用域和函数作用域,为了让整个函数体内部的任何地方都可以被访问到,就在编译阶段提取到执行上下文的变量环境中,这么设计是比较简单的方式。

什么是变量提升?

变量提升是指javascript引擎把变量的声明部分(不包括赋值)和函数的声明部分提升到代码开头的行为。

变量提升规则是什么?

变量提升后,会给变量设置默认值undefined。

如果存在两个相同的函数或者变量和函数同名,如何执行?看个例子

showName()
function showName() {
    console.log(1)
}
function showName() {
    console.log(3)
}
var showName = function() {
    console.log(2)
}

答案是打印3,结论如下:

  • 如果是同名的函数,JavaScript编译阶段会选择最后声明的那个,因此是输出3的函数。
  • 如果变量和函数同名,那么在编译阶段,变量的声明会被忽略,变量是后定义的并没有覆盖函数。

变量提升缺点?

由于变量提升,可能会导致非预期的结果,例如被覆盖或者没有及时销毁。

如何解决变量提升带来的问题?

es6引入了let和const关键字,让JavaScript有了块级作用域。JavaScript引擎是如何同时支持变量提升和块级作用域的?看个例子:

function foo(){
    var a = 1
    let b = 2
    {
      let b = 3
      var c = 4
      let d = 5
      console.log(a)
      console.log(b)
    }
    console.log(b) 
    console.log(c)
    console.log(d)
}   
foo()

我们已经知道JavaScript引擎是通过变量环境实现函数作用域的,对于块级作用域是通过词法环境的栈结构来实现的。参考如下图: fbc328b01fe48e346dd5d37e2131d27b.png 遇到变量具体查找过程:沿着词法环境的栈顶向下查询,如果找的就返回给JavaScript引擎,没有找到继续在变量环境中查找。如下图所示:

f8e688894c95bab004ff77d8a2fb5149.png

扩展

看看下面代码输出什么?

let myname= 'hello world'
{
  console.log(myname) 
  let myname= 'hi world'
}

输出:Uncaught ReferenceError: Cannot access 'myname' before initialization

词法变量在初始化之前被访问,该错误可以发生于任何语句块中,当使用 let 或 const 修饰的变量在初始化之前被访问的时候。

通过这个题目来说明let和const要在声明之后才能使用,很多人称这个为“暂时性死区”。

总结

通过本文我们知道了变量提升相关的知识,也明白了变量环境和词法环境的作用,再也不怕类似题目了。