概要
クライアント側のJavaScriptの基礎の基礎を学ぶために基本的な操作をする。その事始めとして全要素の走査をする。
HTMLはブラウザに読み込まれるとDOM(document object model)という木構造になる。この木構造のrootノードにはdocument.documentElement
でアクセスが可能である。
JavaScriptへのインターフェイスはHTMLElementやそのコレクションクラスのHTMLCollectionである。HTMLCollectionはいわゆるJavaScriptの配列ではないので注意が必要であるがここでのサンプルは最終的にWebpackなどでバンドルするのでその差異はトランスパイラかその際に埋め込むpolyfillに任せる。
サンプル
const idTable={}, classTable={};
const walkElement=(elem, idTable, classTable)=>{
for( const child of elem.children ){
if( child.id.length!==0 ){
if( idTable[child.id]!=null ) throw new Error('!!!!! id='+child.id+'が重複して登録されています !!!!!');
else idTable[child.id]=child;
}
if( child.className.length!==0 ){
for( const className of child.className.split(' ').filter(a=>{ return a.length!==0; })){
if( classTable[className]!=null ) classTable[className].push(child);
else classTable[className]=[ child ];
}
}
walkElement(child, idTable, classTable);
}
}
window.addEventListener('load', ()=>{
walkElement(document.documentElement, idTable, classTable);
console.log(idTable);
console.log(classTable);
});
export { idTable, classTable };
木構造は再帰と非常に相性がいいので再帰呼出しをしている。
ただ走査しただけでは面白くないので関数内ではidやclassがあった場合、tableに入れている。またid被りの検証もしており同じidがあった場合、例外を投げるようにしている。
クラス名は空白区切りで複数指定可能なのでsplit(' ')
で分割しさらに空白2個に対応するために空文字列をfilter
で取り除いている、最近のJavaScriptは高機能なのでだいたい組み込み関数で対応できる。特にfilter,map,reduceなどの関数型的考え方はぜひ取り入れるべきであると思う。
Objectの形で保存しているので動的にHTML要素を追加しない限りdocument.getElementById
やdocument.getElementsByClassName
の代わりに使えるはずです。
動的追加時にidチェック
import { idTable } from './idTable_classTable';
import set from './idUtil/set';
const idUtil={
set: set,
get: (id)=>{ return idTable[id]; },
};
export default idUtil;
import { idTable } from '../idTable_classTable';
const argsParse=(...args)=>{
if( args.length===2 && args[0] instanceof HTMLElement && typeof args[1]==='string' ) return [ args[0], args[1] ];
if( args.length===2 && args[1] instanceof HTMLElement && typeof args[0]==='string' ) return [ args[1], args[0] ];
throw new Error('!!!!! domUtil.idUtil.set invailed arguments !!!!!');
}
const set=(...args)=>{
const [ elem, id ]=argsParse(...args);
if( idTable[id]!=null ){
throw new Error('!!!!! id='+id+'は登録されています !!!!!');
}
idTable[id]=elem;
elem.id=id;
return elem;
}
export default set;
のようにid用のユーティリティを書くと自動的にTableは更新されます。
classのチェック
import { classTable } from './idTable_classTable'
import set from './classUtil/set'
const classUtil={
set: set,
get: get(className)=>{ return classTable[className] },
}
export default classUtil;
import { classTable } from '../idTable_classTable'
const argsParse=(...args)=>{
if( args.length===2 && args[0] instanceof HTMLElement && typeof args[1]==='string' ) return [ args[0], args[1] ];
if( args.length===2 && args[1] instanceof HTMLElement && typeof args[0]==='string' ) return [ args[1], args[0] ];
throw new Error('!!!!! domUtil.classUtil.set invailed arguments !!!!!');
}
const set=(...args)=>{
const [ elem, name ]=argsParse(...args);
if( classTable[name]!=null ) classTable[name].push(elem);
else classTable[name]=[ elem ];
if( elem.className==null || elem.className.length===0 ) elem.className=name;
else elem.className+=' '+name;
return elem;
}
export default set;
クラス名は空白で区切るので前要素がある場合はくっつかないように空白を入れています。
チェック用ユーティリティに対する補足説明
HTMLElementの提供するAPIのinnerHTMLは破壊的操作を含むので注意が必要である。
HTMLの内部構造はすべて(配列を含む)Objectで受け渡しもObjectを使うべきであるがinnerHTMLは(多分)toStringを呼び出して文字列形式にしているのでプロパティ値がすべて消滅します。結果としてinnerHTMLでHTMLElementを再配置するとid検索やclass検索にかからなくなります、onXXXやaddEventLisnterで登録したイベントも消えるので注意しなければなりません。
まとめ
ブラウザの標準APIを使って全要素の走査をやると意外と簡単だった。ついでにid/classテーブルを作った、再び走査を走らせないので高速なはずである。XXXUtilを通してアクセスする限りテーブルは更新される。
しかし要素が削除されてもテーブルに残っているとガベージコレクションにかからないので対策が必要である。またはユーティリティを通さない更新もテーブルにミスマッチを引き起こす、特にinnerHTMLを使ったHTMLの動的操作は要注意である。
解決策は、使う側が気をつけてユーティリティを使ってもらう、もしくは一定時間ごとにcheck関数を走らせるなどが考えられます。setInterval
等を使えば簡単なはずです。
補足説明
いくつか内容とは関係ないですが独自の方法を使っているものがあるので解説しておきます。
分割演算子...
による関数の汎化
...args
はあまり見慣れないかもしれませんが分割演算子で関数の仮引数に使うと引数を配列とみなしてくれます。それ以外で使うと配列を展開します。仮引数->関数の引数->仮引数とすると常に同じ変数(配列になります)。
この性質を使ってどちらにHTMLElementを入れても動くようにしています。工夫すればもっと便利に使えます(次回以降に解説するかもしれません)。
モジュールモデル
私は1モジュール、1関数、1変数を1つのファイルにするようにしている。idTable_classTable
は一回の操作で作ったほうが効率的なので例外としました。
$ idTable_classTable.js
idUtil.js
idUtil/set.js
classUtil.js
classUtil/set.js
のようなファイル構造になる。exportする側では常に名前を付けずにexport default
で受ける側は常にファイル名と同じものでimportすることで名前の衝突を避けるようにしている、異なるモジュール(ディレクトリ)から関数を読み込む際は必ずモジュール経由(XXXUtilのようなもの)ようにする。
idUtil.set
だとidに関するモジュールを使ってsetするのように関数はモジュール名を含めたものを名前として解釈する。