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

引言

前端面試中有這麼一道經典的問題,如何判斷一個對象是否爲數組?

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()方法的一些思考。bash


思考

首先,說一下toString()方法,轉化爲字符串形式

在ECMAScript中,Object類型的每一個實例都有toString()方法,返回對象的字符串表示,因此每一個實例化的對象均可以調用toString()方法。數據結構

調用結果以下app

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(上卷)
相關文章
相關標籤/搜索