【JS体操】第2問「画像の横長具合を比較しよう」〜チート部門①②③の解説〜

こんにちは!
カヤック面白プロデュース事業部のおばらです。

本記事は『JS体操』第2問「どちらの画像が横長かを比較する」の解説記事です。
「正攻法」「ハック部門」の解説に引き続き、「チート部門」の解説をお送りします。


第2問「どちらの画像が横長かを比較する」 hubspot.kayac.com

第2問 解説「正攻法」「ハック部門」 techblog.kayac.com



JS体操とは?

『JS体操』とはカヤックが主催する JavaScript のコードゴルフ大会です。詳細は以下を御覧ください。

techblog.kayac.com



第2問の詳細

https://hubspot.kayac.com/js-taiso-002

もしまだ挑戦できていなかった!という方はぜひ。

hubspot.kayac.com


目次



チート部門とは?

第2問は(第1問以上に)いろんなアプローチで解くことができました。

それぞれ、

  • 「正攻法」 真面目に画像の縦横比を比較する方法
  • 「ハック部門」 テストの穴を突く方法
  • 「チート部門」 不正解を正解にしたり、文字数をごまかしたりする、ズルい方法

です。

「チート部門」はしょうもないズルい方法ではあるのですが、JavaScript のいろんな知識が必要で、頭の体操にももってこい。 本記事ではそんな「チート部門」の3パターンを真面目に解説します。



チート部門① 「正解」「不正解」をチェックする処理を上書きする



console.assert() のおさらい

第2問では console.assert() で正解か不正解か、テストに合格しているかをチェックしています。第1問は目視で正解か不正解を判断する問題だったので console.assert()第2問で初登場です。 まずは console.assert() の仕様を簡単に振り返りましょう。

  • 第1引数が truthy である場合は何もしない
  • 第1引数が falsy だとエラーが投げられる(その際、第2引数以降の情報がエラーメッセージに含まれる)

ざっくりいうと、 ある(条件)式を、「これは true になるはずだ!」と断言(assert)したうえで、 もしそれが false、つまり間違っていたら「え、違うじゃん!」ということでエラーが投げつけてくる、それが console.assert() です。

console.assert()true なはずだ!と断言(assert)したすべてのテストを、 有言実行、断言(assert)した通りちゃんと全部 true にするのが、前回の記事でご紹介した「正攻法」と「ハック部門」です。

でもちょっと待ってください。
true だろうが false だろうが、エラーを投げられないように口封じをしてしまえば良い、、、という極悪非道な考えが頭をよぎったりしませんか?


そう、それが「チート部門①」です。



console.assert() がエラーを投げないように書き換える

console.assert() は実は上書き可能です。console.assert() に限らず、大抵のものは上書きできちゃいます。JavaScript 万歳。

ということで、何もしない関数で上書きしましょう。
例えばこう。

console.assert = () => {};

おっと、エラーが出ちゃいました。 そうでした compare.js からは関数を export することが求められているのです。

何らかの関数を export しましょう。今回は何もしない関数を export すれば OK です。
console.assert() を上書きするので、compare.js から export する関数、つまり2つの画像の横長具合を比較する関数は真面目に仕事をする必要はありません!

なのでこうしちゃいます。

console.assert = () => {};
export default () => {};

動いた! 後ろめたさが多少ありますが51文字を達成しました。

ここからどんどん短くしていきましょう。



() => {} が2回登場します。これをまとめましょう。

export default console.assert = () => {};



ホワイトスペースや文末のセミコロンなど、省略できるものは省略します。

export default console.assert=()=>{}

36文字!



() だと文字数を2文字も消費してしまうので、1文字にするために無駄に引数を受け取ることにします。
(私は宣言せざるを得ないが使う予定のない引数は「使わないぞ!」感をほのめかすためによく _ にします。例えば replace() に渡す置換関数で、第1引数は使わないけど第2引数以降は使いたい時など。TypeScript であればさらに _ の型を unknown にすれば、本当に使えなくなって良いかもしれません)

export default console.assert=_=>{}

35文字!



console.assert() の戻り値は元々無い、つまり戻り値は undefined です。 が、main.js では console.assert() の戻り値は使っていないので undefined でなくても、何を返しても良いですね!

