不積跬步無以至千里。
關於【Step-By-Step】
Step-By-Step (點擊進入項目) 是我於2019-05-20
開始的一個項目,每個工作日發佈一道面試題。每個週末我會仔細閱讀大家的答案,整理最一份較優答案出來,因本人水平有限,有誤的地方,大家及時指正。
如果想 加羣 學習,可以通過文末的公衆號,添加我爲好友。
本週面試題一覽:
- 原型鏈繼承的基本思路是什麼?有什麼優缺點?
- 借用構造函數和組合繼承基本思路是什麼?有什麼優缺點?
- 原型式繼承的基本思路是什麼?有什麼優缺點?
- 寄生式繼承的基本思路是什麼?有什麼優缺點?
- 寄生組合式繼承的基本思路是什麼?有什麼優缺點?
本週是繼承專題,在開始之前,需要先了解構造函數、原型和原型鏈的相關知識。
構造函數
構造函數和普通函數的區別僅在於調用它們的方式不同,任何函數,只要通過 new
操作符來調用,那它就可以作爲構造函數;任何函數,如果不通過 new
操作符來調用,那麼它就是一個普通函數。
實例擁有 constructor(構造函數)
屬性,該屬性返回創建實例對象的構造函數。
function Person(name, age) {
this.name = name;
this.age = age;
}
var Yvette = new Person('劉小夕', 20);
console.log(Yvette.constructor === Person); //true
有一點需要說明的是,除了基本數據類型的 constructor
外( null
和 undefined
無 constructor
屬性),constructor
屬性是可以被重寫的。因此檢測對象類型時,instanceof
操作符比 contsrutor
更可靠一些。
function Person(name) {
this.name = name;
}
function SuperType() { }
var Yvette = new Person('劉小夕');
console.log(Yvette.constructor); //[Function: Person]
Yvette.constructor = SuperType;
console.log(Yvette.constructor); //[Function: SuperType]
原型
我們創建的每個函數都有 prototype
屬性,這個屬性指向函數的原型對象。原型對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。
在默認情況下,所有原型對象都會自動獲得一個 constructor
屬性,這個屬性包含一個指向 prototype
屬性所在函數的指針。
當調用構造函數創建一個新實例後,該實例的內部將包含一個指針,指向構造函數的原型對象(可以通過實例的 __proto__
來訪問構造函數的原型對象)。
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(this.name);
}
var person1 = new Person('劉小夕');
var person2 = new Person('前端小姐姐');
//構造函數原型對象上的方法和屬性被實例共享
person1.sayName();
person1.sayName();
實例.__proto__ === 構造函數.prototype
原型鏈
簡單回顧一下構造函數、原型和實例的關係:
每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個可以執行原型對象的內部指針(可以通過 __proto
訪問)。
假如我們讓原型對象等於另一個類型的實例,那麼此時原型對象包含一個指向另一個原型的指針,相應地,另一個原型中也包含着一個指向另一個構造函數的指針。加入另一個原型又是另一個類型的實例,那麼上述關係仍然成立,如此層層遞進,就構成了實例與原型的鏈條,這就是原型鏈的基本概念。
function SuperType() {
this.type = 'animal';
}
SuperType.prototype.getType = function() {
console.log(this.type);
}
function SubType() {
}
SubType.prototype = new SuperType();
SubType.prototype.sayHello = function() {
console.log('hello');
}
function SimType(name) {
this.name = name;
}
SimType.prototype = new SubType();
SimType.prototype.sayHi = function() {
console.log('hi');
}
var instance = new SimType('劉小夕');
instance.getType();
一圖勝萬言:
調用 instance.getType()
會調用以下的搜索步驟:
- 搜索
instance
實例 - 搜索
SimType.prototype
- 搜索
SubType.prototype
- 搜索
SuperType.prototype
,找到了getType
方法
在找不到屬性或方法的情況下,搜索過程總是要一環一環地前行到原型鏈的末端纔會停下來。
所有引用類型都繼承了 Object
,這個繼承也是通過原型鏈實現的。如果在 SuperType.prototype
還沒有找到 getType
,就會到 Object.prototype
中找(圖中少畫了一環)。
25. 原型鏈繼承
原型鏈繼承的基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。
如 SubType.prototype = new SuperType()
;
function SuperType() {
this.name = 'Yvette';
this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.getName = function () {
return this.name;
}
function SubType() {
this.age = 22;
}
SubType.prototype = new SuperType();
SubType.prototype.getAge = function() {
return this.age;
}
SubType.prototype.constructor = SubType;
let instance1 = new SubType();
instance1.colors.push('yellow');
console.log(instance1.getName()); //'Yvette'
console.log(instance1.colors);//[ 'pink', 'blue', 'green', 'yellow' ]
let instance2 = new SubType();
console.log(instance2.colors);//[ 'pink', 'blue', 'green', 'yellow' ]
可以看出 colors
屬性會被所有的實例共享(instance1、instance2、...)。
缺點:
- 通過原型來實現繼承時,原型會變成另一個類型的實例,原先的實例屬性變成了現在的原型屬性,該原型的引用類型屬性會被所有的實例共享。
- 在創建子類型的實例時,沒有辦法在不影響所有對象實例的情況下給超類型的構造函數中傳遞參數。
26. 借用構造函數
借用構造函數的技術,其基本思想爲:
在子類型的構造函數中調用超類型構造函數。
function SuperType(name) {
this.name = name;
this.colors = ['pink', 'blue', 'green'];
}
function SubType(name) {
SuperType.call(this, name);
}
let instance1 = new SubType('Yvette');
instance1.colors.push('yellow');
console.log(instance1.colors);//['pink', 'blue', 'green', yellow]
let instance2 = new SubType('Jack');
console.log(instance2.colors); //['pink', 'blue', 'green']
優點:
- 可以向超類傳遞參數
- 解決了原型中包含引用類型值被所有實例共享的問題
缺點:
- 方法都在構造函數中定義,函數複用無從談起,另外超類型原型中定義的方法對於子類型而言都是不可見的。
27. 組合繼承
組合繼承指的是將原型鏈和借用構造函數技術組合到一塊,從而發揮二者之長的一種繼承模式。基本思路:
使用原型鏈實現對原型屬性和方法的繼承,通過借用構造函數來實現對實例屬性的繼承,既通過在原型上定義方法來實現了函數複用,又保證了每個實例都有自己的屬性。
function SuperType(name) {
this.name = name;
this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
}
function SuberType(name, age) {
SuperType.call(this, name);
this.age = age;
}
SuberType.prototype = new SuperType();
SuberType.prototype.constructor = SuberType;
SuberType.prototype.sayAge = function () {
console.log(this.age);
}
let instance1 = new SuberType('Yvette', 20);
instance1.colors.push('yellow');
console.log(instance1.colors); //[ 'pink', 'blue', 'green', 'yellow' ]
instance1.sayName(); //Yvette
let instance2 = new SuberType('Jack', 22);
console.log(instance2.colors); //[ 'pink', 'blue', 'green' ]
instance2.sayName();//Jack
缺點:
- 無論什麼情況下,都會調用兩次超類型構造函數:一次是在創建子類型原型的時候,另一次是在子類型構造函數內部。
優點:
- 可以向超類傳遞參數
- 每個實例都有自己的屬性
- 實現了函數複用
28. 原型式繼承
原型繼承的基本思想:
藉助原型可以基於已有的對象創建新對象,同時還不必因此創建自定義類型。
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
在 object()
函數內部,先穿甲一個臨時性的構造函數,然後將傳入的對象作爲這個構造函數的原型,最後返回了這個臨時類型的一個新實例,從本質上講,object()
對傳入的對象執行了一次淺拷貝。
ECMAScript5通過新增 Object.create()
方法規範了原型式繼承。這個方法接收兩個參數:一個用作新對象原型的對象和(可選的)一個爲新對象定義額外屬性的對象(可以覆蓋原型對象上的同名屬性),在傳入一個參數的情況下,Object.create()
和 object()
方法的行爲相同。
var person = {
name: 'Yvette',
hobbies: ['reading', 'photography']
}
var person1 = Object.create(person);
person1.name = 'Jack';
person1.hobbies.push('coding');
var person2 = Object.create(person);
person2.name = 'Echo';
person2.hobbies.push('running');
console.log(person.hobbies);//[ 'reading', 'photography', 'coding', 'running' ]
console.log(person1.hobbies);//[ 'reading', 'photography', 'coding', 'running' ]
在沒有必要創建構造函數,僅讓一個對象與另一個對象保持相似的情況下,原型式繼承是可以勝任的。
缺點:
同原型鏈實現繼承一樣,包含引用類型值的屬性會被所有實例共享。
29. 寄生式繼承
寄生式繼承是與原型式繼承緊密相關的一種思路。寄生式繼承的思路與寄生構造函數和工廠模式類似,即創建一個僅用於封裝繼承過程的函數,該函數在內部已某種方式來增強對象,最後再像真地是它做了所有工作一樣返回對象。
function createAnother(original) {
var clone = object(original);//通過調用函數創建一個新對象
clone.sayHi = function () {//以某種方式增強這個對象
console.log('hi');
};
return clone;//返回這個對象
}
var person = {
name: 'Yvette',
hobbies: ['reading', 'photography']
};
var person2 = createAnother(person);
person2.sayHi(); //hi
基於 person
返回了一個新對象 -—— person2
,新對象不僅具有 person
的所有屬性和方法,而且還有自己的 sayHi()
方法。在考慮對象而不是自定義類型和構造函數的情況下,寄生式繼承也是一種有用的模式。
缺點:
- 使用寄生式繼承來爲對象添加函數,會由於不能做到函數複用而效率低下。
- 同原型鏈實現繼承一樣,包含引用類型值的屬性會被所有實例共享。
30. 寄生組合式繼承
所謂寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法,基本思路:
不必爲了指定子類型的原型而調用超類型的構造函數,我們需要的僅是超類型原型的一個副本,本質上就是使用寄生式繼承來繼承超類型的原型,然後再將結果指定給子類型的原型。寄生組合式繼承的基本模式如下所示:
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype); //創建對象
prototype.constructor = subType;//增強對象
subType.prototype = prototype;//指定對象
}
- 第一步:創建超類型原型的一個副本
- 第二步:爲創建的副本添加
constructor
屬性 - 第三步:將新創建的對象賦值給子類型的原型
至此,我們就可以通過調用 inheritPrototype
來替換爲子類型原型賦值的語句:
function SuperType(name) {
this.name = name;
this.colors = ['pink', 'blue', 'green'];
}
//...code
function SuberType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SuberType, SuperType);
//...code
優點:
只調用了一次超類構造函數,效率更高。避免在SuberType.prototype
上面創建不必要的、多餘的屬性,與其同時,原型鏈還能保持不變。
因此寄生組合繼承是引用類型最理性的繼承範式。
ES6 繼承
Class
可以通過extends關鍵字實現繼承,如:
class SuperType {
constructor(age) {
this.age = age;
}
getAge() {
console.log(this.age);
}
}
class SubType extends SuperType {
constructor(age, name) {
super(age); // 調用父類的constructor(x, y)
this.name = name;
}
getName() {
console.log(this.name);
}
}
let instance = new SubType(22, '劉小夕');
instance.getAge(); //22
對於ES6的 class
需要做以下幾點說明:
- 類的數據類型就是函數,類本身就指向構造函數。
console.log(typeof SuperType);//function
console.log(SuperType === SuperType.prototype.constructor); //true
- 類的內部所有定義的方法,都是不可枚舉的。(ES5原型上的方法默認是可枚舉的)
Object.keys(SuperType.prototype);
-
constructor
方法是類的默認方法,通過new
命令生成對象實例時,自動調用該方法。一個類必須有constructor
方法,如果沒有顯式定義,一個空的constructor
方法會被默認添加。 -
Class
不能像構造函數那樣直接調用,會拋出錯誤。
使用 extends
關鍵字實現繼承,有一點需要特別說明:
- 子類必須在
constructor
中調用super
方法,否則新建實例時會報錯。如果沒有子類沒有定義constructor
方法,那麼這個方法會被默認添加。在子類的構造函數中,只有調用super
之後,才能使用this
關鍵字,否則報錯。這是因爲子類實例的構建,基於父類實例,只有super方法才能調用父類實例。
class SubType extends SuperType {
constructor(...args) {
super(...args);
}
}
參考文章:
[1] 珠峯架構課(牆裂推薦)
[2] CSS-清除浮動
[3] 詳解JS函數柯里化
[4] JavaScript數組去重
謝謝各位小夥伴願意花費寶貴的時間閱讀本文,如果本文給了您一點幫助或者是啓發,請不要吝嗇你的贊和Star,您的肯定是我前進的最大動力。 https://github.com/YvetteLau/...
關注公衆號,加入技術交流羣。