JS 系列四:深刻剖析 instanceof 運算符

引言

JS系列暫定 27 篇,從基礎,到原型,到異步,到設計模式,到架構模式等,javascript

本篇是JS系列中第 4 篇,文章主講 JS instanceof ,包括 instanceof 做用、內部實現機制,以及 instanceoftypeofSymbol.hasInstanceisPrototypeObject.prototype.toString[[Class]] 等的對比使用 ,深刻了解 JS instanceofhtml

1、instanceof

1. 引入 instanceof

在 JS 中,判斷一個變量的類型,經常會用到 typeof 運算符,但當用 typeof 來判斷引用類型變量時,不管是什麼類型的變量,它都會返回 Object前端

// 基本類型
console.log(typeof 100); // number
console.log(typeof 'bottle'); // string
console.log(typeof true); // boolean

// 引用類型
console.log(typeof {}); // object
console.log(typeof [1, 2, 3]); // object
複製代碼

爲此,引入了instanceofjava

instanceof 操做符用於檢測對象是否屬於某個 class,同時,檢測過程當中也會將繼承關係考慮在內。git

// 類
class Bottle {}
// bottle 是 Bottle 類的實例對象
let bottle = new Bottle();
console.log(bottle instanceof Bottle); // true

// 也能夠是構造函數,而非 class
function AnGe() {}
let an = new AnGe();
console.log(an instanceof AnGe); // true
複製代碼

2. instanceof 與 typeof

instanceoftypeof 相比,instanceof 方法要求開發者明確的確認對象爲某特定類型。即 instanceof 用於判斷引用類型屬於哪一個構造函數的方法。github

var arr = []
arr instanceof Array // true
typeof arr // "object"
// typeof 是沒法判斷類型是否爲數組的
複製代碼

3. instanceof 在繼承關係中

另外,更重的一點是 instanceof 能夠在繼承關係中用來判斷一個實例是否屬於它的父類型。web

// 判斷 f 是不是 Foo 類的實例 , 而且是不是其父類型的實例
function Aoo(){} 
function Foo(){} 
//JavaScript 原型繼承
Foo.prototype = new Aoo();
 
var foo = new Foo(); 
console.log(foo instanceof Foo) // true 
console.log(foo instanceof Aoo) // true
複製代碼

f instanceof Foo 的判斷邏輯是:windows

  • f 的 __proto__一層一層往上,是否對應到 Foo.prototype
  • 再往上,看是否對應着Aoo.prototype
  • 再試着判斷 f instanceof Object

instanceof 能夠用於判斷多層繼承關係。設計模式

下面看一組複雜例子數組

console.log(Object instanceof Object) //true 
console.log(Function instanceof Function) //true 
console.log(Number instanceof Number) //false 
console.log(String instanceof String) //false 
console.log(Array instanceof Array) // false
 
console.log(Function instanceof Object) //true 
 
console.log(Foo instanceof Function) //true 
console.log(Foo instanceof Foo) //false
複製代碼

在這組數據中,Object、Function instanceof 本身均爲 true, 其餘的 instanceof 本身都爲 false,這就要從 instanceof 的內部實現機制以及 JS 原型繼承機制講起。

2、instanceof 的內部實現機制

instanceof 的內部實現機制是:經過判斷對象的原型鏈上是否能找到對象的 prototype,來肯定 instanceof 返回值

1. 內部實現

// instanceof 的內部實現 
function instance_of(L, R) {//L 表左表達式,R 表示右表達式,即L爲變量,R爲類型
// 取 R 的顯示原型
var prototype = R.prototype
// 取 L 的隱式原型
L = L.__proto__
// 判斷對象(L)的類型是否嚴格等於類型(R)的顯式原型
while (true) { 
 if (L === null) {
   return false
 }
   
 // 這裏重點:當 prototype 嚴格等於 L 時,返回 true
 if (prototype === L) {
   return true
 } 
 
 L = L.__proto__
} 
}
複製代碼

instanceof 運算符用來檢測 constructor.prototype 是否存在於參數 object 的原型鏈上。

