JavaScript

[JavaScript] オブジェクトの基礎


目的


  • JavaScriptのオブジェクトの基礎的な概念などについて整理します(すぐ忘れちゃうから)

  • オブジェクト指向とか、継承とか、クロージャーとかの詳しい話はしません(できません)

  • 最初から用意されているメソッドの詳しい説明とかもしません

  • 【追記】基本的に ES5 時代の話です。ES2015 以降では変わっているところもあると思います。


関数はオブジェクト

普通の関数定義はこんな感じ。

function hello() {

alert("hello, world.");
}

hello();

これは、以下の書き方とほぼ同じこと。

var hello = function() {

alert("hello, world.");
}

hello();

つまり無名の関数(=関数オブジェクト)を生成して、変数に代入している。変数に引数をつければ、関数が実行できる(今回は空の引数だけど)。

もちろん変数に入れられるだけでなく、引数に指定したり、戻り値で返したりもできる。


ほとんどのものはオブジェクト

JavaScriptにおける通常のオブジェクトは、メンバ(名前)と値を覚えることのできる入れ物で、他の言語でいうところのハッシュテーブルや連想配列みたいなものです。

オブジェクトを生成して、そこにメンバとその値を登録するのは以下のようにできます。

var obj = new Object();

// "var1" というメンバに "hello" という値を登録
obj.var1 = "hello";

// "func" というメンバに関数オブジェクトを登録
obj.func = function() {
alert("world");
}

alert(obj.var1);
obj.func();

ドット演算子ではなく連想配列風にもアクセスできます。

alert(obj["var1"]);

obj["func"]();

そして、メンバの値が関数オブジェクトであれば「メソッド」で、それ以外のオブジェクトなら「プロパティ」ということになります。


配列もオブジェクト

配列の生成や操作は、一般的にこんな感じ。

var array1 = new Array(3);

var array2 = new Array("foo", "bar", "baz");
var array3 = [ "abc", "def", "xyz" ];

array2[0] = "123";
alert(array3[2]);

配列は普通のオブジェクトとは違い、以下のような特別なプロパティやメソッドがあります。逆に言えば、それ以外は普通のオブジェクトとほぼ同じです。


  • lengthプロパティ : 配列のインデックス+1を返す。書き換えると、配列のサイズが変わる。

  • pop() : 配列の末尾を削除して、削除した要素を返す

  • push(value, ...) : 配列の末尾に値を追加

  • shift() : 配列の先頭を削除して、削除した要素を返す

  • unshift(value, ...) : 配列の先頭に値を追加

  • あとは省略(他にもいろいろあるよ)

var array = [ "foo", "bar", "baz" ];

alert(array.length); // -> 3

// 配列にも数値以外のメンバを登録可能
array["a"] = "b";
alert(array.length); // -> 3 のまま
// 数字じゃないメンバ名はインデックスだと判断されません

array[1000] = "xyz";
alert(array.length); // -> 1001
// 最大のインデックスが1000なので、lengthは1001になります

for (var o in array) {
alert(o + " = " + array[o]);
// 0 = "foo"
// 1 = "bar"
// 2 = "baz"
// 1000 = "xyz"
// a = b
}


オブジェクトリテラルでオブジェクト生成

オブジェクトへのメンバの登録はオブジェクトリテラルという形式でもできます。メンバ名の後ろにコロンと値を書き、カンマで区切って、中カッコで囲む形式です。

var obj = {

// var1 プロパティを登録
var1: "hello",

// func メソッドを登録
func: function() {
alert("world");
}
};


new でオブジェクト生成

new でもオブジェクトが生成できます。

new の後には関数オブジェクトを指定します。関数オブジェクト内では、新しく生成されたオブジェクトを this で参照できるので、オブジェクトの初期化ができます。

function Hello(word) {

this.name = word;
this.func = function() {
alert(this.name);
}
}
var aisatsu = new Hello("hello");
aisatsu.func(); // -> "hello"

ちなみに、newで生成されたオブジェクト(aisatsu)と関数オブジェクト(Hello)は別オブジェクトです。

一般的なオブジェクト指向的な役割に当てはめれば、関数オブジェクト(Hello)がコンストラクタで、生成されたオブジェクト(aisatsu)がインスタンスにあたると思います。

