掌握原型链,写出不翻车的 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 如何安全构建原型链,了解其他构建方法有何不妥,为我们写出健壮的继承结构添一把力

相关推荐
文阿花9 分钟前
Echarts实现自定旋转3D饼状图
javascript·3d·echarts·饼状图
meilindehuzi_a43 分钟前
深入理解 JavaScript 的同步与异步机制:从单线程设计到 Promise 核心应用
开发语言·javascript·ecmascript
如烟花的信页1 小时前
加速乐cookie逆向分析
javascript·爬虫·python·js逆向
永远的WEB小白1 小时前
css改变svg图标的颜色
前端·javascript·css
ikoala1 小时前
Codex 不得不装的 12 个插件,都在这了
前端·javascript·后端
赵庆明老师2 小时前
JS检查提交的文件是否合规
开发语言·前端·javascript
颂love2 小时前
Vue的两大生态以及组件通信
前端·javascript·vue.js·typescript
光影少年2 小时前
js单线程,为什在node环境下的js可以处理高并发请求?
前端·javascript·掘金·金石计划
moMo3 小时前
# JavaScript 的“等等我”:聊聊同步与异步
javascript
Cobyte3 小时前
19.Vue Vapor 的实现原理原来这么简单
前端·javascript·vue.js