在 JavaScript 程式語言裡,這個 valueOf() 函式算是非常少用的一個內建函式,甚至於很多人連聽都沒聽過。然而,這個 valueOf() 函式十分重要,我在研究之後發現,理解了 valueOf() 的用途後,不但更能理解 JavaScript 如何針對物件進行數值、布林與字串運算,更能夠利用 valueOf() 解決 使用者定義物件 無法比對大小的問題。
前幾天我曾在 Will 保哥的技術交流中心 分享過一個非常好用的 DevDocs 網站,從該網站搜尋 valueOf 關鍵字之後,你可以查到在 JavaScript 裡,有許多內建物件都有這個 valueOf 方法,幾乎大部分的原始型別包裹物件都有個 valueOf() 函式,例如 Boolean.valueOf 、Number.valueOf 、String.valueOf 等等,另外還有一個 Date 內建物件也有個 Date.valueOf 方法,最後還有個根物件的 Object.valueOf 方法可用。
從文件中可以看見,這個 valueOf 方法最主要的目的,就是用來回傳特定物件相對應原始型別的值:
我們知道 JavaScript 的型別系統中,主要包含兩大類:
- 原始型別 (primitive types)
- 物件型別 (object types)
在 JavaScript 裡面,所有的東西都是物件,除了內建的 5 大原始型別除外,分別是:
- number
- string
- boolean
- null
- undefined
其中 null 與 undefined 屬於特殊用途,並沒有相對應的 原始型別包裹物件 (primititve type wrapper type),但其他另外三個都有,分別是:
所以,當我們比較以下兩個變數時,最後比對兩個不同型別物件的等式 (equality),結果竟然為 true
大部分的情境下,開發人員都會回答說:「因為 JavaScript 是個動態型別,所以型別會自動轉換,雖然一個型別是 number,另一個型別是 object,但用兩個等號 ( == ) 比對兩邊物件時,JavaScript 會幫我們自動轉型,然後才進行比對」。
嗯! 這種解釋,我不能說他「錯」,這段文字解釋了一個在 JavaScript 程式語言中普遍的「現象」,並沒有解釋「為什麼」?而我這次的研究,則是釐清了這個現象背後發生的原因。
事實上,當 JavaScript 任意物件在進行 比較運算 時,如 == , != , < , <= , > , >= 等等,都會先執行 valueOf() 或 toString() 函式,取回該物件相對應原始型別的值,全看你當下兩邊比較的是甚麼原始型別,然後再進行比較。原始型別包裹物件總共有三種,除了 Number 物件之外,還有 String 與 Boolean 物件,其實都是完全一樣的運作方式,我以 number 原始型別物件、string 原始型別物件與 Number 物件進行比較與示範如下圖示:
除了原始型別包裹物件之外,我們還有許多使用者定義物件存在,這些物件在預設的情況下是無法進行比較運算的,舉個例子來說,我們先宣告兩個新的物件,然後去比較兩個物件,所有結果都將是 false。
如下圖示,我無論用自訂物件,或取得 jQuery 物件,其結果都是無法比較的:
我舉個例子來說,即便我額外實作了 valueOf() 或 toString() 方法,你在進行物件的等式運算時,得到的結果依然是 false,如下圖示:
所以,在 JavaScript 裡,所有的物件都是不相等的,每一個都是獨立的物件實體 (object instance),這是一個非常重要的特性,即便你實作了 valueOf 或 toString 方法,還是無法對使用者定義物件進行任何相等比較運算,各位必須謹記在心!
為了實驗 JavaScript 在進行比較運算時,一定會先將物件透過 valueOf() 或 toString() 方法轉換成原始型別的值,我簡單做個小實驗,我把註解都寫在如下圖上了,請各位由上往下,直接看圖說故事:
( 以下範例僅示範用途,實務上不會去覆寫 Object 最上層物件的方法或屬性 )
請記得:只有原始型別物件可以比較 等式 (equality),物件型別物件是無法比較等式的,即便是原始型別包裹物件也一樣!
不過,如果你拿物件型別來比較原始型別,所有的物件型別物件,一定會透過 valueOf() 或 toString() 方法先轉成原始型別物件,然後才進行比較,這也是大家熟知的「自動型別轉換」。而這個自動型別轉換的過程,骨子裡就是透過 valueOf() 或 toString() 方法轉換的!除此之外,你又可以自訂 valueOf() 或 toString() 方法,因此,這個「自動型別轉換」的過程,完全可以讓你自由控制,這將對我們的 JavaScript 應用程式帶來極大的彈性!
我常聽到許多 Web 開發人員誤解 JavaScript 這個程式語言,認為他是弱型別,或物件的值在比較的時候隨便亂轉換,導致經常判斷失誤,寫出一些很難查出來的 Bug,事實上都是因為對 JavaScript 語言特性不了解所致。接下來,我再示範一個例子,這次我們使用建構式函式建立一個自訂物件,然後再進行各種比較運算。
註: 程式碼本身應該就可以解釋一切,如果看不太懂,真的有必要多做進修,或報名下一梯次【前端工程訓練:JavaScript 與 AngularJS 開發實戰】課程,我會在課程中進行非常詳盡的解說,相關課程資訊都會在 Will 保哥的技術交流中心 進行公告。
在了解比較運算之後,我們可以看看更多在物件型別與不同原始型別的交互運算的過程中,物件是如何進行轉換的。
§ 物件型別與 number 型別的混和運算 (算術運算)
我們先定義一個 Person 自訂物件,並定義該物件型別的 valueOf 與 toString 方法,程式碼如下:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.valueOf = function () {
return this.age;
};
Person.prototype.toString = function () {
return this.name;
};
var p1 = new Person('Will', 18);
var p2 = new Person('John', 25);
我做了一連串的測試如下,你將會發現,當我們的自訂物件在做數值運算時,物件型別的資料一定會先透過 valueOf 方法運算出一個 number 型別的結果,然後才進行數值運算!
當然的,由於 valueOf 是你自行定義的,該方法回傳什麼結果都有可能,如果今天 valueOf 回傳的結果不是 number 型別怎麼辦?我們在來看看這個不一樣的例子。
注意: 雖然 valueOf 可以回傳不是 number 型別的值,但實務上不建議這麼做!本範例僅示範用途。
我們讓 Person 物件透過 valueOf 回傳時,回傳的是 boolean 原始型別,這時只會有 true 與 false 的值被回傳,並不是 number 型別,這時 JavaScript 會強迫把 boolean 轉換成 number 型別之後,才會進行數值運算,因為我們用的是「算術運算子」,兩邊的物件,就會先轉成「數值」才會進行運算。
對物件進行算數運算的小結是:
使用者定義物件進行算數運算時,會以 valueOf() 方法的回傳結果為主,如果沒有自定義 valueOf() 方法,就會改以 toString() 方法的回傳結果為主。
§ 物件型別與 boolean 型別的混和運算 (布林運算)
在對物件進行布林運算時,各位應該會很直覺得想到 valueOf 的運算過程,要是真的這麼簡單,就沒有這篇文章啦!事實上,在對物件型別進行布林運算時,所有的物件型別物件只會回傳 true 而已!
注意: 當變數值變成 null 或 undefined 時,該變數指向的物件,已經不是物件,而是原始型別。
所以我們來看看以下例子,各位就可以發現,所有物件進行布林運算,一定會轉換成 true,即便是空物件也一樣,如下範例:
對物件進行布林運算的小結是:
使用者定義物件進行布林運算時,永遠會回傳 true,不會參考 valueOf 或 toString 的執行結果。
§ 物件型別與 string 型別的混和運算
物件型別在進行字串運算時,其過程相對簡單,預設所有物件都會執行 toString() 方法產生字串,當使用者定義物件沒有宣告 toString() 方法時,就會進一步執行 valueOf() 方法,如果 valueOf() 方法的執行結果不是 string 型別,則會再將其執行結果執行 toString() 產生字串,如果 toString() 與 valueOf() 方法都不存在,就會直接回傳 Object.prototype.toString 方法的執行結果,也就是回傳物件的型別名稱。
舉個例子來說,我們試著把 valueOf 回傳一個 boolean 型別:
對物件進行字串運算的小結是:
使用者定義物件進行字串運算時,會以 toString() 方法的回傳結果為主,如果沒有自定義 toString() 方法,就會改以 valueOf() 方法的回傳結果為主,而 valueOf() 方法回傳的結果不是 string 型別的話,還會把 valueOf() 方法回傳的結果再執行一次 toString() 轉型成 string 型別。
§ 物件型別與 Array 型別的關係
我們都知道 Array 物件有個 sort 方法,可以用來排序一個陣列中的元素排列。我們先來看一個簡單的範例。如下圖示,由於陣列元素都是數值(number),透過 sort() 排序之後,預設會採順向排序。
事實上,陣列的 sort() 方法其實是透過字串進行排序的,所以上述執行結果只是個假象,例如我們輸出 [1000, 200, 30, 4].sort() 的運算結果,竟不是預期的 [4, 30, 200, 1000],而是 [1000, 200, 30, 4],如下圖示:
所以,我們如果要修改 Person 型別的預設排序,添加一個 toString 方法,並回傳一個字串值,就能直接使用 sort() 進行排序了!
我們在來看看另一個 sort() 的用法,也就是傳入一個回呼函式 (callback) 來自訂排序規則:
var numbers = [3, 7, 2, 9, 1, 4, 8];
numbers.sort(function(a, b) { return a - b; });
numbers.sort(function(a, b) { return b - a; });
如果我們的陣列元素都是 Person 物件型別,而且希望以 age 屬性透過數值排序的話,我們可以透過回呼函式自行定義排序的規則,並且指定 age 屬性進行算數運算,就能達成任務,如下圖示:
如果我們希望還是以上述 Person 型別的 age 屬性進行數值排序,又不希望在排序的回呼函式中明確指定屬性的話,這時我們還是可以透過自訂 valueOf 方法的方式,讓排序的時候,物件與物件的比較透過數值進行排序,如下範例:
透過 valueOf 的定義,讓你的物件型別物件有個相對應的原始型別物件,事實上有助於架構你的程式,不但能賦予物件之間能夠進行原始型別的比對與運算,透過不同的使用情境,也能有更多的彈性加以變化,像是陣列的 sort() 方法就是一例,你可以在自訂排序時,也可以寫出較為通用的回呼函式,增加程式可讀性與彈性。
結論
最近前端工程正夯,我必須說,前端工程的複雜度,絕對沒有想像的複雜,而是比想像的還更複雜!
有很多魔鬼般的細節,會不斷卡住你的開發過程,唯有不斷的進修、磨練,才能在前端的領域悠遊,可以想見的,這是一條漫長且有趣的道路,有解不完的有趣問題,也有看不完的教學文件,等累積了夠多的知識,自然能發現別人看不見的細節,發揮別人想不到的創意,在前端這個領域享受創作的過程!
相關連結