Help us understand the problem. What is going on with this article?

Angular, React, Vueを始める前に理解しておきたいJavaScriptの基礎

高機能なフレームワークが隆盛を誇る昨今、基礎固めが大事だと思うのです。

いつ書いたのか

2020年1月頃です。

対象読者

  • JavaScriptを触ったことがあるが、言語仕様は曖昧にしている。
  • JavaScriptになんとなく苦手意識がある。

本稿でやること、やらないこと

  • 言語仕様の特徴的なところ(私見)をまとめます。
  • ベストプラクティスが変化してきたところをまとめます。
  • 言語仕様を網羅はしません。

JavaScriptの今に追いつくまでの流れ

  1. 歴史を知る。
    • https://ja.wikipedia.org/wiki/ECMAScript
    • Edition1から始まり、2019年6月時点で10が公開されている。
    • Edition5までが旧時代、6以降が新時代みたいな雰囲気がある。
    • EditionのことをECMAScript○とかES○のように呼称する。例えば、Edition5はECMAScript5。
    • ECMAScript6(Edition6、ES6も同義)以降は年号で呼称されもする。例えばES6はES2015。
  2. 言語仕様の特徴を知る。(後述)
  3. 最新版の仕様を追いかける。(これを無限ループ)
    • 標準規格が毎年公開される。その後、ブラウザの仕様に反映されてゆく。(タイムラグがある)
    • 2019年版
  4. ブラウザの対応状況を追いかける。(これも無限ループ)

言語仕様は進化し続けています。ネット上には有用な情報が溢れていますが、時系列に整理されているわけではなく、陳腐化したものも溢れています。ですから、私(たち)は混乱しますし、時に間違いも冒します。そこで、まずは歴史を振り返ります。

本稿では参考資料をいくつか紹介していますが、それらが書かれた時期に注意する必要があります。現在にも当てはまる話と、そうでないものを識別しながら読み進める必要があります。

整理しておきたいトピック

  • データの型
  • 変数のスコープ
  • Hoisting(巻き上げ)
  • Namespace(名前空間)
  • 関数
  • モジュール
  • JavaScript風オブジェクトとオブジェクト指向
  • DOM
  • イベント
  • 非同期通信
  • 非同期処理
  • クロスブラウザ問題

環境構築が面倒な人へ

https://codepen.io/

ウォーミングアップ

JavaScriptのコードをHTMLに埋め込む方法

  1. <script></script>
    1. HTMLファイル中にコードを書く場合
      <script type="text/javascript">コード</script>
    2. 外部ファイルを読み込む場合
      <script src="path/to/sample.js"></script>
  2. <a href=”JavaScript:コード”> テキスト</a>
  3. イベントハンドラ
    <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>を埋め込む位置

  1. <body>の中
    スクリプトの読み込みや実行が完了するまで描画が行われないので</body>の直前に埋め込むのが吉。
    注意事項:ここで使う関数は、ここより以前の<script>で定義されていなければなりません。
  2. <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
const bros = ['ラオウ', 'トキ', 'ケンシロウ'];
bros.whoAmI = 'ジャギ';

for (const b of bros) {
  console.log(b);
}
// ラオウ
// トキ
// ケンシロウ

for...ofはプロパティの値に対して処理を行う一方、for...inはプロパティ名に対して処理を行います。また、for...ofを相手にするのに対して、for...in参照も相手にします。

forin.js
for (const b in bros) {
  console.log(b);
}
// 0
// 1
// 2
// whoAmI

ループからの脱出にはbreaklabelを使います。

break.js
for () {
  break;
}
label.js
label:
for () {
  for () {
    break label;
  }
}

配列

以下のコードは等価です。

array.js
const friends = ['ゴン', 'キルア'];
const friends = new Array('ゴン', 'キルア');

連想配列については後述します。

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等とは違い、変数の宣言時に型を明示しない。
    • const, let, var のいずれかと共に変数(定数)を宣言する。(後述)
  • 型変換に要注意。
    参考資料:型変換のいろいろ
  • 演算にまつわる型変換(キャスト)
    • +-の違いに注目。
    • ==オペランドの型が異なる場合、キャスト後に評価する。===はキャストせず評価する。
typecasting.js
// string + numberの場合、numberをstringにキャストして文字列連結
const stringPlusNumber = "abc" + 1;
console.log(stringPlusNumber);  // abc1

// string - numberの場合、stringをnumberにキャストして減算(stringが非数字)
const stringMinusNumber = "abc" - 1;
console.log(stringMinusNumber);  // NaN

