在判断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))