函數即物件
2017 年 2 月 13 日
在程式設計中,物件的基本概念是將資料和行為捆綁在一起。這在撰寫一組相關函數時提供了一個通用的資料內容。它也提供了一個用於操作資料的介面,允許物件控制對該資料的存取,使其易於支援衍生資料並防止資料遭到無效修改。許多語言提供明確的語法來定義類別,這些類別充當物件的定義。但是,如果您有一個具備一等函數和閉包的語言,則可以使用這些結構來使用函數即物件模式(最初由 Eugene Wallingford 描述)建立物件。
以下是一個使用 JavaScript 中的函數即物件樣式完成的簡化人物物件範例。 [1]
function createPerson(name) { let birthday; return { name: () => name, setName: (aString) => name = aString, birthday: () => birthday, setBirthday: (aLocalDate) => birthday = aLocalDate, age: age, canTrust: canTrust, }; function age() { return birthday.until(clock.today(), ChronoUnit.YEARS); } function canTrust() { return age() <= 30; } }
函數即物件的外層形式是一個函數,它會作為建構函數被呼叫。呼叫的結果本質上是一個函數雜湊表 [2],它充當方法選擇器。此雜湊表會在閉包中擷取函數中任何變數的狀態,允許資料在單一函數呼叫之外持續存在。此結果雜湊表可以像傳統物件一樣處理。
const kent = createPerson("kent"); kent.setBirthday(LocalDate.parse("1961-03-31")); const youngEnoughToTrust = kent.canTrust();
從傳統物件導向觀點來看函數即物件
- 物件的欄位由建構函數參數
(name)
連同區域變數(birthday)
表示。 - 物件的方法是建構函數內嵌套的函數。就像物件方法一樣,它們可以自由地互相呼叫並操作這些區域範圍變數(欄位)中的資料。
- 建構函數外的任何內容都無法存取這些變數,從而保留資料封裝。
- 物件的公開方法是存在於結果雜湊映射中的那些函數。
- 任何嵌套在建構函數中但不存在於結果雜湊映射中的函數都是私有方法
- 公開方法的名稱是結果雜湊映射的鍵,而不是建構函數中函數的名稱。我比較喜歡讓鍵和函數名稱保持相同,以避免混淆(雖然在需要時,建立函數別名會很方便)。[3]
此模式的常見替代實作是傳回一個函數作為方法選擇器,而不是 JavaScript 中的自然方法選擇器雜湊映射。若要使用函數作為方法選擇器,我會傳回一個函數,其第一個引數是要呼叫的方法名稱。函數主體接著會針對該值進行切換(有關此部分的更多資訊,請參閱Wallingford)。
函數即物件的方法已經存在很長一段時間,我已經看過它在 lisp 中被描述過很多次,而且它已被廣泛用於 JavaScript(在 ES6 之前,JavaScript 對類別的概念非常有限)。它經常被用來主張不需要類別的特定語法,這等同於物件愛好者主張當你可以撰寫具有單一「呼叫」方法的類別時,你不需要一級函數。因此,JavaScript 世界中的許多人主張不要使用 ES6 類別語法。就我個人而言,我喜歡同時擁有函數和類別,而且比較喜歡 ES6 的類別語法。
進一步閱讀
Eugene Wallingford 在他1999 年的模式語言「Envoy」中創造了「函數即物件」這個名稱。他的論文值得一讀,以瞭解更多關於此部分的詳細資訊,包括使用函數作為方法選擇器以及委派來支援某種繼承概念。論文中的範例使用 Scheme。
致謝
Chris Ford、Fred George、James Shore、Kevin Yeung、Lucas Lego、Matteo Vaccari、Rob Miles 和 Eugene Wallingford 對這篇文章的草稿提出評論
備註
1: 對於日期處理,我使用js-joda,它是 Joda-Time 函式庫的移植,它清理了 Java 日期和時間處理的混亂情況。我很高興 joda-js 再次提供服務,讓日期和時間處理變得更合理。
2: 在 JavaScript 術語中,它稱為物件,儘管它是一個 JavaScript 物件,而不是我們嘗試建立的經典物件。因此,我將它稱為雜湊映射,以試著減少混淆。
3: 在 ES6 中,我可以使用簡寫屬性名稱來移除重複,將「age: age,
」替換為「age,
」。