引言
前端面试中有这么一道经典的问题, 如何判断一个对象是否为数组?
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(上卷)
来源: https://juejin.im/post/5af5a7206fb9a07ab83e179d