JavaScript 的单线程特性,看似限制了执行效率,却通过事件循环、Promise 等机制构建了独特的异步编程模型,让复杂的并发操作变得可控且清晰。
从线程的基础概念到 JS 中宏任务与微任务的优先级调度,核心逻辑始终围绕 “如何在有限的执行资源中,高效处理多任务”。
理解这些原理,不仅能帮助我们写出更符合运行机制的代码,更能深入领悟编程语言在设计时对性能与逻辑清晰性的平衡智慧。无论是多线程环境的资源共享,还是单线程下的异步协作,本质上都是对 “并发” 这一核心问题的不同解答。
本文将结合实例,先回顾同步与异步的核心概念,通过 Promise 代码解析异步执行流程,再深入原型链的工作机制,帮你夯实 JS 底层基础。
一、同步和异步的概念回顾
核心定义
- 同步:代码按顺序逐行执行,前一行未执行完毕,后一行无法开始,主线程会被阻塞直到当前任务完成。
- 异步:对于网络请求、定时器、文件读取等耗时任务,不会阻塞主线程执行。这些任务会被移交到浏览器对应模块(如定时器模块、网络模块)在后台处理,完成后通过回调通知主线程处理结果。
- 异步核心思想:不等待耗时操作完成,先继续执行后续代码,最大化利用主线程资源。
实例解析:Promise 异步执行流程
以下通过完整代码示例,拆解异步任务的执行逻辑:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS 异步执行示例</title>
</head>
<body>
<script>
// 创建 Promise 实例
const p = new Promise((resolve, reject) => {
console.log(111); // Promise 构造函数同步执行
// 异步任务:定时器(宏任务)
setTimeout(() => {
console.log(333);
reject('数据加载失败'); // 改变 Promise 状态为 rejected
}, 1000);
});
console.log(222);
console.log(p, '//////'); // 打印当前 Promise 实例状态
console.log(p.__proto__ === Promise.prototype); // 验证原型关系
console.log(p.__proto__); // 打印实例的原型对象
// 注册状态变更回调
p.then((data) => {
console.log(data); // 状态 resolved 时执行
}).catch(err => {
console.log(err); // 状态 rejected 时执行
}).finally(() => {
console.log('异步任务处理完毕'); // 无论状态如何都会执行
});
</script>
</body>
</html>
执行步骤拆解(结合 JS 运行机制)
- 同步执行 Promise 构造函数:
new Promise(executor)会立即调用传入的 executor 函数(同步执行),因此先输出111。 - 异步任务移交后台:遇到
setTimeout(宏任务),主线程不会等待,而是将其移交浏览器定时器模块处理,继续向下执行同步代码。 - 执行后续同步代码:依次输出
222;此时setTimeout尚未完成,Promise 状态仍为pending,因此打印Promise {<pending>} //////;后续两行验证原型关系的代码同步执行,输出true及Promise.prototype对象。 - 注册回调函数(不执行) :
.then()、.catch()、.finally()仅注册回调,不会立即执行,这些回调会被存储,等待 Promise 状态变更后触发。 - 异步任务完成,触发回调:1 秒后,定时器任务完成,其回调被推入宏任务队列;主线程空闲时执行该回调,输出
333,并调用reject('数据加载失败')将 Promise 状态改为rejected。 - 执行对应回调:状态变更后,触发
.catch()回调,输出数据加载失败;最后执行.finally()回调,输出异步任务处理完毕。
最终输出顺序
111
222
Promise {<pending>} //////
true
Promise.prototype 对象(包含 then、catch、finally 等方法)
333
数据加载失败
异步任务处理完毕
二、原型链的核心机制:JS 继承的底层逻辑
原型链是 JS 实现继承的核心机制,其本质是:对象在访问属性 / 方法时,若自身不存在,则会沿着 __proto__ 指向的原型对象逐层查找,直到找到目标或到达原型链终点(null) 。
以下通过两段代码,拆解原型链的工作原理:
示例代码
// 1. 构造函数与原型继承
function Student(name, grade) {
this.name = name;
this.grade = grade;
}
Student.prototype.category = '学生'; // 给构造函数的原型添加属性
let lin = new Student('林同学', 8);
console.log(lin.category); // 输出:'学生'
// 2. 手动修改 __proto__ 改变原型链
const teacher = {
name: '王老师',
hobbies: ['阅读', '备课', '教研']
};
lin.__proto__ = teacher; // 重新指向原型对象
console.log(lin.hobbies); // 输出:['阅读', '备课', '教研']
第一部分:构造函数与原型的默认继承
核心逻辑
-
Student是构造函数,通过new关键字创建实例lin时,lin的默认原型(lin.__proto__)会指向Student.prototype(构造函数的原型对象)。 -
当访问
lin.category时:- 先查找
lin自身属性,未找到category; - 沿着
__proto__查找Student.prototype,找到category: '学生'; - 返回该属性值,因此输出
'学生'。
- 先查找
第二部分:手动修改 __proto__ 重构原型链
核心逻辑
-
lin.__proto__ = teacher手动修改了实例的原型指向,此时lin的原型不再是Student.prototype,而是普通对象teacher。 -
当访问
lin.hobbies时:- 查找
lin自身属性,未找到hobbies; - 沿着
__proto__查找teacher对象,找到hobbies: ['阅读', '备课', '教研']; - 返回该属性值,因此输出
['阅读', '备课', '教研']。
- 查找
关键注意事项
-
__proto__的特性:__proto__是实例对象访问原型的内置属性,属于非标准规范但被主流浏览器广泛支持,其作用是搭建实例与原型对象之间的连接,构成原型链。 -
不推荐生产环境修改
__proto__:手动修改__proto__会破坏原型链的默认结构,导致继承关系混乱,同时会影响 JS 引擎的优化(如属性查找缓存),降低代码性能,还可能引发难以调试的问题。 -
更安全的继承方案:若需自定义继承关系,推荐使用
Object.create()(基于指定原型创建新对象)或 ES6class+extends语法(标准化继承方案),例如:// Object.create 方式 const studentWithTeacherProto = Object.create(teacher); studentWithTeacherProto.name = '林同学'; studentWithTeacherProto.grade = 8; // class extends 方式 class Teacher { constructor(name) { this.name = name; this.hobbies = ['阅读', '备课', '教研']; } } class Student extends Teacher { constructor(name, grade) { super(name); this.grade = grade; } }
总结
- 异步核心:JS 单线程通过 “同步执行主线程代码 + 异步任务后台处理 + 状态变更触发回调” 实现高效并发,Promise 则规范了异步任务的状态管理与回调注册逻辑。
- 原型链本质:对象属性查找的 “链式机制”,默认通过构造函数与
prototype关联,手动修改__proto__会改变这一关联关系,但需谨慎使用。 - 理解这两大底层机制,能帮助我们更精准地预判代码执行结果,规避常见坑点,同时为后续学习闭包、模块化、框架源码等内容打下坚实基础。