これまでずっとサーバサイドをメインでやってきたとは言え、JavaScript に触れる機会も少なくなかったのですが、正直なところ何度やってもコツが掴めないというか、「JavaScript って独特な言語だなあ」というモヤモヤとした苦手意識がありました。
少し前に、「何となくJavaScriptを書いていた人が一歩先に進むための本」(以下「一歩先」)という本をたまたま Kindle Store で見かけたのをきっかけに、同じ著者の続編「JavaScriptの理解を深めた人がさらにもう一歩先に進むための本」(以下「もう一歩先」)を続けて読み、さらに積ん読してあった「Effective JavaScript JavaScriptを使うときに知っておきたい68の冴えたやり方」と合わせて、三冊の JavaScript 本を一気に読んでみたら、これまでのモヤモヤがかなりスッキリして何だか JavaScript の初心者を卒業できたような気がしたので、忘れないうちにメモを残しておきたいと思います。
「一歩先」「もう一歩先」の二冊は薄くてサクサク読めるので非常にオススメです。それぞれ値段も安いですし。
実戦で今すぐ使える知識が多く載っているのが「一歩先」、ES2015 と JavaScript で一番やっかい(?)な this について詳しく学びたければ「もう一歩先」というイメージでしょうか。
こちらは、JavaScript 中級者になるためのポイントが詰まっている本。気になるところだけ読んでもいいし、最初から読み進めて「難しいな」と感じた時点でストップしてもいい。項目ごとに「憶えておくべきポイント」としてまとめが書かれているのが素晴らしいです。
なおこの本では、ES2015 については触れられていません(ES5 を前提)。
目次
ES5 と ES2015(ES6)の対応状況
ES5(ECMAScript 第5版)の主要ブラウザの対応状況は、
ECMAScript 5 compatibility table
となっており、モダンブラウザであればほぼ確実に動くと見てよい。
一方、次期バージョンの ES2015(通称 ES6)は、Chrome, Firefox, Edge 以外はまだまだ未対応のものが多く、Android, iOS も壊滅的。
ECMAScript 6 compatibility table
少なくとも現時点ではES2015を「そのまま」現場に持ち込むことは現実的とは言えないでしょう。
という昨年10月頃からあまり変わっていない状況。
そんな ES2015 を実践投入するには、「Babel」などのトランスパイラ(ES2015 で書かれたコードを ES5 のコードへ変換する仕組み)を使う。
プリミティブ型
JavaScript には、
- boolean
- number
- string
- null
- undefined
のプリミティブ型が存在し、通常は「値そのもの」を示す。
number, string, boolean にはラッパーオブジェクトが存在し、場合によっては一時的にオブジェクトとして振る舞うことがあるが、null, undefined は常にプリミティブ型となる。
var n = 1; console.log(n.toString()); // "1"
関数
JavaScript では、次のいずれかの方法で関数を定義する。
- ① 関数宣言(function 命令)
- ② 関数式(関数リテラル)
- ③ Function コンストラクタ(ぶっちゃけ使う必要なし!)
① 関数宣言(function 命令)
function add(x, y) { return x + y; }
この形式で定義された関数は、コード実行時にスコープの先頭まで「関数の巻き上げ」(ホイスティング)が発生する。
② 関数式(関数リテラル)
var add = function(x, y) { return x + y; };
この形式で定義された関数は「関数の巻き上げ」が発生しない。
また、以下のような「名前付き関数式」は、可搬性がないので使わないこと。
var add = function hoge(x, y) { return x + y; };
(参考)【JavaScript】関数定義いろいろ - Qiita
高階関数
JavaScript は第一級関数で、高階関数を扱える。
高階関数とは「関数自身を引数や戻り値として扱う関数」(Effective JavaScript では「ほかの関数を引数として受け取るか、結果として関数を返す関数」)。
関数もオブジェクトの一種。
function add(x, y) { return x + y; } // これが高階関数 function calc(func, x, y) { return func(x, y); } console.log(calc(add, 1, 2)); // 3
(参考)JavaScriptで関数型プログラミングの入門 - Qiita
即時関数
() 演算子を使って、関数を即時実行できる。一度しか実行したくないときに。
(function add(x, y) { console.log(x + y); })(1, 2); // 3
無名関数を即時実行することで、グローバルオブジェクト(の名前空間)を汚染しないようにすることができる。
(function () { var name = 'not global'; })();
また、即時関数を応用すれば、ES5 には存在しない「ブロックスコープ」を擬似的に実現できる。*1
まずダメな例。
var arr = [1, 2, 3], results = [], i; for (i = 0; i < arr.length; i++) { results[i] = function() { return arr[i]; }; } results[0](); // undefined results[1](); // undefined results[2](); // undefined
8〜10行目の results[0]();, results[1]();, results[2](); が実行されるタイミングではグローバル変数 i の値が 3 となっていて、結果が undefined になってしまっている。
即時関数を使ってブロックスコープの変数 j を作成して修正したものが以下。
var arr = [1, 2, 3], results = [], i; for (i = 0; i < arr.length; i++) { (function() { var j = i; results[i] = function() { return arr[j]; }; })(); } results[0](); // 1 results[1](); // 2 results[2](); // 3
引数
JavaScript の関数は、シグネチャ(引数の型と数)を持たないため、引数の型や数のチェックを行わないし、「オーバーロード」という概念も存在しない。
arguments は呼び出し元から渡された引数を管理しているオブジェクトで、暗黙的に生成される。
var checkArgs = function() { console.log(arguments[0], arguments[1]); }; checkArgs(1, 2); // 1 2 checkArgs(1); // 1 undefined checkArgs(); // undefined undefined
new 演算子
「一歩先」の
new は、「オブジェクトのインスタンスを返せ!」とコンストラクタに命令するための演算子。
という説明がすごく分かりやすい。
通常、JavaScriptの関数は return文が明示的に指定されていない場合には呼び出し元に undefined が返される。new 演算子を用いて呼び出された場合は、例外的に、return文の有無に関わらずオブジェクトのインスタンスが返却される。
// コンストラクタ関数 var Person = function(name) { this.name = name; } // prototype プロパティを使って、オブジェクトにメソッドを追加 Person.prototype.hello = function() { console.log('I am ' + this.name); } // new 演算子によって、person.__proto__ に Person.prototype が代入される var person = new Person('akiyoko');
prototype
ES6 で「クラス」の概念が導入されたが、それ以前には JavaScript には「クラス」は存在しない。prototype がクラスの代わりとして使われる。
(いわゆる「クラス」が実現するような)オブジェクトにメンバ(主にメソッド)を追加する仕組みとして、JavaScript では「prototype プロパティ」を使う。
インスタンス化されたオブジェクトは、使用したコンストラクタの prototype プロパティに対して暗黙の参照を持ち、prototype プロパティに追加されたメソッドに対しても同様に暗黙の参照を持つ。
その際、インスタンス化したオブジェクトは、コンストラクタで定義されたメンバ分のメモリを都度確保するが、prototypeプロパティに格納したメソッド分のメモリは確保しないため、メモリ使用量の節約を目的として prototype プロパティにメソッドを追加する。
つまり、
var Person = function(name) { this.name = name; this.hello = function() { console.log('I am ' + this.name); } }
とするよりも、
var Person = function(name) { this.name = name; } Person.prototype.hello = function() { console.log('I am ' + this.name); }
とした方がメモリの節約になるので、通常は後者のように書く。
メソッドは必ず prototype プロパティで管理するように徹底する
プロトタイプチェーン
プロトタイプチェーンは、JavaScript で「オブジェクト指向の継承」を実現するための仕組み。継承したいオブジェクトのインスタンスを、自身の prototype プロパティとして格納する。(なお継承は、ES5準拠の Object.create でも実現可能。)
var Person = function(name) { this.name = name; } Person.prototype.hello = function() { console.log('I am ' + this.name); } var Student = function(name) { this.name = name; } Student.prototype = new Person(); Student.prototype.study = function() { console.log('I am studying now!'); }
ここで、
var akiyoko = new Student('akiyoko'); akiyoko.hello(); // I am akiyoko
を実行する際に、
1)Student オブジェクトの hello メソッドを検索するも、存在せず
↓
2)Student オブジェクトが暗黙の参照を持つ Student.prototype(すなわち Person オブジェクト)の hello メソッドを検索するも、存在せず
↓
3)Person オブジェクトが暗黙の参照を持つ Person.prototype の hello メソッドを検索して見つかったので、実行!
というプロトタイプチェーンを辿る。
this
this が結合する値(レシーバ)は、メソッドおよび関数の呼び出し時に決まる(呼び出され方によって決まる)。
① メソッド呼び出しされた場合
メソッド呼び出しされた場合のメソッド内の this は、メソッドプロパティがルックアップされるオブジェクトに結合される。というわけで多くの場合、this は呼び出し元のオブジェクトを指すことになる。
var akiyoko = { name : 'akiyoko', hello : function() { console.log('I am ' + this.name); // ★ } } // メソッド内の this(★)は、メソッドプロパティがルックアップされる akiyoko オブジェクトに結合される akiyoko.hello(); // I am akiyoko
上の例では、hello プロパティがルックアップされた akiyoko オブジェクトを this のレシーバとして、hello が呼び出される。
② new 演算子を使ってインスタンス化された場合
new 演算子を使ってインスタンス化された場合のコンストラクタ内の this は、そのインスタンス自身に結合される。
var Person = function(name) { this.name = name; // ★ this.hello = function() { // ★ console.log('I am ' + this.name); } } // コンストラクタ内の this(★)は、返却されるインスタンス自身に結合される var akiyoko = new Person('akiyoko');
③ 関数呼び出しされた場合
関数呼び出しされた場合の関数内の this は、グローバルオブジェクトに結合される。
var name = 'global'; var hello = function() { console.log('I am ' + this.name); // この this はグローバルオブジェクトに結合される } hello(); // I am global
④ メソッドおよび関数が高階関数の引数として渡された場合
メソッドおよび関数が Array.prototype.forEach() のような高階関数の引数として渡された場合は、少しやっかい。
以下の例では、akiyoko.hello のレシーバは akiyoko にならない。akiyoko.hello がいつどういった形で呼び出されるのかはその外側のメソッドおよび関数(ここでは forEach *2)の 実装次第 である。
var Person = function(name) { this.name = name; this.hello = function() { console.log('I am ' + this.name); } } var akiyoko = new Person('akiyoko'); [1, 2].forEach(akiyoko.hello); // I am I am
レシーバを akiyoko にするためには、bind を使って this を束縛すればよい。 *3
var Person = function(name) { this.name = name; this.hello = function() { console.log('I am ' + this.name); } } var akiyoko = new Person('akiyoko'); [1, 2].forEach(akiyoko.hello.bind(akiyoko)); // I am akiyoko I am akiyoko
別の解決策として、this というキーワードを使わなければよいということで、self, _this, that といった名前の変数に this の参照を「逃がしてやる」方法もある。
次は、イベントリスナーの例。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>JavaScript Test | akiyoko blog</title> </head> <body> <button type="submit" id="submit" name="action" value="send">送信</button> <script> var el = document.querySelector('#submit'); el.addEventListener('click', function () { console.log('this=', this); // ★ }, false); </script> </body> </html>
上の例では、addEventListener の引数で渡される無名関数内の this(★)は発火元の DOM要素を指すため、ボタン押下時の実行結果は以下のようになる。
this= <button type=<200b>"submit" id=<200b>"submit" name=<200b>"action" value=<200b>"send"><200b>送信<200b></button><200b>
以下の参考サイトがさらに詳しい。
ハンドラ内でのthisの値について
イベントハンドラの関数内から、その発火元の要素を参照したくなる事がよくあります。 addEventListener()を使用して、関数を割り当てたのであれば、 呼び出し元への参照がthisの値となって関数内に渡されます。
(中略)比較として、仮にHTML内にハンドラが次のように配置されていた場合、
<table id="t" onclick="modifyText();">onclickイベントで呼び出された際の、modifyText()内のthisの値は、 グローバル(window)オブジェクトへの参照になります。
クロージャ
クロージャとは、分かりやすく言うと、
「ローカル変数を参照している、関数の中に定義された関数」
である。
下の例では、
ローカル変数(count)を参照している、関数(outerFunc)の中に定義された関数(innerFunc)
がクロージャ。
function outerFunc(initCount) { var count = initCount; var innerFunc = function() { return ++count; }; return innerFunc; } var myCounter = outerFunc(100); console.log(myCounter()); // 101 console.log(myCounter()); // 102 console.log(myCounter()); // 103
「innerFunc の Callオブジェクト ⇒ outerFunc の Callオブジェクト ⇒ グローバルオブジェクト」というスコープチェーンが生成され、 innerFunc が有効である限りそのスコープチェーンは保持される。innerFunc関数は、myCounter変数に格納されるため、outerFunc関数が呼び出し終わっても破棄されず、従って当該スコープチェーンも破棄されない。これにより、Callオブジェクトに管理されているローカル変数(count)も破棄されず、代入された値が残ったままとなる。
*1:ES2015 では let, const を使ってブロックスコープを実現可能。http://qiita.com/tuno-tky/items/74ca595a9232bcbcd727
*2:ちなみに forEach の場合は this はグローバルオブジェクトを指す。
*3:call メソッドや apply メソッドでも this を束縛することができるが、レシーバオブジェクトのほかに引数も指定する必要があるため、このケースでは使用されない。