JavaScript 的执行机制:先编译,再执行。
showName()
var showName = function() {
console.log(2)
}
function showName() {
console.log(1)
}
输出1
- 编译阶段:
var showName
function showName(){console.log(1)}
- 执行阶段:
showName() //输出1
showName=function(){console.log(2)}
// 如果后面再有showName执行的话,就输出2因为这时候函数引用已经变了
JavaScript 中栈调用溢出与事件循环机制
使用迭代
为没有涉及到函数调用所以自然不会影响到调用栈
使用任务队列
这样相当于每次调用栈只执行了一次sum函数,利用任务队列的特性解决了栈溢出的问题。
function sum(n) {
if (n === 1) {
return Promise.resolve(1)
}
return Promise.resolve(n - 1).then(sum).then(result => result + n)
}
通过 setTimeout 异步函数来调用 b 时,上一次当 b 函数被调用完成后,主线程的执行栈会清除掉该次调用栈帧,因为到 setTimeout 这里的时候,主线程执行栈已经知道 b 在主线的调用已经结束了,不需要为它保存任何记录,它被推入了主线程外的队列中去了,控制权由主线程交到了事件轮询机制手里。既然调用栈帧每一次都会被清除,自然也不会出现调用栈达到最大值的异常了。当定时器到点时,就会在任务队列中产生一个事件,事件轮询机制下一次轮询的时候,会在任务队列中发现这个事件,就会知道 b 函数现在可以被拿回到主线程中执行了。
同时这也解释了为什么 setTimeout 和 setIntervel 异步调用的函数内容的 this 指向的是 window 对象,因为即便他们是处于某个对象的方法中,他们的调用也就是事件轮询机制决定的,并不是主线程一手操控,和他们在被书写时候处于哪个对象内部并没有直接的关系。其实是 js 引擎(对于页面作用域来说也就是 window 对象)调用了它们,而不是代码上的 a 对象调用的,所有 this 也自然不会指向 a 对象
尾调用优化
var缺陷以及为什么要引入let和const?
由于 JavaScript 的变量提升存在着变量覆盖、变量污染等设计缺陷,所以 ES6 引入了块级作用域关键字来解决这些问题。
块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,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()
// 打印:
// 1
// 3
// 2
// 4
// ReferenceError: d is not defined
通过上图,我们可以得出以下结论:
- 函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里面了。
- 通过 let 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。
- 在函数的作用域块内部,通过 let 声明的变量并没有被存放到词法环境中。
在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,