掌握原型链,写出不翻车的 JS 继承

原型与原型链基础

在学习之前需要回顾一下这些基础知识

  • prototype所有函数 都包含的一个属性(对象),而对于内置构造函数通常在上面预定义了部分方法,例如:.push.toString等。
  • __proto__所有 JS 对象都有的一个内部属性,指向该对象的原型对象(即父对象的prototype)。
  • constructor 每个 prototype 对象 都有一个默认的 constructor 属性,指回其构造函数。

不妨来看个例子:

js 复制代码
// 构造函数
function Person(name) {
  this.name = name;
} 

const alice = new Person('Alice');
console.log(alice.__proto__) // Person.prototype
console.log(Person.prototype.constructor) // Person

而它的原型链就是:

bash 复制代码
// --> 代表.__proto__属性
alice --> Person.prototype --> Object.prototype --> null(所有原型链的终点都是 null)

四种原型继承方式详解

1. 直接赋值父类原型(不推荐)

先来看一个例子:

js 复制代码
// 父类构造函数
function Animal(name, age) {
    this.name = name;
    this.age = age;
}
Animal.prototype.species = "动物";

function Cat(color, name, age) {
    // 继承父类的属性
    Animal.apply(this, [name, age]);
    // 使用 .call 也可以
    // Animal.call(this, name, age);
    this.color = color;
}
        
Cat.prototype = Animal.prototype; // 指向父类原型

补充一下 callapply 的区别:

  • call逐个传参 ,即:fn.call(this, arg1, arg2, ...)
  • apply数组传参 ,即:fn.apply(this, [arg1, arg2, ...])

这样做下来感觉并没有什么不合适,继承了父类的属性,同时也指向了父类的原型对象。但是这样并不完整,因为如果调用子类的prototype上的constructor属性,正确的继承应该是指向子类自身。

而当我们在代码中执行console.log(Cat.prototype.constructor)最后得到的结果却是 Animal

所以在最后还需要手动修复构造函数指向,即添加:

js 复制代码
Cat.prototype.constructor = Cat;

但是这样做并非万无一失,在这里我们需要了解 JS 的一个特性,那就是 引用式赋值 。在 JS 中,基本数据类型(8种)是按值赋值 的,而对象类型是按引用赋值

引用式赋值:指当我将一个对象赋值给另一个变量时,并不是复制了这个对象本身,而是复制了对象在内存中的地址引用,这样就导致两个变量都指向同一个内存位置,不论修改哪个都会对另一个造成影响。

举个最简单的例子:

js 复制代码
let obj1 = { name: 'Alice' };
let obj2 = obj1;      // 引用式赋值
obj2.name = 'Bob';

console.log(obj1.name); // "Bob"
console.log(obj1 === obj2); // true(指向同一对象)

回到我们的继承函数,里面就有一个是引用式赋值

js 复制代码
Cat.prototype = Animal.prototype;

这就导致了当我们在Cat.prototype上添加方法还是什么的,会污染Animal.prototype,所以尽量别使用直接赋值父类原型

2. 原型链继承(有点缺点)

我们将上面的例子拿下来

js 复制代码
function Animal(name, age) {
    this.name = name;
    this.age = age;
}
Animal.prototype.species = "动物";

function Cat(color, name, age) {
    Animal.apply(this, [name, age]);
    this.color = color;
}

但是我们这里使用原型链式继承

js 复制代码
Cat.prototype = new Animal(); 
Cat.prototype.constructor = Cat; // 修复构造函数指向     

而这里需要了解一下 new 的伪代码了

js 复制代码
// 伪代码 new Animal()
let obj = {};
Animal.call(obj); // 也可以用 apply
obj.__proto__ = Animal.prototype;
return obj

首先创建一个空对象,再将父类的this指向空对象,并将空对象的__proto__指向父类的prototype,也就是连上原型链,最后再返还这个空对象。

但是需要注意的是,这里后续创建的所有实例都是共享父类的属性的,在任意一个实例中对父类属性进行修改都会对其他实例造成影响,例如:

js 复制代码
function Animal(name) {
  this.name = name;
  this.colors = ['red', 'blue']; // 引用类型属性
}

