◆ セレクタにマッチした要素のすべてにメソッドの処理をしてくれるところ
◆ それくらいだったので DOM メソッド・プロパティで同じ感じなことする関数つくってみた

最初に使った頃から jQuery はなんか嫌いで どうしても必要でない限り使わないスタイルでした
そのころはまだ IE 利用率もあって IE も一応対応させよう ってときに あの機能ない この機能もない ってことで仕方なく使うとかそれくらいです

最近ではどんどん jQuery 離れが世間的に進んでるようで 嬉しいですね
フレームワークができたから 何ていう話もありますが 単純に DOM の API が充実してほとんどの jQuery の機能がなくてよくなったのが大きいと思います
closest とか append とかあきらかに jQuery から来てるだろうというのもありますね

ただ この 2 つなんかは IE11 でも使えません
ですが 最近は IE の利用率は Chrome に負けて 1 割を切ってるようです
日本国内に限定すればまだ 2 割もいるようですけど……

IE の使用者は出来る限り減っていってほしいので 個人的にはもう IE は対応させないつもりで 最新機能をどんどん使っていっています
とは言っても開いたときに画面がぐちゃぐちゃすぎるのはどうかと思うので 一応見ることは出来る状態で ボタン押しても反応なし って言う感じが多めです

非対応です画面だけでもいいのですが 私としては 「対応してなくても 多少崩れててたりボタン反応なくてもいいからとりあえず見たい」と思うのであえてです
昔 IE しか対応しないって個人サイトが Chrome だと中身見えなくされていたのですが 本当に IE でしか動かない機能使っていたとしても コンテンツを見た上で IE で開き直す価値があるか決めれるので とりあえず表示してほしいと思いました
JavaScript 処理動かないと意味のないツール系ならまだしも ブログやホームページみたいのならガラケーでもテキストだけ見れれば十分だったりしますからね


もともと jQuery ってブラウザ間の差を気にせず使えるということで流行ったので IE なしでモダンブラウザのみとなると jQuery を使う必要はほとんどないわけです
append/prepend などの DOM メソッドはネイティブにあるし ajax 機能も fetch があるし animation も web animation があるし といったところです

jQuery 入れるとすれば ライブラリが依存してるから というのが主でしょう
昔は使っていて当たり前 みたいなくらいに使われたライブラリですから jQuery 依存でつくられてるものがいっぱいです
何かの公式サイトっぽいところでけっこうみかける Bootstrap も jQuery 依存です
Bootstrap は jQuery 依存なし版を作るプロジェクトがあるとか聞いた気もしますが確かあくまで外部のもので公式では jQuery 依存のものしかなかったと思います

そういうわけで モダンブラウザのみ対応で jQuery 依存ライブラリが必要でないならわざわざ入れる必要もないでしょう

モダンブラウザのみでも感じる jQuery の良い機能

それでも jQuery で何か作った後に 生の JavaScript でコード書いているとここは jQuery のほうが良かったな と思うところもあります

  • メソッドチェーン
  • セレクタで取得した全要素に処理

メソッドチェーン

メソッドチェーンって便利ですし きれいに書けると気持ちいいですよね
一時期 1 つの要素 もしくはその要素以下のサブツリーの更新は 1 つのメソッドチェーンの式で書く みたいなことしてることもありました

その経験上 一番感じたことは 【やり過ぎ注意】

全部チェーンさせてしまうと 途中の状態がわかりづらく おかしいときにデバッグがしづらいです

特に end まで使ってチェーンする必要はないと思いました
その時の jQuery オブジェクトが持つ要素セットの状態が把握しづらいです

attr, prop, css みたいなのはいいのですが append とか入ると一度切るくらいでいいと思います
返り値が jQuery オブジェクトになるので HTML 要素であるとはわかっても append した場合に親なのか子なのか慣れていないとわかりづらいと思います
(慣れていてもたまに不安でググってます)


