2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScriptを1文字ずつ削る。手ごとの削減幅を実測した10個と、壊れる境界

2
Posted at

短くしたコードは、どこまで元と同じ結果を返すのでしょうか。

削り方の一覧はよく見かけます。ただ、どれが何文字減るのか、そしてどこから結果が変わるのかを並べたものが見当たりませんでした。

なので測りました。Node.js v22.22.2 で、変換前と変換後を両方実行して、返り値が一致することを確かめたうえで文字数を数えています。

削減幅

最小の例で測っています。実際のコードでは前後の文脈でもう少し変わります。

削減
if/else を三項演算子に 38 17 21
セミコロンをカンマ演算子に 21 11 10
Math.floor をビット演算に 15 5 10
アロー関数の return を省く 23 14 9
let を省く(暗黙のグローバル) 10 6 4
forwhile 36 32 4
アロー関数の引数の括弧を省く 16 14 2
split(",") をタグ付きテンプレートに 18 16 2
真偽値を数値にする +(x) 15 14 1
new Array(n).fill(0) を短くする 20 23 -3

最後が負です。短く見える書き方のほうが長い、という例を1つ入れておきました。

new Array(3).fill(0)        // 20文字
[...Array(3)].map(_=>0)     // 23文字

スプレッド構文は短そうに見えますが、この用途では負けます。短縮の候補を思いついたら、実際に数えたほうがいい、という例です。

効きの大きい順に見る

上の3つで、削減の半分を占めます。

分岐は、三項演算子にすると波括弧が2組消えます。

let x=5,r; if(x>3){r=""}else{r=""} r   // 38文字
x=5,r=x>3?"":""                        // 17文字

セミコロンをカンマにすると、変数宣言がまとまります。

let a=1; let b=2; a+b   // 21文字
a=1,b=2,a+b             // 11文字

切り捨ては、ビット演算のほうが10文字短くなります。

Math.floor(7.9)   // 15文字
7.9|0             // 5文字

ただし、ここには境界があります。

壊れる境界

削減幅より、こちらのほうが大事だと思っています。

ビット演算での切り捨ては、範囲と符号で壊れる

|0 は32ビット整数への変換なので、2の31乗を超えると桁が回ります。

Math.floor |0
7.9 7 7 一致
2147483647.9 2147483647 2147483647 一致
2147483648.9 2147483648 -2147483648 不一致
3000000000.5 3000000000 -1294967296 不一致

負の数でも違います。切り捨ての方向が逆になります。

Math.floor |0
-7.9 -8 -7 不一致
-0.5 -1 0 不一致

Math.floor は下に丸め、|0 はゼロ方向に丸めます。座標を扱うコードで負の値が来ると、ここで1ずれます。画面の外に出た瞬間だけ挙動が変わる、という壊れ方をします。

暗黙のグローバルは、strict mode で落ちる

let を省く手は、strict mode では使えません。

(function(){ 'use strict'; z=1; })();
// ReferenceError: z is not defined

ES モジュールは既定で strict mode です。<script type="module"> で読み込んだり、.mjs にしたりすると、そこで止まります。

短縮したコードを HTML に直接埋め込むぶんには通りますが、あとからモジュール化しようとした瞬間に、全部書き直しになります。

タグ付きテンプレートの split は、変数を埋め込めない

これがいちばん見つけにくい罠でした。

"a, b".split`, `      // ["a","b"]  区切りが2文字でも動く

ここまでは期待どおりです。ところが、区切りを変数にすると変わります。

const sep = ",";
"a,b".split(sep)      // ["a","b"]
"a,b".split`${sep}`   // []

空の配列が返ります。エラーは出ません。

タグ付きテンプレートは、文字列と埋め込み値を分けて関数に渡す仕組みです。split はその第1引数(文字列の配列)を区切り文字として受け取ろうとするので、埋め込みがあると意図が崩れます。

リテラルのときだけ使える手でした。あとで区切りを変数にしたくなったら、書き方ごと戻す必要があります。

測り方

同じ形で確かめられます。

const cases = [
  ['アロー関数の引数の括弧', 'f=(x)=>x*2; f(3)', 'f=x=>x*2; f(3)'],
  // ...
];

for (const [name, before, after] of cases) {
  const rb = eval(before);
  const ra = eval(after);
  const ok = JSON.stringify(rb) === JSON.stringify(ra);
  console.log(name, before.length, after.length,
              before.length - after.length, ok ? '一致' : '不一致');
}

eval を使っているのは、変換前後を文字列として持って長さを数えたいからです。計測用の使い捨てなので、それ以外の用途では避けます。

返り値の一致を確かめずに文字数だけ数えると、壊れたコードで喜ぶことになります。上のビット演算の例は、7.9 だけで試していたら気づけませんでした。

削る順番

自分用の結論です。

分岐と宣言から手をつけます。ここは削減が大きく、しかも壊れる境界がありません。三項演算子とカンマ演算子は、意味が変わらない書き換えです。

そのあとに、ビット演算や暗黙のグローバルを検討します。削減は大きいのに、条件付きで壊れます。扱う値の範囲を確かめてから使う。

タグ付きテンプレートのような小技は、最後に1文字ずつ拾う段階でだけ使います。あとから仕様が変わると戻すことになるので。

余談

測っていて意外だったのは、forwhile にしても4文字しか減らないことでした。

let s=0; for(let i=0;i<5;i++)s+=i; s      // 36文字
let s=0,i=0; while(i<5)s+=i++; s          // 32文字

for の中に押し込んでいた初期化とインクリメントが、外に出てくるからです。減った文字と増えた文字がほぼ相殺します。

短縮の記事でよく見かける手ですが、単体ではあまり効きません。効くのは、他の短縮と組み合わせて、while の条件式に処理を押し込めるようになったときでした。

一覧を作って良かったのは、こういう「思っていたより効かない手」が分かったことです。効く順に並べ替えると、上位3つで半分を稼いでいます。残りは、そこまでやったあとの話でした。


ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?