パーフェクトJavaScript読書メモ 5章

パーフェクトJavaScript (PERFECT SERIES 4)

パーフェクトJavaScript (PERFECT SERIES 4)

あまりにも素晴らしい本なので、自分で抜粋させていただき、頭の整理をさせて頂く。
これだけ書かれてる本はちょっとすごい。ちょっと私の頭では1回では難しい。でも本質の理解にはもってこいの書籍すぎてすばらしい。理解に近道はない。きっちりやっておくと、なにかと後々ためになりますよね。とりあえず5章で私にとっての重大ごとを。

5-3変数とプロパティ

変数はスコープの違いで、グローバル変数とローカル変数に分かれる。

グローバル変数

グローバル変数(及びグローバル関数名)はグローバルオブジェクト(実行時に最初から存在するオブジェクト)のプロパティです。クライアントサイドJavaScriptでは最初からグローバルオブジェクトを参照するグローバル変数windowが提供されています。

ローカル変数

関数内で宣言した変数。関数の引数のパラメータ変数もローカル変数の1種。ローカル変数(パラメータ変数)は関数が呼ばれた時に暗黙に生成されるオブジェクトのプロパティ。暗黙に生成されるオブジェうとをCallオブジェクトと呼ぶ。一般にローカル変数の生存期間は関数が呼ばれてから抜けるまで。

5-7オブジェクト生成

5-7-2コンストラクタとnew式

function MyClass(x,y){
	this.x = x;
	this.y = y;
}

var obj = new MyClass(3, 2);
alert(obj.x + " " + obj.y);
  • コンストラクタの形式的な説明
    1. コンストラクタ自体は普通の関数宣言と同じ形
    2. コンストラクタはnew式で呼び出す
    3. コンストラクタを呼び出すnew式の評価値は(新規に生成された)オブジェクト参照
    4. new式で呼ばれたコンストラクタ内のthis参照は(新規に生成する)オブジェクトを参照する


コンストラクタと通常の関数の違いは呼び出し方の違いのみ。


コンストラクタは暗黙に関数の最後にreturn thisがあるような動作をします。ではコンストラクタ内に本当のreturn文があると何が起きるのでしょうか。実はやや分かりにくい挙動になります。returnでオブジェクトを返すとそれがコンストラクタ呼び出し時のnew式の評価値になります。つまりnew式を使っても生成したオブジェクト以外のオブジェクトが返ります。一方、基本型の値をreturnで返すとコンストラクタ呼び出し時には無視され、暗黙にreturn thisがある挙動をします。この挙動は混乱の元なのでコンストラクタにreturn文を書かないことを進めます。


リスト5.9クラス定義もどき(改善の余地あり)

function MyClass2(x,y){
	//フィールド相当
	this.x = x;
	this.y = y;
	//メソッド相当
	this.show = function(){
		alert(this.x + " " + this.y);
	}
}
var obj = new MyClass2(3,2);
obj.show();
  • 上記MyClass2の2つの問題点
    1. すべてのインスタンスが同じメソッド定義の実態のコピーを持つので効率(メモリ効率と実行効率)が良くない。→ プロタイプ継承で解決
    2. プロパティのアクセス制御(private public)ができない → クロージャで解決

5-8プロパティのアクセス

オブジェクト参照に対して、ドット演算子もしくはブラケット演算子でプロパティにアクセスできます。

5-8-2ドット演算子とブラケット演算子の使い分け

慣習的には、記述が簡潔なドット演算子をデフォルトにし、ブラケット演算子でしか書けない場合にブラケット演算子を使うのが一般的。

ブラケット演算子でしか書けないパターン

●識別子に使えないプロパティ名を使う場合

js> obj = {'hoge-hoge':10};
({'hoge-hoge':10})
js> obj.hoge-hoge
NaN
js> obj['hoge-hoge']
10

●変数の値をプロパティ名に使う場合
例えば、配列オブジェクトのプロパティ名は数値です。数値をドット演算子に続けて書けないので必然的にブラケット演算子を使います。配列の要素アクセスにブラケット演算を使うプログラミング言語は多いので可読性も上がります。

●式の評価を結果をプロパティ名に使う場合

5-8-3プロパティの列挙

var obj = {x:3,y:4,z:5};
for ( var key in obj) {
	alert('key = ' + key);
	alert('val = ' + obj[key]);
}

5-9連想配列としてのオブジェクト

5-9-1連想配列について

delete演算子について

オブジェクトからプロパティを削除する。
deleteはMap用語で言うところのキーを削除するだけで、値に関してはメモリに残る。その値はガーベージコレクションで消えるかもしれないが、それはdeleteの直接の働きではない。

var map = {x:3,y:4};
alert(map.x);
var b = delete map.x; //delete map['x']でも可
alert(b); //削除に成功するとtrue
alert(map.x); //削除した要素を読むとundefinedが返る

5-9-2連想配列としてのオブジェクトの注意点

function MyClass(){}
MyClass.prototype.z = 5; //プロトタイプチェーン上にプロパティzをセット
var obj = new MyClass();
alert(obj.z);

