1. 程式人生 > 前端設計 >聊一聊typeof instanceof 實現原理

聊一聊typeof instanceof 實現原理

前言

最近在回頭看看js基礎,正好對判斷資料型別有些不懂的地方,或者說不太明白typeof和instanceof原理,所以準備研究研究?

涉及到原型的可以看看這篇文章?

淺談JavaScript原型

資料型別

最新的 ECMAScript 標準定義了 8 種資料型別:

可能大家對BigInt原始資料型別比較陌生,它的提出解決了一部分問題,比如大於253 - 1 的整數。這原本是 Javascript中可以用 Number 表示的最大數字。BigInt

可以表示任意大的整數。

瞭解了資料型別後,我們接下來就來看看如何檢測資料型別吧。


檢測資料型別

typeof

typeof 操作符返回一個字串,表示未經計算的運算元的型別。

總結一下可能的返回值:

  • "undefined"
  • "object"
  • "boolean"
  • "number"
  • "bigint"
  • "string"
  • "symbol"
  • "function"

附加資訊:

typeof null === 'object';
複製程式碼

這可能說是一個JavaScript設計的Bug吧。MDN規範是這麼解釋的:

在 JavaScript 最初的實現中,JavaScript 中的值是由一個表示型別的標籤和實際資料值表示的。物件的型別標籤是 0。由於 null

代表的是空指標(大多數平臺下值為 0x00),因此,null 的型別標籤是 0,typeof null 也因此返回 "object"

typeof在判斷object型別的資料的時候,不能準確的告知我們具體是哪一種Object,而且在判斷null的時候,也會上述的附加資訊。對於判斷是哪一種object的時候,我們需要用到instanceof這個操作符來判斷,我們後面會說到。

說一說typeof的原理吧,說到這裡,我們應該考慮一下,JavaScript是怎麼儲存資料的呢,又或者說,對於一個變數,它的資料型別權衡的標準是什麼呢?

查閱了相關的資料,其實這個是一個歷史遺留的bug,在 javascript 的最初版本中,使用的 32 位系統,為了效能考慮使用低位儲存了變數的型別資訊:

  • 000:物件
  • 010:浮點數
  • 100:字串
  • 110:布林
  • 1:整數

but,對於 undefinednull 來說,這兩個值的資訊儲存是有點特殊的。

null:對應機器碼的 NULL 指標,一般是全零。

undefined:用 −2^30 整數來表示!

所以,typeof 在判斷 null 的時候就出現問題了,由於 null 的所有機器碼均為0,因此直接被當做了物件來看待。

所以,似乎懂了一點皮毛了(●'◡'●)


instanceof

instanceof 運算子用於檢測建構函式的 prototype 屬性是否出現在某個例項物件的原型鏈上。

語法

object instanceof constructor
object 某個例項物件
construtor 某個建構函式
複製程式碼
// 定義建構函式
function C(){} 
function D(){} 

var o = new C();


o instanceof C; // true,因為 Object.getPrototypeOf(o) === C.prototype


o instanceof D; // false,因為 D.prototype 不在 o 的原型鏈上

o instanceof Object; // true,因為 Object.prototype.isPrototypeOf(o) 返回 true
C.prototype instanceof Object // true,同上

C.prototype = {};
var o2 = new C();

o2 instanceof C; // true

o instanceof C; // false,C.prototype 指向了一個空物件,這個空物件不在 o 的原型鏈上.

D.prototype = new C(); // 繼承
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 因為 C.prototype 現在在 o3 的原型鏈上
複製程式碼

需要注意的是,如果表示式 obj instanceof Foo 返回 true,則並不意味著該表示式會永遠返回 true,因為 Foo.prototype 屬性的值有可能會改變,改變之後的值很有可能不存在於 obj 的原型鏈上,這時原表示式的值就會成為 false。另外一種情況下,原表示式的值也會改變,就是改變物件 obj 的原型鏈的情況,雖然在目前的ES規範中,我們只能讀取物件的原型而不能改變它,但藉助於非標準的 __proto__ 偽屬性,是可以實現的。比如執行 obj.__proto__ = {} 之後,obj instanceof Foo 就會返回 false 了。

原理淺析

要想理解instanceof原理的話,我們需要從兩個方面去了解:

  • 語言規範中是如何定義運算子的
  • JavaScript原型繼承機制

這裡,我直接將規範定義翻譯為 JavaScript 程式碼如下:

function new_instance_of(leftVaule,rightVaule) {
    let rightProto = rightVaule.prototype,leftVaule = leftVaule.__proto__;
    while (true) {
        if (leftVaule === null) {
            return false;
        }
        if (leftVaule === rightProto) {
            return true;
        }
        leftVaule = leftVaule.__proto__
    }
}
複製程式碼

從上面的程式碼看得出來,instanceof主要的原理就是:

只要右邊的prototype在左邊的原型鏈上及可,也就是返回true。因此,instanceof在查詢的過程中會遍歷左邊變數的原型鏈,直到找到右邊變數的prototype,如果查詢失敗的話,返回false,告訴我們左邊的變數並非是右邊變數的例項。

接下來我們看一看有趣的例子:

function Foo() {}
        console.log(Object instanceof Object)
        console.log(Function instanceof Function)
        console.log(Function instanceof Object)
        console.log(Foo instanceof Object)
        console.log(Foo instanceof Function)
        console.log(Foo instanceof Foo)
