js继承
概述
原型链继承
原型链继承是 JavaScript 最基本的继承方式,通过让子类的原型对象(prototype)指向父类的实例,继承父类的属性和方法。但是,它也存在一些问题,比如父类中引用类型值的属性被子类实例共享、不能传递参数等。
构造函数继承
构造函数继承是指在子类的构造函数中调用父类的构造函数,从而实现继承。但是,它存在一个问题,就是无法继承父类原型对象上的属性和方法。
组合继承
组合继承是指将原型链继承和构造函数继承组合在一起,从而实现继承。它既可以继承父类的实例属性和方法,也可以继承父类原型对象上的属性和方法。但是,它的缺点是在子类的构造函数中调用了父类的构造函数,同时在设置子类原型时又会再次创建父类实例,导致父类的构造函数被调用了两次。
原型式继承
原型式继承是指借助一个中间对象,将某个对象作为这个中间对象的原型对象,从而实现继承。它的优点是可以简单地实现对象之间的继承,但是它也存在一个问题,就是父对象的属性会被子对象共享,可能会导致子对象对父对象的属性进行修改,影响其他子对象。
寄生式继承
寄生式继承是指在原型式继承的基础上,增强了继承对象的能力。它在继承的基础上,增加了一些额外的属性或方法。但是,它存在一些问题,比如存在对象识别问题、存在引用类型值的共享问题等。
寄生组合式继承
寄生组合式继承是一种综合利用构造函数继承和原型链继承的方式,从而解决它们各自的问题。它通过借用构造函数来继承实例属性,通过原型链的方式来继承原型对象上的属性和方法。这种继承方式是目前使用最广泛的继承方式,也是最优秀的继承方式之一。
原型链继承
利用原型链的机制来实现对象之间的继承关系。
1 | // 父类构造函数 |
在上面的例子中,我们定义了一个 Parent
构造函数和一个 Child
构造函数,将 Child
的原型对象指向 Parent
的实例,这样 Child
的实例就可以访问 Parent
的属性和方法了。
原型链继承存在以下问题:
1.引用类型属性共享:子类实例对引用类型属性的修改会影响父类实例以及其他子类实例。
例如:
1 | function Parent() { |
在这个例子中,Child
继承了 Parent
的属性和方法,包括 colors
数组。当 child1
向 colors
中添加一个新元素时,child2
的 colors
也会受到影响,因为它们共享同一个原型对象。
2.无法向父类构造函数传递参数:在创建子类实例时,不能向父类构造函数传递参数。
例如:
1 | function Parent(name) { |
在这个例子中,Child
想要从父类 Parent
中继承 name
属性,但是在创建子类实例时,没有传递参数给父类构造函数,导致 child1
的 name
属性值为 undefined
。
构造函数继承
构造函数继承是通过在子类构造函数中调用父类构造函数(Parent.call(this))来实现继承的一种方式。在子类构造函数中,通过调用父类构造函数来获取父类的属性,并将其赋值给子类的实例。这样子类就能够继承父类的属性了。
具体实现步骤如下:
定义父类构造函数,设置父类属性
1 | function Parent() { |
在子类构造函数中调用父类构造函数,获取父类的属性
1 | function Child() { |
这里的call
方法可以改变函数的this
指向,将子类的this
指向父类构造函数中创建的新对象,以便子类继承父类的属性。
定义子类的原型,继承父类的方法
1 | codeChild.prototype = new Parent(); |
这里需要将子类的原型指向一个新的父类实例,这样子类就可以访问到父类原型上的属性和方法了。
构造函数继承的优点是可以在创建子类实例时向父类构造函数传递参数,同时可以避免原型链继承中的引用类型共享问题。但是,构造函数继承的缺点是无法继承父类原型链上的方法,同时每个子类实例都会创建一个新的父类实例,从而可能会降低程序的性能。
缺点:
1.无法继承父类原型对象上的属性和方法:
1 | function Parent() { |
2.每个实例都会拷贝一份父类构造函数中的属性和方法,会浪费内存空间:
1 | function Parent() { |
3.无法判断一个对象的类型:
1 | function Parent() { |
组合继承
组合继承是 JavaScript 中一种常见的继承方式,它结合了原型链继承和构造函数继承的优点。通过在子类构造函数中调用父类构造函数,实现对实例属性的继承,同时通过将子类的原型指向父类的实例,实现对原型属性和方法的继承。
下面是一个简单的示例代码:
1 | function Parent(name) { |
在上面的代码中,Parent
是父类,它有一个实例属性 name
和一个原型方法 sayHello
。Child
是子类,它继承了 Parent
的实例属性 name
,并添加了自己的实例属性 age
和原型方法 sayAge
。Child
的原型指向 Parent
的实例,这样 Child
的原型就继承了 Parent
的原型方法 sayHello
。
组合继承的优点在于解决了属性共享问题,支持原型方法继承。但是,它也有一个缺点,就是在创建子类实例时,会调用两次父类构造函数,一次在子类构造函数中,一次在将父类实例赋值给子类原型时,子类原型中存在冗余属性。
原型式继承
原型式继承是指利用一个已有的对象作为新对象的原型,从而创建出一个新的对象。这个新对象可以与原型对象共享属性和方法。这种继承方式主要使用 Object.create()
方法来实现。
其基本思路是先定义一个原型对象,然后通过 Object.create() 方法来创建新对象,新对象会继承原型对象的所有属性和方法。通过修改原型对象,就能够改变新对象的属性和方法。
下面是一个例子,演示了如何使用原型式继承:
原型式继承是指利用一个已有的对象作为新对象的原型,从而创建出一个新的对象。这个新对象可以与原型对象共享属性和方法。这种继承方式主要使用 Object.create()
方法来实现。
其基本思路是先定义一个原型对象,然后通过 Object.create() 方法来创建新对象,新对象会继承原型对象的所有属性和方法。通过修改原型对象,就能够改变新对象的属性和方法。
下面是一个例子,演示了如何使用原型式继承:
1 | // 定义原型对象 |
在这个例子中,我们先定义了一个原型对象 person
,然后使用 Object.create()
方法来创建新对象 student
。新对象 student
继承了原型对象 person
的所有属性和方法。通过修改新对象的属性,我们可以改变新对象的状态。同时,新对象仍然可以访问原型对象的属性和方法。
原型式继承的缺点与原型链继承类似:由于所有的新对象都会共享同一个原型对象,因此对原型对象的修改会影响到所有继承自它的对象。
寄生式继承
寄生式继承是在原型式继承的基础上进行扩展,它在创建新对象的过程中,对新对象进行一些额外的增强操作。
具体来说,寄生式继承的实现过程是这样的:
- 声明一个用于继承的基础对象,通常是一个空对象。
- 通过某种方式,增强这个基础对象,比如给它添加属性或方法。
- 返回这个基础对象,作为继承结果。
下面是一个简单的例子,演示了如何使用寄生式继承来实现一个简单的对象继承:
1 | // 定义一个用于继承的基础对象 |
可以看到,在这个例子中,我们使用了一个名为 createChild
的函数来实现继承。这个函数的参数是一个对象,它用于指定继承自哪个对象。函数内部,我们首先使用 Object.create
方法来创建一个新对象,该对象的原型链指向传入的对象 obj
,从而实现了继承。然后,我们为这个新对象添加了一个新属性 age
,最后返回这个新对象,作为继承的结果。
优点:可扩展对象功能。
缺点:新增方法无法复用(每个实例单独创建)。
寄生组合式继承
寄生组合式继承是组合继承的优化版本,通过寄生式继承(利用 Object.create())替代直接的原型链继承,从而避免父类构造函数被重复调用,同时保留了构造函数继承和原型链继承的优点。
在寄生组合式继承中,我们先创建一个临时性的构造函数,这个构造函数的作用仅仅是用于取得父类的原型对象。然后,将这个临时性的构造函数的原型对象赋值给子类的原型对象,最后再将子类原型对象的 constructor 属性设置为子类本身。
以下是一个寄生组合式继承的例子:
1 | // 父类构造函数 |
优点:
- 只调用一次父类构造函数,避免了组合继承的缺点。
- 可以继承父类构造函数中的属性和方法,也可以继承父类原型上的属性和方法。
ES6 Class 继承
使用 class 和 extends 语法糖(底层基于寄生组合式继承)。
代码实现
1 | class Parent { |
优点:语法简洁,符合现代编程习惯。
缺点:底层原理仍需理解(本质是寄生组合式继承)。