JavaScript 类型的那些事
概述
JavaScript的类型判断是前端工程师们每天代码中必备的部分,每天肯定会写上个很多遍if (a === 'xxx')
或if (typeof a === 'object')
类似的类型判断语句,所以掌握JavaScript中类型判断也是前端必备技能,以下会从JavaScript的类型,类型判断以及一些内部实现来让你深入了解JavaScript类型的那些事。
类型
JavaScript中类型主要包括了primitive
和object
类型,其中primitive
类型包括了:null
、undefined
、boolean
、number
、string
和symbol(es6)
。其他所有的都为object
类型。
类型判断
类型检测主要包括了:typeof
、instanceof
和toString
的三种方式来判断变量的类型。
typeof
typeof
接受一个值并返回它的类型,它有两种可能的语法:
typeof x
typeof(x)
当在primitive
类型上使用typeof
检测变量类型时,我们总能得到我们想要的结果,比如:
typeof 1; // "number"
typeof ""; // "string"
typeof true; // "boolean"
typeof bla; // "undefined"
typeof undefined; // "undefined"
而当在object
类型上使用typeof
检测时,有时可能并不能得到你想要的结果,比如:
typeof []; // "object"
typeof null; // "object"
typeof /regex/ // "object"
typeof new String(""); // "object"
typeof function(){}; // "function"
这里的[]
返回的确却是object
,这可能并不是你想要的,因为数组是一个特殊的对象,有时候这可能并不是你想要的结果。
对于这里的null
返回的确却是object
,wtf,有些人说null
被认为是没有一个对象。
当你对于typeof
检测数据类型不确定时,请谨慎使用。
toString
typeof
的问题主要在于不能告诉你过多的对象信息,除了函数之外:
typeof {key:'val'}; // Object is object
typeof [1,2]; // Array is object
typeof new Date; // Date object
而toString
不管是对于object
类型,还是primitive
类型,都能得到你想要的结果:
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
是否为一个函数。
从上面的代码,我们可以看到toString
是可依赖的,不管是object
类型还是primitive
类型,它都能告诉我们正确的结果。但它只可以用于判断内置的数据类型,对于我们自己构造的对象,它还是不能给出我们想要的结果,比如下面的代码:
function Person() {
}
var a = new Person();
// [object Object]
console.log({}.toString.call(a));
console.log(a instanceof Person);
我们这时候就要用到我们下面介绍的instanceof
了。
instanceof
对于使用构造函数创建的对象,我们通常使用instanceof
来判断某一实例是否属于某种类型,例如:a instanceof Person
,其内部原理实际上是判断Person.prototype
是否在a
实例的原型链中,其原理可以用下面的函数来表达:
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));
类型转换
因为JavaScript是动态类型,变量是没有类型的,可以随时赋予任意值。但是,各种运算符或条件判断中是需要特定类型的。比如,if判断时会将判断语句转换为布尔型。下面就来深入了解下JavaScript中类型转换。
ToPrimitive
当我们需要将变量转换为原始类型时,就需要用到ToPrimitive
,下面的代码说明了ToPrimitive
的内部实现原理:
// 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)
,则抛出异常; - 否则会根据传入的
hint
来调用DefaultNumber
和DefaultString
,比如,如果为Date
对象,会调用DefaultString
-
DefaultNumber
:首先x.valueOf
,如果为primitive
,则返回valueOf
后的值,否则继续调用x.toString
,如果为primitive
,则返回toString
后的值,否则抛出异常 -
DefaultString
:和DefaultNumber
正好相反,先调用toString
,如果不是primitive
再调用valueOf
那讲了实现原理,这个ToPrimitive
有什么用呢?实际很多操作会调用ToPrimitive
,比如加、相等或比较操作。在进行加操作时会将左右操作数转换为primitive
,然后进行相加。
下面来个实例,({}) + 1
(将{}
放在括号中是为了内核将其认为一个代码块)会输出啥?可能日常写代码并不会这样写,不过网上出过类似的面试题。
加操作只有左右运算符同时为String
或Number
时会执行对应的%_StringAdd
或%NumberAdd
,下面看下({}) + 1
内部会经过哪些步骤:
-
{}
和1首先会调用ToPrimitive
; -
{}
会走到DefaultNumber
,首先会调用valueOf
,返回的是Object {}
,不是primitive
类型,从而继续走到toString
,返回[object Object]
,是String
类型; - 最后加操作,结果为
[object Object]1
。
再比如,有人问你[] + 1
输出啥时,你可能知道应该怎么去计算了,先对[]
调用ToPrimitive
,返回空字符串,最后结果为"1
"。
除了ToPrimitive
之外,还有更细粒度的ToBoolean
、ToNumber
和ToString
,比如在需要布尔型时,会通过ToBoolean
来进行转换。看一下源码,我们可以很清楚的知道这些布尔型、数字等之间转换是怎么发生:
// ECMA-262, section 9.2, page 30
function ToBoolean(x) {
if (IS_BOOLEAN(x)) return x;
// 字符串转布尔型时,如果length不为0就返回true
if (IS_STRING(x)) return x.length != 0;
if (x == null) return false;
// 数字转布尔型时,变量不为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;
// 字符串转数字调用StringToNumber
if (IS_STRING(x)) {
return %_HasCachedArrayIndex(x) ? %_GetCachedArrayIndex(x)
: %StringToNumber(x);
}
// 布尔型转数字时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;
// 数字转字符串,调用内部的_NumberToString
if (IS_NUMBER(x)) return %_NumberToString(x);
// 布尔型转字符串,true返回字符串true
if (IS_BOOLEAN(x)) return x ? 'true' : 'false';
// undefined转字符串,返回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内部的隐式转换和一些细节是非常有用的,比如:
var a = '[object Object]';
if (a == {}) {
console.log('something');
}
你觉得会不会输出something
呢,答案是会的,所以这也是为什么很多代码规范推荐使用===
三等了。那这里为什么会相等呢,是因为进行相等操作时,对{}
调用了ToPrimitive
,返回的结果就是[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)) {
// 上面的代码就是进入了这里,对y调用了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);
}
}
}
所以,了解变量如何转换为primitive
类型的重要性也就可想而知了。具体的代码细节可以看这里:runtime.js。
ToObject
ToObject
顾名思义就是将变量转换为对象类型。可以看下它是如何将非对象类型转换为对象类型:
// 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;
}
因为日常代码很少用到,就不展开了。