JavaScript 類型的那些事

概述

JavaScript的類型判斷是前端工程師們天天代碼中必備的部分,天天確定會寫上個不少遍if (a === 'xxx')if (typeof a === 'object')相似的類型判斷語句,因此掌握JavaScript中類型判斷也是前端必備技能,如下會從JavaScript的類型,類型判斷以及一些內部實現來讓你深刻了解JavaScript類型的那些事。javascript

類型

JavaScript中類型主要包括了primitiveobject類型,其中primitive類型包括了:nullundefinedbooleannumberstringsymbol(es6)。其餘全部的都爲object類型。前端

類型判斷

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

typeof

typeof接受一個值並返回它的類型,它有兩種可能的語法:node

  • typeof x
  • typeof(x)

當在primitive類型上使用typeof檢測變量類型時,咱們總能獲得咱們想要的結果,好比:git

typeof 1; // "number"
typeof ""; // "string"
typeof true; // "boolean"
typeof bla; // "undefined"
typeof undefined; // "undefined"

而當在object類型上使用typeof檢測時,有時可能並不能獲得你想要的結果,好比:es6

typeof []; // "object"
typeof null; // "object"
typeof /regex/ // "object"
typeof new String(""); // "object"
typeof function(){}; // "function"

這裏的[]返回的確倒是object,這可能並非你想要的,由於數組是一個特殊的對象,有時候這可能並非你想要的結果。github

對於這裏的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);
}

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

  1. 若是變量爲字符串,直接返回
  2. 若是!IS_SPEC_OBJECT(x),直接返回
  3. 若是IS_SYMBOL_WRAPPER(x),則拋出異常
  4. 不然會根據傳入的hint來調用DefaultNumberDefaultString,好比若是爲Date對象,會調用DefaultString

    1. DefaultNumber:首先x.valueOf,若是爲primitive,則返回valueOf後的值,不然繼續調用x.toString,若是爲primitive,則返回toString後的值,不然拋出異常
    2. DefaultString:和DefaultNumber正好相反,先調用toString,若是不是primitive再調用valueOf

那講了實現原理,這個ToPrimitive有什麼用呢?實際不少操做會調用ToPrimitive,好比相等比較操。在進行操做時會將左右操做數轉換爲primitive,而後進行相加。

下面來個實例,({}) + 1(將{}放在括號中是爲了內核將其認爲一個代碼塊)會輸出啥?可能平常寫代碼並不會這樣寫,不過網上出過相似的面試題。

操做只有左右運算符同時爲StringNumber時會執行對應的%_StringAdd%NumberAdd,下面看下({}) + 1內部會通過哪些步驟:

  1. {}1首先會調用ToPrimitive
  2. {}會走到DefaultNumber,首先會調用valueOf,返回的是Object {},不是primitive類型,從而繼續走到toString,返回[object Object],是String類型
  3. 最後加操做,結果爲[object Object]1

再好比有人問你[] + 1輸出啥時,你可能知道應該怎麼去計算了,先對[]調用ToPrimitive,返回空字符串,最後結果爲"1"

除了ToPrimitive以外,還有更細粒度的ToBooleanToNumberToString,好比在須要布爾型時,會經過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;
}

由於平常代碼不多用到,就不展開了。

本文首發於有贊技術博客:http://tech.youzan.com/javasc...

相關文章
相關標籤/搜索