看到一篇文章Arrays, symbols, and realms,简单搜了一下,关于判断数组的文章基本上从 typeof
、instanceof
、Object.prototype.toString.call(obj) === '[objectg Array]''
讲到 isArray
就完了。但是它实际上干了什么?
正文
在推上,一个大佬问兄弟们,大家知不知道 Array.isArray(obj) 干了什么,结果显示,并不知道。更重要的是,本大佬也弄错了。
检查 arrays 的类型
function foo(obj){
// ...
}
假如说我们需要针对 obj
是数组的情况,来专门做一些操作,比如 JSON.stringify
,它对于数组的输出,是跟其他对象不同的。
我们可以这样:
if (obj.constructor === Array) // ...
但对于继承于 Array 的数据来说,就不行了:
class SpecialArray extends Array {}
const specialArray = new SpecialArray();
console.log(specialArray.constructor === Array); // false
console.log(specialArray.constructor === SpecialArray); // true
针对子类我们又可以用 instanceof
:
console.log(specialArray instanceof Array); // true
console.log(specialArray instanceof SpecialArray); // true
但是当你遇到多个领域的时候就不好使了:
多个领域
一个领域包含了 JavaScript 的全局对象,self
指向的那个。可以这样讲:跑在 worker 里面的代码,和跑在页面里面的代码,是属于不同领域的。iframe 之间也是这样(属于不同领域)。但是,同源的 iframe 们共享一个 ECMAScript agent
[啥意思?],意味着对象这个东西可以在不同领域之间传递:
<iframe srcdoc="<script>var arr = [];</script>"></iframe>
<script>
const iframe = document.querySelector('iframe');
const arr = iframe.contentWindow.arr;
console.log(arr.constructor === Array); // false
console.log(arr.constructor instanceof Array); // false
</script>
都会返回 false
,因为:
console.log(Array === iframe.contentWindow.Array); // false
iframe 会有各自不同的 array 构造器...
接下来说 Array.isArray
console.log(Array.isArray(arr)); // true
Array.isArray
对在另一个领域里面创建的数组也会返回 true
。它对Array
的子类也会返回 true
,JSON.stringify
内部用的就是它。
但是,就像推上那位大佬说的,这不代表 arr
本身具有数组的方法。所有的方法们可能都被设成了 undefined
,甚至 arr
的 prototype
可能都没了。
const noProtoArray = [];
Object.setPrototypeOf(noProtoArray, null);
console.log(noProtoArray.map); // undefined
console.log(noProtoArray instanceof Array); // false
console.log(Array.isArray(noProtoArray)); // true
下面是你遇见这种数组了怎么办..
if (Array.isArray(noProtoArray)) {
const mappedArray = Array.prototype.map.call(noProtoArray, callback);
// …
}
Symbol 和 realm (领域)
看看这段代码:
<iframe srcdoc="<script>var arr = [1, 2, 3];</script>"></iframe>
<script>
const iframe = document.querySelector('iframe');
const arr = iframe.contentWindow.arr;
for (const item of arr) {
console.log(item);
}
</script>
地球人都知道会 log 出来 1,2,3。但这说明了啥?
for-of 循环内部使用的是 arr[Symbol.iterator]
,这是跨领域之后还可以通用的。看:
const iframe = document.querySelector('iframe');
const iframeWindow = iframe.contentWindow;
console.log(Symbol === iframeWindow.Symbol); // false
console.log(Symbol.iterator === iframeWindow.Symbol.iterator); // true
每个领域有它自己的 Symbol 实例的同时,他们的 Symbol.iterator
却是一样的。
引用一句话,Symbol
同时是 JavaScript 里最特殊和最不特殊的东西了。
最特殊
const symbolOne = Symbol('foo');
const symbolTwo = Symbol('foo');
console.log(symbolOne === symbolTwo); // false
const obj = {};
obj[symbolOne] = 'hello';
console.log(obj[symbolTwo]); // undefined
console.log(obj[symbolOne]); // 'hello'
传给 Symbol
构造器的字符串只是对它的一个描述,同一个领域下面同一个描述的 symbol 也是独特的。
最不特殊
const symbolOne = Symbol.for('foo');
const symbolTwo = Symbol.for('foo');
console.log(symbolOne === symbolTwo); // true
const obj = {};
obj[symbolOne] = 'hello';
console.log(obj[symbolTwo]); // 'hello'
Symbol.for(str)
新建了一个根据你字符串唯一的 symbol 对象。重点是,跨了领域之后,它还是一样的。。
const iframe = document.querySelector('iframe');
const iframeWindow = iframe.contentWindow;
console.log(Symbol.for('foo') === iframeWindow.Symbol.for('foo')); // true
所以这个也就基本上是 Symbol.iterator
为啥能行的原因了。
根据传统,自己手写个 is
函数
那么 symbol 对象可以让我们写出跨领域能用 is
方法:
const typeSymbol = Symbol.for('whatever-type-symbol');
class Whatever {
static isWhatever(obj) {
return obj && Boolean(obj[typeSymbol]);
}
constructor() {
this[typeSymbol] = true;
}
// 或者下面这种不会被遍历出来
get [typeSymbol]() {
return true;
}
}
const whatever = new Whatever();
Whatever.isWhatever(whatever); // true
这样唯一的一点不太好的地方,就是命名冲突了。别人也这么搞,而且还同名的话,就有问题了。