かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JavaScript ループを使わずに 1〜100 までを出力する

Twitter でループを使わずに1〜100まで出力するプログラム書ける?ってのが流れてきた。

元の質問C++ で答えてくれって感じだったけど、大好きな JavaScript だとどう書けるのかやってみたくなったのでトライしてみた。

再帰

再帰はループに含まれるのかわからない。仮に含まれないなら再帰的に関数を呼び出して [1..100] な配列を作るのが簡単そう

const zeroPadding = (digit) => {
  const zero = '0'.repeat(digit);
  // or Array( digit + 1 ).join('0');
  return (n) => {
    return (zero + n).slice(- digit);
  }
};

const addList = (i, arr) => {
  const a = [...arr].concat(zeroPadding(3)(i));
  if (i === 100) { return a }
  return addList(i + 1, a);
};

addList(1, []).join(' ');
// => "001 002 003 004 005 006 007 008 009
// 010 011 012 013 014 015 016 017 018 019 
// 020 021 022 023 024 025 026 027 028 029
// 030 031 032 033 034 035 036 037 038 039
// 040 041 042 043 044 045 046 047 048 049
// 050 051 052 053 054 055 056 057 058 059
// 060 061 062 063 064 065 066 067 068 069
// 070 071 072 073 074 075 076 077 078 079
// 080 081 082 083 084 085 086 087 088 089
// 090 091 092 093 094 095 096 097 098 099 100"

関数型っぽくゼロパディングするだけの関数を作ってみたけど、桁数決まってるから [...arr].concat(('000' + i).slice(-3)); で事足りる。

Array.from を使う

Array(100) で要素が100個ある配列を作って、Array.from の第2引数に値を変換する関数を渡して [1..100] な配列を作る方法
Array.from の値の変換って実質的に map なのでループを使ってるに該当はしてしまいそう。。。

Array.from(Array(100), (v, i) => ('000' + (i + 1)).slice(-3)).join(' ');
// => "001 002 003 004 005 006 007 008 009
// 010 011 012 013 014 015 016 017 018 019 
// 020 021 022 023 024 025 026 027 028 029
// 030 031 032 033 034 035 036 037 038 039
// 040 041 042 043 044 045 046 047 048 049
// 050 051 052 053 054 055 056 057 058 059
// 060 061 062 063 064 065 066 067 068 069
// 070 071 072 073 074 075 076 077 078 079
// 080 081 082 083 084 085 086 087 088 089
// 090 091 092 093 094 095 096 097 098 099 100"

Array.from の map 関数で配列の値をインデックスを利用して 1〜100 にしている。

Array.from(arrayLike[, mapFn[, thisArg]])
arrayLike … 配列に変換する配列風オブジェクトまたは反復可能オブジェクト
mapFn (Optional) … 配列のすべての要素に対して呼び出される Map 関数。
thisArg (Optional) … mapFn を実行する時に this として使用する値。

console.log(Array.from([1, 2, 3], x => x + x));
// expected output: Array [2, 4, 6]
cf. [https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/from:title]

Array().keys() を使う

Array.keys() で配列のインデックスが入った Array Iterator Object を取得できる。 Array Iterator Object は forEachmap メソッドを持っていないので、[...Array.keys()] で配列に変換すれば Array.length 分のインデックスが順番に入った配列を取得できる。

const arr = [0,0,0].keys(); // => Array Iterator Object
[...arr]; // => [0, 1, 2]

インデックスは 0 から始まるので 101 の長さの配列からインデックスの配列を作成して先頭の値を削除すれば [1..100] な配列ができあがる。
map 的なものが無いのでゼロパディングは出来なそうだけど、質問にある 1〜100 を出力することはできる。

[...Array(101).keys()].slice(1).join(' ');
// => "1 2 3 4 5 6 7 8 9
// 10 11 12 13 14 15 16 17 18 19
// 20 21 22 23 24 25 26 27 28 29
// 30 31 32 33 34 35 36 37 38 39
// 40 41 42 43 44 45 46 47 48 49
// 50 51 52 53 54 55 56 57 58 59
// 60 61 62 63 64 65 66 67 68 69
// 70 71 72 73 74 75 76 77 78 79
// 80 81 82 83 84 85 86 87 88 89
// 90 91 92 93 94 95 96 97 98 99 100"

 
JavaScript に用意されている機能を使えば、1行でも書くことが出来ました。(内部的にループ使ってるとか分からないけど…)
パズルみたいなの好きなので楽しかったです。

