JavaScript 6种继承方式详解

1. 原型链继承

// 定义父类
function Parent() {
    this.name = 'Jack';
}
// 父类原型添加方法
Parent.prototype.getName = function () {
    return this.name;
};

// 子类
function Child() {}
// 子类的原型设置为父类Parent的实例
Child.prototype = new Parent();

// 实例化子类
const child = new Child();

console.log(child.getName()); // Jack

父类Parent有属性和方法,子类Child没有属性和方法。实现继承的关键是,子类Child没有使用默认原型,而是将其替换成了一个新的对象,这个对象恰好是Parent的实例,这样Child的实例就能从Parent的实例中继承属性和方法,而且还与Parent的原型挂上了钩。

ChildParent的关系如下如所示:

缺陷

(1)原型中包含引用值的时候,会在所有实例间共享

function SuperType() {
    this.colors = ['red', 'blue', 'green'];
}
function SubType() {}
// 继承 SuperType
SubType.prototype = new SuperType();

const instance1 = new SubType();
instance1.colors.push('black'); // 加入新元素
console.log(instance1.colors); // [ 'red', 'blue', 'green', 'black' ]

const instance2 = new SubType();
console.log(instance2.colors); // [ 'red', 'blue', 'green', 'black' ]

SubType的所有实例都会共享这个colors属性。这一点通过instance1.colors上的修改也能反映到instance2.colors上就可以看出来。

(2)子类型在实例化时不能给父类型的构造函数传参

由于以上两个原因,原型链继承基本不会单独使用。

2. 盗用构造函数

这种技术有时也称作对象伪装经典继承。在子类构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用 apply()call()方法以新创建的对象为上下文执行构造函数。

function SuperType() {
    this.colors = ['red', 'blue', 'green'];
}
function SubType() {
    // 继承 SuperType
    SuperType.call(this);
}
const instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // [ 'red', 'blue', 'green', 'black' ]

const instance2 = new SubType();
console.log(instance2.colors); // [ 'red', 'blue', 'green' ]

通过使用call()apply()方法,SuperType构造函数在为SubType的实例创建的新对象的上下文中执行了。这相当于新的SubType对象上运行了 SuperType()函数中的所有初始化代码。结果就是每个实例都会有自己的colors属性。

优势

相比于使用原型链盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参

function SuperType(name) {
    this.name = name;
}
function SubType() {
    // 继承 SuperType 并传参
    SuperType.call(this, 'Nicholas');
    // 实例属性
    this.age = 29;
}
const usr = new SubType();
console.log(usr.name, usr.age); // Nicholas 29

缺陷

(1)必须在构造函数中定义方法,因此函数不能重用,每次创建实例都会创建一次方法。

(2)子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式

盗用构造函数基本上也不能单独使用

3. 组合继承

组合继承,有时候也叫伪经典继承,综合了原型链盗用构造函数,将两者的优点集中了起来。使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
    console.log(this.name);
};

