JavaScript 原始值與包裝對象
- 2021 年 5 月 11 日
- 筆記
- javascript, 前端
前言
隨著 JavaScript 越來越流行,越來越多地開發者開始接觸並使用 JavaScript。
同時我也發現,有不少開發者對於 JavaScript 最基本的原始值和包裝對象都沒有很清晰的理解。
那麼本篇文章,就由渣皮來給大家詳細介紹一下它們。
🧐 話不多說,Let’s go!
正文
原始類型 (Primitive types)
原始類型也被稱為「基本類型」。
目前在 JavaScript 中有以下幾種原始類型:
string
(字元串)number
(數字)boolean
(布爾)null
(空)undefined
(未定義)bigint
(大整數,ES6)symbol
(標誌?ES6)
📝 如下:
typeof 'chenpipi'; // "string"
typeof 12345; // "number"
typeof true; // "boolean"
typeof null; // "object"
typeof undefined; // "undefined"
typeof 12345n; // "bigint"
typeof Symbol(); // "symbol"
💡 特別注意
typeof null
雖然返回"object"
,但是這不代表null
就是對象,這其實是 JavaScript 的一個 Bug,且從 JavaScript 誕生以來便如此。在 JavaScript 最初的實現中,JavaScript 中的值是由一個表示類型的標籤和實際數據值表示的。對象的類型標籤是 0。由於
null
代表的是空指針(大多數平台下值為 0x00),因此,null
的類型標籤是 0,typeof null
也因此返回"object"
。The history of 「typeof null」://2ality.com/2013/10/typeof-null.html
原始值 (Primitive values)
原始值也就是原始類型的值(數據)。
A primitive value is data that is not an object and has no methods.
原始值是一種沒有任何方法的非對象數據。
也就是說,string
、number
和 boolean
等原始類型的值本身是沒有任何屬性和方法的。
😨 這個時候嗅覺敏銳的小夥伴是不是已經察覺到有什麼不對勁了?
是孜然!我加了孜然!(手動狗頭並劃掉)
🤓 這裡有一個非常有意思的點,但是在討論這個問題之前,先讓我們認識下包裝對象。
包裝對象 (Wrapper objects)
除了 null
和 undefined
外的原始類型都有其相應的包裝對象:
String
(字元串)Number
(數字)Boolean
(布爾)BigInt
(大整數,ES6)Symbol
(標誌?ES6)
對象 (Object)
對象是引用類型。
首先,包裝對象本身是一個對象,也是函數。
String instanceof Object; // true
String instanceof Function; // true
構造函數 (Constructor)
實例 (Instance)
其中 String
、Number
和 Boolean
均支援使用 new
運算符來創建對應的包裝對象實例。
📝 例如 String
的聲明(節選):
interface StringConstructor {
new(value?: any): String;
(value?: any): string;
readonly prototype: String;
}
declare var String: StringConstructor;
📝 使用 new
運算符得到的數據是對象(Object):
// 字元串
typeof 'pp'; // "string"
typeof new String('pp'); // "object"
new String() instanceof Object; // true
// 數字
typeof 123; // "number"
typeof new Number(123); // "object"
new Number() instanceof Object; // true
// 布爾
typeof true; // "boolean"
typeof new Boolean(true); // "object"
new Boolean() instanceof Object; // true
📝 我們可以調用包裝對象實例的 valueOf()
函數來獲取其原始值:
// 字元串
let s = new String('pp');
s.valueOf(); // "pp"
typeof s.valueOf(); // "string"
// 數字
let n = new Number(123);
n.valueOf(); // 123
typeof n.valueOf(); // "number"
// 布爾
let b = new Boolean(true);
b.valueOf(); // true
typeof b.valueOf(); // "boolean"
「異類」 (Attention)
而 BigInt
和 Symbol
都屬於「不完整的類」,不支援 new
運算符。
📝 例如 BigInt
的聲明(節選):
interface BigIntConstructor {
(value?: any): bigint;
readonly prototype: BigInt;
}
declare var BigInt: BigIntConstructor;
可以看到 BigInt
的聲明中沒有 new
運算符相關函數。
普通函數 (Function)
包裝對象也可以作為普通函數來使用。
其中 String()
、Number()
和 Boolean()
函數都可以用來對任意類型的數據進行顯式類型轉換。
另外 Object()
函數也可用於顯式類型轉換,但本文不再展開。
String
📝 示例程式碼:
typeof String(); // "string"
String(); // ""
String('pp'); // "pp"
String(123); // "123"
String(true); // "true"
String(false); // "false"
String(null); // "null"
String(undefined); // "undefined"
String([]); // ""
String({}); // "[object Object]"
💡 小貼士 1
當我們使用
String()
函數來轉換對象時,JavaScript 會先訪問對象上的toString()
函數,如果沒有實現,則會順著原型鏈向上查找。🌰 舉個栗子:執行
String({ toString() { return 'pp'; } })
返回的結果是"pp"
,並非"[object Object]"
。所以
String()
函數並不能夠用來判斷一個值是否為對象(會翻車)。
💡 小貼士 2
常用的判斷對象的方式為
Object.prototype.toString({}) === '[object Object]'
。🌰 舉個栗子:執行
Object.prototype.toString({ toString() { return 'pp'; } })
返回的是"[object Object]"
。
Number
📝 示例程式碼:
typeof Number(); // "number"
Number(); // 0
Number(''); // 0
Number('pp'); // NaN
Number(123); // 123
Number(true); // 1
Number(false); // 0
Number(null); // 0
Number(undefined); // NaN
Number([]); // 0
Number({}); // NaN
💡 小貼士
對於
Number()
函數來說,可能最實用的轉換就是將true
和false
轉換為1
和0
吧。
Boolean
📝 示例程式碼:
typeof Boolean(); // "boolean"
Boolean(); // false
Boolean(''); // false
Boolean('pp'); // true
Boolean(0); // false
Boolean(1); // true
Boolean(null); // false
Boolean(undefined); // false
Boolean([]); // true
Boolean({}); // true
💡 小貼士
某些情況下,我們會在數據中使用
0
和1
來表示真假狀態,此時就可以使用Boolean()
進行狀態的判斷。
BigInt
BigInt()
函數用於將整數轉換為大整數。
該函數接受一個整數作為參數,傳入參數若為浮點數或任何非數字類型數據都會報錯。
📝 示例程式碼:
BigInt(123); // 123n
BigInt(123n); // 123n
typeof 123n; // "bigint"
typeof BigInt(123); // "bigint"
BigInt & Number
需要注意的是,BigInt
和 Number
是不嚴格相等(寬鬆相等)的。
📝 示例程式碼:
123n === 123; // false
123n == 123; // true
Symbol
Symbol()
函數用於創建一個 symbol
類型的值。
該函數接受一個字元串作為描述符(參數),如果傳入其他類型的值則會被轉換為字元串(除了 undefined
)。
注意,每一個 symbol
值都是獨一無二的,即使它們的描述符都是一樣的。
且 symbol
類型的數據只能通過 Symbol()
函數來創建。
📝 示例程式碼:
// 後面的返回值是 Devtools 模擬出來的,並非實際值
Symbol('pp'); // Symbol(pp)
Symbol(123); // Symbol(123)
Symbol(null); // Symbol(null)
Symbol({}); // Symbol([object Object])
// 類型
typeof Symbol('pp'); // "symbol"
Symbol('pp') === Symbol('pp'); // false
// 描述符
Symbol('pp').description; // "pp"
Symbol(123).description; // "123"
Symbol({}).description; // "[object Object]"
Symbol().description; // undefined
Symbol(undefined).description; // undefined
原始值不是對象 (Primitive not Object)
🎃 有意思的來了~
沒有屬性和方法 (No properties, no functions)
本文前面有提到:「原始值是一種沒有任何方法的非對象數據。」
我們都知道對象(Object)上可以有屬性和方法。
但是字元串不是對象,所以你不能給字元串增加屬性。
📝 做個小實驗:
let a = 'chenpipi';
console.log(a.length); // 8
// 嘗試增加新的屬性
a.name = '吳彥祖';
console.log(a.name); // undefined
// 嘗試修改已有的屬性
typeof a.slice; // "function"
a.slice = null;
typeof a.slice; // "function"
🎬 渣皮小劇場
此時一位頭鐵的小夥伴使用了反駁技能。
渣皮你別在這忽悠人了,我平時寫 Bug 哦不寫程式碼的時候明明可以調用到字元串、數字和布爾值上的方法!
📝 比如下面這段程式碼,能夠正常執行並得到符合預期的結果:
// 字元串
let s = 'chenpipi';
s.toUpperCase(); // "CHENPIPI"
'ChenPiPi'.slice(4); // "PiPi"
// 數字
let n = 123;
n.toString(); // "123"
(123.45).toFixed(2); // "123.5"
// 布爾值
let b = true;
b.toString(); // "true"
false.toString(); // "false"
💡 無用小知識
有沒有發現,數字的字面量後面不能直接調用函數?例如執行
123.toString()
會報 SyntaxError(語法錯誤)。這是因為數字(浮點數)本身會用到小數點
.
,而調用函數也需要用小數點,這時就出現了歧義(字元串和布爾值就沒有這種煩惱)。對於這種情況,我們可以使用括弧
()
將數字包裹起來,如(123).toString()
;或者使用兩個連續的小數點..
來調用函數,如123..toString()
。
🤔 奇了怪了
既然字元串不是對象,那麼為什麼字元串會有屬性和方法呢?
轉念一想,數字就是數字,數字身上怎麼會有方法呢?
這確實不符合邏輯,但是這又與實際相矛盾。
咋回事呢???
替身使者 (I can’t translate this)
答案揭曉~
😎 暗中操作
以字元串(string
)為例,當我們在程式碼中讀取字元串的屬性或者方法時, JavaScript 會靜默地執行下面的操作:
- 將字元串通過
new String()
的方式來創建一個臨時的包裝對象實例; - 通過創建的對象來執行我們的程式碼邏輯(讀取屬性或執行函數);
- 臨時對象不再使用,可以被銷毀。
📝 如下面的栗子:
let a = 'chenpipi';
console.log(a); // "chenpipi"
// ------------------------------
let b1 = a.length;
console.log(b1); // 8
// 上面的程式碼相當於:
let b2 = (new String(a)).length;
console.log(b2); // 8
// ------------------------------
let c1 = a.toUpperCase();
console.log(c1); // "CHENPIPI"
// 上面的程式碼相當於:
let c2 = (new String(a)).toUpperCase();
console.log(c2); // "CHENPIPI"
數字(number
)和布爾值(boolean
)同理,但數字通過 new Number()
來創建臨時對象,而布爾值則通過 new Boolean()
來創建。
📝 除了上面的例子,最有力的證明,就是他們的構造函數:
'chenpipi'.constructor === String; // true
(12345).constructor === Number; // true
true.constructor === Boolean; // true
這一切都是 JavaScript 在暗中完成的,且過程中產生的臨時對象都是一次性的(用完就丟)。
😮 原來如此
蕪湖,這麼一來就說得通了!
這也就能解釋為什麼我們能夠訪問字元串上的屬性和方法,卻不能增加或修改屬性。
那是因為我們實際操作的目標其實是 JavaScript 創建的臨時對象,而並非字元串本身!
所以我們的增加或修改操作實際上是生效了的,只不過是在臨時對象上生效了!
📝 就像這樣:
// 程式碼中:
let a = 'chenpipi';
a.name = '吳彥祖';
console.log(a.name); // undefined
// 相當於:
let a = 'chenpipi';
(new String(a)).name = '吳彥祖';
console.log(a.name); // undefined
// 相當於:
let a = 'chenpipi';
let temp = new String(a);
temp.name = '吳彥祖';
console.log(a.name); // undefined
總結 (Summary)
🎉 以上,就是本篇文章的全部內容了。
最後我們來總結一下:
- 多數原始類型都有相應的包裝對象;
- 有些包裝對象可以被
new
,有些不行; - 包裝對象一般被用來進行顯式的類型轉換;
- 對象上有屬性和方法;
- 原始值上沒有屬性和方法;
- 原始值上也不能有屬性和方法;
- 但我們可以像操作對象一樣來操作原始值;
- 這是因為 JavaScript 在執行程式碼的時候偷偷搞小動作;
- JavaScript 會用臨時的包裝對象來替原始值執行操作。
我們平時寫程式碼的時候不太會注意到這件事,實際上這些也不會影響到我們寫程式碼。
所以,這篇文章不就白看啦?
🙉 是,也不全是~
知己知彼,百戰百勝。
學會以上這些無用小知識,也算是對 JavaScript 有了更深的理解了吧,至少還能用來吹牛皮(手動狗頭~)。
相關資料
《JavaScript 高級程式設計(第4版)》
《JavaScript 權威指南(第6版)》
Primitive – MDN://developer.mozilla.org/en-US/docs/Glossary/Primitive
The history of 「typeof null」://2ality.com/2013/10/typeof-null.html
傳送門
更多分享
《Cocos Creator 編輯器擴展:Quick Finder》
公眾號
菜鳥小棧
😺 我是陳皮皮,一個還在不斷學習的遊戲開發者,一個熱愛分享的 Cocos Star Writer。
🎨 這是我的個人公眾號,專註但不僅限於遊戲開發和前端技術分享。
💖 每一篇原創都非常用心,你的關注就是我原創的動力!
Input and output.