iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 7
5

你所不知道的 JS

本文主要會談到

  • 何謂 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)

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

Bad idea!

使用之前...請。三。思!

...

...

如下範例,使用物件包裹器宣告一個布林變數 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)

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

回顧

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

  • Natives(原生功能)即是「內建函式」,像是 String(..)Number(..) 等,除了使用字面值,也可用 Natives 當成建構子來建立值。
  • 若非必要,不建議使用 Natives 當成建構子來建立值!在物件包裹器這部份會提到陷阱和如何解封裝。
  • 在建構子的原生功能部份,雖然優先使用字面值而非使用建構子建立物件,但在某些情況還是不得不用的,並且來看一些需要關心的議題和警惕用的錯誤用法。
  • 一些真心的建議...
    • 不要任意修改這些預設的原生的原型,甚至不要無條件地擴充原生的原型,若要擴充也應撰寫符合規格的測試程式。
    • 不要使用原生的原型當成變數的初始值,以避免無意間的修改。

References


同步發表於部落格


上一篇
你懂 JavaScript 嗎?#6 值(Values)Part 2 - 特殊值
下一篇
你懂 JavaScript 嗎?#8 強制轉型(Coercion)
系列文
你懂 JavaScript 嗎?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
凱文偏執狂
iT邦新手 5 級 ‧ 2018-10-14 10:57:12

作者很認真在提供資訊,可是偏向理論和語法,這些在網路上能找到更詳盡完整的內容(MDN)。建議作者提供更多屬於應用層次的內容,或者丟問題然後提供解決步驟。簡單來說,這個系列多的是事實,缺的是思辨和實務應用

1
hannahpun
iT邦新手 4 級 ‧ 2018-10-25 17:15:22

Thanks for sharing, 覺得這些知識很重要啊

Summer iT邦新手 3 級 ‧ 2018-10-25 17:31:31 檢舉

/images/emoticon/emoticon42.gif

我要留言

立即登入留言