本文基于 原型链与继承别再被问倒了 增删 作者:路易斯 来源:掘金。
继承
js中继承 ?
Javascript中没有真正类的概念 , 它的"类"是通过构造函数实现的 .此后称类即构造函数 , 构造函数即类 .
子类实例拥有父类实例的所有属性(包括访问到原型链上的) , 然后在此基础上 , 添加自己的属性
比如有个父类实例 farther
: 有 farther.xx
, 就有son.xx
.
确定原型和实例的关系
使用原型链后, 我们怎么去判断原型和实例的这种继承关系呢? 方法一般有两种.
第一种是使用 instanceof 操作符, 只要用这个操作符来测试实例(instance)与原型链中出现过的构造函数,结果就会返回true. 以下几行代码就说明了这点.
alert(instance instanceof Object);//true
alert(instance instanceof Father);//true
alert(instance instanceof Son);//true
由于原型链的关系, 我们可以说instance 是 Object, Father 或 Son中任何一个类型的实例. 因此, 这三个构造函数的结果都返回了true.
第二种是使用 isPrototypeOf() 方法, 同样只要是原型链中出现过的原型,isPrototypeOf() 方法就会返回true, 如下所示.
alert(Object.prototype.isPrototypeOf(instance));//true
alert(Father.prototype.isPrototypeOf(instance));//true
alert(Son.prototype.isPrototypeOf(instance));//true
总结: instanceof和isPrototypeOf() 都是用来测试某个对象是否是某个构造函数的实例 , 他们的依据都是实例是否在构造函数原型对象组成的原型链上 ;
继承什么 ?
-
构造函数执行中的属性
-
构造函数的原型对象上的属性
-
构造函数作为对象, 构造函数的属性 (es6
class
中涉及)//这里有两个构造函数 , Son要继承Father -------- function Father(poperty){ this.poperty = poperty;//第一点 } //子类继承父类后, 子类的实例上也应该有这个属性 //比如var obj = new Son(); obj上有poperty这个属性 -------- Farther.prototype.poperty;//第二点 //比如Son实例沿原型链可以访问到父类原型对象的poperty这个属性 -------- Farther.poperty;//第三点 //子类应该继承父类上的属性 //比如Son上有poperty这个属性
上面代码中三部分的东西没有关联 , 只是想说明我们要继承什么 .
把父类的东西继承下来是目的 , 所以不必拘泥于形式 , 而是寻求最好的方法
继承的方式 ?
1.原型链继承(寻找属性时:子类实例自身->父类实例->父类原型)
// 定义父类
function Parent(name){
this.name = name;
this.fnPa = function(){console.log(111)}
}
Parent.prototype.age = 22;
Parent.prototype.fn = function(){console.log(888)};
function Child(){
this.type = "child";
}
Child.prototype = new Parent('parent'); // 借助原型链实现继承
var child = new Child();
console.log(child);
继承方式:子类的原型对为父类的一个实例
继承特点:利用原型链, 子类实例寻找属性时:子类实例自身->父类实例->父类原型
缺点:
-
当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;
-
在创建子类型时,不能向超类型的构造函数中传递参数.
即子类的原型对象是父类实例 , 对比第下面的方法 , 子类在创建完成后作为子类的原型对象父类的实例就不再改变了(也只能有一个).
2.构造函数内部继承(父类混入子类)
// 定义父类
function Parent(color){
this.name = "parent";
this.color = color
}
Parent.prototype.fn = function(){
console.log(1111)
}
// 子类
function Child(){
Parent.call(this,'yellow'); // 这句代码就是借助构造函数实现部分继承,绑定this并执行父构造函数
this.type = 'child';
}
var child1 = new Child();
console.log(child1);
继承方式:call执行父构造函数(不是 new
)并绑定this
继承特点: 如同把父构造函数的内容混入子构造函数
解决了原型链的两大问题:
-
保证了原型链中引用类型值的独立
-
不再被所有实例共享;子类型创建时也能够向父类型传递参数.
缺点: 如果仅仅借用构造函数,那么将无法避免构造函数模式存在的问题–方法都在构造函数中定义, 因此函数复用也就不可用了. 而且超类型(如Father)中定义的方法,对子类型而言也是不可见的
3.组合继承(结合原型链继承和构造函数继承)
既然二者都有长处 , 小孩子才做选择 , 我们攻城狮当然是都要了 .
function Father(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
alert(this.name);
};
function Son(name,age){
Father.call(this,name);//继承实例属性,第一次调用Father()
this.age = age;
}
Son.prototype = new Father();//继承父类方法,第二次调用Father()
Son.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5
var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10
继承方式 : 基于原型继承的基础上 , 在子类构造函数中加入构造函数内部继承的核心代码 Father.call(this,xxx)
继承特点:
- 原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性
- instanceof 和 isPrototypeOf( )也能用于识别基于组合继承创建的对象(上面的构造函数内部继承的方式不行).
缺点:我们很容易可以看出来 , Father.call(this,xxx)
已经帮我们实现了对父类中属性的继承 , 所以让子类原型对象等于父类实例 Son.prototype = new Father();
, 对于子类实例寻找属性是完全无用的 . 此举只是为了把子类实例拽到原型链上 , 使它可以访问到父类的原型对象上的属性 . 所以其仍有改进的空间 .
4.原型式继承(不使用构造函数,把一个对象作为多个生成对象的原型)
此为原型式继承 , 第一种是原型链继承. 它的意味好比 , 类模板就是一个原型对象 .
function object(o){
function F(){}
F.prototype = o;
return new F();
}
var person = {
friends : ["Van","Louis","Nick"]
};
var anotherPerson = object(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.friends.push("Style");
alert(person.friends);//"Van,Louis,Nick,Rob,Style"
继承方式: 在object()函数内部, 先创建一个临时性的构造函数, 然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例.
继承特点: 我们看到 , 这种继承方式实际上没有真正的构造函数了.object完成了一个功能 , 在使用者不使用构造函数的情况下 , 返回一个自定义原型(传入的参数)的对象 . 创造多个实例时 , 如果这个自定义原型都是同一个对象 , 则他们都会继承这个对象的属性 .
缺点:同原型链继承一样 , 原型式继承也有原型对象上引用对象被共享的问题 .
在 ECMAScript5 中,通过新增 object.create() 方法规范化了上面的原型式继承.
object.create() 接收两个参数:
- 一个用作新对象原型的对象
- (可选的)一个为新对象定义额外属性的对象
var person = {
friends : ["Van","Louis","Nick"]
};
var anotherPerson = Object.create(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.friends.push("Style");
alert(person.friends);//"Van,Louis,Nick,Rob,Style"
复制代码
object.create() 只有一个参数时功能与上述object方法相同, 它的第二个参数与Object.defineProperties()方法的第二个参数格式相同: 每个属性都是通过自己的描述符定义的.以这种方式指定的任何属性都会覆盖原型对象上的同名属性.例如:
var person = {
name : "Van"
};
var anotherPerson = Object.create(person, {
name : {
value : "Louis"
}
});
alert(anotherPerson.name);//"Louis"
5.寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路
function createAnother(original){
var clone = object(original);//通过调用object函数创建一个新对象
clone.sayHi = function(){//以某种方式来增强这个对象(增强:为其添加属性或方法)
alert("hi");
};
return clone;//返回这个对象
}
复制代码
继承方式:在原型式继承的基础上 , 为创造的对象添加属性 , 这些过程都放入一个函数中完成
缺点: 使用寄生式继承来为对象添加函数, 会由于不能做到函数复用而降低效率;这一点与构造函数模式类似.
6.组合寄生式继承(组合基础上,改变原型赋值时,采用原型式的方法)
function extend(subClass,superClass){
var prototype = object(superClass.prototype);//创建对象
prototype.constructor = subClass;//增强对象
subClass.prototype = prototype;//指定对象
}
function Father(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
alert(this.name);
};
function Son(name,age){
Father.call(this,name);//继承实例属性,第一次调用Father()
this.age = age;
}
//Son.prototype = new Father();不创建新的父类实例而是,用extend完成
extend(Son,Father);
Son.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5
var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10
继承方式:在组合继承的基础上 , 更改原型替换的方式(采用寄生式继承) , 就无需二次创建父类实例来完成原型链继承的目的了.
继承特点: extend的高效率体现在它没有调用superClass构造函数,因此避免了在subClass.prototype上面创建不必要,多余的属性. 于此同时,原型链还能保持不变; 因此还能正常使用 instanceof 和 isPrototypeOf() 方法.
以上,寄生组合式继承,集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效方法.
回顾继承方法
- 原型链继承:子类的原型对象是父类的实例
- 构造函数内部继承:用call(this)使父类内容混入子类
tip:这两者分别完成了继承原型链和父类内部内容的继承 , 但是构造函数内部继承不利于复用方法 , 纯原型链继承无法解决引用类型共用问题 , 不能传递参数 .
- 组合继承:在原型链继承继承上 添加构造函数继承的核心call(this)
tip:集二者之长 , 但是有一个缺点 , 调用了两次父类构造函数 .导致不必要的开销
-
寄生式继承:核心是可以创造一个原型是父类原型对象的空对象
-
寄生组合式继承:在组合继承的基础上 , 使用寄生式继承的方法来完成原型链的继承 . 以解决二次调用父类构造函数的问题
问题:为什么不能调用两次"new F()"呢
如果父类属性较少还好 , 可是在大型库中 , 父类的属性是非常的多的 , 用一个空对象完成继承比用父类实例完成继承好得多 .
问题:为什么要 extend, 既然extend的目的是将子类型的 prototype 指向超类型的 prototype,为什么不直接做如下操作呢?
subClass.prototype = superClass.prototype;//直接指向超类型prototype
如果你想给子类的原型对象上添加属性 , 但是子类和父类共用原型 , 你根本无法把他们区分开 , 这是一个大问题 . 所以 , 基于如上操作, 子类型原型将与超类型原型共用, 根本就没有继承关系.