你所不知道的 toString()

November 19, 2019

前言

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

js
function getTag(value) {
// highlight-line
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
js
function getTag(value) {
// highlight-line
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 等类型

js
function bar() {
return arguments;
}
getTag(1) === "[object Arguments]";
// true
getTag(new Date()) === "[object Date]";
// true
getTag(/No.1/) === "[object RegExp]";
// true
js
function bar() {
return arguments;
}
getTag(1) === "[object Arguments]";
// true
getTag(new Date()) === "[object Date]";
// true
getTag(/No.1/) === "[object RegExp]";
// true

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

js
function fn() {}
function* foo() {}
async function baz() {}
getTag(fn) === "[object Function]";
// true
getTag(foo) === "[object GeneratorFunction]";
// true
getTag(baz) === "[object AsyncFunction]";
// true
js
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 的替代写法,两者是相等的

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

以 ES6 新的数据结构 Map 为例,它并没有 bulitinTag,而是通过 @@toStringTag 去获取 tag

相同的还有 Promise

js
const p = Promise.resolve();
getTag(p) === "[object Promise]";
// true
p[Symbol.toStringTag] === "Promise";
// true
js
const p = Promise.resolve();
getTag(p) === "[object Promise]";
// true
p[Symbol.toStringTag] === "Promise";
// true

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

js
const obj = {
[Symbol.toStringTag]: "B2D1",
};
getTag(obj) === "[object B2D1]";
// true
js
const obj = {
[Symbol.toStringTag]: "B2D1",
};
getTag(obj) === "[object B2D1]";
// true

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

Function.prototype.toString

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

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

String.prototype.toString

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

Boolean.prototype.toString

js
var yes = new Boolean("yesyes");
var no = new Boolean(null);
yes.toString();
// "true"
no.toString();
// "false"
// 除了假值,此方法都会返回 "true"
// 假值包括 false null undefined +0 -0 '' NaN
js
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

js
var arr = ["a", "b"];
var x = arr.toString();
var y = arr.join(",");
// 以上两种表达式返回相同内容: 'a,b'
// 小技巧:数组扁平化可以利用 toString()
js
var arr = ["a", "b"];
var x = arr.toString();
var y = arr.join(",");
// 以上两种表达式返回相同内容: 'a,b'
// 小技巧:数组扁平化可以利用 toString()

Number.prototype.toString

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

js
var count = 10;
(count.toString() === "10"(17).toString()) === "17";
var x = 6;
(x.toString(2) === "110"(254).toString(16)) === "fe";
js
var count = 10;
(count.toString() === "10"(17).toString()) === "17";
var x = 6;
(x.toString(2) === "110"(254).toString(16)) === "fe";

B2D1 (包邦东)

Written by B2D1 (包邦东)