function Cat() {}
Cat.prototype = new Animal(); // 所有 Cat 实例共享 colors
Cat.prototype.constructor = Cat;

const cat1 = new Cat();
const cat2 = new Cat();
cat1.colors.push('green');
console.log(cat2.colors); // ['red', 'blue', 'green'] 共享引用

3. 空对象中介模式(经典解决方案)

在直接赋值中,不论怎样都会对父类造成影响,那么如果我们在 父类和子类 中间找一个中介来隔断,是不是就能解决这个问题,而这也是我们最经典的解决方法----空对象中介模式

依旧将前面的例子拿来:

js 复制代码
function Animal(name, age) {
    this.name = name;
    this.age = age;
}
Animal.prototype.species = "动物";

function Cat(color, name, age) {
    Animal.apply(this, [name, age]);
    this.color = color;
}

不妨来看看中介模式是怎么使用的

js 复制代码
var F = function() {}; // 空对象中介
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;

其中我们将F.prototype直接继承Animal.prototype,虽然会导致 引用式赋值,但是只要我对Cat.prototype修改不对F造成影响,那么间接对Animal就没有影响。

而最精妙一点就是Cat.prototype = new F();这步,我们根据之前的伪代码可以知道,这步是将Cat.prototype.__proto__ = F.prototype,也就在变相变成Cat.prototype.__proto__ = Animal.prototype

那么即使我们对Cat.prototype本身进行重新赋值,或者添加任何其他属性也不会影响Cat.prototype.__proto__,除非我们显示修改它(或者对修改F.prototype

拓展:

当然我们也可以将其写成继承函数(extend),这也算手写题吧 QwQ

js 复制代码
function extend(Parent, Child) {
    // 中介函数
    var F = function() {}; // 函数表达式(有内存开销,但是因为是空函数问题不大)
    // 指向父类原型
    F.prototype = Parent.prototype;
    // 指向空对象实例
    Child.prototype = new F(); // 实例的修改不会影响原型对象
    // 修复构造函数指向
    Child.prototype.constructor = Child; 
}

extend(Animal, Cat)

4. Object.create()(ES5 推荐方式)

js 复制代码
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

Animal.prototype 为原型创建新对象,在不污染父类构造函数的前提下,更安全地建立子类到父类的原型链连接,并且更加适配现代继承写法。

总结

继承方式 是否推荐 说明
直接赋值原型 污染父类 constructor
原型链继承 引用属性共享问题
中介函数 ✅(兼容旧环境) 安全隔离
Object.create() ✅✅ 现代标准,语义清晰

最佳实践:

  1. 属性继承 → 用 Parent.call(this, ...args)
  2. 方法继承 → 用 Child.prototype = Object.create(Parent.prototype)
  3. 修复 constructor → 显式设置 Child.prototype.constructor = Child

原型继承是 JS 的灵魂。理解 call/apply ,掌握 Object.create 如何安全构建原型链,了解其他构建方法有何不妥,为我们写出健壮的继承结构添一把力

相关推荐
我笔记3 小时前
vue 子父调用
前端·javascript·vue.js
2401_860319523 小时前
在React Native鸿蒙跨平台开发中实现一个冒泡排序算法并将其应用于数据排序,如何进行复制数组以避免直接修改状态中的数组
javascript·算法·react native·react.js·harmonyos
毕设源码-朱学姐3 小时前
【开题答辩全过程】以 基于vue.js的校园二手平台为例,包含答辩的问题和答案
前端·javascript·vue.js
m0_471199633 小时前
【JavaScript】Set 和 Map 核心区别与实战用法(ES6 集合全解析)
前端·javascript·es6
小白|4 小时前
【OpenHarmony × Flutter】混合开发性能攻坚:如何将内存占用降低 40%?Flutter 引擎复用 + ArkTS 资源回收实战指南
开发语言·javascript·flutter
和和和4 小时前
React Scheduler为何采用MessageChannel调度?
前端·javascript
momo061174 小时前
用一篇文章带你手写Vue中的reactive响应式
javascript·vue.js
他是龙5514 小时前
第29天:安全开发-JS应用&DOM树&加密编码库&断点调试&逆向分析&元素属性操作
开发语言·javascript·安全
和和和4 小时前
前端应该知道的浏览器知识
前端·javascript