作为前端的同学,我们应该都知道可以使用 typeof 和 instanceof 在运行时判断 JavaScript 对象的类型。
对于原始类型(primitive type)的数据,大部分可使用 typeof。在 JavaScript 中,primitive 类型包括 Null、Undefined、Boolean、Number、String、Symbol,如果算上 Stage3 的,还有 BigInt:https://github.com/tc39/proposal-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><br>console.log(parent.arr); // 1,2,3<br>console.log(parent.arr instanceof Array); // false<br></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><br>console.log(parent.arr); // 1,2,3<br>console.log(parent.arr instanceof Array); // false<br>console.log(typeof str === 'string'); // true<br></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><br>console.log(Array.isArray(parent.arr)); // true<br></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 这样的属性名,这篇文章(https://jakearchibald.com/2017/arrays-symbols-realms/)使用了 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]
Comments | NOTHING