本文目录
[[toc]]
执行上下文
- 解释一:⼀段 JavaScript 代码在执⾏之前需要被 JavaScript 引擎编译,编译完成之后,才会进⼊执⾏阶段。代码经过编译后会生成执行上下文(Execution Context)和可执行代码(Executable Code)
- 解释二:JavaScript 引擎并⾮⼀⾏⼀⾏地分析和执⾏程序,⽽是⼀段⼀段地分析执⾏。当执⾏⼀段代码的时候,会进⾏⼀个“准备⼯作”,这⾥的“准备⼯作”,更专业⼀点的说法,就叫做"执⾏上下⽂( execution context )"
执⾏上下⽂栈
JavaScript 引擎创建了执⾏上下⽂栈(Execution context stack,ECS)来管理执⾏上下⽂
- 当 JavaScript 遇到下⾯的这段代码
function fun3() {
console.log('fun3')
}
function fun2() {
fun3()
}
function fun1() {
fun2()
}
fun1()// 伪代码
ECStack = []
ECStack = [globalContext]
// fun1()
ECStack.push(fun1_functionContext)
// 发现 func1 中调用了 func2
ECStack.push(fun2_functionContext)
// 发现 func2 中调用了 func3
ECStack.push(fun3_functionContext)
// fun3 执⾏完毕
ECStack.pop()
// fun2 执⾏完毕
ECStack.pop()
// fun1 执⾏完毕
ECStack.pop()
// 任务执行完成, ECStack 最底层永远有 globalContext对于每个执⾏上下⽂,都有三个重要属性:
- 变量对象(
Variable object, VO) - 作⽤域链(
Scope chain) this
全局上下⽂
- 全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使⽤全局对象,可以访问所有其他所有预定义的对象、函数和属性。
- 在顶层 JavaScript 代码中,可以⽤关键字 this 引⽤全局对象。因为全局对象是作⽤域链的头,这意味着所有⾮限定性的变量和函数名都会作为该对象的属性来查询。
- 例如,当 JavaScript 代码引⽤ parseInt()函数时,它引⽤的是全局对象的 parseInt 属性。全局对象是作⽤域链的头,还意味着在顶层 JavaScript 代码中声明的所有变量都将成为全局对象的属性。
- 简单点说: 在客户端
JavaScript中,全局对象就是Window对象
函数上下⽂
- 在函数上下⽂中,⽤活动对象( activation object, AO )来表示变量对象
函数执⾏过程
- 执⾏上下⽂的代码会分成两个阶段进⾏处理:分析和执⾏,也可以叫做:
- 进⼊执⾏上下⽂;
- 代码执⾏;
变量对象( Variable object, VO )
变量对象会包括:
- 函数的所有形参(如果是函数上下⽂)
- 由名称和对应值组成的⼀个变量对象的属性被创建
- 没有实参,属性值设为
undefined
- 函数声明
- 由名称和对应值(函数对象(
function-object))组成⼀个变量对象的属性被创建 - 如果变量对象已经存在相同名称的属性,则完全替换这个属性
- 由名称和对应值(函数对象(
- 变量声明
- 由名称和对应值(
undefined)组成⼀个变量对象的属性被创建 - 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会⼲扰已经存在的这类属性
- 由名称和对应值(
function foo(a) {
var b = 2;
function c() {}
var d = function () {};
b = 3;
}
foo(1);
// 在进⼊执⾏上下⽂后,这时候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1,
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined,
};
// 当代码执⾏完后,这时候的 AO 是:
AO = {
arguments: {
0: 1,
length: 1,
},
a: 1,
b: 3,
c: reference to function c(){},
d: reference to FunctionExpression "d",
};function foo() {
console.log(a)
a = 1
}
foo() // Uncaught ReferenceError: a is not defined
function bar() {
a = 1
console.log(a)
}
bar() // 1
// 这是因为函数中的 "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中。
// 当第⼆段执⾏ console 的时候,全局对象已经被赋予了a属性,这时候就可以从全局找到 a 的值1console.log(foo);
function foo() {
console.log("foo");
}
var foo = 1;
// 会打印函数,⽽不是 undefined
// 这是因为在进⼊执⾏上下⽂时,⾸先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会⼲扰已经存在的这类属性。作用域链( Scope chain )
- 当查找变量的时候,会先从当前上下⽂的变量对象中查找,如果没有找到,就会从⽗级(词法层⾯上的⽗级)执⾏上下⽂的变量对象中查找,⼀直找到全局上下⽂的变量对象,也就是全局对象。这样由多个执⾏上下⽂的变量对象构成的链表就叫做作⽤域链
- 函数的作⽤域在函数定义的时候就决定了
// 举个例⼦:
function foo() {
function bar() {
// ...
}
}
// 函数创建时,各⾃的[[scope]]为:
foo['[[scope]]'] = [
globalContext.VO
]
bar['[[scope]]'] = [
fooContext.AO,
globalContext.VO
]this
Reference
- 这里的
Reference是一个Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的js代码中
Reference 的构成,由三个组成部分,分别是:
base value: 属性所在的对象或者是EnvironmentRecord,它的值只可能是undefined,Object,Boolean,String,Number,environment record其中的一种referenced name: 属性的名称strict: 标识是不是严格模式
如何确定 this 的值
- 计算
MemberExpression的结果赋值给 ref简单理解
MemberExpression其实就是()左边的部分。 - 判断 ref 是不是一个 Reference 类型。
- 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
- 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么 this 的值为 ImplicitThisValue(ref)
- 如果 ref 不是 Reference,那么 this 的值为 undefined
const value = 1
const foo = {
value: 2,
bar() {
return this.value
},
}
// 示例 1
console.log(foo.bar())
// 示例 2
console.log(foo.bar())
// 示例 3
console.log((foo.bar = foo.bar)())
// 示例 4
console.log((false || foo.bar)())
// 示例 5
console.log((foo.bar, foo.bar)())示例 1 ( foo.bar() )
// 该表达式返回了⼀个 Reference 类型;该值为
const Reference = {
base: foo,
name: 'bar',
strict: false,
}
// 该值是 Reference 类型,那么 IsPropertyReference(ref) 的结果是多少呢?
// 前面我们已经铺垫了 IsPropertyReference 方法,如果 base value 是一个对象,结果返回 true。
// base value 为 foo,是一个对象,所以 IsPropertyReference(ref) 结果为 true。
// 这个时候我们就可以确定 this 的值了:
// this = GetBase(ref),
// GetBase 也已经铺垫了,获得 base value 值,这个例子中就是foo,所以 this 的值就是 foo ,示例1的结果就是 2!示例 2 ( (foo.bar)() )
- 实际上 () 并没有对 MemberExpression 进行计算,所以其实跟示例 1 的结果是一样的
示例 3 ( (foo.bar = foo.bar)() )
- 有赋值操作符, (foo.bar = foo.bar) 返回的值不是 Reference 类型
this为undefined,非严格模式下,this的值为undefined的时候,其值会被隐式转换为全局对象。
示例 4 ( (false || foo.bar)() )
(false || foo.bar)返回的不是Reference类型,this为undefined
示例 5 ( (foo.bar, foo.bar)() )
(foo.bar, foo.bar)返回的不是Reference类型,this为undefined
结果
const value = 1
const foo = {
value: 2,
bar() {
return this.value
},
}
// 示例1
console.log(foo.bar()) // 2
// 示例2
console.log(foo.bar()) // 2
// 示例3
console.log((foo.bar = foo.bar)()) // 1
// 示例4
console.log((false || foo.bar)()) // 1
// 示例5
console.log((foo.bar, foo.bar)()) // 1宏任务与微任务
执行栈清空 或者 执行完成一个宏任务 ,会 清空所有的微任务 ,再继续执行执行栈/宏任务
宏任务( macro task )
通常 由宿主环境(浏览器或 Node.js )触发 ,用于处理需要与外部环境交互的任务
setTimeoutsetIntervalsetImmediateMessageChannel- 异步 I/O 操作
- UI 渲染
微任务( micro task )
由 JavaScript 自身产生 ,用于处理需要立即响应的逻辑,如异步操作的后续处理。
Promise.thenasync/awaitqueueMicrotaskrequestAnimationFrameMutationObserverprocess.nextTick
特例
监听事件时,不同的事件回调函数的调用顺序不同。
- 如果是阻塞 DOM 的事件,如
click,回调也会同步被执行 - 如果是非阻塞式的事件,如
message,则回调会在触发事件后插入macro task addEventListener与onXXX定义的事件可以共存,先定义者先调用
console.log('before all') // 第 1 个输出
window.addEventListener('message', () => {
console.log('listen message') // 第 8 个输出
})
window.onmessage = () => {
console.log('onMessage') // 第 9 个输出
}
document.body.onclick = () => {
console.log('onClick') // 第 3 个输出
}
document.body.addEventListener('click', () => {
console.log('listen click') // 第 4 个输出
})
Promise.resolve().then(() => {
console.log('micro task run') // 第 6 个输出
})
setTimeout(() => {
console.log('macro task run') // 第 7 个输出
}, 0)
console.log('before trigger event') // 第 2 个输出
window.postMessage('')
document.body.click()
console.log('after trigger event') // 第 5 个输出