スタートアップのビジョン風なタイトルはさておき、高機能なフレームワークが隆盛を誇る昨今、基礎固めが大事だと思うのです。
いつ書いたのか
2020年1月頃です。
対象読者
- JavaScriptを触ったことがあるが、言語仕様は曖昧にしている。
- JavaScriptになんとなく苦手意識がある。
本稿でやること、やらないこと
- 言語仕様の特徴的なところ(私見)をまとめます。
- ベストプラクティスが変化してきたところをまとめます。
- 言語仕様を網羅はしません。
JavaScriptの今に追いつくまでの流れ
- 歴史を知る。
- https://ja.wikipedia.org/wiki/ECMAScript
- Edition1から始まり、2019年6月時点で10が公開されている。
- Edition5までが旧時代、6以降が新時代みたいな雰囲気がある。
- EditionのことをECMAScript○とかES○のように呼称する。例えば、Edition5はECMAScript5。
- ECMAScript6(Edition6、ES6も同義)以降は年号で呼称されもする。例えばES6はES2015。
- 言語仕様の特徴を知る。(後述)
- 最新版の仕様を追いかける。(これを無限ループ)
- 標準規格が毎年公開される。その後、ブラウザの仕様に反映されてゆく。(タイムラグがある)
- 2019年版
- ブラウザの対応状況を追いかける。(これも無限ループ)
言語仕様は進化し続けています。ネット上には有用な情報が溢れていますが、時系列に整理されているわけではなく、陳腐化したものも溢れています。ですから、私(たち)は混乱しますし、時に間違いも冒します。そこで、まずは歴史を振り返ります。
本稿では参考資料をいくつか紹介していますが、それらが書かれた時期に注意する必要があります。現在にも当てはまる話と、そうでないものを識別しながら読み進める必要があります。
整理しておきたいトピック
- データの型
- 変数のスコープ
- Hoisting(巻き上げ)
- Namespace(名前空間)
- 関数
- モジュール
- JavaScript風オブジェクトとオブジェクト指向
- DOM
- イベント
- 非同期通信
- 非同期処理
- クロスブラウザ問題
環境構築が面倒な人へ
https://codepen.io/
ウォーミングアップ
JavaScriptのコードをHTMLに埋め込む方法
<script></script>
- HTMLファイル中にコードを書く場合
<script type="text/javascript">コード</script>
- 外部ファイルを読み込む場合
<script src="path/to/sample.js"></script>
<a href=”JavaScript:コード”> テキスト</a>
イベントハンドラ
<input type="button" value="Click me, if you can." onclick="コード">
関数を呼び出すとHTMLがすっきりします。
<input type="button" value="Click me, if you can." onclick="func();">
ただ、そもそもHTML中にJavaScriptのコードを混在させることをよしとしない考え方もあります。
<script>
を埋め込む位置
<body>
の中
スクリプトの読み込みや実行が完了するまで描画が行われないので</body>
の直前に埋め込むのが吉。
注意事項:ここで使う関数は、ここより以前の<script>
で定義されていなければなりません。<head>
の中
表示速度を上げるために</body>
の直前で埋め込むのがベストプラクティスとされていますが、個別事情により<head>
で埋め込む場合もあります。
参考資料:Stack Overflow
ステートメント
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements
- 基本、セミコロンで区切られた塊を一命令と解釈する。
- 必ずしも一行が一命令ではなく、空白、改行、タブを含めてもよい。
- 大文字小文字を区別する。
- コメントは
//
または/**/
変数名
- 1文字目は英字、アンダースコア、$。
- 2文字目は、1文字目で使える文字か、数字。
- 大文字小文字を区別する。
- 予約語は使えない。
エスケープ表記
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#Escape_notation
演算子
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators
算術演算について、誤差により計算結果が期待通りにならない場合があるので注意しましょう。整数にしてから計算したり、ライブラリを利用して対処します。
ieee754.js
console.log(0.1*3);// 0.30000000000000004
参考資料:JavaScriptで小数点の誤差が発生する件の備忘録
繰り返し処理
様々な繰り返し処理の説明はこちら。
ES2015でfor...of
が導入されました。for...of
は、反復可能オブジェクト(配列、Map、Set、argumentsのような配列ライクなオブジェクト、イテレータやジェネレータ)のプロパティを順に処理します。
forof.js
constbros=['ラオウ','トキ','ケンシロウ'];bros.whoAmI='ジャギ';for(constbofbros){console.log(b);}// ラオウ// トキ// ケンシロウ
for...of
はプロパティの値に対して処理を行う一方、for...in
はプロパティ名に対して処理を行います。また、for...of
は値を相手にするのに対して、for...in
は参照も相手にします。
forin.js
for(constbinbros){console.log(b);}// 0// 1// 2// whoAmI
ループからの脱出にはbreak
やlabel
を使います。
label.js
label:for(){for(){breaklabel;}}
配列
以下のコードは等価です。
array.js
constfriends=['ゴン','キルア'];constfriends=newArray('ゴン','キルア');
連想配列については後述します。
ES2015で値の取り出しが便利になりました。Destructuring assignmentの使い方はこちら。オブジェクト(後述)にも適用できます。
参考資料:ECMAScript 2015 の分割代入は奥が深かった
Strictモード
strict モードでは、通常の JavaScript の意味にいくつかの変更を加えます。第一に strict モードでは、JavaScript でエラーではないが落とし穴になる一部の事柄を、エラーが発生するように変更することで除去します。第二に strict モードでは、JavaScript エンジンによる最適化処理を困難にする誤りを修正します: strict モードのコードは、非 strict モードの同一コードより高速に実行できることがあります (Firefox 4 ではまだ strict モードにあまり最適化していませんが、将来のバージョンで実現するでしょう)。第三に strict モードでは、将来の ECMAScript で定義される予定の構文を禁止します。
MDN
何がうれしいか、使い方、使用上の注意はこちら。
JavaScriptが使えないブラウザ向け
fossil.html
<noscript>メッセージ</noscript>
整理しておきたい言語仕様
以下、ブラウザの実行結果は主にFirefox(Quantum 60.8.0esrや72.0.2)で確認したものです。
データの型
元々は7つ、ECMAScript 2015(以降、ES2015と呼称)でSymbol型が追加されて8つの型が定義されています。
- Java等とは違い、変数の宣言時に型を明示しない。
- 型変換に要注意。
参考資料:型変換のいろいろ - 演算にまつわる型変換(キャスト)
+
と-
の違いに注目。==
はオペランドの型が異なる場合、キャスト後に評価する。===
はキャストせず評価する。
typecasting.js
// string + numberの場合、numberをstringにキャストして文字列連結conststringPlusNumber="abc"+1;console.log(stringPlusNumber);// abc1// string - numberの場合、stringをnumberにキャストして減算(stringが非数字)conststringMinusNumber="abc"-1;console.log(stringMinusNumber);// NaN// string - numberの場合、stringをnumberにキャストして減算(stringが数字)constnumeralMinusNumber="123"-1;console.log(numeralMinusNumber);// 122// object + numberの場合、objectとnumberをstringにキャストして文字列連結(toStringなし)constPerson=function(){this.name='Thom Browne',this.greeting=function(){console.log('Hi! I\'m '+this.name+'.');}};constperson=newPerson();letobjectPlusNumber=person+1;console.log(objectPlusNumber);// [object Object]1// object + numberの場合、objectとnumberをstringにキャストして文字列連結(toStringあり)Person.prototype.toString=functionpersonToString(){returnthis.name;}objectPlusNumber=person+1;console.log(objectPlusNumber);// Thom Browne1
型を判定する方法がいくつかあります。
参考資料:JavaScriptの「型」の判定について
Symbol型は、ぱっとみ意味が分からないですね。参考資料があります。
ECMAScript6にシンボルができた理由
変数のスコープ
JavaScriptにはかつて、2つのスコープがありました。
- 関数内でのみアクセス可能な、ローカルスコープ。
- どこからでもアクセス可能な、グローバルスコープ。
ES2015で1つ追加されました。
if
やfor
などの{}
の内側でのみアクセス可能な、ブロックスコープ。
(ローカルスコープを関数スコープとブロックスコープに分て考えるようになったっぽい。)
(ざっくりとした)スコープの決定メカニズム
- const, let, var を付けずに宣言した変数はグローバルスコープ。
- var を付けた場合、宣言した場所による。
- 関数内で宣言した場合、関数スコープ。
- そうでない場合、グローバルスコープ。
- const, let を付けた場合、宣言した場所がスコープになる。
- 関数の(値渡しした)引数は関数スコープ。
scope.js
constc="c-global";letl="l-global";varv="v-global";functionscope(){constc="c-function";letl="l-function";varv="v-function";console.log(c);// c-functionconsole.log(l);// l-functionconsole.log(v);// v-functionif(true){constc="c-block";letl="l-block";varv="v-block";console.log(c);// c-blockconsole.log(l);// l-blockconsole.log(v);// v-block}console.log(c);// c-functionconsole.log(l);// l-functionconsole.log(v);// v-block <-- ココに注目}scope();console.log(c);// c-globalconsole.log(l);// l-globalconsole.log(v);// v-global
ちなみに、letが無かった時代には、withを用いたテクニックを使ってブロックスコープを実現していました。withの使用は推奨されません。忘れましょう。
参考資料:Architect Note
const, let, varの使い分けについては、左から順に優先して使えばよさそうです。変数を書き換える(変数から取り出した値を加工して入れ直す)場合、加工してるので別名の変数に格納すべきと考えられ、ゆえにconstで事足りそうです。for
中のi
のように繰り返し上書きする場合にはletが使えそうです。多くの場合、var
は不要なはずです。
いずれスコープチェーンについても理解する必要がありますが、まずはJavaScriptのオブジェクト(後述)を理解するのが先です。
参考資料:tacamy--blog
Hoisting(巻き上げ)
https://developer.mozilla.org/en-US/docs/Glossary/Hoisting
- varの巻き上げ
ローカル変数は、スコープの冒頭で宣言、初期化するのが無難なようです。
hoisting.js
varscope='g';functionlocalScope(){// 暗黙的にvar scope;が宣言されたと解釈される。// scopeにはまだ値が入っていないためundefinedとなる。console.log(scope);// undefinedvarscope='l';console.log(scope);// lreturnscope;}console.log(localScope());// lconsole.log(scope);// g
Namespace(名前空間)
JavaScript でネームスペースを作成する考え方はシンプルです。グローバルオブジェクトをひとつ作成して、すべての変数、メソッド、関数をそのオブジェクトのプロパティとすればよいのです。ネームスペースを使用すると、アプリケーション内で名前が衝突する可能性が低下します。これは各アプリケーションのオブジェクトが、アプリケーションで定義したグローバルオブジェクトのプロパティとなるからです。
MDN
スコープや名前空間に付随するあれこれ
JavaScriptには元々スコープが2つしかなく、モジュール(後述)にも非対応だった等の理由から、変数の汚染(意図せずに変数を上書きしてしまう事故)を防ぐためのテクニックが活用されていました。読みやすく、保守しやすいコードを書くために、変数を多用しすぎないのが吉という考え方がありますが、JavaScriptは比較的変数の汚染が起きやすいという性質も相俟って、このようなテクニックが発達したように見えます。
即時関数
即時関数という仕組みがありましたが、役割を終えた感があります。
参考資料:
JavaScriptで即時関数を使う理由
JavaScriptから即時関数を排除する
closure(クロージャ)
まずは見た目に慣れるところから始めたいと思います。
iamclosure.js
functioniAmClosure(){letval;// ローカル変数constfunc(){// 関数の中で関数を宣言する// ローカル変数valを参照する処理}}
使ってみます。
iamclosuretoo.js
functioniAmClosureToo(init){letcount=init;returnfunction(){// 関数を返すと、呼び出し元のプログラムで繰り返し呼び出せるreturncount++;}}constcounter=iAmClosureToo(1);// counterにはcountを参照する匿名関数がセットされるconsole.log(counter());// 1console.log(counter());// 2
クロージャを理解するには、まずレキシカルスコープ(静的スコープ)とダイナミックスコープを理解する必要がありそうです。
レキシカルスコープは関数を定義した時点でスコープが決まり、ダイナミックスコープは関数を実行した時点でスコープが決まるものです。どちらになるかは言語次第のようで、JavaScriptは前者です。
lexical.js
constval=1;functionsub(){console.log(val);// sub()の定義時点でvalは1}functionmain(){constval=2;sub();// sub()の実行時点でvalは2}main();// 1sub();// 1
MDNの例で、何が特徴的なのかを確認します。
他の言語に慣れている人は"関数内部のローカル変数はその関数が実行されている間だけ存在する"と考えることに慣れているかもしれません。つまり、var myFunc = makeFunc();
の実行時点でローカル変数name
が消滅するように直感し、結果このコードは動かないと考えられます。しかし、JavaScriptでは、このコードが動きます。JavaScriptでは、displayName()の定義時点でスコープが決まり、nameにはMozillaが設定されるものと解釈します。クロージャでは、この時の環境への参照を保持しており、myFuncを実行した時に結果を返すことができます。
closure.js
functionmakeFunc(){varname="Mozilla";functiondisplayName(){console.log(name);}returndisplayName;}varmyFunc=makeFunc();myFunc();// Mozilla
で、何に使うんだよ?という疑問に答えてくれる人がたくさんいます。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures#Practical_closures
https://stackoverflow.com/questions/2728278/what-is-a-practical-use-for-a-closure-in-javascript/39045098
https://artgear.hatenablog.com/entry/20120115/1326635158
http://dqn.sakusakutto.jp/2012/02/javascript_13.html
クロージャ再考
で、オブジェクトとクロージャは何が違うんだよ?という疑問に答えてくれる人も見つけました。(実質答えてないけど)
https://www.ibm.com/developerworks/jp/opensource/library/itm-progevo3/index.html
https://www.ibm.com/developerworks/jp/opensource/library/itm-progevo4/index.html?ca=drs-
オブジェクトとクロージャに関する考察に関する参考資料をもう一つ。
http://kmaebashi.com/programmer/devlang/object.html
クロージャについては、様々な言語の成り立ちやコンピュータサイエンス的なことへの理解が試される気がします。個人的には、クロージャにしかできないことがまだ見つからないため、探求を続けたいと思います。
関数
定義の仕方が3つあります。
- 関数宣言
function functionName () {}
- コードを解析、コンパイルするタイミングで関数を登録するため、同一の
<script>
内で、定義する前に呼び出しのコードがあっても正しく処理される。
- 関数式(匿名関数、無名関数)
const functionName = function(arg) {}; functionName(1);
- この場合、関数は変数なので、呼び出しよりも先に定義する必要がある。
- 変数を節約できる。
- 再利用しない場合(高階関数の引数にする場合やコールバック関数等)に重宝する。
- Functionコンストラクタ
- スコープの解釈が直感と一致しない上、セキュリティとパフォーマンスの問題もある。
- この方法で定義するメリットは基本的にないらしい。
- 但し、Object.prototype.constructorの戻り値として内部的に使われていたりもするため、(開発者が)使わないなら要らないじゃんってことにはならない。
そして、ES2015でアロー関数が導入されました。アロー関数は、関数式の短い代替構文で、2つの理由から導入されました。
- 簡潔に書ける。
- thisを束縛しない。
書き方はこんな感じ。
function(x) {}
が(x) => {}
になる。- 1行で書ける時は{}とreturnを省略できる。
- 引数が1つの場合()を省略できる。
function(x) {}
がx => {}
になる。 - 引数がない場合、()を必ず書く。
function() {}
が() => {}
になる。
"thisを束縛しない"の意味がぱっとみ分からないと思います。参考資料があります。コメント欄にも注目です。
JavaScript の this を理解する多分一番分かりやすい説明
【JavaScript】アロー関数式を学ぶついでにthisも復習する話
引数
デフォルト引数
ES2015で、デフォルト引数の指定が可能となりました。それ以前は、argumentsオブジェクトを利用したテクニックを使って実装していました。デフォルト引数には以下のようなルールがあります。
- デフォルト引数は、引数を指定しない時かundefinedを明示的に指定した時に適用される。
- nullやfalseを指定した場合には適用されない。
- デフォルト値には(先に宣言された)他の引数、関数の結果も指定できる。
使い方はこちら。
必須パラメータを指定するのにも利用できます。
required.js
functionrequired(){thrownewError('Argument is required');}functionisString(value=required()){returntypeofvalue=="string"||valueinstanceofString;}console.log(isString("abc"));// trueconsole.log(isString(123));// falseconsole.log(isString());// Argument is required
Rest parameters(可変長引数と訳す?)
従来はargumentsオブジェクトを活用して実装していましたが、ES2015でRest parametersが使えるようになりました。ちなみにargumentsとの違いがいくつかあります。詳細はこちら。
restparams.js
functionrestparams(...args){console.log(argsinstanceofArray);// truereturnargs.length;}console.log(restparams());// 0console.log(restparams("one"));// 1console.log(restparams("one","two"));// 2
名前付き引数(言語仕様ではなく、テクニックの話)
引数に名前を付けられると、以下のようなメリットが考えられます。
- コードの可読性が上がる。
- 引数の順番を変えられる。
- 引数の省略を柔軟にできる。
namedarguments.js
// function bmi(weight_kg, height_m) {...}functionbmi(args){if(args.weight_kg==undefined){args.weight_kg=70;}if(args.height_m==undefined){args.height_m=1.9;}return(args.weight_kg/(args.height_m**2));}bmi({weight_kg:58,height_m:1.7});// 引数の意味が明確bmi({height_m:1.7});// 前方の引数を省略bmi({height_m:1.7,weight_kg:58});// 引数の順序を入れ替え
モジュール
かつてJavaScriptはWebサイトにちょっとした動きを与えるような小さなコードだったようです。しかし、昨今ではサーバーサイドでも使われたり、フロントエンドでも複雑な動き与えるなどゴリゴリに使い倒され、巨大なアプリケーションになりました。そのため、コードをモジュール化してスコープを分離したり、着脱可能にするアーキテクチャへの要請が高まったようです。結果、ES2015からモジュールが使えるようになりました。
使い方はこちら。
ただし、IEは<script type="module">
に非対応だそうです。
モジュールバンドラ
IEの問題やHTTP/1.1の問題(ファイルをたくさん読み込むと通信が非効率)等への対応や、その他のメリットもありwebpackなどのモジュールバンドラを利用します。
参考資料:最新版で学ぶwebpack 4入門 JavaScriptのモジュールバンドラ
JavaScript風オブジェクトとオブジェクト指向
オブジェクト指向にはクラスベースとプロトタイプベースがあります。JavaやPHPは前者、JavaScriptは後者です。その他のポイントも含めていくつか書き留めてみます。
JavaScriptにおいて、連想配列はオブジェクトです。
associativearray.js
constaArray={x:1,y:2};console.log(typeofaArray);// objectconsole.log(aArrayinstanceofArray);// falseconsole.log(aArray['x']);// 1console.log(aArray.x);// 1
連想配列に限らず様々なものがオブジェクトとして表現されます。標準ビルトインオブジェクトを参照のこと。標準ビルトインオブジェクトの他にもブラウザの操作に使うオブジェクト、DOMの操作に使うオブジェクト、Ajaxに使うオブジェクト等があります。
標準ビルトインオブジェクト
適当に例を列挙します。
Arrayオブジェクト
使い方はこちら。配列の操作(ソート等)に重宝する。
Dateオブジェクト
date.js
lettoday=newDate();letbirthday=newDate('1995/12/17 03:24:00');letbirthday=newDate('December 17, 1995 03:24:00');letbirthday=newDate('1995-12-17T03:24:00');letbirthday=newDate(1995,11,17);letbirthday=newDate(1995,11,17,3,24,0);
日付、時刻を加減算するメソッドはなく、getXxxx(), setXxxx()で実現します。期間の差は以下の様にして求めます。
tmn.js
conststart=newDate(1984,4,21);constend=newDate(1994,4,21);(end.getTime()-start.getTime())/(1000*60*60*24);
上述した通りJavaScriptにはdate型はありません。使い方はこちら。罠にご注意下さい。
参考資料:
JavaScript の Date は罠が多すぎる
【JavaScript】日付処理
Numberオブジェクト
number.js
constnum=123;constnumo=newNumber(123);console.log(num);// 123console.log(numo);// 123console.log(typeofnum);// numberconsole.log(typeofnumo);// objectconsole.log(numoinstanceofNumber);// true
使い方はこちら。ちなみに、NaNは、NaNを含む全ての値と等しくない。NaNを検出するにはisNaN関数を使用します。
isnun.js
console.log(Number.NaN==Number.NaN);// falseconsole.log(isNaN(Number.NaN));// true
Stringオブジェクト
string.js
conststr='文字列';conststro=newString('文字列');console.log(str);// 文字列console.log(stro);// 文字列console.log(typeofstr);// stringconsole.log(typeofstro);// objectconsole.log(stroinstanceofString);// true
使い方はこちら。文字列の操作(部分抽出等)に重宝します。
RegExpオブジェクト
正規表現を司ります。ざっくりとした使い方は以下のような感じ。詳しくはこちら。Editionが上るにつれて進化しているので、最新仕様をご確認のこと。
re.js
constre=newRegExp('正規表現','オプション');constre=/正規表現/オプション;
argumentsオブジェクト
関数の引数の情報を管理する配列ライクなオブジェクト1。引数のチェックや可変長引数(?)の実装に利用されていた部分は、新たに導入されたデフォルト引数やRest parametersにより代替できると考えられます。
使い方はこちら。
再帰関数を作る時にcalleeプロパティが役に立ちます。
factorial.js
functionfactorial(n){if(n!=0){// calleeの代わりに関数名を直書きした場合、関数名を変更した時に修正が必要。// また、匿名関数の場合は、関数名がないのでcallee一択。returnn*arguments.callee(n-1);}return1;}console.log(factorial(3))// 6
Objectオブジェクト
すべてのオブジェクトの元になっているオブジェクトで、すべてのオブジェクトはObject.prototypeからメソッドとプロパティを継承しています。使い方はこちら。
ところで、toString()
は指定したオブジェクトを表す文字列を返します。toString()
は人知れず内部的に使われていることがあります。暗黙の型変換の如く、オブジェクトが文字列に変換されていたりします。
object.js
object=newObject();date=newDate();array=newArray([1,2,3]);console.log(object.toString());// [object Object]console.log(date.toString());// Mon Jan 27 2020 22:57:56 GMT+0900 (JST)console.log(array.toString());// 1,2,3
匿名(無名)オブジェクトというものがあります。再利用する必要がない場合に利用します。
anonymous.js
constobj=newObject();// オブジェクトを宣言した後に外からプロパティを設定できる。obj.name='anonymous';obj.role='anything';console.log(obj.toString());// [object Object]console.log(obj.name);// anonymousconsole.log(obj.role);// anything// const obj = { name:'anonymous', role:'anything' }; でも同じ結果が得られる。
ES2015で追加されたオブジェクト
Proxy、Reflect、Map、Set、WeakMap、WeakSet、Promise(後述)など。
Global object(単数形) と Global objects(複数形)
日本語訳注: 英語版では、JavaScript 実行時に作成される、グローバルスコープを表す唯一となるグローバルオブジェクトを単数形の global object とし、そのグローバルオブジェクトが持つ「グローバルにアクセスできる変数、関数などのオブジェクトの一群」のプロパティを複数形の global objects として表しています。日本語の文法は単数形、複数形を持たないため、上記では単数形の方に『』を付けてそれぞれを区別しています。
MDN
Global objectは他のオブジェクトのようにインスタンス化するものではなく、グローバル関数等を管理するための領域と考えられます。グローバルプロパティや関数の使い方はこちら。
JavaScriptではところどころで暗黙的に型変換が行われますが、明示的に行うこともできます。バグを出さないために、関数の癖をつかんでおく必要があります。
typecast.js
console.log(Number('123abc'));// NaNconsole.log(parseFloat('123abc'));// 123console.log(parseInt('123abc'));// 123console.log(Number('abc123'));// NaNconsole.log(parseFloat('abc123'));// NaNconsole.log(parseInt('abc123'));// NaNconsole.log(Number(newDate(1984,4,21)));// 453913200000console.log(parseFloat(newDate(1984,4,21)));// NaNconsole.log(parseInt(newDate(1984,4,21)));// NaNconsole.log(Number('0o123'));// 83console.log(parseFloat('0o123'));// 0console.log(parseInt('0o123'));// 0console.log(Number('314e-2'));// 3.14console.log(parseFloat('314e-2'));// 3.14console.log(parseInt('314e-2'));// 314
decodeURI()
, encodeURI()
, decodeURIComponent()
, encodeURIComponent
なんかも要チェック。escape()
は非推奨。eval()
はセキュリティホールになりがちなので要注意、パフォーマンスもいまいちらしい。
ブラウザを操作するオブジェクト
ECMAScriptにより標準されているわけではなく、ブラウザベンダーが独自に実装しています(尚、本稿で主に参照しているmozillaの仕様においてはDOMやHTML Living Standardなどを参照しているようです)。共通して使えるものもたくさんありますが、標準ではありません。異るブラウザで動作するようにコードを作るのは大変です。標準化されているDOMを操作するオブジェクトを利用するのが望ましいという考え方もあります。
w3schools.comによると、Browser Objectsには、Window Object, Screen Object, Location Object, History Object, Navigator Objectがあります。
ブラウザオブジェクトは階層構造になっているそうです。参考資料中にあるDocumentオブジェクトはw3schoolsが示すブラウザオブジェクトの中には見当たりません。
参考資料:JavaScript 〜ブラウザオブジェクトを操作してみよう〜
MDNによると、
Window.screenプロパティは、読み取り専用で、Screen objectへの参照を返します。
Window.locationプロパティは、読み取り専用で、Location objectへの参照を返します。
Window.historyプロパティは、読み取り専用で、History objectへの参照を返します。
Window.navigatorプロパティは、読み取り専用で、Navigator objectへの参照を返します。
Window.documentプロパティは、読み取り専用で、そのウィンドウが含むdocumentへの参照を返します。
ここで、Windowとwindowの違いを確認しておきます。Stack Overflowによると、Windowはfunctionで、windowはグローバル変数です。windowはWindowのインスタンスを保持しています。
Window Object
document
については後述しますが、window
のプロパティにアクセスする時、window.
を明記してもしなくても構いません。Window
は開発者が触るもんじゃないようです。
window.js
document.write('abc');// OKwindow.document.write('abc');// OKだけどwindow.が不要Window.document.write('abc');// エラー
alert()
, confirm()
なども同様に使えます。使い方はこちら。
Screen Object
ざっくりURLの情報を取得するのに使うオブジェクトという認識。使い方はこちら。
Location Object
ざっくりURLの操作に使うオブジェクトという認識。使い方はこちら。
History Object
ざっくりブラウザが管理する閲覧履歴の操作に使うオブジェクトという認識。使い方はこちら。
Navigator Object
ざっくりユーザーエージェントの操作に使うオブジェクトという認識。使い方はこちら。
Document
window
よろしくdocument
もDocumentのインスタンスを保持していると考えればいいのかしら。ざっくりDOMの操作に使うオブジェクトという認識。使い方はこちら。
document.html
<script type="text/javascript">functionaccessToForm(){// 以下は全て等価console.log(document.formName.textName.value);console.log(document.forms[0].elements[0].value);console.log(document.forms['formName'].elements['textName'].value);console.log(document['formName']['textName'].value);constsT=Object.prototype.toString;console.log(sT.call(document));// [object HTMLDocument]console.log(sT.call(document.formName));// [object HTMLFormElement]console.log(sT.call(document.formName.textName));// [object HTMLInputElement]}</script><formname="formName"onsubmit="accessToForm(); return false;"><inputtype="text"name="textName"></form>
プロトタイプベースなオブジェクト指向
- JavaScriptにはクラスという概念はなく、プロトタイプという概念がある。
- プロトタイプオブジェクトというものがあり、すべてのオブジェクトはプロトタイプオブジェクトのメソッドとプロパティを継承する。
- newすることを想定した関数オブジェクトのことをコンストラクタと呼ぶ。
- オブジェクトに対して外部からメンバを追加できる。
- その結果、同一のコンストラクタをもとに生成されたインスタンスであっても、それぞれが持つメンバが異なる場合がある。
- JavaScriptにおけるクラスベース風なオブジェクト指向
ES2015でクラスベース風なオブジェクト指向の書式が導入されたが、これはシンタックスシュガー(糖衣構文)で、JavaScriptのオブジェクト指向は引き続きプロトタイプベース。
ちなみに、厳密には"メソッド"という言い方はふさわしくないかもしれませんが、本稿では便宜的にそう呼びます。
JavaScript には、クラスベースの言語が定義する形式の「メソッド」はありません。 JavaScript ではどの関数も、オブジェクトのプロパティという形で追加することができます。
MDN
constructor.js
// コンストラクタの定義constPerson=function(name){// thisキーワードを用いてメンバを定義するthis.name=name;// メソッドの定義this.introduce=function(){return'I am '+this.name+'.';}};constperson=newPerson('Michael');console.log(typeofperson);// objectconsole.log(personinstanceofPerson);// trueconsole.log(person.introduce());// I am Michael.// オブジェクトの定義後にメンバを追加できるperson.nickname='Mike';person.callme=function(){return'Please call me '+this.nickname+'.';}console.log(person.callme());// Please call me Mike.
さて、コンストラクタをnewして生成されるインスタンスは、個々に異るメモリ領域を確保します。上記の例でPerson
のインスタンスを複数作成すると、全てのインスタンスは同じintroduce
メソッドを持ちますが、それぞれ別のメモリ領域を確保してしまいます。この非効率を、prototypeプロパティを用いて解消します。prototypeはデフォルトで空のオブジェクトを参照していて、これにメンバを追加できます。以下の様にコードを書き換えます。
prototype.js
// 変更前person.callme=function(){return'Please call me '+this.nickname+'.';}// 変更後// 'p'ersonインスタンスではなく、'P'ersonコンストラクタのprototypeにメソッドを定義Person.prototype.callme=function(){return'Please call me '+this.nickname+'.';}console.log(person.callme());// Please call me Mike.
Person
はprototypeプロパティを通じてcallmeメソッドを参照しています。Person
を元にして生成されたperson
もcallmeメソッドを参照しています。なので、person.callme()
のようにしてcallmeメソッドを利用できます。const another_person = new Person('Mary');
のようにして生成したanother_person
もcallmeメソッドを参照しており、another_person.callme()
のようにして利用できます。callme()はメモリ上に一つあり、個々のインスタンスはそれを参照するため、別々のメモリ領域を確保することはありません。
プロトタイプにメソッドをたくさん定義する時は、以下の様にすると楽です。
prototype.js
Person.prototype={callme:function(){return'Please call me '+this.nickname+'.';},initialIs:function(){returnthis.name.slice(0,1);}};constperson=newPerson('Michael');person.nickname='Mike';console.log(person.callme());// Please call me Mike.console.log(person.initialIs());// M.
prototype中のメソッドを書き換えると、それを参照する全てのインスタンスに影響があります。
![s_DD270B68D27518943D49DED4298C9E2331541FFEBFA1DA6251D773064106AE8A_1579138403353_image.png]()
ところで、person
インスタンスに後から追加したcallme
メソッドはperson
に固有のメソッドとなり、Person
から生成される他のインスタンスに引き継がれることはありません。(ゆえに、同一のコンストラクタをもとに生成されたインスタンスであっても、それぞれが持つメンバが異なるケースが発生します。)
プロトタイプチェーン
あるオブジェクトのプロパティにアクセスを試みるとき、そのオブジェクトのみならず、そのオブジェクトのプロトタイプ、プロトタイプのプロトタイプ…と、一致する名前のプロパティが見つかるか、プロトタイプチェーンの終端に到達するまで、そのプロパティが捜索されます。
MDN
上記の例で、person.callme()
というコードが実行される時、まずperson
の中にcallmeメソッドを探しに行きます。存在しないため、プロトタイプの中に探しに行きます。そこで見つかったので実行します。
ここで確認しておきたいことがあります。
Person.prototype.callme
が定義済みの状態で、以下のコードを実行します。
prototype.js
person.callme=function(){return'Do NOT call me '+this.nickname+'.';}
この時、プロトタイプチェーンを辿りプロトタイプの中のcallmeメソッドを書き換えることはありません。この場合、person
中に新たにcallmeメソッドが定義されます。その後、person.callme()
を実行すると、プロトタイプチェーンの仕組みに則り、person
の中にcallmeメソッドを探しに行きます。そこで見つかるため実行します。プロトタイプ中のcallmeメソッドが上書きされることはないので、他のインスタンスに影響を与えることはありません。
ここまでの内容を、もし俺がまとめてこられてきてたとしたら2、こう。
prototype.js
constPerson=function(name){this.name=name;this.introduce=function(){return'I am '+this.name+'.';}};constperson=newPerson('Michael');console.log(typeofperson);// objectconsole.log(personinstanceofPerson);// trueconsole.log(person.introduce());// I am Michael.person.nickname='Mike';Person.prototype.callme=function(){return'Please call me '+this.nickname+'.';}console.log(person.callme());// Please call me Mike.person.callme=function(){return'Do NOT call me '+this.nickname+'.';}console.log(person.callme());// Do NOT call me Mike.constanother_person=newPerson('Mary');another_person.nickname='May';console.log(another_person.callme());// Please call me May.
もう一つ注意事項があります。
overrideprototype.js
constMoten=function(){};constKanki=function(){};constPerson=function(){};Moten.prototype={strategize:function(){return'全軍撤退します。';}};Kanki.prototype={strategize:function(){return'鄴をぶんどります。';}};// prototypeにオブジェクトへの参照を設定Person.prototype=newMoten();constperson=newPerson();// Personをnewした後に別のオブジェクトを設定すると...Person.prototype=newKanki();// prototypeが上書きされていないconsole.log(person.strategize());// 全軍撤退します。
静的メソッド
static.js
constPerson=function(){};Person.bePeaceful=function(){return'We are the one.';}// Personをnewせずにメソッドを呼び出すconsole.log(Person.bePeaceful());// We are the one.// インスタンスから呼び出そうとするとエラーになるconstperson=newPerson();console.log(person.bePeaceful());// TypeError: person.bePeaceful is not a function
静的メンバはprototypeを参照しません。
クラスベース風なオブジェクト指向の静的メソッドはこちら。
プライベートメンバ
JavaScriptにはアクセス修飾子(public、protected、private)がありません。言語仕様として、プライベートメンバを定義することができません。しかし、それっぽくするテクニックはあります。例えば、クロージャを利用したやり方があります。その他にも様々な方法が提案されています。
クロージャ派、Symbol型派、WeakMap派と、運用でなんとかする(privateメンバに名前の頭に_を付ける)派をよく見かけます。
クラスベースなオブジェクト指向
ES2015でクラスベース風なオブジェクト指向の書式が導入されました。しかし、これはシンタックスシュガー(糖衣構文)です。JavaScriptのオブジェクト指向は引き続きプロトタイプベースです。
書き方はこんな感じです。クラスの巻き上げはないの定義に呼び出すとエラーになります。
classbaseclass.js
classPerson{constructor(name){this.name=name;}introduce(){return`I am ${this.name}.`;}}constperson=newPerson('Michael');console.log(person.introduce());
匿名クラスが宣言できます。
プロパティを単独で宣言することはできず、constructor中で宣言するか、getter, setterを使うかのどちらかっぽい。getter, setter, 継承など、詳細はこちら。
DOM
DOMを正確に理解するには調べていただくとして、ざっくり以下の様に認識しています。
- DOMはオブジェクトである。
- Webページを、ある決まりに従ってDOMで表現する。
- JavaScriptはDOMを通じてWebページを操作する。
DOMは標準に則り生成されるオブジェクトですが、ブラウザやバージョンによって若干異るようです。
標準はこちら。特にここ。
DOMでは、文書(Webページ)の構成要素(<html>
, <body>
, <p>
やタグで囲まれたテキスト等)をノードとみなします。JavaScriptはノードを取得して、中身を参照したり、書き換えたりします。
要素(タグ)の操作
取得するノードを識別する方法は2つあります。
- idのようなキーで識別する。(
document.getElementById
など) - あるノードからの相対的な位置関係で識別する。
direct.html
<script type="text/javascript">functionaccessToForm(){constform=document.getElementsByTagName('form');constinput=document.getElementsByTagName('input');console.log(form[0]);// <formname="form"onsubmit="accessToForm(); return false;">// <inputname="name"type="text">// <inputname="address"type="text">// <inputvalue="submit"type="submit">// </form>
console.log(form[0][0]);// <inputname="name"type="text">console.log(form[0][1]);// <inputname="address"type="text">console.log(form[1]);// undefinedconsole.log(input[0]);// <inputname="name"type="text">console.log(input[1]);// <inputname="address"type="text">console.log(input[3]);// undefined}</script><formname="form"onsubmit="accessToForm(); return false;"><inputtype="text"name="name"><inputtype="text"name="address"><inputtype="submit"value="submit"></form>
walking.html
<script type="text/javascript">functionaccessToForm(){constselect=document.getElementById('finish');constoptions=select.childNodes;for(leti=0;i<options.length;i++){constoption=options.item(i);if(option.nodeType==1){console.log(option.value+''+option.textContent);// right ひ…ひと思いに右で…やってくれ// left ひ…左?// both もしかしてオラオラですかーッ!?}}}</script><formname="form"onsubmit="accessToForm(); return false;"><selectid="finish"><optionvalue="right">ひ…ひと思いに右で…やってくれ</option><optionvalue="left">ひ…左?</option><optionvalue="both">もしかしてオラオラですかーッ!?</option></select><inputtype="submit"value="やれやれだぜ"></form>
指定したノードが存在しない場合undefined
を出力しています。存在する場合、ノードに応じたオブジェクトを返します。例えば、form
やinput
の場合HTMLCollection
、<select>
の場合HTMLSelectElement
、option
の場合NodeList
。オブジェクトが持つメンバを使って操作できます。
ノードを追加するには以下の様にします。詳細はDOMの仕様書等をご参照のこと。
addnode.html
<script type="text/javascript">functionadd(form){constgraffiti=form.graffiti.value;constnote=document.getElementById('note');// 要素を作成するconstbutton=document.createElement('button');// 属性を作成するconstvalue=document.createAttribute('value');value.nodeValue=graffiti;// 要素に属性、テキストを関連付けるbutton.setAttributeNode(value);button.textContent=graffiti;// ページに要素を関連付けるnote.appendChild(button);// テキストノードを作成するconsttext=document.createTextNode(graffiti);// ページにノードを関連付けるnote.insertBefore(text,button);}</script><form><inputtype="text"name="graffiti"><inputtype="submit"value="ノードを追加する"onclick="add(this.form); return false;"></form><divid="note"></div>
ノードを書き換える場合は<親ノード>.replaceChild(<置換後のノード>, <置換対象のノード>)
、削除する場合は <親ノード>.removeChild(<削除対象ノード>)
。
属性の操作
要素中の属性にアクセスしたい場合、上記の様にして要素を取得してから、<要素>.<属性>
のようにしてアクセスできる属性が多いですが、class
はclassName
でアクセスするなど、変則的なものもあります。
あるいは、Elementのメンバを利用して、<要素>.getAttribute(<属性>)
、<要素>.setAttribute(<属性>, <値>)
のようにして操作することもできます。attributes
により、要素に関連したすべての属性のリスト(NamedNodeMap
)を得られます。IEはNamedNodeMap
のサポートが不完全なようなので注意が必要です。
属性を追加する場合は<要素>.setAttribute(<属性>, <値>);
、属性を削除する場合は<要素>.removeAttribute(<属性>)
。
スタイルシートの操作
<要素>.style.<スタイルプロパティ>
でアクセスする。スタイルプロパティについてはこちら。
DOMの構築やらJavaScriptが処理をするまでの流れやら
参考資料を紹介します。とてもおもしろいです。
【JavaScript基礎】JavaScriptの実行順序について
実際のところ「ブラウザを立ち上げてページが表示されるまで」には何が起きるのか
Life of a Pixel
仮想DOM
JavaScriptの仕様とは直接的に関係ないと思いますが、ついでに仮想DOMについて。仮想DOMを正確に理解するには調べていただくとして、ざっくり以下の様に認識しています。
イベント
画面のロードやクリックなど、ブラウザ上(というのが厳密に正しいかはおいといて)で起きることをイベントと呼びます。イベントが起きたことをトリガーとしてJavaScriptで処理を行います。指定できるイベントはこちら。
イベントハンドラ
以下の様な流れで実装、実行します。
- イベントハンドラと関連付ける処理をJavaScriptで定義する。
- イベントハンドラとその処理を関連付ける。
- イベントが起きた時、イベントハンドラが検知し、関連づけられた処理を実行する。
イベントハンドラの仕掛け方は2つあります。(似たような仕組みとしてイベントリスナがありますが後述します。)
HTMLタグにonイベント名
属性(イベントハンドラ)とJavaScriptのコードを関連付ける。
htmlhandler.html
<script type='text/javascript'>functionclickEvent(){alert('A handler is executed!');}</script><inputtype="button"value="Why don't you click?"onclick="clickEvent(); return false;">
HTML中にJavaScriptのコード(スクリプト)を混在させることをよしとしない考え方もあります。
HTMLタグにスクリプトを埋め込む場合は、デフォルトのスクリプト言語を宣言しておくのが望ましいとされていましたが、HTML5ではデフォルト扱いとなり省略されることになりました。
<meta http-equiv="Content-Script-Type" content="text/javascript">
(ページロード時等に)JavaScriptで登録する。
jshandler.html
<script type='text/javascript'>// onloadによりロード完了後に下記のコードが実行されるため、セレクタが空振らないwindow.onload=function(){// クリックイベントを検知するonclick(イベントハンドラ)と関数を関連付けるdocument.getElementById('btn').onclick=function(){alert('A handler is executed!');}}</script><inputtype="button"value="Why don't you click?"id="btn">
イベント発生時の標準の挙動
以下の様に予め挙動が決まっているものがあります。
- リンクがクリックされたらページを移動する。
- サブミットボタンがクリックされたらフォームを送信する。
- ページ上で右クリックされたらコンテキストメニューを表示する。
このような挙動を抑止したい時があります。抑止できます。
cancelevent.html
<!-- onclick="return false;"でクリックの標準挙動を抑止する --><inputtype="submit"value="Why don't you submit?"onclick="return false;">
イベントリスナの場合は後述します。
イベントリスナ
イベントハンドラとイベントリスナの違いがまだ腹落ちしきっていません3が、一つ分かっていることは、イベントハンドラに関連付けられる処理は1つですが、インベントリスナの場合、複数関連付けられます。
登録
IEの場合attachEvent
、それ以外の場合addEventListener
でイベントと処理を関連付けます。ここにもクロスブラウザの問題が横たわっています。attachEventについてはこちら。addEventListenerについてはこちら。
eventlistener.html
<!-- jshandler.htmlと見比べて見るとよいかも --><script type="text/javascript">// イベントリスナ登録のラッパー関数(クロスブラウザ対策)functionaddListener(element,event,listener){if(element.addEventListener){// IE以外element.addEventListener(event,listener,false);}elseif(element.attachEvent){// IEelement.attachEvent('on'+event,listener);}else{thrownewError('利用できるイベントリスナがありません。');}}// loadによりロード完了後に下記のコードが実行されるため、セレクタが空振らないaddListener(window,'load',function(){addListener(document.getElementById('btn'),'click',function(){alert('A listener is executed!');});});</script><inputtype="button"value="Why don't you click?"id="btn">
Eventオブジェクト
リスナはEventオブジェクトを受け取ります。このあたりもブラウザとバージョンによって仕様が異なるので注意が必要です。
eventlistener.html
addListener(window, 'load', function (e) {
console.log(Object.prototype.toString.call(e)); // [object Event]
});
e.target
とwindow.event.srcElement
、e.button
とwindow.event.button
の違いなど要チェックです。
削除
IEの場合detacheEvent
、それ以外の場合removeEventListener
でイベントと処理を関連付けを解除します。detacheEventについてはこちら。removeEventListenerについてはこちら。
バブリング
バブリングを正確に理解するには調べていただくとして、ざっくり「文書(Webページ)の下位の要素で発生したイベントは、上位の要素でも発生する。但し例外もある。」と認識しています。
bubbling.html
<script type="text/javascript">window.addEventListener('load',function(){document.getElementById('higher').addEventListener('click',function(e){window.alert("The higher listener is executed!");});document.getElementById('lower').addEventListener('click',function(e){window.alert("The lower listener is executed!");});});</script><divid="higher"style="width:100px; height:100px; border: solid 1px #000;"><divid="lower"style="width:50px; height:50px; border: solid 1px #f00;">
Why don't you click?
</div></div>
上位の要素でイベントが発生するのを抑止したいことがあります。抑止できます。IE以外の場合eventObject.stopPropagation()
、IEの場合window.event.cancelBubble=true
とします。
bubbling.html
document.getElementById('lower').addEventListener('click', function(e) {
window.alert("The lower listener is executed!");
e.stopPropagation(); // この行を追加
});
バブリングの参考資料がありました。
DOMイベントのキャプチャ/バブリングを整理する 〜 JSおくのほそ道 #017
イベント発生時の標準の挙動を抑止するには、イベントハンドラではreturn false;
しましたが、イベントリスナでは、IE以外の場合eventObject.preventDefault()
、IEの場合window.event.returnValue=false
とします。
非同期通信
Ajax
Ajaxによる非同期通信を正確に理解するには調べていただくとして、ざっくり「画面を丸々ロードするのではなく、一部を差し替える技術。HTTPリクエストの応答をXMLやJSON等の形式で受けてDOMに反映しページを更新する。」くらいに認識しています。資料は山ほどあると思いますので、軽く流します。
クロスブラウザ問題はここにもあり(と思ったけどさすがにもう気にしなくていいか)、Ajaxに使うオブジェクトが何種類かあります。
オブジェクト | ブラウザ |
---|
XMLHttpRequest | IE 7.0以降及びその他のブラウザ |
ActiveXObject('Msxml2.XMLHTTP') | IE 6.0 |
ActiveXObject('Microsoft.XMLHTTP') | IE 5.5以前 |
XMLHttpRequestの使い方はこちら。メンバをいくつかメモ。
メンバ | メモ |
---|
responseText | 応答をテキストやJSON4で受け取る |
responseXML | 応答をDocumentオブジェクトで受け取る。前述の通り操作できる。 |
readyState | 0~4(4が完了) |
status | HTTPステータスコード(200がOK) |
open(method, url, async, user, password) | async=falseの時、同期通信を行うためサーバーが応答を返すまで後続の処理を行わない。URLは相対パスで指定する。 |
setRequestHeader(header, value) | HTTPリクエストヘッダを設定する。 |
send(body) | リクエストを送信する。 |
セキュリティのためクロスドメインのAjax通信は禁じられています。(サーバーサイドで別ドメインから取得した情報をフロントエンドに渡すことは可能。)かつてJSONPという技術でクロスドメイン通信するテクニックがありましたが、役割を終えた感があります。CORSの実装が推奨されています。
Fetch API
Fetch API を利用すると、リクエストやレスポンスといった HTTP のパイプラインを構成する要素を操作できるようになります。また fetch() メソッドを利用することで、非同期のネットワーク通信を簡単にわかりやすく記述できるようになります。
従来、このような機能は XMLHttpRequest を使用して実現されてきました。 Fetch はそれのより良い代替となるもので、サービスワーカーのような他の技術から簡単に利用することができます。 Fetch は CORS や HTTP 拡張のような HTTP に関連する概念をまとめて定義する場所でもあります。
MDN
ECMAScriptの標準ではなく、Fetch Living Standardに準拠しています。後述するPromiseを理解しておく必要があります。
fetchメソッドの使い方はこちら。Fetchがサポートされていないブラウザ向けにFetch Polyfillが利用できます。Polyfillについては後述します。
非同期処理
非同期を正確に理解するには調べていただくとして、ざっくり「複数の処理を想定した時、非同期処理とは、一方の処理を実行している最中に他方の処理を同時に行うこと。ちなみに同期処理の場合、一方の処理を実行が完了するまで他方の処理を行わない。」くらいの認識です。
JavaScriptではかつて非同期処理を実現するのにsetTimeout
やsetInterval
を利用していましたが、課題がありました。新しい仕組みが導入され、徐々に改善されてきています。
参考資料:
JavaScriptとコールバック地獄
JavaScriptと非同期のエラー処理
Promise
ES2015で標準ビルトインオブジェクトに追加されました。非同期処理を実現します。使い方はこちら。
promise.js
functionasynchronous(testCase){returnnewPromise(function(resolve,reject){setTimeout(function(){if(testCase==='normal'){// 正常系はresolveを呼び出し、結果をthenに渡す。resolve('Normal');}else{// 異常系はrejectを呼び出し、結果をcatchに渡す。reject('ABEND');}},1000);})}console.log('Before asynchronous()');// ①// asynchronous('abnormal') // 異常系asynchronous('normal')// 正常系 // ②.then(function(ret_resolve){console.log(ret_resolve);}).catch(function(ret_reject){console.log(ret_reject);}).finally(function(){console.log('Finish');});console.log('After asynchronous()');// ③
1秒待つため、①、③、②の順に実行されます。Promiseはresolve
関数とreject
関数を受け取ります。resolve
の結果はthen
が受け取り、reject
の結果はerror
が受け取ります。どちらに転んでもfinally
は実行されます。
下記のコードでも同じ結果が得られます。理由はこちら。
promise.js
asynchronous('normal').then(function(ret_resolve){console.log(ret_resolve);},function(ret_reject){console.log(ret_reject);})// .catch()がない.finally(function(){console.log('Finish');});
Promise.prototype.then()
もPromise.prototype.catch()
もPromiseを返すので、連鎖させられます。(then()
やcatch()
を連ねて書ける。)
サンプルコードをもう一つ。
promiseall.js
functionasynchronous(canary){returnnewPromise((resolve)=>{setTimeout(()=>{resolve(canary);},1000*Math.random());})}Promise.all([// 前のasynchronous()の完了を待たずして、後のasynchronous()が呼び出される。asynchronous('1'),asynchronous('2'),asynchronous('3'),])// all()では全てのasynchronous()が完了すると呼び出される。// race()の場合、全完了を待たず、どれか一つが完了した時点でthenなりcatchなりを実行する。.then(ret_resolve=>{// asynchronous()の完了順にかかわらず配列の順番は一定(呼び出し順)。console.log(ret_resolve);// [ "1", "2", "3" ]});
async/await
ES2017で追加されました。非同期処理を実現します。使い方はこちらとこちら。サンプルコードを一つ。
asyncawait.js
functionasynchronous(testCase){returnnewPromise((resolve,reject)=>{setTimeout(()=>{if(testCase==='normal'){resolve('血祭りだ');}else{reject('来ませんでした');}},1000);})}asyncfunctionseries(){// 非同期処理の完了を待つ。その一方で呼び出し元に処理を戻す。result=awaitasynchronous('normal');// 正常系// result = await asynchronous('abnormal'); // 異常系// awaitの完了後に処理する。console.log('しかも7日で');// ①returnresult;}series()// ②.then(ret_resolve=>{console.log(ret_resolve);}).catch(ret_reject=>{// 異常系の場合、awaitの処理を中断してここに飛んでくる。console.log(ret_reject);});// series()よりも後ろにあるけど、seriesがasyncだから並列実行される。// seriesは1秒かかるので、こちらの結果が先に出力される。console.log('ほ…本当に 来てくれたのか');// ③
正常系の場合、③、①、②の順に実行されます。異常系の場合、③、②の順に実行されます。
Promiseやasync/awaitが登場する前jQuery.Deferred()
がよく利用されていたようですが、生のJavaScriptで使える非同期処理を使いこなしたいですね。
参考資料:async/await 入門(JavaScript)
クロスブラウザ問題
ブラウザ間やバージョン間の実装の違いによるJavaScriptの解釈の違いが開発者を悩ませます。
ES2015以降の様式で書かれたコードを古いJavaScriptの様式に変換するコンパイラ(トラスコンパイラ、トランスパイラなどと呼ばれる)にBabelというものがあります。例えば、IEをサポートしなければならないけれども、ES2015の機能を使いたい等の場面で利用します。
Babelが変換できないものもあるので、Polyfillというものも利用します。使い方は@babel/polyfill/core-jsやPolyfill.ioをご参照のこと。
ES2015(ES6)以降に追加された機能
まとめがありました。
ECMAScript6の新機能
ES2016 / ES2017の最新動向を追ってみた
ES2015・ES2016・ES2017・ES2018を手早く学習するTHE ES Guide
さいごに
JavaScriptの学習を楽にするために、散在する情報の整理を試みてみました。何かの役に立てば幸いです。有益な情報を公開して下さった先人たちに敬意と感謝の気持ちでいっぱいです。ありがとうございます。