【JS】型別檢測

sunshine小小倩發表於2017-09-10

本文首發於我的個人部落格 : cherryblog.site/

前言

js 中的型別檢測也是很重要的一部分,所以說這篇文章我們就來講一下怎麼對 JavaScript 中的基本資料型別進行檢測。其實這也是在讀 Zepto 原始碼中學習到的,所以閱讀原始碼對我們的提升還是很有幫助的。本文基於參考了前輩們的文章之後個人理解此文寫的有不當的地方,請各位大佬指正。

其實常規方法主要有四種

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

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

  1. 根據資料型別判斷(1,2)
  2. 根據建構函式判斷(3,4)

前置基礎

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

原型與原型鏈

其實這部分真的是老生常談的問題,但是每一次聽其他人都有新的收穫。真的是不想寫這部分,但是自我感覺整體思路比較清晰,所以推薦大家閱讀一下。

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

  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
    }複製程式碼

相關文章