バグですか?仕様ですか?筋肉です💪

筋肉とプログラミング、その他アウトプットをしていきます。

【js】DOM ノードを追加、置換、削除する

ノードの追加、置換、削除についてまとめます。

HTMLの編集には、innerHTMLプロパティを利用することもできますが、今回紹介する方法だと以下のようなメリットがあります。

  • オブジェクトツリーとして操作できるので、対象となるコンテンツが複雑になった場合にも、コードの可読性が悪くなりにくい
  • 要素、属性とテキストとを区別して扱えるので、ユーザーからの入力によってスクリプトが混入するような危険は回避しやすい

シンプルなコンテンツの編集 → innerHTMLプロパティ

複雑なコンテンツの編集 → 本節のアプローチ

ノードを追加

以下はフォームに入力した内容でリンクを追加するコードです。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>heading.html</title>
</head>
<body>
  <form>
    <div>
      <label for="name">サイト名:</label><br />
      <input id="name" name="name" type="text" size="30" />
    </div>
    <div>
      <label for="url">URL:</label><br />
      <input id="url" name="url" type="text" size="50" />
    </div>
    <div>
      <input id="btn" name="name" type="button" value="追加" />
    </div>
  </form>
  <div id="list"></div>
</body>
<html>
document.addEventListener('DOMContentLoaded', () => {
  document.getElementById('btn').addEventListener('click', () => {
    // テキストボックスを取得
    const name = document.getElementById('name');
    const url = document.getElementById('url');

    // <a>要素を生成
    let link = document.createElement('a');
    // <a>要素のhref属性を設定
    link.href = url.value;

    // テキストノードを生成
    const text = document.createTextNode(name.value);
    // テキストノードを<a>要素の直下に追加
    link.appendChild(text);

    // <br>要素を生成
    const br = document.createElement('br');

    // <div id="list">を取得
    let list = document.getElementById('list');

    // <div>要素の直下に<a>,<br>要素の順番で追加
    list.appendChild(link);
    list.appendChild(br);
  }, false);
}, false);

Image from Gyazo

1. 要素、テキストノードを作成する

コンテンツを追加するにはまず、createElement, createTextNodeメソッドを使用し、追加したい要素、テキストノードを生成します。

今回だと以下のような箇所で行われています。

// <a>要素を生成
let link = document.createElement('a');

// テキストノードを生成
const text = document.createTextNode(name.value);

// <br>要素を生成
const br = document.createElement('br');

他にも生成するノードに応じて以下のようなメソッドがあります

// 要素ノード
createElement(要素名)

// 属性名ノード
createAttribute(属性名)

// テキストノード
createTextNode(テキスト)

createHgehogeでノードを作成した時点では、お互いの階層関係を意識する必要はありません。

現状はバラバラのままなのでこの後の作業で組み立てる必要があります。

2. ノード同士を組み立てる

1の段階でバラバラになっているノードを組み立ててドキュメントに追加する必要があります。

この作業を行うのがappendChildメソッドです

appendChildメソッドは指定された要素を現在の要素の最後の子要素として追加します。

// テキストノードを<a>要素の直下に追加
link.appendChild(text);

link.firstChild;
// "qiita"

// <div>要素の直下に<a>,<br>要素の順番で追加
list.appendChild(link);
list.appendChild(br);

