在判断js数据类型时,经常会用到Object.prototype.toString方法进行判断,那么你有思考过为什么这个方法可以判断出变量的准确类型?为什么使用的时候要用call方法?下面的文章将帮助你了解Object.prototype.toString方法的底层原理。
ES5的Object.prototype.toString方法
首先查看ES5官方文档对于Object.prototype.toString方法的描述:
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 the result of calling ToObject passing the this value as the argument.
- Let class be the value of the [[Class]] internal property of O.
- Return the String value that is the result of concatenating the three Strings
"[object ", class, and"]".
上面的英文翻译过来的意思是:
当toString方法被调用的时候,会执行下面的步骤:
- 如果this的值是undefined,返回
"[object Undefined]"。 - 如果this的值是null,返回
"[object Null]"。 - 如果是其他值,则先将this的值作为参数传给 ToObject 函数调用并得到结果
- 将结果的
[[Class]]属性值作为变量class进行拼接,最终的结果是"[object class]"。
1和2不用赘述了,第3步中的 ToObject 函数是什么意思哪?可以看一下ToObject官方文档:
ToObject
The abstract operation ToObject converts its argument to a value of type Object according to Table 14:
Table 14 — ToObject
| Argument Type | Result |
|---|---|
| Undefined | Throw a TypeError exception. |
| Null | Throw a TypeError exception. |
| Boolean | Create a new Boolean object whose [[PrimitiveValue]] internal property is set to the value of the argument. See 15.6 for a description of Boolean objects. |
| Number | Create a new Number object whose [[PrimitiveValue]] internal property is set to the value of the argument. See 15.7 for a description of Number objects. |
| String | Create a new String object whose [[PrimitiveValue]] internal property is set to the value of the argument. See 15.5 for a description of String objects. |
| Object | The result is the input argument (no conversion). |
翻译过来:
ToObject抽象方法会根据下面的表格把方法的参数转化为类型对象的值。
| 参数类型 | 结果 |
|---|---|
| Undefined | 抛出一个异常 |
| Null | 抛出一个异常 |
| Boolean | 创建一个有[[PrimitiveValue]]内部属性的Boolean对象。查看Boolean对象的描述 15.6 。 |
| Number | 创建一个有[[PrimitiveValue]]内部属性的Number对象。查看Number对象的描述 15.7 。 |
| String | 创建一个有[[PrimitiveValue]]内部属性的String对象。查看String对象的描述 15.5 。 |
| Object | 输入的参数就是结果。(不进行转换) |
第4步中的[[Class]]属性是什么哪?可以看一下ES5官网中的一些片段:
The [[Class]] internal property of the newly constructed object is set to "String".
The [[Class]] internal property of the newly constructed Boolean object is set to "Boolean".
The [[Class]] internal property of the newly constructed object is set to "Number".
The value of the [[Class]] internal property of the Math object is "Math".
The [[Class]] internal property of the newly constructed object is set to "Date".
The [[Class]] internal property of the newly constructed object is set to "RegExp".
The [[Class]] internal property of the newly constructed object is set to "Error".
The value of the [[Class]] internal property is "Function".
The [[Class]] internal property of the newly constructed object is set to "Array".
The value of the [[Class]] internal property of the JSON object is "JSON".
The value of the [[Class]] internal property of a host object may be any String value except one of "Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", and "String".
......
从上面可以看出来String、Boolean、Number、Math、Date、RegExp、Error、Function对象的[[Class]]是对象名的字符串。
总结上面的内容:
当Object.prototype.toString方法被调用的时候,会执行下面的步骤:
- 如果this的值是undefined,返回
"[object Undefined]"。 - 如果this的值是null,返回
"[object Null]"。 - 如果this的值是String、Number、Boolean等原始类型,则先把原始类型准换成对应的引用类型,然后获取其引用类型的
[[Class]]属性值,即 "String" 、 "Number" 、 "Boolean" 等字符串,然后拼接为"[object String]"、"[object Number]"、"[object Boolean]"。 - 如果this的值是Math、Date、RegExp、Error、Function、Array、JSON、Object、Arguments等引用类型,则直接获取其
[[Class]]属性值,即"Math"、"Date"、"RegExp"、"Error"、"Function"、"Array"、"JSON"、"Object"、"Arguments"等字符串。然后拼接为"[object Math]"、"[object Date]"、"[object RegExp]"、"[object Error]"、"[object Function]"、"[object Array]"、"[object JSON]"、"[object Object]"、"[object Arguments]"。
为什么Object.prototype.toString方法使用时要使用call方法
在上面的执行步骤中特别强调了this,当直接使用Object.prototype.toString方法时,this的值是Object本身,执行结果也就是"[object Object]"字符串。
而当使用call方法时,就修改了Object.prototype.toString方法的this值,此时this的值就是call方法的参数。
ES7的Object.prototype.toString方法
在ES7的官方文档中Object.prototype.toString方法发生了新的变化,ES6和ES7相差不大,感兴趣可以直接查看ES6文档,但是与ES5差别比较大:
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 is an exotic String object, let builtinTag be
"String". - Else, if O has an [[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 [[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, let tag be builtinTag.
- Return the String that is the result of concatenating
"[object ", tag, and"]".
This function is the %ObjProto_toString% intrinsic object.
NOTE
Historically, this function was occasionally used to access the String value of the [[Class]] internal slot that was used in previous editions of this specification as a nominal type tag for various built-in objects. The above definition of toString preserves compatibility for legacy code that uses toString as a test for those specific kinds of built-in objects. It does not provide a reliable type testing mechanism for other kinds of built-in or program defined objects. In addition, programs can use @@toStringTag in ways that will invalidate the reliability of such legacy type tests.
下面是个人翻译,不代表官方:
当toString方法被调用时,会执行下面的步骤:
- 如果this的值是undefined,返回
"[object Undefined]"。 - 如果this的值是null,返回
"[object Null]"。 - 假设O是ToObject方法把this的值作为参数执行的结果。
- 使用isArray方法验证O。
- 如果isArray方法返回true,则内部标识为
"Array"。 - 如果O是特定字符串对象,则内部标识为
"String"。 - 如果O有一个
[[ParameterMap]]内部插槽,则内部标识为"Arguments"。 - 如果O有一个
[[Call]]内部插槽,则内部标识为"Function"。 - 如果O有一个
[[ErrorData]]内部插槽,则内部标识为"Error"。 - 如果O有一个
[[BooleanData]]内部插槽,则内部标识为"Boolean"。 - 如果O有一个
[[NumberData]]内部插槽,则内部标识为"Number"。 - 如果O有一个
[[DateValue]]内部插槽,则内部标识为"Date"。 - 如果O有一个
[[RegExpMatcher]]内部插槽,则内部标识为"RegExp"。 - 如果都不是则内部标识为
"Object"。 - 假设tag是O对象的
@@toStringTag属性值。 - 如果tag获取
@@toStringTag属性值不是字符串,则tag等于内部标识。 - 最后返回拼接后的字符串
"[object tag]"。
注意:
在本规范的前几个版本中,toString这个函数有时会被用来访问各种内置对象的名义类型标记[[Class]]的内部插槽的String值。上面toString函数保留了对以前一些特定类型内置对象的检测。但是对于其他内置对象或者预定义对象没有提供可靠的检测机制。针对这种情况,程序提供使用@@toStringTag的检测方式代替原来的类型检测方法。
很明显toString方法在ES7内部产生了比较大的变化,具体体现第4~16步:
- 提供了专门判断数组的方法isArray。
- 不再访问内置对象的
[[call]]属性,而是根据一些特殊内部插槽判断内置对象,例如根据Arguments的[[ParameterMap]]内部插槽标记"Arguments"标识、根据Function的[[ParameterMap]]内部插槽标记"Function"标识等等(这里我认为内部卡槽可以简单理解为标识或属性)。 - 一部分老的内置对象和新增的内置对象都有
@@toStringTag属性值,例如JSON、Symbol、Map、Set、WeekMap、WeekSet、ArrayBuffer、DataView、Promise。 - 获取对象的
@@toStringTag或者内部标识的字符串值。
如何让Object.prototype.toString方法识别自定义对象
看下面的代码:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
setName(name) {
this.name = name;
}
}
const p = new Person('王五', 25);
// undefined
console.log(Person.prototype[Symbol.toStringTag])
// [object Object]
console.log(Object.prototype.toString.call(p))
代码中自定义了Person对象,但是使用Object.prototype.toString.call判断类型时只能返回"[object Object]"。这是因为Person继承了Object的toString方法,根据ES7的toString执行的步骤可以知道执行顺序是3->14->17,获取的tag是Object,所以最终的结果是"[object Object]"。
如果可以让Object.prototype.toString.call判断类型返回"[object Person]"哪?
我们需要再次查看toString执行的步骤,看第15和第16可以知道,如果给Person设置@@toStringTag的话,就可以实现我们的目标。
如何设置@@toStringTag哪?可以查看官方文档Symbol.toStringTag,每个对象都有一个Symbol.toStringTag属性,这个属性设置的值就是@@toStringTag。
所以可以将上面的代码改为:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
setName(name) {
this.name = name;
}
get [Symbol.toStringTag]() {
return 'Person'
}
}
const p = new Person('王五', 25);
// Person
console.log(Person.prototype[Symbol.toStringTag])
// [object Person]
console.log(Object.prototype.toString.call(p))