_人人人人人人人人人人人人人人_
> まったく、JSは最高だぜ! <
 ̄YYYYYYYYYYYYYY^ ̄


[参考]

JavaScriptで学ぶ関数型プログラミング

JavaScriptで学ぶ関数型プログラミング

関数型プログラミングの基礎 JavaScriptを使って学ぶ

関数型プログラミングの基礎 JavaScriptを使って学ぶ

りゅうおうのおしごと! (GA文庫)

りゅうおうのおしごと! (GA文庫)

まったく、JSは最高だぜ!...

React Hooks コンポーネント外のDOMに子コンポーネントを追加したい。

全面ReactなSPAではなく、部分的にReactを導入しているようなサービスにモーダルとそれを表示させるボタンをReact Component で作ろうとすると次のような構成になるかと思います。

function ShowDetailByModal() {
  return (
    <>
      <button onClick={showModal}>
        SHOW
      </button>
      <Modal />
    </>
  );
}

しかし、このような構成の場合コンポーネント内にモーダルのDOMが出力されるので、このコンポーネントが置かれる親要素に positionz-indexoverflow: hidden といったCSSを持っているものがあると、意図したとおりにモーダルが表示されなくなってしまいます。(CSSの影響大きいよね…)

モーダルのDOMだけ<body>直下に置きたい。
つまり、特定の子コンポーネントのDOMをコンポーネント外のDOMに追加したいようなケースのメモ。

Portal を使う

ポータル (portal) は、親コンポーネントの DOM 階層外にある DOM ノードに対して子コンポーネントをレンダーするための公式の仕組みを提供します。

ReactDOM.createPortal(child, container)
第 1 引数 (child) は React の子要素としてレンダー可能なもの、例えば、要素、文字列、フラグメントなどです。第 2 引数 (container) は DOM 要素を指定します。
cf. https://ja.reactjs.org/docs/portals.html

モーダルを追加するDOMを前もって用意する方法

HTMLに、別途追加したいDOMを入れる要素を追加します。
今回はボタンと別にモーダルだけ body 直下に追加したいので、<div id="modal"></div> を追加しました。

<html>
  <body>
    <div id="app">
       <nav id="nav">
         <!-- ここにモーダルを表示するボタンが入る -->
       </nav>
    </div>
    <div id="modal"><!-- ここにModalが入る --></div>
  </body>
</html>
Portal Component を作成
// Portal.js
import ReactDOM from 'react-dom';

export default function Portal({children, targetID}) {
  return ReactDOM.createPortal(
    children,
    document.getElementById(targetID)
  );
}

モーダルを表示するコンポーネントを作成

// ShowDetailByModal.js
import React, { useState } from 'react';
import Portal from './Portal';

function ShowDetailByModal() {
  const [isOpen, setIsOpen] = useState(false);
  const showModal = () => setIsOpen(true);
  const closeModal = () => setIsOpen(false);

  return (
    <>
      <button onClick={showModal}>
        SHOW
      </button>
      <Portal targetID="modal">
        <Modal
          isShow={isOpen}
          onClose={closeModal}
        />
      </Portal>
    </>
  );
}

ReactDOM.render(
  <ShowDetailByModal />,
  document.getElementById('nav')
);

👇 Render

<div id="app">
  <nav id="nav">
    <button>button</button>
  </nav>
</div>
<div id="modal">
  <div class="modal-component"></div>
</div>

Portalを使えば、ReactのDevツールで見た感じだと VDOM上 <Modal> Component はボタンのある <ShowDetailByModal> Component の中にあり通常の親-子コンポーネントと同じように props を渡したりできるが、実DOMでは子コンポーネントが親コンポーネント外に自由な場所に置くことができました!

