深入探究JavaScript的類型判斷(從基礎到精通)
JavaScript 語言具有多種數據類型,它們可以大致分為兩大類:基本數據類型(Primitive Data Types)和引用數據類型(Reference Data Types)。
一、數據類型
基本數據類型(Primitive Data Types) 包括:
- Undefined: 表示變量已被聲明但未被初始化時的值。
- Null: 代表一個刻意的空值或缺失的值。
- Boolean: 只有兩個值,
true
或false
。 - Number: 用于表示整數和浮點數,包括Infinity、-Infinity和NaN。
- String: 用于表示文本,由零個或多個字符組成。
- Symbol: ES6 引入的新類型,表示獨一無二的、不可變的數據類型,主要用于對象的屬性鍵。
- BigInt: ES10 引入,用于表示任意大小的整數。
引用數據類型(Reference Data Types) 包括:
Object: 一種復雜數據結構,可以包含多個鍵值對,包括但不限于普通對象、數組、函數等。
Array: 特殊類型的對象,用于存儲有序的元素集合。
Function: 在JavaScript中,函數也是對象,可以作為值傳遞,擁有方法和屬性
上面的數據類型如果說有不熟悉的,那一般是Symbol和BigInt,下面我簡要說一下:
- Symbol:
最重要的特征就是唯一性,例如:
let a = Symbol(1) let b = Symbol(1) console.log(a === b) // false
用Symbol() 返回的東西,具有唯一性。
- BigInt:
Number
類型的安全整數范圍(-2^53 到 2^53),超出這個范圍的數進行計算會出現精度丟失的問題,算不準確,于是就出現了BigInt.
特點
- 創(chuàng)建: BigInt 可以通過在整數末尾添加
n
來創(chuàng)建,例如123n
。你也可以使用BigInt()
函數將字符串轉換為 BigInt,如BigInt("123")
。 - 運算: BigInt 和 Number 類型在進行算術運算時需要特別注意類型匹配。兩個 BigInt 類型可以直接進行加減乘除等運算,但 BigInt 和 Number 直接運算會導致錯誤,需要先將 Number 轉換為 BigInt。
- 比較: BigInt 和 Number 之間可以進行寬松的相等性比較(==),但嚴格相等性比較(===)會因為類型不同而返回 false。嚴格比較時,需要確保類型一致。
- 不支持: BigInt 不支持一元運算符
++
和--
,也不適用于Math對象的方法,以及不能用于某些JavaScript原生對象的屬性,比如數組的長度。 - 字符串轉換: BigInt 轉換為字符串時,會保持其完整的數值,不會發(fā)生精度丟失。
示例
// 創(chuàng)建 BigInt const largeNum = 1234567890123456789012345678901234567890n; // 運算 const anotherLargeNum = 9876543210987654321098765432109876543210n; const sum = largeNum + anotherLargeNum; // 比較 console.log(largeNum === BigInt('1234567890123456789012345678901234567890')); // true console.log(largeNum == 1234567890123456789012345678901234567890); // true, 松散比較 console.log(largeNum === 1234567890123456789012345678901234567890); // false, 嚴格比較類型不同 // 字符串轉換 console.log(largeNum.toString()); // '1234567890123456789012345678901234567890'
二、類型判斷時會產生的疑問
1. typeof()類型判斷
console.log(typeof (null));//object console.log(typeof (undefined));//undefined console.log(typeof (true));//boolean console.log(typeof (20));//number console.log(typeof ("abc"));//string console.log(typeof (Symbol()));//symbol console.log(typeof (34n));//bigint console.log(typeof ([]));//object console.log(typeof ({}));//object console.log(typeof (function () { }));//function
我們看上面的代碼會產生兩個疑問:
為什么對null的類型判斷為object?
這個行為實際上是JavaScript設計初期的一個決策,后來成為了語言的一部分,被視為一個歷史遺留問題。在JavaScript的最初設計中,類型信息是通過值的內部表示來區(qū)分的,特別是通過值的頭部比特位。對于當時的實現來說,null
的內部二進制表示是全零,這與對象類型在內存中的某些標記模式相吻合(判斷其二進制前三位是否為0,是則為object
,否則為原始類型),尤其是當引擎檢查值的頭部比特以快速區(qū)分基本類型和引用類型時,全零可能被錯誤地解釋為了一個空對象的標記。至于后來為什么不改,則是因為大量的企業(yè)已經用JavaScript寫了大量的項目,改動后,全部項目都會報錯,基于這種考慮就沒動。因此在以后判斷類型是否為object
時,需要將null
排除。
為什么單獨function判斷類型為function,而非object?
在JavaScript中,函數(Function)是一種特殊的對象,這意味著它本質上繼承了對象的特性,可以擁有屬性和方法。然而,出于對語言設計和實用性考慮,typeof
操作符特意將函數類型區(qū)分對待,當應用于函數時,它返回的是"function"
而不是"object"
。
2. instanceof類型判斷
在JavaScript中,instanceof
是一個操作符,用于檢測構造函數的prototype
屬性是否出現在某個實例對象的原型鏈上。它的使用方式與Java中的instanceof
相似,但概念上更符合JavaScript的原型繼承模型。其基本語法如下:
object instanceof Constructor
object
:需要檢查的對象。Constructor
:一個構造函數或者函數對象。
如果object
是通過Constructor
或其任意父類(通過原型鏈)構造的,那么instanceof
操作符返回true
;否則,返回false
。
例如:
function Animal() {} function Dog() {} Dog.prototype = new Animal(); let myDog = new Dog(); console.log(myDog instanceof Dog); // 輸出: true console.log(myDog instanceof Animal); // 輸出: true console.log(myDog instanceof Object); // 輸出: true,因為所有對象都最終繼承自Object
在這個例子中,myDog
對象是通過Dog
構造函數創(chuàng)建的,而Dog
的原型鏈上包含了Animal
,因此myDog
既是Dog
的實例,也是Animal
的實例。同時,由于JavaScript中所有對象都繼承自Object
,所以myDog
也是Object
的實例。
原始類型(如string
、number
、boolean
、null
、undefined
、symbol
、bigint
)不是對象,因此不能直接使用instanceof
來判斷這些類型。如果你嘗試對原始類型值使用instanceof
,它們會被臨時轉換為對應的包裝對象(如new String()
、new Number()
、new Boolean()
),然后再進行檢查,但這通常不是你想要的行為,并且對于null
和undefined
這樣的值,這樣做會直接導致錯誤。
例如:
let str = "some text"; console.log(str instanceof String); // 可能意外地輸出: false,因為字符串不是String對象的實例 // 實際上,"some text" 在進行 instanceof 檢查前會被轉換為 String("some text"),但這是臨時的包裝對象,檢查后即被銷毀。 let num = 2; console.log(num instanceof Number); // 同樣可能輸出: false let bool = true; console.log(bool instanceof Boolean); // 輸出: false console.log(null instanceof Object); // 拋出 TypeError: null is not an object (evaluating 'null instanceof Object') console.log(undefined instanceof Object); // 拋出 TypeError: undefined is not an object (evaluating 'undefined instanceof Object')
面試題補充:請寫出instanceof的判斷原理。
如果面試官出這種題,那對你算是非常溫柔了。
function myinstanceof(object, constructor) { // 當對象不為null時進入循環(huán),因為null沒有__proto__ while (object !== null) { // 如果對象的原型等于構造函數的prototype屬性,說明該對象是構造函數的實例 if (object.__proto__ === constructor.prototype) { return true; } else { // 如果當前對象不是實例,繼續(xù)向上查找其原型鏈 object = object.__proto__; } } // 遍歷完原型鏈都沒有找到匹配,說明不是該構造函數的實例 return false; } console.log(myinstanceof({}, Object)); // true console.log(myinstanceof({}, Array)); // false
3. Object.prototype.toString.call( )
Object.prototype.toString.call()
是JavaScript中一個強大的方法,用于獲取任何值的類型信息。這個方法能夠返回一個表示該值的字符串,這個字符串格式通常為"[object Type]"
,其中Type
是JavaScript中的類型名稱。相比于前兩種判斷存在的瑕疵和不準確,這種更為完美和準確。
console.log(Object.prototype.toString.call({})); // "[object Object]" console.log(Object.prototype.toString.call([])); // "[object Array]" console.log(Object.prototype.toString.call(new Date)); // "[object Date]" console.log(Object.prototype.toString.call(null)); // "[object Null]" console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]" ...... ...... ......
在面試時,很多大廠面試官會為難你,問:有什么辦法可以判斷類型?
你回答:Object.prototype.toString.call( )
,
他又問你為什么這個方法可以判斷類型?
要回答好這個問題,就需要我們對Object.prototype.toString.call( )
有著更為深入的理解:
這個方法要分兩部分理解:Object.prototype.toString
和call()
Object.prototype.toString
先來看官方文檔上寫的Object.prototype.toString
翻譯:
調用tostring方法時,會執(zhí)行以下步驟:
1.如果該值未定義,則返回“[object Undefined]”
2.如果this
值為null
,則返回“[object Null]”
3.讓o
成為調用To Obiect
的結果,將this
值作為參數傳遞。(將 o
作為 To Object(this)
的執(zhí)行結果)
4.定義class
為o
的內部屬性 [[Class]]
的值
5.返回String
值,該值是由三個String“[object”
、class
和 “]”
連接起來的結果。
你可以將以上步驟理解為以下步驟:
1.檢查值是否為undefined
:如果調用toString
的方法的對象是undefined
,則返回"[object Undefined]"
。
2.檢查值是否為null
:如果該值是null
,則返回"[object Null]"
。
3.轉換為對象(To Object
操作):對于非null
和非undefined
的值,首先通過抽象操作To Object
將其轉換為對象(如果還不是對象)。這意味著原始值(如數字、字符串等)會先轉換為它們的包裝對象,然后繼續(xù)后續(xù)步驟。
4.獲取內部屬性[[Class]]
:獲取轉換后的對象的內部屬性[[Class]]
的值。這個屬性由JavaScript引擎維護,代表了對象的類型信息,比如"Array"
、"Date"
、"Object"
等。
5.構造并返回結果字符串:最后,將字符串"[object "
, class
的值,以及"]"
拼接起來,形成并返回最終的類型字符串,如"[object Array]"
、"[object Date]"
等。
然而用Object.prototype.toString()
遠遠不足以有效的判斷類型,盡管它很強大:
Object.prototype.toString()
判斷類型時,通常不會按照預期工作,尤其是當直接在原始值(如字符串、數字、布爾)上嘗試時,因為這樣調用的this
并沒有綁定到你想要檢查的對象上。
其原因在于第三個步驟To Obiect
,官方文檔對此如下描述:
它會new
一個對象,而不是單純的字面量,此時其this
指向發(fā)生改變,其內部屬性[[class]]
為指向對象的內部屬性object
。
因此,需要call()的幫助改變其this指向:
- call
不了解call的,我簡單舉個例子來說明一下效果:
var object = { a: 11 } function foo() { console.log(this.a) } foo.call(obj) // 11
想通過func函數輸出1,可以通過call方法將func里的this指向object。
我們可以嘗試模擬call
方法的行為,寫如下代碼:
var object = { a: 11 }; function foo() { console.log(this.a); } Function.prototype.mycall = function(context) { // 檢查調用mycall的是否為一個函數 if (typeof this !== 'function') { throw new TypeError(this + ' is not a function'); } // 使用Symbol來避免屬性名沖突 const fn = Symbol('key'); // 將當前函數(this指向的func)賦值給context的一個唯一屬性 context[fn] = this; // 調用這個新添加的函數,此時this會被隱式綁定到context上 context[fn](); // 刪除臨時添加的屬性,以清理環(huán)境 delete context[fn]; }; foo.mycall(object); // 輸出: 11
核心原理可以概括為: call
方法通過在指定的context
對象上臨時引用并調用目標函數,實現了對該函數內部this
的隱式綁定,從而使得函數能夠在預期的上下文中執(zhí)行。
具體來說:
- 臨時綁定:它本質上是在
context
對象上創(chuàng)建一個屬性(通常使用一個不易沖突的屬性名,如使用Symbol),并將目標函數賦值給這個屬性。(Symbol作用在于,防止別人調用你寫的方法時,用同名的變量名) - 調用函數:接著,通過
context
上的這個屬性間接調用目標函數。由于是通過對象屬性的方式來調用的,JavaScript的函數調用規(guī)則決定了此時函數內的this
將綁定到該對象(即context
)上。 - 清理:為了不污染
context
對象,調用結束后通常還會刪除之前添加的臨時屬性,即清除本來就不存在context
里的屬性。
看完這兩部分的解釋,再來做一個總結:
使用Object.prototype.toString.call()
時,當參數為Boolean
,Number
和String
類型,call()
先將這個原始值轉換為其對應的包裝對象,即new String('11')
,然后再調用Object.prototype.toString
方法,當執(zhí)行到第三步to object
時,發(fā)現參數為對象,則將對象賦給變量o。在這個過程中this指向因為call的糾正作用沒有發(fā)生改變,因此,其內部屬性[[class]]
沒有發(fā)生改變。
加上了call,你可以理解為以下情況:
Object.prototype.toString.call('11'); // 輸出: "[object String]" Object.prototype.toString.call(new String('11')); // 輸出同樣為: "[object String]"
寫出完整步驟: 當執(zhí)行Object.prototype.toString.call('11')
時,其內部過程大致如下:
- 字符串字面量
'11'
作為call
的第一個參數,使得toString
方法內部的this
指向了一個臨時創(chuàng)建的String
對象(即new String('1')
)。 - 該方法檢查
this
不是null
或undefined
,繼續(xù)執(zhí)行。 - 將這個臨時的字符串對象視為操作對象
O
。 - 從
O
中獲取其內部屬性[[Class]]
,得到值"String"
。 - 組合并返回字符串
"[object String]"
,表示這是一個字符串類型的對象。
4.Array.isArray(x)
Array.isArray(x)
是JavaScript的一個內建函數,用于檢測x
是否為一個數組。這個方法提供了最直接和可靠的方式來判斷一個變量是否是數組類型,相比使用instanceof
或typeof
等方法更準確,因為它不會受到不同全局執(zhí)行環(huán)境(如iframe、Web Workers)中Array構造函數不同的影響。
使用示例:
let arr = [1, 2, 3]; let notArr = "I am not an array"; console.log(Array.isArray(arr)); // 輸出: true console.log(Array.isArray(notArr)); // 輸出: false
這個函數非常有用,尤其是在處理可能是多種類型輸入的動態(tài)數據時,能夠確保你正確地識別并處理數組類型的數據。
三、結語:
在JavaScript的世界里,準確無誤地判斷數據類型是編寫健壯、可維護代碼的基礎。本文從基礎出發(fā),系統(tǒng)梳理了JavaScript的各大數據類型,重點解析了在類型判斷時常見的疑惑與誤區(qū),尤其深入探討了typeof
、instanceof
以及Object.prototype.toString.call()
這三種類型判斷方法的原理與實踐,最后還提到了Array.isArray()
這一專門用于數組類型判斷的便捷工具。
通過本篇內容的學習,你不僅掌握了每種判斷方法的適用場景與限制,還理解了如何利用Object.prototype.toString.call()
這一終極武器來實現精確無誤的類型識別。記住,每種方法都有其獨特的價值和潛在的陷阱,合理選擇才能在實戰(zhàn)中游刃有余。
以上就是深入探究JavaScript的類型判斷(從基礎到精通)的詳細內容,更多關于JavaScript類型判斷的資料請關注腳本之家其它相關文章!
相關文章
JavaScript中Hoisting詳解 (變量提升與函數聲明提升)
函數聲明和變量聲明總是被JavaScript解釋器隱式地提升(hoist)到包含他們的作用域的最頂端。下面這篇文章主要給大家介紹了關于JavaScript中Hoisting(變量提升與函數聲明提升)的相關資料,需要的朋友可以參考借鑒,下面來一起看看吧。2017-08-08