一、变量提升
1、变量声明和赋值
var name = '变量的声明和赋值'
这一行代码应该看成两句话:
// 变量提升 start
var name = undifined
// 变量提升 end
// 可执行代码 start
name = '变量的声明和赋值'
// 可执行代码 start
2、函数的声明和赋值
function foo(){
console.log('foo')
}
var bar = function(){
console.log('bar')
}
这段代码应该看成像下面这样
// 变量提升 start
function foo(){ //声明和赋值是同时的
console.log('foo')
}
var bar = undefined
// 变量提升 end
// 可执行代码 start
bar = function(){
console.log('bar')
}
// 可执行代码 end
3、综合
showName()
showName2()
console.log(name)
var name = '我是name变量'
function showName() {
console.log('我是showName函数')
}
var showName2 = function() {
console.log('我是showName2函数')
}
这段代码应该看成下面这样
// 变量提升 start
var name = undifined
function showName() {
console.log('我是showName函数')
}// 声明和赋值同时发生
var showName2 = undifined
// 变量提升 end
// 可执行代码 start
showName()
showName2() //报错了,showName2不是一个函数,下面的代码不会再执行了.
console.log(name)
name = '我是name变量'
showName2 = function() {
console.log('我是showName2函数')
}
// 可执行代码end
二、执行上下文和它的变量环境
ES6之前还没有let和const,var声明的变量放到变量环境里面。
1、编译
编译和变量提升的关系:编译生成执行上下文和可执行代码.执行上下文中保存了变量提升的内容,放在一个叫做变量环境的对象里头.可执行代码被放到代码空间里头。
执行上下文当然不止变量环境这一个东西.变量环境只是执行上下文的子集.这里讲ES5的编译,还没有涉及到变量环境之外的可执行上下文的内容.你暂时可以把ES5的执行上下文理解为变量环境.
2、执行
编译完之后就会按可执行代码的顺序一行行执行下去.
3、调用栈
当一段代码要被执行的时候,先要进行编译.但其实代码的编译不是一次性完成的,而是通过调用栈来一边编译一边执行的.当调用函数的时候,会再次编译产生新的执行上下文,调用栈即用来存放执行上下文.
JavaScript代码执行过程是这样的:
-
编译.编译全局代码并创建全局执行上下文和全局可执行代码.全局执行上下文会被放到栈底部.
-
执行可执行代码.此时,若遇到以下两种情况,会再编译一次产生新的执行上下文和可执行代码.这个执行上下文会入栈.
- 函数调用
- 调用eval()
-
执行新的可执行代码.
-
若在新的可执行代码中又出现函数调用、eval时,就会创建新的执行上下文.
-
当最栈中最顶层的函数返回的时候,执行上下文才会一个个出栈.
三、执行上下文的新朋友:词法环境
ES6出现了const和let,它们被保存在词法环境里面
而且ES6之前的JavaScript只支持全局作用域和函数作用域,而ES6的let和const的出现使得JavaScript有了块级作用域.
执行上下文添加了词法环境来实现块级作用域.
现在执行上下文由变量环境和词法环境构成.
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()
根据上面这块代码,来看看有了let、const和词法环境之后,编译和执行有什么不同.
编译和执行
有了let和const,编译产生的执行上下文的内容就不一样了.
现在执行上下文增加了一个词法环境的对象,词法环境通过栈结构来存放let和const相关的内容.
-
首先编译生成包含变量环境和词法环境的执行上下文和可执行代码
-
词法环境里面专门用来保存let和const声明的变量.但第一步编译的现在不包括块级作用域中的let和const变量,仅包含一个b=undefined.
-
变量环境用来保存当前作用域中所有var声明的变量以及函数,即:a = undefined,c = undefined.
-
然后代码可以看成下面这样
-
function foo(){ // 变量环境 start var a = undefined var c = undefined // 变量环境 end // 词法环境 start let b = undefined // 词法环境 end // 可执行代码 start a = 1 b = 2 { let b = 3 c = 4 let d = 5 console.log(a) console.log(b) } console.log(b) console.log(c) console.log(d) // 可执行代码 }
-
-
执行可执行代码
-
执行a=1和b=2,现在执行上下文中的这两个a和b的内容有了值.
-
执行块级作用域的内容,此时要先对块级作用域的内容做一下"编译".即把块级作用域里面的用let声明的b和d打包在一起压入词法环境栈.此时词法环境栈的栈顶内容是b=undefined,d=undefined.,此时代码长这样
-
function foo(){ // 变量环境 start var a = 1 var c = undefined // 变量环境 end // 词法环境 start let b = 2 // 词法环境 end // 可执行代码 start { let b = undefined let d = undefined b = 3 c = 4 d = 5 console.log(a) console.log(b) } console.log(b) console.log(c) console.log(d) // 可执行代码 } -
执行块级作用域中一部分可执行代码,现在代码长这样
-
function foo(){ // 变量环境 start var a = 1 var c = 4 // 变量环境 end // 词法环境 start let b = 2 // 词法环境 end { // 词法环境的栈顶 start let b = 3 let d = 5 // 词法环境的栈顶 end // 可执行代码 start console.log(a) console.log(b) } console.log(b) console.log(c) console.log(d) // 可执行代码 } -
执行块级作用域的两行打印操作,打印出a、b分别为1和3.查找顺序为词法环境栈顶到栈底-->变量环境此时词法环境的栈顶元素出栈,此时代码长这样
-
function foo(){ // 变量环境 start var a = 1 var c = 4 // 变量环境 end // 词法环境 start let b = 2 // 词法环境 end // 可执行代码 start console.log(b) console.log(c) console.log(d) // 可执行代码 end } -
执行函数作用域的三行打印操作,打印出b、c、d分别为2、4和ReferenceError: d is not defined
-
三、作用域链和闭包
1、作用域链
调用一个函数时,要是有一句可执行代码,出现了这个函数的执行上下文中没有出现的变量,该怎么办呢?
答案当然是去另外一个作用域的执行上下文找.那去个作用域的执行上下文找呢?
不按专业术语来讲的话,就是看函数在哪个作用域声明的,如果当前执行的函数是在全局作用域声明的,就在全局执行上下文去找;如果当前正在执行的函数是在一个另函数里面定义的,就在那个函数的执行上下文里面找.跟函数在哪个地方调用的没有关系.
按专业术语来讲的话,就是通过词法作用域来找的.
作用域链就是按上面这个规则一步步查找变量,形成的这个查找链条.
下面解释以下什么叫词法作用域
词法作用域:作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。
顺便补充一句,在执行上下文查找的规则是从词法环境的栈顶往下查找,直到查找完词法环境,找到就返回.没有找到就查找变量环境.
2、闭包
上面讲了,调用一个函数时,要是有一句可执行代码,出现了这个函数的执行上下文中没有出现的变量,该怎么办.假如这个被调用的函数,是在B函数内部声明的,那么这个函数的作用域链的下一级就是B函数.那么查找变量时的下家就是B函数.
看这一种情况:
function func1() {
let a = 1;
return function() {
console.log(a)
}
}
let a = 100
let func2 = closure()
func()
那func函数的结果是100还是1呢?
结果是1,原因如下
首先func调用时,在自身的执行上下文中找不到a变量,所以就要根据词法作用域的规则去找a.根据词法作用域,它要到closure函数的执行上下文去找,这样就找到了a=1.
一般来讲,调用栈的栈顶函数执行完毕,ESP会下移,然后这个栈顶函数的执行上下文就被销毁了.
那这个a=1是怎么保存起来的呢?
- 当JavaScript引擎执行到func1函数时,首先会编译,并创建一个空执行上下文.
- 在编译过程中,遇到内部函数时,JavaScript 引擎还要对内部函数做一次快速的词法扫描,发现该内部函数引用了func1函数中的a变量,由于是内部函数引用了外部函数的变量,所以 JavaScript 引擎判断这是一个闭包,于是在堆空间创建一个“closure(func1)”的对象(这是一个内部对象,JavaScript 是无法访问的),用来保存a变量.
所以即使func1的执行上下文已经被销毁,它的内部函数也能够访问到它的被引用的变量.
产生闭包的核心有两步:第一步是需要预扫描内部函数;第二步是把内部函数引用的外部变量保存到堆中。
闭包的回收
-
如果引用闭包的变量是全局的,那么闭包会一直存在直到页面关闭.
-
如果引用闭包的变量是函数内部的,那么闭包会在这个函数执行上下文销毁之后,在下次垃圾回收器工作的时候就被回收.
所以,如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量.
四、this机制
this机制和作用域链的关系不大。this是和执行上下文绑在一起的
全局执行上下文中的this指向的是window(浏览器环境)或global(node环境)
函数执行上下文中的this指向的默认也是window或global
除此之外,this还有一些特殊情况
1、三种特殊情况
① 对象方法调用
对象调用它自己的第一层级的方法时,this指向的是它本身
对象调用它自己的第二层级或以上的方法时,this指向的还是全局
// 第一种情况
// this在第一层级
var obj = {
a : 1,
b : function() {
console.log(this.a)
}
}
var a = 100
obj.b() //1
// 第二种情况
// this在第二层级
var obj2 = {
a : 1,
b : function() {
var c = function() {
console.log(this.a)
}
c()
}
}
var a = 100
obj2.b() //100
// 第二种情况的解决办法
// 法一
var obj2 = {
a : 1,
b : function() {
let that = this
var c = function() {
console.log(that.a)
}
c()
}
}
var a = 100
obj2.b() //1
// 法二
// 箭头函数并不会创建其自身的执行上下文,箭头函数中的this取决于它所在的执行上下文。
var obj2 = {
a : 1,
b : function() {
var c = () => {
console.log(this.a)
}
c()
}
}
var a = 100
obj2.b() //1
② 箭头函数中的this
箭头函数不会创建执行上下文,它的this会继承它所在的执行上下文的this。要么来自全局执行上下文,要么来自函数执行上下文。
// 此处箭头函数的this继承的是b这个function的this
var obj2 = {
a : 1,
b : function() {
var c = () => {
console.log(this.a)
}
c()
}
}
var a = 100
obj2.b() //1
// 此处箭头函数的this继承的是全局执行上下文的this
var obj = {
a : 1,
b : () => {
console.log('我是this',this.a)
}
}
var a = 100
obj.b()
③ 构造函数的this
function CreateObj(name){
this.name = name
}
var myObj = new CreateObj('张三')
console.log(myObj.name)
2、改变this指向
call/apply/bind
bind只是绑定参数,没有调用,就像一个赋值语句那么简单,函数调用的时候会找到这个参数。
call/apply调用函数,不传参数时,它们是一样的,把要绑定的this作为call/apply调用时的第一个参数。若传参,apply就是把函数参数放到一个数组里面,把数组作为apply调用的第二个参数传入;而call就是把函数参数从call调用的第二个位置开始一个个传入。
var a = function() {
console.log(this.c)
}
var b = {
c : 1,
d : 2,
}
var c = 100
a.call(b) //1
五、V8是如何执行一段JavaScript代码的
- 首先:对源代码进行词法分析语法分析生成抽象语法树和执行上下文
- 接下来,解释器解释抽象语法树生成字节码
- 最后,解释器逐行解释执行代码,就是先根据字节码逐行生成机器码给cpu执行
即时编译(JIT)
在解释器逐行解释代码的过程中,若发现有热点代码(多次重复执行的代码),后台的编译器就会把这段热点代码直接生成机器码。
六、如何优化JavaScript执行效率
单次脚本的执行时间和脚本的网络下载方式
- 减少单次JavaScript执行时间
- 同步的脚本尽可能小、尽量内联
- 大一点的脚本尽可能异步,比如aysnc和defer
七、总结
- 编译生成可执行代码和执行上下文,执行上下文的内容有变量环境、词法环境、this。函数作用域还有outer,用来实现作用域链。outer的指向是由函数声明的位置来决定的,跟函数在哪儿调用的无关。
- 编译后,变量环境里面的内容包括函数和var声明的变量。
- 词法环境包括let、const声明的变量。词法环境维护了一个小型的栈结构,用来实现块级作用域
- 有关this。不管是函数执行上下文还是全局执行上下文,this的指向默认都是window/global;对象调用它自身第一层级的方法时,this指向这个对象;箭头函数中的this会继承它所在的执行上下文;对构造函数的this,要知道new的过程;call/apply/bind可以改变this指向。
- 闭包是指,根据词法作用域的规则,内部函数的outer指向外部函数。在函数编译阶段,遇到内部函数时,JavaScript引擎要对内部函数做一次快速的词法扫描,发现内部函数引用了外部函数的变量,所以JavaScript会判断这是一个闭包,于是在堆空间创建一个闭包的对象,用来保存那个被内部函数所引用的变量,栈内存中有这个闭包对象的引用。在外部函数返回的时候,执行上下文被销毁,但是闭包会被保存下来。
- JavaScript是解释型语言,利用解释器编译执行。采用JIT技术,对热点代码进行了优化。
- 要优化JavaScript执行效率,减少JavaScript代码的单次执行时间,并且同步的脚本尽量小和内联,大脚本文件尽量defer或者async加载。