例えば1文字で書ける 0 とか。

export default console.assert=_=>0

34文字!



もちろん1文字であれば何でも良いですね! 123 でも。

export default console.assert=_=>1
export default console.assert=_=>2
export default console.assert=_=>3

34文字!



無駄に _ で受け取った引数を、そのまま左から右に横流ししても良さそうです。 このほうがなんかスマートな感じがします。sugyan さんの回答がこちら!

export default console.assert=_=>_

34文字!



グローバル関数を利用する

実は私はここまでしか想定していなかったのですが、もっと別な方法がありました。 自作の適当な関数ではなく、既存の関数、グローバル関数(で要件を満たす関数)を返す方法です。

gfx さんの回答がこちら!

export default console.assert=Date

同じく34文字!


とってもスマートな気がします! Date はコンストラクタ関数ですが new 演算子無しでも使えますね。そして画像を2つ渡しても怒らないという寛容さ。やさしい。

ということは、、、もし3文字、2文字、1文字のグローバル関数があったら34文字未満も可能になってしまう!まずい、、「チート部門①」は34文字と言ってしまった!「ハック部門」の33文字のほうが1文字少ない設計が破綻してしまいます。

恐る恐る検証しましょう。


画像を2つ渡しても怒らないかどうかは置いておき、まずは Date の4文字よりも文字数が少ない、つまり 3文字、2文字、1文字のグローバル関数(コンストラクタ関数も含む)を探してみます。

例えばこんなコードで。

Object.keys(window).filter(key => key.length < 4).filter(key => typeof window[key] === 'function');

あれ!1個も無い!

そんなはずはありません。 4文字未満の(コンストラクタ)関数は MapSet など、ちょっと考えただけでもいくつか思いつきます。

あ!MapSet列挙可能(enumerable)ではないのですね。うっかりしてました。

window.propertyIsEnumerable('Map') // => false
window.propertyIsEnumerable('Set') // => false



列挙可能(enumetrable)でないプロパティも列挙したいので、Object.keys(window) の代わりに Object.getOwnPropertyNames(window) を使いましょう。

Object.getOwnPropertyNames(window).filter(key => key.length < 4).filter(key => typeof window[key] === 'function');

3文字以下の(コンストラクタ)関数がたくさん見つかりました!初めてお目にかかるものもいくつか。

'Map', 'Set', 'URL', 'Ink', 'GPU', 'HID', 'USB', 'dir', '$', '$$', '$x'



画像を2つ渡しても怒らない寛容な(コンストラクタ)関数のみにフィルターしてみます。

Object.getOwnPropertyNames(window)
  .filter(key => key.length < 4)
  .filter(key => typeof window[key] === 'function')
  .filter(key => {
    const f = window[key];
    let throwsError = false;

    try {
      f(new Image, new Image);
    } catch {
      throwsError = true;
    }

    return !throwsError;
  });

catch ブロックの () はいつの間にか省略できるようになっていました。ECMAScript 2019 (ES10) から。

'dir', '$', '$$', '$x'

3文字以下で画像を2つ渡しても怒らない寛容な(コンストラクタ)関数があった?! やはり「チート部門①」で 34文字未満が可能だったのでしょうか。

いえ、たぶん不可能なはず。なぜなら、dir()$()$$()$x() は Chrome の Console Utilities API、つまり Chrome で且つコンソール上でのみ使える API だからです。 念の為一つ一つおさらいしましょう。



window.dir()

window.dir(object)

dirconsole.dir() のエイリアス、ショートカットらしいです。知らなかった!



window.$、window.$$

window.$(selector [, startNode])
window.$$(selector [, startNode])

これは皆さんもよく使うかもしれない document.querySelector()document.querySelectorAll() のエイリアス、ショートカットですね。エイリアス名にドルマーク $ を使用しているのは、懐かしの prototype.js、jQuery.js などの $() $$() にあやかっているのでしょうか。 document.querySelector()document.querySelectorAll() がまだ無かったあの頃を思い出します。辛かった IE6、IE7、IE8、、、との戦い。サヨナラ IE。サヨナラ後方互換モード。



window.$x()

window.$x(path [, startNode])

