记录-对象有哪些继承方式
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
1. 原型链
温故而知新:
构造函数、原型和实例的关系:
每个构造函数都有一个原型对象,原型有一个属性指回构造函数,实例有一个内部指针指向原型。
思考:如果原型是另一个类型的实例呢?
那就意味着这个原型本身有一个内部指针指向另一个原型,相应的另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本思想。
实现原型链涉及如下代码模式:
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = false; } // 继承SuperType SubType.prototype = new SuperType() SubType.prototype.getSubValue = function(){ return this.subproperty; } let instance = new SubType() console.log(instance.getSuperValue()); //true
以上的代码定义了两个类型:SuperType和SubType。这两个类型分别定义了一个属性和方法。两个类型的主要区别是:SubType通过创建SuperType的实例并将其赋值给自己的原型SubType.prototype实现了对SuperType的继承。这个赋值重写了SubType最初的原型,将其替换成SuperType的实例。这意味着SuperType实例可以访问的所有属性和方法也会存在于SubType.prototype。这样实现继承后,紧接着又给SuperType的实例添加了一个新的方法。最后又创建了SubType的实例并调用了它继承的getSuperValue方法。
默认原型
默认情况下,所有引用类型都继承自Object,这也是通过原型链实现的。任何函数默认的原型都是一个Object的实例,这意味着这个实例有一个内部指针指向Object.prototype。这也是为什么自定义类型能够继承包括toString()、valueOf在内部的所有默认方法的原因。
SubType继承SuperType,而SuperType继承Object。在调用instance.toString时,实际上调用的是保存在Object.prototype上的方法。
原型与继承的关系
原型与继承的关系可以通过两种方式来确定:
使用instanceof操作符:
如果一个实例的原型中出现过相应的构造函数,则instanceof返回true
console.log(instance instanceof Object); //true console.log(instance instanceof SuperType); //true console.log(instance instanceof SubType); //true
从技术上讲,instance是Object、SuperType、SubType的实例,因为instance的原型链中包含这些构造函数的原型。结果就是instanceof对所有这些构造函数都返回true。
使用isPrototypeOf()方法
原型链中的每个原型都可以调用这个方法,这要原型链中包含这个原型,这个方法就会返回true
console.log(Object.prototype.isPrototypeOf(instance)); //true console.log(SuperType.prototype.isPrototypeOf(instance)); //true console.log(SubType.prototype.isPrototypeOf(instance)); //true
关于方法
子类有时需要覆盖父类的方法,或者增加父类没有的方法。为此,这些方法必须在原型赋值之后再添加到原型上。来看下面的例子:
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = false; } // 继承SuperType SubType.prototype = new SuperType() // 新方法 SubType.prototype.getSubValue = function(){ return this.subproperty; } // 覆盖已有的方法 SubType.prototype.getSuperValue = function(){ return false; } let instance = new SubType() console.log(instance.getSuperValue()); //false
以上代码新增两个方法。第一个方法getSubValue()是SubType的新方法。而第二个方法getSuperValue()是原型链上已经存在但在这里被遮蔽的方法。;后面在SubType实例上调用getSuperValue()时调用的是这个方法。而SuperValue的实例仍然会调用最初的方法。重点在于上述两个方法都是在把原型赋值为SuperType的实例之后定义的。
另一个要理解的重点,以对象字面量方式创建原型方法,会破环之前的原型链。因为这样相当于重写原型链。
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = false; } // 继承SuperType SubType.prototype = new SuperType() // 通过对象字面量添加新的方法,会导致上一行无效 SubType.prototype = { getSubValue(){ return this.subproperty; }, someOtherMethod(){ return false; } } let instance = new SubType() console.log(instance.getSuperValue()); //instance.getSuperValue is not a function
子类的原型在被赋值为SuperType的实例后,又被一个对象字面量覆盖了。覆盖后的原型是一个Object的实例,而不再是SuperType的实例。因此,之前的原型链就断了。SubType和SuperType之间也没有关系了。
原型链的额问题
原先的实例属性变成了原型属性
原型包含的引用值会在所有实例间共享,在使用原型实现继承时,原型实际上变成了另一个类型的实例。
function SuperType(){ this.colors= ["red","blue","green"] } function SubType() { } // 继承SuperType SubType.prototype = new SuperType() let instance1 = new SubType() instance1.colors.push("black"); console.log(instance1.colors); //["red", "blue", "green", "black"] let instance2 = new SubType() console.log(instance2.colors); //["red", "blue", "green", "black"]
SuperType构造函数定义了一个colors属性,其中包含一个数组(引用值)。每个SuperType的实例都会有自己的colors属性,包含自己的数组。但是当SubType通过原型继承SuperType后,SubType.prototype变成了SuperType的一个实例,因而也获得了自己的colors属性。这类似于创建了SubType.prototype.colors属性。最终结果是,SubType的所有实例都会共享这个colors属性。这一点通过instance1.colors上的属性也能反应到instance2.colors上就可以看出。
子类型在实例化时不能给夫类型的构造函数传参
我们无法在不影响所有对象实例的情况下把参数传进父类的构造函数。再加上原型中包含引用值的问题,就导致原型链基本不会被单独使用。
2. 盗用构造函数
盗用构造函数是用来解决原型包含引用值导致的继承问题。基本思路:在子类构造函数中调用父类构造函数。因为函数是在特定上下文中执行代码的简单对象,所以可以使用apply()和call()方法以新创建的对象上下文执行构造函数。
function SuperType(){ this.colors = ["red","blue","green"]; } function SubType(){ // 继承SuperType /* *********** */ SuperType.call(this); /* *********** */ } let instance1 = new SubType(); instance1.colors.push("black"); console.log(instance1.colors); // ["red", "blue", "green", "black"] let instance2 = new SubType() console.log(instance2.colors); //["red", "blue", "green"]
以上代码展示了盗用构造函数的调用。通过使用call()或者apply()方法,SuperType构造函数在为SubType的实例创建的新对象的上下文中执行了。这相当于新的SubType对象上运行了SuperType函数中的所有初始化代码。结果就是每个实例都会有自己的colors属性。
1. 优点
可以在子类构造函数中向父类构造函数传参
function SuperType(name){ this.name = name; } function SubType(){ // 继承SuperType并传参 SuperType.call(this,"Nicholas"); // 实例属性 this.age = 29 } let instance = new SubType() console.log(instance.name); //Nicholas console.log(instance.age); //29
SuperType构造函数接受一个参数name,然后将它赋值给一个属性。在SubType构造函数中调用SuperType构造函数时传入这个参数,实际上会在SubType的实例上定义name属性。为确保SuperType构造函数不会覆盖SubType定义的属性,可以在调用父类构造函数之后再给子类实例添加额外的属性。
2. 缺点
必须在构造函数中定义方法,因此函数不能重用。
子类不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。
由于存在以上问题,所以盗用构造函数基本上也不能单独使用。
3. 组合继承
组合继承也叫伪经典继承,综合了原型链和盗用构造函数,将两者的优点集中起来。基本思路是:使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
/* 组合继承 */ function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"] } SuperType.prototype.sayName = function(){ console.log(this.name); } function SubType(name,age){ // 继承属性 SuperType.call(this,name) this.age = age } // 继承方法 SubType.prototype = new SuperType() SubType.prototype.sayAge = function(){ console.log(this.age); } let instance1 = new SubType("Nicholas",29) instance1.colors.push("blcak"); console.log(instance1.colors); //["red", "blue", "green", "blcak"] instance1.sayName() //Nicholas instance1.sayAge(); //29 let instance2 = new SubType("Greg",27) console.log(instance2.colors); //["red", "blue", "green"] instance2.sayName(); //Greg instance2.sayAge(); //27
SuperType构造函数定义了两个属性name和colors,而它的原型上也定义了一个方法叫sayName()。SubType构造函数调用了SuperType构造函数,传入name参数,然后又定义了自己的属性age。此外,SubType.prototype也被赋值为SuperType的实例。原型赋值之后,又在这个原型上添加了新方法sayAge()。这样就可以创建两个SubType实例,让这两个实例都有自己的属性,包括colors,同时还共享相同的方法。
组合继承弥补了原型链和盗用构造函数的不足,是JavaScript中使用最多的继承模式。而且组合继承也保留了instanceof操作符和isPrototypeOf()方法识别合成对象的能力。
缺点:
父类构造函数会被调用两次,一次是在创建子类原型时调用,一次是在子类构造函数中调用。
本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。
4. 原型式继承
2006年Douglas Crockford的文章:《JavaScript中的原型式继承》中介绍了一种不涉及严格意义上构造函数的继承方法。作者的出发点是即使不自定义类型也可以通过原型实现对象之间的信息共享。文章给出了一个函数:
function object(o){ function F(){} F.prototype = o; return new F() }
这个object
方法会创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个临时类型的一个实例。本质上,object()
是对传入对象执行了一次浅复制。
function object(o){ function F(){} F.prototype = o; return new F() } let person = { name:"Nicholas", friends:["Shelby",'Court','Van'] }; let anotherPerson = object(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); let yetAnotherPerson = object(person); yetAnotherPerson.name = 'Linda'; yetAnotherPerson.friends.push("Barbie") console.log(person.friends); //["Shelby", "Court", "Van", "Rob", "Barbie"]
ECMAScript
通过增加Object.create()
方法将原型式继承的概念规范化。这个方法接受两个参数:作为新对象原型的对象,给新对象定义额外属性的对象(可选)。在只有一个参数时,Object.create()
与这里的object()
方法效果类似。
let person = { name:"Nicholas", friends:["Shelby",'Court','Van'] } let anotherPerson = Object.create(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); let yetAnotherPerson = Object.create(person); yetAnotherPerson.name = "Linda" yetAnotherPerson.friends.push("Barbie") console.log(person.friends); //["Shelby", "Court", "Van", "Rob", "Barbie"]
Object.create()
的第二个参数与Object.defineProperties()
的第二个参数一样:每个新增参数属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性。
let person = { name:"Nicholas", friends:["Shelby",'Court','Van'] } let anotherPerson = Object.create(person,{ name:{ value:'Greg' } }); console.log(anotherPerson.name); //Greg
原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住,属性中包含的引用值始终会在相关对象间共享,跟使用原型模式时一样的。
5. 寄生式继承
寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。基本的寄生继承模式如下:
function createAnother(original){ let clone = object(original); //通过调用函数创建一个新对象 clone.sayHi = function(){ //以某种方式增强这个对象 console.log("HI"); }; return clone; //返沪这个对象 }
createAnother()
函数接受一个参数,就是新对象的基准对象。这个对象original
会被传给object()
函数,然后将返回的新对象赋值给clone
对象添加一个新方法sayHi()
。最后返回这个对象。
function object(o){ function F(){} F.prototype = o; return new F() } function createAnother(original){ let clone = object(original); //通过调用函数创建一个新对象 clone.sayHi = function(){ //以某种方式增强这个对象 console.log("Hi"); }; return clone; //返沪这个对象 } let person = { name:"Nicholas", friends:["Shelby",'Court','Van'] } let anotherPerson = createAnother(person); anotherPerson.sayHi(); //Hi
这个例子基于person对象返回了一个新对象。新返回的anotherPerson对象具有person的所有属性和方法,还有一个新方法叫sayHi()
寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。object()函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用。
==注意:==通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
6. 寄生式组合继承
通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而实取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。
function inheritPrototype(subType,superType){ let prototype = object(superType.prototype); //创建对象 prototype.constructor = subType; //增强对象 subType.prototype = prototype; //赋值对象 }
这个inheritPrototype()函数实现了寄生式组合继承的核心逻辑。这个函数接受到两个参数:子类构造函数和父类构造函数。这个函数内部,第一步是创建一个父类原型的一个副本。然后,给返回的prototype对象设置constructor属性,解决由于重写原型导致默认constructor丢失的问题。最后将新创建的对象赋值给子类型的原型。调用inheritPrototype()就可以实现前面例子中的子类型原型赋值:
function object(o){ function F(){} F.prototype = o; return new F() } function inheritPrototype(subType,superType){ let prototype = object(superType.prototype); //创建对象 prototype.constructor = subType; //增强对象 subType.prototype = prototype; //赋值对象 } function SuperType(name){ this.name = name; this.colors =["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ console.log(this.name); } function SubType(name,age){ SuperType.call(this,name); this.age = age; } inheritPrototype(SubType,SuperType); SubType.prototype.sayAge = function(){ console.log(this.age); }
这里只调用了一次SuperType构造函数,避免了SubType.prototype上不必要也用不到的属性,因此可以说这个例子效率更高。而且,原型链仍然保持不变,因此,instanceof操作符和isPrototypeOf()方法正常有效。寄生式组合继承可以说是引用类型继承的最佳方式。