JavaScript执行上下文是如何在函数调用、闭包和异步操作中形成和变化的?

2026-03-31 15:131阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计3187个文字,预计阅读时间需要13分钟。

JavaScript执行上下文是如何在函数调用、闭包和异步操作中形成和变化的?

目录简介JavaScript代码执行过程编译阶段变量提升与let和const作用域单个执行上下文中变量的查找规则调用栈作用域链词法作用域闭包闭包的回收上下文角度讲this简介执行

目录
  • 简介
  • javascript代码的执行过程
    • 编译阶段
  • var变量提升与let和const
    • 作用域
      • 单个执行上下文中变量的查找规则
        • 调用栈
          • 作用域链
            • 词法作用域
              • 闭包
                • 闭包的回收
                  • 从上下文角度讲this

                    简介

                    执行上下文可以说是js代码执行的一个环境,存放了代码执行所需的变量,变量查找的作用域链规则以及this指向等。同时,它也是js很底层的东西,很多的问题如变量提升、作用域链和闭包等都可以在执行上下文中找到答案,所以这也是我们学习执行上下文的原因

                    执行上下文分为三种:

                    • 全局执行上下文:当进入全局代码时会进行编译,在编译中创建全局执行上下文,并生成可执行代码
                    • 函数执行上下文:执行代码的过程中,如果遇到函数调用,会编译函数内的代码和创建函数执行上下文,并创建可执行代码
                    • eval执行上下文:当使用eval函数的时候,eval的代码也会被编译,并创建执行上下文

                    因为执行上下文是在编译阶段创建的,所以接下来先看一下js代码的执行过程吧

                    javascript代码的执行过程

                    一段js代码的执行过程中,先是会进行编译阶段,js引擎会将代码进行编译,再进入执行阶段

                    也就是说,js代码是按照“段”来执行的,具体就是全局代码就是一段代码,函数执行也算一段代码,编译也是按照“段”来编译的,也就是一整个js代码会出现多个编译阶段

                    编译阶段

                    编译阶段是一个很复杂的过程,这里只是简单的介绍:

                    1、编译阶段完成两件事情:创建执行上下文和生成可执行代码

                    2、执行上下文就包括变量环境和词法环境和this指向等,创建执行上下文的过程:

                    • 如果是普通变量的话,js引擎会将该变量添加到变量环境中并初始化为undefined
                    • 如果是函数声明的话,js引擎会将函数定义添加到变量环境中,然后将函数名执行该函数的位置(内存)

                    3、接着,js引擎就会把其他的代码编译为字节码,生成可执行代码

                    编译阶段完成后,js引擎开始执行可执行代码,按照顺序一行一行执行,当遇到函数或者变量时,会在变量环境中寻找,找不到的话就会报错

                    如果遇到赋值语句时,就会将值赋值给变量

                    var变量提升与let和const

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

                    变量提升的实现并不是物理地移动代码的位置,而是在编译阶段被js引擎放入内存中。

                    1、普通变量提升会赋值为undefined,函数变量名会将整个函数提升

                    console.log(fn); // [Function: fn] console.log(a); // undefined function fn() { console.log(111); } var a = 1

                    2、函数表达式只会将变量提升,不会将函数题提升

                    3、当有多个相同类型的声明(同样是函数声明或者同样是普通变量声明),最后一个声明会覆盖之前的声明

                    4、当函数声明与变量声明同时出现时,函数声明优先级更高

                    当然,变量提升有很多的缺陷,所以从es6开始引入了let和const关键字,通过let和const声明的变量不具有变量提升特性,同时也支持块级作用域,先看一下作用域吧

                    作用域

                    作用域其实就是一套定义函数调用和变量使用的规则,其中,就有三种作用域:

                    • 全局作用域:其中对象在代码的任何地方都能访问,其生命周期伴随着页面的生命周期
                    • 函数作用域:在函数内部定义的变量和函数,只能在内部访问,外部不能访问到,函数执行结束后,函数内部定义的变量会被销毁(函数不会吗???)
                    • 块级作用域:由代码块包含的代码会形成一个块级作用域(es6之前没有),跟函数作用域类似

                    通过var声明的变量没有块级作用域,通过const let声明的变量有块级作用域

                    那js是如何var的变量提升和支持块级作用域的呢?这就得从执行上下文的角度说起

                    编译阶段生成执行上下文:

                    假设js需要执行一个函数

                    • 首先,编译创建该函数的执行上下文,创建可执行代码
                    • 在编译阶段,所有通过var声明的变量(包括代码块里面的变量)都会被创建并存放在变量环境中,并初始化为undefined
                    • 通过let或者const声明的变量(不包括代码块码里面的变量)都会被创建并存放在词法环境中,设置为未初始化
                    • 至此,编译阶段结束了,开始执行代码
                    • 执行代码过程中遇到代码块时,会先将里面通过let或者const声明的变量存放在词法环境中并设置为初始化,其实,在词法环境内部,维护了一个小型的栈结构,栈底是函数最外层的变量,每遇到一个代码块,就将所包含的变量压入词法环境的栈结构,代码块执行结束后,就将包含的变量弹出

                    接下来看一段代码:

                    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函数执行上变量环境
                    a = 1, c = 3词法环境
                    {b = 3, d = 5}
                    {b = 2}

                    当代码块的代码执行完毕后,对应的词法环境里的变量就会被弹出栈

                    foo函数执行上下文变量环境
                    a = 1, c = 3词法环境
                    {b = 2}

                    原因

                    JavaScript执行上下文是如何在函数调用、闭包和异步操作中形成和变化的?

                    通过上面的分析,我们可以总结变量提升和块级作用域的实现:

                    • 通过编译阶段,通过var声明的变量已经存在变量环境中并赋值为undefined,所以在执行代码的任何位置都能狗访问得到,而不需要在声明之后才能访问
                    • 而通过let声明的变量,会被存放在词法环境中但并未初始化(不包含代码块的let或const声明的变量),所以并不能访问,而是等到遇到let声明语句的时候才初始化并赋值
                    • 在遇到代码块时,会先将let和const声明的变量存放在词法环境中并设置为初始化,如果此时在代码块中在let声明变量之前使用该变量,并不会去外部作用域找该变量,因为此时词法作用域已经存在改变量了,但未初始化,所以此时会报错误,这也是let暂时性死区的原因

                    单个执行上下文中变量的查找规则

                    沿着词法环境的栈顶向下查询,如果在词法环境的某个快中查找到了,就直接返回给js引擎,如果没有找到,就继续在变量环境中查找


                    调用栈

                    调用栈是用来管理函数调用关系的一种栈结构

                    在函数调用之前,会创建对应的执行上下文,并生成对应的可执行代码

                    • js维护了一个栈结构,每当遇到一个函数调用的时候,就创建一个执行上下文,并压入该栈中,
                    • 这个栈叫做执行上下文栈,也叫做调用栈
                    • 当函数执行完毕之后,会将对应的执行上下文弹出栈结构
                    • 栈的容量是有限的,当栈容量不够的时候就有可能发生栈溢出

                    作用域链

                    • 在函数中如果在当前作用域中找不到所需要的变量,就得沿着作用域链往下去查找,直到找到为止
                    • 我们都知道,当一段代码在执行的时候,会有对应的执行上下文,那变量沿着作用域链查看的规则也是在执行上下文中设置的
                    • 在每个执行上下文中,在变量环境中,都有一个外部引用,用来执行外部的执行上下文,我们把这个外部引用称为outer
                    • 上文已经说到,变量的查找首先会从执行上下文的词法环境中查找,找不到就在变量环境中查找,再找不到的话就会沿着outer去外部的执行上下文中查找
                    • outer具体引用哪一个执行上下文(作用域),是由词法作用域决定的

                    词法作用域

                    词法作用域指的是作用域有代码中函数的声明位置决定的,也叫做静态作用域

                    也就是说,当创建一个执行上下文的时候,其内部的outer就会根据词法作用域去执行对应的外部执行上下文

                    在外部的执行上下文中查找时,也是先从词法环境中开始

                    function fn() { console.log(a); } function fn1() { let a = 1 fn() } let a = 3 // let声明的变量是在词法环境中的 fn1() // 3

                    闭包

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

                    function foo() { var myName = " 极客时间 " let test1 = 1 const test2 = 2 var innerBar = { getName:function(){ console.log(test1) return myName }, setName:function(newName){ myName = newName } } return innerBar } var bar = foo() bar.setName(" 极客邦 ") bar.getName() console.log(bar.getName())

                    上面代码中,由于存在闭包现象,foo函数执行结束后,内部的变量还会被保存,调用栈如下图:

                    当执行到 bar.setName 方法中的myName = "极客邦"这句代码时,JavaScript 引擎会沿着“当前执行上下文–>foo 函数闭包–> 全局执行上下文”的顺序来查找 myName 变量,你可以参考下面的调用栈状态图:

                    闭包的回收

                    • 如果闭包使用不正确,会很容易造成内存泄漏的,关注闭包是如何回收的能让你正确地使用闭包。
                    • 通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。
                    • 如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。
                    • 所以在使用闭包的时候,你要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

                    var bar = { myName:"time.geekbang.com", printName: function () { console.log(myName) } } function foo() { let myName = " 极客时间 " return bar.printName } let myName = " 极客邦 " let _printName = foo() _printName() bar.printName()

                    从上下文角度讲this

                    执行上下文分为三种,对应的this也只有三种:全局上下文的this,函数中的this,eval中的this

                    • 箭头函数没有自己的执行上下文
                    • 全局上下文的this指向全局对象
                    • 函数上下文的this根据四种绑定规则判断this指向
                    • 执行上下文包含this指向

                    参考:time.geekbang.org/column/intro/100033601

                    本文共计3187个文字,预计阅读时间需要13分钟。

                    JavaScript执行上下文是如何在函数调用、闭包和异步操作中形成和变化的?

                    目录简介JavaScript代码执行过程编译阶段变量提升与let和const作用域单个执行上下文中变量的查找规则调用栈作用域链词法作用域闭包闭包的回收上下文角度讲this简介执行

                    目录
                    • 简介
                    • javascript代码的执行过程
                      • 编译阶段
                    • var变量提升与let和const
                      • 作用域
                        • 单个执行上下文中变量的查找规则
                          • 调用栈
                            • 作用域链
                              • 词法作用域
                                • 闭包
                                  • 闭包的回收
                                    • 从上下文角度讲this

                                      简介

                                      执行上下文可以说是js代码执行的一个环境,存放了代码执行所需的变量,变量查找的作用域链规则以及this指向等。同时,它也是js很底层的东西,很多的问题如变量提升、作用域链和闭包等都可以在执行上下文中找到答案,所以这也是我们学习执行上下文的原因

                                      执行上下文分为三种:

                                      • 全局执行上下文:当进入全局代码时会进行编译,在编译中创建全局执行上下文,并生成可执行代码
                                      • 函数执行上下文:执行代码的过程中,如果遇到函数调用,会编译函数内的代码和创建函数执行上下文,并创建可执行代码
                                      • eval执行上下文:当使用eval函数的时候,eval的代码也会被编译,并创建执行上下文

                                      因为执行上下文是在编译阶段创建的,所以接下来先看一下js代码的执行过程吧

                                      javascript代码的执行过程

                                      一段js代码的执行过程中,先是会进行编译阶段,js引擎会将代码进行编译,再进入执行阶段

                                      也就是说,js代码是按照“段”来执行的,具体就是全局代码就是一段代码,函数执行也算一段代码,编译也是按照“段”来编译的,也就是一整个js代码会出现多个编译阶段

                                      编译阶段

                                      编译阶段是一个很复杂的过程,这里只是简单的介绍:

                                      1、编译阶段完成两件事情:创建执行上下文和生成可执行代码

                                      2、执行上下文就包括变量环境和词法环境和this指向等,创建执行上下文的过程:

                                      • 如果是普通变量的话,js引擎会将该变量添加到变量环境中并初始化为undefined
                                      • 如果是函数声明的话,js引擎会将函数定义添加到变量环境中,然后将函数名执行该函数的位置(内存)

                                      3、接着,js引擎就会把其他的代码编译为字节码,生成可执行代码

                                      编译阶段完成后,js引擎开始执行可执行代码,按照顺序一行一行执行,当遇到函数或者变量时,会在变量环境中寻找,找不到的话就会报错

                                      如果遇到赋值语句时,就会将值赋值给变量

                                      var变量提升与let和const

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

                                      变量提升的实现并不是物理地移动代码的位置,而是在编译阶段被js引擎放入内存中。

                                      1、普通变量提升会赋值为undefined,函数变量名会将整个函数提升

                                      console.log(fn); // [Function: fn] console.log(a); // undefined function fn() { console.log(111); } var a = 1

                                      2、函数表达式只会将变量提升,不会将函数题提升

                                      3、当有多个相同类型的声明(同样是函数声明或者同样是普通变量声明),最后一个声明会覆盖之前的声明

                                      4、当函数声明与变量声明同时出现时,函数声明优先级更高

                                      当然,变量提升有很多的缺陷,所以从es6开始引入了let和const关键字,通过let和const声明的变量不具有变量提升特性,同时也支持块级作用域,先看一下作用域吧

                                      作用域

                                      作用域其实就是一套定义函数调用和变量使用的规则,其中,就有三种作用域:

                                      • 全局作用域:其中对象在代码的任何地方都能访问,其生命周期伴随着页面的生命周期
                                      • 函数作用域:在函数内部定义的变量和函数,只能在内部访问,外部不能访问到,函数执行结束后,函数内部定义的变量会被销毁(函数不会吗???)
                                      • 块级作用域:由代码块包含的代码会形成一个块级作用域(es6之前没有),跟函数作用域类似

                                      通过var声明的变量没有块级作用域,通过const let声明的变量有块级作用域

                                      那js是如何var的变量提升和支持块级作用域的呢?这就得从执行上下文的角度说起

                                      编译阶段生成执行上下文:

                                      假设js需要执行一个函数

                                      • 首先,编译创建该函数的执行上下文,创建可执行代码
                                      • 在编译阶段,所有通过var声明的变量(包括代码块里面的变量)都会被创建并存放在变量环境中,并初始化为undefined
                                      • 通过let或者const声明的变量(不包括代码块码里面的变量)都会被创建并存放在词法环境中,设置为未初始化
                                      • 至此,编译阶段结束了,开始执行代码
                                      • 执行代码过程中遇到代码块时,会先将里面通过let或者const声明的变量存放在词法环境中并设置为初始化,其实,在词法环境内部,维护了一个小型的栈结构,栈底是函数最外层的变量,每遇到一个代码块,就将所包含的变量压入词法环境的栈结构,代码块执行结束后,就将包含的变量弹出

                                      接下来看一段代码:

                                      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函数执行上变量环境
                                      a = 1, c = 3词法环境
                                      {b = 3, d = 5}
                                      {b = 2}

                                      当代码块的代码执行完毕后,对应的词法环境里的变量就会被弹出栈

                                      foo函数执行上下文变量环境
                                      a = 1, c = 3词法环境
                                      {b = 2}

                                      原因

                                      JavaScript执行上下文是如何在函数调用、闭包和异步操作中形成和变化的?

                                      通过上面的分析,我们可以总结变量提升和块级作用域的实现:

                                      • 通过编译阶段,通过var声明的变量已经存在变量环境中并赋值为undefined,所以在执行代码的任何位置都能狗访问得到,而不需要在声明之后才能访问
                                      • 而通过let声明的变量,会被存放在词法环境中但并未初始化(不包含代码块的let或const声明的变量),所以并不能访问,而是等到遇到let声明语句的时候才初始化并赋值
                                      • 在遇到代码块时,会先将let和const声明的变量存放在词法环境中并设置为初始化,如果此时在代码块中在let声明变量之前使用该变量,并不会去外部作用域找该变量,因为此时词法作用域已经存在改变量了,但未初始化,所以此时会报错误,这也是let暂时性死区的原因

                                      单个执行上下文中变量的查找规则

                                      沿着词法环境的栈顶向下查询,如果在词法环境的某个快中查找到了,就直接返回给js引擎,如果没有找到,就继续在变量环境中查找


                                      调用栈

                                      调用栈是用来管理函数调用关系的一种栈结构

                                      在函数调用之前,会创建对应的执行上下文,并生成对应的可执行代码

                                      • js维护了一个栈结构,每当遇到一个函数调用的时候,就创建一个执行上下文,并压入该栈中,
                                      • 这个栈叫做执行上下文栈,也叫做调用栈
                                      • 当函数执行完毕之后,会将对应的执行上下文弹出栈结构
                                      • 栈的容量是有限的,当栈容量不够的时候就有可能发生栈溢出

                                      作用域链

                                      • 在函数中如果在当前作用域中找不到所需要的变量,就得沿着作用域链往下去查找,直到找到为止
                                      • 我们都知道,当一段代码在执行的时候,会有对应的执行上下文,那变量沿着作用域链查看的规则也是在执行上下文中设置的
                                      • 在每个执行上下文中,在变量环境中,都有一个外部引用,用来执行外部的执行上下文,我们把这个外部引用称为outer
                                      • 上文已经说到,变量的查找首先会从执行上下文的词法环境中查找,找不到就在变量环境中查找,再找不到的话就会沿着outer去外部的执行上下文中查找
                                      • outer具体引用哪一个执行上下文(作用域),是由词法作用域决定的

                                      词法作用域

                                      词法作用域指的是作用域有代码中函数的声明位置决定的,也叫做静态作用域

                                      也就是说,当创建一个执行上下文的时候,其内部的outer就会根据词法作用域去执行对应的外部执行上下文

                                      在外部的执行上下文中查找时,也是先从词法环境中开始

                                      function fn() { console.log(a); } function fn1() { let a = 1 fn() } let a = 3 // let声明的变量是在词法环境中的 fn1() // 3

                                      闭包

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

                                      function foo() { var myName = " 极客时间 " let test1 = 1 const test2 = 2 var innerBar = { getName:function(){ console.log(test1) return myName }, setName:function(newName){ myName = newName } } return innerBar } var bar = foo() bar.setName(" 极客邦 ") bar.getName() console.log(bar.getName())

                                      上面代码中,由于存在闭包现象,foo函数执行结束后,内部的变量还会被保存,调用栈如下图:

                                      当执行到 bar.setName 方法中的myName = "极客邦"这句代码时,JavaScript 引擎会沿着“当前执行上下文–>foo 函数闭包–> 全局执行上下文”的顺序来查找 myName 变量,你可以参考下面的调用栈状态图:

                                      闭包的回收

                                      • 如果闭包使用不正确,会很容易造成内存泄漏的,关注闭包是如何回收的能让你正确地使用闭包。
                                      • 通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。
                                      • 如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。
                                      • 所以在使用闭包的时候,你要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

                                      var bar = { myName:"time.geekbang.com", printName: function () { console.log(myName) } } function foo() { let myName = " 极客时间 " return bar.printName } let myName = " 极客邦 " let _printName = foo() _printName() bar.printName()

                                      从上下文角度讲this

                                      执行上下文分为三种,对应的this也只有三种:全局上下文的this,函数中的this,eval中的this

                                      • 箭头函数没有自己的执行上下文
                                      • 全局上下文的this指向全局对象
                                      • 函数上下文的this根据四种绑定规则判断this指向
                                      • 执行上下文包含this指向

                                      参考:time.geekbang.org/column/intro/100033601