複製程式碼

JavaScript 的原型繼承原理

關於原型繼承的原理,我簡單用一張圖來表示

這個圖很重要,對於原型鏈不理解的,可以看看這篇文章:

淺談JavaScript原型

舉個例子分析一個有趣的instanceof例子

Object instanceof Object

由圖可知,Object 的 prototype 屬性是 Object.prototype,而由於 Object 本身是一個函式,由 Function 所建立,所以 Object.__proto__ 的值是 Function.prototype,而 Function.prototype 的 __proto__ 屬性是 Object.prototype,所以我們可以判斷出,Object instanceof Object 的結果是 true 。用程式碼簡單的表示一下
複製程式碼
leftValue = Object.__proto__ = Function.prototype;
rightValue = Object.prototype;
// 第一次判斷
leftValue != rightValue
leftValue = Function.prototype.__proto__ = Object.prototype
// 第二次判斷
leftValue === rightValue
// 返回 true
複製程式碼

剩下的Function instanceof Object等有趣的例子可以自己手動去實現一下?


Object.prototype.toString

ES5 規範中的描述

可以知道,Object.prototype.toString 最終會返回形式如 [object,class] 的字串,class 指代的是其檢測出的資料型別,這個是我們判斷資料型別的關鍵。

var toString=Object.prototype.toString;

console.log(toString.call(und));  // [object Undefined]
console.log(toString.call(nul));  // [object Null]
console.log(toString.call(boo));  // [object Boolean]
console.log(toString.call(num));  // [object Number]
console.log(toString.call(str));  // [object String]
console.log(toString.call(obj));  // [object Object]
console.log(toString.call(arr));  // [object Array]
console.log(toString.call(fun));  // [object Function]
console.log(toString.call(date));  // [object Date]
console.log(toString.call(reg));  // [object RegExp]
console.log(toString.call(err));  // [object Error]
console.log(toString.call(arg));  // [object Arguments]
複製程式碼

資料型別檢測終極方法

/**
 * @desc 資料型別檢測
 * @param obj 待檢測的資料
 * @return {String} 型別字串
 */
 let type = (obj) => typeof obj !== 'object' ? typeof obj : Object.prototype.toString.call(obj).slice(8,-1).toLowerCase();

複製程式碼

資料型別的單獨檢測

/**
 * @desc 是否是 Undefined 型別檢測
 * @param obj 待檢測的資料
 * @return {Boolean} 布林值
 */
let isUndefined = obj => obj === void 0
/**
 * @desc 是否是 Null 型別檢測
 * @param obj 待檢測的資料
 * @return {Boolean} 布林值
 */
let isNull = obj => obj === Null
/**
 * @desc 是否是 Boolean 型別檢測
 * @param obj 待檢測的資料
 * @return {Boolean} 布林值
 */
let isBoolean = obj => typeof(obj) === 'boolean'
/**
 * @desc 是否是 Number 型別檢測
 * @param obj 待檢測的資料
 * @return {Boolean} 布林值
 */
let isNumber = obj => typeof(obj) === 'number'
/**
 * @desc 是否是 String 型別檢測
 * @param obj 待檢測的資料
 * @return {Boolean} 布林值
 */
let isString = obj => typeof(obj) === 'string'
/**
 * @desc 是否是 Object 型別檢測
 * @param obj 待檢測的資料
 * @return {Boolean} 布林值
 */
let isObject = obj => Object.prototype.toString.call(obj) === '[object Object]'
/**
 * @desc 是否是 Array 型別檢測
 * @param obj 待檢測的資料
 * @return {Boolean} 布林值
 */
let isArray = obj => Object.prototype.toString.call(obj) === '[object Array]'
/**
 * @desc 是否是 Function 型別檢測
 * @param obj 待檢測的資料
 * @return {Boolean} 布林值
 */
let isFunction = obj => typeof obj === 'function'
/**
 * @desc 是否是 Date 型別檢測
 * @param obj 待檢測的資料
 * @return {Boolean} 布林值
 */
let isDate = obj => Object.prototype.toString.call(obj) === '[object Date]'
/**
 * @desc 是否是 RegExp 型別檢測
 * @param obj 待檢測的資料
 * @return {Boolean} 布林值
 */
let isRegExp = obj => Object.prototype.toString.call(obj) === '[object RegExp]'
/**
 * @desc 是否是 Error 型別檢測
 * @param obj 待檢測的資料
 * @return {Boolean} 布林值
 */
let isError = obj => Object.prototype.toString.call(obj) === '[object Error]'
/**
 * @desc 是否是 Arguments 型別檢測
 * @param obj 待檢測的資料
 * @return {Boolean} 布林值
 */
let isArguments = obj => Object.prototype.toString.call(obj) === '[object Arguments]'

複製程式碼

結論

  • 使用 typeof 來判斷基本資料型別是 ok 的,需要注意的是typeof判斷null型別時的問題
  • 判斷一個物件的話具體考慮用instanceof,但是instanceof判斷一個數組的時候,它可以被instanceof判斷為Obeject
  • 比較準確的的判斷物件例項的型別,採取Object.prototype.toString.call()方法

參考

v8引擎是如何知道js資料型別的?

typeof的原理?

JavaScript中typeof詳解

JavaScript 資料型別檢測終極解決方案