//for in はプロトタイプ継承したプロパティも列挙する。
for ( var key in obj) {
	alert(key);
}

//プロトタイプ継承したプロパティはdeleteできない。
var b = delete obj.z;
alert(b); //trueは返るが、
alert(obj.z); //削除はできない。
//オブジェクトを連想配列として使う場合、オブジェクトリテラルで生成するのが一般的、
//実際にはObjectクラスからプロパティを継承しているので注意が必要。
var map = {};
var r = 'toString' in map;
alert(r);//true

// Objectクラスをプロトタイプ継承したプロパティは列挙されない。
// enumerable属性のため(標準オブジェクトの一部のプロパティはこの属性がfalseである)。
// enumerable属性がfalseのプロパティはfor in文で列挙されない。
for ( var key in map) {
	alert(key);
}

//連想配列の存在チェックにin演算子を使うとプロタイプ継承したプロパティが引っかかる可能性がある
//よってhasOwnPropertyメソッドを使うといい。自分のプロパティのみの存在チェック。
var map = {};
alert(map.hasOwnProperty('toString'));//false
map['toString']=1;
alert(map.hasOwnProperty('toString'));//true
delete map['toString'];
alert(map.hasOwnProperty('toString'));//false

5-13 メソッド

JavaScriptの言語仕様にメソッドは存在しない。オブジェクトのプロパティに関数をセットしたものを便宜上メソッドと呼ぶだけ。

5-14 this参照

JavaC++

メソッドに暗黙に渡る引数とみなすことができる

JavaScript

トップレベルでも関数内でも使える。いつでも使える読み込み専用の変数です。オブジェクトを参照する。そしてコードのコンテキストに応じて自動的に参照先オブジェクトが変わる特別なものである。

5-14-1 this参照の規則

トップレベルコードのthis参照はグローバルオブジェクトを参照
関数内のthis参照は関数の呼び出し方法で異なる
//5-14-1
	alert('5-14-1');
	window.x = 'suda';
	var obj = {
		x : 3,
		doit : function() {
			alert('method is called. ' + this.x);
		}
	};
	obj.doit();
	obj['doit']();
	alert(this.x);

	//5-14-2
	alert('5-14-2');
	var obj = {
		x : 3,
		doit : function() {
			alert('method is called. ' + this.x);
		}
	};
	var fn = obj.doit; //obj.doitが参照する関数オブジェクトをグローバル変数に代入
	fn(); //関数内のthis参照はグルーバルオブジェクトを参照する
	
	var x = 5;
	fn();
	
	var obj2 = {x:4,doit2:fn};
	obj2.doit2();

5-15 applyとcall

function f() {
		alert(this.x);
	}
	var obj = {
		x : 4
	};

	f.apply(obj);
	f.call(obj);

	var obj = {
		x : 3,
		doit : function() {
			alert('method is called. ' + this.x);
		}
	}
	
	var obj2 = {x:4};
	obj.doit();
	obj.doit.apply();
	obj.doit.apply(obj2);

関数オブジェクトに存在するapplyとcallメソッドはその関数を呼び出す。関数内のthis参照を考慮しない場合は単なる関数呼び出しと変わらないが、this参照が関数内に存在する場合はthisが参照するオブジェクトを第1引数に渡すことで指定できる。

applyとcallの違い
function f(a, b) {
		alert('this.x= ' + this.x + ', a = ' + a + ',b = ' + b);
	}
	
	f.apply({x:4}, [1,2]); //第2引数の配列要素が関数fの引数になる
	f.call({x:4}, 1,2); //第2引数以降の引数が関数fの引数になる

5-16プロトタイプ継承

プロトタイプチェーン

前提
  1. すべての関数(オブジェクト)はprototypeという名前のプロパティを持つ(prototypeプロパティの参照先オブジェクトをprototypeオブジェクトと呼ぶことにします)
  2. すべてのオブジェクトは、オブジェクト生成に使ったコンストラクタ(関数オブジェクト)のprototypeオブジェクトへの(隠し)リンクを持つ
オブジェクトのプロパティ読み込み時の探索順序
  1. オブジェクト自身のプロパティ
  2. 暗黙リンクの参照オブジェクト(=コンストラクタのprototypeオブジェクト)のプロパティ
  3. 2のオブジェクトの暗黙リンクの参照オブジェクトのプロパティ
  4. 3の動作を探索が終わるまで続ける(探索の終端はObject.prototypeオブジェクト)
オブジェクトのプロパティ書き込み
  1. オブジェクト自身のプロパティ
プロトタイプチェーンによるプロパティの読み込み
function MyClass() {
		this.x = 'x in MyClass';
	}
	var obj = new MyClass(); //MyClassコンストラクタでオブジェクト生成
	alert(obj.x);			//オブジェクトobjのプロパティxにアクセス
	alert(obj.z);			//オブジェクトobjにプロパティzはない
	
	//関数オブジェクトは暗黙にprototypeプロパティを持つ
	MyClass.prototype.z = 'z in MyClass.prototype'; //コンストラクタのprototypeオブジェクトにプロパティzを追加
	alert(obj.z); //obj.zはコンストラクタのprototypeオブジェクトのプロパティにアクセス
	
	//prototypeに設定したので、以後生成するMyClassオブジェクトには、
	//共通してzプロパティが継承される
	var obj2 = new MyClass();
	alert('obj2 ' + obj2.z);
	
	var obj3 = new MyClass();
	alert('obj3 ' + obj3.z);
