読者です 読者をやめる 読者になる 読者になる

すこしふしぎ.

VR/HI系院生による技術ブログ.まったりいきましょ.(友人ズとブログリレー中.さぼったら焼肉おごらなきゃいけない)

【javascript】配列ライクなオブジェクトでforEachどうやるのか調べた話【querySelectorAll】

javascript

こんばんは.1000chです. 先日,思い出のマーニーをみてきました. facebookで友人の評価がよかったので期待してたんですが,ほんと良かったです. 最後(´;ω;`)ってなりました. この夏,引きこもってる人はぜひ見に行くと良いと思います.

アクティブといえば,コミケ3日目西きー16bあの人の研究所にて配布される研究論文集が気になってます. 有名大学の方々が,盛大に素敵に才能を無駄遣いした自由研究の論文集で,なかなかカオスで面白そうです.

さて脈絡もないですが,今回はjavascriptをつかって配列ライクなオブジェクトをforEachする処理を学んだのでメモっときます.

いきさつ

そもそもの課題はコレ.

ページ内のa要素が持つテキストを全て出力して下さい.

ふむ.簡単じゃん.ということでまず回答.

obj_ls = $("a")
for(var i=0; i<obj_ls.length; i++){
    console.log(obj_ls[i].text);
}

まぁこれで仕様は満たしてますね.

ワンライナーにしよう

ということで次はこんな感じにfix.

$.each( $("a"), function(i,v){ console.log( v.text) })

ここで初めてjQueryのutil関数群の便利さに気づく筆者. コールバックの引数は2つ.第1引数が配列index,第2引数が値だとさ.

使わないとわからんね.

これでもokだった.

$.each( $("a"), function(){ console.log( this.text) })

thisにオブジェクト配列のそれぞれが入るのかー. 便利だけど上の方が明示的でわかりやすいかな.

生jsで書いてみよう

ほう...jQueryなしとな...

なんだっけgetElementByIdとかあるしgetElementBySelectorとかそんなん?

ggったら最近はquerySelecotrAllとやらを使うらしい.

つことで書いた.

obj_list = document.querySelectorAll("a")
for(var i=0; i<obj_list.length; i++){
  console.log(obj_list[i].text);
}

実質やってることはjQueryの時とほとんど変わらないな,焦ること無かったわ.

ワンライナーにしよう(ただしjQuery禁止.こっから本題)

ほう...ワンライナーとな...

処理的にはjQueryの場合とほとんど一緒だったし,querySelectorAllで返ってくる配列をそのままfor文で回してしまう処理がよさそう. 問題は生のjavascriptでforeachってあんの?って話になる.

ぐぐります.

for each ... in の場合

for each...in - MDN

なーんだちゃんとあるじゃん!

てことで書いてみる.

for each(var obj in document.querySelectorAll("a")){console.log(obj.text)}
SyntaxError: Unexpected identifier

...ん?動作していないぞ?

よくよくみてみると

警告: このようなループを決して配列に使わないでください。オブジェクトにだけ使ってください。

なんて書いてあった.さーせん.

Array.forEachの場合

あらためてぐぐるとこれがでてきた.

Array.forEach - MDN

今度は配列におけるメソッドっぽい.適当な配列でためしてみる.

[1,2,3].forEach(function(e,i,a){console.log(e,i,a)})

1 0 [1, 2, 3]
2 1 [1, 2, 3]
3 2 [1, 2, 3] 

コールバック関数は要素,インデックス,元配列を引数と持つようだ. これならさっきの配列も扱えそう.

やってみた.

document.querySelectorAll("a").forEach(function(e,i,a){console.log(e,i,a)})

> TypeError: undefined is not a function

...なぬ?undefined?

嫌な予感がするので確認.(オブジェクトの型判定はここ参考)

document.querySelectorAll("a").forEach
> undefined

[1,2,3] instanceof Array
> true

document.querySelectorAll("a") instanceof Array
> false

なーるほど...どうやらquerySelectorAllが返すのはArrayでは無いようだ. だから当然forEachメソッドなんて持っていないということか.

nodeList

じゃーquerySelectorAllはどんなオブジェクトを返すのか?ってことで仕様書をみる.

与えられた CSS セレクタにマッチする文書中の要素(※深さ優先の先行順走査によるもの)の全てのリスト (NodeList) を返します。

NodeList - MDN をのぞいていきます.

するとそもそも,lengthプロパティとitemメソッドしか無いとか.

挙げ句の果てには

Why can't I use forEach or map on a NodeList?

NodeList are used very much like arrays and it would be tempting to use Array.prototype methods on them, however they don't have those methods.

とかいうことまで書いてあるし.でっすよねー.

その後しばらくNodeListはArray.prototypeを継承してないからforEachとかmapとか使えないZE! HAHAHA! みたいなことが懇切丁寧に書いてある.

使い方の具体例として載っているのも,先程書いた普通のfor文だ. どころかご丁寧に

Don't be tempted to use for...in or for each...in to enumerate the items in the list ...

とか書いてある.もう動かないのは確認済みさ!1

ワンチャンあるっぽいのがこれ.

for...of loops will loop over NodeList objects correctly, in browsers that support for...of (like Firefox 13 and later):

var list = document.querySelectorAll( 'input[type=checkbox]' );
for (var item of list) {
  item.checked = true;
}

for ... of なんて構文もあるんすね.firefoxでやってみると,

for (var i of document.querySelectorAll("a")){console.log(i.text)}

が動作しました!やったぜワンライナーだ!

...が,当然chromeでは動きませんでした(´・ω・`)