list.children;
//HTMLCollection(2) [a, br]length: 20: atarget: ""download: ""ping: ""rel: ""relList: DOMTokenList [value: ""]hreflang: ""type: ""referrerPolicy: ""text: "qiita"coords: ""charset: ""name: ""rev: ""shape: ""href: "https://qiita.com/"origin: "https://qiita.com"protocol: "https:"username: ""password: ""host: "qiita.com"hostname: "qiita.com"port: ""pathname: "/"search: ""hash: ""title: ""lang: ""translate: truedir: ""hidden: falseaccessKey: ""draggable: truespellcheck: trueautocapitalize: ""contentEditable: "inherit"isContentEditable: falseinputMode: ""offsetParent: bodyoffsetTop: 132offsetLeft: 8offsetWidth: 33offsetHeight: 16style: CSSStyleDeclaration {alignContent: "", alignItems: "", alignSelf: "", alignmentBaseline: "", all: "", …}innerText: "qiita"outerText: "qiita"oncopy: nulloncut: nullonpaste: nullonabort: nullonblur: nulloncancel: nulloncanplay: nulloncanplaythrough: nullonchange: nullonclick: nullonclose: nulloncontextmenu: nulloncuechange: nullondblclick: nullondrag: nullondragend: nullondragenter: nullondragleave: nullondragover: nullondragstart: nullondrop: nullondurationchange: nullonemptied: nullonended: nullonerror: nullonfocus: nullonformdata: nulloninput: nulloninvalid: nullonkeydown: nullonkeypress: nullonkeyup: nullonload: nullonloadeddata: nullonloadedmetadata: nullonloadstart: nullonmousedown: nullonmouseenter: nullonmouseleave: nullonmousemove: nullonmouseout: nullonmouseover: nullonmouseup: nullonmousewheel: nullonpause: nullonplay: nullonplaying: nullonprogress: nullonratechange: nullonreset: nullonresize: nullonscroll: nullonseeked: nullonseeking: nullonselect: nullonstalled: nullonsubmit: nullonsuspend: nullontimeupdate: nullontoggle: nullonvolumechange: nullonwaiting: nullonwheel: nullonauxclick: nullongotpointercapture: nullonlostpointercapture: nullonpointerdown: nullonpointermove: nullonpointerup: nullonpointercancel: nullonpointerover: nullonpointerout: nullonpointerenter: nullonpointerleave: nullonselectstart: nullonselectionchange: nullonanimationend: nullonanimationiteration: nullonanimationstart: nullontransitionend: nulldataset: DOMStringMap {}nonce: ""autofocus: falsetabIndex: 0enterKeyHint: ""onpointerrawupdate: nullnamespaceURI: "http://www.w3.org/1999/xhtml"prefix: nulllocalName: "a"tagName: "A"id: ""className: ""classList: DOMTokenList [value: ""]slot: ""attributes: NamedNodeMap {0: href, href: href, length: 1}shadowRoot: nullpart: DOMTokenList [value: ""]assignedSlot: nullinnerHTML: "qiita"outerHTML: "<a href="https://qiita.com/">qiita</a>"scrollTop: 0scrollLeft: 0scrollWidth: 0scrollHeight: 0clientTop: 0clientLeft: 0clientWidth: 0clientHeight: 0attributeStyleMap: StylePropertyMap {size: 0}onbeforecopy: nullonbeforecut: nullonbeforepaste: nullonsearch: nullelementTiming: ""previousElementSibling: nullnextElementSibling: brchildren: HTMLCollection []firstElementChild: nulllastElementChild: nullchildElementCount: 0onfullscreenchange: nullonfullscreenerror: nullonwebkitfullscreenchange: nullonwebkitfullscreenerror: nullnodeType: 1nodeName: "A"baseURI: "http://localhost:63342/memo/temp.html?_ijt=hbsc1amc7s3585dcv59sjlp0cf"isConnected: trueownerDocument: documentparentNode: div#listparentElement: div#listchildNodes: NodeList [text]firstChild: textlastChild: textpreviousSibling: nullnextSibling: brnodeValue: nulltextContent: "qiita"__proto__: HTMLAnchorElement1: br__proto__: HTMLCollection

ここでは、textを要素ノードlinkに追加した上で、この要素ノードlink、brをlist配下に追加しています。

appendChildメソッドは、insertBeforeメソッドに置き換えることもできます。

list.appendChild(link);
list.insertBefore(link, null);

insertBeforeメソッドは第1引数で指定したノードを、第2引数で指定した子ノードの直前に挿入します。

3. 属性ノードを追加する

属性ノードの設定は、属性と同名のプロパティを設定するだけです。

// <a>要素のhref属性を設定
link.href = url.value;

また、createAttributeメソッドで属性ノードを生成することもできます。

let href = document.createAttribute('href');
href.value = url.value;
anchor.setAttributeNode(href);
  • 属性ノードの値を設定するには、valueプロパティを使用
  • 属性ノードを要素ノードに関連づけるには、appendChild, insertBeforeではなく、setAttributeNodeメソッドを使用

このやり方の方が冗長ですが、属性名を文字列で指定できるので、スクリプトから動的に属性名を変更できるメリットがあります。

既存ノードを置換、削除

先ほどの例に、

  • すでにリンクがあった場合はリンクを変更
  • リンクを削除できる

といった処理を追加します。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>heading.html</title>
</head>
<body>
  <form>
    <div>
      <label for="name">サイト名:</label><br />
      <input id="name" name="name" type="text" size="30" />
    </div>
    <div>
      <label for="url">URL:</label><br />
      <input id="url" name="url" type="text" size="50" />
    </div>
    <div>
      <input id="btn" name="name" type="button" value="追加" />
      <input id="del" type="button" value="削除" disabled/>
    </div>
  </form>
  <div id="list"></div>