ちなみに、違う書き方もできます。

// 関数オブジェクトを変数に入れてから

var Hello = function(word) {
this.name = word;
this.func = function() {
alert(this.name);
}
}
var ohayou = new Hello("おはよう");
ohayou.func(); // -> "おはよう"

var konnichiwa = new Hello("こんにちは");
konnichiwa.func(); // -> "こんにちは"

// 関数オブジェクトを直接

var jumbo = new function(word) {
this.name = word;
this.func = function() {
alert(this.name);
}
}("じゃんぼ");
jumbo.func(); // -> "じゃんぼ"

※前述のとおり、関数もオブジェクトです。なのでHello内で生成しているfuncメソッドは、ohayouインスタンスとkonnichiwaインスタンスで(同じようなことをしているけど)別の関数オブジェクトになっています。

var Hello = function(word) {

this.name = word;
this.func = function() {
alert(this.name);
}
}

var ohayou = new Hello("おはよう");
var konnichiwa = new Hello("こんにちは");

alert(ohayou.func === konnichiwa.func); // false


プロトタイプチェーン

JavaScriptのオブジェクトには、プロトタイプという概念があります。

たとえば、new で生成されたオブジェクト(インスタンス ; 前の例では aisatsu)は、生成したオブジェクト(コンストラクタ ; 前の例では Hello)のことを覚えています。これはオブジェクトの特別な constructor プロパティに設定されています。

function Hello() {}

var hello = new Hello();
alert(hello.constructor.toString()); // -> "function Hello() {}"

そして、自オブジェクト(aisatsu)に存在しないメンバにアクセスされた場合には、覚えているコンストラクタのプロトタイプ(前の例では Hello.prototype)にそのメンバが存在するか探しに行きます。

※関数もオブジェクトなので、もちろんメンバを持てます。

function Hello(word) {

this.name = word;
}
Hello.prototype.func = function() {
alert(this.name);
}

var aisatsu = new Hello("hello, world");
aisatsu.func(); // -> "hello, world"

※こうすると、複数のインスタンス間で同じメソッド(=関数オブジェクト)を共通化できます。

function Hello(word) {

this.name = word;
}
Hello.prototype.func = function() {
alert(this.name);
}

var ohayou = new Hello("おはよう");
var konnichiwa = new Hello("こんにちは");

alert(ohayou.func === konnichiwa.func); // true

つまりコンストラクタは、自分が生成したインスタンスの雛形(プロトタイプ)を提供することができるわけです。

そしてプロトタイプ(Hello.prototype)も当然オブジェクトなので、もしそのオブジェクトを作ったコンストラクタがいるなら、そのコンストラクタのプロトタイプを覚えています。

このようにプロトタイプが連鎖していること(そしてそれを辿ること)を「プロトタイプチェーン」といいます。

もし明示的なコンストラクタがいなければ、暗黙のコンストラクタとしてObjectを覚えています。そして、Object.prototype までたどり着くとチェーンの終端となります。

prototype プロパティは好きに設定ができます。そこで以下のような感じで擬似的な継承が可能です。

function Animal() {

this.cry = "meow";
}
Animal.prototype.shout = function() {
alert(this.cry);
}

function Bird() {
this.cry = "sing";
}
Bird.prototype = new Animal(); // 継承

var niwatori = new Bird();
niwatori.shout(); // -> "sing"

※これは動作確認用の簡易的な継承の書き方です。実用レベルの継承の仕方については、自分で調べてね。


関数オブジェクト、再び


仮引数の数(length)

関数オブジェクトには、length というプロパティがあります。

これは、定義されている仮引数の数です。

function Abc(a, b, c) {}

alert(Abc.length); // -> 3


argumentsオブジェクト

関数の処理のなかでは、arguments というオブジェクトが使えます。

これは関数を呼び出したときの引数の情報が入っている(配列風の)オブジェクトです。

function Def() {

for (var o in arguments) {
alert(o + " = " + arguments[o]);
// 0 = Delta
// 1 = Echo
// 2 = Foxtrot
}
alert(arguments.length); // -> 3
}

Def("Delta", "Echo", "Foxtrot");

これを用いて、可変引数処理を行うこともできます。


