LoginSignup
3
0

JavaScript PrimerのTodoアプリに出てきた難しい関数部分のメモ

Last updated at Posted at 2024-03-14

プログラミング学習中に取り組んだjsprimerにて自分の知らないメソッドやリテラルなどが複数使われており、なかなか理解するのが難しかった箇所について整理してみました。

具体的にはTodoアプリの項目で出てきたhtml-util.jsのHTML文字列→HTML要素→DOMに変換する一連の流れを行う関数群の記述がいまいち理解できなかったのでその部分になります。

html-util.js

export function escapeSpecialChars(str) {
    return str
        .replace(/&/g, "&")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
}

/**
 * HTML文字列からHTML要素を作成して返す
 * @param {string} html 
 */
export function htmlToElement(html) {
    const template = document.createElement("template");
    template.innerHTML = html;
    return template.content.firstElementChild;
}

/**
 * HTML文字列からDOM Nodeを作成して返すタグ関数
 * @return {Element}
 */
export function element(strings, ...values) {
    const htmlString = strings.reduce((result, str, i) => {
        const value = values[i - 1];
        if (typeof value === "string") {
            return result + escapeSpecialChars(value) + str;
        } else {
            return result + String(value) + str;
        }
    });
    return htmlToElement(htmlString);
}

/**
 * コンテナ要素の中身をbodyElementで上書きする
 * @param {Element} bodyElement コンテナ要素の中身となる要素
 * @param {Element} containerElement コンテナ要素
 */
export function render(bodyElement, containerElement) {
    // containerElementの中身を空にする
    containerElement.innerHTML = "";
    // containerElementの直下にbodyElementを追加する
    containerElement.appendChild(bodyElement);
}

escapeSpecialChars(str)について

export function escapeSpecialChars(str) {
    return str
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
}

この部分は入力フォームにタグなどを記述して送信された場合に、そのまま処理しない方が良いものを置換するための関数になっています。
String to Stringの処理なのでそこまで複雑なものではなかったです。

htmlToElement(html)について

export function htmlToElement(html) {
    const template = document.createElement("template");
    template.innerHTML = html;
    return template.content.firstElementChild;
}

引数にhtmlを入れるとDOM要素にして返す。

document.createElement("template");

let element = document.createElement(tagName[, options]);

htmlToElement関数の中で使われているdocument.createElementは引数にタグ名を入れると
その要素(element)を返す。

element = document.createElement("div");
とした場合はdivのelementが返されて、

templateを入れているので
templateタグが返される。

templateタグについて

HTML5から追加されたもの。
HTMLの要素で、ページが読み込まれたときにレンダリング(表示)されずに、javascriptを使って後からインスタンス化することができるHTML。

名前の通り雛形的なものをここに記述しておいて、todoなら新しいtodoを保存するたびに雛形としてcloneして利用する。

HTML文字列からDOM Nodeを作成して返すタグ関数

/**
 * HTML文字列からDOM Nodeを作成して返すタグ関数
 * @return {Element}
 */
export function element(strings, ...values) {
    const htmlString = strings.reduce((result, str, i) => {
        const value = values[i - 1];
        if (typeof value === "string") {
            return result + escapeSpecialChars(value) + str;
        } else {
            return result + String(value) + str;
        }
    });
    return htmlToElement(htmlString);
}

テンプレートリテラルとは

文字列を表すときにシングルクォート、ダブルクォートで囲ったものを文字列リテラルのに対して
バッククォートで囲って記述したものをテンプレートリテラルという。

テンプレートリテラルの違い。
改行文字がそのまま改行として扱われる。
文字列リテラルの場合に改行したいときは\nなどを記述する必要があるが、
テンプレートリテラルの場合は普通に改行をするだけで表示上も改行される。

`<ul>
<li>todoitem</li>
</ul>`

のような記述をするとしっかり改行されて表示してくれる。

テンプレートリテラルでは文字列の中に式を埋め込むことができる。

${式}
通常は式の部分に変数を入れて使う。

タグ関数(タグ付きテンプレートリテラル)

fn();
のような括弧内に引数を入れて実行するような書き方ではなく、
fnはじめまして${user}さん;
のように関数にテンプレートリテラルを続けて記述して実行する。

このときに

function fn(strings, name) {
  const first = strings[0];
  const second = strings[1];
  console.log(`${first}${user}${second}`)
}

のようなタグ関数を定義していた場合に

fn`はじめまして${user}さん`;

と実行した場合、

stringsに対して["はじめまして", "さん"]が配列として入り、
nameの部分には${user}が変数として入る。

fn(["はじめまして", "さん"], user)のような形になってから実行されているようなイメージ。

残余引数構文

function f(a, b, ...theArgs) {
  // ...
}

関数が不定数の引数を配列として受け入れることができ、可変長引数関数をjavascriptで表すことができる。

引数の数が決まっていない場合に利用する記述。

タグ関数に使う場合
はじめまして${user}さん
はじめまして${user}さん、{name}さん
などのようにテンプレートリテラルの中式展開(プレースホルダー)の数が変わるケースに用いられる。

なので下記の記述のやっていることとしては

/**
 * HTML文字列からDOM Nodeを作成して返すタグ関数
 * @return {Element}
 */
export function element(strings, ...values) {
    const htmlString = strings.reduce((result, str, i) => {
        const value = values[i - 1];
        if (typeof value === "string") {
            return result + escapeSpecialChars(value) + str;
        } else {
            return result + String(value) + str;
        }
    });
    return htmlToElement(htmlString);
}

