面试官冷笑一声:原型链怎么实现继承?你答得出来吗?

面试官翘着二郎腿,眼神淡淡扫了我一眼:

“简历上写你精通 JavaScript?”

“是的。”

“那……讲讲原型链。”

我脑袋嗡地一下,什么 __proto__prototypeconstructorObject.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 世界清晰很多,接下来我们手动实现下 instanceofnew,彻底理解原型链。

🧪 手写 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 的继承套路。
  • 真实开发中理解原型链,有助于搞懂框架、设计模式、源码实现。

如果你觉得这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬 让我知道你在看!
后续我也会持续输出更多 前端打怪笔记系列文章,敬请期待!❤️

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值