プロパティの書き込みと削除はプロトタイプチェーンを辿らない
        function MyClass(){this.x = 'x in MyClass';}
	MyClass.prototype.y = 'y in MyClass.prototype';
	
	var obj = new MyClass();
	alert(obj.y); 	// プロトタイプチェーンでプロパティyの読み取り
	
	obj.y = 'override'; // オブジェクトobjに直接プロパティyを追加
	alert(obj.y);		// 直接プロパティを読む
	
	var obj2 = new MyClass();
	alert(obj2.y);   // 別オブジェクトから見えるプロパティyは変わっていない 
	


	delete obj.y; //プロパティyを削除
	alert(obj.y);
	delete obj.y; //deleteの演算の評価値はtrueだが、
	alert(obj.y); //プロトタイプチェーン先のプロパティはdeleteできない

プロトタイプオブジェクト

function MyClass(){}
var obj = new MyClass();

MyClass.prototypeとobj.__proto__は同じオブジェクトを参照します。これがobjのプロトタイプオブジェクトです。

プロトタイプオブジェクトの取得方法
//プロトタイプオブジェクトの取得方法3例
function MyClass(){}
MyClass.prototype = {x:1050};
var Proto = MyClass.prototype;
alert('ProtoB ' + Proto.x);
var obj = new MyClass(); //オブジェクトobjのプロトタイプオブジェクトはオブジェクトProto

//インスタンスオブジェクトから取得(ECMAScript第5版の正攻法)
var Proto = Object.getPrototypeOf(obj);
alert('Proto1 ' + Proto.x);
//インスタンスオブジェクトから取得(独自拡張の__proto__利用)
var Proto = obj.__proto__;
alert('Proto2 ' + Proto.x);
//インスタンスオブジェクトからコンストラクタを経由した取得(常に使える保証はない)
var Proto = obj.constructor.prototype;
alert('Proto3 ' + Proto.x);

5-17オブジェクトと型

クラスベース言語

オブジェクトの型というのは雛形となるクラスや実装インターフェースになる。

JavaScript

この観点でのオブジェクトの型は存在しない(クラスやインターフェースはないから)。だが、組み込み基本型以外はすべてObject型であり、それらを生成して、そこに対してプログラマが恣意的にオブジェクトの共通性をprototype継承などを使って作ってゆくことにより、プロトタイプベースのオブジェクト指向での実装が可能になるし、そうするべきである。

5-17-4 型判定(ダックタイピング)

オブジェクトの振る舞いを直接調べて型を判定する手法を俗にダックタイピングと呼ぶ。ダックタイピングに使える手法の一つとしてin演算がある。プロトタイプチェーンで継承したプロパティも判別できる。以下ではobjの直接のプロパティだけでなく継承しているtoStringプロパティもtrueと判定される。

var obj = {};
obj.doit = function(){alert('doitMethod!');}
alert('doit' in obj); //オブジェクトobjがdoitプロパティを持つので結果はtrue
alert('toString' in obj); //toStringプロパティをObjectから継承しているので結果はtrue

5-17-5 プロパティの列挙(プロトタイプ継承を考慮)

ECMAScript第5版のObjectクラスのkeysメソッドとgetOwnPropertyNamesメソッドは引数に指定したオブジェクトの直接のプロパティ名の配列を返します。

var obj = {x:1,y:2};
alert(Object.keys(obj));
alert(Object.getOwnPropertyNames(obj)); //enumerable属性のデフォルト値は真なのでkeysと同じ結果

var arr = [3,4];
alert(Object.keys(arr));
alert(Object.getOwnPropertyNames(arr)); //lengthプロパティのenumerable属性は偽

//Object.prototypeオブジェクト
alert(Object.keys(Object.prototype)); //enumerableなプロパティは存在しない

alert(Object.getOwnPropertyNames(Object.prototype));

5-20 Objectクラス

Objectという名前は厳密にはグローバルオブジェクトのプロパティ名である。そのプロパティが参照しているのはFunctionオブジェクトである。なぜならObject()のように呼び出して機能するということはFunctionであるということでしょう?

5-21 グローバルオブジェクト

オブジェクトは本質的に名なしです。ObjectオブジェクトもStringオブジェクトもObject,Stringという名前(グローバルオブジェクトのプロパティ名)としてアクセス可能なだけ。グローバルオブジェクトの場合は仕様上決まった名前がない。クライアントサイドJavaScriptではwindowという変数名でアクセス可能。このwindowも実はグローバルオブジェクトのプロパティ名として存在している(循環参照)
ただしあくまでもECMAScriptのコア言語規格ではグローバルオブジェクトを参照する決まった名前は存在しません。

//トップレベルコードで下記コードを実行するとグローバルオブジェクトをどこでもglobalで参照可能
var global = this;