深入JavaScript類型判定
JavaScript的數據類型
基本區分方法
ECMAScript標準定義了7種數據類型
6 種 基本類型:
Boolean,兩種取值:true和false
Null,一種取值:null
Undefined,一種取值:undefined
Number,JS的數值為基於 IEEE 754 標準的雙精度 64 位二進制格式的值(-(263 -1) 到 263 -1)。
String,JavaScript的字符串類型用於表示文本數據。它是一組16位的無符號整數值的“元素”。它不可改變。
Symbol,符號是唯一的並且是不可修改的, 並且也可以用來作為Object的key。
一種復雜類型:
Object,可以認為Object是一種鍵值對的集合。Array和Function就是Object的子類型。
另一種區分方法:值和址
從C語言過來的朋友一定常聽到傳值和傳址這樣的說法,C語言裏有指針的概念,指針本質上是一個內存地址,程序員可以通過指針來修改某些內容。
在java和JavaScript這樣的類C語言中雖然沒有了指針這麽強大卻危險的東西,但是在函數中操作變量的方式卻一脈而成。
舉個例子:
var
globalA = 1
;
function foo (a) {
a = 2;
}
foo(globalA);
console.log(globalA);//1
在函數中,如果入參是值類型,那麽函數將會在執行時上下文建立一個副本,在函數中,實際修改的這個副本,不會影響真正的原始入參。
如果入參是值類型,也就是我們常說的引用類型,那麽將直接操作所引用的對象,也就是所說的,通過地址操作值。
var
obj = {a:1}
;
function foo2 (o) {
o.a = 2;
}
ArrFoo(obj);
console.log(obj);//{a:2}
此處需要註意的一點是,函數的形參o雖然是引用類型,但是它也是一個執行上下文中建立的副本,如果直接將它重新賦值,例如
o = {a:2};
這種寫法是不能影響到原始的入參的,執行完畢以後,原始的obj不會被修改。
上面提到的是我們不論在C還是Java中也會涉及的一些傳址傳值的基本概念。
但是在JavaScript中有一些特別的地方。
每個函數都可能是構造函數
值和址一般是在入參裏做體現,出參方面,按照正常理解即可。由於JavaScript的原型鏈特點,每個函數都可能是構造函數。在構造函數中,情況稍有不同。
構造函數中帶有return語句,如果return的是:
值類型,那麽構造函數會忽略掉這個值,返回構造的新對象;
引用類型(數組、函數、對象),那麽構造函數就會直接返回該引用類型;
// 因為Super1返回的 123 是值類型,它被丟棄,直接返回構造對象
function Super1(a){
this.a=a;
return 123; // 將return語句註釋掉也沒影響
}
Super1.prototype.sayHello=function(){
console.log("Hello")
}
console.log(new Super1(1));
// Super2直接返回了對象,而前面構造函數的所有操作全被丟棄
// 包括它的構造函數、原型鏈全部都沒有返回
function Super2(a){
this.a=a;
return {a:2};
}
Super2.prototype.sayHello=function(){
console.log("Hello")
}
console.log(new Super2(3));
換言之,如果是return的是值類型,return則沒什麽作用;如果是引用類型,則對構造對象的任何操作不生效,直接返回原來的引用對象。
不靠譜的typeof
其實js有一個自帶的操作符專門用來進行類型判定———typeof,不過它只能判斷幾種基本類型。對於復雜類型,它有些無能無力,而且,對於一些基本類型,它還有一些陷阱。
類型 | 結果
- | -:
Undefined | "undefined"
Null | "object"
Boolean | "boolean"
Number | "number"
String | "string"
Symbol | "symbol"
function | "function"
任何其他對象(Array,Date等原生對象) | "object"
typeof有一些古怪的bug
即使是在typeof可以判定的地方,也會有一些bug。例如:
最經典的
typeof null === ‘object‘ // true
以及new操作符
typeof Number(1) === ‘number‘ // true
typeof new Number(1) === ‘object‘ // true
由於typeof的功能簡陋,尤其是無法對object下的子類型做出詳細的判定,所以我們常用另一個操作符進行判定。
稍微靠譜的instanceof
instanceof 用來測試一個對象在其原型鏈中是否存在一個構造函數的 prototype 屬性。
大概原理就是不停地去判斷當前對象 _ _ proto _ _ 上的對象是否與實例的prototype相等。不相等的話,就令當前對象
a. _ _ proto _ _ = a. _ _ proto _ _ . _ _ proto _ _ 繼續判斷。直到結果為null和true為止。
舉個栗子:
// 構造函數
function Foo(){}
var
foo1 = new Foo()
;
foo1.constructor === Foo; //true
foo1.__proto__.constructor === Foo; // true;
foo1.__proto__ === Foo.prototype; // true;
foo1.__proto__.__proto__ === Object.prototype; // true
foo1.__proto__.__proto__.__proto__ === null; // true
上面的代碼展示了instanceof的判斷原理,具體實現可以參考規範:
- https://tc39.github.io/ecma262/#sec-instanceofoperator
- https://tc39.github.io/ecma262/#sec-ordinaryhasinstance
根據instanceof,我們基本上可以判斷Object下的所有對象類型了。但是instanceof也有一個問題。
一切都在window下
在瀏覽器環境裏,所有的構造函數,基本的對象,都掛在window下。Object也不例外。這導致了一個問題,在iframe這樣的獨立於當前窗口的環境裏,instanceof可能會有bug產生。
對於Array這樣的常用對象,新版本提供了Array.isArray原生方法進行判定,但是對於Date等對象,就沒有這麽好了。所以我們還是需要一個更加健壯的類型判定方法。
Prototype.js、underScore.js和jQuery的前輩們為我們找到了一個絕妙的方法——
Object.prototype.toString.call
通過這個技巧,再結合typeof和instanceof,我們可以判斷幾乎全部的原生對象類型。
下面是underscore的部分源碼:
_.each([‘Arguments‘, ‘Function‘, ‘String‘, ‘Number‘, ‘Date‘,
‘RegExp‘, ‘Error‘, ‘Symbol‘, ‘Map‘, ‘WeakMap‘, ‘Set‘, ‘WeakSet‘], function(name) {
_[‘is‘ + name] = function(obj) {
return toString.call(obj) === ‘[object ‘ + name + ‘]‘;
};
});
感謝閱讀。
REFER
- typeof
- instanceof
- underscore.js
深入JavaScript類型判定