[譯] 繼承 JavaScript 類中的靜態屬性

馬猴燒酒發表於2018-12-25

自 ES6 釋出以來,JavaScript 對類和靜態函式的支援類似其他面嚮物件語言中的靜態函式。不幸的是,JavaScript 缺乏對靜態屬性的支援,而且谷歌上的推薦方案沒有考慮到繼承問題。在實現一個 Mongoose 特性的時候,我陷入了一個需要更健壯的靜態屬性概念的困難。尤其是我需要通過設定 prototype 或者 extends 來支援繼承靜態屬性。在本文,我將介紹在 ES6 中實現靜態屬性的模式。

靜態方法和繼承

假設你有一個帶有靜態方法的簡單的符合 ES6 語法的類。

class Base {
  static foo() {
    return 42;
  }
}
複製程式碼

你可以使用 extends 建立一個子類並且能夠繼續使用 foo() 函式。

class Sub extends Base {}

Sub.foo(); // 42
複製程式碼

你可以使用靜態的 getter 和 setterBase 類中設定一個靜態的屬性。

let foo = 42;

class Base {
  static get foo() { return foo; }
  static set foo(v) { foo = v; }
}
複製程式碼

不幸的是,在繼承 Base 的時候,這個模式就行不通了。如果你設定子類 foo 的值,它將會覆蓋 Base 和所有其他的子類的 foo

class Sub extends Base {}

console.log(Base.foo, Sub.foo);

Sub.foo = 43;

// 列印 "43, 43"。在上面會覆蓋 “Base.foo” 和 “Sub.foo” 的值
console.log(Base.foo, Sub.foo);
複製程式碼

如果屬性是一個陣列或者是物件這個問題會變得更糟。因為典型的繼承,如果 foo 是一個陣列,每一個子類都會有一個陣列副本的引用,如下所示。

class Base {
  static get foo() { return this._foo; }
  static set foo(v) { this._foo = v; }
}

Base.foo = [];

class Sub extends Base {}

console.log(Base.foo, Sub.foo);

Sub.foo.push('foo');

// 現在這兩個陣列都包含 “foo”,因為它們都是同一個陣列!
console.log(Base.foo, Sub.foo);
console.log(Base.foo === Sub.foo); // true
複製程式碼

所以 JavaScript 支援靜態的 getter 和 setter,但是在陣列和物件的情況下使用它們將會是搬起石頭砸自己腳。事實證明,你可以在 JavaScript 內建的 hasOwnProperty() 函式的幫助實現它。

繼承靜態屬性

關鍵思想是 JavaScript 類只是另一個物件,所以你可以區分 本身的屬性 和繼承的屬性。

class Base {
  static get foo() {
    // 如果 “_foo” 被繼承了,或者不存在的時候將它當做 “undefined”
    return this.hasOwnProperty('_foo') ? this._foo : void 0;
  }
  static set foo(v) { this._foo = v; }
}

Base.foo = [];

class Sub extends Base {}

// 列印 "[] undefined"
console.log(Base.foo, Sub.foo);
console.log(Base.foo === Sub.foo); // false

Base.foo.push('foo');

// 列印 "['foo'] undefined"
console.log(Base.foo, Sub.foo);
console.log(Base.foo === Sub.foo); // false
複製程式碼

這個模式在類中的實現是很簡潔,它也可以被用於 ES6 之前的 JavaScript 標準的繼承。這一點很重要,因為 Mongoose 仍然使用 ES6 風格之前的繼承。事後看來,我們本應該儘早使用這個方法,這個特性是我們第一次看到使用 ES6 類和繼承比只設定函式的 prototype 有明顯優勢。

function Base() {}

Object.defineProperty(Base, 'foo', {
  get: function() { return this.hasOwnProperty('_foo') ? this._foo : void 0; },
  set: function(v) { this._foo = v; }
});

Base.foo = [];

// ES6 之前版本的繼承
function Sub1() {}
Sub1.prototype = Object.create(Base.prototype);
// Static properties were annoying pre-ES6
Object.defineProperty(Sub1, 'foo', Object.getOwnPropertyDescriptor(Base, 'foo'));

// ES6 的繼承
class Sub2 extends Base {}

// 列印 "[] undefined"
console.log(Base.foo, Sub1.foo);
// 列印 "[] undefined"
console.log(Base.foo, Sub2.foo);

Base.foo.push('foo');

// 列印 "['foo'] undefined"
console.log(Base.foo, Sub1.foo);
// 列印 "['foo'] undefined"
console.log(Base.foo, Sub2.foo);
複製程式碼

繼續前進

ES6 類相對於老的 Sub.prototype = Object.create(Base.prototype) 有一個主要的優勢,因為它 extends 了靜態屬性和函式的副本。使用 Object.hasOwnProperty() 做一些額外的工作,就可以建立正確處理繼承的靜態 getter 和 setter。在 JavaScript 中使用靜態屬性要非常地小心:extends 在底層仍然使用典型的繼承。這意味著,除非你使用本篇文章提到的 hasOwnProperty() 模式,否則靜態的物件和陣列在所有的子類中被共享。

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章