2. 你真的瞭解 instanceof 了嗎

看下面一個例子,instanceof 爲何會返回 true?很顯然,an 並非經過 Bottle() 建立的。

function An() {}
function Bottle() {}
An.prototype = Bottle.prototype = {};

let an = new An();
console.log(an instanceof Bottle); // true
複製代碼

這是由於 instanceof 關心的並非構造函數,而是原型鏈。

an.__proto__ === An.prototype; // true
An.prototype === Bottle.prototype; // true
// 即
an.__proto__ === Bottle.prototype; // true
複製代碼

即有 an.__proto__ === Bottle.prototype 成立,因此 an instanceof Bottle 返回了 true

因此,按照 instanceof 的邏輯,真正決定類型的是 prototype,而不是構造函數。

3、 JS 原型鏈繼承關係

原型鏈

圖片來自於 JS原型鏈

由其本文涉及顯示原型 prototype 和隱式原型 __proto__ ,因此下面對這兩個概念做一下簡單說明。

在 JavaScript 原型繼承結構裏面,規範中用 [Prototype]] 表示對象隱式的原型,在 JavaScript 中用 __proto__ 表示,而且在 Firefox 和 Chrome 瀏覽器中是能夠訪問獲得這個屬性的,可是 IE 下不行。全部 JavaScript 對象都有 __proto__ 屬性,但只有 Object.prototype.__proto__ 爲 null,前提是沒有在 Firefox 或者 Chrome 下修改過這個屬性。這個屬性指向它的原型對象。 至於顯示的原型,在 JavaScript 裏用 prototype 屬性表示,這個是 JavaScript 原型繼承的基礎知識,若是想進一步瞭解,請參考 JS 基礎之: 深刻 constructor、prototype、__proto__、[[Prototype]] 及 原型鏈

下面介紹幾個例子(及其推演過程),加深你的理解:

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
複製代碼

4、 instanceof 與 Symbol.hasInstance

Symbol.hasInstance 用於判斷某對象是否爲某構造器的實例。所以你能夠用它自定義 instanceof 操做符在某個類上的行爲。

你可實現一個自定義的instanceof 行爲,例如:

class MyArray {  
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance);
  }
}
console.log([] instanceof MyArray); // true
複製代碼

5、 instanceof 與 isPrototypeOf

isPrototypeOf 也是用來判斷一個對象是否存在與另外一個對象的原型鏈上。

// 判斷 f 是不是 Foo 類的實例 , 
// 而且是不是其父類型的實例
function Aoo(){} 
function Foo(){} 
// JavaScript 原型繼承
Foo.prototype = new Aoo();
 
var foo = new Foo(); 
console.log(Foo.prototype.isPrototypeOf(foo)) //true 
console.log(Aoo.prototype.isPrototypeOf(foo)) //true
複製代碼

須要注意的是:

  • instanceoffoo 的原型鏈是針對 Foo.prototype 進行檢查的
  • isPrototypeOffoo 的原型鏈是針對 Foo 自己

6、 instanceof 和多全局對象(多個 frame 或多個 window 之間的交互)

instanceof 在多個全局做用域下,判斷會有問題,例如:

// parent.html
<iframe src="child.html" onload="test()">
</iframe>
<script>
  function test(){
    var value = window.frames[0].v;
    console.log(value instanceof Array); // false
  }
</script>
複製代碼
// child.html
<script>
  window.name = 'child';
  var v = [];
</script>
複製代碼

嚴格上來講 value 就是數組,但 parent 頁面中打印輸出: false ;

這是由於 Array.prototype !== window.frames[0].Array.prototype ,而且數組從前者繼承。

出現問題主要是在瀏覽器中,當咱們的腳本開始開始處理多個 frame 或 windows 或在多個窗口之間進行交互。多個窗口意味着多個全局環境,不一樣的全局環境擁有不一樣的全局對象,從而擁有不一樣的內置類型構造函數。

解決方法

