JavaScript底层原理解析与面试深度知识点
作者:宁风NF
第十四阶段:底层原理与面试深度知识点
- JS 执行上下文、执行栈、变量对象 VO、活动对象 AO
执行上下文:
- 是什么:执行上下文是 JavaScript 执行一段代码时的运行环境,它定义了变量、函数声明和 this 值的作用域。有三种类型:全局执行上下文、函数执行上下文和 eval 执行上下文(较少用)。
- 为什么:它为代码的执行提供了一个独立的环境,确保变量和函数在正确的作用域内访问和执行,避免不同部分代码之间的干扰。
- 怎么工作:当 JavaScript 引擎开始执行代码时,首先创建全局执行上下文并压入执行栈。每当调用一个函数,就会为该函数创建一个新的执行上下文并压入栈顶,函数执行完毕后,其执行上下文从栈顶弹出。
执行栈:
- 是什么:也叫调用栈,是一种后进先出(LIFO)的数据结构,用于存储执行上下文。
- 为什么:它管理着代码的执行顺序,保证函数调用和返回的正确流程。
- 怎么工作:函数调用时,其执行上下文被压入栈顶,成为当前正在执行的上下文。函数执行结束,该上下文从栈顶弹出,控制权返回给之前的上下文。
变量对象 VO:
- 是什么:在全局执行上下文中,变量对象就是全局对象(在浏览器中是
window)。在函数执行上下文中,变量对象是一个包含函数参数、函数声明和变量声明的对象。 - 为什么:它存储了当前执行上下文中的所有变量和函数声明,方便在执行过程中查找和访问。
- 怎么工作:在函数执行上下文创建阶段,会扫描函数的参数、函数声明和变量声明,将它们添加到变量对象中。变量声明会被提升,值为
undefined,函数声明会被完整提升。
活动对象 AO:
- 是什么:活动对象是函数执行上下文中的变量对象,当函数进入执行阶段,变量对象被激活,就成为活动对象。
- 为什么:在执行阶段,活动对象用于存储函数执行过程中的实际数据,与创建阶段的变量对象有所不同,它包含了函数实际执行时的参数值等。
- 怎么工作:在函数执行上下文中,创建阶段的变量对象在进入执行阶段后成为活动对象,此时函数参数被赋值,变量开始实际参与运算。
- 作用域链创建过程
- 是什么:作用域链是一个由多个作用域对象组成的链表,用于在查找变量和函数时确定访问顺序。
- 为什么:它决定了在何处查找变量和函数,保证了代码在不同作用域下的正确访问。
- 怎么创建:
- 每个执行上下文都有一个与之关联的作用域链。
- 函数执行上下文的作用域链,其最前端是该函数的活动对象(AO),如果存在闭包,还会包含父函数的活动对象,依次向上直到全局执行上下文的变量对象(VO)。例如:
function outer() {
let outerVar = 'outer';
function inner() {
let innerVar = 'inner';
console.log(outerVar);
}
inner();
}
outer();在 inner 函数执行时,其作用域链为 inner 函数的 AO-> outer 函数的 AO-> 全局 VO。当查找 outerVar 时,先在 inner 函数的 AO 中找,没找到则到 outer 函数的 AO 中找,最终找到。
- 原型链完整查找流程
- 是什么:原型链是通过原型对象将对象连接起来的链状结构,用于实现继承和属性查找。
- 为什么:它使得对象可以访问其原型对象上的属性和方法,实现代码复用和继承机制。
- 怎么查找:
- 当访问一个对象的属性或方法时,首先在该对象自身的属性中查找。
- 如果没找到,就到其原型对象(
__proto__)中查找。 - 如果原型对象中也没找到,就继续到原型对象的原型对象中查找,以此类推,直到找到或到达
Object.prototype,如果Object.prototype中也没有,则返回undefined。例如:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
return `${this.name} makes a sound.`;
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
const myDog = new Dog('Buddy', 'Golden Retriever');
console.log(myDog.speak()); 这里 myDog 对象查找 speak 方法,先在自身找,没找到,通过 __proto__ 到 Dog.prototype 找,还没找到,再通过 Dog.prototype.__proto__ 到 Animal.prototype 找到。
- Event Loop 完整执行流程(浏览器 + Node 对比)
浏览器中的 Event Loop:
- 是什么:浏览器的 Event Loop 用于协调事件、用户交互、脚本执行、渲染和网络请求等。
- 为什么:由于 JavaScript 是单线程的,Event Loop 机制使得 JavaScript 可以处理异步操作,不会阻塞主线程。
- 怎么执行:
- 执行栈(Call Stack):同步任务在此执行,当函数调用时,函数压入栈顶,执行完毕后弹出。
- 任务队列(Task Queue,也叫宏任务队列):异步操作(如
setTimeout、setInterval、DOM 事件等)完成后,其回调函数会被放入任务队列。 - 微任务队列(Microtask Queue):一些异步操作(如
Promise.then、MutationObserver等)的回调会被放入微任务队列。 - 流程:首先执行栈中的同步任务,当执行栈为空时,检查微任务队列,依次执行微任务队列中的任务,直到微任务队列为空。然后从任务队列中取出一个宏任务放入执行栈执行,执行完后再次检查微任务队列并执行,如此循环。
Node 中的 Event Loop:
- 是什么:Node.js 的 Event Loop 同样用于处理异步操作,但内部实现和浏览器略有不同。
- 为什么:为 Node.js 提供异步 I/O 等功能,使其能高效处理大量并发请求。
- 怎么执行:
- Node 的 Event Loop 有多个阶段,如
timers(处理setTimeout和setInterval)、I/O callbacks(处理大部分 I/O 回调)、idle, prepare(系统内部使用)、poll(获取新的 I/O 事件)、check(执行setImmediate回调)、close callbacks(处理关闭的回调,如socket.on('close',...))。 - 流程:进入
timers阶段,检查是否有到期的定时器,执行其回调。然后进入I/O callbacks阶段,执行一些系统操作的回调。接着进入poll阶段,等待新的 I/O 事件,若有则执行其回调。如果poll队列为空且有setImmediate回调,则进入check阶段执行setImmediate回调。最后进入close callbacks阶段处理关闭相关回调。在每个阶段执行完后,都会执行微任务队列中的任务。
- Node 的 Event Loop 有多个阶段,如
对比:
- 浏览器的 Event Loop 相对简单,主要围绕 DOM 相关事件和网络请求等。Node 的 Event Loop 更复杂,需要处理各种 I/O 操作和系统调用。
- 在 Node 中,
setImmediate和process.nextTick是特殊的异步操作。process.nextTick的回调会在当前阶段所有同步任务和微任务执行完毕后立即执行,优先级高于setImmediate。而setImmediate的回调会在poll阶段空闲时或I/O操作完成后执行。
- 宏任务微任务优先级细节
- 宏任务:常见的宏任务有
setTimeout、setInterval、setImmediate(Node 环境)、I/O操作、UI rendering(浏览器环境)等。宏任务在一个事件循环中,当执行栈为空且微任务队列为空时,从宏任务队列中取一个任务放入执行栈执行。 - 微任务:常见的微任务有
Promise.then、MutationObserver(浏览器环境)、process.nextTick(Node 环境)等。微任务在执行栈为空时立即执行,即在当前宏任务执行完后,下一个宏任务执行前,会先执行微任务队列中的所有任务。所以微任务优先级高于宏任务。例如:
console.log('同步任务');
setTimeout(() => {
console.log('宏任务 setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('微任务 Promise.then');
});
输出顺序为:“同步任务”-> “微任务 Promise.then”-> “宏任务 setTimeout”。
- 函数提升、变量提升底层原理
函数提升:
- 是什么:在 JavaScript 代码执行前,函数声明会被提升到其所在作用域的顶部,使得在函数声明之前就可以调用该函数。
- 为什么:这是 JavaScript 引擎为了方便函数调用而采用的机制,让开发者在代码组织上更灵活。
- 底层原理:在执行上下文创建阶段,函数声明会被完整地添加到变量对象(VO)或活动对象(AO)中,而不仅仅是声明提升,函数体也会被提升。例如:
func();
function func() {
console.log('函数被调用');
}
这里 func 函数在声明之前被调用,因为函数声明被提升到了作用域顶部。
变量提升:
- 是什么:变量声明会被提升到其所在作用域的顶部,但变量的值不会被提升,即变量在声明前访问,值为
undefined。 - 为什么:同样是为了在代码执行前预先处理变量声明,保证代码执行逻辑的一致性。
- 底层原理:在执行上下文创建阶段,变量声明会被添加到变量对象(VO)或活动对象(AO)中,初始值设为
undefined。例如:
console.log(a); var a = 10;
这里会输出 undefined,因为 var a 被提升,但 a = 10 不会提升。
- Promise 微任务调度机制
- 是什么:Promise 是 JavaScript 中处理异步操作的一种方式,其
then方法的回调会被放入微任务队列。 - 为什么:这样可以保证在当前宏任务执行完后,以异步且优先于下一个宏任务的方式执行
then回调,使得异步操作的处理更有序和高效。 - 怎么调度:
- 当
Promise被resolve或reject时,then方法注册的回调函数不会立即执行,而是被放入微任务队列。 - 当执行栈为空时,JavaScript 引擎会检查微任务队列,并依次执行其中的回调函数。例如:
- 当
console.log('同步任务');
Promise.resolve().then(() => {
console.log('Promise 微任务');
});
console.log('同步任务');
输出顺序为:“同步任务”-> “同步任务”-> “Promise 微任务”。
- 类与原型底层等价转换(class 本质是语法糖)
- 是什么:ES6 的
class语法是基于原型继承的语法糖,它让 JavaScript 的面向对象编程更接近传统面向对象语言的写法,但底层仍然是基于原型的机制。 - 为什么:
class语法提供了更简洁、直观的面向对象编程方式,同时保持了 JavaScript 基于原型继承的核心特性,方便开发者理解和编写面向对象代码。 - 底层等价转换:例如:
// class 写法
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound.`;
}
}
// 等价的原型写法
function AnimalProto(name) {
this.name = name;
}
AnimalProto.prototype.speak = function () {
return `${this.name} makes a sound.`;
};这里 class Animal 本质上就是对 AnimalProto 函数及其原型操作的一种语法封装,class 的 constructor 对应函数的参数和内部赋值,class 的方法定义对应在原型上添加方法。
- 可迭代协议、迭代器协议底层规范
可迭代协议:
- 是什么:可迭代协议定义了对象如何成为可迭代对象。一个对象要成为可迭代对象,必须实现
Symbol.iterator方法,该方法返回一个迭代器对象。例如:
const myArray = [1, 2, 3]; const iterator = myArray[Symbol.iterator](); console.log(iterator.next()); console.log(iterator.next()); console.log(iterator.next()); console.log(iterator.next());
这里数组是可迭代对象,因为它实现了 Symbol.iterator 方法。
- 为什么:可迭代协议使得对象可以在
for...of循环、Array.from等场景中使用,统一了对象的遍历方式。
迭代器协议:
- 是什么:迭代器协议定义了迭代器对象的行为。迭代器对象必须有一个
next方法,该方法返回一个包含value和done属性的对象。value是迭代的当前值,done是一个布尔值,表示迭代是否结束。 - 为什么:它为对象的迭代提供了一种标准的接口,使得不同类型的对象都能以一致的方式进行遍历。
- 模块化加载机制(ES Module 与 CommonJS 差异)
加载时机:
- ES Module:静态加载,在编译阶段就确定模块的依赖关系和导入导出。这使得可以进行一些编译时优化,如 tree- shaking(去除未使用的代码)。例如:
import { func } from './module.js';
// 这里在编译时就确定了依赖关系
- CommonJS:动态加载,在运行时才确定模块的依赖关系和导出。例如:
const module = require('./module.js');
// 这里在运行时才加载模块
导出方式:
- ES Module:有默认导出(
export default)和具名导出(export)。例如:
// 具名导出
export const func1 = () => console.log('Func1');
// 默认导出
export default () => console.log('Default export');
- CommonJS:通过
module.exports或exports导出。例如:
function func1() {
console.log('Func1');
}
exports.func1 = func1;
// 或者
module.exports = {
func1: function () {
console.log('Func1');
}
};导入方式:
- ES Module:使用
import导入。例如:
import { func1 } from './module.js';
import myDefault from './module.js';
- CommonJS:使用
require导入。例如:
const { func1 } = require('./module.js');
const myModule = require('./module.js');
循环依赖处理:
- ES Module:在静态分析阶段就处理循环依赖,对于已经加载但未完全求值的模块,会先使用其部分值,等模块完全求值后再更新。
- CommonJS:如果遇到循环依赖,在加载模块时,会先返回一个未完全初始化的模块对象,可能导致获取到的值不正确。例如:
// a.js
const b = require('./b.js');
console.log('a.js:', b.value);
exports.value = 'a 的值';
// b.js
const a = require('./a.js');
console.log('b.js:', a.value);
exports.value = 'b 的值';
在 CommonJS 中,a.js 中打印 b.value 可能得到 undefined,而 ES Module 对这种情况有更合理的处理方式。
实战练习:
- 混合多层 Promise、定时器、同步代码,手动推导打印顺序
- 将 class 语法手动转换为原生构造函数 + 原型代码
思考题:
- 变量提升底层原理是什么?
- Promise.then 为什么属于微任务?
- ES Module 加载是同步还是异步?
到此这篇关于JavaScript底层原理解析与面试深度知识点的文章就介绍到这了,更多相关js底层原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
