【JS】類型檢測

本文首發於個人我的博客 : cherryblog.site/node

前言

js 中的類型檢測也是很重要的一部分,因此說這篇文章咱們就來說一下怎麼對 JavaScript 中的基本數據類型進行檢測。其實這也是在讀 Zepto 源碼中學習到的,因此閱讀源碼對咱們的提高仍是頗有幫助的。本文基於參考了前輩們的文章以後我的理解此文寫的有不當的地方,請各位大佬指正。web

其實常規方法主要有四種segmentfault

  1. typeof
  2. instanceof
  3. Object.prototype.toString
  4. construcor

其實這四種方式歸根結底就是兩種思路:數組

  1. 根據數據類型判斷(1,2)
  2. 根據構造函數判斷(3,4)

前置基礎

再看 Zepto 以前看了 慕課網一個老師的視頻,一共一個小時左右,開了快進估計也就 45 分鐘左右。只是講了 Zepto 的架構和設計,沒有詳細的將每個方法,初看以前能夠看一下,對 Zepto 有一個大概的印象。瀏覽器

原型與原型鏈

其實這部分真的是老生常談的問題,可是每一次聽其餘人都有新的收穫。真的是不想寫這部分,可是自我感受總體思路比較清晰,因此推薦你們閱讀一下。架構

Zepto 整個的設計思想實際上是基於 js 的原型鏈。關於原型鏈,這個老師講的比較清晰,須要記住三句話:app

  1. 每個函數,都有一個 prototype 屬性。
  2. 全部經過函數 new 出來的對象,這個對象都有一個 __proto__ 指向這個函數的 prototype。
  3. 當你想要使用一個對象(或者一個數組)的某個功能時:若是該對象自己具備這個功能,則直接使用;若是該對象自己沒有這個功能,則去 __proto__ 中找。

什麼是 prototype(顯示原型)

每個函數在建立以後都會擁有一個名爲 prototype 的屬性,這個屬性指向函數的原型對象。經過Function.prototype.bind方法構造出來的函數是個例外,它沒有prototype屬性。函數

var fn = function() {}
console.log( fn.prototype );複製代碼

經過下面這幅圖咱們能夠看出建立一個函數時,都會有一個 prototype 屬性指向它的原型。而 fn.prototype 中有一個 constructor 屬性指向 fn 函數。
學習

原型圖
原型圖

什麼是 __proto__(隱式原型)

JavaScript 中任意對象都有一個內置屬性 __proto__,隱式原型指向建立這個對象的函數(constructor)的 prototype。測試

Object.prototype 這個對象是個例外,它的 __proto__ 值爲 null

console.log( typeof Array );   // 'function'
console.log( Array.prototype );複製代碼

數組構造函數 Array 也是一個函數,而且在 Array 的原型中除了指向 Array 的 constructor 以外還有其餘的內置對象。

__proto__ 的指向

上面應該都不難理解,主要是 __proto__ 的指向,這個問題是比較難理解的,咱們來看剛剛的定義,__proto__ 指向建立這個對象的函數的顯式原型。建立函數一共有三種方式:

  1. 字面量方式
var person1 = {
       name: 'cyl',
       sex: 'male'
    };複製代碼

字面量的方式是一種爲了開發人員更方便建立對象的一個語法糖,本質就是

var o = new Object(); 
   o.xx = xx;
   o.yy=yy;複製代碼

因此說使用字面量方式建立的函對象的 __proto__ 屬性是指向 Object.prototype 的。

  1. 構造函數
    所謂的構造函數,就是經過 new 關鍵字調用的函數,只要是經過 new 關鍵字調用的函數都是構造函數。由構造函數構造的對象,其 __prototype__ 指向其構造函數的 prototype 屬性指向的對象。
var  arr = new Array()複製代碼

好比 arr 是一個實例化的數組,那麼 arr 的 __proto__ 屬性就指向 Array 的 prototype 屬性。

原型圖
原型圖

  1. 函數經過 Object.create 構造的
var person1 = {
    name: 'cyl',
    sex: 'male'
};

var person2 = Object.create(person1);複製代碼

Object.create 的內部實際上是這樣的:

function object(o){
    function F(){}
    F.prototype = o;
    return new F()
}複製代碼

也能夠當作是經過 new 建立的。因此說咱們就能夠一目瞭然,person2 的 __proto__ 是指向 person1 的。(注意,是直接指向 person1,而不是 person1.prototype)。