能夠經過使用

  • Array.isArray(myObj) 或者

  • Object.prototype.toString.call(myObj) === "[object Array]"

來安全的檢測傳過來的對象是不是一個數組

7、擴展:Object.prototype.toString 方法

默認狀況下(不覆蓋 toString 方法前提下),任何一個對象調用 Object 原生的 toString 方法都會返回 "[object type]",其中 type 是對象的類型;

let obj = {};

console.log(obj); // {}
console.log(obj.toString()); // "[object Object]"
複製代碼

1. [[Class]]

每一個實例都有一個 [[Class]] 屬性,這個屬性中就指定了上述字符串中的 type (構造函數名)。 [[Class]] 不能直接地被訪問,但一般能夠間接地經過在這個值上借用默認的 Object.prototype.toString.call(..) 方法調用來展現。

Object.prototype.toString.call("abc"); // "[object String]"
Object.prototype.toString.call(100); // "[object Number]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call([1,2,3]); // "[object Array]"
Object.prototype.toString.call(/\w/); // "[object RegExp]"
複製代碼

2. 使用 Object.prototype.toString.call(..) 檢測對象類型

能夠經過 Object.prototype.toString.call(..) 來獲取每一個對象的類型。

function isFunction(value) {
  return Object.prototype.toString.call(value) === "[object Function]"
}
function isDate(value) {
  return Object.prototype.toString.call(value) === "[object Date]"
}
function isRegExp(value) {
  return Object.prototype.toString.call(value) === "[object RegExp]"
}

isDate(new Date()); // true
isRegExp(/\w/); // true
isFunction(function(){}); //true
複製代碼

或者可寫爲:

function generator(type){
  return function(value){
    return Object.prototype.toString.call(value) === "[object "+ type +"]"
  }
}

let isFunction = generator('Function')
let isArray = generator('Array');
let isDate = generator('Date');
let isRegExp = generator('RegExp');

isArray([]));    // true
isDate(new Date()); // true
isRegExp(/\w/); // true
isFunction(function(){}); //true
複製代碼

3. Symbol.toStringTag

Object.prototype.toString 方法可使用 Symbol.toStringTag 這個特殊的對象屬性進行自定義輸出。

舉例說明:

let bottle = {
  [Symbol.toStringTag]: "Bottle"
};

console.log(Object.prototype.toString.call(bottle)); // [object Bottle]
複製代碼

大部分和環境相關的對象也有這個屬性。如下輸出可能因瀏覽器不一樣而異:

// 環境相關對象和類的 toStringTag:
console.log(window[Symbol.toStringTag]); // Window
console.log(XMLHttpRequest.prototype[Symbol.toStringTag]); // XMLHttpRequest

console.log(Object.prototype.toString.call(window)); // [object Window]
console.log(Object.prototype.toString.call(new XMLHttpRequest())); // [object XMLHttpRequest]
複製代碼

輸出結果和 Symbol.toStringTag(前提是這個屬性存在)同樣,只不過被包裹進了 [object ...] 裏。

因此,若是但願以字符串的形式獲取內置對象類型信息,而不只僅只是檢測類型的話,能夠用這個方法來替代 instanceof

8、總結

適用於 返回
typeof 基本數據類型 string
instanceof 任意對象 true/false
Object.prototype.toString 基本數據類型、內置對象以及包含 Symbol.toStringTag 屬性的對象 string

Object.prototype.toString 基本上就是一加強版 typeof

instanceof 在涉及多層類結構的場合中比較實用,這種狀況下須要將類的繼承關係考慮在內。

9、參考

JavaScript instanceof 運算符深刻剖析

類型檢測:"instanceof"

10、系列文章

想看更過系列文章,點擊前往 github 博客主頁

11、走在最後

1. ❤️玩得開心,不斷學習,並始終保持編碼。👨💻

2. 若有任何問題或更獨特的看法,歡迎評論或直接聯繫瓶子君(公衆號回覆 123 便可)!👀👇

3. 👇歡迎關注:前端瓶子君,每日更新!👇

前端瓶子君
相關文章
相關標籤/搜索