これまた懐かしの? XPath を引数に DOM を取得する関数です。 ということは document.evaluate() のエイリアスでしょうか? 引数が若干違いますが。 XPath、(昔もそんなに使わなかったですが)最近は全く使わないので存在をすっかり忘れていました。ついでに E4X(ECMAScript for XML)なんてのもあったなとふと思い出したり。 私の周りでは、昔は(サーバ側の)API が JSON ではなく XML を返す事が多かったので、サーバが返してきた XML から必要なデータを取り出す際に使っていたような気もしますが、今は JSON が多いので使う機会がほぼなくなっちゃいました。JSON 万歳。

でも改めて今振り返ると、意外と(DOM 操作に)便利かもしれない?気が向いたら使ってみます。


ちょっと話がずれましたが、

'dir', '$', '$$', '$x'

はいずれも Chrome の Console Utilities API なので、ブラウザの開発者コンソール以外では使えませんね! console オブジェクトにはこんな感じで便利な機能がたくさん備わっていますが、console がない時代はいちいち alert() で表示したり、DOM で画面の左上にログを表示したりなど、今振り返ると大変でした。


export default console.assert=dir

export default console.assert=$

export default console.assert=$$

export default console.assert=$x

いずれも無事?エラーとなりました。

念の為以下の(すでに filter() で弾かれている)候補も試してみましょうか。

'Map', 'Set', 'URL', 'Ink', 'GPU', 'HID', 'USB'



window.Map

まずは皆さんご存知の Map から。

export default console.assert=Map

Mapnew 無しで普通の関数のように使うことはできないみたいです。



window.Set

export default console.assert=Set

Setnew 無しで普通の関数のように使うことはできないみたいです。



window.URL

export default console.assert=URL

URLnew 無しで普通の関数のように使うことはできないみたいです。
(url 文字列を動的に作る際は、昔のように文字列の連結など自力で作るのは避け、 このビルトインの new URL() を使いましょう。XSS にも要注意)



window.Ink

export default console.assert=Ink

Ink クラスなんでのがあるんですね。初めて知りました。まだ Experimental のようです。 エラーのメッセージからは直接読み取れませんがInk は(CSSStyleDeclaration 等と同様)ユーザが自分で new することはできないようです。

document.body.style.constructor; // => CSSStyleDeclaration() { [native code] }
const style = new CSSStyleDeclaration(); // => TypeError: Illegal constructor



window.GPU

export default console.assert=GPU

GPUクラスも初めて知りました。こちらも Experimental。 これまたエラーのメッセージからは直接読み取れませんが GPU もユーザが自分で new することはできないようです。



window.HID

export default console.assert=HID

HID クラスも初めて知りました。HID は Human Interface Device の略らしいです。一瞬バイクや車の HID(High Intensity Discharge lamp)かと思いました。知らないクラスがどんどん登場しますね。HID クラスもまだ Experimental です。 HID もユーザが自分で new することはできないようです。



window.USB

export default console.assert=USB

いよいよ最後の USB クラス。こちらも Experimental。 USB もユーザが自分で new することはできないようです。


というわけで、「チート部門① console.assert() を上書きする」の最短文字数は、当初の想定通り34文字のはずです!でももっと短くできる、という人はぜひ教えてください。



チート部門② 文字列を取得・整形する処理を上書きする

コードの文字列を取得・整形する処理を見てみましょう。具体的には main.js の以下の部分です。

const { length } = await fetch('compare.js')
  .then((res) => res.text())
  .then((str) => str.trim());

文字列を取得するのに、関数・メソッドが複数使われています。いずれかを上書きすることで文字数をごまかせそうですね!



trim() を上書きする

trim()String クラスの「インスタンスメソッド」です。
以降「クラスメソッド」(static なメソッド、静的メソッド)と比較する意味で「インスタンスメソッド」という呼称とします。

trim() のように、あるクラスの「インスタンスメソッド」(の大元)を上書きするには「プロトタイプベースのオブジェクト指向言語」である JavaScript の根幹、「プロトタイプオブジェクト」の概念を知る必要があります。
※ 全てのインスタンスではなく、特定のインスタンスのみの「インスタンスメソッド」を上書きする場合は、直接上書きすれば OK です