Portal で出力する DOM をコンポーネント内で作成する方法

HTMLで別途追加するコンポーネントを入れるDOMを用意するのではなく、React内で追加するDOMも作成してしまう方法

HTML

<html>
  <body>
    <div id="app">
       <nav id="nav">
         <!-- ここにモーダルを表示するボタンが入る -->
       </nav>
    </div>
    <!-- ここに Portal を出力したい -->
  </body>
</html>
Portal Component
// Portal.js
import { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

export default function Portal({children}) {
  const el = document.createElement('div');
  const [portal, setPortal] = useState(el);
  
  // portal を実DOMに追加
  useEffect(() => {
    const body = document.querySelector('body');
    body.appendChild(el);
    setPortal(el);
  }, []);

  return ReactDOM.createPortal(
    children,
    portal
  );
}

子要素を追加する DOM を React の Functional Component で動的に作成して追加する際のポイント

  1. 子要素を追加する DOM エレメントを useState で保持していること
  2. 実DOMに追加する useEffect の第二引数に [] を渡して初回のみ実DOMに追加するようにしていること
🙆‍♀️ (追記) 追加する親要素を Functional Component 外で作成する方法

React の Functional Component 関数外であれば再描画の際に再実行されることがないので、動的に追加する親要素を React の関数外で作成してしまえば useState で親要素の el を保持しなくても大丈夫でした!

import { useEffect } from 'react';
import ReactDOM from 'react-dom';

// 親要素を React の Component 関数 外で定義
const el = document.createElement('div');

export default function Portal({children}) {
  // portal を実DOMに追加
  useEffect(() => {
    const body = document.querySelector('body');
    body.appendChild(el);
  }, []);

  return ReactDOM.createPortal(
    children,
    el
  );
}

再描画の際に Portal 関数は再実行されるけど、ReactDOM.createPortal(children, el) で指定している親要素 el は関数外で作成したものを参照しているので、問題なく再描画されるようです。
Portal コンポーネントに prop で親要素 el のクラス名を与えたい場合などは useEffect の中で変更することができます。ellet で定義してしまえば useEffect 内で div 以外に変更してしまうことも可能でした。

import { useEffect } from 'react';
import ReactDOM from 'react-dom';

// 親要素を React の Component 関数 外で定義
let el = document.createElement('div');

export default function Portal({children, classNames}) {
  // portal を実DOMに追加
  useEffect(() => {
    // ▼ この変更は再描画の際も保持されている ▼
    // 親 DOM を変更してしまう
    el = document.createElement('span');
    // props 経由での加工もOK
    el.className = classNames.join(' ');
    // ▲ この変更は再描画の際も保持されている ▲
    const body = document.querySelector('body');
    body.appendChild(el);
  }, []);

  return ReactDOM.createPortal(
    children,
    el
  );
}

state 管理するまでも無いものなので、コンポーネント関数外で親要素を定義してしまうほうが楽かもしれません。(関数的にはあまり美しくはないかもだけど)

上手く動作しないパターン

🙅‍♀️ 1. 子要素を追加する親DOMが保持されず、再描画の際に Portal 内が描画されなくなるパターン

Functional Component では再描画の度に関数が実行されるので、子要素を追加するDOMが保持されてないと、再描画の際に ReactDOM.createPortal() で追加する親要素が不明になってしまって描画されなくなります。

import { useEffect } from 'react';
import ReactDOM from 'react-dom';

export default function Portal({children}) {
  const el = document.createElement('div');
  
  useEffect(() => {
    const body = document.querySelector('body');
    body.appendChild(el);
  }, []);

  return ReactDOM.createPortal(
    children,
    el
  );
}

再描画の際に ReactDOM.createPortal(children, el)children を追加する el は初回描画時の el とは別物で実DOMに追加もされてないので、children は描画されなくなる

🙅‍♀️ 2. 再描画の度に子要素を追加する親要素のDOMが追加されてしまう

useEffect の第二引数に [] を渡して初回しか実行されないようにしていないと、再描画の度に新しい <div> が実DOMに追加されてしまいます。

import { useEffect } from 'react';
import ReactDOM from 'react-dom';

export default function Portal({children}) {
  const el = document.createElement('div');
  
  // 再描画の度に useEffect が実行されてしまう
  useEffect(() => {
    const body = document.querySelector('body');
    body.appendChild(el);
  });

  return ReactDOM.createPortal(
    children,
    el
  );
}

再描画の度に新しい el が実DOMに追加され、その新しく追加された el (<div>) 内に children が出力されてしまいます。

🙅‍♀️ 3. useEffect の中で ReactDOM.createPortal することは出来ない

初回だけ実行される useEffect 内で Portal を出力すれば良いのでは?と思ったのですが、エラーになります。

import { useEffect } from 'react';
import ReactDOM from 'react-dom';

export default function Portal({children}) {
  useEffect(() => {
    const el = document.createElement('div');
    const body = document.querySelector('body');
    body.appendChild(el);
    ReactDOM.createPortal(
      children,
      el
    );
  }, []);
  
  return;
}

そもそもこのコンポーネントが何も返さなくなり、VDOMの方が成立しなくなる為ではないかと推測しています。
return (children) の様な感じで何か返すようにするとエラーにはなりませんが、ReactDOM.createPortal での追加は無効化されてしまうのでダメなようです。

所感

VDOMツリーにある子要素を、実DOMでは別の箇所に追加できる方法が用意されていたのは今回調べて初めて知りました。
めっちょべんりー!! すごーい!!!!

ただ、今回のような方法はモーダルを追加するボタンごとにモーダルが出力されるので、モーダルを使うような場合はSPAのように全体をReact Component化してモーダルコンポーネントは1つだけ用意して Redux や useContext などを通じてアクセスできるようにしておき、各ボタンのアクションがあれば、唯一のモーダルの中身を変えて表示するような設計がキレイそうだな〜とか考えてました。

公式ドキュメントが Class Component で書かれていたので、これが本当に正しいのかな?って部分は気になる


[参考]

ROM

ROM

JavaScript const で作った配列を空にしたい

前回のおさらい

配列は参照

const array = ['シャミ子', 'もも', 'みかん'];
const refArr = array;
array.push('シャミ先');
console.log(refArr);
// => ['シャミ子', 'もも', 'みかん', 'シャミ先']

空配列を代入して配列を空にする方法だと const にできない

配列を空にする方法で一般的なのは 空配列 [] を代入してしまうことではないかと思います。
しかしこの方法では再代入しなければならないので、const 定義することができません。

let array = ['シャミ子', 'もも', 'みかん'];
const refArr = array;
// 配列を空にする
array = [];
console.log(array);
// => []
console.log(refArr);
// => ['シャミ子', 'もも', 'みかん']
// 再代入が発生しているので参照が切れ別の配列になっている

Array.length = 0 で配列を空にできる

length0 にすると配列を空にすることができるようです!

const array = ['シャミ子', 'もも', 'みかん'];
const refArr = array;

array.length = 0;
console.log(array);
// => []
console.log(refArr);
// => []
// 参照が切れないので参照先も空になる

この方法なら再代入がないので const で元の配列を作成することができます!!
ただ、配列への参照が切れないので、参照している変数があると意図せず内容を変化させてしまう可能性 (副作用) があります。
関数に渡した場合も同様なので注意が必要です。

const arrayEmpty = (array) => {
  array.length = 0;
  return array;
}

const array = ['シャミ子', 'もも', 'みかん'];
const refArr = array;

arrayEmpty(refArr);
console.log(refArr);
// => []
console.log(array);
// => []
// refArr を空にしたつもりが元の配列も空になってしまう

副作用を発生させないためには配列をコピーして使うのが良さそうです。


[参考]

関数型プログラミングの基礎 JavaScriptを使って学ぶ

関数型プログラミングの基礎 JavaScriptを使って学ぶ

👆 この本読んでる。出だしにエニグマ買得のチューリングの話とか出てきて面白い。(超数学的だけど…)

まちカドまぞく (4) (まんがタイムKRコミックス)

まちカドまぞく (4) (まんがタイムKRコミックス)