JavaScriptについてのまとめ

JavaScriptに関するまとめです。いきなりプログラミング初心者が読んでもわけ分からないと思います。プログラミングの経験はあるけどJavaScriptははじめてという人にはそこそこ役に立つかもしれません。すでにJavaScriptを勉強したことのある人なら知っていて当然です。なので初心者の人はここに書かれてあることくらいは理解できるように勉強しましょう。

リテラル構文とシンタックスシュガー

オブジェクト、関数、配列、正規表現に関してリテラル構文シンタックスシュガーになってます。

// Objectオブジェクトのコンストラクタ
var obj = new Object();
 
// オブジェクトリテラルはnew Objectのシンタックスシュガー
var obj = { };
 
 
// Functionオブジェクトのコンストラクタ
var fnc = new Function();
 
// 関数リテラルはnew Functionのシンタックスシュガー
var fnc = function() { };
 
 
// Arrayオブジェクトのコンストラクタ
var ary = new Array();
 
// 配列リテラルはnew Arrayのシンタックスシュガー
var ary = [ ];
 
 
// RegExpオブジェクトのコンストラクタ
var rex = new RegExp();
 
// 正規表現リテラルはnew RegExpのシンタックスシュガー
var rex = //;

関数リテラルは無名関数や匿名関数と呼ばれたりもします。

new Stringnew Numbernew Booleanはそれぞれの型のリテラル構文と等価ではありません。つまりこれらに関してはリテラル構文はシンタックスシュガーではありません。気になる方は

console.log( typeof " " );
console.log( typeof new String() );

の結果を見てください。

オブジェクトはハッシュテーブルのようなもの

オブジェクトが基本です。オブジェクトはハッシュテーブルのようなもので、ハッシュテーブルのキーがオブジェクトのプロパティ、ハッシュテーブルのキーの値がオブジェクトのプロパティの値に対応します;

var obj = {
    key: value
};

関数は第一級オブジェクト

関数は第一級オブジェクトです。ということは値になります。ということはオブジェクトのプロパティの値になれます。つまりオブジェクトのメソッドになります。てことで、JavaScriptでのメソッドはプロパティにセットされた関数を指します。プロパティ(ハッシュテーブルでのキー)はメソッド名になります。コードにするとこんな感じです;

var obj = {
    methodName: function() {
            // メソッドの処理
    }
};
 
obj.methodName(); // メソッドの実行

宣言のホイスティング

x = 1;
var x;

var x;
x = 1;

として実行されます。JavaScriptでは宣言var xのホイスティング(巻き上げ)が起こるからです。ホイスティングは変数のスコープ内で起こります。なので次のようなコードだと関数内のconsole.log(x)ではundefinedが返ってきます;

var x = 0;
 
function hoge() {
    console.log( x ); // undefined
    var x = 1;
    return x;
}
 
console.log( hoge() ); // 1

ホイスティングされた後のコードを見るとなぜundefinedになるかは一目瞭然だと思います;

var x = 0;
 
function hoge() {
    var x;
    console.log( x ); // undefined
    x = 1;
    return x;
}
 
console.log( hoge() ); // 1

関数宣言と関数式は違う

関数宣言と関数式は違います。例えば次のコード;

// 関数宣言
function hoge() { }
 
// 関数式
var hoge = function() { };

この二つは決して等価ではありません。シンタックスシュガーでもありません。別物です。関数を実行すれば分かります。まず関数宣言

hoge();
function hoge() { }

は宣言のホイスティングによって

function hoge() { }
hoge();

となってOKですが、関数式の場合

hoge();
var hoge = function() { };

はアウトです。

var hoge;
hoge();
hoge = function() { };

として実行されてundefined is not a function (evaluating 'hoge()')というエラーを吐きます。関数宣言された関数はコンパイル時に定義されますが関数式の関数は実行時に定義されるからです。hoge()を実行する時点で変数hogeは宣言されただけで未定義の状態なので関数として実行できないというわけです。関数式はホイスティングされないために関数宣言と関数式では振る舞いが異なります。

値渡しと参照渡し

