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没有初始化而报错。

相关推荐
qq_3168377530 分钟前
阿里云百炼 fun-asr-realtime uniapp vue3 调用
javascript·vue.js·uni-app
庄园特聘拆椅狂魔1 小时前
Java 后端转全栈的第一课:从前端项目搭建到技术选
java·开发语言·前端
triumph_passion1 小时前
前端路由策略:Memory、Hash 与 History
前端
李剑一1 小时前
AntDesign老版本没有月份和年份区间选择组件?我封装了一个完美版,附源码!
前端·vue.js
牧艺1 小时前
浏览器端 3D 看车重磅升级:从三款展车到可试驾的八车展厅
前端·three.js·交互设计
老男孩游戏时光1 小时前
Electron 桌面应用开发入门,前端也能写客户端
前端
默_笙1 小时前
🚊 别再让 AI 的记忆写两遍了:PostgreSQL 把关系查询和语义检索装进一张表
前端·javascript
进击的明明1 小时前
TypeScript速通笔记(下)
前端·面试·typescript
lzhdim1 小时前
C#不为人知的10个魔法特性:资深开发者也会震惊的底层奥秘
java·前端·javascript·算法·c#
zach2 小时前
前端落地实战:Next.js 项目 Nginx + PM2 生产部署全流程(附踩坑总结)
前端·pm2·next.js