2021年10月16日

オブジェクトメソッド, "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() という形式で呼び出すことができます。これでユーザが話せるようになりました!

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

つまり、ここでは 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” などです。

メソッドの短縮表現

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

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

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() {
    // "this" は "現在のオブジェクト"
    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 の値は、実行時にコンテキストに応じて評価されます。

例えば、ここでは同じ関数が2つの異なるオブジェクトに割り当てられており、呼び出しの際に異なる "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 (ドットでも角括弧でも問題なくメソッドにアクセスできます)

ルールはシンプルです。obj.f() が呼び出されると、f の呼び出し中は thisobj です。つまり、上の例では user または admin となります。

オブジェクトなしでの呼び出し: this == undefined

オブジェクトがなくても関数を呼び出すことができます:

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 の概念は、プラスとマイナスの両方を持っています。一方では、1つの関数を異なるオブジェクトで再利用することができます。他方では、より大きな柔軟性は、ミスの招きやすさにつながります。

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

アロー関数は “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 がアロー関数の中でアクセスされるとき、それは外側から取得されます。

タスク

重要性: 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
let ladder = {
  step: 0,
  up: function() {
    this.step++;
    return this;
  },
  down: function() {
    this.step--;
    return this;
  },
  showStep: function() {
    alert(this.step);
  }
};

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

チュートリアルマップ