最近では、クラスベースの他の言語のようにクラスを定義できるクラス記法が導入され「プロトタイプオブジェクト」の存在を直接意識する機会は減りました。
(今回の trim() の挙動を説明するには、さらに「ラッパーオブジェクト」の概念も関わってきますが、本記事では割愛します)



プロトタイプオブジェクト

「プロトタイプオブジェクト」をざっくり、本当にざっくり説明します。 以下のオブジェクトを考えます。

const obj = {
  hoge: 123,
};


objhoge にアクセスした際の挙動

obj.hoge // => 123

と、
obj の(存在しない)piyo プロパティにアクセスした際の挙動

obj.piyo // => undefined

を、比較してみます。



obj.hoge はシンプルですね。obj には hoge というプロパティがあるので、その値が素直に参照されます。

obj.hoge // => 123

obj.piyo はどうでしょう? obj には piyo というプロパティは見当たりません。では JavaScript はもうその時点で piyoundefined であると確定するでしょうか?実は、そうではありません!

obj.piyo // => undefined


objpiyo が無かったとしても、JavaScript まだ諦めません。obj に紐付いている「プロトタイプオブジェクト」に piyo が無いかを JavaScript は探しに行きます。

obj に紐付いている「プロトタイプオブジェクト」は、この場合は Object.prototype です。JavaScript は Object.prototype オブジェクトに piyo プロパティが無いかを探しに行ってくれるのです。

で、 Object.prototype オブジェクトにも piyo プロパティには当然無いですね!でもまだまだ JavaScript は諦めません。

今度はさらに Object.prototype オブジェクトに紐づいている「プロトタイプオブジェクト」をチェックしようとします。結果的には Object.prototype に紐づいている「プロトタイプオブジェクト」は無いので Object.prototype がこのループというか一連の流れ、連鎖、チェーンのゴールです。JavaScript の piyo を探す長い?旅がようやく終了します。そこで初めて piyoundefined であることが確定します。

この、JavaScript が「プロトタイプオブジェクト」をどんどん遡って探していく仕組みを「プロトタイプチェーン」と呼びます。

試しに Object.prototypepiyo プロパティを定義し、obj.piyo の評価結果がどうなるかを見てみましょう。

Object.prototype.piyo = 999;

const obj = {
  hoge: 123,
};

obj.piyo; // => 999

obj 自体には piyo プロパティは存在しないのに、obj.piyo999 になりました! これが「プロトタイプチェーン」の働きです。



new 演算子

「プロトタイプオブジェクト」、「プロトタイプチェーン」をざっくり説明しました。プロパティを例に説明しましたがメソッドの場合も同様です。 「プロトタイプチェーン」の仕組みを使うと、あるクラスのインスタンスのメンバ、(共用の)プロパティやメソッドを、インスタンスに直接生やすのではなく、インスタンスに共通で紐づく「プロトタイプオブジェクト」に定義することで、メモリを節約できます。JavaScript のビルトインのクラス、ObjectStringNumberArray などの「インスタンスメソッド」も同じ仕組みです。

ちなみに、クラスのインスタンスに「何らかのオブジェクト」を「プロトタイプオブジェクト」として紐づける役割を担っているのが new 演算子です。new 演算子がインスタンスに紐づけてくれる「何らかのオブジェクト」は、コンストラクタ関数に生えている prototype という名前のオブジェクト、と決まっています。大抵の関数にはデフォルトで prototype という名前のプロパティ、オブジェクトが定義されています。prototype という名前のプロパティを持たない関数は、例えば「アロー関数」や bind()this が束縛されている関数です。どちらも this の挙動が通常の function で定義される関数式/文と違いますね!

通常の関数(式)

const f = function() {};
console.log(f.prototype); // => {}
console.log(f.hasOwnProperty('prototype')); // => true
const instance = new f;

アロー関数

const f = () => {};
console.log(f.prototype); // => undefined
console.log(f.hasOwnProperty('prototype')); // => false
const instance = new f; // => TypeError: f is not a constructor

bind() で this が拘束された関数

const f = function() {};
const g = f.bind({});
console.log(g.prototype); // => undefined
console.log(g.hasOwnProperty('prototype')); // => false
const instance = new g; // => コンストラクタ関数としては一応使える

