ES6做爲新一代JavaScript標準,已正式與廣大前端開發者見面。爲了讓你們對ES6的諸多新特性有更深刻的瞭解,Mozilla Web開發者博客推出了《ES6 In Depth》系列文章。CSDN已獲受權,將持續對該系列進行翻譯,組織成【探祕ES6】系列專欄,供你們學習借鑑。本文爲該系列的第十一篇。
前端
在感覺了本系列文章前幾篇的複雜程度後,咱們如今得以有片刻的喘息。再沒有聞所未聞的編碼方式,使用生成器(Generator)編碼;再沒有無所不能的代理對象(Proxy Object),爲JavaScript語言內部算法實現提供了鉤子函數;再沒有新的數據結構,避免了用戶自主開發的須要。相反,咱們要說說與一箇舊問題相關的語法和清理技法(idiom),那就是JavaScript中對象構造函數的建立。es6
問題算法
咱們要說的是,建立面向對象設計原則中最典型的例子:Circle類。想象咱們正在爲Canvas庫編寫一個Circle類,除此以外,恐怕還要知道如何去作如下幾點:express
爲給定的Canvas畫一個Circle。編程
記錄所畫Circle的個數。canvas
記錄給定Circle的半徑,以及如何給不變量(invariant)強行賦值。瀏覽器
計算給定Circle的面積。數據結構
目前JS的慣用技法是先拿構造函數看成函數來建立,而後向函數添加任何想要添加的屬性,再用一個對象替換構造函數中的prototype屬性。該prototype對象包含構造函數最初所建立實例的全部屬性。雖然這只是一個簡單的例子,但敲出來之後,會是不少樣板(boilerplate)代碼:架構
function Circle(radius) { this.radius = radius; Circle.circlesMade++; } Circle.draw = function draw(circle, canvas) { /* Canvas drawing code */ } Object.defineProperty(Circle, "circlesMade", { get: function() { return !this._count ? 0 : this._count; }, set: function(val) { this._count = val; } }); Circle.prototype = { area: function area() { return Math.pow(this.radius, 2) * Math.PI; } }; Object.defineProperty(Circle.prototype, "radius", { get: function() { return this._radius; }, set: function(radius) { if (!Number.isInteger(radius)) throw new Error("Circle radius must be an integer."); this._radius = radius; } });這樣的代碼不只冗長,並且不夠直觀。不是一會兒就能理解函數是如何工做的,也不是很容易理解各個屬性用什麼方式綁定到所建立的實例對象的。即便這樣的實現方式看起來比較複雜也沒必要擔憂。本文的主旨就是要展現一種更簡單的編碼方式,用來解決全部這些問題。
定義方法的語法編程語言
首次嘗試去規範這個問題時,ES6給出了一種新的語法來爲對象添加特殊屬性。雖然很容易在上面的Circle.prototype添加area方法,可是對radius添加一對getter和setter讓人感受過於囉嗦。因爲JS更加傾向於面向對象化的解決方案,因此人們比較關心如何用簡潔的方式給屬性添加訪問器(accessor)。咱們須要一種新的方式給對象添加「方法」,就像obj.prop = method那樣被添加進去,而不須要用Object.defineProperty。你們但願可以輕鬆作到下面幾件事:
給對象添加普通函數(normal function)。
給對象添加生成器函數(generator function)。
給對象添加普通訪問器函數屬性(accessor function property)。
給已建立的對象添加上述任何函數,好像使用方括號[]語法就能完成的樣子。咱們稱之爲計算屬性名(computed property name)。
其中的一些事情之前是無法完成的。例如,無法給obj.prop定義getter或setter對其進行賦值,所以要添加新的語法功能。如今你就能夠編寫像下面這樣的代碼了。
var obj = { // Methods are now added without a function keyword, using the name of the // property as the name of the function. method(args) { ... }, // To make a method that's a generator instead, just add a '*', as normal. *genMethod(args) { ... }, // Accessors can now go inline, with the help of |get| and |set|. You can // just define the functions inline. No generators, though. // Note that a getter installed this way must have no arguments get propName() { ... }, // Note that a setter installed this way must have exactly one argument set propName(arg) { ... }, // To handle case (4) above, [] syntax is now allowed anywhere a name would // have gone! This can use symbols, call functions, concatenate strings, or // any other expression that evaluates to a property id. Though I've shown // it here as a method, this syntax also works for accessors or generators. [functionThatReturnsPropertyName()] (args) { ... } };咱們可使用新的語法重寫上面的代碼段:
function Circle(radius) { this.radius = radius; Circle.circlesMade++; } Circle.draw = function draw(circle, canvas) { /* Canvas drawing code */ } Object.defineProperty(Circle, "circlesMade", { get: function() { return !this._count ? 0 : this._count; }, set: function(val) { this._count = val; } }); Circle.prototype = { area() { return Math.pow(this.radius, 2) * Math.PI; }, get radius() { return this._radius; }, set radius(radius) { if (!Number.isInteger(radius)) throw new Error("Circle radius must be an integer."); this._radius = radius; } };嚴格地說,這段代碼與其上面那段並不徹底相同。方法定義中所使用的對象字面量(object literal)被設置成了configurable和enumerable。而在上一個代碼段中的訪問器則是non-configurable和non-enumerable的。實踐中,這點不多被注意到,爲了簡潔,我決定忽略掉上面這兩點。
不過,應該變得更好了對嗎?很遺憾,即便有了這樣新的定義方法的語法,咱們對Circle的定義仍然沒法作太多的事情。由於尚未定義函數。沒有辦法將屬性綁定到你尚在定義的函數上去。
定義類的語法
雖然這樣更好,但仍然沒法使人滿意,人們想要一種更簡潔的JavaScript面向對象設計解決方案。他們說其餘編程語言爲了解決面向對象設計而擁有一種結構,這種結構被稱爲類。
很公平,那麼就來添加類。
咱們須要一個系統,容許將方法添加到已命名的構造函數當中,並還能添加系統的.prototype屬性中。這樣這些方法就會出如今類的結構化實例中。因爲有新奇的語法能夠定義方法,咱們應該用一下。而後,只須要區分所添加的方法屬於類的全部實例,仍是專屬於某個給定的實例。在C++和Java語言中,解決這一問題的關鍵字是static。用在這裏看起來也不錯,用一下吧!
如今將衆多方法其中的一個指定爲函數,它會被稱爲構造函數。在C++和Java語言中,構造函數的名稱要與類名一致,而且沒有返回類型。因爲JS沒有返回類型,因此爲了向後兼容,的確須要一個.constructor 屬性。咱們稱這個方法爲構造函數。
綜上所述,能夠重寫Circle類了:
class Circle { constructor(radius) { this.radius = radius; Circle.circlesMade++; }; static draw(circle, canvas) { // Canvas drawing code }; static get circlesMade() { return !this._count ? 0 : this._count; }; static set circlesMade(val) { this._count = val; }; area() { return Math.pow(this.radius, 2) * Math.PI; }; get radius() { return this._radius; }; set radius(radius) { if (!Number.isInteger(radius)) throw new Error("Circle radius must be an integer."); this._radius = radius; }; }哇!咱們不只能夠把與Circle相關的一切組織在一塊兒,並且一切看起來如此地整潔。這絕對比一開始好多了。
即使如此,有些人可能還會有問題,找出極端的例子。我就試着預測一下,並解決其中的一些問題。
分號用來作什麼?爲了「讓一切看起來更像傳統的類」,咱們決定使用更爲傳統的分隔符。不喜歡它嗎?這是可選的,分隔符並非必須的。
若是我不想要構造函數,但仍然想給已建立的對象添加方法該怎麼辦?很好!constructor方法徹底是可選的。若是不這樣作,默認狀況就像已經敲了constructor() {}。
「constructor」能夠是生成器嗎?不能夠!使用非普通方法(normal method)添加constructor會致使TypeError,包括生成器和訪問器。
可使用計算屬性名定義constructor嗎?遺憾的是不能夠。這將很是難以被探測。所以咱們就不試了。若是使用計算屬性名定義方法的話,最終方法會被命名爲「constructor」,仍會獲得一個名爲 constructor的方法,而不是類的構造函數。
若是改變Circle的值會怎樣?是否會致使出現新的Circle,而且出錯呢?不會的!就像函數表達式,類內部綁定了它們的名稱。外部力量沒法改變這個綁定。所以,在封閉範圍內無論怎樣設置Circle變量,構造函數中的Circle.circlesMade++都會按預期工做。
好吧,可是我能夠直接傳入對象字面量做爲函數的參數。使用新語法定義的類看起來行不通了。很幸運,ES6還增長了類表達式!能夠對其命名或者匿名。除了在聲明它們的範圍內不會建立變量,其行爲與上面描述的函數徹底相同。
上面的這些把戲若是是可枚舉的,或者有什麼其餘性質會怎麼樣?這樣作是爲了能夠給對象配置方法,可是當你對對象的屬性進行枚舉的時候,僅獲得了已經添加進對象的數據屬性。由於這是合理的,因此類裏所配置的方法是configurable的,但不是enumerable的。
喂,等等……實例變量在哪裏?static常量呢?好問題!在ES6中,類的定義目前不存在實例變量和靜態常量。不過好消息是,連同參與其餘規範的過程當中,我都強有力地支持在類的語法中設有static和const值。事實上,這已經出如今了規範相關的會議上。我想能夠期待之後出現更多與此有關的討論。
好吧,即使如此,這些也都是極好的!我可使用它們了嗎?不徹底能夠。有那些可選用的polyfill(特別是Babel)存在,你能夠試着使用它們。遺憾的是,在被主流瀏覽器原生地實現之前,還須要一些時間。咱們這裏討論的一切都在Nightly版的Firefox瀏覽器實現過。Edge和Chrome瀏覽器也實現了,可是默認狀況下並未被啓用。還有個遺憾就是Safari瀏覽器還沒實現這些新特性。
Java和C++都使用子類和super關鍵字,但這裏並沒提到,JS有相關概念嗎?有的!但這徹底是一個值得另外成文討論的事情。回頭再看咱們從此有關使用子類的更新,將討論更多有關JavaScript中類的威力。
譯者簡介:白雲鵬,移動應用開發者,關注前端技術、架構設計、移動應用開發。我的博客:http://baiyunpeng.com。
本譯文遵循Creative Commons Attribution Share-Alike License v3.0