オブジェクトメソッド, "this"

オブジェクトは通常、ユーザや注文などのような、実世界のエンティティを表現するために作られます。:

let user = {
  name: "John",
  age: 30
};

そして、実世界ではユーザは アクション することができます: ショッピングカードから何かを選んだり、ログイン、ログアウトなど。

アクションは、JavaScriptではプロパティの中で関数で表現されます。

メソッド例

スタートとして、user が Hello と言うようにしましょう:

let user = {
  name: "John",
  age: 30
};

user.sayHi = function() {
  alert("Hello!");
};

user.sayHi(); // Hello!

ここでは関数を作るために関数式を使い、それをオブジェクトの user.sayHi プロパティに代入しました。

その後、関数を呼ぶことができます。ユーザは今話すことができます!

オブジェクトのプロパティの関数は、メソッド と呼ばれます。

従って、ここではオブジェクト user のメソッド sayHi を作りました。

もちろん、次のように、宣言済みの関数をメソッドとして使うこともできます:

let user = {
  // ...
};

// 最初、宣言
function sayHi() {
  alert("Hello!");
};

// その後、メソッドを追加
user.sayHi = sayHi;

user.sayHi(); // Hello!
オブジェクト指向プログラミング

エンティティを表現するためにオブジェクトを使ってコードを書くとき、それは、object-oriented programming, 略すと “OOP” とばれます。

OOPは大きなものであり、それ自体の興味深い科学です。 正しいエンティティを選択するにはどうすればいいですか? どのようにそれらの間の相互作用を整理しますか?それはアーキテクチャーであり、それらは E.Gamma, R.Helm, R.Johnson, J.Vissides による"Design Patterns: Elements of Reusable Object-Oriented Software" または G.Booch による “Object-Oriented Analysis and Design with Applications” などのような、そのトピックについての素晴らしい本があります。私たちは、チャプター 記事 "object-oriented-programming" が見つかりません の後半でそのトピックの表面について触れます。

メソッドの短縮表現

オブジェクトリテラルでは、メソッドのための短縮構文があります:

// これらのオブジェクトは同じことをします

let user = {
  sayHi: function() {
    alert("Hello");
  }
};

// メソッド簡略化はスッキリ見えますね
let user = {
  sayHi() { // "sayHi: function()" と同じです
    alert("Hello");
  }
};

上の通り、"function" を除き、単に sayHi() と書くことができます。

実を言うと、この表記は完全に同一ではありません。オブジェクトの継承(後で説明します)に関して微妙な違いがあります。が、今のところは問題ありません。ほぼ全てのケースでこの短縮構文は好まれます。

メソッド中の “this”

オブジェクトメソッドが処理をするために、オブジェクトに格納されている情報にアクセスする必要があることは一般的です。

例えば、user.sayHi() の内側のコードが user の名前を必要とするかもしれません。

オブジェクトにアクセスするために、メソッドは this キーワードを使うことができます。

this の値はメソッドを呼び出すのに使われた “ドットの前” のオブジェクトです。

例:

let user = {
  name: "John",
  age: 30,

  sayHi() {
    alert(this.name);
  }

};

user.sayHi(); // John

ここで user.sayHi() の実行中、this の値は user になります。

技術的には、this なしでもオブジェクトへのアクセスも可能です – 外部変数経由での参照によって:

let user = {
  name: "John",
  age: 30,

  sayHi() {
    alert(user.name); // "this" の代わりに "user"
  }

};

…しかし、このようなコードは信頼できません。もし useradmin = user のように別の変数にコピーすることにし、何かでuserを上書きすると, 間違ったオブジェクトへアクセスすることになります。

次のような感じです:

let user = {
  name: "John",
  age: 30,

  sayHi() {
    alert( user.name ); // エラーにつながる
  }

};


let admin = user;
user = null; // 明らかにするために上書きします

admin.sayHi(); // Whoops! sayHi() の中で古い名前が使われました! エラーです!

もし alert の内側で、user.name の代わりに this.name を使うと、コードは動作します。

“this” はバインドされていません

