一、JS代码执行流程
JS的执行机制:先编译,再执行。js代码在编译阶段,会创建执行上下文,变量和函数会被放到变量环境中,变量初始化为undefiend;在执行阶段,js引擎会从变量环境中查找自定义的变量和函数。
变量提升:js代码执行过程中,js引擎会把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后会给变量设置默认值undefined。
实际上变量和函数声明在代码里的位置是不会改变的,而是在编译阶段被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 引擎开始执行“可执行代码”,按照顺序一行一行地执行。
- 注:如果存在同名的函数或者同名的变量,在编译阶段,前者会被后者覆盖。即,最终存储在变量环境中的是最后定义的那个。如果变量和函数同名,编译阶段,变量的声明会被忽略,变量环境中存储的是函数声明,而不论顺序(函数提升比变量提升优先级高)
代码编译时有三种情况会创建执行上下文
- 当 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)
-
第一步,创建全局上下文,并将其压入栈低
全局执行上下文压入到调用栈后,JavaScript 引擎便开始执行全局代码了。首先会执行 a=2 的赋值操作,执行该语句会将全局上下文变量环境中 a 的值设置为 2。
-
第二步,调用addAll函数。当调用该函数时,js引擎会编译该函数,并为其创建一个执行上下文,最后将该函数的执行上下文压入栈中
addAll 函数的执行上下文创建好之后,便进入了函数代码的执行阶段了,这里先执行的是 d=10 的赋值操作,执行语句会将 addAll 函数执行上下文中的 d 由 undefined 变成了 10。
-
第三步,当执行到 add 函数调用语句时,同样会为其创建执行上下文,并将其压入调用栈
-
第四步,当add函数返回,该函数的执行上下文会从栈顶弹出,并将result的值设置为add的返回值
-
addAll执行完,其执行上下文也会从栈顶弹出,此时只剩下全局上下文
整个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()
- 第一步:编译并创建执行上下文
var声明的变量在变量环境
let/const声明的变量在词法环境
在作用域块内部,let/const声明的变量不在词法环境中 - 继续执行代码,并赋值,当进入作用域块中,let/const声明的变量会被存放在词法环境的一个单独区域中。
在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出。
块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。
注: - var的创建和初始化被提升,赋值不会被提升
- let/const的创建被提升,初始化和赋值不会被提升
- function的创建、初始化和赋值均被提升
三、作用域链和闭包
每个执行上下文的变量环境中都包含了一个外部引用outer,用来指向外部的执行上下文。
当一段代码使用一个变量时,js引擎会先在“当前的执行上下文”中查找,如果在当前的变量环境中没有找到,js引擎会继续在outer所指向的执行上下文中查找,这个查找的链条就称为作用域链。作用域是由代码中函数声明的位置来决定的
闭包:在 JavaScript 中,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。
闭包还可以这样理解:当函数嵌套时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局作用域下可访问时,就形成了闭包。
四、this
this和作用域链属于两套不同的系统。
执行上下文中包含了变量环境、词法环境、外部环境outer,还有一个this
this是和上下文绑定的,每个执行上下文都有一个this。
执行上下文分文三种:全局执行上下文、函数执行上下文和eval执行上下文,所以this对应的也有三种。
- 全局执行上下文中的this:指向window对象
-
函数执行上下文中的this:要取决于函数是如何调用的
- 函数作为普通函数调用,在严格模式下,this 值是 undefined,非严格模式下 this 指向的是全局对象 window;
- 作为对象的方法调用,this指向该对象
- 函数使用call、apply或bind方法调用,this由调用时传入的参数决定,指向传入的参数(call、apply和bind都是用于指定函数中的this值的方法)
- 在构造函数中,this指向即将创建的新对象
- 在事件处理函数中,this指向触发事件的元素。
- 嵌套函数不会继承外层的this,也是根据函数时如何调用来决定的,但是箭头函数不会创建其自身的执行上下文,箭头函数中的this,指向外层非箭头函数的this