メソッドチェーンはわかりやすい程度に収めていれば 余計な一時変数が減って見やすくなるので便利な機能です
10 数行程度ならともかく 一般的な画面サイズで 一度に見える以上の長さの関数があると 1 つ下の行でしかつかわないような一時変数が量産されるのは見づらいだけですからね

配列の要素全部に処理

jQuery の基本の動きは querySelectorAll のように全部のセレクタにマッチする要素を持ってきて jQuery オブジェクト内部にそれらの参照を保持します
jQuery メソッドの処理は jQuery オブジェクト内の全要素に対してとなります

生の JavaScript だと毎回 forEach しないといけないのがめんどうです
まぁ 楽に書けるからこそ 必要ないのに何十 何百もの要素が対象になっていても気にせず 全部に処理してしまって 初心者が書いたコードが変に重いとかあるのですけど

そういう一面はあるものの addEventListener するときなんかは毎回 forEach 書くのが面倒でまとめてやりたいんですよね

いいところだけ使えるように

いい部分を使えるように関数を作ってみました
function $$$(selector, scope, multiple, strict){
const elems = [...(scope || document).querySelectorAll(selector)]
if(strict && elems.length === 0) throw new Error("No matched elements.")

return new Proxy({}, {
get(_, prop){
if(prop in $$$.properties){
if(typeof $$$.properties[prop].get === "function"){
return $$$.properties[prop].get(elems, {multiple})
}else{
throw new Error(`"${prop}" does not allow getter access.`)
}
}else if(prop in HTMLElement.prototype){
if(typeof document.createElement("div")[prop] === "function"){
return function(...args){
const results = elems.map(e => e[prop](...args))
return multiple ? results : results[0]
}
}else{
if(multiple){
return elems.map(e => e[prop])
}else{
return (elems[0] || {})[prop]
}
}
}else{
throw new Error(`"${prop}" is not defined.`)
}
},
set(_, prop, value){
if(prop in $$$.properties){
if(typeof $$$.properties[prop].set === "function"){
$$$.properties[prop].set(elems, value, {multiple})
}else{
throw new Error(`"${prop}" does not allow setter access.`)
}
}else if(prop in HTMLElement.prototype){
elems.forEach(e => e[prop] = value)
}else{
throw new Error(`"${prop}" is not defined.`)
}
},
})
}

$$$.properties = {
length: {
get(elems, {multiple}){
return elems.length
},
},
elements: {
get(elems, {multiple}){
return elems
},
},
hasClass: {
get(elems, {multiple}){
return name => {
const results = elems.map(e => e.classList.contains(name))
return this.multiple ? results : results[0]
}
},
},
appendClone: {
get(elems, {multiple}){
return (...args) => {
return elems.forEach(e => {
args.forEach(a => e.append(a.cloneNode(true)))
})
}
},
},
forEach: {
get(elems, {multiple}){
return (fn) => {
return elems.forEach((e, i) => fn(e, i, elems))
}
},
},
map: {
get(elems, {multiple}){
return (fn) => {
return elems.map((e, i) => fn(e, i, elems))
}
},
},
runnableInnerHTML: {
set(elems, value, {multiple}){
elems.forEach(e => {
e.innerHTML = value
for(const script of e.querySelectorAll("script")){
const s = document.createElement("script")
s.innerHTML = script.innerHTML
for(const attr of script.attributes){
s.setAttribute(attr.name, attr.value)
}
script.replaceWith(s)
}
})
},
},
}
Gist

基本機能

$$$ にセレクタを入れて DOM のプロパティやメソッドを使います
document.body.innerHTML = `
<div></div>
<div></div>
`
$$$("div").setAttribute("data-name", "a")
$$$("div").className = "c"
結果はこうなります
<div data-name="a" class="c"></div>
<div data-name="a" class="c"></div>

プロパティの取得の場合は 3 つめの引数の multiple を true にするかどうかで変わります
document.body.innerHTML = `
<a></a>
<span></span>
<p></p>
`
console.log($$$("body *").tagName)
console.log($$$("body *", null, true).tagName)

