一、执行上下文
通常一段javascript代码要经过编译生成两部分:执行上下文和可执行代码。其中执行上下文就是JavaScript执行一段代码时的运行环境。在这个环境中存在着变量环境对象。
VariableEnvironment:
myname ->undefined;
showName-> function:{console.log(myname)}
接下来,JavaScript引擎会把声明以外的代码编译为字节码。
- 什么时候会创建执行上下文
1.执行全局代码的时候会创建,而且在整个页面的生命周期内,全局执行上下文只有一份。
2.调用一个函数的时候会创建,函数执行结束之后,创建的执行上下文会被销毁。
3.使用eval函数的时候会创建。
- 调用栈
调用栈是用来管理函数调用关系的一种数据结构。包括:函数调用和栈结构。
var a=2;
function add(){
var b=10;
return a+b;
}
add();//JavaScript会判断这是一个函数调用,所以会创建该函数的执行上下文。
为了执行add(),首先JavaScript引擎会先创建全局执行上下文。
当执行到add()语句时,JavaScript判断到这是一个函数的调用,所以会创建该函数的执行上下文。
管理不同执行上下文的结构就是栈结构
- 栈结构和栈溢出
栈结构遵循后进先出的原则。调用栈是有大小的,当入栈的执行上下文超过一定数目时,JavaScript引擎就会报错,也叫栈溢出。
- 变量提升
在JavaScript 代码被执行的过程中,JavaScript引擎把变量的声明和函数的声明提升到代码开头。变量提升后会给变量设置默认值:undefined。变量提升并不是将变量和函数声明的物理位置移到代码最前面,实际上变量和函数声明在代码里的位置不会改变,而是在编译阶段被JavaScript引擎放入执行上下文环境中。
二、栈溢出
由于JavaScript代码运行时遵循调用栈结构,在使用递归函数时容易造成栈溢出。常见的递归:
//快排
const quickSort=array=>{
if(array.length<=1){
return array;
}
const [first,...rest]=array;
let smaller=[];
let bigger=[];
for(var i=0;i<rest.length:i++){
const value=rest[i];
if(value<first){
smaller.push(value);
}else{
bigger.push(value);
}
}
return [
...quickSort(smaller),
first,
...quickSort(bigger)
]
}
//取一棵树的子节点
const getLeaves=tree=>{
if(!tree.children){
return tree
}
return tree.children
.map(getLeaves)
.reduce((acc,item)=>acc.concat(item),[])
}
针对递归容易造成的栈溢出,采取:尾调用优化,蹦床函数优化。尾调用是指一个函数的最后一条语句是对另外一个函数的调用,所以递归调用必须是递归函数的最后一条语句。
//尾调用
const sum = (array, result = 0) => {
if (!array.length) {
return result;
}
const [first, ...rest] = array;
return sum(rest, first + result);
}
//蹦床函数
const sum = (array) => {
const loop = (array, result = 0) =>
() => { // 代码不是立即执行的,而是返回一个稍后执行的函数:它是惰性的
if (!array.length) {
return result;
}
const [first, ...rest] = array;
return loop(rest, first + result);
};
// 当我们执行这个循环时,我们得到的只是一个执行第一步的函数,所以没有递归。
let recursion = loop(array);
// 只要我们得到另一个函数,递归过程中就还有其他步骤
while (typeof recursion === 'function') {
recursion = recursion(); // 我们执行现在这一步,然后重新得到下一个
}
// 一旦执行完毕,返回最后一个递归的结果
return recursion;
}
蹦床函数的思想是使用延迟计算稍后执行递归调用,由于每次都需要新创建一个函数,所以会导致执行效率变慢。
三、作用域
在ES6之前,ES的作用域只有两种:全局作用域和函数作用域。通常var定义的变量会在编译阶段存到变量环境中。故而var定义的变量具有函数作用域和全局作用域。
- 全局作用域的生命周期伴随着页面的生命周期。
- 函数作用域:只能在函数内部被访问。函数执行结束后,函数内部定义的变量会被销毁。
由于JS不能像其他语言一样拥有块级作用域。而没有块级作用域会带来一系列问题:变量容易在不被察觉的情况下被覆盖掉。
var myname = "极客时间"
function showName(){
console.log(myname);
if(0){
var myname = "极客邦"
}
console.log(myname);
}
showName()
//在编译阶段
//全局执行上下文->myname变量
// ->showName()函数
//函数执行上下文 ->myname变量
当JavaScript运行时,会优先从当前的执行环境上下文中查找变量。 为了避免变量提升导致的问题,ES6提出let和const。 我们都知道let和const存在块级作用域,那么javascript是如何同时存在变量提升和块级作用域这两大特性的呢?
答案就是:变量环境和词法环境。二者都是在编译阶段创建执行上下文的时候创建的。 变量环境里面存储着全局变量或var定义的变量或函数。 词法环境里面存储着let,const 定义的变量。同时在词法环境内部,也存在着一个独立的栈结构。当进行变量查找时,会先在词法环境的栈结构中查找,如果找到了就返回给javascript引擎,没有找到就继续在变量环境中查找。
所以变量提升是通过变量环境来实现的,而块级作用域是通过词法环境来实现的。
四、闭包
- 作用域链
作用域链是用来查找变量的。首先在每个执行上下文环境中都包含一个外部引用,指向外部的执行上下文。在查找变量时,首先会在当前执行上下文中查找,找不到再去外部执行上下文中查找。而怎么确定外部执行环境是由词法作用域决定的。
- 词法作用域
词法作用域是在编译阶段决定的。也就是说词法作用域是根据代码中函数声明的位置来决定,和函数调用的位置没有关系。
- 闭包
在javascript中,根据词法作用域的规则,内部函数可以访问外部函数声明的变量,当通过一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量任然保存在内存中。
- this指向
由于javascript中存在作用域链,所以有的时候我们需要改变作用域,从而出现了this。this是在编译阶段执行上下文创建的时候创建的,改变this指向有三种方法:call,apply,bind。
1.apply:参数的传入可以是数组,临时改变一次this的指向,改变指向的同时运行目标函数。
2.call:参数的传入是列表形式,临时改变一次this的指向,改变指向的同时运行目标函数。
3.bind:参数的传入是列表形式,永久改变this的指向,改变指向的同时不会运行目标函数而是返回一个值。
五、垃圾回收机制
javascript中的数据分为两块:引用类型和普通类型。引用类型存储在堆中,普通类型存储在栈中。相应的垃圾回收机制就有分别针对堆内存和栈内存的方法。
- 栈内存:依靠记录当前执行状态的指针(ESP)。当一个函数执行完之后这个指针就会下移指向下一个执行上下文。这个函数的执行上下文虽然还保存在栈内存中,但是已经是无效内存了。
- 堆内存:垃圾回收器
六、V8引擎执行JavaScript代码
- 源代码会先通过词法分析和语法分析生成抽象语法树和执行上下文。
- 通过解释器(lgnition)生成字节码并执行字节码,其中有热点代码的话,编译器(TurboFan)会将这段热点代码编译为机器码。提高代码执行效率。字节码配合解释器和编译器这种编译热点代码的技术就是即时编译。
- 代码执行效率优化
基于V8引擎的工作原理,我们对于脚本执行效率的优化关注点就应该放在单次脚本执行时间和脚本网络下载上。可以从一下几个方面考虑:提升脚本执行速度,避免大的内联脚本,减少JavaScript文件的容量。