自習室

こもります

開眼!JavaScript:オブジェクトの振る舞いについて細かいとこメモ (3/3)

どんな本

開眼!  JavaScript ―言語仕様から学ぶJavaScriptの本質

開眼! JavaScript ―言語仕様から学ぶJavaScriptの本質

(ECMA-262 3rd edition 準拠の) オブジェクトの振る舞いについて、こまかーいところまで拾って説明してくれている本。

最近仕事でJavaScriptを書くことがふえたので、がむしゃらに書いて動いてはいるけど、いまいちよく分かっていないことが多いなぁ、と感じていたので、冷静に一度勉強してみようと思い手に取りました。

この記事は3記事目になります。ようやく終わりです。

6. this

this とは、関数のスコープ内で有効な値で、実行中の関数をプロパティもしくはメソッドとして保持しているオブジェクトへの参照である

一般的に、関数の中で使われる this は、その関数が格納されているオブジェクトを参照します。 new 演算子を使用する場合、 call()apply() で関数を実行する場合は例外。

this は、関数が実行時に呼び出される際のコンテクストに依存する

var foo = 'foo';
var myObject = { foo: "this is myObject.foo" };
var anotherObject = { foo: "this is anotherObject.foo" };

var sayFoo = function() {
  console.log( this.foo );
};

myObject.sayFoo = sayFoo;
anotherObject.sayFoo = sayFoo;
// myObject と anotherObject はそれぞれ同じ sayFoo() 関数を参照しているが
// 実行時に this がそれぞれ myObject, anotherObject を指し示すので、出力が異なる

sayFoo();  // foo
myObject.sayFoo();  // this is myObject.foo
anotherObject.sayFoo(); // this is anotherObject.foo

ECMA-262 3rd Edition では、入れ子関数内で this がグローバルオブジェクトを参照する

これは本書作者も「完全に方向を誤っている」と書いている。

var myObject = {
  func1: function() {
    console.log( this ); // 出力: myObject
    var func2 = function() {
      console.log( this ); // 出力: window
      var func3 = function() {
        console.log( this ); // 出力: window
      }();
    }();
  }
}

myObject.func1();

ECMA-262 5th Edition では修正される、ということが書いてあるが、私の手元で試してみたところ、最新のChrome( V8エンジンは5th editionのはずだが…)やFirefoxでも、入れ子になった関数内のthisはwindowを指した。

call() 関数での this は、第一引数のオブジェクト

var myObject = {};

var myFunction = function( p1, p2 ) {
  this.foo = p1;
  this.bar = p2;
  console.log( this );
}

console.log( myObject );  // この時点ではすっからかんのオブジェクト
myFunction.call( myObject, "foo", "bar" );  // myFunction を myObject のメソッドとして呼び出している
console.log( myObject );  // その結果、foo, bar プロパティが足された。

コンストラクタ関数内の this

コンストラクタ関数内で this は、新しく生成されるインスタンスへの参照として使用される。

var Person = function Person(age) {
  // this = new Object(); 新しいオブジェクトが生成される
  this.age = age;
  // return this; 的な事が行われる
};

var John = new Person( 27 );
console.log( John.age );

// 一方 `new` しなかった場合
var Paul = Person( 72 );

//console.log( Paul.age );  // TypeError: Cannot read property 'age' of undefined 
console.log( window.age );  // new しなかった場合は、function Person での this はグローバルオブジェクトを指す

プロトタイプメソッド内の this も同上

var Person = function( name ) {
  if( name ) this.name = name;
};

Person.prototype.getName = function() {
  return this.name; // この this はコンストラクタで作り出すインスタンスを参照する
}

Person.prototype.name = "Beatles";

var john = new Person( "John" );
var paul = new Person( "Paul" );
var beatles = new Person();

console.log( john.getName() );  // John
console.log( paul.getName() );  // Paul
console.log( beatles.getName() ); // beatles
// beatlesオブジェクト自体はnameプロパティを持たないので、プロトタイプチェーンを潜って 
// Person.prototype を this として name を解決した

7. スコープとクロージャ

スコープは関数実行時ではなく関数定義時に決められる -クロージャの基本-

関数実行の場所ごとに解決される this とは異なる。

var parentFunction = function() {
  var foo = "foo";
  return function() {
    console.log( foo ); // この無名関数定義時点の foo は parentFunction() スコープの foo
  }
}

var nestedFunction = parentFunction();  // parentFunction() が返す無名関数

nestedFunction(); // 出力: foo
// parentFunction() スコープに定義された foo 変数に、グローバルからアクセスしている。
// これがクロージャ

Developer Toolsで見るとこんな感じ

f:id:AMANE:20140628221550p:plain

クロージャを一言で表すと「スコープチェーンに存在する変数への参照を(囲い込んで)保持している関数」と言える

クロージャもう一つ

var countUp = function() {
  var count = 0;
  return function() {
    return ++count;
  };
}();  //無名関数が一度即時実行され、countUp に 関数が収められる

