JavaScript对象继承常用详解

28 阅读4分钟

JavaScript对象继承常用详解

一、概念: JS继承是让** 一个对象共享另一个对象的属性和方法的核心机制,JS是基于原型(proptotype)的继承,和传统类继承不同。**

原型链核心,JS每个对象都有一个__propto属性,指向它的原型对象,函数有proptotype属性,实例对象的propto__指向构造函数的proptotype, 继承的本质是让子类的原型指向父类的实例/原型

5种常见的继承方式

1、原型链继承(基础)

核心: 子类原型=父类实例

// 父类
function Parent() {
    this.name = '父类';
    this.arr=[1,2,3]; // 引用类型属性
}
Parent.prototype.say = function () {
    console.log('我是来自父类的name ' + this.name);
}

// 子类 
function  Child() {}
// 核心:子类原型继承父类实例
Child.prototype = new Parent();

const child1 = new Child();
const child2 = new Child();
child1.say(); // 我是父类 父类

child1.arr.push(4);
console.log(child1.arr);   // [1, 2, 3, 4]
console.log(child2.arr);   // [1, 2, 3, 4]   // 子类实例共享父类实例的引用类型属性

缺点:

  • 引用类型属性(数组/对象)会被所有子类实例共享,修改一个实例,所有实例都会变化,如修改child1.arr,push一个值4,child2.arr也会变更,
  • 无法向父类构造函数传值

2、借用构造函数继承,解决第1方法的缺点

核心:在子类构造函数中,用call() 调用父类构造函数

function Parent(name) {
    this.name = name;
    this.arr = [1, 2, 3];
}

Parent.prototype.say = function () {    
    console.log('我是来自父类的方法 say');
}

function Child(name) {
    Parent.call(this, name); // 通过 call 方法调用父类构造函数,传递子类实例作为 this
    this.age = 18; // 子类特有属性
}
const child1 = new Child('子类1');
const child2 = new Child('子类2');
// 独立修改,不影响其他实例
child1.arr.push(4);
console.log(child1.arr); // [1, 2, 3, 4]  // 子类实例共享父类实例的引用类型属性,
console.log(child2.arr); // [1, 2, 3]  // 子类实例共享父类实例的引用类型属性,
console.log(child1.name,child2.name); // 子类1 子类2

console.log(child1.say); // undefined  // 子类实例无法访问父类原型上的方法
console.log(child2.say); // undefined  // 子类实例无法访问父类原型上的方法

缺点:

  • 只能继承父类实例属性,无法继承父类原型上的方法
  • 方法会重复创建,浪费内存

3、组合继承(最常用经典方案)

核心:原型链继承+借用构造函数继承,取长补短, 解决第2中方法的缺点

function Parent(name) {
    this.name = name;
    this.arr = [1, 2, 3];
}

Parent.prototype.say = function () {
    console.log('我是来自父类的方法 say ' + this.name);
}

function Child(name, age) {
    //1. 借用构造函数:继承实例属性+传参
    Parent.call(this, name); 
    this.age = age; // 子类特有属性
}
//2. 原型链继承:继承原型方法
Child.prototype = new Parent(); 
Child.prototype.constructor = Child; // 修正子类原型的 constructor 属性
const child1 = new Child('子类1', 18);
const child2 = new Child('子类2', 20);
child1.say(); //
console.log(child1.name, child1.age); // 子类1 18
child1.arr.push(4);
console.log(child1.arr); // [1, 2, 3, 4]  // 子类实例共享父类实例的引用类型属性,    

console.log(child2.arr); // [1, 2, 3]  // 子类实例共享父类实例的引用类型属性,独立属性,child1修改不影响child2

优点:

  • 支持传参
  • 引用类型(对象/数据)属性不共享
  • 方法复用,不浪费内存

缺点:

  • 父类构造函数会执行两次,new Parent()一次, call() 一次

4、寄生组合式继承(JS最佳实践)

核心:解决组合式继承父类构造函数执行两次的问题

function Parent(name) {
    this.name = name;
    this.arr = [1, 2, 3];
}
Parent.prototype.say = function () {
    console.log('我是来自父类的方法 say ' + this.name);
}
function Child(name, age) {
    Parent.call(this, name); // 仅执行一次父类构造函数,传递子类实例作为 this
    this.age = age; 
}   
// 核心:创造父类原型的副本,不执行父类构造函数
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;    
const child1 = new Child('子类1', 18);;
child1.say(); // 我是来自父类的方法 say 子类1
console.log(child1.name, child1.age);// 子类1 18

优点:

  • 父类构造函数只执行一次
  • 完美支持传参、方法复用、属性独立
  • ES6 class继承的底层实现原

5、ES6 class继承

核心:extends + super() ,语法简洁,底层还是寄生组合式继承

// 父类
class Parent {
    constructor(name) {
        this.name = name;
        this.arr = [1, 2, 3];
    }
    say() {
        console.log('我是来自父类的方法 say ' + this.name);
    }   
}   

// 子类继承父类
class child extends Parent { 
    constructor(name, age) {
        super(name);    // 调用父类构造函数,传递 name 参数,必须在this之前调用super()
        this.age = age; // 子类特有属性
    }
}
const child1 = new child('子类1', 18);
child1.say(); // 我是来自父类的方法 say 子类1
console.log(child1.name, child1.age); // 子类1 18

总结

  • 日常开发:直接使用ES6 class + extends,最简单、最规范
  • 面试手写:了解继承继承,掌握 寄生组合式继承。
  • 了解历程:可以更好的理解为什么需要更好继承写法,就是解决过去的缺点。