プリミティブ型(文字列型、数値型、論理値、null、undefinedの5つ)は値渡し、オブジェクト型(プリミティブ型以外)は参照渡しです。参照渡しだと次のような振る舞いをします。

var a, b;
a = { hoge: "hoge" };
b = a;
 
b.hoge = "foo";
 
console.log( a.hoge ); // foo
console.log( b.hoge ); // foo
 
b = { hoge: "bar" };
 
console.log( a.hoge ); // foo
console.log( b.hoge ); // bar

ここではこんな感じのことが起こっています;

var a, b;
 
// 無名オブジェクト{ hoge: "hoge" }がメモリ上のどこかに置かれて、変数aはそれを参照する
a = { hoge: "hoge" };
 
// aからbへ参照が渡される
b = a;
 
// bが参照している無名オブジェクト{ hoge: "hoge" }のhogeプロパティがfooに変更される
b.hoge = "foo";
 
// aが参照しているのは変更された無名オブジェクト{ hoge: "foo" }なのでfooが出力
console.log( a.hoge ); // foo
// bが参照しているのは変更された無名オブジェクト{ hoge: "foo" }なのでfooが出力
console.log( b.hoge ); // foo
 
 
// 無名オブジェクト{ hoge: "bar" }がメモリ上のどこかに置かれて、変数bは新たにそれを参照する
b = { hoge: "bar" };
 
// aが参照しているのはすでにあった無名オブジェクト{ hoge: "foo" }なのでfooが出力
console.log( a.hoge ); // foo
// bが参照しているのは新しくつくられた無名オブジェクト{ hoge: "bar" }なのでbarが出力
console.log( b.hoge ); // bar

クラスはあるの?

ありません。仕様です。

ES2015でJava Scriptにもクラス構文が導入されましたが、これはあくまでもプロトタイプベースでのオブジェクト指向のシンタックスシュガーであって、言語レベルで新たにクラスが実装されたわけではないです。

プロパティの検索にはプロトタイプチェインが使われる

(例外を除いて)オブジェクトは生成されると__proto__プロパティをもちます。__proto__には別のオブジェクトへの参照が入ります。あるオブジェクトobjが、あるプロパティpropを参照するときには以下のような手順で検索されます;

  1. obj.prop
  2. obj.__proto__.prop
  3. obj.__proto__.__proto__.prop
  4. 参照先がObjectオブジェクトのprototypeになるまで検索が続く
  5. そこまでいってなければ検索終了

これがプロトタイプチェインです。ただし__proto__は標準仕様ではありません。

コンストラクタ関数からnew演算子でオブジェクトを生成したときにおこなわれること

var obj = new FuncObject();としたときの挙動です;

var obj = {};
 
obj.__proto__ = ( FuncObject.prototype instanceof Object ) ? FuncObject.prototype: Object.prototype;
 
var returnedValue = FuncObject.apply( obj, arguments );
 
return if ( returnedValue instanceof Object ) ? returnedValue: obj;

つまりnew演算子によって、生成されたオブジェクトの__proto__プロパティにコンストラクタ関数のprototypeプロパティが参照しているオブジェクトが代入されます。これにプロトタイプチェインの仕組みが合わさることで、プロトタイプ(原型)となるオブジェクトをベースにして新しいオブジェクトが生成されます。JavaScriptが『プロトタイプベースのオブジェクト指向プログラミング言語』と呼ばれるのはこのためです。

グローバルスコープとローカルスコープ

JavaScriptは関数スコープです。関数が変数のスコープを決定し、各関数の中がローカルスコープになります。ローカルスコープで宣言された変数はその関数の内側からだけ参照できます。つまり『外側から内側は見えない』けど『内側から外側は見れる』ようになっています。

変数のスコープがグローバルスコープの場合はプログラム全体からアクセスすることができます。varをつけて宣言しないとどこにいようが変数のスコープはグローバルスコープになります。たとえ関数の中でもvarをつけないとグローバル変数になります。scriptタグの直下で定義された変数もグローバル変数になります。ローカル変数は関数の実行終了時に、グローバル変数はプログラムの実行終了時に破棄されます。