第二引数に残余引数を利用したタグ関数にテンプレートリテラルとして

`<ul>
    <li>新しい要素</li>
</ul>`;

を渡している記述になる。

この場合 

newElement = element`<ul>
  <li>新しい要素</li>
</ul>;

風に渡している、このとき中にプレースホルダーはないので
第一引数に全てが入り、第二引数には何も入らない。

example

function element(strings, ...values) {
  console.log(strings);
  console.log(values);
}

const todoElement = element`<ul>
    <li>新しい要素</li>
</ul>`;

スクリーンショット 2024-03-11 0.27.51.png

この場合プレースホルダーが含まれていないので第二引数は空になる。

function element(strings, ...values) {
  console.log(strings);
  console.log(values);
}

const name = "tarou";
const user = "user";
const todoElement = element`<ul>
${name}
    <li>新しい要素</li>
${user}
</ul>`;

スクリーンショット 2024-03-11 0.30.42.png

この場合はプレースホルダーごとに分割された文字列が一つずつ配列になって第一引数に入っており、プレースホルダーは配列にまとめられて第二引数に入る。

reduce( )

配列からひとつの値を求めるときに使われる。
array.eachのように配列の要素一つずつに対して順番に処理を回していく関数。

array.reduce(callback関数)
または
array.reduce(callback関数, 初期値)
で記述する。

第一引数に入れるcallback関数にも書き方があり、callback関数の引数は

第一引数:accumulator
第二引数:currentValue
第三引数:currentIndex
第四引数:array

となっている。

(※reduceの第一引数に入れるcallback関数の引数群であり、 reduce自体の1-4の引数ではないので注意。)

第一引数は前回のcallback関数実行時にreturnで返った値が入る。
初回の呼び出し時はreduce自身に第二引数で初期値を設定しているかどうかで動作が変わり、初期値が設定されている場合は初期値が入り、そうでない場合はarray[0]が入る。

第二引数は現在の要素の値が入る(array[x])。こちらも初回の呼び出し時はreduce自身に初期値が渡されているかどうかで動作が変わり、初期値が設定されている場合はarray[0],そうでない場合array[1]が入る。

まとめるとreduceの第二引数に初期値が渡されている場合

accumulator currentValue
初期値 array[0]

初期値なしの場合

accumulator currentValue
array[0] array[1]

になる。

const htmlString = strings.reduce((result, str, i) => {
        const value = values[i - 1];
        if (typeof value === "string") {
            return result + escapeSpecialChars(value) + str;
        } else {
            return result + String(value) + str;
        }
    });

今回はreduce(callback関数)のみで第二引数に初期値がない形で
中のcallback関数は
result, str, iでaccumulator, currentValue, currentIndex
第四引数のarray(配列自身)は渡していない形になってます。

  1. resultは前回のcallback関数の呼び出し結果(returnの値)
  2. strは現在の値(array[x]になる)
  3. iはインデックス(1始まり)

例えば
上のケースだと
strings=["<ul>", "<li>新しい要素</li>", "</ul>"]
values=["tarou", "user"]
のようになっていて、
reduceも配列に対してeachのような動きをするため、

一巡目

resultはstrings[0]が入る
strはstrings[1]
iは1始まりのためi==1
処理は
values[i-1]はvalues[0]
values[0] "tarou"なので
value = values[0]を行なって
valueに"tarou"を入れる。

typeof value == "string"でvalueの中身が文字列か調べて
文字列な場合
return result + escapeSpecialChars(value) + str;
なので
return strings[0] + escapeSpecialChars(value[0]) + strings[1];
となり、

return "<ul>" + "tarou" + "<li>新しい要素</li>";

返り値は "<ul>tarou<li>新しい要素</li>";

この返り値は二巡目のresultに入る

二巡目

resultは1巡目のcallback関数の返り値が入る、なのでresultには
"<ul>tarou<li>新しい要素</li>";が入っている
strはstrings[2]
iは1始まりの二巡目なのでi==2
処理は
values[i-1]values[1]
values[1] == "user"なので
value = values[1]を行なって
value"user"を入れる。

typeof value == "string"でvalueの中身が文字列かどうかを調べて

return result + escapeSpecialChars(value) + str;
return "<ul>tarou<li>新しい要素</li>" + escapeSpecialChars(value[1]) + strings[2];

となり、

return "<ul>tarou<li>新しい要素</li>" + "user" + "</ul>";

返り値は

"<ul>tarou<li>新しい要素</li>" + "user" + "</ul>";

この返り値は三巡目のresultに入る

三巡目

stringsの数的に実行はされるはずだが、ほとんどnilが入るのでresultに影響がないっぽい。

function elementのまとめ

/**
 * HTML文字列からDOM Nodeを作成して返すタグ関数
 * @return {Element}
 */
export function element(strings, ...values) {
    const htmlString = strings.reduce((result, str, i) => {
        const value = values[i - 1];
        if (typeof value === "string") {
            return result + escapeSpecialChars(value) + str;
        } else {
            return result + String(value) + str;
        }
    });
    return htmlToElement(htmlString);
}
element`<ul>
    <li>新しい要素</li>
</ul>`;

をすると、プレースホルダーがなければそのまま同じやつが文字列置換だけされたあとにDOMの要素として返される。

まとめ

正直入口(引数)と出口(return)だけはわかっていたのでなんとなく使うことはできていたが、reduceやタグ関数、テンプレートリテラル、剰余引数などの理解出来て良かった。

参考

3
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
3
0