你懂 JavaScript 嗎?#7 原生功能(Natives)
14 Oct 2018本文主要會談到
- 何謂 Natives(原生功能)?怎麼用?
- 物件包裹器、陷阱、解封裝。
- 各類建構子的原生功能、原生的原型。雖然優先使用字面值而非使用建構子建立物件,還是需要來看一些需要關心的議題和警惕用的錯誤用法。
何謂原生功能(Natives)?
原生功能(Natives)其實指的就是「內建函式」(built-in function),最常用的像是 String()
、Number()
、Boolean()
、Array()
、Object()
、Function()
、RegExp()
、Date()
、Error()
、Symbol()
,其中 null 和 undefined 是沒有內建函式的。我們也可以將 Natives 當成建構子(constructor)來建立值。注意,使用建構子建立出來的值是一個包裹了基本型別值的物件包裹器(object wrapper),而這個包裹器在其原型(prototype)上定義了許多屬性和方法,因此這些資料型態就能如物件般擁有屬性和方法以供使用。
範例如下,使用 new String('...')
來建立字串值「Hello World!」,
const s = new String('Hello World!');
s; // String {"Hello World!"}
s.toString(); // "Hello World!"
typeof s; // "object"
s instanceof String; // true
Object.prototype.toString.call(s); // "[object String]"
說明
- s 是一個包裹了基本型別值 string 的物件包裹器,簡稱為「字串包裹器物件」,包裹了字串「Hello World!」,而非只是建立了字串本身。
- s 這個字串包裹器物件的原型上定義了 toString 方法,因此可使用
s.toString()
得到字串值。 - 使用 typeof 來判斷值的型別,例如,
typeof s
檢視 s 的型別,結果是「物件」。 - 使用 instanceof 來判斷是否為指定的物件型別,例如,
s instanceof String
確認 s 為 String 的實體物件。 - 使用
Object.prototype.toString
取得物件的子分類,得到字串。
Internal [[Class]]
物件型別的值其內部有一個 [[Class]]
屬性來標記這個值是屬於物件的哪個子分類,雖然無法直接取用,但可透過 Object.prototype.toString
間接取得,範例如下。
Object.prototype.toString.call(123456789); // "[object Number]"
Object.prototype.toString.call('Hello World'); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call([1, 2, 3]); // "[object Array]"
Object.prototype.toString.call({ name: 'Jack' }); // "[object Object]"
Object.prototype.toString.call(function sayHi() {}); // "[object Function]"
Object.prototype.toString.call(/helloworld/i); // "[object RegExp]"
Object.prototype.toString.call(new Date()); // "[object Date]"
Object.prototype.toString.call(Symbol('foo')); // "[object Symbol]"
封裝用的包裹器(Boxing Wrappers)
由於 JavaScript 引擎會自動為基本型別值包裹(或稱封裝)物件包裹器,因此字面值能有屬性或方法可用,例如
const s = 'Hello World!';
s.length; // 12
那麼,直接使用物件形式的物件包裹器來宣告變數,而非隱含地讓 JavaScript 引擎轉換,是不是比較好呢?答案是否定的,第一,這樣效能不佳,使用字面值可讓 JavaScript 預先編譯並快取起來!第二,沒有必要,字面值可幾乎可完全取代物件包裹器做的事情-因此,就讓 JavaScript 引擎自動為我們做這個封裝的工作吧。
const s = new String('Hello World!'); // 錯誤示範!效能差!
s.length; // 12
const s_the_other = Object('Hello World!'); // 錯誤示範!效能差!
s_the_other.length; // 12
const s_another = 'Hello World!'; // 正確示範!效能佳!
s_another.length; // 12
物件包裹器的陷阱(Object Wrapper Gotchas)
由於直接使用物件形式的物件包裹器來宣告變數會造成一些誤用,像是難以做條件判斷,因此非常不建議這麼做!
使用之前…請。三。思!
…
…
如下範例,使用物件包裹器宣告一個布林變數 isValid,其值希望是 false,但實際上卻是一個物件 Boolean {true}
,導致進入判斷式時轉型為 true,印出訊息「可以繼續運作…」。
const isValid = new Boolean(false);
if (isValid) {
console.log('可以繼續運作...');
} else {
console.log('不合規則,等待處理...');
}
// 可以繼續運作...
…
…
怎麼辦?很簡單,「解封裝」就行啦!繼續看下去吧。
解封裝(Unboxing)
解封裝是指將其底層的基本型別值取出來。
承上範例,isValid 的值居然是物件 Boolean {true}
,只好使用 valueOf
來抽出底層的基型值摟,其他強制轉型的方法待後強制轉型的部份補充。
isValid.valueOf(); // false
建構子的原生功能
再次強調,優先使用字面值而非使用建構子建立物件。但在這個「建構子的原生功能」部份,我們還是來看一些需要關心的議題和警惕用的錯誤用法。
Array(..)
- 不管是否使用 new,陣列的物件包裹器所建立的物件是相同的,意即
new Array(...)
和Array(...)
同義。 - 若只傳入一個數字,則不會被當成陣列內容,而會是陣列長度來預先設定陣列的大小,實際上這是個虛胖的空陣列,而裡面沒有存任何東西,是 empty。這種具有空插槽(empty slot)的陣列在做陣列處理時容易產生不可預期的錯誤。
const a = Array(10);
a; // (10) [empty × 10]
a.length; // 10
const b = [undefined, undefined, undefined];
delete b[1]; // true,成功刪除一個元素?
b; // [undefined, empty, undefined],這裡產生一個空插槽!
RegExp(..)
在正規表達式方面,只有一種狀況會需要用到物件包裹器而非字面值,就是必須「動態地」為正規表達式建立範式(pattern),意即 new RegExp('pattern', 'flags')
的格式。
const name = 'Apple';
const pattern = new RegExp('\\b(?:' + name + ')+\\b', 'ig');
const matches = 'Hi, Apple'.match(pattern);
matches; // ["Apple"]
Date(..)
與 Error(..)
Date 與 Error 沒有字面值格式,只能用物件包裹器作為建構子的方式建立物件。
Error 需要注意的地方是,不管是否使用 new,陣列的物件包裹器所建立的物件是相同的,意即 new Error(...)
和 Error(...)
同義。
Symbol(..)
Symbol 同樣沒有字面值格式,若要自定義的 Symbol,就要使用建構子 Symbol(..)
且不可在前面加上 new,否則會報錯。
原生的原型(Native Prototype)
每個建構子都有自己的 .
物件,例如:Array.prototype
、String.prototype
等,而這些 .prototype
物件擁有各自子物件的專屬行為。白話來說,就是經由建構子建立的物件與經由 JavaScript 引擎封裝的字面值,由於原型委派(prototype delegation)的緣故,都能使用定義於 .prototype
的屬性和方法。例如,無論是經由 String()
建構子或經由 JavaScript 引擎封裝的字串基本型別字面值,由於原型委派(prototype delegation)的緣故,都能使用定義於 String.prototype
的屬性和方法。又, String.prototype.XYZ
可簡寫為 String#XYZ
,例如:String#indexOf(..)
、String#charAt(..)
等,其他型別都各自有其行為。
注意,不要任意修改這些預設的原生的原型(甚至建議不要無條件地擴充原生的原型,若要擴充也應撰寫符合規格的測試程式),這在後續強制轉型的部份會看到一些例子(心酸血淚,哭 (〒︿〒))。
…
…
來人啊,還不趕快點辛曉琪的領悟?
「啊 多麼痛的領悟」
什麼?你說這歌太老沒聽過?
…
…
Array.prototype
是空陣列,Function.prototype
是空的函式,RegExp.prototype
是空的正規表達式,因此有人會拿來做為變數的初始值,雖然可能節省了重新創建新值和垃圾回收的工作而讓效能變好,但這可能會在無意間修改了這些預設的原生的原型,這是要避免的。
回顧
看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到…
- Natives(原生功能)即是「內建函式」,像是
String(..)
、Number(..)
等,除了使用字面值,也可用 Natives 當成建構子來建立值。 - 若非必要,不建議使用 Natives 當成建構子來建立值!在物件包裹器這部份會提到陷阱和如何解封裝。
- 在建構子的原生功能部份,雖然優先使用字面值而非使用建構子建立物件,但在某些情況還是不得不用的,並且來看一些需要關心的議題和警惕用的錯誤用法。
- 一些真心的建議…
- 不要任意修改這些預設的原生的原型,甚至不要無條件地擴充原生的原型,若要擴充也應撰寫符合規格的測試程式。
- 不要使用原生的原型當成變數的初始值,以避免無意間的修改。
References
同步發表於2019 鐵人賽。