es5 編寫類風格的代碼
分享下《JavaScript忍者秘籍》中的一種編寫類風格代碼的方法
JavaScript可以讓我們通過原型實現繼承,許多開發人員,尤其是那些有傳統面向對象背景的開發人員,都希望將JavaScript的繼承系統簡化并抽象成一個他們更熟悉的系統。
所以,這不可避免地引導我們走向類的領域。類是面向對象開發人員所期望的內容,盡管JavaScript本身不支持傳統的類繼承。
通常,這些開發人員希望它有如下特性:
- 一套可以構建新構造器函數和原型的輕量級系統
- 一種簡單的方式來執行原型繼承
- 一種可以訪問被函數原型所覆蓋的方法的途徑
以下代碼展示了一個可以實現上述目標的示例。
//通過subClass()方法,創建一個Person類作為Object的一個子類,該方法之后實現
var Person = Object.subClass({
init: function (isDancing) {
this.dancing = isDancing;
},
dance: function () {
return this.dancing;
}
});
//通過繼承Person類,創建一個Ninja子類
var Ninja = Person.subClass({
init: function () {
//需要一種調用父類構造器的方法——這里展示我們將這樣做
this._super(false);
},
dance: function () {
//Ninja-specific stuff here
return this._super();
},
swingSword: function () {
return true;
}
});
//創建一個實例對Person類進行測試,看其是否能夠跳舞
var person = new Person(true);
assert(person.dance(),
"The person is dancing.");
//創建一個實例對Ninja類進行測試,看其是否有swingSword方法以及繼承過來的dance方法
var ninja = new Ninja();
assert(ninja.swingSword(),
"The sword is swinging.");
assert(!ninja.dance(),
"The ninja is not dancing.");
//執行instanceof測試,驗證類的繼承
assert(person instanceof Person,
"Person is a Person");
assert(ninja instanceof Ninja && ninja instanceof Person,
"Ninja is a Ninja and a Person");</code></pre>
注意事項:
- 通過調用現有構造器函數的subClass()方法可以創建一個新“類”,例如,通過Object創建一個Person類,以及通過Person創建一個Ninja類
- 為了讓構造器的創建更加簡單。我們建議的語法是,為每個類只提供一個init()方法,就像為Person和Ninja提供的init()方法一樣
- 我們所有的“類”最終都繼承于一個祖先:Object。因此,如果要創建一個新類,它必須是Object的一個子類,或者是一個在層級上繼承于Object的類(完全模仿當前的原型系統)
- 該語法的最大挑戰是訪問被覆蓋的方法,而且有時這些方法的上下文也有可能被修改了。通過this._super()調用Person的原始init()和dance()方法,我們就可以了解這種用法
實現:
(function () {
var initializing = false,
//粗糙的正則表達式用于判斷函數是否可以被序列化。
superPattern =
/xyz/.test(function () {
xyz;
})?
/\b_super\b/:
/.*/;
//給Object添加一個subClass方法
Object.subClass = function (properties) {
var _super = this.prototype;
//初始化超類
initializing = true;
var proto = new this();
initializing = false;
for (var name in properties) {
//將屬性復制到prototype里
proto[name] = typeof properties[name] === 'function' &&
typeof _super[name] === 'function' &&
superPattern.test(properties[name]) ?
//定義一個重載函數
(function (name, fn) {
return function () {
var tmp = this._super;
this._super = _super[name];
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
}
})(name, properties[name]) :
properties[name];
}
//創造一個仿真類構造器
function Class() {
if (!initializing && this.init) {
this.init.apply(this, arguments);
}
}
//設置類的原型
Class.prototype = proto;
//重載構造器引用
Class.constructor = Class;
//讓類繼續可擴展
Class.subClass = arguments.callee;
return Class;
};
})();</code></pre>
檢測函數是否可序列化
代碼實現的一開始就很深奧,而且還可能讓人困惑。在后續代碼中,我們需要知道瀏覽器是否支持函數序列化。但該測試又有相當復雜的語法,所以現在就要得到結果,然后保存結果,以便在后續代碼中不再進行復雜的操作,因為后續代碼本身已經夠復雜了。
函數序列化就是簡單接收一個函數,然后返回該函數的源碼文本。稍后,我們可以使用這種方法檢查一個函數在我們感興趣的對象中是否存在引用。
在大多數瀏覽器中,函數的toString()方法都會奏效。一般來說 ,一個函數在其上下文中序列化成字符串,會導致它的toString()方法被調用。所以,可以用這種方法測試函數是否可以序列化。
在設置一個名為initializing的變量為false之后,我們使用如下表達式測試一個函數是否能夠被序列化:
/xyz/.test(function () { xyz; })
該表達式創建一個包含xyz的函數,將該函數傳遞給正則表達式的test()方法,該正則表達式對字符串“xyz”進行測試。如果函數能夠正常序列化(test()方法將接收一個字符串,然后將觸發函數的toString()方法),最終結果將返回true。
使用該文本表達式,我們在隨后的代碼中使用了該正則表達式:
superPattern =
/xyz/.test(function () {
xyz;
}) ?
/\b_super\b/ :
/.*/;
建立了一個名為superPattern的變量,稍后用它來判斷一個函數是否包含字符串"_super"。只有函數支持序列化才能進行判斷,所以在不支持序列化的瀏覽器上,我們使用一個匹配任意字符串的模式進行代替。
子類的實例化
此時,我們準備開始定義一個方法用于子類化父類,我們使用如下代碼進行實現:
Object.subClass = function (properties) {
var _super = this.prototype;
給Object添加一個subClass()方法,該方法接收一個參數,該參數是我們期望添加到子類的屬性集。
為了用函數原型模擬繼承,我們創建父類的一個實例,并將其賦值給子類的原型。我們在代碼中定義了一個initializing變量,每當我們想使用原型實例化一個類的時候,都將該變量設置為true。
因此,在構造實例時,我們可以確保不再實例化模式下進行構建實例,并可以相應地運行或跳過init()方法:
if (!initializing && this.init) {
this.init.apply(this, arguments);
}
尤其重要的是,init()方法可以運行各種昂貴的啟動代碼(連接到服務器、創建DOM元素,還有其他未知內容),所以如果只是創建一個實例作為原型的話,我們要避免任何不必要的昂貴啟動代碼。
保留父級方法
大多數支持繼承的語言中,在一個方法被覆蓋時,我們保留了訪問被覆蓋方法的能力。這是很有用的,因為有時候我們是想完全替換方法的功能,但有時候我們卻只是想增加它。在我們特定的實現中,我們創建一個名為_super的臨時新方法,該方法只能從子類方法內部進行訪問,并且該方法引用的是父類中的原有方法。
例如:
var Person = Object.subClass({
init: function (isDancing) {
this.dancing = isDancing;
}
});
var Ninja = Person.subClass({
init: function () {
this._super(false);
}
});</code></pre>
在Ninja構造器內,我們調用了Person的構造器,并傳入了一個相應的值。這可以防止重新復制代碼——我們可以重用父類中已經編寫好的代碼。
該功能的實現是一個多步驟的過程。為了增強子類,我們向subClass()方法傳入了一個對象哈希,只需要將父類的屬性和傳入的屬性合并在一起就可以了。
首先,使用如下代碼,創建一個超類的實例作為一個原型:
initializing = true;
var proto = new this();
initializing = false;
注意,我們是如何“保護”初始化代碼的,正如我們在前一節中討論的initializing變量的值。
現在,是時候將傳入的屬性合并到proto對象中了。如果不在意父類函數,合并代碼將非常簡單:
for (var name in properties) {
proto[name] = properties[name];
}
但是,我們需要關心父類的函數,所以前面的代碼和除了調用父類函數的函數之外是等價的。重寫函數時,可以通過_super調用父函數,我們需要通過名為_super的屬性,將子類函數和父類函數的引用進行包裝。但在完成該操作之前,我們需要檢測即將被包裝的子類函數。可以使用如下條件表達式:
typeof properties[name] === "function" &&
typeof _super[name] === "function" &&
superPattern.test( properties[name] )
這個表達式包含三個檢測條件:
- 子類屬性是否是一個函數?
- 超類屬性是否是一個函數?
- 子類函數是否都包含一個_super()引用?
只有三個條件都為true的時候,我們才能做所要做的事情,而不是復制屬性值。注意,我們使用了之前設置的正則表達式,和函數序列化一起,測試函數是否會調用等效的父類。
如果條件表達式表明我們必須包裝功能,我們通過給即時函數的結果進行賦值,將該結果作為子類的屬性:
(function (name, fn) {
return function () {
var tmp = this._super;
this._super = _super[name];
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
}
})(name, properties[name])</code></pre>
該即時函數創建并返回了一個新函數,該新函數包裝并執行了子類的函數,同時可以通過_super屬性訪問父函數。首先需要先保持舊的this._super引用(不管它是否存在),然后處理完以后再恢復該引用。這在同名變量已經存在的情況下會很有用(不想意外的丟失它)。
接下來,創建新的_super方法,它只是在父類原型中已經存在的一個方法的引用。值得慶幸的是,我們不需要做任何額外的代碼修改或作用域修改。當函數成為我們對象的一個屬性時,該函數的上下文會自動設置(this引用的是當前的子類實例,而不是父類實例)。
最后,調用原始的子類方法執行自己的工作(也有可能使用了_super),然后將_super恢復成原來的狀態,并將方法調用結果進行返回。
有很多方式可以達到類似的結果(有的實現,會通過訪問arguments.callee,將_super方法綁定到方法自身),但是該特定技術提供了良好的可用性和簡便性。
來自:https://juejin.im/post/58ff42cda0bb9f0065d37d4f