Object.prototype.toString

469 阅读8分钟

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

  1. If the this value is undefined, return "[object Undefined]".
  2. If the this value is null, return "[object Null]".
  3. Let O be the result of calling ToObject passing the this value as the argument.
  4. Let class be the value of the [[Class]] internal property of O.
  5. Return the String value that is the result of concatenating the three Strings "[object ", class, and "]".

上面的英文翻译过来的意思是:

toString方法被调用的时候,会执行下面的步骤:

  1. 如果this的值是undefined,返回"[object Undefined]"
  2. 如果this的值是null,返回"[object Null]"
  3. 如果是其他值,则先将this的值作为参数传给 ToObject 函数调用并得到结果
  4. 将结果的[[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 TypeResult
UndefinedThrow a TypeError exception.
NullThrow a TypeError exception.
BooleanCreate 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.
NumberCreate 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.
StringCreate 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.
ObjectThe 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方法被调用的时候,会执行下面的步骤

  1. 如果this的值是undefined,返回"[object Undefined]"
  2. 如果this的值是null,返回"[object Null]"
  3. 如果this的值是StringNumberBoolean等原始类型,则先把原始类型准换成对应的引用类型,然后获取其引用类型的[[Class]]属性值,即 "String""Number""Boolean" 等字符串,然后拼接为"[object String]""[object Number]""[object Boolean]"
  4. 如果this的值是MathDateRegExpErrorFunctionArrayJSONObjectArguments等引用类型,则直接获取其[[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:

  1. If the this value is undefined, return "[object Undefined]".
  2. If the this value is null, return "[object Null]".
  3. Let O be ToObject(this value).
  4. Let isArray be ? IsArray(O).
  5. If isArray is true, let builtinTag be "Array".
  6. Else, if O is an exotic String object, let builtinTag be "String".
  7. Else, if O has an [[ParameterMap]] internal slot, let builtinTag be "Arguments".
  8. Else, if O has a [[Call]] internal method, let builtinTag be "Function".
  9. Else, if O has an [[ErrorData]] internal slot, let builtinTag be "Error".
  10. Else, if O has a [[BooleanData]] internal slot, let builtinTag be "Boolean".
  11. Else, if O has a [[NumberData]] internal slot, let builtinTag be "Number".
  12. Else, if O has a [[DateValue]] internal slot, let builtinTag be "Date".
  13. Else, if O has a [[RegExpMatcher]] internal slot, let builtinTag be "RegExp".
  14. Else, let builtinTag be "Object".
  15. Let tag be ? Get(O, @@toStringTag).
  16. If Type(tag) is not String, let tag be builtinTag.
  17. 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方法被调用时,会执行下面的步骤:

  1. 如果this的值是undefined,返回"[object Undefined]"
  2. 如果this的值是null,返回"[object Null]"
  3. 假设OToObject方法把this的值作为参数执行的结果。
  4. 使用isArray方法验证O
  5. 如果isArray方法返回true,则内部标识为"Array"
  6. 如果O是特定字符串对象,则内部标识为"String"
  7. 如果O有一个[[ParameterMap]]内部插槽,则内部标识为"Arguments"
  8. 如果O有一个[[Call]]内部插槽,则内部标识为"Function"
  9. 如果O有一个[[ErrorData]]内部插槽,则内部标识为"Error"
  10. 如果O有一个[[BooleanData]]内部插槽,则内部标识为"Boolean"
  11. 如果O有一个[[NumberData]]内部插槽,则内部标识为"Number"
  12. 如果O有一个[[DateValue]]内部插槽,则内部标识为"Date"
  13. 如果O有一个[[RegExpMatcher]]内部插槽,则内部标识为"RegExp"
  14. 如果都不是则内部标识为"Object"
  15. 假设tagO对象的@@toStringTag属性值。
  16. 如果tag获取@@toStringTag属性值不是字符串,则tag等于内部标识。
  17. 最后返回拼接后的字符串"[object tag]"

注意:

在本规范的前几个版本中,toString这个函数有时会被用来访问各种内置对象的名义类型标记[[Class]]的内部插槽的String值。上面toString函数保留了对以前一些特定类型内置对象的检测。但是对于其他内置对象或者预定义对象没有提供可靠的检测机制。针对这种情况,程序提供使用@@toStringTag的检测方式代替原来的类型检测方法。

很明显toString方法在ES7内部产生了比较大的变化,具体体现第4~16步:

  1. 提供了专门判断数组的方法isArray
  2. 不再访问内置对象的[[call]]属性,而是根据一些特殊内部插槽判断内置对象,例如根据Arguments[[ParameterMap]]内部插槽标记"Arguments"标识、根据Function[[ParameterMap]]内部插槽标记"Function"标识等等(这里我认为内部卡槽可以简单理解为标识或属性)。
  3. 一部分老的内置对象和新增的内置对象都有@@toStringTag属性值,例如JSONSymbolMapSetWeekMapWeekSetArrayBufferDataViewPromise
  4. 获取对象的@@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,获取的tagObject,所以最终的结果是"[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))