JS实现继承的几种方式

  • 1.原型链法继承(使用原型)

基本思想:利用原型让一个引用类型继承另外一个引用类型的方法和实例。

我的理解是: 将父类的实例作为子类的原型

function Father() {
    this.qwe = 'abc'
    this.info = {
        name: 'whl',
        age: 22
    }
}

Father.prototype.getInfo = function () {
    console.log(this.info);
}

function Son(a, b) {
    this.a = a
    this.b = b
}
// 继承Father
// Son的原型对象prototype是Father的实例,其内部__proto__指向的是Father的原型对象
Son.prototype = new Father()
// 将这个对象的 constructor 手动改成 Son,否则还是Father
Son.prototype.constructor = Son
// 不使用对象字面量方式创建原型方法,会重写原型链
Son.prototype.showInfo = function () {
    return this.a + this.b
}
let Son1 = new Son(2, 4)
Son1.getInfo() // { name: 'whl,age: 22 }
console.log(Son1.showInfo()); // 6
// 通过hasOwnProperty() 方法来确定自身属性和其原型的属性
console.log(Son1.hasOwnProperty('qwe')); // false
console.log(Son1.hasOwnProperty('a')); // true
console.log(Son1.hasOwnProperty('b')); // true
// 通过isPrototypeOf() 方法来确定原型和实例的关系
console.log(Father.prototype.isPrototypeOf(Son1)); // true
console.log(Son.prototype.isPrototypeOf(Son1)); // true
console.log(Object.prototype.isPrototypeOf(Son1)); // true

优点

  • 父类的方法可以复用

缺点

  • 父类的所有引用属性会被所有子类,更改一个子类的引用属性,其他子类也会受到影响
function Father(){ 
  this.test = [1,2,3,4];
}
function Son(a,b){
  this.a = a;
  this.b = b;
}
Father.prototype = new Son();
let instanceOne = new Son();
let instanceTwo = new Son();
instanceOne.test.push(5);
console.log(instanceTwo.test); // [1, 2, 3, 4, 5]
  • 没有办法在不影响所有对象实例的情况下,给父类的构造函数传递参数

  • 2.构造函数法

此方法可以解决原型中引用类型被修改的问题

function staff() {
    this.test = [1, 2, 3];
}
staff.prototype.companyName = function () {
    return this.test;
}

function Father() {
    this.test = [1, 2, 3]
}
Father.prototype.getTest = function () {
    console.log(this.test);
}

function Son(a, b) {
    Father.call(this)
    this.a = a
    this.b = b
}
// 不使用对象字面量方式创建原型方法,会重写原型链
Son.prototype.showTest = function () {
    console.log(this.a + this.b);
}
let Son1 = new Son(1, 2)
let Son2 = new Son(2, 3)
Son1.test.push(4)
console.log(Son1.test); // [1,2,3,4]
console.log(Son2.test); // [1,2,3]
// console.log(Son1.getTest()); // 报错
// // 通过 hasOwnProperty() 方法来确定自身属性与其原型属性
console.log(Son1.hasOwnProperty('test')); // true
// 通过 isPrototypeOf() 方法来确定原型和实例的关系
console.log(Father.prototype.isPrototypeOf(Son1)); // false

从上面的结果可以看出:

  • 1.借用构造函数法可以解决原型中引用类型被修改的问题
  • 2.但是Son1和Father已经没有原型链关系了
  • ​ 通过使用call()apply()方法,Father构造函数在为Son的实例创建的新对象的上下文执行了,就相当于新的Son实例对象上运行了Father()函数中的所有初始化代码,结果就是每个实例都有自己的test属性。

优点:

  • 1.可以传递参数
function Father(name) {
    this.info = { name: name };
}
function Son(name) {
    //继承自Father,并传参
    Father.call(this, name);
    
     //实例属性
    this.age = 22
}

let Son1 = new Son("whl");
console.log(Son1.info.name); // "whl"
console.log(Son1.age); // 22

