转载自掘金网络,原文链接:https://juejin.im/post/5ce767eef265da1bbf68f724
前言
对于一名前端开发者来说,深入理解JavaScript程序内部执行机制当然是很有必要的,其中一个关键概念就是JavaScript的执行上下文和执行栈,理解这部分内容也有助于理解作用域、闭包等
本次重点
- 执行上下文概念、类型、特点
- 执行上下文的生命周期
- 关于变量提升
- this指向问题
- 执行上下文栈
基本概念:
所谓的JavaScript执行上下文就是当前JS代码代码被解析和执行时所在环境的抽象概念,js代码都是在执行上下文中运行的
一、执行上下文类型
1.全局执行上下文
它的特点有以下几个:
a.它是最基础、默认的全局执行上下文
b.它会创建一个全局对象,并且将this指向全局对象,在浏览器中全局对象是window,在nodejs中全局对象是global
c.一个程序中只有一个
2.函数执行上下文
它的特点有以下几个:
a.有自己的执行上下文
b.可以在一个程序中存在任意数量
c.是函数被执行时创建
3.eval函数执行上下文:
eval函数可以计算某个字符串,并执行其中的js代码,这样就会存在一个安全性问题,在代码字符串未知或者是来自于用户输入源的话,绝对不要使用eval函数
以上就是执行上下文的几种类型和相应的特点,我们可以看下下面这段代码:
里面的三个函数都被执行了,所以是有三个函数执行上下文
1 | // 全局执行上下文 |
二、执行上下文的生命周期
执行上下文的生命周期分了三个阶段:
- 创建阶段
- 执行阶段
- 回收阶段
创建阶段
对于函数执行上下文,函数被调用的时候,但是还未执行里面的代码之前,会做三件事情:
创建变量对象:会初始化函数的参数,提升函数声明和变量声明
创建作用域链:作用域链用于标识符解析,看下面代码:
f3函数被调用的时候,里面的变量num要求被解析的时候,会在当前f3的作用域里查找,如果没找到,就会向上一层作用域中查找,直到在全局作用找到该变量为30
1 | var num = 30; |
- 确定this指向:这个情况比较多,会在下文统一介绍
在一个程序执行之前,要先解析代码,会先创建全局执行上下文环境,把需要执行的变量和函数声明都取出来并暂时赋值为undefined,函数也要先声明好待调用,这也是我们下文中会讲到的变量提升,以上几步做完后,开始正式执行程序
执行阶段
执行的变量赋值、函数调用等代码执行
回收阶段
执行上下文出栈,等待虚机垃圾回收执行上下文
三、变量提升
变量提升分为两种:
- 变量声明提升
- 函数声明提升
关于变量声明提升,先看以下代码片段:
1 | console.log(a) // undefined |
以上代码中,第1个 a 是在全局执行上下文环境中,由于在全局执行上下文创建的时候,把需要执行的变量和函数声明都取出来并暂时赋值为undefined,所以打印出来的就是undefined
第2个 a 是在test这个函数执行上下文环境中,同上,所以打印出来的就是undefined
1 | var a |
关于函数声明提升,看以下代码:
1 | console.log(f1) // function f1() {} |
打印结果在注释中,由于变量声明和函数声明提升原则可以把代码改成如下:
1 | function f1() {} |
f1和f2不一样的地方是:f1是普通函数声明的方式,f2是函数表达式,在f2未被赋值的时候,它就是一个变量,这个时候变量提升,所以打印的f2为undefined
如果一个变量既是函数声明的方式,又是变量声明的方式,代码如下:
我们发现函数声明的优先级是高于变量提升的优先级的
1 | function test(arg){ |
总结:变量提升的几个特点:
- 如果有形参,先给形参赋值
- 函数声明的优先级是高于变量提升的优先级的,但可以重新赋值
- 私有作用域代码从上到下执行
四、确定this指向问题
this指向问题通常会在一些面试题中出现,情况比较多,先了解下它的一些特点:
- this是执行上下文的一部分
- 需要在执行时确定
- 浏览器中 this 指向 window, node中this指向global
对于非严格模式和es5的js程序中,this指向可以分为以下几种情况:
第一种:a()直接调用的方式,this === window
1 | function a() { |
打印出的值为 0
第二种:谁调用了函数,谁就是this
1 | function a() { |
打印出的值为obj这个对象
第三种:构造函数模式下,this指向当前执行类的实例
1 | function getPersonInfo(name, age) { |
打印出来的值是:
getPersonInfo{ name: ‘linda’, age: 13 }
第四种:call/apply/bind调用函数的方式,this指向第一个参数
1 | function add (b, c) { |
打印出来的值就是obj的值
对于严格模式的js程序中,this指向对于直接调用的方式有所不同:
严格模式下,函数直接调用的方式中this指向undefined
1 | 'use strict' |
这个时候函数里的this打印出 undefined
对于箭头函数
箭头函数没有自身的this关键字,看外层是否有函数,如果有函数,外层函数的this就是内部箭头函数的this,如果没有,this就是指向window
可以看以下几种情况:
1 | var person = { |
打印结果:Person name is undefined, age is undefined
里面的函数show被调用的时候,是普通函数调用的情况,所以this指向window,而全局函数中没有myName和age,所以打印出来是undefined
可以换成箭头函数:
1 | var person = { |
打印出的结果是:Person name is linda, age is 1
对于箭头函数自身没有this关键字,所以看外层函数,而外层函数中是我们前面说到的第二种情况,this指向person这个对象,所以是有myName和age的值
如果把clickPerson也换成箭头函数:
1 | var person = { |
我们发现打印的结果是:Person name is undefined, age is undefined
由于都是箭头函数,最后找到了全局的window,所以this指向window,而全局函数中没有myName和age,所以打印出来是undefined
再看另外一个例子:
1 | function getPersonInfo(name,age){ |
show()函数调用结果打印:Person name is linda, age is 18
friend()函数调用打印结果:["my friend undefined age is undefined", "my friend undefined age is undefined"]
对于friend函数内部,this指向的是当前的getPersonInfo这个构造函数初始化的实例,但是在内部使用map是一个闭包函数,且内部是普通函数的调用方式,所以内部this是指向了window,可以把里面普通函数调用的方式改成箭头函数的方式即可
1 | function getPersonInfo(name,age){ |
这次打印结果就是["my friend linda age is 18", "my friend linda age is 18"]
就是我们预想的了
总结:(非严格模式下)可以按照下图规律查找this的指向
五、执行上下文栈
js创建了执行上下文栈来管理执行上下文,我们通过如下一段代码和进栈出栈顺序图来理解执行上下文栈
1 | var name = 'Tom'; |
过程:
1.全局执行上下文进栈
2.调用函数father,father函数执行上下文进栈
3.father函数内部代码执行,son函数被执行,son函数执行上下文进栈
4.son函数执行完毕,son函数的执行上下文出栈
5.father函数执行完毕,father函数的执行上下文出栈
6.浏览器关闭时,全局执行上下文出栈
执行上下文栈特点:
- 先创建全局执行上下文,并压入栈顶
- 函数执行时创建函数执行上下文,再压入栈顶
- 函数执行完函数的执行上下文出栈,等待垃圾回收
- JS执行引擎总是访问栈顶的执行上下文
- js代码是单线程的,代码是排队执行
- 全局执行上下文在浏览器关闭时出栈