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

相关推荐
miss34 分钟前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan1 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真1 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment1 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan1 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么2 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模
上海懒图科技2 小时前
工厂厂区电子地图定位导航:厂区人车精准定位,安全合规双保障
前端·arcgis·vue
西安小哥2 小时前
端侧跑大模型,真正的门槛不在参数量:一次 596 秒到 10 秒的压缩路径复盘
前端·人工智能
陈随易3 小时前
Cloudflare 出钱,Vite 团队挑人,44 个维护者分 30 万美元
前端·后端·程序员
恋猫de小郭3 小时前
Flutter THLC 的 WebView 滚动终于从 70 FPS 提升到 120 FPS
android·前端·flutter