首页 话题 小组 问答 好文 用户 我的社区 域名交易

[教程]js继承您了解多少呢

发布于 2025-03-16 18:22:39
0
10

实现继承的方式有很多,下面我们来写常用的几种(包括但不限于原型链继承、构造函数继承、组合继承、寄生组合继承、ES6继承):原型链继承原型链继承通过修改子类的原型为父类的实例,从而实现子类可以访问到父类...

实现继承的方式有很多,下面我们来写常用的几种(包括但不限于原型链继承、构造函数继承、组合继承、寄生组合继承、ES6继承):

原型链继承

原型链继承通过修改子类的原型为父类的实例,从而实现子类可以访问到父类构造函数以及原型上的属性或者方法。

// 原型链继承
function Parent () {
   this.name = 'kobe'
}

Parent.prototype.getName = function () {
    return this.name
}

Parent.prototype.info = {
    age: 20,
    
}

function Child() {}

Child.prototype = new Parent() 
let child = new Child()
console.log('child----', child)  // Child {}
var child2 = new Child()
console.log('child.info === child2.info', child.info === child2.info) // true
child.getName() // kobe
child2.getName() // kobe
child.info.age = 20
console.log('child1.info.age---', child.info.age) // 20
console.log('child2.info.age---', child2.info.age) // 20 我们很惊奇的发现child2的age也被改掉了

上面的例子是修改的原型对象上的引用数据类型的属性,如果改成下面这样,就不会有影响

// 原型链继承
function Parent () {
   this.name = 'kobe'
}

Parent.prototype.getName = function () {
    return this.name
}

Parent.prototype.info = {
    age: 20,
}

function Child() {}

Child.prototype = new Parent() 
let child = new Child()
console.log('child----', child)  // Child {}
var child2 = new Child()
console.log('child.info === child2.info', child.info === child2.info) // true
child.getName() // kobe
child2.getName() // kobe
child.info = {
    age: 18,
    num: 24
}
console.log('afterchild === child2', child.info === child2.info) // false
console.log('child1.info---', child.info) // 20 {age: 18, num: 24}
console.log('child2.info---', child2.info) // {age: 20}

因为我们刚开始child的info是引用类型,存的是相同的地址,后面直接给info了一个新对象,相当于生成了一个新对象的地址,两个info此时指向了不同的地址,互不干扰了

优点

实现逻辑简单

缺点

父类构造函数中的引用类型(比如对象/数组),会被所有子类实例共享。其中一个子类实例进行修改,会导致所有其他子类实例的这个属性值都会改变

构造函数继承

构造函数继承其实就是通过修改父类构造函数this实现的继承。我们在子类构造函数中执行父类构造函数,同时修改父类构造函数的this为子类的this。

我们直接看如何实现:

function Parent(name) {
  this.name = [name]
}

function Child(name) {
    Parent.call(this, name)
}

let child = new Child('kobe')
child.name.push('jordan')

var child2 = new Child('james')
console.log('child---', child.name) // ['kobe', 'jordan']
console.log('child2---', child2.name) // ['james'] 属性互不影响,但是方法是能各写各的,方法不通用

优点

解决了原型链继承中构造函数引用类型共享的问题,同时可以向构造函数传参(通过call传参)

缺点

所有方法都定义在构造函数中,每次都需要重新创建,方法无法复用(对比原型链继承的方式,方法直接写在原型上,子类创建时不需要重新创建方法)

所以为了解决原型链继承和构造函数继承的问题,我们决定把二者优点合一

组合继承

同时结合原型链继承、构造函数继承就是组合继承了。

function Parent () {
    this.name='kobe'
}

Parent.prototype.getName = function () {
    return this.name    
}

function Child () {
    Parent.call(this)
    this.num = 24
}

Child.prototype = new Parent() 
Child.prototype.constructor = Child
let child = new Child()
console.log(child) // {name: 'kobe', num: 24}
console.log('Child.prototype.__proto__ = Parent.prototype', Child.prototype.__proto__ === Parent.prototype) // true

此时child为:

图片alt


可以看到它的对象上有name属性,原型对象上也有name属性

优点

同时解决了构造函数引用类型的问题,同时解决了方法无法共享的问题

缺点

父类构造函数被调用了两次(第一次是new Parent(), 第二次是Parent.call(this))。同时子类实例以及子类原型对象上都会存在name属性。虽然根据原型链机制,并不会访问到原型对象上的同名属性,但总归是不美。

寄生组合继承

寄生组合继承其实就是在组合继承的基础上,解决了父类构造函数调用两次的问题。我们来看下如何解决的:
第一种写法:

function Parent () {
  this.name = 'kobe'
}

Parent.prototype.getName = function () {
  return this.name
}
function Child () {
  Parent.call(this)
  this.num = 24
}

clone(Child, Parent)
function clone (Child, Parent) {
  Child.prototype = Object.create(Parent.prototype)
  Child.prototype.constructor = Child
}

let child  = new Child()
console.log('child-----', child)

第二种写法:

function Parent () {
  this.name = 'kobe'
}

Parent.prototype.getName = function () {
  return this.name
}
function Child () {
  Parent.call(this)
  this.num = 24
}

clone(Child, Parent)

// 这个函数的作用可以理解为复制了一份父类的原型对象
// 如果直接将子类的原型对象赋值为父类原型对象
// 那么修改子类原型对象其实就相当于修改了父类的原型对象
function clone2 (o) {
    function F() {}
    F.prototype = o
    return new F()   
}
function clone (Child, Parent) {
  Child.prototype = clone2(Parent.prototype)
  Child.prototype.constructor = Child
}

let child  = new Child()
console.log('child-----', child)

其实这两种的本质是一样的,都是把Parent.prototype给Child.prototype,而不是把Parent给Child.prototype
此时child为:

图片alt

此时原型对象上已经没有同名的name属性了

优点

这种方式就解决了组合继承中的构造函数调用两次,构造函数引用类型共享,以及原型对象上存在多余属性的问题。是推荐的最合理实现方式(排除ES6的class extends继承)

缺点

没有啥特别的缺点

ES6继承

ES6提供了class语法糖,同时提供了extends用于实现类的继承,这是项目中最常见的继承方式。

使用class继承很简单,也很直观:

class Parent {
    constructor (name) {
        this.name = name
    }
    getName () {
        return this.name
    }
}

class Child extends Parent {
    constructor (name) {
        super(name)
        this.num = 24
    }
}

const child1 = new Child('kobe')
const child3 = new Child('jordan')
console.log('child1', child1) // {name: 'kobe', num: 24}
console.log('child3', child3) // {name: 'jordan', num: 24}
console.log('child1.getName()', child1.getName()) // kobe
console.log('child3.getName()', child3.getName()) // jordan

补充:

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

const person = {name :'kobe'}
const player = Object.create(person); // player.__proto__ === person

proto必填参数,是新对象的原型对象,如上面代码里新对象player__proto__指向person。注意,如果这个参数是null,那新对象就彻彻底底是个空对象,没有继承Object.prototype上的任何属性和方法,如hasOwnProperty()、toString()等。

js
评论
凯特网
Lv.1普通用户

99

帖子

11

小组

651

积分

站长交流