「备战金三银四」你搞懂JS数据类型判断了吗?

257 阅读9分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

0. 前言📰

众所周知,JavaScript 作为一门弱类型(weakly typed),动态类型(dynamically typed)的编程语言,在一定程度上提升了开发者的编码效率,但同时类型易于变动的隐患也增加了项目维护的成本,所以在没有类型检查机制的前提下,数据类型判断便显得很有必要啦😎。

虽然 JavaScript 数据类型判断作为一个老生常谈的问题,但你知道为什么 typeof null 的结果是 "object" 吗?你能手写实现 instanceof 运算符吗,为什么 Object.prototype.toString.call() 一定要 call() 📞呢?

🧐面对即将到来的金三银四,相信不少人和笔者一样都在紧张努力地备战着。最近在重拾基础的过程中,发现以往囫囵吞枣式的学习收效甚微,所以便总结了这篇文章,如果你也对上述问题不太熟悉,那么推荐你看完这篇笔记,欢迎大家一起交流学习。

本文从 3 种常用的数据类型判断方法出发,通过介绍各方法的细节来对上述的问题进行逐一解答,并对面试常考的类型判断方法进行简单的手写实现。

🚗接下来便是本文的正式内容。

1. 常用数据类型与判断方法🔎

JavaScript 的数据类型分为两种,即原始类型(即基本数据类型)和对象类型(即引用数据类型)。

基本数据类型目前包括 7 种,分别为:NumberStringBooleanNullUndefinedSymbolBigint,而引用数据类型常见的有 ObjectArrayFunctionDate等。

判断 JavaScript 类型,比较常用的有以下几种方法:

  • typeof 操作符
  • instanceof 运算符
  • Object.prototype.toString.call() 方法

typeof 操作符

typeof 操作符返回一个字符串,表示未经计算的操作数的类型。

console.log(typeof 1024);
// "number"

console.log(typeof 'hello');
// "string"

console.log(typeof true);
// "boolean"

下表总结了 typeof 操作符可能的返回值。

类型结果
Undefined"undefined"
Null"object"
Boolean"boolean"
Number"number"
String"string"
Symbol (ECMAScript 2015 新增)"symbol"
BigInt(ECMAScript 2020 新增)"bigint"
Function 对象"function"
其他任何对象"object"

可见,在 JavaScript 中的常见7种基本数据类型中,仅有 Null 无法利用 typeof 操作符正确地得出结果。这是 JavaScript历史遗留缺陷🕳,简而言之, Null 值用来表示一个空对象指针,而其 TYPE TAG 恰好与 Object 的相同,故最终体现的结果为 "object" ,与预期不符,详见:The history of “typeof null”

另外,对于引用数据类型typeof 操作符仅可正确判断 Function 类型,其余的引用类型一律返回 "object"

📌值得注意的是:

typeof null 返回的是 "object"

typeof NaN 返回的是 "number"

typeof 操作符优先级高于四则运算,所以 typeof 1/0 的解析过程将会是:先判断数字 1 的类型得到一个字符串结果 "number" ,再将其进行 Number 隐式类型转换后与 0 相除,得到的最终结果是 NaN,该结果并非由 typeof 操作符得出。

综上, typeof 操作符是一个比较好的基本数据类型判断工具,但使用时需要额外注意 Null 值判断返回结果的历史遗留问题。而在引用数据类型的判断上, typeof 操作符便显得有些捉襟见肘,对此,我们可以采用另外一种方式:使用 instanceof() 方法进行引用数据类型判断。

instanceof 运算符

instanceof 运算符用于检测构造函数的 prototype 属性(即 constructor 参数)是否出现在某个实例对象(即 object 参数)的原型链上,返回结果是一个布尔值,其用法为 object instanceof constructor

// 定义构造函数
function C(){}
function D(){}

var o = new C(); // o 由构造函数 C 直接生成

o instanceof C; // true

o instanceof D; // false

o instanceof Object; // true,因为 o 由构造函数 Object 间接生成(原型链)

📌值得注意的是:

instanceof 运算符无法正确处理多全局对象的类型判断。在多个 frame 与多个 window 之间的交互中:

// 为 body 创建并添加一个 iframe 对象
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
xArray = window.frames[0].Array;   // 取得iframe对象的构造数组方法
const arr = new xArray(1, 2, 3);   // 通过构造函数获取一个数组实例
console.log(arr instanceof Array); // false

// 因为 Array.prototype !== window.frames[0].Array.prototype
// 此例中,建议采用 Array.isArray()方法 用于确定传递的值是否是一个数组

instanceof 运算符无法正确处理基本数据类型的类型判断,比如:

var simpleStr = "Hello";	// 基本数据类型 String
simpleStr instanceof String;    // 返回 false, 非对象实例,因此返回 false

instanceof 运算符优先级低于逻辑运算符,故判断非类型时,需要注意加上括号:

if (!(mycar instanceof Car)) {
      // 正确
}

if(!mycar instanceof Car){
      // 错误,实际上该 if 语句判断的是一个布尔值是否是 Car 的一个实例
}

④ 当 instanceof 运算符的前后参数是两种构造函数时,返回结果应该遵循原型继承机制。

在下例 Object instanceof Object 判断中,根据 instanceof 运算符的逻辑,首先判断的是第一个 Object__proto__ 属性是否为第二个 Object 的原型对象,而常用Array()Object()Function()Number() 这些内置的构造函数的__proto__ 属性都是指向了一个函数:Function.prototype注意 typeof Function.prototype 的返回结果为 "function"

