初めに
この記事は前回の記事のゼロ幅を利用した難読化の小ネタの内容です。
javascriptの == を利用した比較は以下のとおりに評価が行われます。
この評価方法を利用して条件を成立させます。
成立させる方法
1. ToPrimitiveの動作を利用
var a = {
value : 1,
valueOf: function () {
return this.value++;
}
};
if (a == 1 && a == 2 && a == 3) {
console.log('ok');
} else {
console.log('no');
}
// ok
9. にあるように、オブジェクトと数字(または文字列)の比較、つまりa(x)と1,2,3(y)を比較するとToPrimitive(x) == yの結果が返ります。
ToPrimitiveは名前通りプリミティブに変換する関数です。
JavaScript において、プリミティブ (primitive、プリミティブ値、プリミティブデータ型) はオブジェクトでなく、メソッドを持たないデータのことです。 6 種類のプリミティブデータ型があります。文字列、数値、BigInt、真偽値、undefined、そしてシンボル (ECMAScript 2016 で追加) です。
詳しくはこちらをどうぞ: Primitive (プリミティブ) / MDN Web Docs
このToPrimitiveの処理は以下の通りになっています。
- [Symbol.toPrimitive](hint) メソッドがあればそれを呼び出す
- なければhintが
2-1. "string"ならば toString() -> valueOf()の順に優先される
2-2. それ以外ならば valueOf() -> toString()の順に優先される
hintは"string", "number", "default"のいずれかであり、その値は以下のように選ばれます。
const obj = {
[Symbol.toPrimitive](hint) {
console.log(hint);
}
}
// 数字が返されることを期待されるときはnumber
console.log(+obj);
// number
// NaN
console.log(0 < obj);
// number
// false
// 文字列が返されることを期待されるときはstring
alert(obj);
// string
// undefined
// それ以外のときはdefault
console.log(0 == obj);
// default
// false
細かい話は以下のサイトをご確認ください。
・Symbol.toPrimitive / MDN Web Docs
・ECMAScript® 2022 Language Specification
つまり、今回は2-2の順番で優先されます。
※ obj のvalueOf() -> toString()の順に優先されるため、以下のコードのようにtoString()だけを定義してもObjectのメソッドであるvalueOf()は利用されません。
var a = {
value : 1,
toString: function () {
return this.value++;
}
};
if (a == 1 && a == 2 && a == 3) {
console.log('ok');
} else {
console.log('no');
}
// ok
どちらも定義していれば以下のように動作します。
var a = {
value : 1,
valueOf: function() {
console.log('valueOf', this.value);
return this.value++;
},
toString: function () {
console.log('toString', this.value);
return this.value++;
}
};
if (a == 1 && a == 2 && a == 3) {
console.log('ok');
} else {
console.log('no');
}
// valueOf 1
// valueOf 2
// valueOf 3
// ok
2. ArrayのtoString()の動作を利用
var a = [1,2,3];
a.join = a.shift;
if (a == 1 && a == 2 && a == 3) {
console.log('ok');
} else {
console.log('no');
}
// ok
こちらは配列のtoString()の動作を利用した方法です。
- で説明したように、オブジェクトと数字の比較をするとtoString() or valueOf()が実行されます。
そこで、配列のtoString()の動作を利用します。
toString()が実行されると以下の画像にあるようにjoinが呼ばれます。つまり、joinをshiftにオーバーライドすることで == の比較が行われるたびshiftが実行されるということです。
ECMAScript® 2022 Language Specification
3. RegExpを利用
var a = {
value: /\d/g,
valueOf: function(){
return this.value.exec(123)[0];
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('ok');
} else {
console.log('no');
}
こちらも1. と同様にToPrimitive()の動作を利用したものです。
javascriptのRegExpの静的プロパティにはlastIndexがあり、gフラグのある正規表現でtestやexecを実行したときに次の一致を開始する位置が格納されます。
つまり、exec()を実行すると123の1にマッチし、lastIndexには次の開始位置である1が格納されます。そのため次のマッチは2になるということです。
終わりに
今回のようなことは通常のコーディングでは起こりにくいと思いますが、比較を行う際は == ではなく === を利用しましょう。
参考
Can (a== 1 && a ==2 && a==3) ever evaluate to true? / Stack overflow