prototype__proto__ 的做用

在瞭解了什麼是顯示原型 prototype 和隱式原型 __proto__ 以後,咱們也知道了怎麼去找隱式原型,那麼它們有什麼做用呢?

  • 顯式原型的做用:用來實現基於原型的繼承與屬性的共享。
  • 隱式原型的做用:構成原型鏈,一樣用於實現基於原型的繼承。舉個例子,當咱們訪問 obj 這個對象中的 x 屬性時,若是在 obj 中找不到,那麼就會沿着 __proto__ 依次查找。

這裏咱們要注意了,當咱們訪問 obj 這個對象中的 x 屬性時,若是在 obj 中找不到,那麼就會沿着 __proto__ 依次查找。

劃重點,是在 __proto__ 中依次查找

重寫 __proto__

既然咱們知道了繼承其實是繼承對象 __proto__ 上的屬性,那咱們就能夠改寫咱們的 __proto__ 屬性。

var arr = [1,2,3];
arr.__proto__.addClass = function () {
    console.log(123);
}
arr.push(4);
arr.addClass();   // 123複製代碼

修改了以後,arr 不只有內置的 concatpush 等功能,還多了一個 addClass 功能。

也能夠徹底改寫 __proto__ 屬性,那麼其原先的全部的功能都沒有了,以下圖所示。

原型圖
原型圖

是時候祭上這張圖了:

深刻理解JS原型鏈
深刻理解JS原型鏈

typeof

typeof 是解釋器內部實現,根據 ECMA-262 規定的幾種類型的值來返回類型名稱。

可是 typeof 的應用場景很是有限,基本上只能判斷出來使用字面量方式賦值的基本數據類型,例如:

var  a = 123;
    console.log(typeof(a));   // number

    var  b = "string";
    console.log(typeof(b));   // string

    var  c = true;
    console.log(typeof(c));   // boolean

    var  d;
    console.log(typeof(d));   // undefined

    var  e = null;
    console.log(typeof(e));   // object

    var  f = [1,2,3];
    console.log(typeof(f));   // object

    var  g = {};
    console.log(typeof(g));   // object

    var  fun = function () {};
    console.log(typeof(fun)); // function

    var  A = new Number(123);
    console.log(typeof(A));   // object
    console.log(A instanceof Number);  // true

    var  B = new String("123");
    console.log(typeof(B));    // object
    console.log(B instanceof String);  // true複製代碼

由以上例子能夠看出,typeof 測試的結果並非特別的準確,而且只能檢測使用字面量命名的基本數據類型(除了 null)。因此咱們通常不使用 typeof 進行數據檢測。

instanceof

在上面的例子中,咱們已經使用了 typeof 進行數據檢測。
instance 是「例子,實例」的意思,因此 instanceof 意思是用於判斷變量是不是某一個對象的實例。

instanceof 原理

如下部分是根據 JavaScript instanceof 運算符深刻剖析 理解。

instanceof 的原理能夠認爲是以下:

function instance_of(L, R) {    //L 表示左表達式,R 表示右表達式
 var O = R.prototype;           // 取 R 的顯示原型
 L = L.__proto__;               // 取 L 的隱式原型
 while (true) { 
   if (L === null) 
     return false; 
   if (O === L)                 // 這裏重點:當 O 嚴格等於 L 時,返回 true 
     return true; 
   L = L.__proto__; 
 } 
}複製代碼

再結合咱們在最開始介紹的前置知識的這張圖來看幾個例子幫助咱們更好的理解 instanceof 的原理:

JS原型鏈
JS原型鏈

例1:

Object instanceof Object;
// 爲了方便表述,首先區分左側表達式和右側表達式
ObjectL = Object, ObjectR = Object; 
// 下面根據規範逐步推演
O = ObjectR.prototype = Object.prototype 
L = ObjectL.__proto__ = Function.prototype 
// 第一次判斷
O != L 
// 循環查找 L 是否還有 __proto__ 
L = Function.prototype.__proto__ = Object.prototype 
// 第二次判斷
O == L 
// 返回 true複製代碼

例2:

Function instanceof Function
// 爲了方便表述,首先區分左側表達式和右側表達式
FunctionL = Function, FunctionR = Function; 
// 下面根據規範逐步推演
O = FunctionR.prototype = Function.prototype 
L = FunctionL.__proto__ = Function.prototype 
// 第一次判斷
O == L 
// 返回 true複製代碼

