無駄と文化

実用的ブログ

【JavaScript クイズ】return 後に無理やり処理を続けるやつ

みなさん "return 文" ってご存知ですか?

return 文が関数本体の中で使用された際、その関数の実行が停止します。

return - JavaScript | MDN

本当でしょうか?実験してみましょう。 ※ 楽しいクイズつき!

 

クイズ1: 出力される数字は何?

function main() {
  try {
    return 1;
  }
  finally {
    console.log(2);
  }
}
console.log(main());

 

・・・

 

正解は 2\n1

finally {} 実行 → return 1; の順番なんだね。

 

クイズ2: 出力される数字は何?

function main() {
  try {
    return 1;
  }
  finally {
    return 2;
  }
}
console.log(main());

 

・・・

 

正解は 2

finally {} の中で return 値を上書きできる。

 

クイズ3: 出力される数字は何?

function main() {
  L: {
    try {
      return 1;
    }
    finally {
      break L;
      return 2;
    }
  }
  return 3;
}
console.log(main());

 

・・・

 

正解は 3

ラベル付き break で try {} finally {} からの脱出に成功。

 

クイズ4: 出力される数字は何?

function main() {
  L: {
    try {
      return 1;
    }
    finally {
      break L;
    }
  }
}
console.log(main());

 

・・・

 

正解は undefined

break のせいで return 1; した事実が消えてしまった!

 

クイズ5: 出力される数字は何?

function main() {
  let x;
  L: {
    try {
      return x = 1;
    }
    finally {
      break L;
    }
  }
  x += 1;
  return x;
}
console.log(main());

 

・・・

 

正解は 2

break したのに return x = 1; した事実が消えていない?!

 

クイズ6: 出力される数字は何?

function main() {
  let x = 1;
  try {
    return x;
  }
  finally {
    x += 1;
  }
}
console.log(main());

 

・・・

 

正解は 1

return 後に x を変更しても return 値は変えられない。

 

クイズ7: 出力される数字は何?

function main() {
  let obj = { x: 1 };
  try {
    return obj;
  }
  finally {
    obj.x += 1;
  }
}
console.log(main().x);

 

・・・

 

正解は 2

参照なら return 後でも変更できる!

 

まとめ

  • return した後も finally {} で処理を続行できる!
  • ラベル付き break で finally {} から生きたまま脱出することも可能!
  • return 値を後から変更することだってできちゃう!

return すれば関数の実行が終了する……とでも思ったか??

 

 

私からは以上です。

