1. 程式人生 > >【重寫 CryptoJS】一、ECMAScript 類與繼承

【重寫 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);
});