js常见继承方法详解

一、核心机制:原型链

在 JS 中,每个对象都有一个内部属性 [[Prototype]](在浏览器中通常通过__proto__访问),指向它的原型对象。当我们访问一个对象的属性或方法时,如果该对象自身没有,JS 引擎就会沿着__proto__一直往上找,直到找到为止(如果找到 null 还没找到,则返回 undefined)。这条链式查找结构,就是原型链。

关键点区分:

prototype:这是函数特有的属性(构造函数才有),指向该函数构造出的实例对象的原型。

__proto__:这是实例对象特有的属性,指向构造函数的 prototype。

constructor:每个原型对象都有一个 constructor 属性,指向关联的构造函数。

二、继承的六种实现方式

1.原型链继承

复制代码
function Parent() {
  this.colors = ['red', 'blue'];
}
function Child() {}
Child.prototype = new Parent(); // 核心

const c1 = new Child();
c1.colors.push('green');
const c2 = new Child();
console.log(c2.colors); // ['red', 'blue', 'green'] ------ 大问题:引用类型被共享!
注解

1.Child.prototype = new Parent();

这行代码是核心,它相当于通过让Child的原型等于父亲的一个实例。让二者的原型链产生链接,Child的实例可以通过原型链访问到Parent上的属性和方法。

2.引用类型被共享的原因

当c1想要操作colors时,它自身没有,就沿着原型链找,在Parent找到并进行操作,这个操作影响的是原型链上的数据,c2访问colors,自身没有,也是沿着原型链找,最终和c1找到的一样,所以c1的操作影响了c2.

2.借用构造函数继承

复制代码
function Parent(name) {
  this.name = name;
  this.colors = ['red'];
}
function Child(name) {
  Parent.call(this, name); // 核心:借用父类构造函数
}

const c1 = new Child('c1');
c1.colors.push('blue');
const c2 = new Child('c2');
console.log(c2.colors); // ['red'] ------ 解决了共享问题
console.log(c2.name); // 'c2'
注解

1.Parent.call(this, name);

这行代码让Parent的this指向变成Child的this指向,也就是Child的实例对象。所以在执行Parent函数时,像this.name = name;这样借助this进行的赋值相当于再给Child的实例对象赋值,让Child的实例对象可以获得Parent内部所有显示挂载在this上的属性。

2.缺点

这样的继承原型链是缺失的,执行完Parent.call(this, name),两者就没关系了,Child访问不到Parent原型链上的属性和方法。并且Child的实例只能继承Parent内部所有显示挂载在this上的属性,像let hobby = 'sing'这样的属性他无法继承。

3.组合继承

复制代码
function Parent(name) {
  this.name = name;
  this.colors = ['red'];
}
Parent.prototype.sayName = function() { console.log(this.name); };

function Child(name, age) {
  Parent.call(this, name); // 第二次调用 Parent
  this.age = age;
}
Child.prototype = new Parent(); // 第一次调用 Parent
Child.prototype.constructor = Child; // 修复构造函数指向
Child.prototype.sayAge = function() { console.log(this.age); };

const c = new Child('Tom', 18);
c.colors.push('blue');
console.log(c.colors); // ['red', 'blue']
c.sayName(); // 'Tom'
注解

1.核心逻辑

通过两次调用Parent实现继承,第一次Child.prototype = new Parent();,让Child实例可以通过原型链访问到Parent上的属性和方法。第二次Parent.call(this, name);把所有挂载到this上的属性和方法直接写在实例身上,遇到引用类型直接在自身就能找到,不用在原型链上找,防止共享。

2.Child.prototype.constructor = Child

执行完Child.prototype = new Parent(),会让Child的构造函数指向Parent,及Child.prototype.constructor =Parent,所以需要修复构造函数指向。

3.优缺点

这个方法结合了前两种方法的优点,但如果引用类型的数据没有被挂载在this上,它依然会被共享。并且它调用了两次Parent,会产生两个Parent实例,造成冗余。