</body>
<html>
document.addEventListener('DOMContentLoaded', () => {
  const name = document.getElementById('name');
  const url = document.getElementById('url');
  const del = document.getElementById('del');
  let list = document.getElementById('list');
  let btn = document.getElementById('btn');

  // 追加ボタンがクリックされた時の処理
  btn.addEventListener('click', () => {
    let link = document.createElement('a');
    link.href = url.value;

    const text = document.createTextNode(name.value);
    link.appendChild(text);

    // <div>要素配下に<a>要素が存在するか(リンクが追加されているか)
    if (list.getElementsByTagName('a').length > 0) {
      // 存在している場合はリンクを変更する
      list.replaceChild(link, list.lastChild);
      btn.value = '変更';
    } else {
      // 存在していない場合でサイト名が入力されているかどうか
      if (link.text) {
        // 新しいリンクを追加し、削除ボタン有効、追加ボタンを変更ボタンに
        list.appendChild(link);
        del.disabled = false;
        btn.value = '変更';
      }
    }
  }, false);

  // 削除ボタンがクリックされた時の処理
  del.addEventListener('click', () => {
    // <div id="list">配下の子要素を削除し、削除ボタンを有効、変更ボタンを追加ボタンに
    list.removeChild(list.lastChild);
    del.disabled = true;
    btn.value = '追加';
  }, false)
}, false);

Image from Gyazo

1. ノードを置換する

子ノードを置き換えるには、replaceChildメソッドを使用します。

list.replaceChild(link, list.lastChild);

第1引数に置き換えたいノードを、第2引数に置き換え対象のノードを指定します。

置き換え対象のノードは現在のノードに対する子ノードでならなければいけません。

2. ノードを削除する

子ノードを削除するには、removeChildメソッドを使用します。

list.removeChild(list.lastChild);

引数には、削除したい対象のノードを指定します。

こちらも同様に、対象のノードは現在のノードに対する子ノードでならなければいけません。

【js】DOM イベント操作

属性値やテキストを取得、設定する方法にについてまとめます。

コードを使用したページ

Qiita

特定の属性を取得、設定

多くの属性は要素ノードの同名のプロパティとしてアクセスできます。

// 設定前
document.querySelectorAll('.tr-Item_title')[3];
// <a class=<200b>"tr-Item_title" href=<200b>"/<200b>kaya517/<200b>items/<200b>31657ec26356efaa7688"><200b>[GitHub] 東京都公式 新型コロナウイルス対策サイトがプルリク募集してる[COVID-19]<200b></a><200b>

// aタグのhref属性の値を設定
document.querySelectorAll('.tr-Item_title')[3].href = 'https://b.hatena.ne.jp/';
//"https://b.hatena.ne.jp/"

// 設定後
document.querySelectorAll('.tr-Item_title')[3];
//<a class=<200b>"tr-Item_title" href=<200b>"https:<200b>/<200b>/<200b>b.hatena.ne.jp/<200b>"><200b>[GitHub] 東京都公式 新型コロナウイルス対策サイトがプルリク募集してる[COVID-19]<200b></a><200b>

例えばこのようにするとqiitaの記事なのにはてぶのトップページに飛ぶようになります。

しかし、classはclassNameでないとアクセスできません。

属性とプロパティとの名前の違いを意識したくないならば、getAttribute, setAttributeメソッドを使用しましょう。

// 設定前
document.getElementById('globalHeader').className;
// "st-Header"

// クラス名を設定する
document.getElementById('globalHeader').className = 'hogehoge';
// "hogehoge"

// 設定後
document.getElementById('globalHeader').className;
"hogehoge"


// getAttribute, setAttributeメソッドの場合

// 設定前
document.getElementById('globalHeader').getAttribute('class');
// "hogehoge"

// クラス名を設定する
document.getElementById('globalHeader').setAttribute('class', 'fugafuga');
// undefined

// 設定後
document.getElementById('globalHeader').getAttribute('class');
// "fugafuga"

不特定の属性を取得

特定の要素ノードに属する全ての属性を取得したい場合には、attributesプロパティを使用します。

// 全ての属性を取得
let attrs = document.getElementsByClassName('st-Header_searchInput')[0].attributes;

// forEachを使うために配列に変換
attrs = Array.from(attrs);
// (6) [class, placeholder, type, autocomplete, name, required]

全ての属性を表示
attrs.forEach((attr) => console.log(`${attr.name}: ${attr.value}`));
// class: st-Header_searchInput
// placeholder: キーワードを入力
// type: search
// autocomplete: off
// name: q
// required:

属性を追加、削除するには

// 新規にtitle属性を追加

// 全ての属性を取得
let attrs = document.getElementsByClassName('st-Header_searchInput')[0].attributes;

attrs
// NamedNodeMap {0: class, 1: placeholder, 2: type, 3: autocomplete, 4: name, 5: required, class: class, placeholder: placeholder, type: type, autocomplete: autocomplete, name: name, …}

let title = document.createAttribute('title');

title.value = 'タイトル';

title
// title=<200b>"タイトル"

// 属性を追加
attrs.setNamedItem(title);

attrs
// NamedNodeMap {0: class, 1: placeholder, 2: type, 3: autocomplete, 4: name, 5: required, 6: title, class: class, placeholder: placeholder, type: type, autocomplete: autocomplete, name: name, …}

// 既存のname属性を削除

// 属性を削除
attrs.removeNamedItem('name');

attrs
// NamedNodeMap {0: class, 1: placeholder, 2: type, 3: autocomplete, 4: required, 5: title, class: class, placeholder: placeholder, type: type, autocomplete: autocomplete, required: required, …}

テキストを取得、設定

要素は以下のテキストを取得、設定するにはinnerHTML, textContentというプロパティを使用します。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>heading.html</title>
</head>
<body>
  <div id="text">
    <p style="color: Red">設定なし</p>
  </div>
  <div id="html">
    <p style="color: Red">設定なし</p>
  </div>
</body>
<html>
document.addEventListener('DOMContentLoaded', function() {
  document.getElementById('text').textContent = '<a href="https://qiita.com/">qiitaへのリンク</a>';
  document.getElementById('html').innerHTML = '<a href="https://qiita.com/">qiitaへのリンク</a>';
}, false);

f:id:takuma521:20200308183614p:plain

両者のプロパティに共通しているのは、

配下の子要素、テキストを完全に置き換えている

元々あった

要素は残っていません。

異なる点は、

与えられたテキストをHTML文字列として認識するかどうかです。

textContentプロパティでは、プレーンテキストとして埋め込まれるので、タグ文字列がそのまま表示されています。

innerHTMLプロパティでは、HTMLテキストを埋め込むのでリンクが有効になっています。

一般的に、HTML文字列を埋め込むのでなければ、textContentプロパティを優先して使用したほうが、高速でかつ、セキュリティー上の問題も発生しにくいです。

【js 】DOM 文書ツリー間を行き来する ノードウォーキング

getElementByIdやquerySelectorAllメソッドはいずれもピンポイントで特定の要素ノード(群)を取得するためのメソッドです。

しかし、いちいち文書全体から目的の要素を検索するのは無駄が多くそれはそのままパフォーマンス低下の原因にもなります。

そこでDOMではあるノードを起点として相対的な位置関係からノードを取得することもできます。

これをノードウォーキングと呼びます。

コードを使用したページ

Qiita

// カレントノード
const currentNode = document.querySelectorAll('.tr-Item')[3];
currentNode
// <div class=<200b>"tr-Item"><200b><a class=<200b>"tr-Item_userImage" href=<200b>"/<200b>tomikiya"><200b>…<200b></a><200b><div class=<200b>"tr-Item_body"><200b>…<200b></div><200b></div><200b>

// 親ノードを取得
currentNode.parentNode
// <div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>…<200b></div><200b><div class=<200b>"tr-Item"><200b>....


// 最初の子ノードを取得
currentNode.firstChild
// <a class=<200b>"tr-Item_userImage" href=<200b>"/<200b>tomikiya"><200b><img src=<200b>"https:<200b>/<200b>/<200b>qiita-user-profile-images.imgix.net/<200b>https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F97601%2Fprofile-images%2F1473707244?ixlib=rb-1.2.2&auto=compress%2Cformat&lossless=0&w=48&s=f1b22bd97397df6085c15efc4fbc0ad4" alt=<200b>"tomikiya"><200b></a><200b>

// 最後の子ノードを取得
currentNode.lastChild
// <div class=<200b>"tr-Item_body"><200b><a class=<200b>"tr-Item_title" href=<200b>"/<200b>tomikiya/<200b>items/<200b>11d7a21ce778038567c9"><200b>[Excel]実務で役に立たない!?VBAを使わないで3D迷路を作る<200b></a><200b><div class=<200b>"tr-Item_meta"><200b>…<200b></div><200b></div><200b>

// 子ノードに含まれるノード数を取得
currentNode.childNodes.length
// 2

// 直前のノード(兄ノード)
currentNode.previousSibling
// <div class=<200b>"tr-Item"><200b><a class=<200b>"tr-Item_userImage" href=<200b>"/<200b>youwht"><200b>…<200b></a><200b><div class=<200b>"tr-Item_body"><200b>…<200b></div><200b></div><200b>
// カレントノードが[3]番目を取得していたのに対し、previousSiblingは1つ手前の[2]番目のノードを取得している
currentNode.previousSibling === document.querySelectorAll('.tr-Item')[2];
//true

// 直後のノード(弟ノード)
currentNode.nextSibling
// <div class=<200b>"tr-Item"><200b><a class=<200b>"tr-Item_userImage" href=<200b>"/<200b>mi_upto"><200b>…<200b></a><200b><div class=<200b>"tr-Item_body"><200b>…<200b></div><200b></div><200b>
// カレントノードが[3]番目を取得していたのに対し、nextSiblingは1つ後の[4]番目のノードを取得している
currentNode.nextSibling === document.querySelectorAll('.tr-Item')[4];
// true

これでノードウォーキングをすることができますが、以下の例を見てください。

コードを使用したページ

スタック・オーバーフロー

// カレントノード取得
const currentNode = document.getElementsByTagName('li')[0];

// 弟ノード取得?
currentNode.nextSibling;
// #text

// ノードの種類判定
currentNode.nextSibling.nodeType
// 3(テキストノード)

実はnextSiblingなど上記で紹介した取得方法では、要素ノード以外も取得してしまいます。

この場合だとテキストノードを取得してしまいます。

currentNode.nodeType
// 1(要素ノード)

要素ノードを取り出したい場合はnodeTypeが1であるのでその条件で取得するのもありですが、

const nextNode = document.getElementsByTagName('li')[1];
nextNode
// <li class=<200b>"-item inbox-button-item"><200b>…<200b></li><200b>

currentNode.nextElementSibling;
// <li class=<200b>"-item inbox-button-item"><200b>…<200b></li><200b>

currentNode.nextElementSibling.nodeType
// 1

currentNode.nextElementSibling === nextNode;
// true

のようにnextElementSiblingを使えば要素ノードに限定してくれます。

// 最初の子要素ノードを取得
currentNode.firstElementChild

// 最後の子要素ノードを取得
currentNode.lastElementChild

// 直前の要素ノードを取得
currentNode.perviousElementSibling

// 直後の要素ノードを取得
currentNode.nextElementSibling

// 子ノードに含まれる要素ノード数を取得
currentNode.childElementCount

// 親ノードを取得
currentNode.parentNode

ちなみにnodeTypeの一覧です。

要素ノード    = 1
属性ノード    = 2
テキストノード  = 3
CDATAセクション = 4
実体参照ノード  = 5
実体宣言ノード  = 6
処理命令ノード  = 7
コメントノード  = 8
文書ノード    = 9
文書型宣言ノード = 10
文書の断片    = 11
記法宣言ノード  = 12

【js】DOM 要素の検索まとめ

DOM(Document Object Model)はHTMLをJavascriptから操作できるようにしたインターフェイスのことです。

JavascriptからはDOM APIを通じてHTMLの情報を取得、変更、イベントの登録などができます。

今回は情報を取得するために要素の検索についてまとめます。

コードを使用したページ

Qiita

id指定

指定したIDを持つ要素をElementオブジェクトとして返します。

document.getElementById('globalHeader');
// <div class=<200b>"st-Header" id=<200b>"globalHeader"><200b>…<200b></div><200b>

タグ名指定

タグ名をキーに要素を取得します。

document.getElementsByTagName('form');
// HTMLCollection(2) [form.st-Header_search, form.st-Header_searchModal]
// length: 2
// 0: form.st-Header_search
// 1: form.st-Header_searchModal
// __proto__: HTMLCollection

idをキーにする場合と異なり、複数の要素を取得する可能性があるので、getElementsByTagNameの戻り値は要素の集合になります。

また、引数に*を指定することで全ての要素を取得することもできます。

const allTag = document.getElementsByTagName('*');
// HTMLCollection(1040) [html, head, meta, title, meta, meta, meta, link, link, meta,,,,,

// リストに含まれる要素数
allTag.length
// 1040

// i番目の要素を取得(allTag[0]でも取得可能)
allTag.item(0)
// <html><200b><head><200b>…<200b></head><200b><body data-__gyazo-extension-added-paste-support=<200b>"true"><200b>…<200b></body><200b></html><200b>

// id,またはname属性が一致する要素を取得
allTag.namedItem('globalHeader');
// <div class=<200b>"st-Header" id=<200b>"globalHeader"><200b>…<200b></div><200b>

// 属性にアクセスする
allTag[11]
// <link rel=<200b>"shortcut icon" type=<200b>"image/<200b>x-icon" href=<200b>"https:<200b>/<200b>/<200b>cdn.qiita.com/<200b>assets/<200b>favicons/<200b>public/<200b>production-c620d3e403342b1022967ba5e3db1aaa.ico"><200b>
allTag[11].type
// "image/x-icon"

getElementsByTagNameメソッドの戻り値であるHTMLCollectionオブジェクトはlength, item(i), namedItem(name)が利用できます。

name属性指定

document.getElementsByName('q');
// NodeList(2) [input.st-Header_searchInput, input.st-Header_searchModalInput]

getElementsByNameメソッドでは戻り値がNodeListですが、namedItemメソッドが利用できないくらいで、ほぼHTMLCollectionと同じです。

class指定

class属性をキーに要素群(HTMLCollection)を取得します。

document.getElementsByClassName('st-Header st-Footer');
// HTMLCollection [div#globalHeader.st-Header, globalHeader: div#globalHeader.st-Header]

