深入淺出ES6(十三):類 Class
目前面臨的問題
假如我們想要建立一個經典的面向物件設計示例:Circle類。想象一下我們正在為一個簡單的Canvas庫編寫這個Circle類,在眾多需要考慮的因素中,我們可能更想了解以下功能的實現方式:
- 在給定的Canvas上繪製一個給定圓。
- 跟蹤記錄生成圓的總數。
- 跟蹤記錄給定圓的半徑,以及如何使其值成為圓的不變條件。
- 計算給定圓的面積。
按照目前常見的JS編碼風格,我們首先應該以函式的形式建立一個建構函式,然後給該函式新增任何我們可能想要的屬性,然後用一個物件替換建構函式的prototype
屬性。這個prototype
物件將包含建構函式建立的例項的所有初始化屬性。下面是一個簡單的示例,可以直接作為樣板檔案(boilerplate)重複使用:
function Circle(radius) {
this.radius = radius;
Circle.circlesMade++;
}
Circle.draw = function draw(circle, canvas) { /* Canvas繪製程式碼 */ }
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("圓的半徑必須為整數。");
this._radius = radius;
}
});
這段程式碼非常繁瑣且不符合人的直覺,要想讀懂必須對函式的執行方式有著非凡的掌握,然後你才能理解各種已裝載的屬性與生成的例項物件進行繫結的方式。如果這種方法看起來很複雜,不要擔心,這篇文章會為你展示一種更簡單的方法來實現所有這些功能。
方法定義語法
ES6提供一種向物件新增特殊屬性的新語法,可以幫助我們清理這些方法。給Circle.prototype
新增area
方法非常簡單,但是給radius
新增getter/setter方法對就很難。隨著JS引入越來越多的面向物件方法,人們開始對簡化給物件新增訪問器的方法感興趣。我們需要一種功能類似obj.prop = method
的新方法來給物件新增“方法”,同時不借助Object.defineProperty
的力量。人們想要能夠簡單地實現以下功能:
- 給物件新增標準的函式屬性。
- 給物件新增生成器函式屬性。
- 給物件新增標準的訪問器函式屬性。
- 給物件新增任意使用
[]
語法新增的函式屬性,我們稱其為預計算(computed)屬性名。
其中一些功能在以前無法實現,例如:我們不能通過給obj.prop
賦值來定義getter或setter。因此,我們亟需新語法來編寫以下程式碼:
var obj = {
// 現在不再使用function關鍵字給物件新增方法
// 而是直接使用屬性名作為函式名稱。
method(args) { ... },
// 只需在標準函式的基礎上新增一個“*”,就可以宣告一個生成器函式。
*genMethod(args) { ... },
// 藉助|get|和|set|可以在行內定義訪問器。
// 只是定義行內函數,即使沒有生成器。
// 注意通過這種方式裝載的getter不能接受引數
get propName() { ... },
// 注意通過這種方式裝載的setter至少接受一個引數
set propName(arg) { ... },
// []語法可以用於任意支援預計算屬性名的地方,來滿足上面的第4中情況。
// 這意味著你可以使用symbol,呼叫函式,聯結字串
// 或其它可以給property.id求值的表示式。
// 這個語法對訪問器或生成器同樣有效,我在這裡只是舉個例子。
[functionThatReturnsPropertyName()] (args) { ... }
};
現在,我們可以用這種新語法重寫上面的程式碼片段:
function Circle(radius) {
this.radius = radius;
Circle.circlesMade++;
}
Circle.draw = function draw(circle, canvas) { /* Canvas繪製程式碼 */ }
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("圓的半徑必須為整數。");
this._radius = radius;
}
};
講究地說,這段程式碼與上面的程式碼段並不完全相同,裝載後的物件字面量中的方法定義是可配置(configurable)和可列舉(enumerable) 的,然而在第一段程式碼段中卻不是這樣。事實上,很少有人會注意到這個問題,我決定為了簡潔起見暫時省略可列舉性和可配置性。
不過,這段程式碼依然變得更好了,不是麼?不幸的是,即使有了新的方法定義語法,我們仍然不能武裝到牙齒,所以仍然需要通過定義函式來定義Circle
類。沒有一種方法能夠讓你在定義函式時就獲取它的屬性。
類定義語法
儘管這比以前更好,但是它仍然不能滿足人們對於簡潔的JavaScript面向物件解決方案的渴望。在其它語言中,有一個句法結構可以用來處理面向物件設計的問題,經過一番討論後他們將其命名為類(Class)。
好吧,讓我們也來新增一些類。
我們需要這樣一個系統:給命名建構函式新增方法的同時給函式的.prototype
屬性也新增相應方法,從而用這個類構造出的例項也包含相應的方法。既然我們掌握了一種嶄新的方法定義語言,我們一定要物盡其用。在類的所有例項中,我們只需要一種區分普通函式與特殊函式的方法,在C++或Java中,這種功能對應的關鍵字是static
。這種方法看起來不錯,讓我們用起來!
我們還需要一個方法,可以在一堆方法中指定出唯一的建構函式。在C++或Java中,建構函式與類同名,並且沒有返回型別。既然JS沒有返回型別,我們無論如何都需要一個.constructor
屬性來支援向後相容性,你可以稱之為方法建構函式
(method constructor)。
將所有的概念組合到一起後,我們可以重寫Circle類並實現所有功能:
class Circle {
constructor(radius) {
this.radius = radius;
Circle.circlesMade++;
};
static draw(circle, canvas) {
// Canvas繪製程式碼
};
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("圓的半徑必須為整數。");
this._radius = radius;
};
}
哇嗷!我們不僅可以實現Circle
所需的功能,還能使程式碼如此簡潔,這比剛開始好多了!
雖然如此,有的人有可能會遇到問題或碰到邊緣用例。我會嘗試著預測你們將會遇到的問題並一一解答:
-
分號是怎麼回事?—— 在一次“打造傳統類”的嘗試中,我們決定編寫一個更傳統的分隔符。如果不喜歡可以不寫,分隔符是可選的。
-
如果我不想要一個建構函式,但是仍然想在建立的物件中放置方法呢?—— 好吧,
constructor
方法也是可選的,物件中會預設宣告一個空的建構函式constructor() {}
。 -
可以用生成器作為
建構函式
麼?—— 堅決不可以!構造器不是普通方法,隨意新增將會觸發型別錯誤(TypeError)
,這條規則同樣適用於生成器和訪問器。 -
我可以用預計算屬性名來定義
建構函式
麼?—— 很不幸的是不可以!那將會變得很難預測,所以我們不去嘗試。如果你用預計算屬性名定義一個方法來命名建構函式
,你將得到一個名為constructor
的方法,它就不是類的構造函數了。 -
如果我改變了
Circle
的值,會導致new Circle
的行為異常麼?—— 不會!類與函式表示式類似,會得到一個給定名稱的內部繫結,這個繫結不會被外部力量改變,所以無論你在外圍作用域給Circle
變數設定什麼值,構造器中的Circle.circlesMade++
依然會像預期一樣執行。 -
好的,但是我可能直接給函式傳一個物件字面量作為引數,類是不是就不能例項化了?—— 幸運的是,ES6中也支援類表示式!可以是命名或匿名錶達式,且行為與上述一致,唯一的區別是它們不會在你宣告它們的作用域中建立變數。
-
上面提到的可列舉性、可配置性又如何解釋呢?—— 人們希望在類中裝載的方法是可配置、不可列舉的。一來你可以在物件中裝載方法,二來當你列舉物件屬性時,不會將裝載的方法枚舉出來,得到的只是附加的資料屬性,這樣做是有道理的。
-
嗨,等等……什麼……?我的例項變數在哪兒?
靜態
和常量呢?—— 好吧,你問住我了。ES6目前的定義中不存在相關資訊。但是有個好訊息,在諸多的規範程序中,我強烈支援在類語法中加入可選的static
和const
關鍵字,該提案已經正式向規範會議遞交併處於議程中,我認為我們可以期待在未來會產生更多的相關討論。 -
好的,即使這樣,這些內容都很贊!我們現在可以使用這些技術麼?—— 不完全可以。目前,你們可以藉助polyfill(尤其是Babel)來熟悉特性的相關語法,等到所有主流瀏覽器原生支援還需要一段時間。我已經在Firefox的Nightly版本中實現了我們所討論的所有特性;同樣,這些特性在Edge和Chrome中也已實現,只是預設不啟用;目前Safari尚未實現相關特性。
-
在這裡我們沒有提及Java和C+++中的子類(subclassing)和
super
關鍵字,JS也有麼?– 是的,它有!我們完全可以在另一篇文章中詳細討論,後續歡迎回來與我們一起探索子類,挖掘更多JavaScript類實現的強大之處。
相關推薦
深入淺出ES6(十三):類 Class
目前面臨的問題 假如我們想要建立一個經典的面向物件設計示例:Circle類。想象一下我們正在為一個簡單的Canvas庫編寫這個Circle類,在眾多需要考慮的因素中,我們可能更想了解以下功能的實現方式: 在給定的Canvas上繪製一個給定圓。跟蹤記錄生成圓的總數。跟蹤記錄給
深入淺出ES6(六):解構 Destructuring
什麼是解構賦值? 解構賦值允許你使用類似陣列或物件字面量的語法將陣列和物件的屬性賦給各種變數。這種賦值語法極度簡潔,同時還比傳統的屬性訪問方法更為清晰。 通常來說,你很可能這樣訪問陣列中的前三個元素: var first = someArray[0]; va
深入淺出ES6(九):學習Babel和Broccoli,馬上就用ES6
自ES6正式釋出,人們已經開始討論ES7:未來版本會保留哪些特性,新標準可能提供什麼樣的新特性。作為Web開發者,我們想知道如何發揮這一切的巨大能量。在深入淺出ES6系列之前的文章中,我們不斷鼓勵你開始在編碼中加入ES6新特性,輔以一些有趣的工具,你完全可以從現在開始使用E
gulp構建專案(十三):babel-polyfill編譯es6新增api
需求分析: es6語法以及提供的強大api給前端帶來了很大便利,可是部分瀏覽器無法識別es6語法 gulp-babel只能將es6語法編譯成es5,比如:箭頭函式、let變數等,但是API不能編譯,比如Object.assign 引用babel-p
淺談Kotlin(三):類
ide pos 中一 androi 文件 rri object 淺談 spa 淺談Kotlin(一):簡介及Android Studio中配置 淺談Kotlin(二):基本類型、基本語法、代碼風格 前言: 已經學習了前兩篇文章,對Kotlin有了一個基本的認識,往後
把握linux內核設計思想(十三):內存管理之進程地址空間
color 區域 left ons 文章 進程的地址空間 tmp ica interval 【版權聲明:尊重原創,轉載請保留出處:blog.csdn.net/shallnet。文章僅供學習交流,請勿用於商業用途】 進程地址空間由進程可尋址的虛擬內存組成
OpenCV探索之路(十三):詳解掩膜mask
ret 如果 拷貝 ace 設置 之路 動作 與運算 區域 在OpenCV中我們經常會遇到一個名字:Mask(掩膜)。很多函數都使用到它,那麽這個Mask到底什麽呢? 一開始我接觸到Mask這個東西時,我還真是一頭霧水啊,也對無法理解Mask到底有什麽用。經過查閱大量資料後
深入淺出Mesos(三):持久化存儲和容錯
osql 不同 stand eth 還在 哪裏 技術分享 運行 允許 【編者按】Mesos是Apache下的開源分布式資源管理框架,它被稱為是分布式系統的內核。Mesos最初是由加州大學伯克利分校的AMPLab開發的,後在Twitter得到廣泛使用。InfoQ接下來將會策
深入淺出Mesos(四):Mesos的資源分配
http software hrp 分享 例如 自定義模塊 工作原理 ces 根據 http://www.infoq.com/cn/articles/analyse-mesos-part-04 【編者按】Mesos是Apache下的開源分布式資源管理框架,它被稱為是分布式系
深入淺出Mesos(六):親身體會Apache Mesos
反饋 存儲 stat tar getting multi -a sources 其他 http://www.infoq.com/cn/articles/analyse-mesos-part-06 關於下一代數據中心操作系統Apache Mesos的系列文章,已經完成的內
深入淺出Mesos(五):成功的開源社區
存在 出了 啟用 其中 ngs 它的 -c 新的 響應 http://www.infoq.com/cn/articles/analyse-mesos-part-05 【編者按】Mesos是Apache下的開源分布式資源管理框架,它被稱為是分布式系統的內核。Mesos最初
Docker(十三):OpenStack部署Docker集群實戰
-a 模塊 -1 -name nbsp col arm ons http 1、介紹 本教程使用Compose、Machine、Swarm工具把WordPress部署在OpenStack上。 本節采用Consul作為Swarm的Discovery Service模塊,
R語言學習筆記(十三):時間序列
abs 以及 stat max 時間 aic air ror imp #生成時間序列對象 sales<-c(18,33,41,7,34,35,24,25,24,21,25,20,22,31,40,29,25,21,22,54,31,25,26,35) tsal
es6(六):module模塊(export,import)
導入 運行時 發現 let 腳本文件 推薦 必須 哪些 書寫 es6之前,社區模塊加載方案,主要是CommonJS(用於服務器)和AMD(用於瀏覽器) 而es6實現的模塊解決方案完全可以替代CommonJS和AMD ES6模塊設計思想:盡量靜態化,在編譯時就能確定模塊的依
Scala筆記整理(八):類型參數(泛型)與隱士轉換
大數據 Scala [TOC] 概述 類型參數是什麽?類型參數其實就是Java中的泛型。大家對Java中的泛型應該有所了解,比如我們有List list = new ArrayList(),接著list.add(1),沒問題,list.add("2"),然後我們list.get(
Python筆記(十三):urllib模塊
二進制 數據 print web 應用程序 IE query request file (一) URL地址 URL地址組件 URL組件 說明 scheme 網絡協議或下載方案 net_loc 服務器所在地(也許含有用戶信息)
Android項目實戰(十三):淺談EventBus
app mage tar 一句話 creat 簡單 銷毀 second gradle 原文:Android項目實戰(十三):淺談EventBus概述: EventBus是一款針對Android優化的發布/訂閱事件總線。 主要功能是替代Intent,Handler,Bro
Spring(十三):使用工廠方法來配置Bean的兩種方式(靜態工廠方法&實例工廠方法)
color 示例 簡單的 rgs icc tostring pac ng- clas 通過調用靜態工廠方法創建Bean 1)調用靜態工廠方法創建Bean是將對象創建的過程封裝到靜態方法中。當客戶端需要對象時,只需要簡單地調用靜態方法,而不需要關心創建對象的具體細節。 2
Python3學習筆記(十三):裝飾器
nbsp lee 一個 col false UNC for strong 直接 裝飾器就是一個閉包,它的主要作用是在不改變原函數的基礎上對原函數功能進行擴展。 我們先來寫一個簡單的函數: from time import sleep def foo(): pr
Spark筆記整理(十三):RDD持久化性能測試(圖文並茂)
才會 不執行 分享 綠色 做的 specified ffffff cto 最好 [TOC] 1 前言 其實在之前的文章《Spark筆記整理(五):Spark RDD持久化、廣播變量和累加器》中也有類似的測試,不過當時做的測試僅僅是在本地跑代碼,並以Java/Scala代碼