4.寄生组合继承

复制代码
function Parent(name) {
  this.name = name;
}
Parent.prototype.sayName = function() { console.log(this.name); };

function Child(name, age) {
  Parent.call(this, name); // 只调用一次
  this.age = age;
}

// 核心:用一个空对象作为媒介,指向 Parent.prototype
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child; // 修复构造函数

const c = new Child('Tom', 18);
c.sayName(); // 'Tom'
注解

1.Child.prototype = Object.create(Parent.prototype);

这段代码利用Object.create创造一个空对象,并让空对象的prototype指向Parent.prototype,Child.prototype的又指向了空对象的prototype,借此完成原型链的链接,相比较上一个方法,他减少了Parent的调用,没有造成冗余。

5. 原型式继承(Object.create)

复制代码
const parent = { name: 'parent', colors: ['red'] };
const child1 = Object.create(parent);
const child2 = Object.create(parent);
child1.colors.push('blue');
console.log(child2.colors); // ['red', 'blue'] ------ 引用类型依然共享
注解

这种方法主要还是通过对Object.create的使用完成原型链的链接,完成继承,代码简单,但共享问题依然存在。

6.class继承

复制代码
class Parent {
  constructor(name) {
    this.name = name;
    this.colors = ['red'];
  }
  // 定义在原型上
  sayName() {
    console.log(this.name);
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 必须调用,相当于 Parent.call(this, name)
    this.age = age;
  }
  sayAge() {
    console.log(this.age);
  }
}

const c = new Child('Tom', 18);
c.colors.push('blue');
c.sayName(); // 'Tom'
注解

1.class Child extends Parent

这句话让Child实例不仅可以调用Parent构造函数上的属性和方法,还可以调用Parent上面的属性和方法。并且将this标记为未初始化。

2.两个constructor函数

Parent上的constructor主要作用是给Parent的实力挂载上自己的专属属性,另一个constructor主要作用是先将Parent的属性挂载到Child实例身上,然后再挂载上自己的专属属性,并且必须先执行super(name); ,然后才能挂载上自己的专属属性,否则会因为this没有初始化而报错。

ctor函数 Parent上的constructor主要作用是给Parent的实力挂载上自己的专属属性,另一个constructor主要作用是先将Parent的属性挂载到Child实例身上,然后再挂载上自己的专属属性,并且必须先执行super(name); `,然后才能挂载上自己的专属属性,否则会因为this没有初始化而报错。

相关推荐
Lsx_5 分钟前
微信支付接入全流程:JSAPI支付、小程序支付与H5支付
前端·javascript·全栈
OpenTiny社区23 分钟前
React 正式登场!GenUI SDK v1.4.0 三框架渲染、出码与 Agent 生态重磅升级~
前端·ai编程
试图让你心动32 分钟前
【无标题】
前端·javascript·vue.js
Hilaku35 分钟前
为什么每个前端都应该拥有一个自己的独立博客?
前端·javascript·程序员
燕落南枝38 分钟前
CSS 后代选择器与子代选择器:从入门到实战
前端·css
Amos_Web41 分钟前
Rspack 源码解析(二十八):HtmlRspackPlugin 如何把模板变成带资源标签的最终产物
前端
马可家的菠萝44 分钟前
个人知识库怎么建?从一个正在学的技术主题开始
前端·笔记·知识管理·个人知识库
几何心凉44 分钟前
端到端具身交互智能正式上线:让 AI 通过屏幕和机器人,真正走进现实世界
前端·人工智能·实时互动·机器人·交互
木圭的AI时代指南1 小时前
实测 MiniMax M3.1 Flash:同一道 Three.js 大题,两个 agent 差出 4.6 倍
开发语言·javascript·ecmascript
西红柿炖牛腩3541 小时前
3D 模型压缩脚本又报错又配环境?换个不用维护的方案
javascript·3d·性能优化