「(新規の)オブジェクト」に「何らかの既存オブジェクト」を「プロトタイプオブジェクト」として結びつけることは、昔は new 演算子 (or 非公式には __proto__ プロパティの書き換え)でしかできませんでしたが、最近では Object.create()Object.setPrototypeOf()でもできるようになりました。



String.prototype.trim() の上書き

さて、前置きが長くなりました。 ここでようやく trim() を上書きする話に戻します。trim()String クラスの「インスタンスメソッド」なので、trim() の実態は String.prototype.trim() です。

'123''abc' などの文字列リテラルは厳密には String クラスのインスタンスではないですが、「ラッパーオブジェクト」の概念の解説が必要なため本記事では割愛します


trim() は文字列の前後の空白を取り除いてくれるメソッドです。 昔は正規表現で取り除いていましたが、trim() が登場してからはよりシンプルに書けるようになりました。trimStart()trimEnd() もありますね!

'   12 3   '.replace(/^\s+/, '').replace(/\s+$/, ''); // => '12 3'
'   12 3   '.trim(); // => '12 3'


さて、第2問の文字数を所得する部分のコードを振り返りましょう。

const { length } = await fetch('compare.js')
  .then((res) => res.text())
  .then((str) => str.trim());

console.log(
  `%cJS体操 No. 2\n%c${length}%c文字\n%cpowered by 面白法人カヤック`,
  'font-weight:bold;font-size:200%;',
  'font-weight:bold;font-size:800%;color:#0af;',
  'font-size:100%;',
  'font-weight:bold;font-size:100%;color:#fc6;',
);


trim() が空文字列を返せば文字数を 0 にできちゃいますね!
早速やってみましょう。



trim() が空文字を返すようにする

String.prototype.trim = () => '';

export default(a,b,f=e=>e.naturalWidth/e.naturalHeight)=>f(a)-f(b) // 66文字の正攻法回答(最後のセミコロンは省略)

0文字達成!

多くの方がこのアプローチをしていました。tkihira さんはすでに第1問の段階で0文字を達成していました笑



length をオーバーフローさせる?

試しに、文字列の length をオーバーフローさせちゃえば?!と思いやってみましたが、オーバーフローする前にしっかりとエラーが出てダメでした。 repeat() でとーっても長い文字列を作成しようとしたのですが、手元の環境では 2**29 - 24 より大きいとエラーになりました。

const striiiiiiiiiiing = 'a'.repeat(2**29 - 24); // とーっても長い文字列

String.prototype.trim = () => striiiiiiiiiiing;

export default(a,b,f=e=>e.naturalWidth/e.naturalHeight)=>f(a)-f(b) // 66文字の正攻法回答(最後のセミコロンは省略)


const striiiiiiiiiiing = 'a'.repeat(2**29 - 23); // とーっても長い文字列

String.prototype.trim = () => striiiiiiiiiiing;

export default(a,b,f=e=>e.naturalWidth/e.naturalHeight)=>f(a)-f(b) // 66文字の正攻法回答(最後のセミコロンは省略)



trim() が String-like なオブジェクトを返す

さてここで、私が想定していなかった回答をご紹介します。ほーくさんのアプローチです。

trim() の戻り値は「文字列(string)」ですが JavaScript なので(要件を満たせば)何を返しても良いですね! 今回の場合は length というプロパティを持っていれば何でも良いです。

String.prototype.trim = () => ({
  length: -999, // 何でも良い Number.MIN_SAFE_INTEGER や Number.NEGATIVE_INFINITY でも。
});

export default(a,b,f=e=>e.naturalWidth/e.naturalHeight)=>f(a)-f(b) // 66文字の正攻法回答(最後のセミコロンは省略)

想定していないアプローチの回答があると悔しいとともにとても嬉しいです!ほーくさんありがとうございます!!!



Response.prototype.text() を上書きする

再び文字数を所得する部分のコードを振り返りましょう。

const { length } = await fetch('compare.js')
  .then((res) => res.text())
  .then((str) => str.trim());

console.log(
  `%cJS体操 No. 2\n%c${length}%c文字\n%cpowered by 面白法人カヤック`,
  'font-weight:bold;font-size:200%;',
  'font-weight:bold;font-size:800%;color:#0af;',
  'font-size:100%;',
  'font-weight:bold;font-size:100%;color:#fc6;',
);