例3:

Foo instanceof Foo
// 爲了方便表述,首先區分左側表達式和右側表達式
FooL = Foo, FooR = Foo; 
// 下面根據規範逐步推演
O = FooR.prototype = Foo.prototype 
L = FooL.__proto__ = Function.prototype 
// 第一次判斷
O != L 
// 循環再次查找 L 是否還有 __proto__ 
L = Function.prototype.__proto__ = Object.prototype 
// 第二次判斷
O != L 
// 再次循環查找 L 是否還有 __proto__ 
L = Object.prototype.__proto__ = null 
// 第三次判斷
L == null 
// 返回 false複製代碼

其實,instanceof 的重點也就是左邊對象的隱式原型等於右邊構造函數的顯示原型,是否是聽着很熟悉呢,這就是在 new 操做中的關鍵一步(new 操做是賦值),這樣就能夠判斷指定的對象是不是某個構造函數的實例,使 L = L.proto(繼續沿原型鏈向上尋找),一直循環判斷左邊對象的隱式原型等於右邊構造函數的顯示原型,直到L.__proto__ 爲 null(L 已經循環到 object.prototype) 或者 true

instanceof 侷限性

instanceof 的侷限性應該也就是不能檢測基本數據類型了吧,其餘的貌似沒什麼。經過對 instanceof 的原理進行分析,咱們能夠得知,只要左邊的對象的對象可以經過原型鏈 __proto__ 是指向右邊的構造函數就能夠~

instanceof 右邊必須是對象或構造函數,不然會拋出 TypeError 的錯誤。

Object.prototype.toString

全部的數據類型均可以用 Object.prototype.toString 來檢測,並且很是的精準。

如下內容參考 談談Object.prototype.toString

咱們先來看一下 Object.prototype.toString 是怎麼進行類型檢測的

var a = 123;
    console.log(Object.prototype.toString.call(a));    // [object Number]

    var b = "string";
    console.log(Object.prototype.toString.call(b));    // [object String]

    var c = [];
    console.log(Object.prototype.toString.call(c));    // [object Array]

    var d = {};
    console.log(Object.prototype.toString.call(d));    // [object Object]

    var e = true;
    console.log(Object.prototype.toString.call(e));    // [object Boolean]

    var f =  null;
    console.log(Object.prototype.toString.call(f));    // [object Null]

    var g;
    console.log(Object.prototype.toString.call(g));    // [object Undefined]

    var h = function () {};
    console.log(Object.prototype.toString.call(h));    // [object Function]

    var A = new Number();
    console.log(Object.prototype.toString.call(A));    // [object Number]複製代碼

因此說,Object.prototype.toString 仍是可以比較準確的檢測出對應的類型的。

Object.prototype.toString 的實現過程

在 ECMAScript 5中,Object.prototype.toString() 被調用時,會進行以下步驟:

  1. 若是 thisundefined ,返回 [object Undefined]
  2. 若是 thisnull, 返回 [object Null]
  3. Object 爲以 this 做爲參數調用 ToObject 的結果;
  4. classObject 的內部屬性 [[Class]] 的值;
  5. 返回三個字符串 [object", class, 以及"] 拼接而成的字符串。

[[Class]]

本規範的每種內置對象都定義了 [[Class]] 內部屬性的值。宿主對象的 [[Class]] 內部屬性的值能夠是除了 "Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", "String" 的任何字符串。[[Class]] 內部屬性的值用於內部區分對象的種類。注,本規範中除了經過 Object.prototype.toString ( 見 15.2.4.2) 沒有提供任何手段使程序訪問此值。

在 JavaScript 代碼裏,惟一能夠訪問該屬性的方法就是經過 Object.prototype.toString,一般方法以下:

Object.prototype.toString.call/apply(value)複製代碼

在 ES6 請參見 談談 Object.prototype.toString

construtor

construtor 其實也是用了原型鏈的知識。

constructor 屬性返回對建立此對象的數組函數的引用。