JavaScriptでは、 “this” キーワードは他のほとんどのプログラミング言語とは異なる振る舞いをします。まず、どの関数にでも使えます。

このようなコードも構文エラーにはなりません:

function sayHi() {
  alert( this.name );
}

this の値は実行時に評価されます。そしてそれは何にでもなれます。

例えば、異なるオブジェクトから呼ばれた場合、同じ関数でも異なる “this” を持つ可能性があります:

let user = { name: "John" };
let admin = { name: "Admin" };

function sayHi() {
  alert( this.name );
}

// 2つのオブジェクトで同じ関数を使う
user.f = sayHi;
admin.f = sayHi;

// これらの呼び出しは異なる this を持ちます
// 関数の中の "this" は "ドット" の前のオブジェクトです
user.f(); // John  (this == user)
admin.f(); // Admin  (this == admin)

admin['f'](); // Admin (ドットでも角括弧でも問題なくメソッドにアクセスできます)

実際、オブジェクトまったくなしで関数を呼び出すこともできます:

function sayHi() {
  alert(this);
}

sayHi(); // undefined

このケースでは、 strict モードでは thisundefined になります。もし this.name にアクセスしようとするとエラーになります。

非 strict モード(誰かが use strict を忘れた場合)では、このようなケースでは this の値は グローバルオブジェクト (ブラウザでは window, 後ほど学びます)になります。これは "use strict" が修正した歴史的な振る舞いです。

一般的に、オブジェクトなしで this を使う関数の呼び出しは、普通ではなくプログラム上の誤りであることに注意してください。もし関数が this を持っていたら、それは通常オブジェクトコンテキストで呼ばれることを意味しています。

バインドしていない this の結果

もしあなたが別のプログラミング言語から来ていたら、恐らく "this のバインド" の考えに慣れているでしょう。それは、オブジェクトに定義されたメソッドは常にそのオブジェクトを参照する this を持っている、と言うものです。

JavaScriptでは、 this は “自由” です。その値は実行時に評価され、メソッドが宣言されている場所には依存せず、 “ドットの前の” オブジェクトが何であるか、に依存します。

実行時に評価される this の概念はプラスとマイナス両方を持っています。一方では、関数は異なるオブジェクトで再利用することができます。他方では、より大きな柔軟性はミスを導きやすいです。

ここで、我々のポジションはこの言語が決めたことが良いか悪いかを判断するものではありません。我々は、それをどうやって使うか、どうやって利益を得るか/問題を回避するかを理解することです。

内部: 参照型

詳細な言語機能

このセクションでは、特殊なケースをより理解するための高度なトピックについて説明します。

あなたが速く読み進めたいのであれば、スキップまたは別の機会に見てください。

複雑なメソッド呼び出しは、 this を失う可能性があります。例えば:

let user = {
  name: "John",
  hi() { alert(this.name); },
  bye() { alert("Bye"); }
};

user.hi(); // John (シンプルな呼び出しは動作します)

// 今、name に応じて user.hi または user.bye を読んでみましょう
(user.name == "John" ? user.hi : user.bye)(); // Error!

最後の行では、user.hiuser.bye を選択する三項演算子があります。このケースでは、結果は user.hi です。

メソッドは丸括弧 () ですぐに呼び出されます。しかし、それは正しく動きません!

呼び出しはエラーになります、なぜなら、呼び出しの内側の "this" の値は undefined になるからです。

これは動きます (オブジェクトドットメソッド):

user.hi();

これはダメです (評価されたメソッド):

(user.name == "John" ? user.hi : user.bye)(); // Error!

なぜでしょう?なぜそのようなことが起こるのか理解したい場合、obj.method() の呼び出しがどのように機能するのかを理解してみましょう。

よく見ると、 obj.method() 文に2つの操作があります:

  1. まず、ドット '.' がプロパティ obj.method を抽出します。
  2. 次に、丸括弧 () でそれを実行します。

そして、this についての情報は最初の処理から2つ目の処理へどのように渡されるでしょう?

それらの操作を別々の行に書いた場合、this が失われるのは明らかでしょう:

let user = {
  name: "John",
  hi() { alert(this.name); }
}