let Son2 = new Son("qwe");
console.log(Son2.info.name); // "qwe"
console.log(Son2.age); // 22

缺点:

  • 1.只能继承父类对象的实例属性和方法,不能继承父类对象原型的属性和方法
  • 2.无法实现函数复用,每个子对象都有父对象实例,性能较差

  • 3.组合继承

组合继承是将原型链和构造函数结合,发挥二者之长的一种继承模式。

思路:使用原型链实现对原型属性和方法的继承,而通过构造函数实现对实例属性的继承。

function Father(num) {
    this.numOne = num;
    this.numList = [1, 2];
}
Father.prototype.getNumOne = function () {
    return this.numOne;
};

function Son(num1, num2) {
    // 第二次调用了Father(),实例对象继承了Father的两个属性,会屏蔽掉原型Son.prototype中的两个同名属性
    Father.call(this, num1);
    this.numTwo = num2;
}
// 第一次调用了Father(),继承了Father的原型方法, Son.prototype会得到Father的两个属性
Son.prototype = new Father();
Son.prototype.constructor = Son;
Son.prototype.getNumTwo = function () {
    return this.numTwo;
}
var Son1 = new Son(3, 5);
Son1.numList.push(6);
console.log(Son1.numList); // [1, 2, 6]
console.log(Son1.getNumOne()); // 3
console.log(Son1.getNumTwo());; // 5

var Son2 = new Son(4, 6);
Son2.numList.push(7);
console.log(Son2.numList); // [1, 2, 7]
console.log(Son2.getNumOne()); // 4
console.log(Son2.getNumTwo()); // 6
console.log(Son1.hasOwnProperty('numOne')); // true
console.log(Father.prototype.isPrototypeOf(Son1)); // true

优点

  • 可以复用原型上定义的方法
  • 可以保证每个函数都有自己的属性,可以解决原型中引用类型值被修改的问题

缺点

  • Father会被调用两次,第一次是Son.prototype = new Son(),第二次是调用Father.call(this)

  • 4.原型式继承

思想:必须有一个对象作为另外一个对象的基础,然后把这个对象传给object函数,再根据具体需求,修改得到的对象,object函数如下:

function object(obj) {
    function F() {}
    F.prototype = obj
    return new F()
}

由上可以看出返回的对象是构造函数F的实例,其内置对象__proto__指向F的原型对象,而F.prototype = o,即返回的对象的__proto__指向传入的对象o

Object.create()这个方法与object函数的行为相同此方法接受两个参数

  • 一是用作新对象原型的对象
  • 二十一个为新对象定义额外属性的对象,这个参数是可选项\

基本模式

var person = {
    name: 'whl',
    array: [1, 2, 3]
}
var person1 = Object.create(person)
person1.array.push(4)
console.log(person1.name); // whl
console.log(person1.array); // [1,2,3,4]

var person2 = Object.create(person, {
    name: {
        value: 'qwe'
    }
})
person2.array.push(5)
console.log(person2.name); // qwe
// 对于引用类型的值的属性会共享
console.log(person1.array); // [1,2,3,4,5]
console.log(person2.array); // [1,2,3,4,5]

原型式继承的特点:

  • Object.create()有兼容问题,IE9以下不支持。
  • 对于包含引用类型值的属性始终都会共享相应的值

  • 5.寄生式继承

寄生式继承可以看作是原型式继承的变式,它通过包覆函数来增强实例对象,为实例对象添加更多的功能。

var person = {
    name: 'whl',
    array: [1, 2, 3]
}

function createObj(origin) {
    const clone = Object.create(origin)
    clone.sayName = function () {
        console.log(this.name); // whl
    }
    return clone
}

const person1 = createObj(person)
person1.sayName()

缺点:

  • 使用寄生式函数来对对象添加函数,无法做到函数复用
  • 原型中引用类型的值也会被修改

  • 6.寄生式组合继承

它主要是为了解决组合继承的效率问题。组合继承当中父类的构造函数始终会被调用两次

