Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
akira-cn opened this issue Aug 5, 2019 · 8 comments
Open

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

akira-cn opened this issue Aug 5, 2019 · 8 comments

Comments

@akira-cn
Copy link
Owner

akira-cn commented Aug 5, 2019

作为前端的同学,我们应该都知道可以使用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中留言。

@Liugq5713
Copy link

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

@akira-cn
Copy link
Owner Author

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

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

@Liugq5713
Copy link

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

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

thx

@Liugq5713
Copy link

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

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

@hax
Copy link

hax commented Sep 21, 2019

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

@Liugq5713

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

@Liugq5713
Copy link

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

@Liugq5713

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

我就是不理解 装箱 这个词

@hax
Copy link

hax commented Sep 22, 2019

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

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

@Liugq5713
Copy link

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

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

thx ,受教

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants