javaScript 继承简单总结
继承给我们提供了一种优雅可复用的编码方式,使得我们可以复用父类的属性和方法.
常用继承方式有:
1. 原型链继承
2. 借用构造函数继承
3. 组合继承(伪经典继承)
4. 原型式继承
5. 寄生式继承
6. 寄生组合式继承
7. Class 继承
1. 原型链继承
将父类的实例作为子类的原型
基本模式:
function People(name){
// 属性
this.name = name || 'people'
this.colors = ['red','blue','yellow']
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉')
}
}
// 原型方法
People.prototype.eat = function(food){
console.log(this.name + '正在吃' + food)
}
function Woman(name){
this.name = name
}
Woman.prototype = new People()
Woman.prototype.constructor = Woman
Woman.prototype.sex = '女'
let woman1 = new Woman('小丽')
console.log(woman1.name) // 小丽
woman1.sleep() // 小丽正在睡觉
woman1.eat('冰淇淋') // 小丽正在吃冰淇淋
console.log(woman1.sex) // 女
console.log(woman1 instanceof People) // true
console.log(woman1 instanceof Woman) // true
woman1.colors.push('green')
let woman2 = new Woman('小敏')
console.log(woman1.colors) // ["red", "blue", "yellow", "green"]
console.log(woman2.colors) // ["red", "blue", "yellow", "green"]
特点:
简单易于实现,父类新增原型方法/原型属性,子类都能访问到
缺点:
1. 子类要在继承后定义新方法
因为,原型继承实质上是重写原型对象.所以,如果在继承前就在子类的 prototype 上定义一些方法和属性,那么继承的时候,这些属性和方法将会被覆盖.
Woman.prototype.sex = '女' // 将会被覆盖
Woman.prototype = new People()
2. 不能使用对象字面量创建原型方法
这个的原理与上一点的实际上是一样的.当你使用对象字面量创建原型方法重写原型的时候,实质上相当于重写了原型链,所以原来的原型链就被切断了.
Woman.prototype = new People()
Woman.prototype = { //重写原型,将上面的继承覆盖了
sex: '女'
}
*3. 父类有引用类型的时候,各个实例对该引用的操作会影响其他实例.
woman1.colors.push('green')
let woman2 = new Woman('小敏')
console.log(woman1.colors) // ["red", "blue", "yellow", "green"]
console.log(woman2.colors) // ["red", "blue", "yellow", "green"]
*4. 没有办法在不影响所有对象实例的情况下,给父类构造函数传递参数.
5. 无法实现多继承
有鉴于此,实践中很少会单独使用原型继承.
2. 借用构造函数继承
在子类型构造函数的内部调用父类构造函数.
利用 call(thisObj,arg1,arg2,...) 或者 apply(thisObj,[args]) 把父类中通过 this 指定的属性和方法复制(借用)到子类创建的实例中.
借用构造函数: new 对象的时候,创建了一个新的实例对象,并且执行子类里面的代码,而子类里面用 call 调用父类,也就是说把 this 指向改成了指向新的实例,所以就会把父类里面的 this 相关属性和方法赋值到新的实例上,而不是赋值到父类上面,所以实例中就拥有了父类定义的这些 this 的属性和方法.
注意: 函数调用时 this 指向:
在全局中,this 等于 window;
当函数作为某个对象的方法调用时,this 指向那个对象,call(thisObj),apply(thisObj)方法可以将一个函数的对象上下文改为由 thisObj 指定的新对象;
以函数的方式直接调用时,this 指向 window;
new 创建的时候,this 指向创建的这个实例.
基本模式:
function People(name){
// 属性
this.name = name || 'people'
this.colors = ['red','blue','yellow']
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉')
}
}
// 原型方法
People.prototype.eat = function(food){
console.log(this.name + '正在吃' + food)
}
function Woman2(name){
// 继承了 People
People.call(this);
this.name = name || 'lily'
}
var w1 = new Woman2()
w1.colors.push('black')
console.log(w1.colors) // ["red", "blue", "yellow", "black"]
var w2 = new Woman2()
console.log(w2.colors) // ["red", "blue", "yellow"]
w1.eat('提子') // Uncaught TypeError: w1.eat is not a function
特点:
1. 可以在子类构造函数中向父类构造函数传递参数.
2. 各个实例的值不会互相影响.(解决引用类型问题)
3. 可以实现多继承(call 或 apply 多个父类)
缺点:
1. 方法都在构造函数中定义,无法复用;
2. 只能继承父类的实例属性和方法,不能继承原型属性和方法;
所以,借用构造函数继承也是很少单独使用的.
3. 组合继承(伪经典继承)
是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式.
使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承.
基本模型:
function People(name){
// 属性
this.name = name || 'people'
this.colors = ['red','blue','yellow']
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉')
}
}
// 原型方法
People.prototype.eat = function(food){
console.log(this.name + '正在吃' + food)
}
function Woman3(name,age){
// 继承属性
People.call(this,name)
this.age = age
}
// 继承方法
Woman3.prototype = new People()
Woman3.prototype.constructor = Woman3
let w31 = new Woman3('lily',27)
w31.colors.push('black')
w31.eat('桃子') // lily正在吃桃子
console.log(w31.colors) //["red", "blue", "yellow", "black"]
let w32 = new Woman3('lucy',29)
console.log(w32.colors) //["red", "blue", "yellow"]
特点:
避免了原型链和借用构造函数的缺陷,融合了它们的优点,称为 JavaScript 中最常用的继承模式.
1. 函数可以复用
2. 不存在引用属性共享问题
3. 可以继承实例属性和方法,也可以继承原型的属性和方法
4. 可以给父类构造函数传参
缺点:
无论什么情况下,都会调用两次父类构造函数
一次是在创建子类原型的时候,
另一次是在子类构造函数内部
虽然子类最终会包含父类对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性.
4. 原型式继承
原理: 借助原型,可以基于已有的对象创建新对象.
基本模型:
function object(o){
function W(){
}
W.prototype = o
return new W()
}
ES5新增了 Object.create() 方法规范化了原型式继承.调用方法为: Object.create(o).
适用场景:
只想让一个对象跟另一个对象建立继承这种关系的时候,可以用 Object.create(),不兼容的时候,则手动添加该方法兼容.
缺点
1. 有引用类型的时候,各个实例对该引用类型的操作会影响其他实例.
5. 寄生式继承
寄生式继承是原型式继承的加强版.
基本模型:
function object(o){
function W(){
}
W.prototype = o;
return new W();
}
function createAnother(origin){
var clone = object(origin);
clone.say = function(){
console.log('123')
}
return clone;
}
即在产生了这个继承了父类的对象之后,为这个对象添加一些增强方法.
6. 寄生组合式继承
实质上,是寄生式继承的加强版.这也是为了避免组合继承中无可避免的要调用两次父类构造函数的最佳方案.所以,开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式.
基本模式:(兼容写法)
function object(o){
function W(){}
W.prototype = o;
return new W();
}
function SuperType(name){
this.name = name;
this.colors = ['green','yellow','purple'];
}
SuperType.prototype.sayName = function(){
console.log('My name is ' + this.name)
}
function SubType(name){
// 继承实例属性和方法
SuperType.call(this,name);
}
function inheritPrototype(SubType,SuperType){
var prototype;
if(typeof Object.create === 'function'){
prototype = Object.create(SuperType.prototype);
}else{
prototype = object(SuperType.prototype);
}
// 修正constructor指向
prototype.constructor = SubType;
// 继承父类原型属性和方法, prototype实例(W的实例)继承了父类的原型
SubType.prototype = prototype;
}
object 是一个自定义的相当于 ES5 中 Object.create() 方法的函数.
特点
1. 解决了组合继承需要调用两次父类构造函数的问题.
7. Class 继承
Class 可以通过 extends 关键字实现继承.
子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错.
这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实力属性和方法.如果不调用 super 方法,子类就得不到 this 对象.
注意:
ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this)).
ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this.
class ColorPoint extends Point {
constructor(x, y, color){
super(x, y); // 调用父类的 constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的 toString()
}
}
Class 的继承链
大多数浏览器的 ES5 实现之中,每一个对象都有 __proto__ 属性,指向对应的构造函数的 prototype 属性.
Class 作为构造函数的语法糖,同时有 prototype 属性和 __proto__ 属性,因此同时存在两条继承链.
1. 子类的 __proto__ 属性,表示构造函数的继承,总是指向父类.
2. 子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的 prototype 属性.
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true