作者:有贊技術(shù)團(tuán)隊(duì)
原文地址:http://tech.youzan.com/javascript-type/
概述
JavaScript的類型判斷是前端工程師們每天代碼中必備的部分邀摆,每天肯定會(huì)寫(xiě)上個(gè)很多遍if (a === 'xxx')
或if (typeof a === 'object')
類似的類型判斷語(yǔ)句初肉,所以掌握J(rèn)avaScript中類型判斷也是前端必備技能,以下會(huì)從JavaScript的類型痕寓,類型判斷以及一些內(nèi)部實(shí)現(xiàn)來(lái)讓你深入了解JavaScript類型的那些事。
類型
JavaScript中類型主要包括了primitive
和object
類型,其中primitive
類型包括了:null
、undefined
、boolean
疑务、number
沾凄、string
和symbol(es6)
。其他所有的都為object
類型知允。
類型判斷
類型檢測(cè)主要包括了:typeof
撒蟀、instanceof
和toString
的三種方式來(lái)判斷變量的類型。
typeof
typeof
接受一個(gè)值并返回它的類型温鸽,它有兩種可能的語(yǔ)法:
typeof x
typeof(x)
當(dāng)在primitive
類型上使用typeof
檢測(cè)變量類型時(shí)保屯,我們總能得到我們想要的結(jié)果手负,比如:
typeof 1; // "number"
typeof ""; // "string"
typeof true; // "boolean"
typeof bla; // "undefined"
typeof undefined; // "undefined"
而當(dāng)在object
類型上使用typeof
檢測(cè)時(shí),有時(shí)可能并不能得到你想要的結(jié)果姑尺,比如:
typeof []; // "object"
typeof null; // "object"
typeof /regex/ // "object"
typeof new String(""); // "object"
typeof function(){}; // "function"
這里的[]
返回的確卻是object
竟终,這可能并不是你想要的,因?yàn)閿?shù)組是一個(gè)特殊的對(duì)象切蟋,有時(shí)候這可能并不是你想要的結(jié)果统捶。
對(duì)于這里的null
返回的確卻是object
,wtf柄粹,有些人說(shuō)null
被認(rèn)為是沒(méi)有一個(gè)對(duì)象喘鸟。
當(dāng)你對(duì)于typeof
檢測(cè)數(shù)據(jù)類型不確定時(shí),請(qǐng)謹(jǐn)慎使用驻右。
toString
typeof
的問(wèn)題主要在于不能告訴你過(guò)多的對(duì)象信息什黑,除了函數(shù)之外:
typeof {key:'val'}; // Object is object
typeof [1,2]; // Array is object
typeof new Date; // Date object
而toString
不管是對(duì)于object
類型,還是primitive
類型堪夭,都能得到你想要的結(jié)果:
var toClass = {}.toString;
console.log(toClass.call(123));
console.log(toClass.call(true));
console.log(toClass.call(Symbol('foo')));
console.log(toClass.call('some string'));
console.log(toClass.call([1, 2]));
console.log(toClass.call(new Date()));
console.log(toClass.call({
a: 'a'
}));
// output
[object Number]
[object Boolean]
[object Symbol]
[object String]
[object Array]
[object Date]
[object Object]
在underscore
中你會(huì)看到以下代碼:
// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
_['is' + name] = function(obj) {
return toString.call(obj) == '[object ' + name + ']';
};
});
這里就是使用toString
來(lái)判斷變量類型愕把,比如你可以通過(guò)_.isFunction(someFunc)
來(lái)判斷someFunc
是否為一個(gè)函數(shù)。
從上面的代碼茵瘾,我們可以看到toString
是可依賴的礼华,不管是object
類型還是primitive
類型,它都能告訴我們正確的結(jié)果拗秘。但它只可以用于判斷內(nèi)置的數(shù)據(jù)類型圣絮,對(duì)于我們自己構(gòu)造的對(duì)象,它還是不能給出我們想要的結(jié)果雕旨,比如下面的代碼:
function Person() {
}
var a = new Person();
// [object Object]
console.log({}.toString.call(a));
console.log(a instanceof Person);
我們這時(shí)候就要用到我們下面介紹的instanceof
了扮匠。
instanceof
對(duì)于使用構(gòu)造函數(shù)創(chuàng)建的對(duì)象,我們通常使用instanceof
來(lái)判斷某一實(shí)例是否屬于某種類型凡涩,例如:a instanceof Person
棒搜,其內(nèi)部原理實(shí)際上是判斷Person.prototype
是否在a
實(shí)例的原型鏈中,其原理可以用下面的函數(shù)來(lái)表達(dá):
function instance_of(V, F) {
var O = F.prototype;
V = V.__proto__;
while (true) {
if (V === null)
return false;
if (O === V)
return true;
V = V.__proto__;
}
}
// use
function Person() {
}
var a = new Person();
// true
console.log(instance_of(a, Person));
類型轉(zhuǎn)換
因?yàn)镴avaScript是動(dòng)態(tài)類型活箕,變量是沒(méi)有類型的力麸,可以隨時(shí)賦予任意值。但是育韩,各種運(yùn)算符或條件判斷中是需要特定類型的克蚂。比如,if判斷時(shí)會(huì)將判斷語(yǔ)句轉(zhuǎn)換為布爾型筋讨。下面就來(lái)深入了解下JavaScript中類型轉(zhuǎn)換埃叭。
ToPrimitive
當(dāng)我們需要將變量轉(zhuǎn)換為原始類型時(shí),就需要用到ToPrimitive
悉罕,下面的代碼說(shuō)明了ToPrimitive
的內(nèi)部實(shí)現(xiàn)原理:
// ECMA-262, section 9.1, page 30. Use null/undefined for no hint,
// (1) for number hint, and (2) for string hint.
function ToPrimitive(x, hint) {
// Fast case check.
if (IS_STRING(x)) return x;
// Normal behavior.
if (!IS_SPEC_OBJECT(x)) return x;
if (IS_SYMBOL_WRAPPER(x)) throw MakeTypeError(kSymbolToPrimitive);
if (hint == NO_HINT) hint = (IS_DATE(x)) ? STRING_HINT : NUMBER_HINT;
return (hint == NUMBER_HINT) ? DefaultNumber(x) : DefaultString(x);
}
// ECMA-262, section 8.6.2.6, page 28.
function DefaultNumber(x) {
if (!IS_SYMBOL_WRAPPER(x)) {
var valueOf = x.valueOf;
if (IS_SPEC_FUNCTION(valueOf)) {
var v = %_CallFunction(x, valueOf);
if (IsPrimitive(v)) return v;
}
var toString = x.toString;
if (IS_SPEC_FUNCTION(toString)) {
var s = %_CallFunction(x, toString);
if (IsPrimitive(s)) return s;
}
}
throw MakeTypeError(kCannotConvertToPrimitive);
}
// ECMA-262, section 8.6.2.6, page 28.
function DefaultString(x) {
if (!IS_SYMBOL_WRAPPER(x)) {
var toString = x.toString;
if (IS_SPEC_FUNCTION(toString)) {
var s = %_CallFunction(x, toString);
if (IsPrimitive(s)) return s;
}
var valueOf = x.valueOf;
if (IS_SPEC_FUNCTION(valueOf)) {
var v = %_CallFunction(x, valueOf);
if (IsPrimitive(v)) return v;
}
}
throw MakeTypeError(kCannotConvertToPrimitive);
}
上面代碼的邏輯是這樣的:
- 如果變量為字符串赤屋,直接返回立镶;
- 如果
!IS_SPEC_OBJECT(x)
,直接返回类早; - 如果
IS_SYMBOL_WRAPPER(x)
媚媒,則拋出異常; - 否則會(huì)根據(jù)傳入的
hint
來(lái)調(diào)用DefaultNumber
和DefaultString
莺奔,比如欣范,如果為Date
對(duì)象,會(huì)調(diào)用DefaultString
-
DefaultNumber
:首先x.valueOf
令哟,如果為primitive
恼琼,則返回valueOf
后的值,否則繼續(xù)調(diào)用x.toString
屏富,如果為primitive
晴竞,則返回toString
后的值,否則拋出異常 -
DefaultString
:和DefaultNumber
正好相反狠半,先調(diào)用toString
噩死,如果不是primitive
再調(diào)用valueOf
那講了實(shí)現(xiàn)原理,這個(gè)ToPrimitive
有什么用呢神年?實(shí)際很多操作會(huì)調(diào)用ToPrimitive
已维,比如加、相等或比較操作已日。在進(jìn)行加操作時(shí)會(huì)將左右操作數(shù)轉(zhuǎn)換為primitive
垛耳,然后進(jìn)行相加。
下面來(lái)個(gè)實(shí)例飘千,({}) + 1
(將{}
放在括號(hào)中是為了內(nèi)核將其認(rèn)為一個(gè)代碼塊)會(huì)輸出啥堂鲜?可能日常寫(xiě)代碼并不會(huì)這樣寫(xiě),不過(guò)網(wǎng)上出過(guò)類似的面試題护奈。
加操作只有左右運(yùn)算符同時(shí)為String
或Number
時(shí)會(huì)執(zhí)行對(duì)應(yīng)的%_StringAdd
或%NumberAdd
缔莲,下面看下({}) + 1
內(nèi)部會(huì)經(jīng)過(guò)哪些步驟:
-
{}
和1首先會(huì)調(diào)用ToPrimitive
; -
{}
會(huì)走到DefaultNumber
霉旗,首先會(huì)調(diào)用valueOf
痴奏,返回的是Object {}
,不是primitive
類型厌秒,從而繼續(xù)走到toString
读拆,返回[object Object]
,是String
類型简僧; - 最后加操作建椰,結(jié)果為
[object Object]1
雕欺。
再比如岛马,有人問(wèn)你[] + 1
輸出啥時(shí)棉姐,你可能知道應(yīng)該怎么去計(jì)算了,先對(duì)[]
調(diào)用ToPrimitive
啦逆,返回空字符串伞矩,最后結(jié)果為"1
"。
除了ToPrimitive
之外夏志,還有更細(xì)粒度的ToBoolean
乃坤、ToNumber
和ToString
,比如在需要布爾型時(shí)沟蔑,會(huì)通過(guò)ToBoolean
來(lái)進(jìn)行轉(zhuǎn)換湿诊。看一下源碼瘦材,我們可以很清楚的知道這些布爾型厅须、數(shù)字等之間轉(zhuǎn)換是怎么發(fā)生:
// ECMA-262, section 9.2, page 30
function ToBoolean(x) {
if (IS_BOOLEAN(x)) return x;
// 字符串轉(zhuǎn)布爾型時(shí),如果length不為0就返回true
if (IS_STRING(x)) return x.length != 0;
if (x == null) return false;
// 數(shù)字轉(zhuǎn)布爾型時(shí)食棕,變量不為0或NAN時(shí)返回true
if (IS_NUMBER(x)) return !((x == 0) || NUMBER_IS_NAN(x));
return true;
}
// ECMA-262, section 9.3, page 31.
function ToNumber(x) {
if (IS_NUMBER(x)) return x;
// 字符串轉(zhuǎn)數(shù)字調(diào)用StringToNumber
if (IS_STRING(x)) {
return %_HasCachedArrayIndex(x) ? %_GetCachedArrayIndex(x)
: %StringToNumber(x);
}
// 布爾型轉(zhuǎn)數(shù)字時(shí)true返回1朗和,false返回0
if (IS_BOOLEAN(x)) return x ? 1 : 0;
// undefined返回NAN
if (IS_UNDEFINED(x)) return NAN;
// Symbol拋出異常,例如:Symbol() + 1
if (IS_SYMBOL(x)) throw MakeTypeError(kSymbolToNumber);
return (IS_NULL(x)) ? 0 : ToNumber(DefaultNumber(x));
}
// ECMA-262, section 9.8, page 35.
function ToString(x) {
if (IS_STRING(x)) return x;
// 數(shù)字轉(zhuǎn)字符串簿晓,調(diào)用內(nèi)部的_NumberToString
if (IS_NUMBER(x)) return %_NumberToString(x);
// 布爾型轉(zhuǎn)字符串眶拉,true返回字符串true
if (IS_BOOLEAN(x)) return x ? 'true' : 'false';
// undefined轉(zhuǎn)字符串,返回undefined
if (IS_UNDEFINED(x)) return 'undefined';
// Symbol拋出異常
if (IS_SYMBOL(x)) throw MakeTypeError(kSymbolToString);
return (IS_NULL(x)) ? 'null' : ToString(DefaultString(x));
}
講了這么多原理憔儿,那這個(gè)ToPrimitive
有什么卵用呢忆植?這對(duì)于我們了解JavaScript內(nèi)部的隱式轉(zhuǎn)換和一些細(xì)節(jié)是非常有用的,比如:
var a = '[object Object]';
if (a == {}) {
console.log('something');
}
你覺(jué)得會(huì)不會(huì)輸出something
呢皿曲,答案是會(huì)的唱逢,所以這也是為什么很多代碼規(guī)范推薦使用===
三等了。那這里為什么會(huì)相等呢屋休,是因?yàn)檫M(jìn)行相等操作時(shí)坞古,對(duì){}
調(diào)用了ToPrimitive
,返回的結(jié)果就是[object Object]
,也就返回true
了毫玖。我們可以看下JavaScript中EQUALS的源碼就一目了然了:
// ECMA-262 Section 11.9.3.
EQUALS = function EQUALS(y) {
if (IS_STRING(this) && IS_STRING(y)) return %StringEquals(this, y);
var x = this;
while (true) {
if (IS_NUMBER(x)) {
while (true) {
if (IS_NUMBER(y)) return %NumberEquals(x, y);
if (IS_NULL_OR_UNDEFINED(y)) return 1; // not equal
if (IS_SYMBOL(y)) return 1; // not equal
if (!IS_SPEC_OBJECT(y)) {
// String or boolean.
return %NumberEquals(x, %$toNumber(y));
}
y = %$toPrimitive(y, NO_HINT);
}
} else if (IS_STRING(x)) {
// 上面的代碼就是進(jìn)入了這里残腌,對(duì)y調(diào)用了toPrimitive
while (true) {
if (IS_STRING(y)) return %StringEquals(x, y);
if (IS_SYMBOL(y)) return 1; // not equal
if (IS_NUMBER(y)) return %NumberEquals(%$toNumber(x), y);
if (IS_BOOLEAN(y)) return %NumberEquals(%$toNumber(x), %$toNumber(y));
if (IS_NULL_OR_UNDEFINED(y)) return 1; // not equal
y = %$toPrimitive(y, NO_HINT);
}
} else if (IS_SYMBOL(x)) {
if (IS_SYMBOL(y)) return %_ObjectEquals(x, y) ? 0 : 1;
return 1; // not equal
} else if (IS_BOOLEAN(x)) {
if (IS_BOOLEAN(y)) return %_ObjectEquals(x, y) ? 0 : 1;
if (IS_NULL_OR_UNDEFINED(y)) return 1;
if (IS_NUMBER(y)) return %NumberEquals(%$toNumber(x), y);
if (IS_STRING(y)) return %NumberEquals(%$toNumber(x), %$toNumber(y));
if (IS_SYMBOL(y)) return 1; // not equal
// y is object.
x = %$toNumber(x);
y = %$toPrimitive(y, NO_HINT);
} else if (IS_NULL_OR_UNDEFINED(x)) {
return IS_NULL_OR_UNDEFINED(y) ? 0 : 1;
} else {
// x is an object.
if (IS_SPEC_OBJECT(y)) {
return %_ObjectEquals(x, y) ? 0 : 1;
}
if (IS_NULL_OR_UNDEFINED(y)) return 1; // not equal
if (IS_SYMBOL(y)) return 1; // not equal
if (IS_BOOLEAN(y)) y = %$toNumber(y);
x = %$toPrimitive(x, NO_HINT);
}
}
}
所以,了解變量如何轉(zhuǎn)換為primitive
類型的重要性也就可想而知了奶陈。具體的代碼細(xì)節(jié)可以看這里:runtime.js。
ToObject
ToObject
顧名思義就是將變量轉(zhuǎn)換為對(duì)象類型附较〕粤#可以看下它是如何將非對(duì)象類型轉(zhuǎn)換為對(duì)象類型:
// ECMA-262, section 9.9, page 36.
function ToObject(x) {
if (IS_STRING(x)) return new GlobalString(x);
if (IS_NUMBER(x)) return new GlobalNumber(x);
if (IS_BOOLEAN(x)) return new GlobalBoolean(x);
if (IS_SYMBOL(x)) return %NewSymbolWrapper(x);
if (IS_NULL_OR_UNDEFINED(x) && !IS_UNDETECTABLE(x)) {
throw MakeTypeError(kUndefinedOrNullToObject);
}
return x;
}
因?yàn)槿粘4a很少用到,就不展開(kāi)了拒课。