コンストラクタ

実は関数オブジェクトにはコンストラクタがあります。

一般的に関数の生成は以下のようになると思います。

function hello(message) {

alert(message);
}

hello("word");

var hello = function(message) {

alert(message);
}

hello("word");

これは以下と同じことです。

var hello = new Function("message", "alert(message);");

hello("word");

Functionコンストラクタの引数は、0個以上の仮引数と、関数本体を文字列で指定したものです。

var sum = new Function("a", "b", "return a + b;");

alert(sum(1,2));


apply/callメソッド

Function コンストラクタのプロトタイプには以下のような特別なメソッドがあります。


  • Function.prototype.apply (thisArg, argArray)

  • Function.prototype.call (thisArg [ , arg1 , … ] ] )

どちらも、関数オブジェクトの実行をするものです。

違いは apply は引数として配列を指定し、call は引数に直接記載することです。

これらのメソッドは Function のプロトタイプにあるため、Functionで生成されたインスタンス(=関数オブジェクト)で使用することができます。

function ghi(arg1, arg2) {

alert(arg1 + arg2);
}

ghi.apply(null, ["a", "b"]);
ghi.call(null, "x", "y");

先頭の thisArg は、関数オブジェクト内での this でアクセスできるオブジェクトを指定します。null や undefined を指定した場合にはグローバルオブジェクトが this になります(ブラウザの場合には Window オブジェクト)。

function jkl(arg1, arg2) {

alert(this + arg1 + arg2);
}

// 意味はないが this を置き換えて呼び出す
var string = "abc";
jkl.call(string, "x", "y"); // -> "abcxy"

JavaScript でコンテキストによって this の示すものが違うことがありますが、この apply()/call() で渡しているものが、状況によって異なるからです。


スコープチェーン


変数のスコープ

変数の種類(スコープ)には「グローバル変数」と「ローカル変数」があります。


  • 関数の中でvarを使って変数を定義 →ローカル変数

  • 関数の中で varを使わず 変数を定義 →グローバル変数

  • 関数の外で変数を定義 → グローバル変数

またローカル変数のスコープは関数単位だけで、ブロック単位ではありません。関数内のどこで書いても、関数の先頭で定義しているのと同じ意味です。

function foo() {

for (var o in arguments) {
var x = arguments[o];
}
alert(x); // -> "c"
}

foo("a", "b", "c");


変数スコープもオブジェクト

関数内のローカル変数は、特別なオブジェクト(=つまり連想配列)で管理されています。この特別なオブジェクトを変数オブジェクトといいます。

関数が実行される場合には、自動的に関数オブジェクト内に変数オブジェクトが生成されます。そしてローカル変数を定義したら、それは変数オブジェクトに新しいメンバを追加することと同じです。

また、ローカル変数はその外側の変数も参照することができます。

function Foo() {

// Fooの変数オブジェクトが生成されている
var count = 0;
return function() {
// 無名関数の変数オブジェクトが生成されている
count++;
return count;
}
}

var foo = Foo();
alert(foo()); // -> 1
alert(foo()); // -> 2

上記の例では、無名関数内からFoo関数内のローカル変数に参照ができています。

変数オブジェクトはその外側の変数オブジェクトの参照を持っており、自分の変数オブジェクトにない変数については、外側の変数オブジェクトから探します。

関数オブジェクトは自分の変数オブジェクトを持っており、変数オブジェクトはその外側の変数オブジェクトへの参照を保持しています(たとえ、外側の関数の実行が終了した後でも)。これでクロージャを実現しているわけです。

もちろん、外側の変数オブジェクトになければ、さらに外側の変数オブジェクトに探しにいきます。

最終的な一番外側の変数オブジェクトは、グローバルオブジェクトそのものになります(ブラウザの場合には Windowオブジェクト)。

なので、グローバル変数はどこからでも見えるローカル変数という言い方もできますね。

// ★グローバルオブジェクトがグローバル変数の変数オブジェクトとなっている

function Foo() {
// Fooの変数オブジェクトが生成されている
var count = 0;
return function() {
// 無名関数の変数オブジェクトが生成されている
count++;
return count;
}
}

var foo = Foo();
alert(foo()); // -> 1
alert(foo()); // -> 2