你所不知道的 toString()

949 阅读3分钟

Object.prototype.toString

最近在看 Lodash 的源码,其精简的语法和巧妙的设计,值得大家去细品 。其中有一个工具函数叫 getTag,旨在获取对象的类型标记(Tag),即我们所熟知的,利用 Object.prototype.toString.call() 去做类型检测。


function getTag(value) {
  if (value == null) { // 执行非严格相等,判断为 undefined 或 null
    return value === undefined ? '[object Undefined]' : '[object Null]'
  }
  return Object.prototype.toString.call(value) // 检测其他类型,返回 "[object, tag]" 形式
}

getTag({}) === '[object Object]' 
// true
getTag(1) === '[object Number]'
// true

此方法不仅可检测常见的基本类型,还可检测诸如 Date RegExp Arguments 等类型

function bar() {
  return arguments
}

getTag(1) === '[object Arguments]'
// true
getTag(new Date()) === '[object Date]'
// true
getTag(/No.1/) === '[object RegExp]'
// true

让我们加大力度,发现除了普通函数,还能检测出是 异步函数 又或是 生成器函数

function fn() {}

function* foo() {}

async function baz() {}

getTag(fn) === '[object Function]'
// true
getTag(foo) === '[object GeneratorFunction]'
// true
getTag(baz) === '[object AsyncFunction]'
// true

到目前为止,Object.prototype.toString.call() 表现得规规矩矩,但是大家发现没有?我们上述的例子都是采用 JS 的内置对象,并且没有修改其内部结构。如果修改了其内部结构就不一定了!

要追究其原理,我们先来细看 ECMAScript® 2020 : 19.1.3.6 Object.prototype.toString ( ) 中相关描述

当调用 toString(O) 方法时,将执行以下步骤:

  1. 如果 Oundefined,返回 "[object Undefined]"
  2. 如果 Onull,返回 "[object Null]"
  3. 调用 toObject(O)
    1. 如果 O 已是一个对象类型,直接返回 O
    2. 如果是基本数据类型,则对 O 进行装箱操作,以布尔值为例,会返回 new Boolean(O)
  4. 如果 OArray,使 bulitinTag"Array"
  5. 如果 O 拥有 [[ParameterMap]] 内部插槽,使 bulitinTag"Arguments"
  6. 如果 O 拥有 [[Call]] 内部插槽,使 bulitinTag"Arguments"
  7. 如果 O 拥有 [[ErrorData]] 内部插槽,使 bulitinTag"Error"
  8. 如果 O 拥有 [[BooleanData]] 内部插槽,使 bulitinTag"Boolean"
  9. 如果 O 拥有 [[NumberData]] 内部插槽,使 bulitinTag"Number"
  10. 如果 O 拥有 [[StringData]] 内部插槽,使 bulitinTag"String"
  11. 如果 O 拥有 [[DateValue]] 内部插槽,使 bulitinTag"Date"
  12. 如果 O 拥有 [[RegExpMatcher]] 内部插槽,使 bulitinTag"RegExp"
  13. 否则,使 bulitinTag"Object"
  14. tag 设置为 O@@toStringTag
  15. 如果 tag 不是 string,将 tag 设置为 bulitinTag
  16. 返回 "[object, tag]"

这里的内部插糟实现,我理解为对象的内部初始化属性,好比下图中,新建了一个布尔对象,它的 [[PrimitiveValue]]true 对应上述步骤中的 [[BooleanData]]

我们重点来看第 14 步,这里有个 @@toStringTag,其实它就是 Symbol.toStringTag 的替代写法,两者是相等的

const m = new Map()

getTag(m) === '[object Map]'
// true
m[Symbol.toStringTag] === 'Map' // 注意:不能使用点操作符去获取 Symbol 属性,会报错
// true

很容易看出,Map 这个 ES6 才出来的数据结构,并没有 bulitinTag,而是通过 @@toStringTag 去获取 tag

相同的还有 Promise

const p = Promise.resolve()

getTag(p) === '[object Promise]'
// true
p[Symbol.toStringTag] === 'Promise'
// true

并且可以人为修改 @@toStringTag,所以使用此方法也不是百分百准确,还是有一定的局限性

const obj = {
    [Symbol.toStringTag]: "B2D1"
};

getTag(obj) === '[object B2D1]'
// true

看到这里,相信读者们对 Object.prototype.toString.call() 的原理已经很熟悉了,本文最重要的部分已经结束,不如借着势头,看看其他类型的 toString(),相信能大大夯实读者的 JavaScript 基础

Function.prototype.toString

此方法可以帮助你获得函数的源代码(包括注释),搭配正则可以从中提取出有效的信息

var fnc = function(x) {
    // i am comment
    return x;
}

fnc.toString()
// "function(x) {
//	// i am comment
//	return x;
//}"

String.prototype.toString

var x = new String("Hi")
x.toString()
// "Hi"

Boolean.prototype.toString

var yes = new Boolean('yesyes')
var no = new Boolean(null)

yes.toString()
// "true"
no.toString()
// "false"

// 除了假值,此方法都会返回 "true"
// 假值包括 false null undefined +0 -0 '' NaN

Array.prototype.toString

var arr = ['a', 'b']
var x = arr.toString()
var y = arr.join(',')

// 以上两种表达式返回相同内容: 'a,b'
// 小技巧:数组扁平化可以利用 toString()

Number.prototype.toString

只接受一个整形参数 radix(2 <= radix <= 36),默认为 10,表示要转化的进制,返回转化后数字的字符串表示

var count = 10

count.toString() === '10'
(17).toString() === '17'

var x = 6;

x.toString(2) === '110'
(254).toString(16) === 'fe'