【重寫 CryptoJS】一、ECMAScript 類與繼承
原始碼地址: entronad/crypto-es
無論是前端還是後端,資訊的加解密、摘要校驗是常常碰到的需求,開發中一旦涉及到敏感資料,什麼 MD5 、 Base64 、 AES 演算法基本上都是要來上一套的。
在 JavaScript 的各種加密演算法工具庫中, CryptoJS 以其全面的功能、良好的通用性,一直是首選。它誕生較早,主倉庫的程式碼還是託管在 Google Code 上,雖然後續也被移植到了 npm 上,持續有維護和更新,但由於歷史原因,還是有幾點在現在看來不太合時宜:
- 物件採用一套自己實現的原型繼承( prototypal inheritance )系統
- 通過名為 Wordarray 的自定義類以 32 位整數的方式進行位操作
在 ES6 之前的年代裡,這兩點還是很巧妙和創新的,規避了 JavaScript 語言本身的缺點,同時也保證了瀏覽器相容性和開箱即用。不過既然新的 ECMAScript 規範已經添加了類定義和 ArrayBuffer ,解決了原本的問題,我想嘗試利用最新的 ECMAScript 特性對 CryptoJS 進行實驗性的重寫。
重寫的專案定名為 CryptoES ,既然是實驗性的,生產應用和相容性等就不多作考慮,對使用場景的定義為:滿足 ECMAScript 的最新標準( 2018 ),比如模組採用 ECMAScript Module 而非 CommonJs ;成員變數定義還是提案就先不用。在 Babel 、 loader hook 的幫助下程式碼已經能在各種環境下了,未來隨著 ES 規範的普及,可直接使用的場景會更多。
此外,既然是重寫,還要保證所有使用介面不變。
ECMAScript 類與繼承
CryptoJS 在核心架構中擴充套件了 JavaScript 的原型鏈,自己實現了一套原型繼承體系,具有 extend 、 override 、 mixin 等功能,使其比較符合通用的面向物件變成的習慣,可以說與 ECMAScript 類異曲同工,我們重寫的第一步就是直接應用 ECMAScript 類與繼承改造它,去除冗餘和變扭之處,使其更符合規範。再這之前,先看一下 ECMAScript 類中我們會用到的一些關鍵點:
constructor
我們知道,ES6 中以 class 為關鍵字的類定義只是一個語法糖,它本質上還是一個建構函式,因此可以通過例項的 constructor 屬性,獲取該例項的類。這有什麼用呢,我們可以在例項方法中通過 new this.constructor()
this
與 JavaScript 傳統的原型鏈繼承不同,ECMAScript 類的繼承是先呼叫父類的建構函式,新增父類例項物件的屬性和方法到 this 上面,然後再呼叫子類的建構函式修改 this,這就使得我們在子類中定義的方法可以正確的新增和覆蓋。
值得注意的是,例項方法中的 this 指向的是例項,而靜態方法中的 this 則指向類,因此類定義中可以通過靜態方法裡的 new this()
實現工廠模式。
super
在類的定義中,加了括號的 super() 指父類的建構函式,不加括號的 super 類似 this ,在靜態方法中指父類,在例項方法中指父類的原型物件,因此在子類重寫例項方法中,可以通過 super.overridedMethod.call(this)
的方式先呼叫一下父類的該方法
prototype 與 __proto__
類本質上是建構函式,因此類有 prototype 屬性指向類的原型物件。
類本身也是一個物件,它也有 __proto__ 屬性,指向父類。而類的原型物件的 __proto__ 則指向父類的原型物件,這樣就實現了原型鏈。
例項的 __proto__ 指向類的原型物件。
通過這些屬性可以獲取類或例項的繼承關係。
CryptoJS 類風格改造
CryptoJS 目前比較常用的 npm 版是託管在 GitHub 上的:brix/crypto-js, CryptoES 也以此為參考基準。其核心的物件定義在 core.js 檔案中。
CryptoJS 通過名為 Base 的物件實現原型繼承:
var Base = C_lib.Base = (function () {
return {
extend: function (overrides) {
// Spawn
var subtype = create(this);
// Augment
if (overrides) {
subtype.mixIn(overrides);
}
// Create default initializer
if (!subtype.hasOwnProperty('init') || this.init === subtype.init) {
subtype.init = function () {
subtype.$super.init.apply(this, arguments);
};
}
// Initializer's prototype is the subtype object
subtype.init.prototype = subtype;
// Reference supertype
subtype.$super = this;
return subtype;
},
create: function () {
var instance = this.extend();
instance.init.apply(instance, arguments);
return instance;
},
init: function () {
},
};
}());
具體來說,繼承是通過給呼叫“父”物件的 extend 方法傳入需要重寫的成員變數和方法,生成新的“子”物件。通過這些物件的 create 方法來返回真正的例項,而 init 方法則在例項建立時被呼叫,起到建構函式的作用。這種方式存在的缺點是不能像習慣的那樣使用 new 關鍵字來新建例項,而且每個物件會在 $super 中遞迴的儲存繼承鏈中所有”父“物件的例項,資訊冗餘。
這些功能,通過 ECMAScript 類的 extend 關鍵字和建構函式就可以很好實現,無需額外的程式碼,且避免以上問題。不過為了儲存使用介面不變,我們還是保留 create 作為一個靜態方法,以便通過 ClassName.create()
的方式建立例項,同時使用 rest 運算子和解構賦值,將傳遞給 create 方法的引數賦給真正的建構函式:
export class Base {
static create(...args) {
return new this(...args);
}
}
Base 中提供的一個基本功能是 mixin,在 CryptoJS 的原型繼承體系中,類似 Base 這樣的“類”物件和例項物件的界限並不明晰,mixin 是可以可通過“類”物件來呼叫的:
mixIn: function (properties) {
for (var propertyName in properties) {
if (properties.hasOwnProperty(propertyName)) {
this[propertyName] = properties[propertyName];
}
}
// IE won't copy toString using the loop above
if (properties.hasOwnProperty('toString')) {
this.toString = properties.toString;
}
},
但從邏輯和實際的使用情況來看,mixin 應該是一個例項方法,其實就是實現了 Object.assign() 的功能:
mixIn(properties) {
return Object.assign(this, properties);
}
最後是例項拷貝的功能,按照原型繼承思路,它的實現是比較變扭的:
clone: function () {
return this.init.prototype.extend(this);
}
我們按比較直白的方式來,通過 new this.constructor()
使其不需指明類名,適用於任意物件的例項:
clone() {
const clone = new this.constructor();
Object.assign(clone, this);
return clone;
}
完成了 Base 類後,所需要的各種核心類都可以通過類繼承的方式來獲取這些基本方法。並且使用了 ECMAScript 類後,很多呼叫方式可以更為規範,比如原先 Wordarray 定義中構造新例項的兩個方法:
var WordArray = C_lib.WordArray = Base.extend({
init: function (words, sigBytes) {
words = this.words = words || [];
if (sigBytes != undefined) {
this.sigBytes = sigBytes;
} else {
this.sigBytes = words.length * 4;
}
},
random: function (nBytes) {
...
return new WordArray.init(words, nBytes);
}
});
現在可以通過建構函式和類的靜態方法實現:
export class WordArray extends Base {
constructor(words = [], sigBytes = words.length * 4) {
super();
this.words = words;
this.sigBytes = sigBytes;
}
static random(nBytes) {
...
return new WordArray(words, nBytes);
}
}
單元測試
在單元測試中,我們額外要測試一下類的繼承關係是否正確實現了,即
- 子類的 __proto__ 指向父類
- 子類例項的原型物件與父類原型物件在原型鏈中是上下級
data.Obj = class Obj extends C.lib.Base {
};
data.obj = data.Obj.create();
it('class inheritance', () => {
expect(data.Obj.__proto__).toBe(C.lib.Base);
});
it('object inheritance', () => {
expect(data.obj.__proto__.__proto__).toBe(C.lib.Base.prototype);
});