console.log( countUp() ); // 1 - var count は囲い込まれ、参照できている
console.log( countUp() ); // 2 - var count は囲い込まれ、参照できている
console.log( countUp() ); // 3 - var count は囲い込まれ、参照できている

8. 関数のprototypeプロパティ

関数にはprototypeプロパティが付く

正確に言うと、Function() インスタンスにprototypeプロパティが自動的に付与される

var myFunc = function( var1 ) {
  if( typeof var1 === "number" ) return var1 * var1;
}

var MyClass = function( name ) {
  this.name = name;
}

var myObj = new MyClass("John");

console.log( myFunc.hasOwnProperty("prototype") );  // true 関数にはprototypeプロパティがある
console.log( MyClass.hasOwnProperty("prototype") ); // true コンストラクタも関数である
console.log( myObj.hasOwnProperty("prototype") );   // false コンストラクタから作られたインスタンスにはない。

インスタンスオブジェクトにはコンストラクタ関数のprototypeプロパティが継承されてくる

(1/3) 回でも書きましたが、大事なことなのでもう一度。

Array.prototype.foo = function(){ console.log("foo"); };
var myArray = ['foo', 'bar'];
myArray.foo();  // ネイティブオブジェクトであるArrayに独自に foo() という関数を加えた。そんなArrayのインスタンス、myArray

// インスタンス.__proto__ が コンストラクタのprototype プロパティへのリンクになる
console.log( myArray.__proto__ === Array.prototype ); // true
// 先ほどの foo() 関数は、下記のようにプロトタイプチェーンを渡って解決される
console.log( myArray.__proto__.foo === Array.prototype.foo ); // trur

// (ついでに) インスタンス.constructor が コンストラクタへのリンクになる
console.log( myArray.constructor === Array ); // true
// (更についでに) 冗長(だが言語使用上正式)な foo関数までのたどり着き方
console.log( myArray.constructor.prototype.foo === Array.prototype.foo ); // true
コンストラクタ宣言時に関数名を与えるとちょっとだけ嬉しい
var Foo = function(){};

var fooInstance = new Foo();
console.log( fooInstance ); // Foo{}
console.log( fooInstance.constructor === Foo ); // true
console.l
console.log( fooInstance.constructor ); // function(){}

var Foo2 = function Foo2() {};  // コンストラクタの宣言時に関数名を与えている
var fooInstance2 = new Foo2();
console.log( fooInstance2.constructor ); // function Foo2(){} 関数名まで取得できている

しかしコンストラクタ宣言時に関数名をあたえて「いない」Foo も fooInstance.constructor === FoofooInstance instanceof Foo とすれば確認が取れる。正直、関数名を与えるメリットはそこまで大きくない。

コンストラクタの prototype プロパティを変更して、インスタンスを動的に変更可能

var Foo = function() {};

foo1 = new Foo();
foo2 = new Foo();

foo1.x = 1;
console.log( foo1.x, foo2.x );  // 出力: 1 undefined
// foo1 はxというプロパティを加えられた

Foo.prototype.x = 100;
console.log( foo1.x, foo2.x );  // 出力: 1 100
// foo1はもともとxというプロパティを持っているので、Foo.prototype.x を動的に加えても、プロトタイプチェーンのルールに則り foo1直下のxが採用される
// foo2はxというプロパティを持たないので、動的に加えられた Foo.prototype.x が参照される

delete Foo.prototype.x;
console.log( foo1.x, foo2.x );  // 出力: 1 undefined
// コンストラクタのprototypeプロパティを動的に削除。 元の状態に戻る。

継承のチェーンの基本系:SonClass.prototype = new ParentClass();

子クラスのprototypeを、親クラスのインスタンスで塗り替える。親クラスの機能を強制的に子クラスに持たせる手法。

var Person = function() {
  this.bar = 'bar';
}
Person.prototype.foo = 'foo';

var Chef = function() {
  this.goo = 'goo';
}
Chef.prototype = new Person();  // これで、Personクラスの全機能を含んだChefクラスが出来る
var Cody = new Chef();

console.log( Cody );

console.log( Cody.goo );  // goo
console.log( Cody.bar );  // bar
console.log( Cody.foo );  // foo

// プロトタイプチェーンはだいぶ複雑な感じに
console.log( Cody.__proto__ === Chef.prototype );
console.log( Cody.__proto__.bar === Chef.prototype.bar ); // Cody.bar の見つけ方
console.log( Cody.__proto__.__proto__.foo === Chef.prototype.__proto__.foo ); //Cody.foo の見つけ方

以上

3回にわたって、オブジェクトの振る舞いについて整理しました。

この本ではこの後、他のネイティブオブジェクトである

  • Array()
  • String()
  • Number()
  • Boolean()
  • null
  • undefined
  • Math 関数

についても細かく説明してくれています。

「何となく使えてしまっているけど実は結構独特なJavaScriptの言語仕様を整理して学ぶ」のに、非常に持ってこいな本だと思いました。今後も繰り返し読むことになりそうです。