// string - numberの場合、stringをnumberにキャストして減算(stringが数字)
const numeralMinusNumber = "123" - 1;
console.log(numeralMinusNumber);  // 122

// object + numberの場合、objectとnumberをstringにキャストして文字列連結(toStringなし)
const Person = function() {
  this.name = 'Thom Browne',
  this.greeting = function() {
    console.log('Hi! I\'m ' + this.name + '.');
  }
};
const person = new Person();

let objectPlusNumber = person + 1;
console.log(objectPlusNumber);  // [object Object]1

// object + numberの場合、objectとnumberをstringにキャストして文字列連結(toStringあり)
Person.prototype.toString = function personToString() {
  return this.name;
}
objectPlusNumber = person + 1;
console.log(objectPlusNumber);  // Thom Browne1

型を判定する方法がいくつかあります。
参考資料:JavaScriptの「型」の判定について

Symbol型は、ぱっとみ意味が分からないですね。参考資料があります。
ECMAScript6にシンボルができた理由

変数のスコープ

JavaScriptにはかつて、2つのスコープがありました。

  • 関数内でのみアクセス可能な、ローカルスコープ。
  • どこからでもアクセス可能な、グローバルスコープ。

ES2015で1つ追加されました。

  • ifforなどの{}の内側でのみアクセス可能な、ブロックスコープ。
    (ローカルスコープを関数スコープとブロックスコープに分て考えるようになったっぽい。)

(ざっくりとした)スコープの決定メカニズム

  • const, let, var を付けずに宣言した変数はグローバルスコープ。
  • var を付けた場合、宣言した場所による。
    • 関数内で宣言した場合、関数スコープ。
    • そうでない場合、グローバルスコープ。
  • const, let を付けた場合、宣言した場所がスコープになる。
  • 関数の(値渡しした)引数は関数スコープ。
scope.js
const c = "c-global";
let l = "l-global";
var v = "v-global";

function scope() {
  const c = "c-function";
  let l = "l-function";
  var v = "v-function";

  console.log(c);   // c-function
  console.log(l);   // l-function
  console.log(v);   // v-function

  if (true) {
    const c = "c-block";
    let l = "l-block";
    var v = "v-block";

    console.log(c);  // c-block
    console.log(l);  // l-block
    console.log(v);  // v-block
  }

  console.log(c);    // c-function
  console.log(l);    // l-function
  console.log(v);    // v-block  <-- ココに注目
}
scope();

console.log(c);      // c-global
console.log(l);      // l-global
console.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
var scope = 'g';

function localScope() {
    // 暗黙的にvar scope;が宣言されたと解釈される。
    // scopeにはまだ値が入っていないためundefinedとなる。
    console.log(scope);     // undefined
    var scope = 'l';
    console.log(scope);     // l
    return scope;
}

console.log(localScope());  // l
console.log(scope);         // g
  • 関数の巻き上げ

    <script>は何度でも埋め込むことができますが、同一スコープ中で関数を定義、使用する場合、どちらを先に書いても構いません。巻き上げられるため。しかし、定義する<script>と使用する<script>が異る場合、先に定義しておく必要があります。

  • 関数式の巻き上げ

    巻き上げません。定義する前に関数式を使用できません。変数と考えれば自然な挙動ですね。

Namespace(名前空間)

JavaScript でネームスペースを作成する考え方はシンプルです。グローバルオブジェクトをひとつ作成して、すべての変数、メソッド、関数をそのオブジェクトのプロパティとすればよいのです。ネームスペースを使用すると、アプリケーション内で名前が衝突する可能性が低下します。これは各アプリケーションのオブジェクトが、アプリケーションで定義したグローバルオブジェクトのプロパティとなるからです。
MDN

スコープや名前空間に付随するあれこれ

JavaScriptには元々スコープが2つしかなく、モジュール(後述)にも非対応だった等の理由から、変数の汚染(意図せずに変数を上書きしてしまう事故)を防ぐためのテクニックが活用されていました。読みやすく、保守しやすいコードを書くために、変数を多用しすぎないのが吉という考え方がありますが、JavaScriptは比較的変数の汚染が起きやすいという性質も相俟って、このようなテクニックが発達したように見えます。

即時関数

即時関数という仕組みがありましたが、役割を終えた感があります現時点の言語仕様で、もっといい書き方ができないか吟味するのが良さそうです。

参考資料:
JavaScriptで即時関数を使う理由
JavaScriptから即時関数を排除する

