引言
前端面試中有這麼一道經典的問題,如何判斷一個物件是否為陣列?
ES5提供了一個確定物件是否為陣列的函式
Array.isArray(object);
複製程式碼
其中,object是必須的,表示要測試的物件
Array.isArray([]); //true
Array.isArray({}); //false
Array.isArray(''); //false
複製程式碼
但是,當我們考慮到瀏覽器相容性問題時,我們需要一個更為穩妥的判斷方式
Object.prototype.toString.call/apply(object);
複製程式碼
比較結果如下
Object.prototype.toString.call([]);
<!--"[object Array]"-->
Object.prototype.toString.call({});
<!--"[object Object]"-->
複製程式碼
至於為什麼要使用該方法確定一個物件是否為陣列,只需瞭解下關於typeof和instanceof的資料型別判斷即可。
這裡主要想談一談關於toString()方法的一些思考。
思考
首先,說一下toString()方法,轉化為字串形式
在ECMAScript中,Object型別的每個例項都有toString()方法,返回物件的字串表示,所以每個例項化的物件都可以呼叫toString()方法。
呼叫結果如下
var obj = {a: 1};
obj.toString(); //"[object Object]"
複製程式碼
那麼,obj的toString()方法是哪裡來的呢?
我們順著原型鏈,obj => obj.proto => Object.prototype,可以發現,toString()方法是定義在Object的原型物件Object.prototype上的,這樣Object的每個例項化物件都可以共享Object.prototype.toString()方法。
如果不通過原型鏈查詢,怎麼直接呼叫Object.prototype.toString()方法呢?
Object.prototype.toString();
<!--"[object Object]"-->
複製程式碼
這樣寫對嗎?上述的程式碼中toString()的呼叫和obj物件沒有關係啊,為什麼還得到了同樣的結果呢?這是因為Object.prototype也是物件,所以返回了物件的字串表示!
通過obj物件呼叫Object.prototype.toString()方法的正確方式如下所示:
Object.prototype.toString.call/apply(obj);
<!--"[object Object]"-->
複製程式碼
接下來,我們再來分析一下不同型別的“物件”呼叫toString()方法,返回值有什麼不同之處?
我們先明確一下ECMAScript的資料型別,7種
- Undefined
- Null
- String
- Number
- Boolean
- Object
- Symbol(ES6引入)
其中,Object作為引用型別,它是一種資料結構,常被稱為Object類(但這種稱呼並不妥當,JS中沒有類,一切都是語法糖而已)。
另外,基於Object型別,JS還實現了其他常用的物件子型別(就是不同型別的物件)
- Object
- Array
- Function
- String
- Boolean
- Number
- Date
- RegExp
- Error
- ...
我們可以說,Object類是所有子類的父類
Object instanceof Object; //true
Function instanceof Object; //true
Array instanceof Object; //true
String instanceof Object; //true
Boolean instanceof Object; //true
Number instanceof Object; //true
複製程式碼
所以,上文提到的定義在Object.prototype上的toString()方法,可以說是最原始的toString()方法了,其他型別都或多或少重寫了toString()方法,導致不同型別的物件呼叫toString()方法產生返回值各不相同。
我們還要知道的是,例項物件的建立有兩種形式,建構函式形式和字面量形式,具體區別暫不討論。
下面,具體分析不同的物件子型別重寫toString()方法後的返回結果
- 物件object(Object類)
toString():返回物件的字串表示
var obj = {a: 1};
obj.toString();//"[object Object]"
Object.prototype.toString.call(obj);//"[object Object]"
複製程式碼
這裡我們思考一個問題,任何物件object都可以通過this繫結呼叫Object.prototype.toString()方法嗎?答案是可以,結果如下
Object.prototype.toString.call({});
<!--"[object Object]"-->
Object.prototype.toString.call([]);
<!--"[object Array]"-->
Object.prototype.toString.call(function(){});
<!--"[object Function]"-->
Object.prototype.toString.call('');
<!--"[object String]"-->
Object.prototype.toString.call(1);
<!--"[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();
<!--"[object Undefined]"-->
Object.prototype.toString.call(new Date());
<!--"[object Date]"-->
Object.prototype.toString.call(/at/);
<!--"[object RegExp]"-->
複製程式碼
從上述程式碼可以看到,因為Object是所有子類的父類,所以任何型別的物件object都可以通過this繫結呼叫Object.prototype.toString()方法,返回該物件的字串表示!
- 陣列array(Array類)
toString():返回由陣列中每個值的字串形式拼接而成的一個以逗號分隔的字串
var array = [1, 's', true, {a: 2}];
array.toString();//"1,s,true,[object Object]"
Array.prototype.toString.call(array);//"1,s,true,[object Object]"
複製程式碼
這裡我們同樣思考上述問題,非陣列物件也可以通過this繫結呼叫Array.prototype.toString()方法嗎?答案是可以,結果如下
Array.prototype.toString.call({});
<!--"[object Object]"-->
Array.prototype.toString.call(function(){})
<!--"[object Function]"-->
Array.prototype.toString.call(1)
<!--"[object Number]"-->
Array.prototype.toString.call('')
<!--"[object String]"-->
Array.prototype.toString.call(true)
<!--"[object Boolean]"-->
Array.prototype.toString.call(/s/)
<!--"[object RegExp]"-->
Array.prototype.toString.call();
<!--Cannot convert undefined or null to object at toString-->
Array.prototype.toString.call(undefined);
Array.prototype.toString.call(null);
複製程式碼
從上述程式碼中我們可以發現,陣列物件通過this繫結呼叫Array.prototype.toString()方法,返回陣列值的字串拼接,但是非陣列物件通過this繫結呼叫Array.prototype.toString()方法,返回的是該物件的字串表示,另外null和undefined不可以通過繫結呼叫Array.prototype.toString()方法。
- 函式function(Function類)
toString():返回函式的程式碼
function foo(){
console.log('function');
};
foo.toString();
<!--"function foo(){-->
<!-- console.log('function');-->
<!--}"-->
Function.prototype.toString.call(foo);
<!--"function foo(){-->
<!-- console.log('function');-->
<!--}"-->
複製程式碼
此處,我們還需要注意到一個問題,上述我們提到的所有“類”,本質上都是建構函式,所以呼叫toString()方法返回的都是函式程式碼。
Object.toString();
//"function Object() { [native code] }"
Function.toString();
//"function Function() { [native code] }"
Array.toString();
//"function Array() { [native code] }"
....
複製程式碼
另外,我們再考慮一下上述提到的問題,非函式物件也可以通過this繫結呼叫Array.prototype.toString()方法嗎?答案是不可以,結果如下
Function.prototype.toString.call({});
<!--Function.prototype.toString requires that 'this' be a Function-->
複製程式碼
另外,通過對其他Object子類的測試,除了上述提到的Object和Array兩種情況,其他型別都不支援非自身例項通過this繫結呼叫該Object子類原型物件上的toString()方法,這說明它們在重寫toString()方法時,明確限定了呼叫該方法的物件型別,非自身物件例項不可呼叫。所以,一般我們只使用Object.prototype.toString.call/apply()方法。
- 日期(Date類)
toString():返回帶有時區資訊的日期和時間
Date型別只有構造形式,沒有字面量形式
var date = new Date();
date.toString();
//"Fri May 11 2018 14:55:43 GMT+0800 (中國標準時間)"
Date.prototype.toString.call(date);
//"Fri May 11 2018 14:55:43 GMT+0800 (中國標準時間)"
複製程式碼
- 正規表示式(RegExp類)
toString():返回正規表示式的字面量
var re = /cat/g;
re.toString();// "/cat/g"
RegExp.prototype.toString.call(re);// "/cat/g"
複製程式碼
- 基本包裝型別(Boolean/Number/String類)
ECMAScript提供了三個特殊的引用型別Boolean、Number、String,它們具有與各自基本型別相應的特殊行為。
以String型別為例簡單說一下
var str = 'wangpf';
str.toString();//"wangpf"
複製程式碼
關於上述程式碼存在疑問,首先我定義了一個基本型別的字串變數str,它不是物件,但為什麼可以呼叫toString()方法呢,另外,toString()方法又是哪裡來的呢?
我們先看一下str和strObject的區別:
var str = 'I am a string';
typeof str; //"string"
str instanceof String; //false
var strObject = new String('I am a string');
typeof strObject; //"object"
strObject instanceof String; //true
strObject instanceof Object; //true
複製程式碼
原來,由於String基本包裝型別的存在,在必要的時候JS引擎會把字串字面量轉換成一個String物件,從而可以執行訪問屬性和方法的操作,具體過程如下所示:
(1)建立一個String型別的例項;
(2)在例項上呼叫指定的方法;
(3)銷燬這個例項。
複製程式碼
因此呼叫toString()方法的過程如下所示:
var strObject = new String('wangpf');
strObject.toString(); //'wangpf'
strObject = null;
複製程式碼
注意,上述程式碼是JS引擎自動執行的,你無法訪問strObject物件,它只存在於程式碼的執行瞬間,然後立即銷燬,所以我們無法再執行時給基本型別新增屬性和方法,除非直接通過new顯示呼叫基本包裝型別建立物件,但我們不建議!!!
- 字串string(String類)
toString():返回字串的一個副本
var str = "a";
str.toString(); //"a"
String.prototype.toString.call(str); //"a"
複製程式碼
- 數值number(Number類)
toString():返回字串形式的數值
var num = 520;
num.toString(); //"520"
Number.prototype.toString.call(num); //"520"
複製程式碼
- 布林值boolean(Boolean類)
toString():返回字串"true"或"false"
var boo = true;
boo.toString(); //"true"
Boolean.prototype.toString.call(boo); //"true"
複製程式碼
- null和undefined
null和undefined沒有相應的建構函式,所以它們沒有也無法呼叫toString()方法,也就是說它們不能訪問任何屬性和方法,只是基本型別而已。
- 全域性物件window(Window類)
全域性物件Global可以說是ECMAScript中最特別的一個物件了,它本身不存在,但是會作為終極的“兜底兒物件”,所有不屬於其他物件的屬性和方法,最終都是它的屬性和方法。
ECMAScript無法沒有指出如何直接訪問Global物件,但是Web瀏覽器將這個Global物件作為window物件的一部分加以實現了。所以上述提到的所有物件型別,如Object、Array、Function都是window物件的屬性。
toString(): 返回物件的字串表示
window.toString();
<!--"[object Window]"-->
Window.prototype.toString.call(window);//這裡其實有問題
<!--"[object Window]"-->
複製程式碼
經檢視,Winodw類並沒有在Window.prototype原型物件上重寫toString()方法,它會順著原型鏈查詢呼叫Object.prototype.toString()。
所以,任何物件object都可以通過this繫結呼叫Window.prototype.toString()方法,也就是呼叫Object.prototype.toString()方法,結果和Object類一樣。
故上述程式碼實質上是
Object.prototype.toString.call(window);
<!--"[object Window]"-->
複製程式碼
最後,說一說直接執行toString()方法
直接執行toString(),輸出結果如下
toString();
<!--"[object Undefined]"-->
(function(){
console.log(toString());
})();
<!--[object Undefined]-->
複製程式碼
也就是說直接呼叫toString()方法,等價於
Object.prototype.toString.call();
<!--"[object Undefined]"-->
Object.prototype.toString.call(undefined);
<!--"[object Undefined]"-->
複製程式碼
所以直接呼叫toString()應該就是變相的undefined.toString()方法。
注意,直接呼叫toString()方法這裡不可以理解成為全域性作用域呼叫toString()方法,即window.toString();
另外,再說一下toString.call/apply(this)方法
toString.call({});
<!--"[object Object]"-->
toString.call([]);
<!--"[object Array]"-->
複製程式碼
經常有人用toString.call/apply(this)去代替Object.prototype.toString.call/apply(this)使用,我認為這樣是不嚴謹的,容易導致一些問題,如下所示
function toString(){
console.log("wangpf")
}
toString();//"wangpf"
toString.call({});//"wangpf"
toString.call([]);//"wangpf"
複製程式碼
我們可以發現,當我們自定義了toString()方法時,直接呼叫toString()方法,就不會再預設呼叫Object類的toString()方法,而是會使用我們自定義的方法,這樣可能得不到我們想要的結果,所以我們還是應當儘量使用Object.prototype.toString.call/apply(this)。
擴充
類似toString()方法,Object的不同子型別還重寫了toLocaleString()、valueOf()等方法,這裡我想說的是不管物件子型別怎麼重寫方法,只要我們明白這些方法是哪裡來的,怎麼呼叫的,就能很好的理解這些方法呼叫後產生的結果!
說到底,對JS中物件和原型的理解真的非常非常重要!
參考
- JavaScript高階程式設計(第三版)
- 你不知道的JavaScript(上卷)