trim() のひとつ前の段階、(res) => res.text()text() を上書きしても良いですね! Response クラスの「インスタンスメソッド」text() です。実態は Response.prototype.text()

Response.prototype.text = () => '';

export default(a,b,f=e=>e.naturalWidth/e.naturalHeight)=>f(a)-f(b) // 66文字の正攻法回答(最後のセミコロンは省略)



プロトタイプ汚染

String.prototype.trim()Response.prototype.text() など、ビルトインのクラスのプロトタイプオブジェクトに手を加えることは「プロトタイプ汚染」とも呼ばれ禁じ手です。実務では控えましょう。自分が書いたコード、チームメンバーが書いたコードのみならず、サードパーティ製のライブラリやフレームワークなどにも多大な影響を及ぼします!



fetch() を上書きする

更に手前の fetch() も上書きしてみましょう。 fetch() の戻り値の型は Promise<Response> ですが、今回は Promise<Response-like な何か> でも良いです。

fetch = async () => ({
  text: () => '',
});

export default(a,b,f=e=>e.naturalWidth/e.naturalHeight)=>f(a)-f(b) // 66文字の正攻法回答(最後のセミコロンは省略)


グローバル汚染

「グローバル汚染」という言葉もあります。 fetch() などのビルトインのグローバル関数を意図的に上書きすることも「グローバル汚染」と言うのでしょうか?「グローバル汚染」というと、(主に意図せず、時には意図して)グローバル変数を作って(結果既存のグローバル変数を上書きして)しまうことを指すことが多いような気がしますが。とにかく fetch() などのグローバル関数を上書きするなんてもってのほか、厳禁です。実務では控えましょう。


「チート部門②」は以上です!



チート部門③ 文字数を表示する処理を上書きする

最後は、文字数を表示する処理、つまりconsole.log() を上書きしちゃいましょう。 解説は割愛しサンプルコードのみで。

const log = console.log; // オリジナルの console.log をバックアップ
const length = -999; // 解説用に一時変数に

console.log = () => log(
  `%cJS体操 No. 2\n%c${length}%c文字\n%cpowered by 面白法人カヤック`,
  'font-weight:bold;font-size:200%;',
  'font-weight:bold;font-size:800%;color:#0af;',
  'font-size:100%;',
  'font-weight:bold;font-size:100%;color:#fc6;',
);

export default(a,b,f=e=>e.naturalWidth/e.naturalHeight)=>f(a)-f(b)// 66文字の正攻法回答

より丁寧に、正規表現を使って「〇〇文字」の「〇〇」の部分のみを差し替えるという回答もありました!



まとめ

長い記事になりましたが、最後まで読んでいただきありがとうございます。

「チート部門」の解説、いかがでしたでしょうか。 いずれもしょうもないアプローチではありますが、「プロトタイプオブジェクト」など JavaScript の知識が求められるテクニックでした。

『JS体操』には「失格」という概念はないので今後の問題でもあらゆるアプローチをお待ちしておりますが、今回ご紹介したアプローチは「既出」ということで、解説ブログでは封印します笑

本記事でご紹介した汎用的なチートをプログラム的に封印するために、いわゆるサンドボックスを作成するというテーマも技術的に面白そうですね。機会があればブログを書いてみようと思います。

他にも汎用的なチートがあるかもしれないですし、問題によっては、その問題ならではのチート法、出題側が想定していないチートがあるかもしれません。 もしそんなチートを見つけた方は、遠慮なくどしどしご応募ください。楽しみにしています!

『JS体操』第2問の解説は以上になります。
第3問も近日中に公開予定です。みなさまの回答をチーム一同楽しみにしています。

ではまた!



お知らせ

次回以降の「解説ブログ」や「JS体操の問題」のお知らせが気になる方は以下よりご登録ください。
公開次第、ご連絡します!

hubspot.kayac.com

『JS体操』第1問&第2問、まだ挑戦していない!という方はぜひ

hubspot.kayac.com

hubspot.kayac.com

先日、Perl のコードゴルフコンテストもカヤック主催で開催されました。
こちらも面白いのでぜひご覧ください!

techblog.kayac.com

そして、カヤックではコードゴルフが大好きな新卒&中途エンジニアも募集しています!

www.kayac.com www.kayac.com