JavaScript实现继承的方式
一、JavaScript实现继承的方式
1、原型链继承
2、构造函数继承
3、组合继承
4、原型式继承
5、寄生式继承
6、寄生组合式继承
二、从哪些方面分析继承方式特点、优缺点
- 继承的属性、不会继承的属性
- 是否实现了多继承
- 是否能向父类构造函数传参
| 继承方式 | 关键代码 | 原理 | 重点 | 特点 | 优点 | 缺点 | 使用场景 |
| —- | —- | —- | —- | —- | —- | —- | —- |
| 原型链继承 | Student.prototype = new Person(); | 构造函数的实例能访问到它的原型对象上 | 让新实例的原型等于父类的实例 |
- 实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。
- 不会继承:新实例不会继承父类实例的属性
|
- 简单易实现
- 父类新增原型方法/原型属性,子类都能访问
- 实例是子类的实例也是父类的实例
|
- 无法实现多继承
- 创建子类实例时,不能向父类构造函数传参数
- 所有新实例都会共享父类实例的属性。
- 原型上的属性是共享的,一个实例改变了原型属性,另一个实例的原型属性也会被修改
| | | 构造函数继承 | function Student(name, grade, site) {
Person.call(this, name);
} | | 用call / apply将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制)) |
- 实例继承的属性:父类构造函数的属性
- 不会继承:新实例不会继承父类原型的属性
|
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call多个对象)
- 不需要修复构造函数指向
|
- 方法在构造函数中定义,无法复用
- 每次用都要重新调用
- 只能继承父类的实例属性,不能继承原型属性、方法
- 在超类型的原型中定义的方法,对子类型而言是不可见的,结果所有类型都只能使用构造函数模式
- 实例并不是父类的实例,而只是子类的实例
- 每个新实例都有父类构造函数的副本,臃肿
| | | 组合继承 | function Student(name, grade, site) {
Person.call(this, name);
}
Student.prototype = new Person() // 继承方法
Student.prototype.constructor = Student | 将原型链与借用构造函数的技术组合起来。在原型中定义公用的属性方法,在实例中定义私有的属性方法。实现组合式继承。 | 结合了以上两种模式的优点:传参、复用 | |
- 可以继承实例属性、方法,也可以继承原型属性、方法
- 可传参、可复用
- 实例既是子类的实例,也是父类的实例
|
- 调用了两次父类构造函数,耗内存
- 一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
- 需要修复构造函数指向
| 最常用 | | 原型式继承 | function object(o) {
function F() {}
F.prototype = obj;
return new F();;
} | | 用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。
Object.create()就是这个原理
类似于复制一个对象,用函数来包装 |
- 所有实例都会继承原型上的属性
| |
- 不支持多继承
- 实例是父类的实例
- 包含引用类型值的属性始终都会共享相应的值
- 无法实现复用
| 只是想让一个对象与另一个对象类似的情况下,原型式继承是完全可以胜任的 | | 寄生式继承 | function object(o) {
function F() {}
F.prototype = obj;
return new F();
}
// 给原型式继承套个壳子传递参数
function Student(obj) {
const person = object(obj);
person.grade = ‘g’;
return person;
}
const person = new Person()
const stu = Student(person) | | 给原型式继承外面套了个壳子 | |
- 没有创建自定义类型
- 因为只是套了个壳子返回对象,这个函数顺理成章就成了创建的新对象
|
- 不支持多继承
- 实例是父类的实例,不是子类的实例
- 因为只是在父类的实例上添加属性、方法而已
- 做不到函数复用
- 因为没用到原型
| 在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。 | | 继承组合式继承 | function Student(name, grade, site) {
Person.call(this, name);
this.grade = grade;
}
let proto = Object.create(Person.prototype); / / 创建超类型原型的一个副本
proto.constructor = Student; // 修复构造函数指向
Student.prototype = proto; // 继承方法
const stu = new Student(‘asuncat’, 3, ‘https://yquanmei.github.io‘); | | | | 通过Object.create()来代替给子类原型赋值的过程,解决了两次调用父类构造函数的问 |
- 修复了组合继承的问题
| |
| ☆-1【示例】父类```javascript function Person(name) { this.name = name; this.say = function() { console.log(‘1 + 1 = 2’); } } Person.prototype.listen = function() { return this.property }
|
| --- |
> 超类型:父级、被继承的对象
<a name="TScwC"></a>
## 原型链继承
一、将父类的实例作为子类的原型
- 实现的本质:重写子类型的原型对象,代之以一个新类型的实例
- 子类型的新原型对象中有一个内部属性Prototype指向了「父类」的原型,还有一个从「父类」原型中继承过来的属性constructor指向了「父类」构造函数
| ☆-1【示例】```javascript
// 实现原型链的一种基本模式
function Person() {
this.name = name;
this.say = function() {
console.log('1 + 1 = 2');
}
}
Person.prototype.listen = function() {
console.log('hello world');
}
function Student() {
}
// 继承,用父类型的一个实例来重写子类型的原型对象
Student.prototype = new Person(); // 关键
const stu = new Student();
stu.grade = 3;
console.log(stu.grade); // 3
stu.say(); // 1 + 1 = 2
stu.listen() // hello world
1、其中,Student 继承了Person, 而继承是通过创建Person的实例,将该实例赋值给Student的原型实现的。
2、最终的原型链:stu指向Student的原型,Student的原型又指向Person的原型,Person的原型又指向Object的原型(所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype) |
| —- |
二、实例是子类的实例也是父类的实例
| ☆-1【示例】```javascript stu instanceof Student // true stu instanceof Person // true
|
| --- |
**存在的问题**
- prototype里有个属性ocnstructor指向构造函数本身,但是,「子类」的原型已经被父类的实例取代了,所以指向也不正确,所以需要修复构造函数指向
| ☆-1【示例】```javascript
function Student() {}
console.log(Student.prototype.constructor);
Student.prototype = new Person(); // 关键
const stu = new Student()
stu.grade = 3
console.log(stu.grad) //3
stu.say() // 1 + 1 = 2
stu.listen() // hello world
console.log(Student.prototype.constructor)
|
| —- |
解决问题
| ☆-1【示例】```javascript Student.prototype.constructor = Student
 |
| --- |
<a name="PKoao"></a>
## 构造函数继承/伪造对象/经典继承
一、在一个类中执行另一个类的构造函数,通过apply/ call函数设置this的指向,这样就可以得到另一个类的所有属性
| ☆-1【示例】```javascript
function Person() {
this.name = name;
this.say = function () {
console.log('1 + 1 = 2');
}
}
Person.prototype.listen = function () {
console.log('hello world');
}
function WebsiteMaster(site) {
this.site = site
}
function Student(name, grade, site) {
// 关键代码
Person.call(this, name); // 继承Person,在这里还可以给超类型构造函数传参
WebsiteMaster.call(this, site)
this.grade = grade
}
const stu = new Student('asuncat', 3, 'https://yquanmei.github.io');
console.log(stu.name, stu.grade, stu.site); // asuncat, 3, 'https://yquanmei.github.io'
stu.say(); // 1 + 1 = 2
stu.listen() // Uncaught TypeError: stu.listen is not a function
| | —- |
二、实例并不是父类的实例,而只是子类的实例
| 【示例】```javascript stu instanceof Student // true stu instanceof Person // false
|
| --- |
<a name="O7net"></a>
## 组合继承/伪经典继承
一、原型链继承 + 构造函数继承
| ☆-1【示例】```javascript
function Person(name) {
this.name = name;
this.say = function() {
console.log('1 + 1 = 2');
}
}
Person.prototype.listen = function() {
return this.property
}
function WebsiteMaster(site) {
this.site = site;
}
function Student(name, grade, site) {
Person.call(this, name); // 继承属性
WebsiteMaster.call(this, site);
this.grade = grade
}
Student.prototype = new Person() // 继承方法 // 将Student的prototype对象指向一个Animal实例,相当于完全删除了prototype对象原先的值,然后赋予了一个新值
// 任何一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有Student.prototype = new Person()这一行,Student.prototype.constructor指向Student,加了这一行后,Student.prototype.constructor指向Animal
// 每个实例也有一个constructor属性,默认调用prototype的constructor属性。因此,在执行Student.prototype = new Person()后,stu的consturctor也指向Person, 这会造成继承链的紊乱(stu命名是调用构造函数Student生成的)
// 因此,我们必须将Student.prototype的constructor改成Student
Student.prototype.constructor = Student
const stu = new Student('asuncat', 3, 'https://yquanmei.github.io')
console.log(stu.name, stu.grade, stu.site); // asuncat, 3, https://yquanmei.github.io
stu.say(); // 1 + 1 = 2
stu.listen(); // hello world
console.log(stu.constructor);
|
| —- |
二、组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 javascript 中最常用的继承模式。而且,使用 instanceof 操作符和 isPrototype() 方法也能够用于识别基于组合继承创建的对象。
三、如果替换了prototype对象,下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数。
| 【示例】```javascript o.prototype = {}; o.prototype.constructor = o;
|
| --- |
<a name="XpAlu"></a>
## 原型式继承
一、为父类实例添加属性、方法,作为子类实例。
<a name="pXTrc"></a>
### es5:自定义一个函数来实现原型式继承
一、道格拉斯·克罗克福德在一篇文章中介绍了一种实现继承的方法,这种方法并没有使用严格意义上的构造函数。它的想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义模型。为了达到这个目的,它给出了如下函数
```javascript
// 封装一个函数容易,用来输出对象和承载继承的原型
function object(o) {
funciton F() {} // 创建临时性构造函数
F.prototype = o; // 将传入的对象作为这个构造函数的原型
return new F(); // 返回临时类型的一个新实例
}
1、在 object() 函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。实质上,object() 对传入其中的对象执行了一次浅复制
| ☆-1【示例】```javascript function Person(name) { this.name = name; this.say = function () { console.log(‘1 + 1 = 2’); } } Person.prototype.listen = function () { return this.property }
function object(o) { function F() { } F.prototype = o; return new F(); }
const person = new Person(‘asuncat’);
const stu = object(person);
stu.grade = 3; stu.study = function () { console.log(‘FrontEnd’); }
console.log(stu.name, stu.grade); // asuncat, 3
stu.say(); // 1 + 1 = 2 stu.listen(); // hello world
stu.study() // FrontEnd
|
| --- |
<a name="xERnT"></a>
### es6:使用 Object.create() 方法实现原型式继承
一、Object.create()方法接收两个参数:一是用作新对象原型的对象和一个为新对象定义额外属性的对象。在传入一个参数的情况下,此方法与 object() 方法作用一致。 在传入第二个参数的情况下,指定的任何属性都会覆盖原型对象上的同名属性。
| ☆-2【示例】```javascript
var person = {
name: 'asuncat',
colors: ['red', 'green', 'blue']
}
var anotherPerson1 = Object.create(person, {
name: {
value: 'tom'
}
})
var anotherPerson2 = Object.create(person, {
name: {
value: 'jerry'
}
})
anotherPerson1.colors.push('purple');
console.log(anotherPerson1.name); // tom
console.log(anotherPerson2.name); // jerry
console.log(anotherPerson1.colors); // ['red,green,blue,purple']
console.log(anotherPerson2.colors); // ['red,green,blue,purple']
| | —- |
寄生式继承
一、为父类实例添加属性、方法,作为子类实例。(原理和原型式继承一样)
| ☆-1【示例】```javascript function Person(name) { this.name = name; this.say = function() { console.log(‘1 + 1 = 2’); } } Person.prototype.listen = function() { return this.property }
function object(o) { function F() {} F.prototype = o; return new F(); }
function Student(name, grade) { const person = object(new Person(name));
person.grade = grade person.study = function() { console.log(‘FrontEnd’); }
return person; }
const stu = new Student(‘asuncat’, 3)
console.log(stu.name, stu.grade); // asuncat, 3 stu.say(); // 1 + 1 = 2 stu.listen(); // hello world
stu.study(); // FrontEnd
|
| --- |
| ☆-2【示例】```javascript
// 创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回这个对象
function createPerson(o){
var clone = Object.create(o); // 通过 Object.create() 函数创建一个新对象
clone.study = function(){ // 增强这个对象
alert("FrontEnd");
};
return clone; // 返回这个对象
}
| | —- |
寄生组合式继承
一、通过Object.create()来代替给子类原型赋值的过程,解决了两次调用父类构造函数的问题
| ☆-1【示例】```javascript function Person(name) { this.name = name; this.say = function () { console.log(‘1 + 1 = 2’); } } Person.prototype.listen = function () { return this.property }
function WebsiteMaster(site) { this.site = site }
function Student(name, grade, site) { Person.call(this, name); // 继承属性 WebsiteMaster.call(this, site); this.grade = grade; }
let proto = Object.create(Person.prototype); // 核心 // 创建超类型原型的一个副本 proto.constructor = Student; // 核心 // 修复构造函数指向 Student.prototype = proto // 核心 // 继承方法
const stu = new Student(‘asuncat’, 3, ‘https://yquanmei.github.io‘);
console.log(stu.name, stu.grade, stu.site); // asuncat, 3, https://yquanmei.github.io
stu.say(); // 1 + 1 = 2
stu.listen(); // hello world
```javascript
function Person(name) {
this.name = name;
this.say = function () {
console.log('1 + 1 = 2');
}
}
Person.prototype.listen = function () {
return this.property
}
function WebsiteMaster(site) {
this.site = site
}
function Student(name, grade, site) {
Person.call(this, name); // 继承属性
WebsiteMaster.call(this, site);
this.grade = grade;
}
let proto = object(Person.prototype); // 核心 // 创建超类型原型的一个副本
proto.constructor = Student; // 核心 // 继承方法
Student.prototype = proto; // 核心 // 修复构造函数指向
const stu = new Student('asuncat', 3, 'https://yquanmei.github.io');
console.log(stu.name, stu.grade, stu.site); // asuncat, 3, https://yquanmei.github.io
stu.say(); // 1 + 1 = 2
stu.listen(); // hello world
| | —- |
| ☆-2【示例】```javascript // 通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型 function SuperType(name){ this.name = name; this.colors = [“red”,”green”,”blue”]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name,age){ SuperType.call(this,name); this.age = age; } // 创建超类型原型的一个副本 var anotherPrototype = Object.create(SuperType.prototype); // 重设因重写原型而失去的默认的 constructor 属性 anotherPrototype.constructor = SubType; // 将新创建的对象赋值给子类型的原型 SubType.prototype = anotherPrototype;
SubType.prototype.sayAge = function(){ alert(this.age); }; var instance1 = new SubType(“luochen”,22); instance1.colors.push(“purple”); alert(instance1.colors); // “red,green,blue,purple” instance1.sayName(); instance1.sayAge();
var instance2 = new SubType(“tom”,34); alert(instance2.colors); // “red,green,blue” instance2.sayName(); instance2.sayAge();
1、这个例子的高效率体现在它只调用一次SuperType构造函数,并且因此避免了在SubType.prototype上面创建不必要,多余的属性。与此同时,原型链还能保持不变;因此还能正常使用instanceof操作符和isPrototype()方法 |
| --- |
<a name="WfInL"></a>
### 为什么不可以直接把父类原型赋值给子类原型来实现呢
一、因为直接赋值的话,那就是引用关系
| ☆-2【示例】```javascript
function Person(name) {
this.name = name;
this.say = function () {
console.log('1 + 1 = 2');
}
}
Person.prototype.listen = function () {
return this.property
}
function WebsiteMaster(site) {
this.site = site
}
function Student(name, grade, site) {
Person.call(this, name) // 继承属性
WebsiteMaster.call(this, site)
this.grade = grade
}
Student.prototype = Person.prototype // 继承方法
Student.prototype.constructor = Student // 修复实例
const stu = new Student('clz', 3, 'https://clz.vercel.app')
console.log(stu.name, stu.grade, stu.site) // clz, 3, https://clz.vercel.app
stu.say() // 1 + 1 = 2
stu.listen() // エウテルペ
Student.prototype.listen = function () {
console.log('EGOIST')
}
console.log(Person.prototype.listen)
1、修改Student原型上的方法时,Person上的原型上也会跟着变化。
2、Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。所以,此时修改Student原型上的方法时,Person的原型上的不会跟着变化。 |
| —- |
| ☆-2【示例】es6之前没有Object.create()方法,可以自己实现(实际就是原型式继承的关键函数)
1、关键:
- 接收一个对象obj
- 返回一个新对象newObj
- 让newObj.**proto** === objjavascript
function object(obj) {
function F() {} // 新的构造函数
F.prototype = obj; // 继承传入的参数obj
return new F(); // 返回新的函数对象
}
|
| —- |