akira-cn/FE_You_dont_know

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

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 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]

好了,以上是类型判断相关的几种办法,如果你还有什么想要讨论的,欢迎在issue中留言。

多realm的环境 指的是什么啊?

多realm的环境 指的是什么啊?

一般指的是一个页面上iframe之间以及iframe和parent之间

多realm的环境 指的是什么啊?

一般指的是一个页面上iframe之间以及iframe和parent之间

thx

不过注意不要使用stringTag判断Number、Boolean等primitive类型,因为它没法区分装箱的类型

文末说的,没法区分装箱的类型,这是什么意思啊?

hax commented

文末说的,没法区分装箱的类型,这是什么意思啊?

@Liugq5713

仔细阅读,文章里已经解释了。

文末说的,没法区分装箱的类型,这是什么意思啊?

@Liugq5713

仔细阅读,文章里已经解释了。

我就是不理解 装箱 这个词

hax commented

装箱就是指把primitive值包装成对象。

参见 https://en.wikipedia.org/wiki/Object_type_(object-oriented_programming)

装箱就是指把primitive值包装成对象。

参见 https://en.wikipedia.org/wiki/Object_type_(object-oriented_programming)

thx ,受教