浏览器中的js执行机制

一、JS代码执行流程

JS的执行机制:先编译,再执行。js代码在编译阶段,会创建执行上下文,变量和函数会被放到变量环境中,变量初始化为undefiend;在执行阶段,js引擎会从变量环境中查找自定义的变量和函数。

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

变量提升1.png

变量提升2.png

实际上变量和函数声明在代码里的位置是不会改变的,而是在编译阶段被js引擎放入内存中。(js代码先被js引擎编译,编译完成后进入执行阶段)。具体分析:
一、编译阶段
1、执行上下文:js执行一段代码时的运行环境,在执行上下文中存在一个变量环境的对象(Viriable Environment),该对象中保存了变量提升的内容。
示例:

showName()
console.log(myname)
var myname = '极客时间'
function showName() {
    console.log('函数showName被执行');
}
  • 第 1 行和第 2 行,这两行代码不是声明操作,所以 JavaScript 引擎不会做任何处理;
  • 第 3 行,由于这行是经过 var 声明的,因此 JavaScript 引擎将在环境对象中创建一个名为 myname 的属性,并使用 undefined 对其初始化
  • 第 4 行,JavaScript 引擎发现了一个通过 function 定义的函数,所以它将函数定义存储到堆 (HEAP)中,并在环境对象中创建一个 showName 的属性,然后将该属性值指向堆中函数的位置。
    这样就生成了变量环境对象。接下来 JavaScript 引擎会把声明以外的代码编译为字节码。
    二、执行阶段
    JavaScript 引擎开始执行“可执行代码”,按照顺序一行一行地执行。
js执行流程图.png
  • 注:如果存在同名的函数或者同名的变量,在编译阶段,前者会被后者覆盖。即,最终存储在变量环境中的是最后定义的那个。如果变量和函数同名,编译阶段,变量的声明会被忽略,变量环境中存储的是函数声明,而不论顺序(函数提升比变量提升优先级高)

代码编译时有三种情况会创建执行上下文

  • 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
  • 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
  • 当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。

调用栈:在执行上下文创建好后,js引擎会将执行上下文压入栈中。这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。
示例:

var a = 2
function add(b,c) {
  return b+c
}
function addAll(b,c) {
var d = 10
result = add(b,c)
return  a+result+d
}
addAll(3,6)
  • 第一步,创建全局上下文,并将其压入栈低


    全局执行上下文压栈.png

    全局执行上下文压入到调用栈后,JavaScript 引擎便开始执行全局代码了。首先会执行 a=2 的赋值操作,执行该语句会将全局上下文变量环境中 a 的值设置为 2。

  • 第二步,调用addAll函数。当调用该函数时,js引擎会编译该函数,并为其创建一个执行上下文,最后将该函数的执行上下文压入栈中


    执行addAll时的调用栈.png

    addAll 函数的执行上下文创建好之后,便进入了函数代码的执行阶段了,这里先执行的是 d=10 的赋值操作,执行语句会将 addAll 函数执行上下文中的 d 由 undefined 变成了 10。

  • 第三步,当执行到 add 函数调用语句时,同样会为其创建执行上下文,并将其压入调用栈


    执行add时的调用栈.png
  • 第四步,当add函数返回,该函数的执行上下文会从栈顶弹出,并将result的值设置为add的返回值


    add执行结束时的调用栈.png
  • addAll执行完,其执行上下文也会从栈顶弹出,此时只剩下全局上下文


    addAll执行结束时的调用栈.png

    整个js流程执行结束

二、js如何支持块级作用域

作用域:全局作用域、函数作用域、块级作用域
ES6通过let/const关键字可以实现块级作用域,在编译阶段,js引擎不会把块级作用域中的变量存放到变量环境中,而是存放到词法环境中,块级作用域是通过词法环境的栈结构来实现的。
示例:

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()
  • 第一步:编译并创建执行上下文
    foo执行上下文.png

    var声明的变量在变量环境
    let/const声明的变量在词法环境
    在作用域块内部,let/const声明的变量不在词法环境
  • 继续执行代码,并赋值,当进入作用域块中,let/const声明的变量会被存放在词法环境的一个单独区域中
    执行到块作用域时的上下文.png

    在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出。
    块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。
    注:
  • var的创建和初始化被提升,赋值不会被提升
  • let/const的创建被提升,初始化和赋值不会被提升
  • function的创建、初始化和赋值均被提升

三、作用域链和闭包

每个执行上下文的变量环境中都包含了一个外部引用outer,用来指向外部的执行上下文。
当一段代码使用一个变量时,js引擎会先在“当前的执行上下文”中查找,如果在当前的变量环境中没有找到,js引擎会继续在outer所指向的执行上下文中查找,这个查找的链条就称为作用域链作用域是由代码中函数声明的位置来决定的

变量查找顺序.png

闭包:在 JavaScript 中,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。
闭包还可以这样理解:当函数嵌套时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局作用域下可访问时,就形成了闭包。

四、this

this和作用域链属于两套不同的系统。
执行上下文中包含了变量环境、词法环境、外部环境outer,还有一个this


执行上下文.png

this是和上下文绑定的,每个执行上下文都有一个this。
执行上下文分文三种:全局执行上下文、函数执行上下文和eval执行上下文,所以this对应的也有三种。

  1. 全局执行上下文中的this:指向window对象
  2. 函数执行上下文中的this:要取决于函数是如何调用的
    • 函数作为普通函数调用,在严格模式下,this 值是 undefined,非严格模式下 this 指向的是全局对象 window;
    • 作为对象的方法调用,this指向该对象
    • 函数使用call、apply或bind方法调用,this由调用时传入的参数决定,指向传入的参数(call、apply和bind都是用于指定函数中的this值的方法)
    • 在构造函数中,this指向即将创建的新对象
    • 在事件处理函数中,this指向触发事件的元素。
    • 嵌套函数不会继承外层的this,也是根据函数时如何调用来决定的,但是箭头函数不会创建其自身的执行上下文,箭头函数中的this,指向外层非箭头函数的this
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容