// メソッドの取得呼び出しを2行に分けます
let hi = user.hi;
hi(); // Error, this は undefined なので

ここで hi = user.hi は関数を変数の中においています。そして最後の行は完全に独立しています。なので、this がありません。

user.hi() 呼び出しを動作させるために、JavaScriptはトリックを使います – ドット '.' は関数ではなく、特別な参照型を返します。

参照型は “仕様上の型” です。私たちは明示的にそれを使うことはできませんが、言語の中で内部的に使われています。

参照型の値は、3つの値の組み合わせ (base, name, strict) です。ここで:

  • base はオブジェクトです。
  • name はプロパティです。
  • strictuse strict が効いている場合は true です。

user.hi へのプロパティアクセスの結果は、関数ではなく参照型です。strict mode での user.hi はこうなります:

// 参照型の値
(user, "hi", true)

参照型に対して丸括弧 () 呼び出しがされると、それらはオブジェクトとそのメソッドについての完全な情報を受け取り、正しい this (このケースでは user)をセットできます。

代入 hi = user.hi のような他の操作は、参照型を破棄し、user.hi(関数)の値を渡します。従って、それ以降の操作は全て this を “失います”。

なので、結果として、this の値は、関数がドット obj.method()、もしくは角括弧 obj[method]()構文を使って直接呼び出された場合のみ正しく渡されます。このチュートリアルの後半では、func.bind() など、この問題を解決するためのさまざまな方法を学びます。

アロー関数は “this” を持ちません

アロー関数は特別です: それらは “自身の” this を持ちません。もしこのような関数で this を参照した場合、外部の “通常の” 関数から取得されます。

例えば、ここで arrow() は外部の user.sayHi() メソッドから this を使います:

let user = {
  firstName: "Ilya",
  sayHi() {
    let arrow = () => alert(this.firstName);
    arrow();
  }
};

user.sayHi(); // Ilya

これはアロー関数の特別な機能です。別の this ではなく、外側のコンテキストから取り出したい場合に便利です。アロー関数ふたたびのセクションの後半では、より多くのアロー関数を扱います。

サマリ

  • オブジェクトのプロパティに格納されている関数は “メソッド” と呼ばれます。
  • メソッドを使うと、オブジェクトは object.doSomething() のように “振る舞う” ことができます。
  • メソッドはオブジェクトを this で参照することができます。

this の値は実行時に定義されます。

  • 関数が宣言されている場合、this を使うことができますが、その this は関数が呼び出されるまで値を持っていません。
  • その関数はオブジェクト間でコピーできます。
  • 関数が “メソッド” 構文で呼び出されたとき: object.method(), 呼び出し中の this の値は、object です。

アロー関数は特別であることに注意してください: それは this を持っていません。this がアロー関数の中でアクセスされるとき、それは外側から取得されます。

タスク

重要性: 2

このコードの結果はなんでしょう?

let user = {
  name: "John",
  go: function() { alert(this.name) }
}

(user.go)()

P.S. 落とし穴があります :)

エラーです!

やってみましょう:

let user = {
  name: "John",
  go: function() { alert(this.name) }
}

(user.go)() // error!

ほとんどのブラウザでのエラーメッセージは何を間違えているのか理解できません。

user = {...} の後にセミコロンがないため、エラーになります。

JavaScript 括弧 (user.go)() の前にはセミコロンを想定していないので、このようにコードを解釈します:

let user = { go:... }(user.go)()

そして、このようなジョイント式は構文的にはオブジェクト { go: ...} を引数 (user.go) をもつ関数として呼びだすことができます。また、それは let user と同じ行で起こります。なので、user オブジェクトはまだ定義されていないのでエラーになります。

セミコロンを挿入すると、すべてうまく行きます。:

let user = {
  name: "John",
  go: function() { alert(this.name) }
};

(user.go)() // John

(user.go) の周りの括弧はここではなにもしないことに注意してください。通常それらは操作の順番のために設定されますが、ここではドット . がとにかく最初に動作するので影響がありません。セミコロンだけが関係します。

重要性: 3

下のコードで、user.go() メソッドを4回連続で呼び出すつもりです。

