「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」
0. 前言📰
众所周知,JavaScript
作为一门弱类型(weakly typed),动态类型(dynamically typed)的编程语言,在一定程度上提升了开发者的编码效率,但同时类型易于变动的隐患也增加了项目维护的成本,所以在没有类型检查机制的前提下,数据类型判断便显得很有必要啦😎。
虽然 JavaScript
数据类型判断作为一个老生常谈的问题,但你知道为什么 typeof null
的结果是 "object"
吗?你能手写实现 instanceof
运算符吗,为什么 Object.prototype.toString.call()
一定要 call()
📞呢?
🧐面对即将到来的金三银四,相信不少人和笔者一样都在紧张努力地备战着。最近在重拾基础的过程中,发现以往囫囵吞枣式的学习收效甚微,所以便总结了这篇文章,如果你也对上述问题不太熟悉,那么推荐你看完这篇笔记,欢迎大家一起交流学习。
本文从 3
种常用的数据类型判断方法出发,通过介绍各方法的细节来对上述的问题进行逐一解答,并对面试常考的类型判断方法进行简单的手写实现。
🚗接下来便是本文的正式内容。
1. 常用数据类型与判断方法🔎
JavaScript
的数据类型分为两种,即原始类型(即基本数据类型)和对象类型(即引用数据类型)。
基本数据类型目前包括 7
种,分别为:Number
、String
、Boolean
、Null
、Undefined
、Symbol
、Bigint
,而引用数据类型常见的有 Object
、Array
、Function
、Date
等。
判断 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
的不足,是一个比较好的引用数据类型判断工具。但其主要的缺点也很明显:无法判断基本数据类型(比如常见的 null
和 undefined
判断)。
那么有没有一种好的方法,可以正确检测基本数据类型和引用数据类型呢,答案是: 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]
但在 Array
,Function
等引用类型中都重写了 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
的原型对象,然后比较该原型对象和 right
的 prototype
属性是否相等,直到相等返回 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 以下无法正确检测 null 与 undefined、无法判断某实例具体由哪种自定义类生成 |
在日常开发中,可以根据具体应用场合进行类型判断方法的选取,适合的才是最好的。
而对于 typeof
与 instanceof
的手写实现,明确其核心逻辑便可轻松掌握,朋友们也可在学有余力的情况下思考怎么正确处理传入的参数。
需要补充说明的是,😿文中提到的原型继承机制与本次文章主题不太相关,所以没有占用太多篇幅,笔者打算在接下来的文章中再进行详细地解读。
这是我的第一篇博客文章,很感谢你可以看到这里,也期待你可以留下一个印记👍
若文章出现了纰漏或者你有更好的建议,欢迎评论区留言或是私信指出✨