Promise、构造函数和原型链:理解 JavaScript 底层机制的三个入口

在学习 JavaScript 的过程中,Promise、构造函数和原型链是三个经常被混淆、也最容易被遗忘的概念。原因并不在于它们本身多么复杂,而在于它们分别对应了 JavaScript 中三类不同的底层机制:
- Promise 处理异步任务的结果流转;
- 构造函数负责对象的批量创建;
- 原型链决定对象属性和方法的查找规则。
如果只把它们当成孤立的定义去记忆,很快就会忘。更有效的方式,是从它们各自解决的问题出发:为什么需要它们,它们解决了什么问题,以及在代码执行时它们具体做了什么。
本文将围绕这三个问题,重新梳理 Promise、构造函数和原型链之间的关系。
我按重要程度讲:
1. Promise:解决“异步结果未来才回来”的问题
第一步:先理解同步和异步
同步代码是这样:
const a = 1;
const b = 2;
console.log(a + b);
代码从上往下执行,结果马上有。
但读取文件、请求接口、连接数据库这些操作不是马上完成的:
fs.readFile('./a.txt', 'utf-8', (err, data) => {
console.log(data);
});
这里的问题是:
console.log('开始');
fs.readFile('./a.txt', 'utf-8', (err, data) => {
console.log('文件内容', data);
});
console.log('结束');
输出顺序不是:
开始
文件内容
结束
而是:
开始
结束
文件内容
因为 readFile 是异步的。
第二步:Promise 本质是什么?
Promise 本质上是一个承诺对象:
现在没有结果,但未来会给你一个结果。
要么成功,要么失败。
它有三种状态:
pending 等待中
fulfilled 成功
rejected 失败
一旦从 pending 变成成功或失败,就不会再变。
第三步:最原始的 Promise 写法
const p = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve('成功结果');
} else {
reject('失败原因');
}
});
p.then((res) => {
console.log(res);
}).catch((err) => {
console.log(err);
});
这里你要抓住三个点:
new Promise(...)
创建一个 Promise 对象。
resolve()
表示成功。
reject()
表示失败。
第四步:Promise 主要是为了解决回调地狱
传统回调写法:
fs.readFile('./a.txt', 'utf-8', (err, data1) => {
fs.readFile('./b.txt', 'utf-8', (err, data2) => {
fs.readFile('./c.txt', 'utf-8', (err, data3) => {
console.log(data1, data2, data3);
});
});
});
这玩意嵌套一多就很恶心。
Promise 写法:
readA()
.then(data1 => readB())
.then(data2 => readC())
.then(data3 => {
console.log(data3);
})
.catch(err => {
console.log(err);
});
但现代 JS 更推荐:
async function main() {
try {
const data1 = await readA();
const data2 = await readB();
const data3 = await readC();
console.log(data1, data2, data3);
} catch (err) {
console.log(err);
}
}
第五步:你现在最该记住的 Promise 结论
你不需要死背 Promise 内部细节,先记住:
Promise = 用对象包装一个未来才会完成的异步结果。
await = 等这个 Promise 完成,然后拿到结果。
async = 声明这个函数里面可以用 await,并且这个函数本身会返回 Promise。
比如:
import fs from 'node:fs/promises';
async function main() {
const data = await fs.readFile('./a.txt', 'utf-8');
console.log(data);
}
main();
这段代码里面:
fs.readFile('./a.txt', 'utf-8')
返回的是 Promise。
await fs.readFile(...)
意思是:等文件读完,把结果给我。
2. 构造函数:用来批量创建同类对象
第一步:为什么需要构造函数?
比如你要创建很多用户:
const user1 = {
name: '张三',
age: 18,
sayHi() {
console.log('你好,我是' + this.name);
}
};
const user2 = {
name: '李四',
age: 20,
sayHi() {
console.log('你好,我是' + this.name);
}
};
问题是:重复代码太多。
所以 JS 需要一种“对象工厂”。
第二步:构造函数就是专门用来创建对象的函数
function User(name, age) {
this.name = name;
this.age = age;
}
const user1 = new User('张三', 18);
const user2 = new User('李四', 20);
console.log(user1);
console.log(user2);
输出类似:
User { name: '张三', age: 18 }
User { name: '李四', age: 20 }
第三步:new 到底干了什么?
这一句:
const user1 = new User('张三', 18);
底层大概做了四件事:
1. 创建一个空对象 {}
2. 把 this 指向这个新对象
3. 执行 User 函数,把 name 和 age 挂到 this 上
4. 返回这个新对象
所以:
function User(name, age) {
this.name = name;
this.age = age;
}
等价理解为:
const 新对象 = {};
新对象.name = name;
新对象.age = age;
return 新对象;
第四步:构造函数的关键特征
构造函数通常首字母大写:
function User() {}
function Person() {}
function Product() {}
调用时必须配合 new:
const u = new User();
如果不用 new:
const u = User('张三', 18);
那 this 就可能指向错误,甚至是 undefined。现代严格模式下会直接出问题。
第五步:现在更常见的是 class,但底层还是构造函数
现代写法:
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const user1 = new User('张三', 18);
这只是语法更舒服。
本质上 class 仍然是基于 JS 的原型机制。
3. 原型链:解决“方法复用”和“属性查找”的问题
这个最容易忘,因为它抽象。
先给你一句硬定义:
原型链 = JS 对象查找属性和方法时,沿着 __proto__ 一层一层往上找的机制。
但你先别背,直接看问题。
第一步:构造函数里直接写方法有什么问题?
function User(name, age) {
this.name = name;
this.age = age;
this.sayHi = function () {
console.log('你好,我是' + this.name);
};
}
const user1 = new User('张三', 18);
const user2 = new User('李四', 20);
表面没问题。
但实际上:
console.log(user1.sayHi === user2.sayHi);
结果是:
false
为什么?
因为每 new 一次,都会重新创建一个新的 sayHi 函数。
这很浪费。
第二步:所以方法应该放到 prototype 上
function User(name, age) {
this.name = name;
this.age = age;
}
User.prototype.sayHi = function () {
console.log('你好,我是' + this.name);
};
const user1 = new User('张三', 18);
const user2 = new User('李四', 20);
console.log(user1.sayHi === user2.sayHi);
结果是:
true
因为 sayHi 不是分别存在 user1 和 user2 身上,而是存在它们共同的原型对象上。
第三步:prototype 和 proto 的关系
这两个最容易混。
记住:
函数有 prototype
对象有 __proto__
对象的 __proto__ 指向构造函数的 prototype
看代码:
function User(name) {
this.name = name;
}
const user1 = new User('张三');
console.log(User.prototype);
console.log(user1.__proto__);
console.log(user1.__proto__ === User.prototype);
结果:
true
也就是说:
user1.__proto__ === User.prototype
第四步:对象查找属性的过程
function User(name) {
this.name = name;
}
User.prototype.sayHi = function () {
console.log('你好');
};
const user1 = new User('张三');
user1.sayHi();
执行 user1.sayHi() 时,JS 会这样找:
1. user1 自己身上有没有 sayHi?
2. 没有,就去 user1.__proto__ 上找
3. user1.__proto__ 指向 User.prototype
4. User.prototype 上有 sayHi
5. 执行它
这就是原型链。
第五步:原型链继续往上是什么?
user1.__proto__ === User.prototype
User.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
可以理解为:
user1
↓
User.prototype
↓
Object.prototype
↓
null
这条查找路径就是原型链。
4. 三者放在一起怎么理解?
这三个东西不是一个层级。理解时应从问题场景出发,而非孤立记忆定义,这样才能真正掌握其设计原理和应用场景。
Promise 是异步机制
解决:
结果现在没有,未来才有,怎么优雅处理?
比如:
const data = await fs.readFile('./a.txt', 'utf-8');
构造函数是对象创建机制
解决:
怎么批量创建结构相同的对象?
比如:
function User(name) {
this.name = name;
}
const u = new User('张三');
原型链是属性查找和方法复用机制
解决:
对象自己没有某个方法时,去哪里找?
多个对象如何共享方法?
比如:
User.prototype.sayHi = function () {
console.log(this.name);
};更多推荐
所有评论(0)