(a == 1 && a == 2 && a == 3) === true - 等等,稍等……
严格平等
包起来
你们中的一些人可能已经认出标题中的问题了。这是一个著名的wtfJS示例,Brandon Morelli 在 2018 年对此进行了非常详细的解释。
代码示例如下:
if (a == 1 && a == 2 && a == 3) {
console.log("What?!");
}
// logs: What?!
为什么它能奏效?诀窍在于意识到a这里并非原始类型,而是一个带有 getter 方法的对象——它伪装成原始类型。
那么,当我们尝试比较对象和基本类型时会发生什么呢?如果我们查看规范,会发现(根据规则 8 和 9)我们会尝试将对象强制转换为基本类型。如何转换呢?通过规范ToPrimitive中定义的函数。
简而言之,它会尝试将对象强制转换为数字。如果转换失败,它会尝试将其强制转换为字符串。让我们尝试将一个对象强制转换为字符串和数字。
const num = Number({});
console.log(num); // NaN
const str = String({});
console.log(str); // [object Object]
好吧,这两种方法都无济于事。那么,究竟是如何胁迫他们的呢?
根据规范,它会调用函数.valueOf来获取数字和.toString字符串。如果.valueOf返回一个对象,则会继续执行后续操作.toString。如果.toString未返回原始类型,则会抛出错误:Uncaught TypeError: Cannot convert object to primitive value。
我们可以像这样自行覆盖它们:
const a = {
valueOf() {
return 55;
},
toString() {
return 100;
}
};
if (55 == a) console.log("we got valueOf()!");
if (100 == a) console.log("we got toString()!");
// logs: we got valueOf()!
// returning an object, so it will be skipped
a.valueOf = function() { return {} };
if (55 == a) console.log("we got valueOf()!");
if (100 == a) console.log("we got toString()!");
// logs: we got toString()!
你看,我们其实都不需要返回字符串或数字。
那么我们如何利用这一点来解决我们的问题呢?我们让其中一个获取器返回一个值,然后递增它。
const a = {
val: 0,
valueOf() {
this.val++;
console.log("value incremented!");
return this.val;
}
};
if (a == 1 && a == 2 && a == 3) {
console.log("We got it!");
}
// logs:
// value incremented!
// value incremented!
// value incremented!
// We got it!
我们可以对该类做Proxy类似的事情,但利用的是相同的概念。
const a = new Proxy({ value: 1 }, {
get(obj, prop) {
if (prop !== 'valueOf') return obj[prop];
return () => obj.value++;
}
})
Proxy本文就不赘述了,因为Keith Cirkel在这里写了一篇更好的文章。
本质上,我们定义了一个带有 getter“陷阱”的新对象,该 getter 返回当前值属性,并在.valueOf()调用其方法时递增该值。这只是用一种更复杂的方式实现了我们之前用更简单的方法实现的功能。
无论如何,使用严格相等是否不可能实现这一点?如果我们面对同样的例子,但使用了三个等号呢?
严格平等
其实,这并非不可能。但首先,我们必须先搞清楚一些基本概念。
首先是window对象本身。该对象上的任何属性都会自动赋予我们,就好像它是在某个全局作用域中定义的一样。因此,` window.parseIntis` 等同于 `just` parseInt,window.alert`is` 等同于 `just` alert,依此类推。
我们还可以定义自己的属性,并动态创建变量。
function makeVariables() {
window.foo = 55;
window.bar = "hello";
}
makeVariables()
if (foo) console.log(foo);
if (bar) console.log(bar);
if (baz) console.log(baz);
// logs:
// 55
// "hello"
// Uncaught ReferenceError: baz is not defined
附注——这是个馊主意,千万别这么做。但我们为了举例子需要用到它。
接下来,我们需要了解一下Object.defineProperty。这个函数允许我们为对象定义具有独特属性的属性。虽然感觉很新,但它实际上在 IE9 上也能正常工作。
这个很棒的方法可以让我们将属性设置为绝对常量,这样人们就不会轻易更改它。它还允许我们定义自定义的 getter 方法!是不是感觉有点熟悉了?
const myObj = {}
Object.defineProperty(myObj, 'val', {
get() {
return Math.random();
}
})
console.log(myObj.val);
console.log(myObj.val);
console.log(myObj.val);
// logs:
// 0.6492479252057994
// 0.6033118630593071
// 0.6033118630593071
为什么这种方法比以前的方法更好?因为这次我们不必依赖强制手段!
让我们把刚才讨论的两点结合起来,完成第二个例子:
let value = 0;
Object.defineProperty(window, 'a', {
get() {
value++;
console.log("value incremented!");
return value;
}
})
if (a === 1 && a === 2 && a === 3) {
console.log("We got it!");
}
// logs:
// value incremented!
// value incremented!
// value incremented!
// We got it!
太棒了!现在我们已经实现了严格的等式相等!
很遗憾,我们不能在对象本身中定义一个变量(然后在 getter 中访问它),但如果我们真的不想污染作用域,我们可以用一种非常巧妙的方式使用闭包和 IIFE(感谢SpeakJS discord 服务器的 P35)。
Object.defineProperty(window, 'a', (function(){
let value = 0;
return {
get() {
value++;
console.log("value incremented!");
return value;
}
}
})());
但这显然是一个相当混乱的例子。
那么呢Proxy?我们可以在这里使用它吗?很遗憾,Proxy它不能与该window对象一起使用,因此在这种情况下对我们没有帮助。
包起来
那么,这什么时候有用呢?几乎没有。
嗯,确实有些时候会遇到这种情况。你有没有在使用 JS 框架时遇到过非常奇怪的错误?比如类似这样的Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute?
你的框架底层可能使用了代理和getter方法。它们很有用,但只有在情况变得复杂,你想隐藏底层复杂性时才有用。
文章来源:https://dev.to/emnudge/a-1-a-2-a-3-true-wait-hold-on-2olk