面试官翘着二郎腿,眼神淡淡扫了我一眼:
“简历上写你精通 JavaScript?”
“是的。”
“那……讲讲原型链。”
我脑袋嗡地一下,什么
__proto__
、prototype
、constructor
、Object.create(null)
,在我脑中排着队开始游行……这一刻我明白了,原型链,不仅是 JS 的灵魂,更是前端面试的照妖镜。
🎯 原型链到底是个啥?
一句话理解:
原型链是一种对象查找机制,JavaScript 借此实现继承。
通俗解释:
- 每个对象都有一个内部属性
__proto__
,它指向该对象的原型; - 原型对象本身也有自己的
__proto__
; - 这样层层向上,最终以
null
结束,形成一条链——原型链。
当我们访问一个对象的属性时,如果找不到,就会沿着原型链一直往上找,直到找到或走到尽头。
一图看懂原型链查找流程:
用个简单栗子演示查找
const a = {};
a.toString()
- 检查
a
对象自身是否有toString
属性 → 没有。 - 检查
a.__proto__
(即Object.prototype
)是否有toString
属性 → 有。 - 执行
Object.prototype.toString()
,返回"[object Object]"
。
🧱 prototype 和 proto 有什么区别?
这是个经典问题,搞不清这两个,一开口就原形毕露。
名字 | 类型 | 属于谁 | 作用 |
---|---|---|---|
__proto__ | 属性 | 所有对象 | 指向构造函数的 prototype,形成原型链 |
prototype | 属性 | 构造函数 | 创建实例时赋值给实例的 __proto__ |
一句话记住:
__proto__
是对象的,prototype
是函数的。
再看看 构造函数、原型对象、实例对象 三者关系:
捋清楚这三者关系后,就好理解下面的查找规则了。
🧠 原型链的查找流程
我们访问对象属性时,会按照以下顺序查找:
对象本身 -> __proto__ -> __proto__.__proto__ -> ... -> Object.prototype -> null
如果中途找到了属性,就停止查找。
示例:
const grandpa = { state: 'smile' };
const father = Object.create(grandpa);
const son = Object.create(father);
console.log(son.state); // smile
这是链式查找过程:
son.state -> father.state -> grandpa.state => 找到了!
注意:只有 读取 属性时才会走原型链,赋值不会!
son.state = 'sad';
console.log(son.state); // 'sad'
console.log(grandpa.state); // 'smile',没变
赋值会在对象本身创建一个新属性,而不会影响原型链上的同名属性。
🧬 JS 如何通过原型链实现继承
原型链不仅是属性查找机制,更是 JS 实现继承的基础。
原型链继承
它的原理很简单,让子类的原型指向父类的实例对象,子类查找属性方法的时候,自身没有则往父类原型上去查找,形成原型链查找。
举个栗子:
// 父构造函数
function Animal() {
this.species = '动物';
}
Animal.prototype.eat = function() {
console.log(`${this.species}在进食`);
};
// 子构造函数
function Dog() {
this.breed = '哈士奇'; // 新增特定属性
}
// 实现原型链继承
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog; // 修正constructor指向
// 创建Dog的实例
let myDog = new Dog();
myDog.eat(); // 输出:动物在进食
console.log(myDog.breed); // 输出:哈士奇
简单快捷的就实现了个继承,但是,它有几个严重缺点:
- 所有子类实例共享父类属性(尤其是引用类型)
- 无法给父类构造函数传参
我们把 species
改为引用类型看下:
// 父构造函数
function Animal() {
this.species = ['动物'];
}
// 创建Dog的实例
let myDog1 = new Dog();
let myDog2 = new Dog();
myDog1.species[0] = '柴犬';
console.log(myDog1.species[0]); // 输出:柴犬
console.log(myDog2.species[0]); // 输出:柴犬
这肯定不是我们想要的结果,那么有哪种继承可以解决这个问题?
优化方案:寄生组合式继承
结合两种方式:
- 用构造函数继承父类属性(解决共享问题)
- 用
Object.create
实现原型链继承(复用原型方法)
// 父构造函数
function Animal(species) {
this.species = species;
}
Animal.prototype.eat = function() {
console.log(`${this.species}在进食`);
};
// 子构造函数
function Dog(species) {
Animal.call(this, species); // 继承父类实例属性
this.breed = '哈士奇'; // 新增特定属性
}
// 创建父类原型的副本作为子类原型
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修正constructor指向
// 测试
const myDog = new Dog('犬科');
myDog.eat(); // 输出:犬科在进食
console.log(myDog.breed); // 输出:哈士奇
console.log(myDog instanceof Dog); // true
console.log(myDog instanceof Animal); // true
对比原来的原型链继承,有以下优点:
- 属性独立:通过
Animal.call(this)
,避免了原型链继承中共享属性的问题。 - 原型方法复用:通过
Object.create(Animal.prototype)
复用父类原型方法,保持了原型链的优势。 - 效率更高:只创建一次父类原型的副本,而不是每次创建子类实例时都调用父类构造函数。
由原型链引出继承的话题,从此不再被面试官牵着鼻子走~
🤔 为什么前端必须搞懂原型链?
你可能觉得原型链太底层,用不到,其实它无处不在:
- instanceof 原理?靠原型链判断
- Object.create?构建原型链
- new 操作符?设置对象的
__proto__
- class 的继承?原型链继承的语法糖
- 手写深拷贝、节流、防抖?很多都跟原型链相关
所以搞懂原型链,你会发现 JS 世界清晰很多,接下来我们手动实现下
instanceof
跟new
,彻底理解原型链。
🧪 手写 instanceof
的底层实现
instanceof
的作用就是判断对象的原型链上是否能找到构造函数的 prototype,也是比较常用的一个方法了。
让我们一起来手写一个简化版的 instanceof
,透彻理解它的本质:
function myInstanceOf(obj, Constructor) {
// 判断基本类型,直接返回 false
if (typeof obj !== 'object' || obj === null) return false;
let proto = Object.getPrototypeOf(obj); // 等价于 obj.__proto__
const prototype = Constructor.prototype;
// 循环查找原型链
while (proto) {
if (proto === prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
这里做了:
instanceof
操作符主要用于判断对象类型,基本类型和null
没有原型链,所以直接返回false
。- 获取传入参数的原型链进行遍历对比
- 通过原型链查找,一层一层原型对象进行对比,直到找到或者
null
验证一下:
function Person() {}
const p = new Person();
console.log(myInstanceOf(p, Person)); // true
console.log(myInstanceOf(p, Object)); // true
console.log(myInstanceOf(p, Array)); // false
instanceof
是原型链在 JS 中非常典型的应用场景,它不会检查对象“有没有继承方法”,而是通过原型路径的存在性判断继承关系。
🛠️ new
操作符的执行流程
你是否也曾好奇,JS 中 new
操作符背后到底做了哪些事?
来看下面这个常见写法:
function Person(name) {
this.name = name;
}
const p = new Person('Tom');
这背后发生了什么?下面就来为你解析:
手写 new
的核心逻辑
function myNew(Constructor, ...args) {
// 1. 创建一个空对象,继承构造函数的原型
const obj = Object.create(Constructor.prototype);
// 2. 将构造函数的 this 指向这个新对象
const result = Constructor.apply(obj, args);
// 3. 如果构造函数返回对象,就用它,否则用我们创建的对象
return result !== null && (typeof result === 'object' || typeof result === 'function')
? result
: obj;
}
如果构造函数返回的是对象类型,就直接返回它;否则返回我们用 Object.create
创建的那个新对象。
使用示例
function Person(name) {
this.name = name;
}
const p = myNew(Person, 'Tom');
console.log(p.name); // Tom
console.log(p instanceof Person); // true
new
的背后,其实是原型链的构建 + 构造函数 this 的绑定 + 返回值的处理。理解new
的底层机制,不仅能搞清构造过程,还能让你手写继承时思路更清晰。
🧩 总结
- 原型链是 JS 的对象继承机制。
__proto__
是对象的,prototype
是函数的。- 原型链查找属性时会层层向上,直到 null。
- 构造函数 + 原型链 = JS 的继承套路。
- 真实开发中理解原型链,有助于搞懂框架、设计模式、源码实现。
如果你觉得这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬 让我知道你在看!
后续我也会持续输出更多 前端打怪笔记系列文章,敬请期待!❤️