1. 程式人生 > >深入JavaScript類型判定

深入JavaScript類型判定

引用 logs 二進制 字符 陷阱 操作 獨立 date tostring

JavaScript的數據類型

基本區分方法

ECMAScript標準定義了7種數據類型
6 種 基本類型:
Boolean,兩種取值:true和false
Null,一種取值:null
Undefined,一種取值:undefined
Number,JS的數值為基於 IEEE 754 標準的雙精度 64 位二進制格式的值(-(263 -1) 到 263 -1)。
String,JavaScript的字符串類型用於表示文本數據。它是一組16位的無符號整數值的“元素”。它不可改變。
Symbol,符號是唯一的並且是不可修改的, 並且也可以用來作為Object的key。
一種復雜類型:
Object,可以認為Object是一種鍵值對的集合。Array和Function就是Object的子類型。

另一種區分方法:值和址

從C語言過來的朋友一定常聽到傳值和傳址這樣的說法,C語言裏有指針的概念,指針本質上是一個內存地址,程序員可以通過指針來修改某些內容。
在java和JavaScript這樣的類C語言中雖然沒有了指針這麽強大卻危險的東西,但是在函數中操作變量的方式卻一脈而成。
舉個例子:

var 
    globalA = 1
;
function foo (a) {
    a = 2;
}

foo(globalA);
console.log(globalA);//1

在函數中,如果入參是值類型,那麽函數將會在執行時上下文建立一個副本,在函數中,實際修改的這個副本,不會影響真正的原始入參。
如果入參是值類型,也就是我們常說的引用類型,那麽將直接操作所引用的對象,也就是所說的,通過地址操作值。

var
    obj = {a:1}
;
function foo2 (o) {
    o.a = 2;
}
ArrFoo(obj);
console.log(obj);//{a:2}

此處需要註意的一點是,函數的形參o雖然是引用類型,但是它也是一個執行上下文中建立的副本,如果直接將它重新賦值,例如

o = {a:2};

這種寫法是不能影響到原始的入參的,執行完畢以後,原始的obj不會被修改。
上面提到的是我們不論在C還是Java中也會涉及的一些傳址傳值的基本概念。
但是在JavaScript中有一些特別的地方。

每個函數都可能是構造函數

值和址一般是在入參裏做體現,出參方面,按照正常理解即可。由於JavaScript的原型鏈特點,每個函數都可能是構造函數。在構造函數中,情況稍有不同。

構造函數中帶有return語句,如果return的是:
值類型,那麽構造函數會忽略掉這個值,返回構造的新對象;
引用類型(數組、函數、對象),那麽構造函數就會直接返回該引用類型;

// 因為Super1返回的 123 是值類型,它被丟棄,直接返回構造對象
function Super1(a){
    this.a=a;
    return 123;          // 將return語句註釋掉也沒影響
}
Super1.prototype.sayHello=function(){
    console.log("Hello")
}
console.log(new Super1(1));
// Super2直接返回了對象,而前面構造函數的所有操作全被丟棄
// 包括它的構造函數、原型鏈全部都沒有返回
function Super2(a){
    this.a=a;
    return {a:2};
}
Super2.prototype.sayHello=function(){
    console.log("Hello")
}
console.log(new Super2(3));

換言之,如果是return的是值類型,return則沒什麽作用;如果是引用類型,則對構造對象的任何操作不生效,直接返回原來的引用對象。

不靠譜的typeof

其實js有一個自帶的操作符專門用來進行類型判定———typeof,不過它只能判斷幾種基本類型。對於復雜類型,它有些無能無力,而且,對於一些基本類型,它還有一些陷阱。
類型 | 結果

  • | -:
    Undefined | "undefined"
    Null | "object"
    Boolean | "boolean"
    Number | "number"
    String | "string"
    Symbol | "symbol"
    function | "function"
    任何其他對象(Array,Date等原生對象) | "object"

typeof有一些古怪的bug

即使是在typeof可以判定的地方,也會有一些bug。例如:
最經典的

typeof null === ‘object‘ // true

以及new操作符

typeof Number(1) === ‘number‘ // true
typeof new Number(1) === ‘object‘ // true

由於typeof的功能簡陋,尤其是無法對object下的子類型做出詳細的判定,所以我們常用另一個操作符進行判定。

稍微靠譜的instanceof

instanceof 用來測試一個對象在其原型鏈中是否存在一個構造函數的 prototype 屬性。
大概原理就是不停地去判斷當前對象 _ _ proto _ _ 上的對象是否與實例的prototype相等。不相等的話,就令當前對象

a. _ _ proto _ _ = a. _ _ proto _ _ . _ _ proto _ _ 繼續判斷。直到結果為null和true為止。
舉個栗子:

// 構造函數
function Foo(){}

var
    foo1 = new Foo()
;

foo1.constructor === Foo; //true
foo1.__proto__.constructor === Foo; // true;


foo1.__proto__ === Foo.prototype; // true;

foo1.__proto__.__proto__ === Object.prototype; // true

foo1.__proto__.__proto__.__proto__ === null; // true

上面的代碼展示了instanceof的判斷原理,具體實現可以參考規範:

  1. https://tc39.github.io/ecma262/#sec-instanceofoperator
  2. https://tc39.github.io/ecma262/#sec-ordinaryhasinstance

根據instanceof,我們基本上可以判斷Object下的所有對象類型了。但是instanceof也有一個問題。

一切都在window下

在瀏覽器環境裏,所有的構造函數,基本的對象,都掛在window下。Object也不例外。這導致了一個問題,在iframe這樣的獨立於當前窗口的環境裏,instanceof可能會有bug產生。
對於Array這樣的常用對象,新版本提供了Array.isArray原生方法進行判定,但是對於Date等對象,就沒有這麽好了。所以我們還是需要一個更加健壯的類型判定方法。
Prototype.js、underScore.js和jQuery的前輩們為我們找到了一個絕妙的方法——
Object.prototype.toString.call
通過這個技巧,再結合typeof和instanceof,我們可以判斷幾乎全部的原生對象類型。
下面是underscore的部分源碼:

 _.each([‘Arguments‘, ‘Function‘, ‘String‘, ‘Number‘, ‘Date‘, 
  ‘RegExp‘, ‘Error‘, ‘Symbol‘, ‘Map‘, ‘WeakMap‘, ‘Set‘, ‘WeakSet‘], function(name) {
    _[‘is‘ + name] = function(obj) {
      return toString.call(obj) === ‘[object ‘ + name + ‘]‘;
    };
  });

感謝閱讀。

REFER

  1. typeof
  2. instanceof
  3. underscore.js

深入JavaScript類型判定