Contents
- 1 varとconst, letの違い
- 2 Promiseでforループを順番にまわす
- 3 順番に実行したい場合
- 4 自分でイベントを発行する
- 5 スコープと実行タイミングについて
- 6 なぜコールバック関数なのか
- 7 img要素をDOMで追加できない件
- 8 ノードの追加・削除で配列のインデックスがずれる件
- 9 ノードの削除で配列のインデックスがずれることでゴミが残る件
- 10 要素を複製するにはcreateElementとcloneNodeがある
- 11 google adsenseのコードをコピーして挿入するとエラーになってるっぽい件
- 12 要素を操作するときは親の立場から
- 13 javascriptの変数をPHPでセットするとき
- 14 正規表現
- 15 HTTPS化したら外部サイトのjavascriptを読み込めなくなったとき
- 16 超シンプルな閉じるボタン
- 17 nth-of-typeよりjQueryのeq
- 18 IE11ではバッククォートが使えない
- 19 DOMオブジェクトとjQueryオブジェクトの変換
- 20 jQuery超便利な件
- 21 アロー関数
- 22 addEventListenerのコールバック関数中のthisの挙動
- 23 jQueryでの要素の指定方法
varとconst, letの違い
varは関数スコープだが、const, letはブロックスコープ。なので、const, letを無名関数で囲う以下のような書きかたは、動くけど誤解している。
1 2 3 4 5 6 7 |
(function() { const a = 1; let b = 2; console.log(a); console.log(b); })(); |
こう書いたほうが無駄がない。
1 2 3 4 5 6 7 |
{ const a = 1; let b = 2; console.log(a); console.log(b); } |
もちろん以下はまちがい。ブロック外でもaを参照できてしまう。
1 2 3 4 5 |
{ var a = 1; console.log(a); } |
Promiseでforループを順番にまわす
1 2 3 4 5 |
const data_array= [1, 2, 3, 4, 5]; data_array.forEach(data => { setTimeout(() => console.log(data), Math.random() * 10000); }); |
↑は1~5がランダムな順番で表示される。これを1、2、3、4、5の順番で表示させるには以下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const data_array= [1, 2, 3, 4, 5]; let p = new Promise(resolve => resolve()); // resolve状態のPromiseを生成。 data_array.forEach(data => { p = p.then(() => { const p_next = new Promise(resolve => { setTimeout(() => { console.log(data); resolve(); // 処理してからresolveする。関数を呼ぶ場合はfunc(resolve)みたいに引数にわたしてfuncの中でresolve()とする。 }, Math.random() * 10000); }); return p_next; // returnしたPromiseは次のループのpに入る。 }); }); |
あるいはもっと短くこう書くこともできる。
1 2 3 4 5 6 7 8 9 10 11 |
const data_array= [1, 2, 3, 4, 5]; let p = new Promise(resolve => resolve()); // resolve状態のPromiseを生成。 data_array.forEach(data => { p = p.then(() => new Promise(resolve => { setTimeout(() => { console.log(data); resolve(); // 処理してからresolveする。関数を呼ぶ場合はfunc(resolve)みたいに引数にわたしてfuncの中でresolve()とする。 }, Math.random() * 1000); })); }); |
もっと簡潔に書くこともできて、時間のかかる処理を関数にしてしまえばループの中は1行になる。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const data_array= [1, 2, 3, 4, 5]; function func(data, resolve) { setTimeout(() => { console.log(data); resolve(); // 処理してからresolveする。関数を呼ぶ場合はfunc(resolve)みたいに引数にわたしてfuncの中でresolve()とする。 }, Math.random() * 1000); } let p = new Promise(resolve => resolve()); // resolve状態のPromiseを生成。 data_array.forEach(data => { p = p.then(() => new Promise(resolve => func(data, resolve))); }); |
data_arrayが非常に大きな配列の場合、Promiseを大量に生成するためメモリーをかなり圧迫してしまう。その場合には一定間隔でループをまわすといい。
あとで気づいたんだが、Promiseオブジェクトは大量に生成してもメモリーをあまり食わない。しかしDeferredオブジェクトはさらっとGB単位を食う。DeferredではなくPromiseを使っているかぎりは以下は不要。まずDeferredから入ったことですごく遠回りになってしまった。
forループを一定間隔でまわす
まずは一定間隔でまわすだけのコード。
1 2 3 4 5 6 7 8 9 10 11 12 |
const data_array = [1, 2, 3, 4, 5]; const loop = i => { if (i >= data.length) return; // 再帰の終了 // ここに処理を書く console.log(i); setTimeout(() => loop(++i), 10); }; loop(0); |
forループを一定間隔でまわしつつ順番に実行する
上記ふたつのコードを組み合わせて、data_arrayが巨大な配列だった場合に大量に生成されるPromiseに対処する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
const data_array = [1, 2, 3, 4, 5]; let p = new Promise(resolve => resolve()); const loop = i => { if (i >= data_array.length) return; // 再帰の終端 const data = data_array[i]; // thenメソッドの中でiを参照してはいけない(++iしているので)のでここで値をとっておく。 p = p.then(() => { const p_next = new Promise(resolve => { setTimeout(() => { console.log(data); resolve(); }, Math.random() * 1000); }); return p_next; }); setTimeout(() => loop(++i), 10); }; loop(0); |
順番に実行したい場合
以下のようにDeferredを使えば、1のあとに2、そのあとに3を表示することができる。doneではなくthen。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
let d = $.Deferred(); setTimeout(function() { console.log('1'); d.resolve(); }, 1000); d.then(function() { d = $.Deferred(); setTimeout(function() { console.log('2'); d.resolve(); }, 500); return d; }).then(function() { setTimeout(function() { console.log('3'); }, 300); }); |
ポイントは
- 時間がかかる関数(この場合setTimeout関数)のコールバック関数の最後でresolveする。つまり時間がかかる関数が終わったらresolveする。
- Deferredオブジェクトを受け取るthenメソッドの先頭で新たにDeferredオブジェクトを生成する
- thenメソッドの最後に先頭で生成したDeferredオブジェクトをreturnしてつぎのthenメソッドにわたす
自分で生成しなくてもjQueryの関数はDeferredオブジェクトを返してくれるようだ。以下では1のあとに2が表示される。
1 2 3 4 5 |
$.get('test_ajax.php', function() { console.log('1'); }).then(function() { console.log('2'); }); |
ただ、以下だと1、3、2となる。1、2、3にしたい場合はひとつ目のthenメソッドのなかで自分でDeferredオブジェクトを生成する。
1 2 3 4 5 6 7 8 9 |
$.get('test_ajax.php', function() { console.log('1'); }).then(function() { setTimeout(function() { console.log('2'); }, 1000); }).then(function() { console.log('3'); }); |
forループ内でDeferrdを使いたい場合は以下のような感じ。10秒後に0、そこから9秒後に1、そこから8秒後に2、…と表示される。
1 2 3 4 5 6 7 8 9 10 11 12 |
let d = $.Deferred().resolve(); for (let i = 0; i < 10; i++) { d = d.then(function() { d = $.Deferred(); setTimeout(function() { console.log(i); d.resolve(); }, 10000 - i * 1000); return d; }); } |
ポイントはthenメソッド内からreturnしたオブジェクトをforループ内の1行目で受けてあげること。
自分でイベントを発行する
button要素からカスタムイベントを発行するには以下のように書く。
1 |
document.querySelector('button').dispatchEvent(new Event('myEvent')); |
jQuery版は以下。
1 |
$('button').trigger('myEvent'); |
イベントをキャッチするには、ふつうのイベントとおなじように以下のように書けばいい。
1 2 3 |
$('button').on('myEvent', function() { alert('myEvent発火!'); }); |
スコープと実行タイミングについて
まずは結論
素のjavascriptコードはbody要素の最後に置き、以下の無名関数で囲む。
1 2 3 |
(function() { })(); |
↑はスコープを限定するだけのものであり、いつ実行するかは制御していない。しかし、body要素の最後であれば↓で囲むのとおなじ。
jQueryを使ったコードは以下で囲む。DOM構築完了後に実行されるのでどこに置いてもいい。
1 2 3 |
jQuery(function($) { }); |
素のjavascriptコードも↑で囲むのであればどこに置いてもいい。
$とjQuery
chromeのコンソールでjQueryと$がおなじものか調べてみる。
1 2 |
jQuery == $ # true |
ほかのライブラリがグローバルスコープで$を使っている場合に衝突してしまうので、あとからjQueryというグローバル変数が追加されたらしい。$を使うのはローカルスコープのみ。グローバルスコープでは$は使わず、jQuery変数を使う。よって、以下で囲むのはだめ。
1 2 3 |
$(document).ready(function() { }); |
以下はOK。
1 2 3 |
jQuery(document).ready(function($) { }); |
ちなみに↑は2番目に出てきた以下とおなじ意味。
1 2 3 |
jQuery(function($) { }); |
なぜコールバック関数なのか
pythonとかふつうのプログラムでは
1 |
ret = func() |
こんな感じで戻り値retを受け取る。一方Javascriptでは
1 2 3 |
func(function(ret) { //retに応じた処理 }); |
みたいな感じでfuncの戻り値をコールバック関数で受け取って処理することが多い。
ずっと不思議に思っていたが、ようやくわかった。非同期に動作させるためだ。
pythonのコードではfunc関数が終わらないと次の行に処理は進まないが、Javascriptのコードではfunc関数が終わる前に次の行に処理が進んでいる。終わるとコールバック関数が処理される。逐次ではなく並列に処理が進んでいる時間がある。
次のコードでは、「あとに出るはず」より「先に出るはず」がさきにアラートされる。
1 2 3 4 5 |
setTimeout(function() { alert('あとに出るはず') }, 3000) alert('先に出るはず'); |
setTimeout関数が終わるまえに次の行に処理が進んだので「先に出るはず」がアラートされる。
img要素をDOMで追加できない件
1 2 3 |
var elem = document.getElementById("article"); var img = document.createElement("img"); elem.insertBefore(img, elem.firstChild); |
このように最初の子要素としてimg要素を追加しようとしても、なぜかできない。それ以外の要素(a要素、br要素、div要素、span要素など)は追加できる。なぜなのかは不明。とりあえずa要素やdiv要素で囲んでから追加することで回避。
ほんとうに謎。firstChildをlastChildにするとできたりする。つぎはぎの知識でやってるからわからないんだろう。いずれ体系的に学びたい。
→後日、現象を再現できず。ふつうにimg要素でも追加できる。ただ体系的に勉強しなおした結果わかったことがある。firstChildよりもfirstElementChildを使うべきとわかった。firstChildは最初の要素のまえに改行やスペースがあると、最初の要素ではなく改行やスペースを指してしまう。思ったように動作しないケースが多いので、firstElementChildを使うべし。同様にlastChildよりlastElementChildを使うべし。
ノードの追加・削除で配列のインデックスがずれる件
1 2 3 4 5 6 7 8 9 10 11 12 |
var elems = document.getElementsByClassName("entry"); for (var i = 0; i < elems.length; i++) { a_elems = elems[i].getElementsByTagName("a"); for (var j = 0; j < a_elems.length; j++) { if (a_elems[j].getAttribute("href").match(/(.jpg|jpeg|.png|.gif|.bmp)$/)) { var a_new = a_elems[j].cloneNode(true); elems[i].insertBefore(a_new, elems[i].firstChild); a_elems[j].parentNode.removeChild(a_elems[j]); break; } } } |
たとえばこのコードは、entryクラスの要素のそれぞれに対して、最初の画像リンクのa要素をentryクラスの要素の先頭子要素に移動するものだが、ループ中最後の処理であるa_elems[j]の削除がうまくいかない。苦労してデバッグした結果、ひとつまえのa要素が削除されている。先頭にa要素を追加した結果、jが指している要素がひとつ前にずれた。ということで、以下のようにj++してやることで解決できる。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var elems = document.getElementsByClassName("entry"); for (var i = 0; i < elems.length; i++) { a_elems = elems[i].getElementsByTagName("a"); for (var j = 0; j < a_elems.length; j++) { if (a_elems[j].getAttribute("href").match(/(.jpg|jpeg|.png|.gif|.bmp)$/)) { var a_new = a_elems[j].cloneNode(true); elems[i].insertBefore(a_new, elems[i].firstChild); j++; a_elems[j].parentNode.removeChild(a_elems[j]); break; } } } |
ノードの削除で配列のインデックスがずれることでゴミが残る件
↑と類似の現象。idが〇〇〇ではじまるdiv要素を全部削除したくて以下のコードをこしらえた。
1 2 3 4 5 6 7 8 9 10 11 12 |
(function () { let elems = document.getElementsByTagName("div"); if (elems != null) { for (let i = 0; i < elems.length; i++) { if (elems[i] == null) continue; let id_name = elems[i].getAttribute("id"); if (id_name != null && id_name.match(/^〇〇〇/)) { elems[i].parentNode.removeChild(elems[i]); } } } })(); |
一部は消せているが一部残ってしまう。これもやはり↑と同様インデックスがずれることが原因。削除すると後ろが前にずれてきてしまうためひとつスキップしてしまう。というわけで以下のようにi–;を追加すればいい。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
(function () { let elems = document.getElementsByTagName("div"); if (elems != null) { for (let i = 0; i < elems.length; i++) { if (elems[i] == null) continue; let id_name = elems[i].getAttribute("id"); if (id_name != null && id_name.match(/^〇〇〇/)) { elems[i].parentNode.removeChild(elems[i]); i--; } } } })(); |
この件は↑の件があったのでそんなに時間をかけずに解決できた。成長してるぞー。
要素を複製するにはcreateElementとcloneNodeがある
google adsenseなんかを任意の位置に挿入するのは、createElementではできない。cloneNode(true)を使えばかんたんにできる。
1 2 3 4 5 6 7 8 |
<div id="google_ad">code of google adsense</div> <div id="target"><!--ここにアドセンスを挿入したい--></div> <script> var div_ad = document.getElementById("google_ad").cloneNode(true); document.getElementById("target").appendChild(div_ad); div_ad.parentNode.removeChild(div_ad); </script> |
google adsenseのコードをコピーして挿入するとエラーになってるっぽい件
google adsenseのコードを挿入したあとのコードが実行されない。ので、挿入するのはいちばん最後にしないといけない。どうしても挿入したあとに実行したいコードは、scriptタグをもうひとつつくって書く。
要素を操作するときは親の立場から
要素elemがあるとして、それ自身を削除するにはいったん親の立場になってから削除する。
1 |
elem.parentNode.removeChild(elem); |
要素elemのまえに要素elem_divを挿入したい場合、やはり親の立場になってから挿入する。
1 |
elem.parentNode.insertBefore(elem_div, elem); |
javascriptの変数をPHPでセットするとき
数値をセットするときはいいけど、文字列をセットするときは注意が必要。
1 2 3 |
$str = "bold"; -------- var str = <!--?=$str?-->; //エラー |
javascriptには「”bold”」ではなく「bold」と展開されエラーとなる。以下のようにしなければならない。
1 |
var str = "<!--?=$str?-->"; |
正規表現
正規表現については各言語関数が用意されているが少しずつ違うのでメモ。
まずはマッチするかどうか
1 2 3 4 |
if (str.match(/正規表現/) { //ここにマッチしたときの処理を書く //(マッチしたときはnullではないオブジェクトが返る) } |
つぎに置換。自分的にはマッチした文字列を削除するときに置換を使うことが多い。
1 2 3 4 5 6 7 8 9 10 11 |
let str = "abc"; str.replace(/a/, ""); //strの中身は変わらない console.log(str); //abc str = str.replace(/a/, ""); //strからaを削除するにはこう書く console.log(str); //bc str = "abcabc"; str = str.replace(/a/g, ""); //複数削除するにはgオプションをつける console.log(str); //bcbc |
HTTPS化したら外部サイトのjavascriptを読み込めなくなったとき
おもむろにHTTPS化したら以下のjQueryが読めなくなった。
1 |
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script> |
src属性のhttp:を削るだけでOK。↓で動く。HTTPS化して忍者ツールズのスクリプトが読めなくなったときもhttp:削るだけでOK。
1 |
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script> |
超シンプルな閉じるボタン
1 |
<button>閉じる</button> |
最初HTML/CSSでやろうとしたが行数が多くなる。javascriptなら1行でいける。
nth-of-typeよりjQueryのeq
1 |
let elem = document.querySelector('section.content > div:nth-of-type(3)'); |
↑みたいにセレクタの最後が要素名だけならnth-of-typeを使えばいい。意図どおりsection.content直下のdivのなかの3番目をとってきてくれる。しかし次のようにすると意図どおりに動作しない。
1 |
let elem = document.querySelector('section.content > div.post:nth-of-type(3)'); |
.post以外のdivが混ざっている場合、section.content直下のdiv.postのなかの3番目をとってきてはくれない。divのなかの3番目をとってくる。3番目が.postでなければnullが返る。つまり
1 |
let elem = document.querySelector('section.content > div.post:nth-child(3)'); |
とほとんど動作が変わらない。
クラスでセレクトとするときはjQueryのeq使ったほうがいい。
1 |
let elem = $('section.content > div.post:eq(2)'); |
これなら意図通りsection.content直下のdiv.postのなかの3番目をとってきてくれる。
nth-child/nth-of-type/eqのちがいは以下。
- nth-childはsection.content直下のすべての要素(div以外も)が範囲
- nth-of-typeはsection.content直下のdivだけが範囲
- jQueryのeqはsection.content直下のdiv.postだけが範囲
IE11ではバッククォートが使えない
驚愕。IE11ではこういう書き方するとエラーになる。
1 |
let elem = document.querySelector(`section.content > div.post:nth-of-type(${insert_index})`); |
こう書かないといけない。まじかよ
1 |
let elem = document.querySelector("section.content > div.post:nth-of-type(" + insert_index + ")"); |
DOMオブジェクトとjQueryオブジェクトの変換
DOMオブジェクト→jQueryオブジェクトに変換するには、$関数(?)にDOMを入れるとjqueryに変換できる。
1 2 |
let elem = document.getElementById("ID"); $elem = $(elem); //jqueryオブジェクトを入れる変数は先頭に$をつけるのが慣習らしい。 |
jQueryオブジェクト→DOMオブジェクトは先頭要素を指定。たぶんjQueryオブジェクトがひとつの要素を指す場合のみ。
1 |
elem = $elem[0]; //0番目を指定するとDOMになる。 |
jQuery超便利な件
余計な勉強したくないのでかたくなにjquery使ってなかったが、ひとたび使ってみると病みつきだ。いまのところ素のjavascriptとくらべて以下の点が便利だなぁと(随時更新予定)。
- forループで回さなくても、指している全要素に対して処理できる
- 要素を取得した結果がnullであっても、if文でnull判定する必要なし。nullの場合もよきにはからってくれているっぽい。
アロー関数
奇妙な感じがしてなかなかなじめなかったが、無名関数を短く書くためのもの、と理解した。名前ないならfunctionキーワードすらいらないんじゃね、ってことなんでしょうか。
さらに中身が1行だけなら{}も要らないし、引数がひとつなら()すら要らないんじゃね、と。
addEventListenerのコールバック関数中のthisの挙動
どういう理屈かまだ調べてないが、試した結果、無名関数をわたすとthisは要素を指すが、アロー関数をわたすとwindowオブジェクトを指す。
したがって、アロー関数のなかでthis.classList.add()とかすると「そんなメソッドは定義されてない!(windowオブジェクトはaddメソッドを持っていない)」と怒られる。10分くらいはまった。
jQueryでの要素の指定方法
- セレクタ
- 要素名
- ID(#~)
- クラス名(.~)
- 属性セレクタ
- 完全一致(=)
- 部分一致(*=)
- など
- 疑似クラス
- :nth-child
- :nth-of-type
- :first-child
- :last-child
- :hoverとかは使えない
- フィルタ
- :eq
- :first
- :last
- :contains
セレクタ、属性セレクタ、疑似クラスはCSSでも使える。フィルタのみjQuery特有。