// iはvarなしなのでグローバル変数
i = "global";
// jはどの関数にも入っていないのでグローバル変数
var j = "global";
 
// どの関数にも入っていないのでfuncもグローバル変数
function func() {
// kはvarなしなのでグローバル変数
  k = "global";
// lは関数内でvarつきで宣言されているのでローカル変数
  var l = "local";
}

関数の引数は、もしそれが値渡しであるプリミティブ型であればローカル変数になります。でもオブジェクトや配列のような参照渡しであればローカルスコープから外れます。つまり関数内外から同じ実体にアクセスすることができます。

JavaScriptにブロックスコープはありません。forやifの中の”{}”に変数が定義されていても、その変数のスコープを決定するのは変数が含まれている関数です。

var v = "global";
function f() {
// 宣言のホイスティングによりifの中のvは関数fのすぐ下で宣言される
// 代入はifの中で行われるのでalertの時点ではundefinedになる
  alert(v); // undefined
  if (!v) {
    var v = "local";
  }
}

スコープチェインは変数オブジェクトのリスト

変数の宣言と参照は、変数名をプロパティ、変数値を値とした変数オブジェクトへの読み書きに他なりません。グローバルスコープにおける変数オブジェクトはグローバルオブジェクトです。ローカルスコープにおける変数オブジェクトはCallオブジェクト/activationオブジェクトです。

// グローバル変数はグローバルオブジェクトであるwindowのプロパティとしてセットされる
var a = 1;
console.log( window.a ); // 1

Callオブジェクトは関数の呼び出し時に生成されて直接アクセスすることはできません。Callオブジェクトにはthis、argumentsオブジェクト、変数に渡された引数、ローカル変数、親のCallオブジェクトへの参照がセットされます。関数の実行が完了するとCallオブジェクトは破棄されます。

変数の参照はCallオブジェクトをたどっていくことで行われます。検索はグローバルオブジェクトにたどりついたときに終了します。変数の名前解決に使われるこの仕組みがスコープチェインで、プロトタイプチェインとまったく同じ仕組みになっています。

レキシカルスコープとCallオブジェクト

JavaScriptはレキシカルスコープを採用しています。つまり関数が実行されるときではなくて、関数が定義されたとき(字句が解析されたとき)に変数のスコープが決まります。関数実行時には以下の順番でスコープが決定されます;

  1. 関数宣言時にスコープチェインが設定される
  2. Callオブジェクトを生成してスコープチェインの先頭に追加
  3. Callオブジェクトを初期化するときにargumentsプロパティをセット
  4. 関数の引数、var宣言されたローカル変数をCallオブジェクトに追加

通常の関数の場合の流れは以下の通りです;

// 定義時にスコープチェインが設定される
function outer(num) {
    var count = num;
    return count;
}
 
// 実行時にCallオブジェクトが生成されてスコープチェインに追加される
console.log( outer(1) );
// 関数の実行が完了するとCallオブジェクトはスコープチェインから外される
// このときスコープチェインがCallオブジェクトへの唯一の参照だったために
// Callオブジェクトはスコープチェインから外されるとともにガベージコレクタに渡って破棄される。

関数が入れ子関数を含む場合でも外部から参照されない限り同じような流れになります;

// 定義時に関数outerのスコープチェインが設定される
function outer(num) {
    var count = num;
    function inner() {
        return ++count;
    }
    inner();
    return count;
}
 
// 関数outerの実行時にouterのCallオブジェクトが生成されてスコープチェインに追加される
// 関数outerの中で関数innterが定義され、innerのスコープチェインが設定される
// 関数innerのスコープチェインの先頭は関数outerのCallオブジェクトとなっている
// 関数innerが実行されるときにinnerのCallオブジェクトが生成される
// 関数innerの実行が完了するとinnerのCallオブジェクトはどこからも参照されなくなり破棄される
console.log( outer(1) );
// 関数outerの実行が完了するとouterのCallオブジェクトを参照しているのは
// innerのスコープチェインのみとなる
// また関数innerを参照しているのもouterのCallオブジェクトのみとなる
// そこでouterのCallオブジェクトと関数innerともにガベージコレクタに渡って破棄される