var a = 123;
    console.log( a.constructor == Number);    // true

    var b = "string";
    console.log( b.constructor == String);    // true

    var c = [];
    console.log( c.constructor == Array);    // true

    var d = {};
    console.log( d.constructor == Object);    // true

    var e = true;
    console.log( e.constructor == Boolean);    // true

    var f =  null;
    console.log( f.constructor == Null);    // TypeError: Cannot read property 'constructor' of null

    var g;
    console.log( g.constructor == Undefined);    // Uncaught TypeError: Cannot read property 'constructor' of
    undefined

    var h = function () {};
    console.log( h.constructor == Function);    // true

    var A = new Number();
    console.log( A.constructor == Number);    // true

    var A = new Number();
    console.log( A.constructor == Object);    // false複製代碼

經過上述的實例,咱們能夠看到,不管是經過字面量或者構造函數建立的基本類型,均可以檢測出。而且也能夠檢測出 ArrayObjectFunction引用類型,可是不能檢測出 NullUndefined

Zepto 中檢測數據類型

在 Zepto 中主要是用 Object.prototype.toString 來作數據類型的判斷的
如今讓咱們來看一下 Zepto 中是怎麼檢測這些數據類型的:

var class2type = {},
        toString = class2type.toString

    // 在代碼中部,執行了
    // $.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
    // class2type[ "[object " + name + "]" ] = name.toLowerCase()
    // })
    // 用來給 class2type 對象賦值
    //
    // a ? b : c || d
    //type 用來判斷類型
    function type(obj) {
        return obj == null ? String(obj) :
        class2type[toString.call(obj)] || "object"
    }複製代碼

這裏你可能會有疑問,爲何使用 toString 而不是 Object.prototype.toString
那是由於若是將基本數據類型,好比 string、number、boolean等類型的值使用 toString 的方法時,是直接將基本數據類型轉換爲 string 類型,可是若是對 object 類型使用 toString 方法,則是會調用其原型上的 toString 方法,也就是 Object.prototype.toString。因此 Zepto 在開頭的地方就定義了 class2type 爲一個 object 類型。

若是 obj 的類型爲 null 或者 undefined 直接返回,若是該對象的 Object.prototype.toString 將返回的結果做爲 class2type 的 key 取值。Object.prototype.toString 對不一樣的數據類型會返回形如 [object Boolean] 的結果。

若是都不是以上狀況,默認返回 object 類型。

Zepto 中的其餘檢測方法

isFunction

// 判斷是不是函數
    function isFunction(value) { return type(value) == "function" }複製代碼

isWindow

// 判斷是不是 window對象(注意,w爲小寫)指當前的瀏覽器窗口,window對象的window屬性指向自身。
    // 即 window.window === window
    function isWindow(obj) { return obj != null && obj == obj.window }複製代碼

isDocument

// 判斷是不是 document 對象
    // window.document.nodeType == 9 數字表示爲9,常量表示爲 DOCUMENT_NODE
    function isDocument(obj) { return obj != null && obj.nodeType == obj.DOCUMENT_NODE }複製代碼

isObject

// 判斷是不是 object
    function isObject(obj) { return type(obj) == "object" }複製代碼

isPlainObject

function isPlainObject(obj) {
        return isObject(obj) && !isWindow(obj) && Object.getPrototypeOf(obj) == Object.prototype
    }複製代碼

isArray

// 判斷是不是arr
    isArray = Array.isArray || function(object){ return object instanceof Array };複製代碼

likeArray

// 判斷是不是數組或者對象數組
    // !!的做用是把一個其餘類型的變量轉成的bool類型。
    // !!obj 直接過濾掉了false,null,undefined,''等值
    function likeArray(obj) {
        var length = !!obj && 'length' in obj && obj.length,

            // 獲取obj的數據類型
            type = $.type(obj);

        // 不能是function類型,不能是window
        // 若是是array則直接返回true
        // 或者當length的數據類型是number,而且其取值範圍是0到(length - 1)這裏是經過判斷length - 1 是否爲obj的屬性

        return 'function' != type && !isWindow(obj) && (
                'array' == type || length === 0 ||
                (typeof length == 'number' && length > 0 && (length - 1) in obj)
            )
    }複製代碼

isEmptyObject

// 空對象
    $.isEmptyObject = function(obj) {
        var name
        for (name in obj) return false
        return true
    }複製代碼

isNumeric

//數字
    $.isNumeric = function(val) {
        var num = Number(val), type = typeof val;
        return val != null && type != 'boolean' &&
            (type != 'string' || val.length) &&
            !isNaN(num) && isFinite(num) || false
    }複製代碼
相關文章
相關標籤/搜索