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.

  • typeof x
  • typeof(x)

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.

  1. If the variable is a string, return it directly

  2. if !IS_SPEC_OBJECT(x), return directly

  3. if IS_SYMBOL_WRAPPER(x), then throw an exception

  4. 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.

  1. {} and 1 will first call ToPrimitive
  2. {} 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
  3. 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/