JavaScript 類型的那些事

作者:有贊技術(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中類型主要包括了primitiveobject類型,其中primitive類型包括了:nullundefinedbooleannumberstringsymbol(es6)。其他所有的都為object類型。

類型判斷

類型檢測主要包括了:typeofinstanceoftoString的三種方式來判斷變量的類型。

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);
}

上面代碼的邏輯是這樣的:

  1. 如果變量為字符串,直接返回;
  2. 如果!IS_SPEC_OBJECT(x),直接返回;
  3. 如果IS_SYMBOL_WRAPPER(x),則拋出異常;
  4. 否則會根據(jù)傳入的hint來調(diào)用DefaultNumberDefaultString,比如,如果為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)上出過類似的面試題。

操作只有左右運算符同時為StringNumber時會執(zhí)行對應(yīng)的%_StringAdd%NumberAdd,下面看下({}) + 1內(nèi)部會經(jīng)過哪些步驟:

  1. {}和1首先會調(diào)用ToPrimitive
  2. {}會走到DefaultNumber,首先會調(diào)用valueOf,返回的是Object {},不是primitive類型,從而繼續(xù)走到toString,返回[object Object],是String類型;
  3. 最后加操作,結(jié)果為[object Object]1

再比如,有人問你[] + 1輸出啥時,你可能知道應(yīng)該怎么去計算了,先對[]調(diào)用ToPrimitive,返回空字符串,最后結(jié)果為"1"。

除了ToPrimitive之外,還有更細(xì)粒度的ToBooleanToNumberToString,比如在需要布爾型時,會通過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;
}

因為日常代碼很少用到,就不展開了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容