closure(クロージャ)

まずは見た目に慣れるところから始めたいと思います。

iamclosure.js
function iAmClosure() {
  let val;        // ローカル変数
  const func() {  // 関数の中で関数を宣言する
    // ローカル変数valを参照する処理
  }
}

使ってみます。

iamclosuretoo.js
function iAmClosureToo(init) {
  let count = init;
  return function() {  // 関数を返すと、呼び出し元のプログラムで繰り返し呼び出せる
    return count++;
  }
}
const counter = iAmClosureToo(1);  // counterにはcountを参照する匿名関数がセットされる
console.log(counter());            // 1
console.log(counter());            // 2

クロージャを理解するには、まずレキシカルスコープ(静的スコープ)とダイナミックスコープを理解する必要がありそうです。

レキシカルスコープは関数を定義した時点でスコープが決まり、ダイナミックスコープは関数を実行した時点でスコープが決まるものです。どちらになるかは言語次第のようで、JavaScriptは前者です。

lexical.js
const val  = 1;

function sub(){
  console.log(val);  // sub()の定義時点でvalは1
}

function main(){
  const val = 2;
  sub();             // sub()の実行時点でvalは2
}

main();  // 1
sub();   // 1

MDNの例で、何が特徴的なのかを確認します。

他の言語に慣れている人は"関数内部のローカル変数はその関数が実行されている間だけ存在する"と考えることに慣れているかもしれません。つまり、var myFunc = makeFunc();の実行時点でローカル変数nameが消滅するように直感し、結果このコードは動かないと考えられます。しかし、JavaScriptでは、このコードが動きます。JavaScriptでは、displayName()の定義時点でスコープが決まり、nameにはMozillaが設定されるものと解釈します。クロージャでは、この時の環境への参照を保持しており、myFuncを実行した時に結果を返すことができます。

closure.js
function makeFunc() {
  var name = "Mozilla";

  function displayName() {
    console.log(name);
  }
  return displayName;
}

var myFunc = 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つあります。

  1. 関数宣言
    • function functionName () {}
    • コードを解析、コンパイルするタイミングで関数を登録するため、同一の<script>内で、定義する前に呼び出しのコードがあっても正しく処理される。
  2. 関数式(匿名関数、無名関数)
    • const functionName = function(arg) {}; functionName(1);
    • この場合、関数は変数なので、呼び出しよりも先に定義する必要がある。
    • 変数を節約できる。
    • 再利用しない場合(高階関数の引数にする場合やコールバック関数等)に重宝する。
  3. Functionコンストラクタ
    • スコープの解釈が直感と一致しない上、セキュリティとパフォーマンスの問題もある。
    • この方法で定義するメリットは基本的にないらしい。
    • 但し、Object.prototype.constructorの戻り値として内部的に使われていたりもするため、(開発者が)使わないなら要らないじゃんってことにはならない。

そして、ES2015でアロー関数が導入されました。アロー関数は、関数式の短い代替構文で、2つの理由から導入されました。

  1. 簡潔に書ける。
  2. thisを束縛しない。

書き方はこんな感じ。

  • function(x) {}(x) => {}になる。
  • 1行で書ける時は{}とreturnを省略できる。
  • 引数が1つの場合()を省略できる。 function(x) {}x => {}になる。
  • 引数がない場合、()を必ず書く。 function() {}() => {}になる。

"thisを束縛しない"の意味がぱっとみ分からないと思います。参考資料があります。コメント欄にも注目です。
JavaScript の this を理解する多分一番分かりやすい説明
【JavaScript】アロー関数式を学ぶついでにthisも復習する話

引数

デフォルト引数

ES2015で、デフォルト引数の指定が可能となりました。それ以前は、argumentsオブジェクトを利用したテクニックを使って実装していました。デフォルト引数には以下のようなルールがあります。

  • デフォルト引数は、引数を指定しない時かundefinedを明示的に指定した時に適用される。
  • nullやfalseを指定した場合には適用されない。
  • デフォルト値には(先に宣言された)他の引数、関数の結果も指定できる。

使い方はこちら

必須パラメータを指定するのにも利用できます。

required.js
function required() {
  throw new Error('Argument is required');
}

function isString(value = required()) {
  return typeof value == "string" || value instanceof String;
}

console.log(isString("abc"));  // true
console.log(isString(123));    // false
console.log(isString());       // Argument is required

Rest parameters(可変長引数と訳す?)