document.getElementsByClassName('class1 class2');のように空白区切りで複数のクラス名を記述することで、class1, class2両方が指定された要素だけを検索します。

document.getElementsByClassName('st-HeaderAlert st-HeaderAlert-warning');
// HTMLCollection [div.st-HeaderAlert.st-HeaderAlert-warning]

cssセレクターで指定

これまでのgetHogehogeメソッドが特定の名前、属性値をキーに要素を検索していたのに対して、

より、複雑な条件での検索を可能にするのが、querySelector, querySelectorAllメソッドです。

querySelectorは合致した最初の要素を返しquerySelectorAllは合致した全ての要素を返します。

// 最初に合致した要素を取得
document.querySelector('.tr-Item');
// div.tr-Item

// 合致した全ての要素を取得
document.querySelectorAll('.tr-Item');
// NodeList(30) [div.tr-Item, div.tr-Item, div.tr-Item, div.tr-Item,,,,
// 全ての要素を取得 *
document.querySelectorAll('*');
// NodeList(1040) [html, head, meta, title, meta, meta, meta, link,,,,,

// 指定したIDの要素を取得 #id
document.querySelector('#globalHeader');
// <div class=<200b>"st-Header" id=<200b>"globalHeader"><200b>…<200b></div><200b>

// 指定したクラス名の要素を取得 .class
document.querySelectorAll('.tr-Item');
// NodeList(30) [div.tr-Item, div.tr-Item, div.tr-Item, div.tr-Item,,,,

// 指定したタグ名を取得 elem
document.querySelectorAll('form');
// NodeList(2) [form.st-Header_search, form.st-Header_searchModal]

// parent要素の子要素childを取得
document.querySelectorAll('.st-Header_start > form');
// NodeList [form.st-Header_search]

// ancestor要素の子孫要素descendantを全て取得
document.querySelectorAll('html form');
NodeList(2) [form.st-Header_search, form.st-Header_searchModal]

// prev要素の直後のnext要素を取得
document.querySelector('.tr-Item + div');
// <div class=<200b>"tr-Item"><200b>…<200b></div><200b>

// prev要素の以降のsiblings兄弟要素を取得
document.querySelectorAll('.tr-Item ~ div');
// NodeList(29) [div.tr-Item, div.tr-Item, div.tr-Item, div.tr-Item,,,,,

// 指定した属性を持つ要素を取得
document.querySelector('input[type]');
// <input class=<200b>"st-Header_searchInput" placeholder=<200b>"キーワードを入力" type=<200b>"search" autocomplete=<200b>"off" name=<200b>"q" required><200b>

// 属性がvalue値に等しい要素を取得
document.querySelector('input[type= "text"]');
// <input class=<200b>"st-Header_searchModalInput" type=<200b>"text" autocomplete=<200b>"off" placeholder=<200b>"キーワードを入力" name=<200b>"q" required><200b>

// 属性がvalueから始まる値をもつ要素を取得
document.querySelector('a[href^="http"]');
// <a class=<200b>"st-Header_dropdownItem st-RealmItem" href=<200b>"https:<200b>/<200b>/<200b>qiita.com/<200b>"><200b>…<200b></a><200b><200b>

// 属性がvalueで終わる値をもつ要素を取得
document.querySelector('img[src$=".png"]');
// <img alt=<200b>"COTOHA API" src=<200b>"/<200b>/<200b>cdn.qiita.com/<200b>assets/<200b>public/<200b>qiita-x-cotoha-4424ecc70ff95b4106a44dab2749eade.png"><200b>

// 属性がvalueを含む値をもつ要素を取得
document.querySelector('[title*="Qiita"]');
// <link href=<200b>"/<200b>opensearch.xml" rel=<200b>"search" title=<200b>"Qiita" type=<200b>"application/<200b>opensearchdescription+xml"><200b>

// 複数の属性フィルタ全てにマッチする要素を取得
document.querySelector('img[src][alt]');
<img src=<200b>"https:<200b>/<200b>/<200b>qiita-user-profile-images.imgix.net/<200b>https%3A%2F%2Fpbs.twimg.com%2Fprofile_images%2F1050777844039573504%2FP4feUgGa_bigger.jpg?ixlib=rb-1.2.2&auto=compress%2Cformat&lossless=0&w=48&s=ca8cb412d7885df16ba5dbae92cba870" alt=<200b>"t_fit_engineer"><200b>

【js】ES6 モジュール

ES6から言語としてモジュールがサポートされました。

モジュールとは、関連性を持たせて切り離された一塊のコード群のことです。

モジュールのメリット

モジュールを使うことで、以下のようなメリットがあります。

  1. 他のコードとの依存性が少なくなるので、変更や拡張がしやすくなる
  2. モジュール毎に適切な変数名(名前空間)を割り当てることで、変数名の競合リスクを減らすことができる
  3. どの機能がどこに書かれているのかが把握しやすくなる
  4. 汎用性の高いコードをモジュールにすることで再利用しやすくなる

