如何准确判断一个对象的类型?
akira-cn opened this issue · 8 comments
作为前端的同学,我们应该都知道可以使用typeof和instanceof在运行时判断JavaScript对象的类型。
对于原始类型(primitive type)的数据,大部分可使用typeof。在JavaScript中,primitive类型包括Null、Undefined、Boolean、Number、String、Symbol,如果算上Stage3的,还有BigInt。
这里说大部分,是因为除了一个例外,那就是Null。
console.log(undefined); // undefined
console.log(null); // object
console.log(1.0); // number
console.log(true); // boolean
console.log('hello'); // string
console.log(Symbol()); // symbol
console.log(100n); // bigint
上述7种原始类型的数据中,除了typeof null
返回"object"
之外,其他的都返回对应类型名的小写字母。
而typeof null
返回"object"
是因为历史原因,这也不是我们讨论的重点,大家只要记住typeof null === 'object'
这个例外就好。
除了原始类型外,对象返回'object'
,函数返回'function'
。那么我们如果要判断不同类型的对象,就不能用typeof
了:
const arr = [];
const obj = {};
const date = new Date();
const regexp = /a/;
console.log(typeof arr); // object
console.log(typeof obj); // object
console.log(typeof date); // object
console.log(typeof regexp); // object
那么我们判断对象类型的时候,可以使用instanceof:
const arr = [];
const obj = {};
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true
console.log(obj instanceof Array); // false
console.log(obj instanceof Object); // true
💡 注意instanceof是能匹配类型的父类的,所以arr instanceof Array
和arr instanceof Object
都是true,因为Object是Array的父类。
满足class extends
和原型链规则的父子类关系的对象都能被匹配:
class Base {
}
class Current extends Base {
}
const obj = new Current();
console.log(obj instanceof Current); // true
console.log(obj instanceof Base); // true
function Foo() {
}
function Bar() {
}
Bar.prototype = new Foo();
const obj = new Bar();
console.log(obj instanceof Bar); // true
console.log(obj instanceof Foo); // true
注意如果我们修改obj的原型链能改变instanceof
的结果:
function Other() {
}
obj.__proto__ = new Other();
console.log(obj instanceof Other); // true
console.log(obj instanceof Foo); // false
实际上,只要一个类型Type的prototype在一个对象obj的原型链上,那么obj instanceof Type
就是true,否则就是false。
instanceof 的局限性
如果在realm的情况下,比如页面上包含iframe,将当前页面上的对象传给iframe执行,使用instanceof判断就会出问题,我们看一个简单的例子:
var arr = [1, 2, 3];
console.log(arr instanceof Array); // true
var sandbox = document.createElement('iframe');
document.body.append(sandbox);
sandbox.contentDocument.open();
sandbox.contentDocument.write(`<script>
console.log(parent.arr); // 1,2,3
console.log(parent.arr instanceof Array); // false
</script>`);
sandbox.contentDocument.close();
上面的例子里,在当前window中,arr instanceof Array
是true,但是到了sandbox里面,parent.arr instanceof Array
变成false。这是因为,两个Array类型在不同的realm中,实际上要使用:parent.arr instanceof parent.Array
,这样返回的就是true。
而typeof是字符串比较,自然不受此影响:
var arr = [1, 2, 3];
var str = 'hello';
console.log(arr instanceof Array); // true
var sandbox = document.createElement('iframe');
document.body.append(sandbox);
sandbox.contentDocument.open();
sandbox.contentDocument.write(`<script>
console.log(parent.arr); // 1,2,3
console.log(parent.arr instanceof Array); // false
console.log(typeof str === 'string'); // true
</script>`);
sandbox.contentDocument.close();
👉🏻【冷知识】结论:使用instanceof判断的时候,在多realm环境中要小心使用。
用 constructor 判断
有时候我们不希望匹配父类型,只希望匹配当前类型,那么我们可以用constructor来判断:
const arr = [];
console.log(arr.constructor === Array); // true
console.log(arr.constructor === Object); // false
当然和instanceof的问题一样,遇到多realm的环境,constructor判断要确保类型是和判断的对象在同一个realm下。不过我们如果想匹配不同realm,在一些特殊情况下,我们可以使用constructor的只读属性name:
parent.arr.constructor.name === 'Array'
👉🏻对象的constructor会返回它的类型,而类型在定义的时候,会创建一个name只读属性,值为类型的名字。
class Foo {
}
console.log(Foo.name); // Foo
const foo = new Foo();
console.log(foo.constructor === Foo); // true
console.log(foo.constructor.name === 'Foo'); // true
不过使用constructor.name有非常大的限制,如果使用定义匿名的class,那么name就变成空的:
const MyClass = (function() {
return class {
}
}());
console.log(MyClass.name); // ''
另外如果使用es-modules,我们import的类名不一定是包里面的类名。
再者,如果我们使用脚本压缩工具,那么文件中的类名会被替换为短名,那样的话,name属性的名字也随着改变了。
所以依赖constructor.name来判断不是一个好的方案。
Array.isArray
如果我们只是针对数组来判断,那么我们可以使用Array.isArray。
这个方法能够判断一个对象是否是一个Array类型或者其派生类型。
class MyArray extends Array {}
const arr1 = [];
const arr2 = new MyArray();
console.log(Array.isArray(arr1), Array.isArray(arr2)); // true, true
Array.isArray在多realm中能正常判断:
var arr = [1, 2, 3];
var sandbox = document.createElement('iframe');
document.body.append(sandbox);
sandbox.contentDocument.open();
sandbox.contentDocument.write(`<script>
console.log(Array.isArray(parent.arr)); // true
</script>`);
sandbox.contentDocument.close();
Array.isArray 给我们带来启发,既然在多realm环境中,使用instanceof不安全,那么我们可以构造类似Array.isArray的方法来实现我们自己的isType方法。
class Foo {
static isFoo(obj) {
// ...
}
}
那么我们需要给予类型的实例一个标志,以使得我们能够根据这一标志来判断:
class Foo {
static isFoo(obj) {
return !!obj.isFooInstanceTag;
}
get isFooInstanceTag() {
return true;
}
}
为了避免暴露isFooInstanceTag这样的属性名,这篇文章使用了Symbol.for,这样更好:
const instanceTag = Symbol.for('check_is_Foo_instance_tag');
class Foo {
static isFoo(obj) {
return !!obj[instanceTag];
}
get [instanceTag]() {
return true;
}
}
注意这里必须使用Symbol.for
而不能直接使用Symbol
,因为在不同的realm下,同样key的Symbol.for
返回的是相同ID。
stringTag
如果你看过一些库的早期实现,你会发现使用Object.prototype.toString来做类型判断的方式:
var ostring = Object.prototype.toString;
function isArray(it) {
return ostring.call(it) === '[object Array]';
}
比如这是requirejs里面的代码片段。
在早期的JS中,不支持Array.isArray时,很多库是利用这个方法来判断数组的,同样我们还可以判断其他类型:
const ostring = Object.prototype.toString;
console.log(ostring.call(/a/)); // [object RegExp]
console.log(ostring.call(new Date())); // [object Date]
不过注意不要使用stringTag判断Number、Boolean等primitive类型,因为它没法区分装箱的类型:
const ostring = Object.prototype.toString;
console.log(ostring.call(1.0)); // [object Number]
console.log(ostring.call(new Number(1.0))); // [object Number]
像上面的代码,1.0
和new Number(1.0)
的stringTag都返回[object Number]
,但是我们一般认为1.0和new Number(1.0)
是两个不同的类型。
在ES2015之前,我们不能自定义类型的stringTag,我们自己定义的任何类型实例的stringTag都返回[object Object]
。
👉🏻 但是现在,我们可以通过实现Symbol.toStringTag
的getter来自定义类型的stringTag:
class Foo {
get [Symbol.toStringTag]() {
return 'Foo';
}
}
const foo = new Foo();
console.log(Object.prototype.toString.call(f)); // [object Foo]
好了,以上是类型判断相关的几种办法,如果你还有什么想要讨论的,欢迎在issue中留言。
多realm的环境 指的是什么啊?
多realm的环境 指的是什么啊?
一般指的是一个页面上iframe之间以及iframe和parent之间
多realm的环境 指的是什么啊?
一般指的是一个页面上iframe之间以及iframe和parent之间
thx
不过注意不要使用stringTag判断Number、Boolean等primitive类型,因为它没法区分装箱的类型
文末说的,没法区分装箱的类型,这是什么意思啊?
文末说的,没法区分装箱的类型,这是什么意思啊?
仔细阅读,文章里已经解释了。
我就是不理解 装箱 这个词
装箱就是指把primitive值包装成对象。
参见 https://en.wikipedia.org/wiki/Object_type_(object-oriented_programming)
装箱就是指把primitive值包装成对象。
参见 https://en.wikipedia.org/wiki/Object_type_(object-oriented_programming)
thx ,受教