在看这篇文章《JS - 如何判断一个变量是Array类型?如何判断一个变量是Number类型?(都不止一种)》的时候。文章提到可以通过下面方式判断变量是否是一个Array类型:
Object.prototype.toString.call(o) === '[object Array]'
然后文章继续说,
为什么不直接o.toString()?嗯,虽然Array继承自Object,也会有 toString方法,但是这个方法有可能会被改写而达不到我们的要求,而Object.prototype则是老虎的屁股,很少有人敢去碰它的,所以能一定程度保证其“纯洁性”
虽说表述并没错误,但是这个方法有可能会被改写而达不到我们的要求这句话有点过于简单了。
所以,让我们来通过ECMA262规范来一探究竟。
Object.prototype.toString ( )
20.1.3.6 Object.prototype.toString ( )
When the toString method is called, the following steps are taken:
- If the this value is undefined, return "[object Undefined]".
- If the this value is null, return "[object Null]".
- Let O be ! ToObject(this value).
- Let isArray be ? IsArray(O).
- If isArray is true, let builtinTag be "Array".
- Else if O has a [[ParameterMap]] internal slot, let builtinTag be "Arguments".
- Else if O has a [[Call]] internal method, let builtinTag be "Function".
- Else if O has an [[ErrorData]] internal slot, let builtinTag be "Error".
- Else if O has a [[BooleanData]] internal slot, let builtinTag be "Boolean".
- Else if O has a [[NumberData]] internal slot, let builtinTag be "Number".
- Else if O has a [[StringData]] internal slot, let builtinTag be "String".
- Else if O has a [[DateValue]] internal slot, let builtinTag be "Date".
- Else if O has a [[RegExpMatcher]] internal slot, let builtinTag be "RegExp".
- Else, let builtinTag be "Object".
- Let tag be ? Get(O, @@toStringTag).
- If Type(tag) is not String, set tag to builtinTag.
- Return the string-concatenation of "[object ", tag, and "]".
我们通过以下代码,按照该规范来走一遍:
const o = [1,2,3]
console.log(Object.prototype.toString.call(o)) // ?
- If the this value is undefined, return "[object Undefined]".
【1. 如果this的值是undefined,返回"[object Undefined]"】。
我们知道,一个函数的call方法的第一个参数就是绑定的this,此时this为o=[1,2,3],并不是undefined,判断不成立,走向2.
- If the this value is null, return "[object Null]".
【2. 如果this的值是null,返回"[object Null]"】。
由第一步我们可知this不为null,判断不成立,走向3.
- Let O be ! ToObject(this value).
【3. 令 O 为 ! ToObject(this value)】。
在前一篇文章里,我们也遇到了ToObject这个抽象方法
ToObject是一个抽象方法,它根据参数的类型转化为相对应的值。
| Argument Type | Result |
|---|---|
| ... | ... |
| Object | Return argument. |
在这里,ToObject的参数O是一个array更是一个Object,则! ToObject(O)返回O本身。走向4。
(注意:ToObject方法前的!并不是javascript语言中的非,它在规范中表示此时调用ToObject方法一定不会抛异常。)
- Let isArray be ? IsArray(O).
【4. 令 isArray 为 ? IsArray(O)】
在这里第4步判断O是否为一个数组。显然O是一个数组,isArray为true。
- If isArray is true, let builtinTag be "Array".
【5. 如果isArray为true,令 builtinTag 为 "Array"】。
此时,我们就可以跳过6、7、8、9、10、11、12、13、14步了。走向第15步。
- Let tag be ? Get(O, @@toStringTag).
【15. 令 tag 为 ? Get(O, @@toStringTag)】
Get(O, @@toStringTag)的顾名思义是从O中取名为@@toStringTag的property,查一下规范@@toStringTag是啥:
A String valued property that is used in the creation of the default string description of an object. Accessed by the built-in method Object.prototype.toString.
【一个字符串的property,在object创建时作为其默认的描述字符串。通过Object.prototype.toString得到】。
我们只需要知道,一个Array[@@toStringTag]的初始值是"Array"(同理,Map[@@toStringTag]的初始值是"Map"),所以此时tag为"Array"。
走向第16步。
- If Type(tag) is not String, set tag to builtinTag.
【16. 如果Type(tag)不是String,将tag设置成builtinTag】。
因为tag为"Array"是一个字符串,所以略过。走向17。
- Return the string-concatenation of "[object ", tag, and "]".
【17. 返回字符串拼接的"[object ", tag, and "]"】,因为tag为"Array",所以返回值是"[object , Array]"。
结论
通过上述流程,我们知道了答案:
const o = [1,2,3]
console.log(Object.prototype.toString.call(o)) // 为[object , Array]。
而为什么我看的那篇文章说为什么不直接o.toString()?呢,因为Array的确由一套自己的toString方法。查看规范得知:
23.1.3.30 Array.prototype.toString ( )
When the toString method is called, the following steps are taken:
- Let array be ? ToObject(this value).
- Let func be ? Get(array, "join").
- If IsCallable(func) is false, set func to the intrinsic function %Object.prototype.toString%.
- Return ? Call(func, array).
以下述代码举例
const o = [1,2,3]
console.log(o.toString()) // ?
简单说说就是,第1步得array=o;第2步令func为array的join方法;第3步因为func确实可以调用,略过;第4步,返回Call(func,array),即在array上调用func(此处为join)方法,等价于[1,2,3].join()得到"1,2,3"。
手动验证一下[1,2,3].toString()的确为"1,2,3"。
所以,这也是为什么不能直接用array.toString()方法作为数组判断依据的原因。
结语
通过阅读ECMA规范,确实对很多js的用法从单纯的是什么明白了为什么。
因初读ECMA262规范,能力有限,本文或许存在理解错误的地方,还望指正。