所以第一次判断结果为 false,但 instanceof 运算符仍会沿着原型链继续往上查找,故下一轮会检查第一个 Object 原型对象(函数)的原型对象,即 Function.prototype.__proto__是否为 Object.prototype ,所以在第二轮检查中返回 true

console.log(Object instanceof Object); // true Object.__proto__.prototype.__proto__ == Object.prototype
console.log(Function instanceof Function); // true Function.__proto__ ==  Function.prototype
console.log(Number instanceof Number); // false  Function.prototype(以及向上查找的原型对象) != Number.prototype
console.log(String instanceof String); // false  以此类推
console.log(Function instanceof Object); // true
console.log(Foo instanceof Function); // true
console.log(Foo instanceof Foo); // false

综上,instanceof 运算符解决了 typeof 的不足,是一个比较好的引用数据类型判断工具。但其主要的缺点也很明显:无法判断基本数据类型(比如常见的 nullundefined 判断)。

那么有没有一种好的方法,可以正确检测基本数据类型和引用数据类型呢,答案是: Object.prototype.toString.call() 方法。

Object.prototype.toString.call() 方法

JavaScript 中,所有引用类型都继承自 Object,故每个对象都享有 Object.prototype.toString() 方法。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中 type 是对象的类型,所以我们可以通过该方法来进行数据类型判断。

var o = new Object();
o.toString(); // 返回 [object Object]

但在 ArrayFunction 等引用类型中都重写了 toString() 方法,根据原型链的遮蔽效应,在访问这些引用类型的 toString() 方法时,会优先采用重写后的方法。

var arr = new Array(1,2,3);
arr.toString(); // "1,2,3"  该方法重写为输出代表数组元素集合的字符串

所以为了每个对象都能通过 Object.prototype.toString() 来检测,我们需要使用 call() 方法来指明该方法的调用者 this

// 基本数据类型判断
Object.prototype.toString.call(null); // “[object Null]”
Object.prototype.toString.call(undefined); // “[object Undefined]”
Object.prototype.toString.call(123);// “[object Number]”

// 引用数据类型判断
// 函数类型
function fn(){
  console.log("test");
}
Object.prototype.toString.call(fn); // "[object Function]"
 
// 日期类型
var date = new Date();
Object.prototype.toString.call(date); // "[object Date]"
 
// 数组类型
var arr = [1,2,3];
Object.prototype.toString.call(arr); // "[object Array]"

综上可见,该方法在数据类型判断上,综合了前述介绍的两种方法的优点,但仍存在部分缺点,如下代码所示:

var p = new Person("Rose", 18);
console.log(Object.prototype.toString.call(p)); // 此时输出[object Object]
console.log (p instanceof Person); // true
// 不能判断 p 是 Person 类的实例还是原生的 Object 类型。

所以在一些需要判断到具体类型的应用场合中,建议使用 instanceof 操作符进行数据类型判断。

2. 手写实现✍

在前端面试中,手写数据类型判断是较为常考的题目,常见题型为:

  • 手写实现 typeof
  • 手写实现 instanceof

手写实现 typeof

基于前文对于 Object.prototype.toString.call() 方法的介绍,我们可以利用该方法实现一个自定义的“进阶版” typeof

// 调用 Object.prototype.toString.call() 方法并对结果进行截取与小写转换
function typeOf(obj) {
	return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()
}

typeOf([])        // 'array'
typeOf(null)      // 'null'
typeOf({})        // 'object'
typeOf(new Date)  // 'date'

手写实现 instanceof

基于前文对于 instanceof 运算符的介绍,其核心逻辑便是:重复地获取对象 left 的原型对象,然后比较该原型对象和 rightprototype 属性是否相等,直到相等返回 true,或者 left 变为 null,也就是遍历完整个原型链,返回 false

明确其思路后,便可写出如下代码:

// left instanceof right
// 即判断 right.prototype 是否在 left 实例的原型链上

function instanceOf(left, right) {
    // 实际上可以做得更多:对入参进行进一步校验,如是否为引用数据类型或者是否为空。 
    // 在此只是对核心逻辑的简易实现
    
    let proto = left.__proto__     // 取出实例的原型对象
    while (true) {
        if (proto === null) return false 	// 找到最顶层都未能与 right.prototype 匹配,返回 false
        if (proto === right.prototype) {
            return true
        }
        proto = proto.__proto__   // 不符合条件便继续往上一层查找
    }
}

instanceOf(Object, Object) // true
instanceOf(Number, Number) // false

3. 总结💬

针对常用的 3 种数据类型判断方法的优缺点,可以总结得出下表:

typeof 操作符instanceof 运算符Object.prototype.toString.call() 方法
优点可识别主要的基本数据类型可识别引用数据类型准确检测所有类型
缺点不能正确识别 null 与具体的引用数据类型不能判断基本数据类型、无法正确处理多全局对象的类型判断IE6 以下无法正确检测 nullundefined、无法判断某实例具体由哪种自定义类生成

在日常开发中,可以根据具体应用场合进行类型判断方法的选取,适合的才是最好的。

而对于 typeofinstanceof 的手写实现,明确其核心逻辑便可轻松掌握,朋友们也可在学有余力的情况下思考怎么正确处理传入的参数。

需要补充说明的是,😿文中提到的原型继承机制与本次文章主题不太相关,所以没有占用太多篇幅,笔者打算在接下来的文章中再进行详细地解读。

这是我的第一篇博客文章,很感谢你可以看到这里,也期待你可以留下一个印记👍

若文章出现了纰漏或者你有更好的建议,欢迎评论区留言或是私信指出✨

参考资料📜

MDN: typeof

MDN: instanceof

死磕 36 个 JS 手写题(搞懂后,提升真的大)