由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()方法。markdown

调用结果以下数据结构

var obj = {a: 1};
obj.toString(); //"[object Object]"
复制代码

那么,obj的toString()方法是哪里来的呢?app

咱们顺着原型链,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(上卷)
相关文章
相关标签/搜索