如何准确判断一个对象的类型?

北京字节跳动网络技术有限公司

作为前端的同学,我们应该都知道可以使用 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 Arrayarr 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.0new 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]
复制代码

好了,以上是类型判断相关的几种办法,如果你还有什么想要讨论的,欢迎在评论区评论呀~


欢迎关注「 字节前端 ByteFE 」

简历投递联系邮箱「 tech@bytedance.com

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改