1.变量提升
JavaScript代码在执行的时候,利用var声明的变量是会提升到代码的开头并且赋值为unddefined。
使用函数声明创建的函数也会被提升,称为函数提升。
上述两个机制的存在使得我们可以在变量和函数声明之前使用它们。
JavaScript代码会经过JavaScript引擎的编译之后再执行。代码经过编译之后,会生成包含变量对象和词法环境的执行上下文以及可执行代码。变量对象中保存的就是被提升的变量声明和函数声明。如果遇到var声明的变量,变量对象中就会生成一个属性名为变量名、属性值为undefined的属性;如果遇到函数声明,就会生成一个方法名为函数名,方法为函数本身的方法(函数存储在堆内存中,方法名保留的是地址值。)
简单总结执行机制。
- 编译。进行变量和函数声明的提升,存储在变量对象中。
- 执行。顺序执行可执行的代码。
2.调用栈
JavaScript引擎在执行代码的时候,会创建执行上下文。主要有全局执行上下文,函数执行上下文和eval函数的执行上下文。首先在执行JavaScript代码前,就会创建一个全局执行上下文压入栈,然后根据函数的调用顺序依次将执行上下文压入栈。函数内部代码被执行完毕,对应的执行上下文就会被弹出栈。
执行上下文中包含变量对象(VO)和词法环境两个部分,执行上下文是在JavaScript引擎编译阶段创建的,也就是代码执行前。
一段代码
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)
在开发者工具中给代码打上断点之后,在右边的调用栈堆里面能看见目前调用栈中的执行上下文有哪些。
或者给代码加上一个console.trace也可以。
var a = 2
function add(b,c){
console.trace()//查看函数调用关系
return b+c
}
function addAll(b,c){
var d = 10
result = add(b,c)
return a+result+d
}
addAll(3,6)
控制台输出结果:
栈溢出问题,执行上下文的调用栈是有大小的。如果创建一个递归函数并且不设置终止条件,最终就会发生栈溢出。为避免栈溢出,需要尽量将递归任务分解成其他的小型任务来执行。
3.块级作用域
ES6之前JavaScript只有全局作用域和函数作用域,ES6通过let和const实现了块级作用域。简单来说一对大括号就是一个块级作用域。ES6如何同时支持变量提升和块级作用域?
前面提到,函数调用时会创建执行上下文,执行上下文中有两个部分,变量对象和词法环境。支持块级作用域实际上就是通过词法环境实现的。比如下面这个代码:
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()
来分析一下这段代码的如何执行,以及执行上下文的情况。
- 1.调用函数,创建执行上下文。变量对象中有a,c两个变量,值都是undefined(var声明的变量的变量提升导致的)。词法环境中有一个变量b,但是由于let会生成暂时性死区,所以虽然b已经存在了,但是在声明b之前的语句去访问b浏览器都会报错。
- 2.执行大括号内部代码,此时a=1,b=2。此时由于进入了一个新的作用域块,所以词法环境中会将这个块压入栈,这个新的块中也有两个变量b,d,同样在它们声明之前不可访问。
- 3.console.log(a)。执行这句代码的时候,就涉及到了如何在词法环境和变量对象中寻找变量了。首先会查找位于词法环境的栈顶的块中的变量,然后沿着这个顺序一直向下查找到位于栈底的块,如果都没有找到则会进入变量对象中去寻找变量。也就是说,优先在词法环境中寻找,其后再到变量对象中寻找。
4.作用域和闭包
作用域。始终记住作用域是静态的,并且作用域在函数被定义的时候就已经是确定了的,不会再发生改变了。
闭包。记住闭包产生的两个条件,函数嵌套以及内部的函数引用了外部函数的变量。有了闭包,即使外部函数已经执行完毕,它的执行上下文也已经被弹出了执行栈,但是内部的变量还是会被保留下来,当内部函数被调用,这些被保存的变量随时可以被这个内部函数调用。就像一个专属于内部函数的背包一样。
闭包的存在使得变量查找的作用域链发生了一定变化。首先会在内部函数自身的执行上下文中寻找,之后进入闭包中寻找,最后进入全局作用域寻找。
5.this
- 函数直接以函数形式被调用的时候,this指向全局对象window
- call apply bind可以修改函数的this指向,使其指向参数中的对象
- 作为对象的方法被调用的时候,指向调用该方法的对象
- 构造函数形式被调用,this指向利用构造函数创建的对象
- 箭头函数的this是静态的,指向其被创建时所在的对象
嵌套函数,内部的函数不会继承外部函数的this指向
6.小结
本节重点:
- JavaScript代码也是需要先编译后执行的,编译的工作由JavaScript引擎来完成。
- 代码经过编译后会生成执行上下文和可执行代码,执行上下文中由变量对象和词法环境。也解释了为什么JavaScript会存在变量提升,其实是JavaScript引擎编译的锅。
- var声明和函数声明的变量和函数会被存储在变量环境中,变量的初始化值是undefined,函数则是函数的定义。let,const声明的变量被存储在词法环境中,形成暂时性死区,也就是在声明变量前不能对他们进行访问。
- 每进入一个新的代码块。词法环境会创建一个新的块压入栈,里面是这个代码块中使用let和const声明的变量。
- JavaScript寻找变量的方式。首先在词法环境栈顶的块中寻找,然后依次向下到栈底的块,最后再到变量对象中寻找。