色記法を雑に正規化する (#000 とか rgb() とか hsl() とか)

CSS では様々な色記法が認められています。

例えば私が好きなコーンフラワーブルーは、

  • cornflowerblue
  • #6495ed
  • rgb(100, 149, 237)
  • hsl(218.54, 79%, 66%)
  • hwb(218.54 39% 7%)
  • lab(61.2% 2.4% -40.2%)

これらどの記法でも同じ色が表示されます。

ぜんぶ同じ色!

mdn の <color> のページを見ると、他にも lch(), oklab(), oklch() などの記法があるようです。

とはいえ手っ取り早く RGB で知りたいよ

とはいえプログラムから色を扱っていると「なんでもいいから RGB で教えてくれ」と思いますよね? これらの色記法をどうにかして RGB 形式に変換してみます。

どの色記法であれブラウザが受け入れてくれてるということはブラウザの API で正規化できるってことです、たぶん。

style 属性に書き込んで読み出す (ボツ案)

手始めに適当な要素の style 属性に書き込んで、それを読み出してみます。

const div = document.createElement('div');

div.style.color = '#6495ed';
console.log(div.style.color); // => rgb(100, 149, 237)

div.style.color = 'hsl(218.54, 79%, 66%)';
console.log(div.style.color); // => rgb(100, 149, 237)

やった!

遊んでたらできちゃった!

が、このやり方では <named-color> には対応できません。

const div = document.createElement('div');

div.style.color = 'cornflowerblue';
console.log(div.style.color); // => cornflowerblue

ダメだ。もうちょっと頑張ってみましょう。

 

style 属性に書き込んで getComputedStyle() で読み出す (ボツ案)

getComputedStyle() ってやつを使います。ブラウザの開発者ツールで見られる "Computed Style" を返してくれる関数です。

const div = document.createElement('div');
document.body.appendChild(div);

div.style.color = '#6495ed';
console.log(getComputedStyle(div).color); // => rgb(100, 149, 237)

div.style.color = 'cornflowerblue';
console.log(getComputedStyle(div).color); // => rgb(100, 149, 237)

やるじゃん!ちゃんと <named-color> に対応できました。
ただし document.body.appendChild(div) しているのでページに影響を与えます。丁寧にやるならこんな感じかな。

const div = document.createElement('div');
document.body.appendChild(div);
div.style.display = 'none';

div.style.color = 'cornflowerblue';
console.log(getComputedStyle(div).color); // => rgb(100, 149, 237)

document.body.removeChild(div);

実はこれでもまだ color() 関数記法 ってやつに対応していないんです。

const div = document.createElement('div');
document.body.appendChild(div);

div.style.color = 'color(srgb 0.392 0.584 0.929)';
console.log(getComputedStyle(div).color); // => color(srgb 0.392 0.584 0.929)

表示はできるけど色記法はそのまま

おそらく色を決定するためのプロファイルが環境依存で、プロファイルを定めないと color() 記法と RGB の値が一意にならないからだろうね。
うーむ残念。次いきましょう。

 

canvas を fillRect で塗って getImageData で読み出す

1px × 1px の canvas を作って塗ったり読んだりします。

const canvas = document.createElement('canvas');
canvas.width = canvas.height = 1;
const ctx = canvas.getContext('2d');

ctx.fillStyle = 'color(srgb 0.392 0.584 0.929)';
ctx.fillRect(0, 0, 1, 1);

const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
console.log(a === 255
  ? `rgb(${r}, ${g}, ${b})`
  : `rgba(${r}, ${g}, ${b}, ${a / 255})`);
// => rgb(100, 149, 237)

結局 canvas かー。

関数化する

今回試した3つのやり方をそれぞれ関数化したものがここにあります。
どれも DOM API を前提としているのでブラウザ以外のランタイム (Node.js, Deno) などでは動きません。
あと、Chrome 以外のブラウザでの動作確認もしていません。

gist.github.com

まとめ

結局 canvas かー。style 属性でうまいこといけると思ったんだけどな。

 

 

私からは以上です。

要素を入れ替えたときにスクロール位置が追従することがあるみたい

JavaScript を使って画面上の要素を入れ替えたとき、スクロール位置が勝手に動いて画面がカクつくという現象に遭遇しました。調査して分かったことをまとめます。

まずはデモをご覧ください

todays-mitsui.github.io

カラフルな <div> が並んでいて、右上のボタンを押すとすぐ上の要素と位置が入れ替わるデモを作りました。

ボタンを押すとすぐ上の要素と入れ替わる

ここまではなんの変哲もありません。ところが、まったく同じコードでも要素が大きくなると違う挙動になります。

すぐ上の要素と入れ替わる、と同時にスクロール位置がシフトする

ボタンを押すと同時にスクロール位置がシフトしているのが分かります。背景に敷いてある「01, 02, 03, 04, ...」の数字に着目すると分かりやすいかも。

この記事ではどのような条件でスクロールシフトするのか調べてみます。

目次

3行まとめ

  • 要素の高さは無関係
  • 移動した後の要素の上端がビューポートからはみ出すか、ビューポートの上端に近いとスクロールが追従する
  • すぐ下の要素と入れ替える処理ではもっと不可解な挙動になる
  • CSS の overflow-anchor プロパティ で制御できる

解決策だけ見たい人は スクロール調整 (スクロールアンカリング) を無効にする方法 を見てください。

デモのすべてのコードは GitHub に置いています。 → https://github.com/todays-mitsui/swap-element-lab

要素入れ替えのコード

今回のデモは React で組んでいます。要素のレンダリングはのような感じです。

function App() {
  const [colors, setColors] = useState(['#ff6e61', '#4a8a98', '#fff2c2', ...]);

  return (
    <>
      {colors.map((color, index) => (
        <div style={{ backgroundColor: color }} key={color}>
          <h3>{color}</h3>
          <button type="button"></button>
        </div>
      ))}
    </>
  );
}

要素に背景色 (background-color) をつけているのは入れ替わりをわかりやすくするためで、スクロール位置がずれる挙動には直接関係ありません。

ここに要素入れ替えの処理を追加します。

const handleSwap = (i: number) => {
  const newColors = [...colors];
  [newColors[i], newColors[i-1]] = [newColors[i-1], newColors[i]];
  setColors(newColors);
};

素朴に i 番目 と i-1 番目 を入れ替えてから setColors しているだけです。

二つを合わせるとこんな感じになります。

function App() {
  const [colors, setColors] = useState(['#ff6e61', '#4a8a98', '#fff2c2', ...]);

  const handleSwap = (i: number) => {
    const newColors = [...colors];
    [newColors[i], newColors[i-1]] = [newColors[i-1], newColors[i]];
    setColors(newColors);
  };

  return (
    <>
      {colors.map((color, index) => (
        <div style={{ backgroundColor: color }} key={color}>
          <h3>{color}</h3>
          <button type="button" onClick={() => handleSwap(index)}></button>
        </div>
      ))}
    </>
  );
}

入れ替え後の要素の top の位置が条件になっているみたい

いろいろと実験してみました。入れ替え後の要素の上端 (top) がビューポートに対してどの位置にあるかによってレイアウトシフトが 起こる/起こらない が変わるようです。

 

要素が高くてもスクロールシフトが起こらないことがある

要素が高くてもスクロールシフトが起こらない場合があります。入れ替え後に要素がビューポートにおさまる位置にあればスクロールシフトは起こりません。

https://todays-mitsui.github.io/swap-element-lab/?height=500&scroll=2100

ビューポートの下のほうにある要素を入れ替え、ビューポートの真ん中に持ってくる

 

要素が低くてもスクロールシフトが起こることがある

逆に、要素が低くても入れ替え後にビューポートの上端付近に来る場合にはレイアウトシフトが起こります。
入れ替え後にも要素が確実にビューポートに入るように調整される、と考えると分かりやすいです。

ビューポート上端に近いと、ビューポートの中ほどにスクロール調整される

 

ここまでのまとめ

  • 入れ替え後にビューポートにおさまっていればスクロールシフトはおこらない
  • 入れ替え後にビューポートからはみ出しているか端すぎればスクロール位置が調整される

このように考えると理解しやすいですね。

「下の要素と入れ替える」という動作にするともっと不可解なことが起こる

先ほどまではボタン押下で上の要素と入れ替えるという動作のもと実験していました。これをボタン押下で 下の要素 と入れ替えるにすると、もっと理解しがたい挙動になります。

 

入れ替え後に要素がビューポートからはみ出していてもスクロールシフトしない

大きめの要素をすぐしたの要素と入れ替えます。入れ替え後に要素はビューポートの下のほうにわずかに見えるばかりですがスクロール調整は起こりません。

https://todays-mitsui.github.io/swap-element-lab/?height=500&scroll=2000&direction=down

入れ替え後に要素がビューポート下部に置かれるがレイアウトは調整されない

 

スクロールシフトの結果、要素が下のほうに吹っ飛ぶ

元の要素を見失ってしまいそうなスクロールシフトが起こることもあります。

元の要素がビューポートより下に吹っ飛んでいるように見える

整理します。

  • 入れ替えられた2要素のうち、入れ替え後に上にあるものに着目する
  • 上の要素の上端がビューポートの上端に近ければ、ビューポートに充分におさまるように下にスクロール調整する
  • 結果として入れ替え後に下にある要素はより下に移動しビューポートから完全に見えなくなる

スクロール調整 (スクロールアンカリング) を無効にする方法

このような挙動は スクロールアンカリング という機能によるものだそうです。この挙動を無効にする方法があります。
CSS の overflow-anchor プロパティ を使います。{ overflow-anchor: none; } を指定した要素は入れ替えによって位置が変わってもスクロール調整の対象になりません。

 function App() {
   const [colors, setColors] = useState(['#ff6e61', '#4a8a98', '#fff2c2', ...]);

   return (
     <>
       {colors.map((color, index) => (
-         <div style={{ backgroundColor: color }} key={color}>
-         <div style={{ backgroundColor: color, overflowAnchor: "none" }} key={color}>
           <h3>{color}</h3>
           <button type="button">↑</button>
         </div>
       ))}
     </>
   );
 }

※ コード例示の都合で style タグを使っています。普通に CSS で設定すれば OK です

今回のデモでは「スクロール位置固定」というオプションに ✅ を入れることでこの挙動を体験できます。

https://todays-mitsui.github.io/swap-element-lab/?height=550&scroll=6000&fixed=true

まとめ

  • 入れ替えられた2要素のうち、入れ替え後に上にあるものに着目する
  • 上の要素の上端がビューポートの上端に近ければスクロール調整されることがある
  • overflow-anchor プロパティでこの挙動を無効化することは可能

こんな感じかなー。たぶん。

 

この記事の公開後すぐに id:nanto_vi さんから「それってスクロールアンカリングの影響では?」と情報をもらったので スクロール調整 (スクロールアンカリング) を無効にする方法 セクションを加筆修正しました。ズバリの情報ありがとうございました。

 

 

私からは以上です。