関数が入れ子関数を含み、なおかつ入れ子関数が定義されたスコープの外部から入れ子関数への参照がある場合にはまったく違うことが起こります。これがクロージャです。

クロージャの仕組み

関数が入れ子関数を含んでいて、なおかつ入れ子関数が定義されたスコープの外部から入れ子関数への参照がある場合に、その入れ子関数はクロージャと呼ばれます。クロージャの典型例はカウンター用の関数です;

// 関数enclosureが定義されるとenclosureのスコープチェインが設定される
// closureはenclosureが実行されてから定義されるのでclosureのスコープチェインはまだ存在しない
function enclosure(num) {
    var count = num;
    function closure() {
        return ++count;
    }
    return closure;
}
// enclosureが実行されることで、enclosureのスコープチェインの先頭にCallオブジェクトが追加される
// var宣言されたcountや引数である1がCallオブジェクトに追加され、count=1になる
// さらに関数closureが定義され、closureのスコープチェインが設定される
// closureのスコープチェインの先頭はenclosurenのCallオブジェクトになっている
// 関数closureがenclosureの戻り値として返されてcounterから参照される
var counter = enclosure(1);
// 通常は関数の実行が完了するとCallオブジェクトは破棄されるが
// counterがclosureを参照しているためにclosureはenclosureのCallオブジェクトへの参照を持ち続ける
// そのためenclosureのCallオブジェクトは破棄されずに残る
 
// closureが実行され、closureのCallオブジェクトが生成されてスコープチェインに追加される
// closureはスコープチェイン内のenclosureのCallオブジェクトからcountを参照する
// ++countによりcount=2となって返り値としてコンソールに表示される
// countはclosureのCallオブジェクトには存在しないためスコープチェインをたどって
// enclosureのCallオブジェクトにcount=2として書き込まれる
console.log( counter() ); // 2
// 関数closureの実行が終了するとclosureのCallオブジェクトは破棄されるが
// counterからclosureへの参照があるためにenclosureのCallオブジェクトは破棄されない
 
// 再びclosureが実行され、closureのCallオブジェクトが新たに生成されてスコープチェインに追加される
// closureはスコープチェイン内のenclosureのCallオブジェクトからcountを参照する
// ++countによりcount=3となって返り値としてコンソールに表示され
// enclosureのCallオブジェクトにcount=3と書き込まれる
console.log( counter() ); // 3
// 関数の実行が終了されるとclosureのCallオブジェクトは破棄される

closureの参照元を二つつくった場合はCallオブジェクトが異なる別々のスコープチェインがつくられます;

function enclosure(num) {
    var count = num;
    function closure() {
        return ++count;
    }
    return closure;
}
// enclosureが実行されることで、enclosureのスコープチェインの先頭にCallオブジェクトが追加される
var counter1 = enclosure(0);
// counter1でつくられたCallオブジェクトとは別のCallオブジェクトが生成され
// enclosureのスコープチェインの先頭に追加される
// つまりcounter1とcounter2は別々のスコープチェインをもつようになる
var counter2 = enclosure(10);
 
// counter1とcounter2のスコープチェインは別なのでカウンターは独立して動く
console.log( counter1() ); // 1
console.log( counter2() ); // 11

入れ子関数が複数ある場合ではenclosureのCallオブジェクトは共有されます;

function enclosure(num) {
    count = num;
    return {
        'up': function() {
            return ++count;
        },
        'down': function() {
            return --count;
        },
        'reset': function() {
            count = 0;
            return count;
        }
    }
}
 
// enclosureの実行とともにenclosureのスコープチェインの先頭にCallオブジェクトが追加される
// returnで返されるオブジェクト内のすべての関数のスコープチェインの先頭はこのCallオブジェクトになる
// つまり一つのCallオブジェクトが共有される
var counter = enclosure(10);
 
// 変数countが共有されるため一つの変数を異なる関数から参照できる
console.log(counter.up()); // 11
console.log(counter.reset()); // 0
console.log(counter.down()); // -1

参考ページ