作者:有贊技術(shù)團(tuán)隊
原文地址:http://tech.youzan.com/javascript-type/
概述
JavaScript的類型判斷是前端工程師們每天代碼中必備的部分,每天肯定會寫上個很多遍if (a === 'xxx')
或if (typeof a === 'object')
類似的類型判斷語句,所以掌握J(rèn)avaScript中類型判斷也是前端必備技能,以下會從JavaScript的類型,類型判斷以及一些內(nèi)部實現(xiàn)來讓你深入了解JavaScript類型的那些事。
類型
JavaScript中類型主要包括了primitive
和object
類型,其中primitive
類型包括了:null
、undefined
、boolean
、number
、string
和symbol(es6)
。其他所有的都為object
類型。
類型判斷
類型檢測主要包括了:typeof
、instanceof
和toString
的三種方式來判斷變量的類型。
typeof
typeof
接受一個值并返回它的類型,它有兩種可能的語法:
typeof x
typeof(x)
當(dāng)在primitive
類型上使用typeof
檢測變量類型時,我們總能得到我們想要的結(jié)果,比如:
typeof 1; // "number"
typeof ""; // "string"
typeof true; // "boolean"
typeof bla; // "undefined"
typeof undefined; // "undefined"
而當(dāng)在object
類型上使用typeof
檢測時,有時可能并不能得到你想要的結(jié)果,比如:
typeof []; // "object"
typeof null; // "object"
typeof /regex/ // "object"
typeof new String(""); // "object"
typeof function(){}; // "function"
這里的[]
返回的確卻是object
,這可能并不是你想要的,因為數(shù)組是一個特殊的對象,有時候這可能并不是你想要的結(jié)果。
對于這里的null
返回的確卻是object
,wtf,有些人說null
被認(rèn)為是沒有一個對象。
當(dāng)你對于typeof
檢測數(shù)據(jù)類型不確定時,請謹(jǐn)慎使用。
toString
typeof
的問題主要在于不能告訴你過多的對象信息,除了函數(shù)之外:
typeof {key:'val'}; // Object is object
typeof [1,2]; // Array is object
typeof new Date; // Date object
而toString
不管是對于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
中你會看到以下代碼:
// 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
來判斷變量類型,比如你可以通過_.isFunction(someFunc)
來判斷someFunc
是否為一個函數(shù)。
從上面的代碼,我們可以看到toString
是可依賴的,不管是object
類型還是primitive
類型,它都能告訴我們正確的結(jié)果。但它只可以用于判斷內(nèi)置的數(shù)據(jù)類型,對于我們自己構(gòu)造的對象,它還是不能給出我們想要的結(jié)果,比如下面的代碼:
function Person() {
}
var a = new Person();
// [object Object]
console.log({}.toString.call(a));
console.log(a instanceof Person);
我們這時候就要用到我們下面介紹的instanceof
了。
instanceof
對于使用構(gòu)造函數(shù)創(chuàng)建的對象,我們通常使用instanceof
來判斷某一實例是否屬于某種類型,例如:a instanceof Person
,其內(nèi)部原理實際上是判斷Person.prototype
是否在a
實例的原型鏈中,其原理可以用下面的函數(shù)來表達(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)換
因為JavaScript是動態(tài)類型,變量是沒有類型的,可以隨時賦予任意值。但是,各種運算符或條件判斷中是需要特定類型的。比如,if判斷時會將判斷語句轉(zhuǎn)換為布爾型。下面就來深入了解下JavaScript中類型轉(zhuǎn)換。
ToPrimitive
當(dāng)我們需要將變量轉(zhuǎn)換為原始類型時,就需要用到ToPrimitive
,下面的代碼說明了ToPrimitive
的內(nèi)部實現(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)
,則拋出異常; - 否則會根據(jù)傳入的
hint
來調(diào)用DefaultNumber
和DefaultString
,比如,如果為Date
對象,會調(diào)用DefaultString
-
DefaultNumber
:首先x.valueOf
,如果為primitive
,則返回valueOf
后的值,否則繼續(xù)調(diào)用x.toString
,如果為primitive
,則返回toString
后的值,否則拋出異常 -
DefaultString
:和DefaultNumber
正好相反,先調(diào)用toString
,如果不是primitive
再調(diào)用valueOf
那講了實現(xiàn)原理,這個ToPrimitive
有什么用呢?實際很多操作會調(diào)用ToPrimitive
,比如加、相等或比較操作。在進(jìn)行加操作時會將左右操作數(shù)轉(zhuǎn)換為primitive
,然后進(jìn)行相加。
下面來個實例,({}) + 1
(將{}
放在括號中是為了內(nèi)核將其認(rèn)為一個代碼塊)會輸出啥?可能日常寫代碼并不會這樣寫,不過網(wǎng)上出過類似的面試題。
加操作只有左右運算符同時為String
或Number
時會執(zhí)行對應(yīng)的%_StringAdd
或%NumberAdd
,下面看下({}) + 1
內(nèi)部會經(jīng)過哪些步驟:
-
{}
和1首先會調(diào)用ToPrimitive
; -
{}
會走到DefaultNumber
,首先會調(diào)用valueOf
,返回的是Object {}
,不是primitive
類型,從而繼續(xù)走到toString
,返回[object Object]
,是String
類型; - 最后加操作,結(jié)果為
[object Object]1
。
再比如,有人問你[] + 1
輸出啥時,你可能知道應(yīng)該怎么去計算了,先對[]
調(diào)用ToPrimitive
,返回空字符串,最后結(jié)果為"1
"。
除了ToPrimitive
之外,還有更細(xì)粒度的ToBoolean
、ToNumber
和ToString
,比如在需要布爾型時,會通過ToBoolean
來進(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)布爾型時,如果length不為0就返回true
if (IS_STRING(x)) return x.length != 0;
if (x == null) return false;
// 數(shù)字轉(zhuǎn)布爾型時,變量不為0或NAN時返回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ù)字時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));
}
講了這么多原理,那這個ToPrimitive
有什么卵用呢?這對于我們了解JavaScript內(nèi)部的隱式轉(zhuǎn)換和一些細(xì)節(jié)是非常有用的,比如:
var a = '[object Object]';
if (a == {}) {
console.log('something');
}
你覺得會不會輸出something
呢,答案是會的,所以這也是為什么很多代碼規(guī)范推薦使用===
三等了。那這里為什么會相等呢,是因為進(jìn)行相等操作時,對{}
調(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)入了這里,對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)換為對象類型。可以看下它是如何將非對象類型轉(zhuǎn)換為對象類型:
// 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;
}
因為日常代碼很少用到,就不展開了。