しかし、呼び出し (1)(2)(3)(4) とは異なっています。なぜでしょう?

let obj, method;

obj = {
  go: function() { alert(this); }
};

obj.go();               // (1) [object Object]

(obj.go)();             // (2) [object Object]

(method = obj.go)();    // (3) undefined

(obj.go || obj.stop)(); // (4) undefined

説明します。

  1. これは通常のオブジェクトメソッド呼び出しです。

  2. 同じです。ここでは括弧は操作の順番を変更しません。ドットが最初です。

  3. ここにより複雑な呼び出し (expression).method() があります。この呼出しはまるで2行に分割されたかのようにして動作します。:

    f = obj.go; // 式を計算します。
    f();        // 持っているものを実行します

    ここで、f()this なしの関数として実行されます。

  4. (3) と似たようなもので、ドット . の左側に式を持っています。

(3)(4) の振る舞いを説明するために、プロパティ・アクセサ(ドットまたは角括弧)が参照型の値を返すことを思い出す必要があります。

メソッド呼び出し(代入 =||のような)以外の操作は、 this に設定できる情報を持たない通常の値にします。

重要性: 5

ここにオブジェクトを返す makeUser 関数があります。

その ref へのアクセス結果なんでしょう?それはなぜでしょう?

function makeUser() {
  return {
    name: "John",
    ref: this
  };
};

let user = makeUser();

alert( user.ref.name ); // 結果はなに?

答え: エラーです

やってみましょう:

function makeUser() {
  return {
    name: "John",
    ref: this
  };
};

let user = makeUser();

alert( user.ref.name ); // Error: Cannot read property 'name' of undefined

これは this をセットするルールがオブジェクトリテラルを見ないためです。

ここで makeUser() の中の this 値は undefined です。なぜなら、関数として呼ばれており、メソッドではないためです。

また、オブジェクトリテラル自身は this に影響しません。this の値は関数全体で、コードブロックやオブジェクトリテラルはそれに影響しません。

従って、ref: this は実際にはその関数の現在の this を取ります。

これは反対のケースです:

function makeUser() {
  return {
    name: "John",
    ref() {
      return this;
    }
  };
};

let user = makeUser();

alert( user.ref().name ); // John

これは動作します。なぜなら user.ref() はメソッドだからです。そして this の値はドット . の前のオブジェクトがセットされます。

重要性: 5

3つのメソッドをもつ calculator オブジェクトを作りなさい。:

  • read() は2つの値を聞き、オブジェクトプロパティとしてそれらを格納します。
  • sum() は保存した値の合計を返します。
  • mul() は保存した値を掛け、その結果を返します。
let calculator = {
  // ... your code ...
};

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

デモを実行

テストと一緒にサンドボックスを開く

let calculator = {
  sum() {
    return this.a + this.b;
  },

  mul() {
    return this.a * this.b;
  },

  read() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  }
};

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

サンドボックスでテストと一緒に解答を開く

重要性: 2

上下に移動できる ladder オブジェクトがあります。:

let ladder = {
  step: 0,
  up() {
    this.step++;
  },
  down() {
    this.step--;
  },
  showStep: function() { // 現在の段を表示します
    alert( this.step );
  }
};

さて、順番にいくつかの呼び出しをする場合、このようにできます:

ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1

updown のコードを修正して、連鎖可能な呼び出しができるようにしてください。:

ladder.up().up().down().showStep(); // 1

このようなアプローチはJavaScriptライブラリの中で広く使われています。

テストと一緒にサンドボックスを開く

解決策はすべての呼び出しでオブジェクト自身を返すことです。

let ladder = {
  step: 0,
  up() {
    this.step++;
    return this;
  },
  down() {
    this.step--;
    return this;
  },
  showStep() {
    alert( this.step );
    return this;
  }
}

ladder.up().up().down().up().down().showStep(); // 1

また、1行毎に1つの呼び出しで書くこともできます。長い連鎖の場合はより読みやすいです。:

ladder
  .up()
  .up()
  .down()
  .up()
  .down()
  .showStep(); // 1

サンドボックスでテストと一緒に解答を開く

チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。