JavaScript 原始值與包裝對象
本文轉(zhuǎn)載自微信公眾號「菜鳥小?!?,作者文弱書生陳皮皮 。轉(zhuǎn)載本文請聯(lián)系菜鳥小棧公眾號。
前言
隨著 JavaScript 越來越流行,越來越多地開發(fā)者開始接觸并使用 JavaScript。
同時我也發(fā)現(xiàn),有不少開發(fā)者對于 JavaScript 最基本的原始值和包裝對象都沒有很清晰的理解。
那么本篇文章,就由渣皮來給大家詳細介紹一下它們。
話不多說,Let's go!
正文
原始類型 (Primitive types)
原始類型也被稱為“基本類型”。
目前在 JavaScript 中有以下幾種原始類型:
- string(字符串)
- number(數(shù)字)
- boolean(布爾)
- null(空)
- undefined(未定義)
- bigint(大整數(shù),ES6)
- symbol(標(biāo)志?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 最初的實現(xiàn)中,JavaScript 中的值是由一個表示類型的標(biāo)簽和實際數(shù)據(jù)值表示的。對象的類型標(biāo)簽是 0。由于 null 代表的是空指針(大多數(shù)平臺下值為 0x00),因此,null 的類型標(biāo)簽是 0,typeof null 也因此返回 "object"。
The history of “typeof null”:https://2ality.com/2013/10/typeof-null.html
原始值 (Primitive values)
原始值也就是原始類型的值(數(shù)據(jù))。
A primitive value is data that is not an object and has no methods.
原始值是一種沒有任何方法的非對象數(shù)據(jù)。
也就是說,string、number 和 boolean 等原始類型的值本身是沒有任何屬性和方法的。
這個時候嗅覺敏銳的小伙伴是不是已經(jīng)察覺到有什么不對勁了?
是孜然!我加了孜然!(手動狗頭并劃掉)
這里有一個非常有意思的點,但是在討論這個問題之前,先讓我們認識下包裝對象。
包裝對象 (Wrapper objects)
除了 null 和 undefined 外的原始類型都有其相應(yīng)的包裝對象:
- String(字符串)
- Number(數(shù)字)
- Boolean(布爾)
- BigInt(大整數(shù),ES6)
- Symbol(標(biāo)志?ES6)
對象 (Object)
對象是引用類型。
首先,包裝對象本身是一個對象,也是函數(shù)。
- String instanceof Object; // true
- String instanceof Function; // true
構(gòu)造函數(shù) (Constructor)
實例 (Instance)
其中 String、Number 和 Boolean 均支持使用 new 運算符來創(chuàng)建對應(yīng)的包裝對象實例。
例如 String 的聲明(節(jié)選):
- interface StringConstructor {
- new(value?: any): String;
- (value?: any): string;
- readonly prototype: String;
- }
- declare var String: StringConstructor;
使用 new 運算符得到的數(shù)據(jù)是對象(Object):
- // 字符串
- typeof 'pp'; // "string"
- typeof new String('pp'); // "object"
- new String() instanceof Object; // true
- // 數(shù)字
- 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
我們可以調(diào)用包裝對象實例的 valueOf() 函數(shù)來獲取其原始值:
- // 字符串
- let s = new String('pp');
- s.valueOf(); // "pp"
- typeof s.valueOf(); // "string"
- // 數(shù)字
- 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 的聲明(節(jié)選):
- interface BigIntConstructor {
- (value?: any): bigint;
- readonly prototype: BigInt;
- }
- declare var BigInt: BigIntConstructor;
可以看到 BigInt 的聲明中沒有 new 運算符相關(guān)函數(shù)。
普通函數(shù) (Function)
包裝對象也可以作為普通函數(shù)來使用。
其中 String()、Number() 和 Boolean() 函數(shù)都可以用來對任意類型的數(shù)據(jù)進行顯式類型轉(zhuǎn)換。
另外 Object() 函數(shù)也可用于顯式類型轉(zhuǎn)換,但本文不再展開。
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
當(dāng)我們使用 String() 函數(shù)來轉(zhuǎn)換對象時,JavaScript 會先訪問對象上的 toString() 函數(shù),如果沒有實現(xiàn),則會順著原型鏈向上查找。
舉個栗子:執(zhí)行 String({ toString() { return 'pp'; } }) 返回的結(jié)果是 "pp",并非 "[object Object]"。
所以 String() 函數(shù)并不能夠用來判斷一個值是否為對象(會翻車)。
小貼士 2
常用的判斷對象的方式為 Object.prototype.toString({}) === '[object Object]'。
舉個栗子:執(zhí)行 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() 函數(shù)來說,可能最實用的轉(zhuǎn)換就是將 true 和 false 轉(zhuǎn)換為 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
小貼士
某些情況下,我們會在數(shù)據(jù)中使用 0 和 1 來表示真假狀態(tài),此時就可以使用 Boolean() 進行狀態(tài)的判斷。
BigInt
BigInt() 函數(shù)用于將整數(shù)轉(zhuǎn)換為大整數(shù)。
該函數(shù)接受一個整數(shù)作為參數(shù),傳入?yún)?shù)若為浮點數(shù)或任何非數(shù)字類型數(shù)據(jù)都會報錯。
示例代碼:
- BigInt(123); // 123n
- BigInt(123n); // 123n
- typeof 123n; // "bigint"
- typeof BigInt(123); // "bigint"
BigInt & Number
需要注意的是,BigInt 和 Number 是不嚴(yán)格相等(寬松相等)的。
示例代碼:
- 123n === 123; // false
- 123n == 123; // true
Symbol
Symbol() 函數(shù)用于創(chuàng)建一個 symbol 類型的值。
該函數(shù)接受一個字符串作為描述符(參數(shù)),如果傳入其他類型的值則會被轉(zhuǎn)換為字符串(除了 undefined)。
注意,每一個 symbol 值都是獨一無二的,即使它們的描述符都是一樣的。
且 symbol 類型的數(shù)據(jù)只能通過 Symbol() 函數(shù)來創(chuàng)建。
示例代碼:
- // 后面的返回值是 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)
本文前面有提到:「原始值是一種沒有任何方法的非對象數(shù)據(jù)。」
我們都知道對象(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 哦不寫代碼的時候明明可以調(diào)用到字符串、數(shù)字和布爾值上的方法!
比如下面這段代碼,能夠正常執(zhí)行并得到符合預(yù)期的結(jié)果:
- // 字符串
- let s = 'chenpipi';
- s.toUpperCase(); // "CHENPIPI"
- 'ChenPiPi'.slice(4); // "PiPi"
- // 數(shù)字
- let n = 123;
- n.toString(); // "123"
- (123.45).toFixed(2); // "123.5"
- // 布爾值
- let b = true;
- b.toString(); // "true"
- false.toString(); // "false"
無用小知識
有沒有發(fā)現(xiàn),數(shù)字的字面量后面不能直接調(diào)用函數(shù)?例如執(zhí)行 123.toString() 會報 SyntaxError(語法錯誤)。
這是因為數(shù)字(浮點數(shù))本身會用到小數(shù)點 .,而調(diào)用函數(shù)也需要用小數(shù)點,這時就出現(xiàn)了歧義(字符串和布爾值就沒有這種煩惱)。
對于這種情況,我們可以使用括號 () 將數(shù)字包裹起來,如 (123).toString();或者使用兩個連續(xù)的小數(shù)點 .. 來調(diào)用函數(shù),如 123..toString()。
奇了怪了
那么既然字符串不是對象,那么為什么字符串會有屬性和方法呢?
轉(zhuǎn)念一想,數(shù)字就是數(shù)字,數(shù)字身上怎么會有方法呢?
這確實不符合邏輯,但是這又與實際相矛盾。
咋回事呢???
替身使者 (I can't translate this)
答案揭曉~
暗中操作
以字符串(string)為例,當(dāng)我們在代碼中讀取字符串的屬性或者方法時, JavaScript 會靜默地執(zhí)行下面的操作:
- 將字符串通過 new String() 的方式來創(chuàng)建一個臨時的包裝對象實例;
- 通過創(chuàng)建的對象來執(zhí)行我們的代碼邏輯(讀取屬性或執(zhí)行函數(shù));
- 臨時對象不再使用,可以被銷毀。
如下面的栗子:
- let a = 'chenpipi';
- console.log(a); // "chenpipi"
- // ------------------------------
- let b1 = a.length;
- console.log(b1); // 8
- // 上面的代碼相當(dāng)于:
- let b2 = (new String(a)).length;
- console.log(b2); // 8
- // ------------------------------
- let c1 = a.toUpperCase();
- console.log(c1); // "CHENPIPI"
- // 上面的代碼相當(dāng)于:
- let c2 = (new String(a)).toUpperCase();
- console.log(c2); // "CHENPIPI"
數(shù)字(number)和布爾值(boolean)同理,但數(shù)字通過 new Number() 來創(chuàng)建臨時對象,而布爾值則通過 new Boolean() 來創(chuàng)建。
除了上面的例子,最有力的證明,就是他們的構(gòu)造函數(shù):
- 'chenpipi'.constructor === String; // true
- (12345).constructor === Number; // true
- true.constructor === Boolean; // true
這一切都是 JavaScript 在暗中完成的,且過程中產(chǎn)生的臨時對象都是一次性的(用完就丟)。
原來如此
蕪湖,這么一來就說得通了!
這也就能解釋為什么我們能夠訪問字符串上的屬性和方法,卻不能增加或修改屬性。
那是因為我們實際操作的目標(biāo)其實是 JavaScript 創(chuàng)建的臨時對象,而并非字符串本身!
所以我們的增加或修改操作實際上是生效了的,只不過是在臨時對象上生效了!
就像這樣:
- // 代碼中:
- let a = 'chenpipi';
- a.name = '吳彥祖';
- console.log(a.name); // undefined
- // 相當(dāng)于:
- let a = 'chenpipi';
- (new String(a)).name = '吳彥祖';
- console.log(a.name); // undefined
- // 相當(dāng)于:
- let a = 'chenpipi';
- let temp = new String(a);
- temp.name = '吳彥祖';
- console.log(a.name); // undefined
總結(jié) (Summary)
以上,就是本篇文章的全部內(nèi)容了。
最后我們來總結(jié)一下:
- 多數(shù)原始類型都有相應(yīng)的包裝對象;
- 有些包裝對象可以被 new,有些不行;
- 包裝對象一般被用來進行顯式的類型轉(zhuǎn)換;
- 對象上有屬性和方法;
- 原始值上沒有屬性和方法;
- 原始值上也不能有屬性和方法;
- 但我們可以像操作對象一樣來操作原始值;
- 這是因為 JavaScript 在執(zhí)行代碼的時候偷偷搞小動作;
- JavaScript 會用臨時的包裝對象來替原始值執(zhí)行操作。
我們平時寫代碼的時候不太會注意到這件事,實際上這些也不會影響到我們寫代碼。
所以,這篇文章不就白看啦?
是,也不全是~
知己知彼,百戰(zhàn)百勝。
學(xué)會以上這些無用小知識,也算是對 JavaScript 有了更深的理解了吧,至少還能用來吹牛皮(手動狗頭~)。
相關(guān)資料
《JavaScript 高級程序設(shè)計(第4版)》
《JavaScript 權(quán)威指南(第6版)》
Primitive - MDN:https://developer.mozilla.org/en-US/docs/Glossary/Primitive
The history of “typeof null”:https://2ality.com/2013/10/typeof-null.html