従来はargumentsオブジェクトを活用して実装していましたが、ES2015でRest parametersが使えるようになりました。ちなみにargumentsとの違いがいくつかあります。詳細はこちら

restparams.js
function restparams(...args) {
  console.log(args instanceof Array);   // true
  return args.length;
}

console.log(restparams());              // 0
console.log(restparams("one"));         // 1
console.log(restparams("one", "two"));  // 2

名前付き引数(言語仕様ではなく、テクニックの話)

引数に名前を付けられると、以下のようなメリットが考えられます。

  • コードの可読性が上がる。
  • 引数の順番を変えられる。
  • 引数の省略を柔軟にできる。
namedarguments.js
// function bmi(weight_kg, height_m) {...}
function bmi(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
const aArray = { x:1, y:2 };

console.log(typeof aArray);            // object
console.log(aArray instanceof Array);  // false
console.log(aArray['x']);  // 1
console.log(aArray.x);     // 1

連想配列に限らず様々なものがオブジェクトとして表現されます。標準ビルトインオブジェクトを参照のこと。標準ビルトインオブジェクトの他にもブラウザの操作に使うオブジェクト、DOMの操作に使うオブジェクト、Ajaxに使うオブジェクト等があります。

標準ビルトインオブジェクト

適当に例を列挙します。

Arrayオブジェクト

使い方はこちら。配列の操作(ソート等)に重宝する。

Dateオブジェクト

date.js
let today = new Date();
let birthday = new Date('1995/12/17 03:24:00');
let birthday = new Date('December 17, 1995 03:24:00');
let birthday = new Date('1995-12-17T03:24:00');
let birthday = new Date(1995, 11, 17);
let birthday = new Date(1995, 11, 17, 3, 24, 0);

日付、時刻を加減算するメソッドはなく、getXxxx(), setXxxx()で実現します。期間の差は以下の様にして求めます。

tmn.js
const start = new Date(1984, 4, 21);
const end = new Date(1994, 4, 21);
(end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);

上述した通りJavaScriptにはdate型はありません。使い方はこちら。罠にご注意下さい。

参考資料:
JavaScript の Date は罠が多すぎる
【JavaScript】日付処理

Numberオブジェクト

number.js
const num = 123;
const numo = new Number(123);

console.log(num);                     // 123
console.log(numo);                    // 123
console.log(typeof num);              // number
console.log(typeof numo);             // object
console.log(numo instanceof Number);  // true

使い方はこちら。ちなみに、NaNは、NaNを含む全ての値と等しくない。NaNを検出するにはisNaN関数を使用します。

isnun.js
console.log(Number.NaN == Number.NaN);  // false
console.log(isNaN(Number.NaN));         // true

Stringオブジェクト

string.js
const str = '文字列';
const stro = new String('文字列');

console.log(str);                     // 文字列
console.log(stro);                    // 文字列
console.log(typeof str);              // string
console.log(typeof stro);             // object
console.log(stro instanceof String);  // true

使い方はこちら。文字列の操作(部分抽出等)に重宝します。

manipulateString.js
console.log('Qiita'.split(''));           // ["Q", "i", "i", "t", "a"]
console.log('Qiita'.split('').join(''));  // "Qiita"

const title = new String('Qiita');
console.log(title.split(''));           // ["Q", "i", "i", "t", "a"]
console.log(title.split('').join(''));  // "Qiita"

Stringは配列のように扱えますが配列ではありません(例えばmap()やsplice()は使えない)。

charAt.js
console.log('Qiita'.charAt(0));  // "Q"

RegExpオブジェクト

正規表現を司ります。ざっくりとした使い方は以下のような感じ。詳しくはこちら。Editionが上るにつれて進化しているので、最新仕様をご確認のこと。

re.js
const re = new RegExp('正規表現', 'オプション');
const re = /正規表現/オプション;

argumentsオブジェクト

関数の引数の情報を管理する配列ライクなオブジェクト1。引数のチェックや可変長引数(?)の実装に利用されていた部分は、新たに導入されたデフォルト引数やRest parametersにより代替できると考えられます。

使い方はこちら

再帰関数を作る時にcalleeプロパティが役に立ちます。

factorial.js
function factorial(n) {
  if (n != 0) {
    // calleeの代わりに関数名を直書きした場合、関数名を変更した時に修正が必要。
    // また、匿名関数の場合は、関数名がないのでcallee一択。
    return n * arguments.callee(n - 1);
  }
  return 1;
}

console.log(factorial(3))  // 6

Objectオブジェクト

すべてのオブジェクトの元になっているオブジェクトで、すべてのオブジェクトはObject.prototypeからメソッドとプロパティを継承しています。使い方はこちら

ところで、toString()は指定したオブジェクトを表す文字列を返します。toString()は人知れず内部的に使われていることがあります。暗黙の型変換の如く、オブジェクトが文字列に変換されていたりします。

object.js
object = new Object();
date = new Date();
array = new Array([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
const obj = new Object();
// オブジェクトを宣言した後に外からプロパティを設定できる。
obj.name = 'anonymous';
obj.role = 'anything';

console.log(obj.toString());  // [object Object]
console.log(obj.name);        // anonymous
console.log(obj.role);        // anything

// const obj = { name:'anonymous', role:'anything' }; でも同じ結果が得られる。

ES2015で追加されたオブジェクト

ProxyReflectMapSetWeakMapWeakSetPromise(後述)など。

Global object(単数形) と Global objects(複数形)

日本語訳注: 英語版では、JavaScript 実行時に作成される、グローバルスコープを表す唯一となるグローバルオブジェクトを単数形の global object とし、そのグローバルオブジェクトが持つ「グローバルにアクセスできる変数、関数などのオブジェクトの一群」のプロパティを複数形の global objects として表しています。日本語の文法は単数形、複数形を持たないため、上記では単数形の方に『』を付けてそれぞれを区別しています。
MDN

Global objectは他のオブジェクトのようにインスタンス化するものではなく、グローバル関数等を管理するための領域と考えられます。グローバルプロパティや関数の使い方はこちら

JavaScriptではところどころで暗黙的に型変換が行われますが、明示的に行うこともできます。バグを出さないために、関数の癖をつかんでおく必要があります。

typecast.js
console.log(Number('123abc'));      // NaN
console.log(parseFloat('123abc'));  // 123
console.log(parseInt('123abc'));    // 123

console.log(Number('abc123'));      // NaN
console.log(parseFloat('abc123'));  // NaN
console.log(parseInt('abc123'));    // NaN

console.log(Number(new Date(1984,4,21)));      // 453913200000
console.log(parseFloat(new Date(1984,4,21)));  // NaN
console.log(parseInt(new Date(1984,4,21)));    // NaN

console.log(Number('0o123'));       // 83
console.log(parseFloat('0o123'));   // 0
console.log(parseInt('0o123'));     // 0

console.log(Number('314e-2'));      // 3.14
console.log(parseFloat('314e-2'));  // 3.14
console.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');        // OK
window.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">
  function accessToForm() {
    // 以下は全て等価
    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);

    const sT = 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>

<form name="formName" onsubmit="accessToForm(); return false;">
  <input type="text" name="textName">
</form>
  • HTMLFormElementを使って<form>を操作する。
    • document.formNameで取得した<form>の属性にdocument.formName.<属性名>でアクセスすることもできる。
  • HTMLInputElementオブジェクトを使って<input>を操作する。
  • ラジオボタンやチェックボックスの場合はNodeListを継承しているRadioNodeListで操作する。
  • <select>は、HTMLSelectElementで操作する。

プロトタイプベースなオブジェクト指向

  • JavaScriptにはクラスという概念はなく、プロトタイプという概念がある。
  • プロトタイプオブジェクトというものがあり、すべてのオブジェクトはプロトタイプオブジェクトのメソッドとプロパティを継承する。
  • newすることを想定した関数オブジェクトのことをコンストラクタと呼ぶ。
    • コンストラクタは戻り値を返してはならない。
  • オブジェクトに対して外部からメンバを追加できる。
    • その結果、同一のコンストラクタをもとに生成されたインスタンスであっても、それぞれが持つメンバが異なる場合がある。
  • JavaScriptにおけるクラスベース風なオブジェクト指向
    ES2015でクラスベースなオブジェクト指向の書式が導入されたが、これはシンタックスシュガー(糖衣構文)で、JavaScriptのオブジェクト指向は引き続きプロトタイプベース。

ちなみに、厳密には"メソッド"という言い方はふさわしくないかもしれませんが、本稿では便宜的にそう呼びます。

JavaScript には、クラスベースの言語が定義する形式の「メソッド」はありません。 JavaScript ではどの関数も、オブジェクトのプロパティという形で追加することができます。
MDN

constructor.js
// コンストラクタの定義
const Person = function(name) {
  // thisキーワードを用いてメンバを定義する
  this.name = name;

  // メソッドの定義
  this.introduce = function () {
    return 'I am ' + this.name + '.';
  }
};

const person = new Person('Michael');

console.log(typeof person);             // object
console.log(person instanceof Person);  // true
console.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() {
    return this.name.slice(0, 1);
  }
};

const person = new Person('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
const Person = function(name) {
  this.name = name;

  this.introduce = function () {
    return 'I am ' + this.name + '.';
  }
};

const person = new Person('Michael');

console.log(typeof person);             // object
console.log(person instanceof Person);  // true
console.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.

const another_person = new Person('Mary');
another_person.nickname = 'May';

console.log(another_person.callme());   // Please call me May.

もう一つ注意事項があります。

overrideprototype.js
const Moten = function() {};
const Kanki = function() {};
const Person = function() {};

Moten.prototype = {
  strategize : function() { return '全軍撤退します。'; }
};

Kanki.prototype = {
  strategize : function() { return '鄴をぶんどります。'; }
};

// prototypeにオブジェクトへの参照を設定
Person.prototype = new Moten();

const person = new Person();

// Personをnewした後に別のオブジェクトを設定すると...
Person.prototype = new Kanki();

// prototypeが上書きされていない
console.log(person.strategize());  // 全軍撤退します。

静的メソッド

static.js
const Person = function() {};

Person.bePeaceful = function() {
  return 'We are the one.';
}

// Personをnewせずにメソッドを呼び出す
console.log(Person.bePeaceful());  // We are the one.

// インスタンスから呼び出そうとするとエラーになる
const person = new Person();
console.log(person.bePeaceful());  // TypeError: person.bePeaceful is not a function

静的メンバはprototypeを参照しません。

クラスベース風なオブジェクト指向の静的メソッドはこちら

プライベートメンバ

JavaScriptにはアクセス修飾子(public、protected、private)がありません。言語仕様として、プライベートメンバを定義することができません。しかし、それっぽくするテクニックはあります。例えば、クロージャを利用したやり方があります。その他にも様々な方法が提案されています。

クロージャ派、Symbol型派、WeakMap派と、運用でなんとかする(privateメンバに名前の頭に_を付ける)派をよく見かけます。

オブジェクトのコピー

copy.js
const person = {
  name: 'Mike'
};

const clonePerson = {
  ...person
};

person.name = 'Jane';

console.log(person);
// Object {
//   name: "Jane"
// }

console.log(clonePerson);
// Object {
//   name: "Mike"
// }

失敗例はこちら。

reference.js
const person = {
  name: 'Mike'
};

// clonePersonにはpersonへのポインタが格納される
const clonePerson = person;

person.name = 'Jane';

console.log(person);
// Object {
//   name: "Jane"
// }

console.log(clonePerson);
// Object {
//   name: "Jane"  <-- ココに注目
// }

クラスベースなオブジェクト指向

ES2015でクラスベースなオブジェクト指向の書式が導入されました。しかし、これはシンタックスシュガー(糖衣構文)です。JavaScriptのオブジェクト指向は引き続きプロトタイプベースです。

書き方はこんな感じです。クラスの巻き上げはないの定義に呼び出すとエラーになります。

classbaseclass.js
class Person {
  constructor(name) {
    this.name = name;
  }

  introduce() {
    return `I am ${this.name}.`;
  }
}
const person = new Person('Michael');
console.log(person.introduce());

匿名クラスが宣言できます。

anonymousclass.js
const Person = class {
  // 同上
}

プロパティを単独で宣言することはできず、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">
  function accessToForm() {
    const form = document.getElementsByTagName('form');
    const input = document.getElementsByTagName('input');

    console.log(form[0]);      // <form name="form" onsubmit="accessToForm(); return false;">
                               // <input name="name" type="text">
                               // <input name="address" type="text">
                               // <input value="submit" type="submit">
                               // </form>
    console.log(form[0][0]);   // <input name="name" type="text">
    console.log(form[0][1]);   // <input name="address" type="text">
    console.log(form[1]);      // undefined
    console.log(input[0]);     // <input name="name" type="text">
    console.log(input[1]);     // <input name="address" type="text">
    console.log(input[3]);     // undefined
  }
</script>
<form name="form" onsubmit="accessToForm(); return false;">
  <input type="text" name="name">
  <input type="text" name="address">
  <input type="submit" value="submit">
</form>
walking.html
<script type="text/javascript">
  function accessToForm() {
    const select = document.getElementById('finish');
    const options = select.childNodes;

    for(let i = 0; i < options.length; i++) {
      const option = options.item(i);
      if(option.nodeType == 1) {
        console.log(option.value + ' ' + option.textContent);
        // right ひ…ひと思いに右で…やってくれ
        // left ひ…左?
        // both もしかしてオラオラですかーッ!?
      }
    }
  }
</script>
<form name="form" onsubmit="accessToForm(); return false;">
  <select id="finish">
    <option value="right">ひ…ひと思いに右で…やってくれ</option>
    <option value="left">ひ…左?</option>
    <option value="both">もしかしてオラオラですかーッ!?</option>
  </select>
  <input type="submit" value="やれやれだぜ">
</form>

指定したノードが存在しない場合undefinedを出力しています。存在する場合、ノードに応じたオブジェクトを返します。例えば、forminputの場合HTMLCollection<select>の場合HTMLSelectElementoptionの場合NodeList。オブジェクトが持つメンバを使って操作できます。

ノードを追加するには以下の様にします。詳細はDOMの仕様書等をご参照のこと。

addnode.html
<script type="text/javascript">
  function add(form) {
    const graffiti = form.graffiti.value;
    const note = document.getElementById('note');

    // 要素を作成する
    const button = document.createElement('button');

    // 属性を作成する
    const value = document.createAttribute('value');
    value.nodeValue = graffiti;

    // 要素に属性、テキストを関連付ける
    button.setAttributeNode(value);
    button.textContent = graffiti;

    // ページに要素を関連付ける
    note.appendChild(button);

    // テキストノードを作成する
    const text = document.createTextNode(graffiti);
    // ページにノードを関連付ける
    note.insertBefore(text, button);
  }
</script>

<form>
  <input type="text" name="graffiti">
  <input type="submit" value="ノードを追加する" onclick="add(this.form); return false;">
</form>
<div id="note"></div>

ノードを書き換える場合は<親ノード>.replaceChild(<置換後のノード>, <置換対象のノード>)、削除する場合は <親ノード>.removeChild(<削除対象ノード>)

属性の操作

要素中の属性にアクセスしたい場合、上記の様にして要素を取得してから、<要素>.<属性>のようにしてアクセスできる属性が多いですが、classclassNameでアクセスするなど、変則的なものもあります。

あるいは、Elementのメンバを利用して、<要素>.getAttribute(<属性>)<要素>.setAttribute(<属性>, <値>)のようにして操作することもできます。attributesにより、要素に関連したすべての属性のリスト(NamedNodeMap)を得られます。IEはNamedNodeMapのサポートが不完全なようなので注意が必要です。

属性を追加する場合は<要素>.setAttribute(<属性>, <値>);、属性を削除する場合は<要素>.removeAttribute(<属性>)

スタイルシートの操作

<要素>.style.<スタイルプロパティ>でアクセスする。スタイルプロパティについてはこちら

DOMの構築やらJavaScriptが処理をするまでの流れやら

参考資料を紹介します。とてもおもしろいです。

【JavaScript基礎】JavaScriptの実行順序について
実際のところ「ブラウザを立ち上げてページが表示されるまで」には何が起きるのか
Life of a Pixel

仮想DOM

JavaScriptの仕様とは直接的に関係ないと思いますが、ついでに仮想DOMについて。仮想DOMを正確に理解するには調べていただくとして、ざっくり以下の様に認識しています。

  • なにがうれしいのか?
    高速な処理が可能。従来はリクエスト毎にDOMを0から作っていた。仮想DOMがある世界では、古い仮想DOMと新しい仮想DOMの差分をDOMに反映するだけで済む。
    (数年前から優位性が薄れている説も見かける。)

  • どうやって使うのか?
    仮想DOMをサポートしたフレームワークを利用する。ReactとかVueとか。

イベント

画面のロードやクリックなど、ブラウザ上(というのが厳密に正しいかはおいといて)で起きることをイベントと呼びます。イベントが起きたことをトリガーとしてJavaScriptで処理を行います。指定できるイベントはこちら

イベントハンドラ

以下の様な流れで実装、実行します。

  1. イベントハンドラと関連付ける処理をJavaScriptで定義する。
  2. イベントハンドラとその処理を関連付ける。
  3. イベントが起きた時、イベントハンドラが検知し、関連づけられた処理を実行する。

イベントハンドラの仕掛け方は2つあります。(似たような仕組みとしてイベントリスナがありますが後述します。)

  1. HTMLタグにonイベント名属性(イベントハンドラ)とJavaScriptのコードを関連付ける

    htmlhandler.html
    <script type='text/javascript'>
      function clickEvent() {
        alert('A handler is executed!');
      }
    </script>
    
    <input type="button" value="Why don't you click?" onclick="clickEvent(); return false;">
    

    HTML中にJavaScriptのコード(スクリプト)を混在させることをよしとしない考え方もあります。
    HTMLタグにスクリプトを埋め込む場合は、デフォルトのスクリプト言語を宣言しておくのが望ましいとされていましたが、HTML5ではデフォルト扱いとなり省略されることになりました。
    <meta http-equiv="Content-Script-Type" content="text/javascript">

  2. (ページロード時等に)JavaScriptで登録する。

    jshandler.html
    <script type='text/javascript'>
      // onloadによりロード完了後に下記のコードが実行されるため、セレクタが空振らない
      window.onload = function() {
        // クリックイベントを検知するonclick(イベントハンドラ)と関数を関連付ける
        document.getElementById('btn').onclick = function() {
          alert('A handler is executed!');
        }
      }
    </script>
    
    <input type="button" value="Why don't you click?" id="btn">
    

イベント発生時の標準の挙動

以下の様に予め挙動が決まっているものがあります。

  • リンクがクリックされたらページを移動する。
  • サブミットボタンがクリックされたらフォームを送信する。
  • ページ上で右クリックされたらコンテキストメニューを表示する。

このような挙動を抑止したい時があります。抑止できます。

cancelevent.html
<!-- onclick="return false;"でクリックの標準挙動を抑止する -->
<input type="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">
  // イベントリスナ登録のラッパー関数(クロスブラウザ対策)
  function addListener(element, event, listener) {
    if (element.addEventListener) {    // IE以外
      element.addEventListener(event, listener, false);
    } else if (element.attachEvent) {  // IE
      element.attachEvent('on' + event, listener);
    } else {
      throw new Error('利用できるイベントリスナがありません。');
    }
  }

  // loadによりロード完了後に下記のコードが実行されるため、セレクタが空振らない
  addListener(window, 'load', function () {
    addListener(document.getElementById('btn'), 'click', function() {
      alert('A listener is executed!');
    });
  });
</script>

<input type="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.targetwindow.event.srcElemente.buttonwindow.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>

<div id="higher" style="width:100px; height:100px; border: solid 1px #000;">
  <div id="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ではかつて非同期処理を実現するのにsetTimeoutsetIntervalを利用していましたが、課題がありました。新しい仕組みが導入され、徐々に改善されてきています。

参考資料:
JavaScriptとコールバック地獄
JavaScriptと非同期のエラー処理

Promise

ES2015で標準ビルトインオブジェクトに追加されました。非同期処理を実現します。使い方はこちら

promise.js
function asynchronous(testCase) {
  return new Promise(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
function asynchronous(canary) {
  return new Promise((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
function asynchronous(testCase) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (testCase === 'normal') {
        resolve('血祭りだ');
      } else {
        reject('来ませんでした');
      }
    }, 1000);
  })
}

async function series() {
  // 非同期処理の完了を待つ。その一方で呼び出し元に処理を戻す。
  result = await asynchronous('normal');  // 正常系
  // result = await asynchronous('abnormal');  // 異常系

  // awaitの完了後に処理する。
  console.log('しかも7日で');          // ①
  return result;
}

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-jsPolyfill.ioをご参照のこと。

ES2015(ES6)以降に追加された機能

まとめがありました。

ECMAScript6の新機能
ES2016 / ES2017の最新動向を追ってみた
ES2015・ES2016・ES2017・ES2018を手早く学習するTHE ES Guide

さいごに

JavaScriptの学習を楽にするために、散在する情報の整理を試みてみました。何かの役に立てば幸いです。有益な情報を公開して下さった先人たちに敬意と感謝の気持ちでいっぱいです。ありがとうございます。


  1. lengthプロパティと0から始まる添字のプロパティを持つが、forEachやmapのようなArrayの組み込みメソッドを持たない。 

  2. M-1 2019 

  3. 諸説ある模様。Stack OverflowQuora檜山正幸のキマイラ飼育記 (はてなBlog)。 

  4. HTTPプロトコルの仕様上JSONのオブジェクトを授受できないのでシリアライズ(テキスト形式に変換)して授受する。eval('(' + XMLHttpRequestObject.responseText + ')')でテキストからJSONオブジェクトに戻す。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした