keywords:natives
,internal [[Class]]
,box
,unboxing
原生功能 ( Natives )
JavaScript 有幾個常用的 natives:
- String()
- Number()
- Boolean()
- Array()
- Object()
- Function()
- RegExp()
- Date()
- Error()
- Symbol() (ES6新增)
如果是從別的語言 ( C# ) 來到 JavaScript,String() 看起來很像建立字串值的建構器,不過實際建構出來的東西可能與想像的不同
看看例子:let a = new String('xyz'); typeof a; // object a; // String {'xyz'} 展開後 {0:'x', 1:'y', 2:'z', length:3, [[PrimitiveValue]]: "xyz" } Object.prototype.toString.call(a); // [object String] 可想像成 a.toString();
建構器形式建立 即 new String('xyz') 是一個字串包裹器物件,包裹了 'xyz' ,而非建立 'xyz' 這個基型值。
typeof 顯示出這些物件並非自己的特殊型別,恰當的說法是 object 型別的子型別typeof 為 'object' 的那些值( 例如一個陣列 )會額外標示一個有內部的 [[Class]] 特性 ( 想成是一個內部的分類, classification 非傳統類別導向編成方法中的類別 class ),此特性無法直接被取用,可以借取預設的 Object.prototype.toString(..) 方法,以目標值呼叫它來間接顯露此特性
Object.prototype.toString.call([1,2,3]); // '[object Array]' Object.prototype.toString.call(function(){}); // '[object Function]'
基本型別值( primitive values )Null() 、Undefined() 原生建構器並不存在 (即沒有這種東西 new Null(null); ),但顯露出來的內部 [[Class]] 值就是 'Null' 和 'Undefined'
Object.prototype.toString.call(null); // '[object null]' Object.prototype.toString.call(undefined); // '[object undefined]'
簡單的基型值,像是 string、number 以及 boolean ,實際上會有封裝 ( boxing ) 的行為涉入
Object.prototype.toString.call('xyz'); // '[object String]' Object.prototype.toString.call(99); // '[object Number]' Object.prototype.toString.call(false); // '[object Boolean]'
封裝用的包裹器
基本型別值沒有特性 ( properties ) 或方法 ( methods ),所以要存取 .length 或 .toString() 時會需要包裹基型值的物件包裹器,幸好 JavaScript 會自動封裝 ( box 也就是 包裹 wrap )
看看例子:let a = 'xyz'; a.length; // 3 a.toUpperCase(); // 'XYZ'
- 如果想藉由直接使用物件形式,試著預先最佳化,程式實際上會執行的更慢,因為瀏覽器很久以前就針對如 .length 這類的常見情況進行最佳化了
- 一般來說沒必要直接使用物件形式,最好是讓封裝需要時隱含地發生。換句話說永遠都不要做
new String('xyz')
或new Number(99)
,永遠優先選用'xyz'
或99
這樣字面形式的基本型別值
物件包裹器的陷阱
如果真的要使用物件包裹器,有幾個陷阱必須特別留意
看看例子:let a = new Boolean(false); typeof a; // object a; // Boolean {false} !!a; // true
🚩問題在於 new Boolean 建立了包 false 值的物件包裹器,但他依然屬於物件型別 ( objects ),而物件本身是 truthy
比較一下物件包裹器與基本型別值
let a = new String('xyz'); let b = 'xyz;
a | b |
---|---|
a:
- 有 .length 特性
- 每個字占有一個索引
- 本身就有 [[PrimitiveValue]]: "xyz"
b:
- 本身沒有 .length 特性
- 需要調用 b.proto ( 可以看到一個物件的原型的一個特性 ) 才能看到原型
- 無 [[PrimitiveValue]]: "xyz"
- 通常不鼓勵直接使用封裝的物件包裹器
解封裝 ( unboxing )
如果有一個物件包裹器,而我們想要取出其底層的基本型別值 ( underlying primitive value ),可以使用 valueOf()
let a = new String('xyz'); let b = new Number(99); let c = new Boolean(false); a; // String {"xyz"} b; // Number {99} c; // Boolean {false} a.valueOf(); // 'xyz' b.valueOf(); // 99 c.valueOf(); // false
解封裝也可能隱含地發生,必須用到物件包裹器的基本型別值時,這個過程為強制轉型,之後會有更詳細的文章介紹
let a = new String('xyz'); let b = a + ''; a; // String {"xyz"} b; // 'xyz' typeof a; // object typeof b; // string