モジュールの基本

// lib/Util.js
const AUTHOR = 'tappei nagatsuki'

export class Anime {
  constructor(title) {
    this.title = title;
  }

  callTitle() {
    return this.title;
  }
}

export class Workout {
  constructor(name) {
    this.name = name;
  }

  getTrainingVolume(weight, rep, set) {
    return `${}のトレーニングボリュームは${weight * rep * set}kgです`;
  };
}

モジュールは1つのファイルとして定義するのが基本です。

この例だとlib/Util.jsがそれです。

モジュールとして外部から使用するためにはexportキーワードを記述します。

ここでは、Animeクラス、Workoutクラスに記述していますが、それ以外に、

変数、定数、関数の宣言にも記述できます。

定数AUTHORにはexportの記述がないため外から参照することはできません。

// main.js
import { Anime, Workout } from './lib/Util'

const anime = new Anime('リゼロ');
console.log(anime.callTitle());
// リゼロ

const workout = new Workout('ベンチプレス');
console.log(workout.getTrainingVolume(100, 10, 5));
// ベンチプレスのトレーニングボリュームは5000kgです

別ファイルで定義されたモジュールをインポートするためにはimport命令が必要です。

ここでは、Animeクラス、Workoutクラスをインポートしています。

import命令の様々な記法

1. モジュール全体をまとめてインポート

// main2.js
import * as app from './lib/Util'

const anime = new app.Anime('リゼロ');
console.log(anime.callTitle());
// リゼロ

const workout = new app.Workout('ベンチプレス');
console.log(workout.getTrainingVolume(100, 10, 5));
// ベンチプレスのトレーニングボリュームは5000kgです

アスタリスク*でモジュール内の全ての要素をインポートできます。

この場合as句で別名を指定する必要があります。

2. モジュール内の個々の要素に別名を付与する

// main2.js
import { Anime as MyAnime, Workout as MyWorkout } from './lib/Util'

const anime = new MyAnime('リゼロ');
console.log(anime.callTitle());
// リゼロ

const workout = new MyWorkout('ベンチプレス');
console.log(workout.getTrainingVolume(100, 10, 5));
// ベンチプレスのトレーニングボリュームは5000kgです

モジュールの個々の要素に別名を付与することもできます。

3. デフォルトのエクスポートをインポートする

// lib/Anime.js

export default class {
  constructor(title) {
    this.title = title;
  }

  callTitle() {
    return this.title;
  }
}

モジュールに含まれる要素が1つであれば、デフォルトのエクスポートを宣言できます。

クラス名、関数名を書かずにdefaultキーワードをつけます。

// main_default.js
import Anime from './lib/Anime'

const anime = new Anime('リゼロ');
console.log(anime.callTitle());
// リゼロ

import命令により、Animeモジュールのデフォルトエクスポートに対して、Animeという名前でアクセスできます。

【js】ES6 オブジェクトリテラル

オブジェクトリテラル

ES6ではオブジェクトリテラルの構文もシンプルに表現できるようになりました。

const title = 'この素晴らしい世界に祝福を';
const mainCharacter = 'カズマ';

const Anime = {
  title: title,
  mainCharacter: mainCharacter,
  subTitle1: 'この自称女神と異世界転生を!',
  subTitle2: 'この中二病に爆焔を!',
  subTitle3: 'この右手にお宝(ぱんつ)を!',
  callInformation: function() {
    return console.log(`このアニメのタイトルは「${this.title}」です。主人公の名前は「${this.mainCharacter}」です。`);
  }
};

Anime.callInformation();
// このアニメのタイトルは「この素晴らしい世界に祝福を」です。主人公の名前は「カズマ」です。

console.log(Anime.subTitle1);
// この自称女神と異世界転生を!
console.log(Anime.subTitle2);
// この中二病に爆焔を!
console.log(Anime.subTitle3);
// この右手にお宝(ぱんつ)を!

これが以前の書き方ですが、

  • メソッド(関数型のプロパティ)としての定義をシンプルにできる
  • 変数を同名のプロパティに割り当てられる
  • プロパティを動的に生成できる

ことができるようになりました。

メソッド(関数型のプロパティ)としての定義をシンプルにできる

メソッド(関数型のプロパティ)としての定義をする場合には、: functionを省略できます。

