最近我了解到了包括typeof
、instanceof
、Object.prototype.toString()
等在内的类型判断方法。我将在这里深入探究这些类型检测方法的工作原理,以及如何自定义实现instanceof
逻辑,以加深对JavaScript类型系统的了解。
JavaScript的内存布局与类型存储
首先,理解JavaScript的内存布局是基础。原始类型(如字符串、数字、布尔值等)因数据量小,直接存储在调用栈中,便于快速访问和管理。而复杂类型(如对象、数组)因其潜在的复杂性和大小,被存放在堆内存中,栈中仅保存一个指向堆中数据的引用地址。这样的设计既避免了栈空间有限导致的“栈溢出”风险,又能灵活处理大数据结构。
typeof操作符的深入分析
让我们来看看下面这段例子
let str = "Hello" // string
let num = 123 // number
let flag = false // boolean
let un = undefined // undefined
let nu = null // object
let symbol = Symbol() // symbol
let bigInt = BigInt(123)// bigint
let obj ={} // object
let arr = [] // object
let fn = function() {} // function
let reg = /^abc$/ // object
let date = new Date() // object
let map = new Map() // object
let set = new Set() // object
后面的注释是输出的结果
typeof
操作符在检测原始类型时表现良好,但对引用类型,除了函数外,它一律返回"object"
。这源于typeof
通过检查值的内部表示,对于非函数引用类型,其二进制表示前三位为0,导致无法精确区分具体类型。特别地,它错误地将null
识别为"object"
,这是typeof
的一个已知缺陷。
以上可以总结为三句话:
- 可以判断除null之外的原始类型
- 无法判断function之外的引用类型
- typeof的判断原理是:将值转换为二进制后看前三位是不是0,除函数的所有引用类型的二进制前三位都是0,null转换为二进制全是0
instanceof的精髓:原型链的探索
下面为instanceof使用的示例
// 基本类型
let str = "Hello"
let num = 123
let flag = false
let un = undefined
let nu = null
let symbol = Symbol()
let bigInt = BigInt(123)
// console.log(str instanceof String); // false
// console.log(num instanceof Number); // false
// console.log(flag instanceof Boolean); // false
// console.log(un instanceof Undefined); // 报错
// console.log(nu instanceof Null); // 报错
// 引用类型
let obj ={}
let arr = []
let fn = function() {}
let reg = /^abc$/
let date = new Date()
let map = new Map()
let set = new Set()
// console.log(obj instanceof Object); // true
// console.log(arr instanceof Array); // true
// console.log(fn instanceof Function); // true
// console.log(reg instanceof RegExp); // true
// console.log(date instanceof Date); // true
// console.log(map instanceof Map); // true
// console.log(set instanceof Set); // true
// console.log(arr instanceof Object); // true
欸,发现了一个特殊的例子
// console.log(arr instanceof Object); // true
使用instanceof把数组判断为Object返回的也是true。那这是怎么回事呢,那我们就需要了解intanceof方法的实现原理
其实啊
instanceof
是通过原型链的追溯,说到原型链,不知道大家还记得我在 JavaScript原型探秘:构建对象与继承的艺术中的概述吗,实例对象的隐式原型会等于构造函数的显示原型,所以arr.__proto__
=Array.prototype
,并且Array是通过Object来创建的,所以Array.__proto__
=Object.prototype
,找到了Object所以返回true。如果没找到,会跟原型链一样层层往上找,找到null为止。
使用instanceof这个方法来判断一个对象是否为某个构造函数的实例,这对于区分复杂的引用类型非常有用,但仅限于引用类型。
手写instanceof
根据上面的描述,我们知道了instanceof方法的工作原理,那么我们来根据这些原理来仿写一个myInstanceof方法
/**
* while循环,直到原型为null或找到R.prototype
* @param {Object} L 需要判断的对象
* @param {Function} R 构造函数
* @return {Boolean} 返回布尔值
*/
function myInstanceof(L, R) {
while(L !== null) {
if(L.__proto__ === R.prototype) return true;
L = L.__proto__;
}
return false;
}
console.log(myInstanceof(new Array(), Array)); // true
console.log(myInstanceof(new Array(), Object)); // true
console.log(myInstanceof(1, Object)); // true
instanceof
通过检查对象的原型链来判断其是否为某构造函数的实例。自定义的myInstanceof
函数通过迭代对象的原型链直至null
,寻找是否有构造函数的原型,复现了instanceof
的行为,展示了如何利用这一机制进行类型判断。也可以使用递归来实现
Object.prototype.toString()的全面性
let a = {};
let b = [];
let c = "hello";
console.log(Object.prototype.toString.call(a)); // [object Object]
console.log(Object.prototype.toString.call(b)); // [object Array]
console.log(Object.prototype.toString.call(c)); // [object String]
Object.prototype.toString()
通过直接访问对象的内部[[Class]]
属性,提供了最准确的类型信息。通过.call()
方法确保方法作用于正确的对象,它能够返回如"[object Array]"
这样精确的类型字符串。这种方法克服了typeof
的局限性,能够准确区分所有内置类型,包括数组、正则表达式等。
那么,我们可以把Object.prototype.toString()方法的实现原理总结为如下五步:
- 如果toString接收的值为 undefinded,则返回“ [object Undefined] ”。
- 如果toString接收的值为 null,则返回“ [object Null] ”。
- 调用ToObject(x) 将x转为对象,此时得到的对象内部一定拥有一个属性[[class]],而这个属性[[class]]的值就是x的类型。
- 设 class 是[[class]]的值
- 返回由“[ object” 和 class 和 “]”拼接的字符串
toString()与Object.prototype.toString()的对比
console.log([1, 2, 3].toString()); // "1,2,3"
console.log(({}).toString()); // "[object Object]"
标准的toString()
方法在不同对象上有不同的表现:对于数组,它将数组元素转换为字符串并用逗号连接;对于普通对象,它返回"[object Object]"
。而Object.prototype.toString()
提供了更全面的信息,是进行类型检查时的首选。
toString方法的使用可以总结为:
- 对象的toString(): Object.prototype.toString
- 数组的toString(): 将数组中的元素用逗号的方式拼接成字符串
- 其他的toString(): 直接将值修改成字符串字面量,也就是直接加个引号
实现一个全面的数据类型判断方法
// 打造一个type函数,能够判断参数的类型 使用Object.prototype.toString() 方法
function type(x) {
return Object.prototype.toString.call(x).slice(8, -1);
}
console.log(type(5));
console.log(type("hello"));
这个 type
函数通过利用 Object.prototype.toString.call()
的能力,提供了一种强大的类型检测机制,能够准确识别并返回JavaScript中各种数据类型的名称,利用数组的slice方法把需要的内容分离出来,使方法更直观
Array.isArray()的独特性
let arr = [];
console.log(Array.isArray(arr)); // true
Array.isArray()
是一个专门用于检测数组的工具,它直接、明确,避免了使用instanceof
或typeof
可能带来的误判。值得注意的是,它只能作为构造函数调用,体现了其针对性和准确性。
结论
综上所述,JavaScript的类型检测与内存管理机制是编程中不可或缺的知识点。通过理解typeof
、instanceof
、Object.prototype.toString()
以及Array.isArray()
的工作原理和适用场景,我们能够更精准地处理各种数据类型,写出更高效、健壮的代码。自定义实现如myInstanceof
不仅增强了对原型链和类型判断的深入理解,也为解决特定问题提供了灵活性。