今日题目:

可以详细说说Object.prototype.toString实现类型判断的原理吗?他有什么缺点吗?怎么弥补呢?

题目解析:

平时我们就可以直接使用Object.prototype.toString用来做类型判断。他是几种方法里可以开箱即用、且判断类型最完善了。

他的具体原理:

在toString方法被调用时,会执行下面的操作步骤~

  1. 获取this指向的那个对象的[[Class]]属性的值。(这也是我们为什么要用call改变this指向的原因)
  2. 计算出三个字符串”[object “、 第一步的操作结果Result(1)、 以及 “]” 连接后的新字符串。
  3. 返回第二步的操作结果Result(2),也就是类似 [object class] 这种格式字符串。

    [[Class]] 类属性:

    对象的类属性(class attribute)是一个字符串,用以表示对象的类型信息。ES3和ES5都没有提供设置这个属性的方法,并只有一种间接的方法可以查询到它。默认的toString方法(继承自Object.prototype)返回了如下格式的字符串: [object class] 因此,想要获得对象的类,可以调用对象的toString方法,然后提取已返回字符串的第8个到倒数第2个位置之间的字符。

  1. Object.prototype.toString.call(target).slice(8, -1);

综上,[[Class]]是一个字符串值,表明了该对象的类型。他是一个内部属性,所有的对象(原生对象和宿主对象)都拥有该属性,且不能被任何人修改。在规范中,[[Class]]是这么定义的:内部属性 描述。

宿主对象也包含有意义的“类属性”,但这和具体的JavaScript实现有关。

因为js中每个类型都有自己私有的[[Class]]属性,而且这个class是不能被任何人修改的,所以tostring方法是检测属性类型最准确的方法,比instanceof还准确。

他也可以细分内置构造函数创建的类对象:

通过内置构造函数(Array、Date等)创建的对象包含“类属性”(class attribute),他与构造函数的名称相匹配(这里也是我从contructor.name来区分数据类型的启发点)。

但是,他无法区分自定义对象类型。

通过对象直接量和Object.create创建的对象的类属性是“object”,那些自定义构造函数创建的对象也是一样。类属性都是“Object”,因此对于自定义类来说,没办法通过类属性来区分对象的类。

为什么用call

这里用call是为了改变toString函数内部的this指向,其实也可以用apply。
之所以必须用是因为,toString内部是获取this指向那个对象的[[Class]]属性值的,如果不改变this指向为我们的目标变量,this将永远指向调用toString的prototype。
另外也是因为,很多对象继承的toString方法重写了,为了能调用正确的toString,才间接的使用call/apply方法。
代码演示:

  1. Object.prototype.toString = function () {
  2. console.log(this);
  3. };
  4. const arr1 = [];
  5. Object.prototype.toString(arr1); // 打印(即this指向) Object.prototype
  6. Object.prototype.toString.call(arr1); // 打印(即this指向) arr1

为什么null也能判断?undefined和null不是没有属性值吗?

因为每一个类型都有自己唯一的特定 类属性(class attribute) 标识,null也有、undefined也有。

该方法的缺陷:

他虽然判断类型完善,但也不是没有缺点,主要有两点:

  1. tostring会进行装箱操作,产生很多临时对象(所以真正进行类型转换时建议配合typeof来区分是对象类型还是基本类型,见最后代码)
  2. 他无法区分自定义对象类型,返回的都是Object(所以针对“自定义类型”可以采用instanceof区分)

    什么是装箱操作?

  • “装箱”就是把基本类型用它们相应的引用类型包装起来,使其具有对象的性质。可以简单理解为“包装类”(详细信息,可参考阅读《JavaScript权威指南-3.6 包装对象》相关解析,这里不再延伸)。
  • 而对应的“拆箱”,就是将引用类型的对象简化成值类型的数据

封装一个用于类型判断的完整工具

虽然toString方法是一个比较完善的方案,但是为了弥补其装箱的缺点,我平时工作中封装了一个工具函数,结合typeof 和 toString 各自的特点,各取其所长,进行变量数据类型的判断:

  1. function classof(o) {
  2. if (o === null) return "null";
  3. if (typeof o !== "object") return typeof o;
  4. else
  5. return Object.prototype.toString
  6. .call(o)
  7. .slice(8, -1)
  8. .toLocaleLowerCase();
  9. }

测试类型校验结果:

  1. classof(2020); // number
  2. classof("石头加油"); // string
  3. classof(true); // boolean
  4. classof(undefined); // undefined
  5. classof(null); // null
  6. classof(Symbol("没毛病!")); // symbol
  7. classof(1n); // bigint
  8. classof({}); // object
  9. classof(classof); // function
  10. classof([]); // array
  11. classof(new Date()); // date
  12. // 还是没法细分自定义类,如果需要的话可以再结合constructor.name继续封装
  13. classof(new classof()); // object

撰写人:小石头