前言
js 中的類型檢測也是很重要的一部分缤弦,所以說這篇文章我們就來講一下怎么對 JavaScript 中的基本數(shù)據(jù)類型進(jìn)行檢測领迈。其實(shí)這也是在讀 Zepto 源碼中學(xué)習(xí)到的,所以閱讀源碼對我們的提升還是很有幫助的碍沐。本文基于參考了前輩們的文章之后個人理解此文寫的有不當(dāng)?shù)牡胤嚼晖保埜魑淮罄兄刚?/p>
其實(shí)常規(guī)方法主要有四種
typeof
instanceof
Object.prototype.toString
construcor
其實(shí)這四種方式歸根結(jié)底就是兩種思路:
- 根據(jù)數(shù)據(jù)類型判斷(1,2)
- 根據(jù)構(gòu)造函數(shù)判斷(3累提,4)
前置基礎(chǔ)
再看 Zepto 之前看了 慕課網(wǎng)一個老師的視頻尘喝,一共一個小時左右,開了快進(jìn)估計(jì)也就 45 分鐘左右斋陪。只是講了 Zepto 的架構(gòu)和設(shè)計(jì)朽褪,沒有詳細(xì)的將每一個方法,初看之前可以看一下无虚,對 Zepto 有一個大概的印象缔赠。
原型與原型鏈
其實(shí)這部分真的是老生常談的問題,但是每一次聽其他人都有新的收獲友题。真的是不想寫這部分嗤堰,但是自我感覺整體思路比較清晰,所以推薦大家閱讀一下度宦。
Zepto 整個的設(shè)計(jì)思想其實(shí)是基于 js 的原型鏈踢匣。關(guān)于原型鏈,這個老師講的比較清晰斗埂,需要記住三句話:
- 每一個函數(shù)符糊,都有一個 prototype 屬性。
- 所有通過函數(shù) new 出來的對象呛凶,這個對象都有一個
__proto__
指向這個函數(shù)的 prototype男娄。 - 當(dāng)你想要使用一個對象(或者一個數(shù)組)的某個功能時:如果該對象本身具有這個功能,則直接使用漾稀;如果該對象本身沒有這個功能模闲,則去
__proto__
中找。
什么是 prototype(顯示原型)
每一個函數(shù)在創(chuàng)建之后都會擁有一個名為 prototype 的屬性崭捍,這個屬性指向函數(shù)的原型對象尸折。通過Function.prototype.bind方法構(gòu)造出來的函數(shù)是個例外,它沒有prototype屬性殷蛇。
var fn = function() {}
console.log( fn.prototype );
通過下面這幅圖我們可以看出創(chuàng)建一個函數(shù)時实夹,都會有一個 prototype
屬性指向它的原型橄浓。而 fn.prototype
中有一個 constructor
屬性指向 fn 函數(shù)。
什么是 __proto__
(隱式原型)
JavaScript 中任意對象都有一個內(nèi)置屬性 __proto__
亮航,隱式原型指向創(chuàng)建這個對象的函數(shù)(constructor)的 prototype荸实。
Object.prototype
這個對象是個例外,它的 __proto__
值為 null
console.log( typeof Array ); // 'function'
console.log( Array.prototype );
數(shù)組構(gòu)造函數(shù) Array
也是一個函數(shù)缴淋,并且在 Array 的原型中除了指向 Array 的 constructor 之外還有其他的內(nèi)置對象准给。
__proto__
的指向
上面應(yīng)該都不難理解,主要是 __proto__
的指向重抖,這個問題是比較難理解的露氮,我們來看剛剛的定義,__proto__
指向創(chuàng)建這個對象的函數(shù)的顯式原型钟沛。創(chuàng)建函數(shù)一共有三種方式:
- 字面量方式
var person1 = {
name: 'cyl',
sex: 'male'
};
字面量的方式是一種為了開發(fā)人員更方便創(chuàng)建對象的一個語法糖畔规,本質(zhì)就是
var o = new Object();
o.xx = xx;
o.yy=yy;
所以說使用字面量方式創(chuàng)建的函對象的 __proto__
屬性是指向 Object.prototype
的。
- 構(gòu)造函數(shù)
所謂的構(gòu)造函數(shù)讹剔,就是通過new
關(guān)鍵字調(diào)用的函數(shù)油讯,只要是通過new
關(guān)鍵字調(diào)用的函數(shù)都是構(gòu)造函數(shù)。由構(gòu)造函數(shù)構(gòu)造的對象延欠,其__prototype__
指向其構(gòu)造函數(shù)的 prototype 屬性指向的對象陌兑。
var arr = new Array()
比如 arr
是一個實(shí)例化的數(shù)組,那么 arr 的 __proto__
屬性就指向 Array 的 prototype 屬性由捎。
- 函數(shù)通過
Object.create
構(gòu)造的
var person1 = {
name: 'cyl',
sex: 'male'
};
var person2 = Object.create(person1);
Object.create
的內(nèi)部其實(shí)是這樣的:
function object(o){
function F(){}
F.prototype = o;
return new F()
}
也可以看成是通過 new
創(chuàng)建的兔综。所以說我們就可以一目了然,person2 的 __proto__
是指向 person1 的狞玛。(注意软驰,是直接指向 person1,而不是 person1.prototype
)心肪。
prototype
和 __proto__
的作用
在了解了什么是顯示原型 prototype
和隱式原型 __proto__
之后锭亏,我們也知道了怎么去找隱式原型,那么它們有什么作用呢硬鞍?
- 顯式原型的作用:用來實(shí)現(xiàn)基于原型的繼承與屬性的共享慧瘤。
- 隱式原型的作用:構(gòu)成原型鏈,同樣用于實(shí)現(xiàn)基于原型的繼承固该。舉個例子锅减,當(dāng)我們訪問 obj 這個對象中的 x 屬性時,如果在 obj 中找不到伐坏,那么就會沿著
__proto__
依次查找怔匣。
這里我們要注意了,當(dāng)我們訪問 obj 這個對象中的 x 屬性時桦沉,如果在 obj 中找不到每瞒,那么就會沿著 __proto__
依次查找金闽。
劃重點(diǎn),是在 __proto__
中依次查找
重寫 __proto__
既然我們知道了繼承實(shí)際上是繼承對象 __proto__
上的屬性剿骨,那我們就可以改寫我們的 __proto__
屬性呐矾。
var arr = [1,2,3];
arr.__proto__.addClass = function () {
console.log(123);
}
arr.push(4);
arr.addClass(); // 123
修改了之后,arr
不僅有內(nèi)置的 concat
懦砂、push
等功能,還多了一個 addClass
功能组橄。
也可以完全改寫 __proto__
屬性荞膘,那么其原先的所有的功能都沒有了,如下圖所示玉工。
是時候祭上這張圖了:
typeof
typeof
是解釋器內(nèi)部實(shí)現(xiàn)羽资,根據(jù) ECMA-262 規(guī)定的幾種類型的值來返回類型名稱。
但是 typeof
的應(yīng)用場景非常有限遵班,基本上只能判斷出來使用字面量方式賦值的基本數(shù)據(jù)類型屠升,例如:
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
測試的結(jié)果并不是特別的準(zhǔn)確狭郑,并且只能檢測使用字面量命名的基本數(shù)據(jù)類型(除了 null
)腹暖。所以我們一般不使用 typeof 進(jìn)行數(shù)據(jù)檢測。
instanceof
在上面的例子中翰萨,我們已經(jīng)使用了 typeof
進(jìn)行數(shù)據(jù)檢測脏答。
instance
是“例子,實(shí)例”的意思亩鬼,所以 instanceof
意思是用于判斷變量是否是某一個對象的實(shí)例殖告。
instanceof
原理
以下部分是根據(jù) JavaScript instanceof 運(yùn)算符深入剖析 理解。
instanceof
的原理可以認(rèn)為是如下:
function instance_of(L, R) { //L 表示左表達(dá)式雳锋,R 表示右表達(dá)式
var O = R.prototype; // 取 R 的顯示原型
L = L.__proto__; // 取 L 的隱式原型
while (true) {
if (L === null)
return false;
if (O === L) // 這里重點(diǎn):當(dāng) O 嚴(yán)格等于 L 時黄绩,返回 true
return true;
L = L.__proto__;
}
}
再結(jié)合我們在最開始介紹的前置知識的這張圖來看幾個例子幫助我們更好的理解 instanceof
的原理:
例1:
Object instanceof Object;
// 為了方便表述,首先區(qū)分左側(cè)表達(dá)式和右側(cè)表達(dá)式
ObjectL = Object, ObjectR = Object;
// 下面根據(jù)規(guī)范逐步推演
O = ObjectR.prototype = Object.prototype
L = ObjectL.__proto__ = Function.prototype
// 第一次判斷
O != L
// 循環(huán)查找 L 是否還有 __proto__
L = Function.prototype.__proto__ = Object.prototype
// 第二次判斷
O == L
// 返回 true
例2:
Function instanceof Function
// 為了方便表述玷过,首先區(qū)分左側(cè)表達(dá)式和右側(cè)表達(dá)式
FunctionL = Function, FunctionR = Function;
// 下面根據(jù)規(guī)范逐步推演
O = FunctionR.prototype = Function.prototype
L = FunctionL.__proto__ = Function.prototype
// 第一次判斷
O == L
// 返回 true
例3:
Foo instanceof Foo
// 為了方便表述爽丹,首先區(qū)分左側(cè)表達(dá)式和右側(cè)表達(dá)式
FooL = Foo, FooR = Foo;
// 下面根據(jù)規(guī)范逐步推演
O = FooR.prototype = Foo.prototype
L = FooL.__proto__ = Function.prototype
// 第一次判斷
O != L
// 循環(huán)再次查找 L 是否還有 __proto__
L = Function.prototype.__proto__ = Object.prototype
// 第二次判斷
O != L
// 再次循環(huán)查找 L 是否還有 __proto__
L = Object.prototype.__proto__ = null
// 第三次判斷
L == null
// 返回 false
其實(shí),instanceof 的重點(diǎn)也就是左邊對象的隱式原型等于右邊構(gòu)造函數(shù)的顯示原型冶匹,是不是聽著很熟悉呢习劫,這就是在 new 操作中的關(guān)鍵一步(new 操作是賦值),這樣就可以判斷指定的對象是否是某個構(gòu)造函數(shù)的實(shí)例嚼隘,使 L = L.proto(繼續(xù)沿原型鏈向上尋找)诽里,一直循環(huán)判斷左邊對象的隱式原型等于右邊構(gòu)造函數(shù)的顯示原型,直到L.__proto__
為 null(L 已經(jīng)循環(huán)到 object.prototype) 或者 true
instanceof
局限性
instanceof
的局限性應(yīng)該也就是不能檢測基本數(shù)據(jù)類型了吧飞蛹,其他的貌似沒什么谤狡。通過對 instanceof
的原理進(jìn)行分析灸眼,我們可以得知,只要左邊的對象的對象能夠通過原型鏈 __proto__
是指向右邊的構(gòu)造函數(shù)就可以~
instanceof
右邊必須是對象或構(gòu)造函數(shù)墓懂,否則會拋出 TypeError 的錯誤焰宣。
Object.prototype.toString
所有的數(shù)據(jù)類型都可以用 Object.prototype.toString
來檢測,而且非常的精準(zhǔn)。
以下內(nèi)容參考 談?wù)凮bject.prototype.toString
我們先來看一下 Object.prototype.toString
是怎么進(jìn)行類型檢測的
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
還是能夠比較準(zhǔn)確的檢測出對應(yīng)的類型的匕积。
Object.prototype.toString
的實(shí)現(xiàn)過程
在 ECMAScript 5中,Object.prototype.toString()
被調(diào)用時榜跌,會進(jìn)行如下步驟:
- 如果
this
是undefined
闪唆,返回[object Undefined]
; - 如果
this
是null
钓葫, 返回[object Null]
悄蕾; - 令
Object
為以this
作為參數(shù)調(diào)用ToObject
的結(jié)果; - 令
class
為Object
的內(nèi)部屬性[[Class]]
的值础浮; - 返回三個字符串
[object", class, 以及"]
拼接而成的字符串帆调。
[[Class]]
本規(guī)范的每種內(nèi)置對象都定義了 [[Class]] 內(nèi)部屬性的值。宿主對象的 [[Class]] 內(nèi)部屬性的值可以是除了 "Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", "String" 的任何字符串豆同。[[Class]] 內(nèi)部屬性的值用于內(nèi)部區(qū)分對象的種類番刊。注,本規(guī)范中除了通過 Object.prototype.toString ( 見 15.2.4.2) 沒有提供任何手段使程序訪問此值影锈。
在 JavaScript 代碼里撵枢,唯一可以訪問該屬性的方法就是通過 Object.prototype.toString
,通常方法如下:
Object.prototype.toString.call/apply(value)
在 ES6 請參見 談?wù)?Object.prototype.toString
construtor
construtor
其實(shí)也是用了原型鏈的知識精居。
constructor 屬性返回對創(chuàng)建此對象的數(shù)組函數(shù)的引用锄禽。
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
通過上述的實(shí)例,我們可以看到靴姿,無論是通過字面量或者構(gòu)造函數(shù)創(chuàng)建的基本類型沃但,都可以檢測出。并且也可以檢測出 Array
佛吓、Object
宵晚、Function
引用類型,但是不能檢測出 Null
和 Undefined
Zepto 中檢測數(shù)據(jù)類型
在 Zepto 中主要是用 Object.prototype.toString
來做數(shù)據(jù)類型的判斷的
現(xiàn)在讓我們來看一下 Zepto 中是怎么檢測這些數(shù)據(jù)類型的:
var class2type = {},
toString = class2type.toString
// 在代碼中部维雇,執(zhí)行了
// $.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
那是因?yàn)槿绻麑⒒緮?shù)據(jù)類型,比如 string吱型、number逸贾、boolean等類型的值使用 toString 的方法時,是直接將基本數(shù)據(jù)類型轉(zhuǎn)換為 string 類型,但是如果對 object 類型使用 toString 方法铝侵,則是會調(diào)用其原型上的 toString 方法灼伤,也就是 Object.prototype.toString
。所以 Zepto 在開頭的地方就定義了 class2type 為一個 object 類型咪鲜。
如果 obj 的類型為 null 或者 undefined 直接返回狐赡,如果該對象的 Object.prototype.toString
將返回的結(jié)果作為 class2type 的 key 取值。Object.prototype.toString 對不同的數(shù)據(jù)類型會返回形如 [object Boolean] 的結(jié)果疟丙。
如果都不是以上情況颖侄,默認(rèn)返回 object 類型。
Zepto 中的其他檢測方法
isFunction
// 判斷是否是函數(shù)
function isFunction(value) { return type(value) == "function" }
isWindow
// 判斷是否是 window對象(注意享郊,w為小寫)指當(dāng)前的瀏覽器窗口发皿,window對象的window屬性指向自身。
// 即 window.window === window
function isWindow(obj) { return obj != null && obj == obj.window }
isDocument
// 判斷是否是 document 對象
// window.document.nodeType == 9 數(shù)字表示為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
// 判斷是否是數(shù)組或者對象數(shù)組
// !!的作用是把一個其他類型的變量轉(zhuǎn)成的bool類型。
// !!obj 直接過濾掉了false惶室,null温自,undefined,''等值
function likeArray(obj) {
var length = !!obj && 'length' in obj && obj.length,
// 獲取obj的數(shù)據(jù)類型
type = $.type(obj);
// 不能是function類型皇钞,不能是window
// 如果是array則直接返回true
// 或者當(dāng)length的數(shù)據(jù)類型是number悼泌,并且其取值范圍是0到(length - 1)這里是通過判斷l(xiāng)ength - 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
//數(shù)字
$.isNumeric = function(val) {
var num = Number(val), type = typeof val;
return val != null && type != 'boolean' &&
(type != 'string' || val.length) &&
!isNaN(num) && isFinite(num) || false
}