由Object.prototype.toString.call( )引發關於toString( )方法的思考

wangpf發表於2018-05-11

引言

前端面試中有這麼一道經典的問題,如何判斷一個物件是否為陣列?

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()方法後的返回結果

  1. 物件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()方法,返回該物件的字串表示!

  1. 陣列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()方法。

  1. 函式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()方法。

  1. 日期(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 (中國標準時間)"
複製程式碼
  1. 正規表示式(RegExp類)

toString():返回正規表示式的字面量

var re = /cat/g;
re.toString();// "/cat/g"
RegExp.prototype.toString.call(re);// "/cat/g"
複製程式碼
  1. 基本包裝型別(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顯示呼叫基本包裝型別建立物件,但我們不建議!!!

  1. 字串string(String類)

toString():返回字串的一個副本

var str = "a";
str.toString(); //"a"
String.prototype.toString.call(str); //"a"
複製程式碼
  1. 數值number(Number類)

toString():返回字串形式的數值

var num = 520;
num.toString(); //"520"
Number.prototype.toString.call(num); //"520"
複製程式碼
  1. 布林值boolean(Boolean類)

toString():返回字串"true"或"false"

var boo = true;
boo.toString(); //"true"
Boolean.prototype.toString.call(boo); //"true"
複製程式碼
  1. null和undefined

null和undefined沒有相應的建構函式,所以它們沒有也無法呼叫toString()方法,也就是說它們不能訪問任何屬性和方法,只是基本型別而已。

  1. 全域性物件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(上卷)

相關文章