この記事はJavaScript Advent Calendar 2016の9日目の記事です。
jQueryのセレクタはめちゃくちゃ楽なので多用しているのですが、最近ちょこちょこRiot.jsを使い始めたこともあり、jQueryを使うのもいいけど出来れば外部ライブラリは最小限にして書きたいよねーってことで、jQueryでこうやって書いてたの、ネイティブのJavaScriptで実現する場合どうやるんだっけ?という備忘録。
- jQuery3.1.1
- GoogleChrome54で検証
- HTML5が解釈できるWebブラウザなら動くはず (
IE?知らない子ですね……)
セレクタ
Selectors API
jQueryでお馴染みCSSセレクタライクのセレクタをネイティブなJavaScriptでも使えるように実現しているのがSelectors APIです。
覚えるのはたった2つだけ!
- document.querySelector : 文書中に存在する最初の要素(elementオブジェクト)を取得 (取得できなかった場合はnull)
- [document.querySelectorAll]
(https://developer.mozilla.org/ja/docs/Web/API/Document/querySelectorAll) : 文書中に存在するすべての要素リスト(NodeList)を取得 (取得できなかった場合は空のNodeList)
element.querySelector(All)としても使えます。
$('div#hoge'); // <- jQueryオブジェクト
$('div.fuga'); // <- jQueryオブジェクト
document.querySelector('div#hoge'); // <- element
document.querySelectorAll('div.fuga'); // <- NodeList
// そのほか
document.querySelectorAll('*');
document.querySelectorAll('div, p');
document.querySelectorAll('#hoge .fuga');
document.querySelector('#hoge').querySelectorAll('.fuga'); // $('#hoge').find('.fuga');
Selectors APIで使えないセレクタ
以下のセレクタはjQueryの独自拡張のため、Selectors APIでは使えません。
フォーム絡みのもの
$(':button'); // -> querySelectorAll('button, input[type="button"]')
$(':checkbox'); // -> querySelectorAll('input[type="checkbox"]')
$(':file'); // -> querySelectorAll('input[type="file"]')
$(':image'); // -> querySelectorAll('input[type="image"]')
$(':hidden'); // -> querySelectorAll('[type="hidden"]')
$(':password'); // -> querySelectorAll('[type="password"]')
$(':radio'); // -> querySelectorAll('[type="radio"]')
$(':reset'); // -> querySelectorAll('[type="reset"]')
$(':text'); // -> querySelectorAll('[type="text"]')
$(':input');
$(':submit');
- :button Selector
- :checkbox Selector
- :file Selector
- :image Selector
- :hidden Selector
- :password Selector
- :radio Selector
- :reset Selector
- :text Selector
- :input Selector
- :submit Selector
タグ絡みのもの
:header
要素の状態を表すもの
:animated, :visible, :hidden, :selected
要素の位置を表すもの
:first, :last, :parent, :odd, :even
その他
[name!="value"],
:contains(), :eq(), :gt(), :lt(), :has(),
- Attribute Not Equal Selector [name!=”value”]
- :contains() Selector
- :eq() Selector
- :gt() Selector
- :lt() Selector
- :has() Selector
ほとんどフォーム処理ですね。
selected,visible,hiddenあたりはないと困るかもしれません。
解釈できないセレクタが指定された場合、DOMException例外が発生します。
Selectors API以外で要素を取得する
大変便利なSelectors APIですが、速度面的な意味から単純な要素取得は昔ながらのDOM関数を使った方がよいみたいです。
ID
文書中の指定したIDの要素を取得する。
$('#hoge'); // <- jQueryオブジェクト
document.getElementById('hoge'); // <- element
// Selectors API
document.querySelector('#hoge'); // <- element
IDは文書中でユニークでなくてはならないので、他のどのメソッドよりも早いようです。Elementオブジェクトを返します。
指定したIDの要素が見つからなかった場合はnullが返ってきます。
クラス
文書中の指定したクラスを含む要素を全て取得する。
$('.fuga'); // <- jQueryオブジェクト
document.getElementsByClassName('fuga'); // <- HTMLCollection
// Selectors API
document.querySelectorAll('.fuga'); // <- NodeList
タグ名
文書中の指定したタグを持つ要素を全て取得する。
$('div'); // <- jQueryオブジェクト
document.getElementsByTagName('div'); // <- HTMLCollection
// Selectors API
document.querySelectorAll('div'); // <- NodeList
Name
文書中の指定したnameを持つ要素を全て取得する。
$('[name=first_name]'); // <- jQueryオブジェクト
document.getElementsByName('first_name'); // <- NodeList
// Selectors API
document.querySelectorAll('[name=first_name]'); // <- NodeList
NodeListとHTMLCollection
複数要素を取得するメソッドはNodeListかHTMLCollectionが返ってきます。
まとめると、
-
querySelectorAll
はNodeListが返ってくる。 -
getElementsByTagName
とgetElementsByClassName
はHTMLCollectionが返ってくる。 -
getElementsByName
はNodeListが返ってくる。(ちなみにIE11だとHTMLCollectionが返ってくる)
ややこしい・・・
どちらもlengthプロパティを持ち、角括弧を使うことによりelementを取り出すことが出来ます。ArrayオブジェクトっぽいですがArrayオブジェクトを継承しているわけではない、配列のようなオブジェクト(Array-likeオブジェクト)です。ふざけんなって感じですね。
<div class="fuga">fuga1</div>
<div class="fuga">fuga2</div>
<script type="text/javascript">
// NodeList
let node = document.querySelectorAll('.fuga'); // <- NodeList
console.log(node.length); // <- 2
node[0].style.backgroundColor = 'red'; // 最初のdiv.fuga要素の背景色を赤にする
// HTMLCollection
let list = document.getElementsByClassName('fuga'); // <- HTMLCollection
console.log(list.length); // <- 2
list[list.length-1].style.backgroundColor = 'blue'; // 最後のdiv.fuga要素の背景色を青にする
</script>
NodeListとHTMLCollectionはパッと見あまり差異がないように見えるのですが、NodeListは静的であり、DOMへの変更がNodeList オブジェクトの内容に影響を与えないです。これだけだとよくわからないと思いますが、かいつまんで説明しますと、NodeList取得後に要素の追加や削除されたDOMの変更が反映されないのがNodeList、変更が反映されるのがHTMLCollectionということです。
↓詳しくは下記のサイトで説明されています。
NodeListとHTMLCollectionも別物なので気を付けよう。(DOMおれおれAdvent Calendar 2015 – 13日目)
要素の操作
セレクタで要素を取得できるようになったので、要素の操作をしてみましょう。jQueryではjQueryオブジェクト、ネイティブのJavaScriptはElementオブジェクトが返ってくるので操作方法が異なります。
注意
getElementById
やquerySelector
で要素が見つからず取得できなかった場合、nullが返ってくるのでコードを続けて記述する場合は注意が必要です。
// jQueryの場合
$('#no-id'); // <- 空のjQueryオブジェクト
$('#no-id').css('background-color'); // <- undefined
$('#no-id').css('background-color', 'red'); // エラーにはならない
// ネイティブJavaScriptの場合
document.getElementById('no-id'); // <- null
document.getElementById('no-id').style.backgroundColor; // エラー!
document.getElementById('no-id').style.backgroundColor = 'red'; // エラー!
// ちゃんとnullチェックを行う
let elm = document.getElementById('no-id');
if (elm) {
elm.style.backgroundColor = 'red';
}
HTML
取得
$('div#hoge').html();
document.querySelector('div#hoge').innerHTML;
設定
$('div#hoge').html('<span>hogehoge</span>');
document.querySelector('div#hoge').innerHTML = '<span>hogehoge</span>';
テキスト
取得
$('div#hoge').text();
document.querySelector('div#hoge').textContent;
設定
raw textが設定されます(HTMLタグはエスケープ処理されてテキストとして表示される)
$('div#hoge').text('<span>hogehoge</span>');
document.querySelector('div#hoge').textContent = '<span>hogehoge</span>';
フォーム値
取得
$('input[name="first_name"]').val();
document.querySelector('input[name="first_name"]').value;
設定
$('input[name="first_name"]').val('azusa');
document.querySelector('input[name="first_name"]').value = 'azusa';
スタイル操作
ネイティブJavaScriptにおけるstyleプロパティの詳細については下記参照。
CSS Properties Reference
取得
$('#hoge').css('background-color');
document.getElementById('hoge').style.backgroundColor;
設定
$('#hoge').css('background-color', '#fff');
document.getElementById('hoge').style.backgroundColor = '#fff';
クラス操作
クラス追加
$('#hoge').addClass('fuga');
document.getElementById('hoge').classList.add('fuga');
クラス削除
$('#hoge').removeClass('fuga');
document.getElementById('hoge').classList.remove('fuga');
Toggle
$('#hoge').toggleClass('fuga');
document.getElementById('hoge').classList.toggle('fuga');
クラスの存在確認
$('#hoge').hasClass('fuga'); // <- bool
document.getElementById('hoge').classList.contains('fuga'); // <- bool
属性(Attribute)
取得
$('a.fuga.piyo1').attr('href');
document.querySelector('a.fuga.piyo1').getAttribute('href');
設定
$('a.fuga.piyo1').attr('href', 'http://qiita.com/');
document.querySelector('a.fuga.piyo1').setAttribute('href', 'http://qiita.com/');
削除
$('a.fuga.piyo1').removeAttr('href');
document.querySelector('a.fuga.piyo1').removeAttribute('href');
要素の作成
$('<div>'); // <- jQueryオブジェクト
document.createElement('div'); // <- element
余談ですが、SVGの要素を生成する場合は、SVGElementを生成するためにnamespaceを指定しないといけないため、jQueryでは生成できません。
そのため、document.createElementNSを使って生成します。
let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewbox', '0 0 100 100');
let line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', 10);
line.setAttribute('y1', 10);
line.setAttribute('x2', 90);
line.setAttribute('y2', 90);
line.setAttribute('stroke', 'black');
svg.append(line);
document.body.append(svg);
子要素の追加/移動
新規要素を追加
<div id="hoge">
<!-- ココ(div#hogeの末尾)にdiv.piyo1要素追加 -->
<div class="fuga piyo1">piyo2</div>
<!-- ココ(div#hogeの末尾)にdiv.piyo3要素追加 -->
</div>
// div.piyo1要素生成
let $div1 = $('<div>').addClass('fuga piyo1').text('piyo1');
// div.piyo3要素生成
let $div3 = $('<div>').addClass('fuga piyo3').text('piyo3');
// div.piyo1要素追加
$('#hoge').prepend($div1);
// div.piyo3要素追加
$('#hoge').append($div3);
// div.piyo1要素生成
let div1 = document.createElement('div');
div1.classList.add('piyo1');
div1.textContent = 'piyo1';
// div.piyo3要素生成
let div3 = document.createElement('div');
div3.classList.add('piyo3');
div3.textContent = 'piyo3';
// 追加
document.getElementById('hoge').prepend(div1);
document.getElementById('hoge').append(div3);
// または
document.getElementById('hoge').insertAdjacentHTML("afterbegin", div1.outerHTML);
document.getElementById('hoge').insertAdjacentHTML("beforeend", div3.outerHTML);
既存要素を移動
<div id="hogehoge" class="fuga">div#hogehoge</div>
<div class="fuga piyo1">div.fuga.piyo1-1</div>
<div class="fuga piyo1">div.fuga.piyo1-2</div>
<div id="hoge">
<!-- ココ(div#hogeの末尾)にdiv.piyo1要素移動 -->
<div class="fuga piyo2">piyo2</div>
<!-- ココ(div#hogeの末尾)にdiv.piyo3要素移動 -->
</div>
<div class="fuga piyo3">div.fuga.piyo3-1</div>
<div class="fuga piyo3">div.fuga.piyo3-2</div>
/** 単要素の移動 */
// 要素の先頭に移動
$('#hoge').prepend($('#hogehoge'));
// 要素の末尾に移動
$('#hoge').append($('#hogehoge'));
/** 複数要素の移動 */
// 要素の先頭に移動
$('#hoge').prepend($('div.piyo1'));
// 要素の末尾に移動
$('#hoge').append($('div.piyo3'));
ネイティブJSでの複数要素の移動は若干面倒くさい…
/** 単要素の移動 */
// 要素の先頭に移動
document.getElementById('hoge').prepend(document.getElementById('hogehoge'));
// 要素の末尾に移動
document.getElementById('hoge').append(document.getElementById('hogehoge'));
/** 複数要素の移動 */
// 要素の先頭に移動
let divs1 = document.querySelectorAll('div.piyo1');
for (let i = divs1.length-1; i >= 0; i--) {
document.getElementById('hoge').prepend(divs1[i]);
}
// 要素の末尾に移動
let divs3 = document.querySelectorAll('div.piyo3');
for (let i = 0; i < divs3.length; i++) {
document.getElementById('hoge').append(divs3[i]);
}
要素の挿入\移動
指定した要素の前後に新規要素を追加
<!-- ココ(#hogeの前)にdiv.piyo1を追加 -->
<div id="hoge">hoge</div>
<!-- ココ(#hogeの後)にdiv.piyo3を追加 -->
// div.piyo1要素生成
let $div1 = $('<div>').addClass('fuga piyo1').text('piyo1');
// div.piyo3要素生成
let $div3 = $('<div>').addClass('fuga piyo3').text('piyo3');
// div.piyo1要素追加
$('#hoge').before($div1);
// div.piyo3要素追加
$('#hoge').after($div3);
ネイティブJSでは指定した要素の後に追加するというメソッドがない。
そのため、nextSibling
を使って実現している。
// div.piyo1要素生成
let div1 = document.createElement('div');
div1.classList.add('piyo1');
div1.textContent = 'piyo1';
// div.piyo3要素生成
let div3 = document.createElement('div');
div3.classList.add('piyo3');
div3.textContent = 'piyo3';
let hoge = document.getElementById('hoge');
// div.piyo1要素追加
hoge.parentNode.insertBefore(div1, hoge);
// div.piyo3要素追加
hoge.parentNode.insertBefore(div3, hoge.nextSibling);
// または
document.getElementById('hoge').insertAdjacentHTML("beforebegin", div1.outerHTML);
document.getElementById('hoge').insertAdjacentHTML("afterend", div3.outerHTML);
指定した要素の前後に既存要素を移動
<!-- ココ(div#hogeの末尾)にdiv.piyo1要素移動 -->
<div id="hoge">
<div class="fuga piyo1">div.fuga.piyo1-1</div>
<div class="fuga piyo1">div.fuga.piyo1-2</div>
<div id="hogehoge" class="fuga">div#hogehoge</div>
<div class="fuga piyo3">div.fuga.piyo3-1</div>
<div class="fuga piyo3">div.fuga.piyo3-2</div>
</div>
<!-- ココ(div#hogeの末尾)にdiv.piyo3要素移動 -->
/** 単要素の移動 */
// 要素の先頭に移動
$('#hoge').before($('#hogehoge'));
// 要素の末尾に移動
$('#hoge').after($('#hogehoge'));
/** 複数要素の移動 */
// 要素の先頭に移動
$('#hoge').before($('div.piyo1'));
// 要素の末尾に移動
$('#hoge').after($('div.piyo3'));
ネイティブJSでの複数要素の移動は若干面倒くさい…
let hoge = document.querySelector('#hoge');
/** 単要素の移動 */
// 要素の先頭に移動
hoge.parentNode.insertBefore(document.querySelector('#hogehoge'), hoge);
// 要素の末尾に移動
hoge.parentNode.insertBefore(document.querySelector('#hogehoge'), hoge.nextSibling);
/** 複数要素の移動 */
// 要素の先頭に移動
let divs1 = document.querySelectorAll('div.piyo1');
for (let i = 0; i < divs1.length; i++) {
hoge.parentNode.insertBefore(divs1[i], hoge);
}
// 要素の末尾に移動
let divs3 = document.querySelectorAll('div.piyo3');
for (let i = divs3.length-1; i >= 0; i--) {
hoge.parentNode.insertBefore(divs3[i], hoge.nextSibling);
}
要素の削除
<div class="fuga piyo1">div.fuga.piyo1-1</div>
<div class="fuga piyo1">div.fuga.piyo1-2</div>
<div id="hogehoge" class="fuga">div#hogehoge</div>
<div class="fuga piyo3">div.fuga.piyo3-1</div>
<div class="fuga piyo3">div.fuga.piyo3-2</div>
/** 単要素の削除 */
$('#hogehoge').remove();
/** 複数要素の削除 */
$('div.fuga').remove();
/** 単要素の削除 */
document.querySelector('#hogehoge').remove();
/** 複数要素の削除 */
let fugas = document.querySelectorAll('div.fuga');
for (let i = 0; i < fugas.length; i++) {
fugas[i].remove();
}
// または
let fugas = document.getElementsByClassName('fuga');
while (fugas.length) {
fugas[0].remove();
}
/** これはNG (NodeListは静的なのでlengthが変わらず無限ループに陥る) */
let fugas = document.querySelectorAll('div.fuga');
while (fugas.length) {
fugas[0].remove();
}
繰り返し処理
jQueryオブジェクトではeachメソッドを使うことができ、簡単に繰り返し処理が実現可能です。
$(".fuga").each(function(){
if ($(this).css('background-color') == 'red') { // $(this)は子要素のjQueryオブジェクト
$(this).css('background-color', 'black');
}
...
});
かんたんですね。
一方、ネイティブのJavaScriptではNodeListまたはHTMLCollectionが返ってくるのでそれらを処理します。
上述した通り、これらはArrayオブジェクトではないのでforEachは当然使えません。
document.getElementsByClassName('fuga').forEach(function(elm, val, arr){ // エラー!
if (elm.style.backgroundColor == 'red') {
elm.style.backgroundColor = 'black';
}
...
});
for...of構文が使えるのでそちらを使います。
let list = document.getElementsByClassName('fuga');
for (let elm of list) {
if (elm.style.backgroundColor == 'red') { // elmはElementオブジェクト
elm.style.backgroundColor = 'black';
}
...
}
ただこの書き方はわりかし新しめの書き方でブラウザによっては使えない(IEは完全に未サポート、Edgeは12~)ので、ダメな場合は昔ながらのfor構文を使います。
let list = document.getElementsByClassName('fuga');
for (let i = 0; i < list.length; i++) {
if (list[i].style.backgroundColor == 'red') {
list[i].style.backgroundColor = 'black';
}
...
}
↓こんな方法もあるみたいです。
配列ライクなオブジェクトをforEachするときのイディオム
一括操作
jQueryだと指定されたクラスを持つ要素への一括操作は
$(".fuga").css('background-color', 'red');
の一行で書けます。べんりですね!
しかし、ネイティブのJavaScriptはできません。
document.querySelectorAll('.fuga').style.backgroundColor = 'red'; // エラー!
querySelectorAll
の戻り値はNodeListなのでstyleプロパティがないためです。
素直に繰り返し処理するしかなさそうですね。
for (let elm of document.querySelectorAll('.fuga')) {
elm.style.backgroundColor = 'red';
}
参考
は~、やっぱjQuery使うか。。。