multiple がない場合は jQuery と同じように 1 つ目の値が返ってきて multiple があれば それぞれの結果が配列で返ってきます
A
[ "A", "SPAN", "P"]

残りの引数は 2 つめがスコープで ルートになる要素を選べます
4 つめは strict 機能でセレクトの結果が 0 個だとエラーにします

jQuery だと セレクタの要素が絶対あるはずとして作っていたところが タイプミスやバグでなかった場合も動いてしまうのでどこがおかしいのか見つけにくい欠点があります
要素がなければ何もしないというのは 使い方によっては便利なので 良い機能ではあるのですが 自分で要素数チェックをするのも面倒なことが多いので 機能として含めました

拡張機能

コードの後半がそうなのですが DOM メソッドやプロパティはあくまで 1 つの要素前提としての動きなので独自のメソッドを追加したいということがあります
$$$.properties にサンプルみたいにプロパティ追加すれば独自の getter/setter を定義できます

例えば append を使った場合 DOM メソッドの append を複数の要素に実行しても クローンはされないので 最終的には最後に実行した要素だけに子要素が追加されてしまいます
クローンすることで対象全部の子要素に追加できるようにした appendClone メソッドを用意しています

document.body.innerHTML = `
<ul>
<li></li>
<li></li>
<li></li>
</ul>
`
$$$("li").append(document.createElement("div"))
$$$("li").appendClone(document.createElement("span"))
<ul>
<li><span></span></li>
<li><span></span></li>
<li><div></div><span></span></li>
</ul>

jQuery のよくも悪くもある script を実行してくれる html() にあたるものもつくってみました
$$$("body").runnableInnerHTML = "<span>aa</span><script>console.log(Date.now())</script>"

innerHTML のかわりに runnableInnerHTML に代入すれば script タグがあれば実行してくれます

その他 length プロパティで要素数を取得 elements プロパティで配列で要素を取得 forEach/map でそれぞれに処理するようになってます

multiple の引数は結果を配列で返すかどうかでしたが 拡張機能として使う分には作る側が決めることになります
length や elements では multiple かどうかは無視して結果を返すことになりますし hasClass のようなメソッドを作るなら multiple かどうかを見て 配列もしくは最初の値を返すようにします

メソッドチェーンの代わりに

mulitple みたいに メソッドチェーン用のオブジェクトを返すか選ぶようにしようとしたのですけど 上で書いたようななんでもメソッドチェーンしてしまうデメリットもありますし HTML 要素を対象にしたときにメソッドチェーンで何するかを考えるとなくてもよさそうなのでなしにしました

基本やることは 属性・プロパティ・クラス・スタイルの変更や子要素の追加・削除くらいです
find して書き変えて end して また find して……みたいなものや addClass → 何かの処理 → removeClass で戻す というような操作はメソッドチェーンでしないほうがいい という考えで除外してます
そうすると プロパティ類を宣言する感じで設定できるものがあれば十分そうです

プロパティを逐次代入じゃなくて Object.assign 使ってオブジェクトリテラルで定義書いてまとめて設定みたいなものです
こういう感じ
$$$("#selector").prop = {
additionalClass: ["aa", "bb"],
data_attributes: {
id: 100
foo: "bar"
},
style: {
background: "#ffeedd",
},
children: [
document.createElement("div")
],
hidden: false,
id: "item1234",
}

prop プロパティに設定を書いたオブジェクトを代入すれば内部で設定するように setter を作っておけばメソッドチェーンなくても困らないと思います
プロパティ名やどれを対象にするかとかはっきり決めてないので prop の代入機能は作ってなくて使う側で好きに定義する使い方の予定です

$$$

ところで $$$ なのは

window.$ = (selector, scope) => (scope || document).querySelector(selector)
window.$$ = (selector, scope) => [...(scope || document).querySelectorAll(selector)]

CommandLine API で使える このセレクタたちを定義してる前提だったからです