[浏览器工作原理]--JavaScript的执行机制

502 阅读5分钟

先编译-后执行

JavaScript 代码在执行之前需要被 JavaScript 引擎编译,并将代码中的函数和var变量保存到执行上下文的变量环境中,let变量保存到执行上下文的词法环境中,生成执行上下文可执行代码之后,执行代码时就会到当前的执行上下文查找,如果没找到则到外部的执行上下文中查找,直到在全局执行上下文中没找到则报错。

什么情况下需要编译(创建执行上下文)?

  1. 执行全局代码时,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份;

  2. 调用一个函数时,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁;

  3. 当使用 eval 函数时,eval 的代码也会被编译,并创建执行上下文;

也就是说以上,编译时变量环境和词法环境最顶层数据已经确定了,而词法环境中,进入或者退出一个块级作用域,里面的数据都会改变(进入时追加,块执行完销毁)

var和let变量创建执行上下文有什么区别?

由于 var声明的变量的作用范围是全局的,在整个函数有效,变量保存在执行上下文的变量环境

let声明的变量是块级作用域,不影响块外面的变量,在编译阶段会被存放到词法环境 wps1.jpg

在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个块作用

域后,就会把该块作用域内部的变量压到栈顶,直到该块作用域执行完毕,则弹出词法环境中该块作用域的信息,这就是词法环境的结构

注意:进入块内后,let声明的变量虽然压倒了栈顶,由于暂时性死区,没有赋值前使用它就会报错。

如何查找变量?

首先会在当前执行上下文的词法环境查找,沿着栈顶向下查询,如果在词法环境中的某个块中找到了,就直接返回给 JavaScript 引擎,没找到,那么继续在当前执行上下文的变量环境中查找,如果没找到,就往外部执行上下文的词法环境查找,依次向外直到全局执行上下文。

如何管理多个执行上下文?

而在执行 JavaScript 时,可能会存在多个执行上下文,需要通过调用栈来管理执行上下文

首先,创建全局上下文,并将其压入栈底

调用函数时,JavaScript 引擎会编译该函数,并为其创建一个执行上下文,并压入栈中,之后便进入了函数代码的执行阶段,当函数执行结束时,该函数的执行上下文就会从栈顶弹出,此时调用栈中就只剩下全局上下文

wps2.jpg

需要说明的是:调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引

擎就会报错,我们把这种错误叫做栈溢出

想要查看调用栈,可以打开“开发者工具”,点击“Source”标签,选择 JavaScript 代码的页面,然后加上断点,并刷新页面,右边的“call stack”下面显示出来了函数的调用关系;

还可以使用 console.trace() 来输出当前的函数调用关系

如何找到外部执行上下文?

**变量是通过作用域链查找,**在每个执行上下文的变量环境中有一个外部引用outer,它指向其外部的执行上下文,在当前执行上下文无法找到该变量时,可以通过outer到外部执行上下文的词法环境和变量环境查找,直到在全局执行上下文中,其outer为NULL,结束查找

其中,确定外部执行上下文需要理解词法作用域,它是指作用域是由函数声明位置决定,和函数调用无关,所以词法作用域是静态的作用域,能够预测代执行过程中如何查找变量

灵魂思考

题目:变量提升

var myname = " 全局的 " 
function showName(){ 
    console.log(myname); 
    if(0){ 
        var myname = " 内部的 " 
        } 
    console.log(myname); 
} 
showName() 
console.log(myname); 
//undefined
//undefined
//全局的

因为在调用一个函数时,函数内部var声明的变量都会被编译,生成当前函数的执行上下文并保存到变量环境中,和if条件(while或者for)是否符合无关,因此全局下的myname是在全局执行上下文的变量环境中,值还是" 全局的 ",函数内部代码执行时会到函数的词法环境找,找不到再到变量环境找,结果就是编译时产生的undefined

wps3.jpg

题目:全局作用域中的变量查找

function bar() {
    console.log(myName)
}
function foo() {
    var myName = " 666 "
    bar()
}
var myName = " 888 "
foo()
//888

执行 bar 函数时的调用栈:

wps4.jpg

题目:函数作用域中的变量查找

var bar = {
    myName:"那是bar",
    printName: function () {
        console.log(myName)
    } 
}
function foo() {
    let myName = " 那是foo "
    return bar.printName
}
let myName = " 那是全局 "
let _printName = foo()
_printName()
bar.printName()

形成闭包的条件:有函数/作用域的嵌套;内部函数引用外部函数的变量/参数。

上面代码打印结果都是“那是全局”,foo()实际返回的是bar对象中的printName函数,以上代码相当于调用了2次bar.printName(),又因为{}不代表作用域,在es6语法中,{}会被看做代码块,相当于printName是全局作用域下的,打印的全局下的myName ,没有形成闭包

题目:块级作用域中的变量查找

function bar() {
    var myName = "那是bar"
    let test1 = 100
    if (1) {
        let myName = "bar里面if"
        console.log(test)
    }
}
function foo() {
    var myName = "那是foo"
    let test = 2
    {
        let test = 3
        bar()
    }
}
var myName = "全局的"
let myAge = 10
let test = 1
foo()
//1

调用栈结构如下,查找过程如下面标注1,2,3,4

wps5.jpg