別の方法として,NodeListをArrayにコンバートする手法も紹介されていた.

var turnObjToArray = function(obj) {
  return [].map.call(obj, function(element) {
    return element;
  })
};

var box = document.querySelectorAll('div');

var listBox = turnObjToArray(box)

アイディアは良さげだけれど,ワンラインにはならなそうだなーという印象.

chromeでforEachしたいよ!

そしてたどり着いたのがこれ.

One idea would be to add Array.prototype methods to NodeList.prototype. However, be aware that Extending the DOM is dangerous, especially in old version of Internet Explorer (6, 7, 8).

Array.prototypeのメソッドをnodelistに付け加えちゃえば良いんじゃね的なノリ.

例として上がってるのがこんな感じ.

var forEach = Array.prototype.forEach;

var divs = document.getElementsByTagName( 'div' );
var firstDiv = divs[ 0 ];

forEach.call(firstDiv.childNodes, function( divChild ){
  divChild.parentNode.style.color = '#0F0';
});

まずcallってなんだ?ってことでぐぐる

applyとcallの使い方を丁寧に説明してみる

めっちゃ丁寧!わかりやすい! つまり,call / apply使うと,外部の関数をあるオブジェクトのメソッドとして呼び出せるのか.使い方はこんな感じで,

関数.call(オブジェクト,引数)

これが

オブジェクト.関数(引数)

と同値なわけだね.

あらためて先のコードをみると,これ結局Array.prototypeのforEachに対しcallよんでるだけっぽい. (本当はまずArray.prototypeの中で実装されているforEachの手続きを理解した上で,それをnodeListに適用できるか確認するべきなんだろうけど,今回はまぁ例として上がってるから「つかえる」ってことなんだろう.この場合lengthプロパティもってればokなのかな?)

というわけでforEachをcallしてみる. Array.prototype.forEachの引数としては,先程のようにcallbackとなる関数を1つ与えてやればいいことに注意すると...

Array.prototype.forEach.call(document.querySelectorAll("a"),function(e,i,a){console.log(e.text)})

できた!!

apply使う場合はこう.

Array.prototype.forEach.apply(document.querySelectorAll("a"),[function(e,i,a){console.log(e.text)}])

引数1つでも,配列にするとこがポイント(ここで悩んだ).

コレでついに,chromeでも生jsのワンライナーで仕様を満たせた!

ちなみに

querySelectorAll forEach でぐぐるとドンピシャな記事がでてきた.

配列ライクなオブジェクトをforEachするときのイディオム - Petittech

この努力はいったい(´・ω・`)

まとめ

  • javascript,配列ライクオブジェクトでforEach
  • call / apply が便利!
  • ぐぐる力 is とても大切
  • されど document から理解する力も大切!