function Father(name) {
    this.name = name
    this.array = [1, 2, 3]
}
Father.prototype.getArray = function () {
    console.log(this.name);
}

function Son(name, age) {
    Father.call(this, name)
    this.age = age
}
Son.prototype = new Father()

let Son1 = new Son('whl', 24)
console.log(Son1);

为了解决这个问题,我们可以使用寄生式继承的思想来替代掉父类构造函数第一次调用生成实例对象的操作。我们先来创建一个包装函数,它会使用父类的原型对象来创建一个新对象,然后将这个新对象关联到子类的原型对象上,这样父类构造函数就不用调用两次。

function inheritPrototype(Son, Father) {
    const prototype = Object.create(Father.prototype) // 创建对象:父类型原型的副本
    prototype.constuctor = Son // 增强对象:为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性
    Son.prototype = prototype
}

function Father(name) {
    this.name = name
    this.test = [1, 2, 3]
}

Father.prototype.sayName = function () {
    console.log(this.name);
}

function Son(name, age) {
    Father.call(this, name)
    this.age = age
}

// 将父类原型指向字类
inheritPrototype(Son, Father)
const Son1 = new Son('qwe', 22)
const Son2 = new Son('aaa', 23)
Son1.test.push(4)
console.log(Son1.test); // [1,2,3,4]
console.log(Son2.test); // [1,2,3]
Son1.sayName() // qwe
Son2.sayName() // aaa
console.log(Son1.hasOwnProperty('test')); // true
console.log(Father.prototype.isPrototypeOf(Son1)); // true

组合寄生式继承,只调用了一次父类的构造函数,避免了在子类型原型对象prototype上创建不必要的、多余的属性,同时保持原型链不变,是引用类型最理想的继承范式。


  • 7.ES6的继承实现方式通过extends关键字来实现继承
class One {
    constructor(name, age) {
        this.name = name
        this.age = age
        this.jn = 'shuijiao'
    }
    getInfo() {
        return `我叫:${this.name},今年${this.age}岁了,我喜欢${this.jn}`
    }
}

// // Two这个类通过extends关键字,继承了One这个类的所有属性和方法。
class Two extends One {
    constructor(x, y, gender) {
        // 调用父类的constructor
        super(x, y)
        this.gender = gender
    }
    getTwoInfo() {
        // 调用父类原型上的方法 getInfo(),父类的属性会被子类继承
        return `${super.getInfo()},喜欢${this.jn},我是${this.gender}`
    }
}
const two = new Two('whl', 22, 'boy')
console.log(two.getTwoInfo());

在这里插入图片描述

  1. 1.子类必须在constructor方法中调用super方法,否则新建实例时会报错
  2. 2.子类型自己的this对象,必须先通过调用父类的构造函数来塑造,得到和父类实例同样的属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。
  3. 3.只有super方法才能调用父类实例,不调用super(),子类就没有自己的this对象。只有调用super之后,才可以使用this关键字,否则会报错。

总结:

  • 1.ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的属性和方法添加到子类实例对象的this上面(Father.call(this))。
  • . 2.ES6 的继承机制完全不同,实质是先创建父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this实现继承。
  • . 3.大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。
  • . 4.Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

1.子类的__proto__属性,表示构造函数的继承,总是指向父类。
2.子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。


ES5中的继承:
  • 原型链继承: 缺点主要有无法向父类构造函数传递参数和原型对象上引用类型篡改导致实例对象互相影响。
  • 构造函数继承: 解决了原型链继承的弊端,缺点在于无法共享父类原型对象的方法。
  • 组合继承:将原型链继承和盗用构造函数相结合解决了前两者的问题,缺点在于父类构造函数会执行两次影响效率
  • 原型式继承: 可以直接通过包装函数将实例和源对象关联起来,但是存在源对象上引用类型篡改导致实例对象互相影响的问题。
  • 寄生式继承: 原型式继承的变式,通过再包装来添加增强实例的功能。
  • 寄生式组合继承: 是寄生式继承和组合继承的结合体,目前最佳的继承实践模式。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值