你所不知道的 toString()

November 19, 2019

JavaScript

前言

最近在看 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

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

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

以 ES6 新的数据结构 Map 为例,它并没有 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";

Written by B2D1(包邦东)