Overview
The type judgment of JavaScript
is a necessary part of the front-end engineers’ code every day, and every day they must write a if (a === 'xxx')
or if (typeof a === 'object')
similar type judgment statement, so mastering the type judgment of JavaScript
is also a necessary skill for the front-end, the following will be from the type of JavaScript
, type judgment and some internal implementation to give you an in-depth understanding of JavaScript
type of those things.
Type
The types in JavaScript
mainly include primitive
and object
types, where the primitive
types include: null
, undefined
, boolean
, number
, string
and symbol(es6)
. All others are of type object
.
Type judgment
Type detection mainly includes three ways to determine the type of a variable: typeof
, instanceof
and toString
.
typeof
typeof
takes a value and returns its type, and it has two possible syntaxes.
When using typeof
on the primitive
type to detect the type of a variable, we always get the result we want, e.g.
1
2
3
4
5
|
typeof 1; // "number"
typeof ""; // "string"
typeof true; // "boolean"
typeof bla; // "undefined"
typeof undefined; // "undefined"
|
And when using typeof
detection on object
types, sometimes you may not get the result you want, e.g.
1
2
3
4
5
|
typeof []; // "object"
typeof null; // "object"
typeof /regex/ // "object"
typeof new String(""); // "object"
typeof function(){}; // "function"
|
Here []
returns object
, which may not be what you want, because an array is a special object, and sometimes that may not be what you want.
For null
here it does return object
, wtf, some people say null
is considered to be the absence of an object.
Use caution when you are unsure about typeof
to detect data types.
toString
The problem with typeof
is mainly that it doesn’t tell you too much about the object, except for the function.
1
2
3
|
typeof {key:'val'}; // Object is object
typeof [1,2]; // Array is object
typeof new Date; // Date object
|
And toString
will give you the result you want, whether for object
types or primitive
types.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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]
|
In the underscore
you will see the following code.
1
2
3
4
5
6
|
// 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 + ']';
};
});
|
Here is where toString
is used to determine the type of a variable, for example you can tell if someFunc
is a function by _.isFunction(someFunc)
.
From the above code we can see that toString
is dependable and it can tell us the correct result regardless of whether it is of type object
or primitive
. However, it can only be used to determine built-in data types. For objects we construct ourselves, it still can’t give us the result we want, such as the following code.
1
2
3
4
5
6
7
|
function Person() {
}
var a = new Person();
// [object Object]
console.log({}.toString.call(a));
console.log(a instanceof Person);
|
This is where we need to use instanceof
, which we describe below.
instanceof
For objects created using constructors, we usually use instanceof
to determine whether a certain instance belongs to a certain type, for example: a instanceof Person
, the internal principle is actually to determine whether Person.prototype
is in the prototype chain of a
instance, the principle can be expressed by the following function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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));
|
Type conversion
Because JavaScript
is dynamically typed, variables are untyped and can be assigned arbitrary values at any time. However, there are various operators or conditional judgments that require specific types, such as if judgments that convert judgment statements to Boolean types. Here’s an in-depth look at type conversion in JavaScript
.
ToPrimitive
When we need to convert a variable to a primitive type, we need to use ToPrimitive
. The following code illustrates the internal implementation principle of ToPrimitive
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
// 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);
}
|
The logic of the above code is as follows.
-
If the variable is a string, return it directly
-
if !IS_SPEC_OBJECT(x)
, return directly
-
if IS_SYMBOL_WRAPPER(x)
, then throw an exception
-
Otherwise, DefaultNumber
and DefaultString
will be called according to the hint
passed in, for example, if it is a Date
object, DefaultString
will be called
DefaultNumber
: first x.valueOf
, if primitive
, return the value after valueOf
, otherwise continue to call x.toString
, if primitive
, return the value after toString
, otherwise throw an exception
DefaultString
: the opposite of DefaultNumber
, call toString
first, then valueOf
if it is not primitive
So after talking about the implementation principle, what is the use of this ToPrimitive
? ToPrimitive is called for many operations, such as adding, equaling, or comparing. In the add operation, the left and right operands will be converted to primitive
and then added together.
Here’s an example. What does ({}) + 1
(the {}
is put in parentheses so that the kernel thinks of it as a block of code) output? You probably don’t write code like this every day, but there are similar interview questions online.
The add operation only performs the corresponding %_StringAdd
or %NumberAdd
when the left and right operators are both String
or Number
, so look at the following steps inside ({}) + 1
.
{}
and 1
will first call ToPrimitive
{}
goes to DefaultNumber
, which first calls valueOf
, which returns Object {}
, not the primitive
type, and thus continues to toString
, which returns [object Object]
, the String
type
- The final add operation results in
[object Object]1
If someone asks you what the output of [] + 1
is, you may know how to calculate it, first call ToPrimitive
on [], return the empty string, and the final result is "1"
.
In addition to ToPrimitive
, there are more fine-grained ToBoolean
, ToNumber
and ToString
, for example, when a boolean type is needed, it will be converted by ToBoolean
. Looking at the source code we can clearly see how these conversions between boolean types, numbers, etc. occur.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
// ECMA-262, section 9.2, page 30
function ToBoolean(x) {
if (IS_BOOLEAN(x)) return x;
// Returns true if length is not 0 when converting a string to boolean
if (IS_STRING(x)) return x.length != 0;
if (x == null) return false;
// Returns true if the variable is not 0 or NAN when converting from numeric to boolean
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;
// String to number call StringToNumber
if (IS_STRING(x)) {
return %_HasCachedArrayIndex(x) ? %_GetCachedArrayIndex(x)
: %StringToNumber(x);
}
// Boolean to numeric true returns 1, false returns 0
if (IS_BOOLEAN(x)) return x ? 1 : 0;
// undefined return NAN
if (IS_UNDEFINED(x)) return NAN;
// Symbol throws an exception, e.g. 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;
// Number to string, call the internal _NumberToString
if (IS_NUMBER(x)) return %_NumberToString(x);
// Boolean to string, true returns string true
if (IS_BOOLEAN(x)) return x ? 'true' : 'false';
// undefined to string, return undefined
if (IS_UNDEFINED(x)) return 'undefined';
// Symbol throws an exception
if (IS_SYMBOL(x)) throw MakeTypeError(kSymbolToString);
return (IS_NULL(x)) ? 'null' : ToString(DefaultString(x));
}
|
After all these principles, what is the use of this ToPrimitive
? It is very useful for us to understand the implicit conversions inside JavaScript
and some details, such as
1
2
3
4
|
var a = '[object Object]';
if (a == {}) {
console.log('something');
}
|
Do you think it will output something
? The answer is yes, so that’s why many code specifications recommend using ===
three equal. So why is it equal here? It’s because when the equal operation is performed, {}
calls ToPrimitive
, and the result returned is [object Object]
, which also returns true
. We can look at the source code of EQUALS
in JavaScript
to see this at a glance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
// 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);
}
}
}
|
So the importance of understanding how variables are converted to primitive
types is understandable. The details of the code can be found here: runtime.js
.
ToObject
ToObject
is, as the name implies, a conversion of variables to object types. You can see how it converts a non-object type to an object type.
1
2
3
4
5
6
7
8
9
10
11
|
// 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;
}
|
Because the daily code is rarely used, we will not go into it much.
Reference https://tech.youzan.com/javascript-type/