const Anime = {
  callInformation() {
    return console.log(`このアニメのタイトルは「${this.title}」です。主人公の名前は「${this.mainCharacter}」です。`);
  }

変数を同名のプロパティに割り当てられる

プロパティ名: 変数名と記述していましたが、

プロパティ名と変数名が同名の場合は省略して以下のように書けます。

const Anime = {
  title,
  mainCharacter,

プロパティを動的に生成できる

プロパティ名をブラケット([]のこと)でくくり動的にプロパティ名を生成できます。

これをComputed property namesと呼ぶそうです。

let i = 0;

const Anime = {
  [`subTitle${++i}`]: 'この自称女神と異世界転生を!',
  [`subTitle${++i}`]: 'この中二病に爆焔を!',
  [`subTitle${++i}`]: 'この右手にお宝(ぱんつ)を!',

変数iに順に加算することでsubTitle1, subTitle2, subTitle3 ...のようなプロパティ名を生成できます。

まとめ

ES6からオブジェクトリテラルが以下のようによりシンプルに書けるようになった。

  • メソッド(関数型のプロパティ)としての定義をシンプルにできる
  • 変数を同名のプロパティに割り当てられる
  • プロパティを動的に生成できる
const title = 'この素晴らしい世界に祝福を';
const mainCharacter = 'カズマ';
let i = 0;

const Anime = {
  title,
  mainCharacter,
  [`subTitle${++i}`]: 'この自称女神と異世界転生を!',
  [`subTitle${++i}`]: 'この中二病に爆焔を!',
  [`subTitle${++i}`]: 'この右手にお宝(ぱんつ)を!',
  callInformation() {
    return console.log(`このアニメのタイトルは「${this.title}」です。主人公の名前は「${this.mainCharacter}」です。`);
  }
};

Anime.callInformation();
// このアニメのタイトルは「この素晴らしい世界に祝福を」です。主人公の名前は「カズマ」です。

console.log(Anime.subTitle1);
// この自称女神と異世界転生を!
console.log(Anime.subTitle2);
// この中二病に爆焔を!
console.log(Anime.subTitle3);
// この右手にお宝(ぱんつ)を!

【js】クラスを定義する

ES6からjsでもclassを定義することができるようになりました。

オブジェクト指向に関わる箇所なので、是非マスターしたいところです。

オブジェクト指向については以前記事にまとめてあります。

https://takuma521.hatenablog.com/entry/2019/07/15/214850

クラスの定義

class Anime {
  // コンストラクター
  constructor(options) {
    this.title = options.title;
  }

  // メソッド
  callTitle() {
    return this.title;
  }
}

const anime = new Anime({ title: 'リゼロ' });

console.log(anime);
// Anime {title: "リゼロ"}
console.log(anime.callTitle());
// リゼロ

コンストラクターの名前はconstructorで固定です。

※jsではpublic, protected, privateのようなアクセス修飾子は利用できません。 jsのクラスでは、全てのメンバーがpublicとなります。(どこからでもアクセスできる)

継承

継承とは、クラスの定義の共通する部分を別のクラスにまとめることです。

そうすることで、コードの重複を防ぐことができます。

class Rezero extends Anime {
  constructor(options) {
    super(options);
    this.deathCount = options.deathCount;
  }

  returnToDeath() {
    return `${this.deathCount}回死に戻りました。`;
  }
}

const rezero = new Rezero ({ title: 'リゼロ', deathCount:4 })

console.log(rezero);
// Rezero {title: "リゼロ", deathCount: 4}

console.log(rezero.callTitle());
// リゼロ

console.log(rezero.returnToDeath());
// 4回死に戻りました。

クラスを継承するにはextendsを使います。

Rezeroクラスで定義されたreturnToDeath()メソッドはもちろん、

Animeクラスで定義されていたcallTitle()メソッドも呼び出すことができます。

オーバーライド

基底クラス(Animeクラス)で定義されたメソッド、コンストラクターはサブクラス(Rezeroクラス)で上書きすることもできます。

これを、メソッドのオーバーライドと言います。

今回は基底クラス(Animeクラス)で定義されていたcallTitle()メソッドにサブタイトルも返すようにオーバーライドします。

class Rezero extends Anime {
  constructor(options) {
    super(options);
    this.deathCount = options.deathCount;
    this.subTitle = options.subTitle;
  }

    callTitle() {
      return `${this.title} ${this.subTitle}`;
    }

  returnToDeath() {
    return `${this.deathCount}回死に戻りました。`;
  }
}

const rezero = new Rezero ({ title: 'リゼロ', deathCount:4, subTitle: '18話 ゼロから' })

console.log(rezero);
// Rezero {title: "リゼロ", deathCount: 4, subTitle: "18話 ゼロから"}

console.log(rezero.callTitle());
// リゼロ 18話 ゼロから

console.log(rezero.returnToDeath());
// 4回死に戻りました。

コンストラクターの引数にはオブジェクトを渡しているので、コンストラクターthis.subTitle = options.subTitle;を追記します。

そして、インスタンス化の際にsubTitleを追加します。

callTitle()メソッドの中身を書き換えます。

すると、callTitle()を呼び出すとtitleだけでなく、subTitleも返すようになります。