原型
ECMAScript 中描述了原型鏈的概念,並將原型鏈作爲實現繼承的主要方法。其基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。簡單回顧一下構造函數、原型和實例的關係:每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。那麼,假如我們讓原型對象等於另一個類型的實例,結果會怎麼樣呢?顯然,此時的原型對象將包含一個指向另一個原型的指針,相應地,另一個原型中也包含着一個指向另一個構造函數的指針。假如另一個原型又是另一個類型的實例,那麼上述關係依然成立,如此層層遞進,就構成了實例與原型的鏈條。這就是所謂原型鏈的基本概念
繼承
繼承是OO 語言中的一個最爲人津津樂道的概念。許多OO 語言都支持兩種繼承方式:接口繼承和
實現繼承。接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。如前所述,由於函數沒有簽名,
在ECMAScript 中無法實現接口繼承。ECMAScript 只支持實現繼承,而且其實現繼承主要是依靠原型鏈
來實現的
原型與原型鏈
- prototype :每個函數都會有這個屬性,這裏強調,是函數,普通對象是沒有這個屬性的(這裏爲什麼說普通對象呢,因爲JS裏面,一切皆爲對象,所以這裏的普通對象不包括函數對象)。它是構造函數的原型對象;
- proto :每個對象都有這個屬性,,這裏強調,是對象,同樣,因爲函數也是對象,所以函數也有這個屬性。它指向構造函數的原型對象;
- constructor :這是原型對象上的一個指向構造函數的屬性。
function Pig(name: string, age: number){
this.name = name
this.age = age
}
// 創建Pig實例
const Pepig = new Pig('Pepig', 18)
// 在實例化的時候,prototype上的屬性會作爲原型對象賦值給實例, 也就是Pepig原型
Pepig.__proto__ === Pig.prototype // true
// Pig是一個函數對象, 它是Function對象的一個實例 Funtcion的原型對象又指向Object對象
Pig.__proto__ === Function.prototype // true
Function.prototype.__proto__ === Object.prototype // true
Object.prototype.__proto__ === null // true
// 原型對象上的constructor指向構造函數本身
Pig.prototype.constructor = Pig
繼承的方式
1.原型鏈繼承
function P(name: string){
this.name = name
}
P.prototype.sayName = function(){
console.log('P', this.name)
}
function C(name: string){
this.name = name
}
C.prototype = new P('P')
C.prototype.constructor = C
C.prototype.sayName = function(){
console.log('C', this.name)
}
const child = new C('C')
child.sayName() // 'C' C
缺點
- 子類型無法給超類型傳遞參數,在面向對象的繼承中,我們總希望通過 var child = new Child(‘son’, ‘father’); 讓子類去調用父類的構造器來完成繼承。而不是通過像這樣 new Parent(‘father’) 去調用父類。
- 引用類型值的原型屬性會被所有實例共享
2.借用構造函數(經典繼承)
function P(age: number){
this.names = ['alex', 'jet']
this.age = age
}
function C(age: number){
P.call(this, age)
}
const child = new C()
child.names.push('mark')
child.names // alex jet mark
const child1 = new C()
child1.names // alex jet
const child = new C('15')
child.age // 15
const child = new C('18')
child.age // 18
優點
- 避免了引用類型的屬性被所有實例共享
- 可以在 Child 中向 Parent 傳參
缺點
- 方法都在構造函數中定義,因此函數複用就無從談起了。而且,在超類型的原型中定義的方法,對子類型而言也是不可見的。
3.組合繼承
function P(name: string){
this.name = name
this.colors = ['yellow', 'blue']
}
P.prototype.getName = function(){
console.log(this.name)
}
function C(name: string, age: number){
P.call(this, name)
this.age = age
}
C.prototype = new P()
const child = new C('jet', 18)
child.colors.push('black') // yellow blue black
child.name // jet
child.age // 18
const child1 new C('jack', 20)
child1.colors // yellow blue
child1.name // jack
child1.age // 20
優點
- 融合原型鏈繼承和構造函數的優點,是 JavaScript 中最常用的繼承模式。
- 都會調用兩次超類型構造函數
4.原型式繼承
function object(o: Object){
function F(){}
F.prototype = o
return new F()
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
}
var anotherPerson = object(person)
anotherPerson.name = "Greg"
anotherPerson.friends.push("Rob")
var yetAnotherPerson = object(person)
yetAnotherPerson.name = "Linda"
yetAnotherPerson.friends.push("Barbie")
alert(person.friends) //"Shelby,Court,Van,Rob,Barbie"
特點
- 包含引用類型值的屬性始終都會共享相應的值,就像使用原型模式一樣
5.寄生式繼承
思想: 寄生式繼承的思路與寄生構造函數和工廠模式類似,即創建一個僅用於封裝繼承過程的函數,該
函數在內部以某種方式來增強對象,最後再像真地是它做了所有工作一樣返回對象
function createAnother(original){
var clone = object(original) //通過調用函數創建一個新對象
clone.sayHi = function(){ //以某種方式來增強這個對象
alert("hi")
}
return clone //返回這個對象
}
缺點
- 使用寄生式繼承來爲對象添加函數,會由於不能做到函數複用而降低效率
6.寄生組合式繼承
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); //創建對象
prototype.constructor = subType; //增強對象
subType.prototype = prototype; //指定對象
}
優點
1.只調用了一次 SuperType 構造函數,並且因此避免了在 SubType.
prototype 上面創建不必要的、多餘的屬性。與此同時,原型鏈還能保持不變;因此,還能夠正常使用
instanceof 和 isPrototypeOf()