function SubType(name, age) {
    // 继承属性
    SuperType.call(this, name);
    this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function () {
    console.log(this.age);
};

const usr1 = new SubType('Nicholas', 29);
usr1.colors.push('black');
console.log(usr1.colors); // [ 'red', 'blue', 'green', 'black' ]
usr1.sayName(); // Nicholas
usr1.sayAge(); // 29

const usr2 = new SubType('Greg', 27);
console.log(usr2.colors); // [ 'red', 'blue', 'green' ]
usr2.sayName(); // Greg
usr2.sayAge(); // 27

组合继承弥补了原型链盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。而且组合继承也保留了instanceof操作符和isPrototypeOf()方法识别合成对象的能力。

4. 原型式继承

Object.create()方法接收两个参数:

  1. 作为新对象原型的对象
  2. 给新对象定义额外属性的对象(可选)

原型式继承适用于这种情况,有一个对象,想在它的基础上再创建一个新对象。需要把这个对象先传给Object.create(),然后再对返回的对象进行适当修改。本质上,Object.create()是对传入的对象执行了一次浅复制

const person = {
    name: 'Nicholas',
    friends: ['Shelby', 'Court', 'Van'],
};
const anotherPerson = Object.create(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');

const anotherPerson2 = Object.create(person);
anotherPerson2.name = 'Linda';
anotherPerson2.friends.push('Barbie');

console.log(person.friends); // [ 'Shelby', 'Court', 'Van', 'Rob', 'Barbie' ]

person.friends不仅是person的属性,也会跟anotherPersonanotherPerson2共享。这里实际上克隆了两个person

Object.create()的第二个参数,每个新增属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性

const person = {
    name: 'Nicholas',
    friends: ['Shelby', 'Court', 'Van'],
};
const anotherPerson = Object.create(person, {
    name: {
        value: 'Greg',
    },
});
anotherPerson.name = 'Jack';

console.log(anotherPerson.name); // Greg

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。

5. 寄生式继承

与原型式继承比较接近的一种继承方式是寄生式继承

function createAnother(original) {
    const clone = Object.create(original); // 通过调用函数创建一个新对象
    clone.sayHi = function () {
        // 以某种方式增强这个对象
        console.log('hi');
    };
    return clone; // 返回这个对象
}

const person = {
    name: 'Nicholas',
    friends: ['Shelby', 'Court', 'Van'],
};
const anotherPerson = createAnother(person);

anotherPerson.sayHi(); // hi
console.log(anotherPerson.name); // Nicholas
console.log(anotherPerson.friends); // [ 'Shelby', 'Court', 'Van' ]

这个例子基于person对象返回了一个新对象。新返回的anotherPerson对象具有person的所有属性方法,还有一个新方法sayHi()

寄生式继承同样适合主要关注对象,而不在乎类型构造函数的场景。Object.create()函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用。

6. 寄生式组合继承

组合继承存在效率问题:父类构造函数始终会被调用两次,一次在是创建子类原型时调用,另一次是在子类构造函数中调用。

寄生式组合继承的基本模式如下所示:

function inheritPrototype(subType, superType) {
    let prototype = object(superType.prototype); // 创建对象
    prototype.constructor = subType; // 增强对象
    subType.prototype = prototype; // 赋值对象
}

inheritPrototype()函数实现了寄生式组合继承的核心逻辑。函数接收两个参数:子类构造函数父类构造函数。在这个函数内部,第一步是创建父类原型的副本。然后,给返回的prototype对象设置constructor属性,解决由于重写原型导致默认constructor丢失的问题。最后将新创建的对象赋值给子类型的原型

function inheritPrototype(subType, superType) {
    let prototype = Object.create(superType.prototype); // 创建对象
    prototype.constructor = subType; // 增强对象
    subType.prototype = prototype; // 赋值对象
}

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
    console.log(this.name);
};
function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function () {
    console.log(this.age);
};

const usr = new SubType('Jack', 18);
usr.sayName(); // Jack
usr.sayAge(); // 18
console.log(usr.colors); // [ 'red', 'blue', 'green' ]

这里只调用了一次SuperType构造函数,避免了SubType.prototype上不必要也用不到的属性, 因此可以说这个例子的效率更高。而且,原型链仍然保持不变,因此instanceof操作符和isPrototypeOf()方法正常有效。

寄生式组合继承可以算是引用类型继承最佳模式

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
JavaScript 中,继承是一个非常常见的概念。ES6 引入了 Class 语法糖,让继承更加易于理解和实现。在 Class 中,我们可以使用 extends 关键字来创建一个子类,使其继承父类的属性和方法。 下面我们来详细了解一下如何在 JavaScript 中使用 extends 实现继承。 ### 基础语法 首先,我们需要定义一个父类。在 ES6 中,我们可以使用 Class 来定义一个类。例如: ```javascript class Animal { constructor(name) { this.name = name; } speak() { console.log(this.name + ' makes a noise.'); } } ``` 这个 Animal 类有一个构造函数和一个 speak 方法。构造函数会在创建实例时被调用,而 speak 方法则可以让动物发出一些声音。 接下来,我们来创建一个子类。使用 extends 关键字来创建子类,并使用 super() 方法调用父类的构造函数。例如: ```javascript class Dog extends Animal { constructor(name) { super(name); } speak() { console.log(this.name + ' barks.'); } } ``` 这个 Dog 类继承了 Animal 类,并覆盖了其 speak 方法。在构造函数中,我们通过 super() 方法来调用父类的构造函数,并将传递的参数传递给它。 现在,我们可以创建一个 Dog 的实例,并调用其 speak 方法: ```javascript let d = new Dog('Mitzie'); d.speak(); // Mitzie barks. ``` ### 继承父类的方法 当一个子类继承了一个父类时,它会继承父类的属性和方法。例如,在上面的例子中,Dog 类继承了 Animal 类,因此它继承了 Animal 类的 speak 方法。 当我们调用子类的方法时,如果子类没有实现该方法,它会自动调用父类的方法。例如,在上面的例子中,如果我们不覆盖 Dog 类的 speak 方法,它将调用 Animal 类的 speak 方法。 ### 覆盖父类的方法 如果一个子类需要覆盖父类的方法,我们可以在子类中重新定义该方法。例如,在 Dog 类中,我们覆盖了 Animal 类的 speak 方法,使其输出“barks”而不是“makes a noise”。 ### 调用父类的方法 有时候,我们需要在子类中调用父类的方法。我们可以使用 super 关键字来调用父类的方法。例如,在 Dog 类中,我们可以通过调用 super.speak() 来调用 Animal 类的 speak 方法。 ### 总结 继承是一个非常常见的概念,也是面向对象编程中的重要概念之一。在 JavaScript 中,我们可以使用 extends 关键字来实现继承。通过继承,子类可以继承父类的属性和方法,也可以覆盖父类的方法,并且可以调用父类的方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

火星飞鸟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值