你懂 JavaScript 嗎?#7 原生功能(Natives)

你所不知道的 JS

本文主要會談到

何謂原生功能(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]"

說明

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)

由於直接使用物件形式的物件包裹器來宣告變數會造成一些誤用,像是難以做條件判斷,因此非常不建議這麼做!

Bad idea!

使用之前…請。三。思!

如下範例,使用物件包裹器宣告一個布林變數 isValid,其值希望是 false,但實際上卻是一個物件 Boolean {true},導致進入判斷式時轉型為 true,印出訊息「可以繼續運作…」。

const isValid = new Boolean(false);

if (isValid) {
  console.log('可以繼續運作...');
} else {
  console.log('不合規則,等待處理...');
}

// 可以繼續運作...

怎麼辦?很簡單,「解封裝」就行啦!繼續看下去吧。

解封裝

解封裝(Unboxing)

解封裝是指將其底層的基本型別值取出來。

承上範例,isValid 的值居然是物件 Boolean {true},只好使用 valueOf 來抽出底層的基型值摟,其他強制轉型的方法待後強制轉型的部份(https://cythilya.github.io/2018/10/15/coercion/)補充。

isValid.valueOf() // false

建構子的原生功能

再次強調,優先使用字面值而非使用建構子建立物件。但在這個「建構子的原生功能」部份,我們還是來看一些需要關心的議題和警惕用的錯誤用法。

Array(..)

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.prototypeString.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 是空的正規表達式,因此有人會拿來做為變數的初始值,雖然可能節省了重新創建新值和垃圾回收的工作而讓效能變好,但這可能會在無意間修改了這些預設的原生的原型,這是要避免的。

回顧

看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到…

References


同步發表於2019 鐵人賽


comments powered by Disqus