本文是中文翻译,英文原文链接 jakearchibald.com/...
Allen Wirfs-Brock 在 Twitter上没有查找到Array.isArray(obj)的原理,并且找到了错误的答案。
数组类型检查
function foo(obj) {
// …
}
假设obj是一个数组,并且对其做些特殊处理。举例JSON.stringify对数组做了特殊处理。
我们可以这样做:
if (obj.constructor == 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
但是当引入多realms时事情就变得复杂了。
Multiple realms
realm 包含JavaScript全局对象,self引用的它,因此可以说程序在不同的页面中运行在不同的realm 里。在iframe中也是这样的,在同源iframe中共享 ECMAScript agent,这意味着对象可以在跨realm传播。
认真的看:
<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.isArray
console.log(Array.isArray(arr)); // true
Array.isArray判断数组时会一直返回true,即使是在其他realm创建的数组,它一直返回true无论是派生的数组还是其他realm中。这就是JSON.stringify使用的。
但是,正如 Allen 所揭示的,这并不意味这arr有任何数组的方法。一些或者所有方法都可能被设置为undefined,甚至删除掉数组的整个原型:
const noProtoArray = [];
Object.setPrototypeOf(noProtoArray, null);
console.log(noProtoArray.map); // undefined
console.log(noProtoArray instanceof Array); // false
console.log(Array.isArray(noProtoArray)); // true
这就是我在 Allen 的投票中弄错的地方,我选择了’it has Array methods‘,这是选择最少的答案。所以现在感觉挺时髦的。
总之,你想防御上述问题,你可以从数组原型上使用数组方法:
if (Array.isArray(noProtoArray)) {
const mappedArray = Array.prototype.map.call(noProtoArray, callback);
// …
}
Symbols and realms
看看这个:
<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>
上面会打印出1、2、3。但是for-of循环工作时会调用arr[Symbol.iterator],这是它可以跨realm工作的原因:
const iframe = document.querySelector('iframe');
const iframeWindow = iframe.contentWindow;
console.log(Symbol === iframeWindow.Symbol); // false
console.log(Symbol.iterator === iframeWindow.Symbol.iterator); // true
虽然每个realm都有自己的Symbol对象,但是Symblo.iterator在不同的realm都是相同的。
借用 Keith Cirkel 的话:Symbols是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函数中的string参数仅仅是个描述,即使在相同的realm中这些symbols都是唯一的。
最不独特的
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和你传递的字符串绑定。有趣的是在不同的realm中它得到的也是一样的。
const iframe = document.querySelector('iframe');
const iframeWindow = iframe.contentWindow;
console.log(Symbol.for('foo') === iframeWindow.Symbol.for('foo')); // true
这就可以解释Symbol.iterator是怎么工作的。
创建自己的is函数
如果我们想创建一个可以在不同realm工作的is函数,可以通过Symbol做到。
const typeSymbol = Symbol.for('whatever-type-symbol');
class Whatever {
static isWhatever(obj) {
return obj && Boolean(obj[typeSymbol]);
}
constructor() {
this[typeSymbol] = true;
}
}
const whatever = new Whatever();
Whatever.isWhatever(whatever); // true
来自其他realm的实例、该实例的子类和删除原型的实例都可以使用这个方法。
唯一的小问题是你要手动保证Symbol名称是在所有代码中是唯一的。如果有别人创建了Symbol.for('whatever-type-symbol') 并赋予了其他含义,则isWhatever可能会报错。
(原文地址 juejin.cn/post/686... ,转载需经过作者同意!)