2022年5月22日

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

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

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

そして、現実世界では、ユーザーは行動できます。ショッピングカートから何かを選択したり、ログインしたり、ログアウトしたりなどです。

JavaScriptでは、プロパティ内の関数によってアクションを表します。

メソッドの例

まず、userに挨拶をさせましょう。

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

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

user.sayHi(); // Hello!

ここでは、関数式を使用して関数を作成し、オブジェクトのプロパティuser.sayHiに割り当てました。

その後、user.sayHi()として呼び出すことができます。ユーザーは話すことができるようになりました!

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

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

もちろん、このように事前に宣言された関数をメソッドとして使用することもできます。

let user = {
  // ...
};

// first, declare
function sayHi() {
  alert("Hello!");
}

// then add as a method
user.sayHi = sayHi;

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

エンティティを表すオブジェクトを使用してコードを作成する場合、それはオブジェクト指向プログラミング(略して「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」など、このトピックに関する優れた書籍があります。

メソッドの簡略表記

オブジェクトリテラルには、メソッドのより短い構文が存在します。

// these objects do the same

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

// method shorthand looks better, right?
user = {
  sayHi() { // same as "sayHi: function(){...}"
    alert("Hello");
  }
};

示されているように、"function"を省略してsayHi()と書くことができます。

正直に言うと、表記は完全に同一ではありません。オブジェクトの継承(後で説明します)に関連する微妙な違いがありますが、今のところ問題ではありません。ほとんどの場合、短い構文が優先されます。

メソッド内の"this"

オブジェクトメソッドが、その仕事をするためにオブジェクトに保存されている情報にアクセスする必要があることは一般的です。

たとえば、user.sayHi()内のコードは、userの名前を必要とする場合があります。

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

thisの値は、オブジェクト「ドットの前」、メソッドの呼び出しに使用されたオブジェクトです。

たとえば

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

  sayHi() {
    // "this" is the "current object"
    alert(this.name);
  }

};

user.sayHi(); // John

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

技術的には、外部変数を通じて参照することで、thisなしでオブジェクトにアクセスすることもできますが…

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

  sayHi() {
    alert(user.name); // "user" instead of "this"
  }

};

…このようなコードは信頼性がありません。userを別の変数(例:admin = user)にコピーし、userを別のものに上書きすると、間違ったオブジェクトにアクセスします。

以下に示します。

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

  sayHi() {
    alert( user.name ); // leads to an error
  }

};


let admin = user;
user = null; // overwrite to make things obvious

admin.sayHi(); // TypeError: Cannot read property 'name' of null

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 );
}

// use the same function in two objects
user.f = sayHi;
admin.f = sayHi;

// these calls have different this
// "this" inside the function is the object "before the dot"
user.f(); // John  (this == user)
admin.f(); // Admin  (this == admin)

admin['f'](); // Admin (dot or square brackets access the method – doesn't matter)

ルールは簡単です。obj.f()が呼び出されると、fの呼び出し中はthisobjになります。したがって、上記の例ではuserまたはadminのいずれかになります。

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

オブジェクトなしで関数を呼び出すこともできます。

function sayHi() {
  alert(this);
}

sayHi(); // undefined

この場合、厳格モードではthisundefinedです。this.nameにアクセスしようとすると、エラーが発生します。

厳格モードではない場合、そのような場合のthisの値はグローバルオブジェクト(ブラウザではwindow、この章の後半でグローバルオブジェクトで説明します)になります。これは、"use strict"で修正された履歴的な動作です。

通常、このような呼び出しはプログラミングエラーです。関数内にthisがある場合、オブジェクトコンテキストで呼び出されることを期待しています。

バインドされていない"this"の結果

他のプログラミング言語を使用している場合、オブジェクトで定義されたメソッドは常にそのオブジェクトを参照する「バインドされたthis」という考え方に慣れている可能性があります。

JavaScriptでは、thisは「フリー」であり、その値は呼び出し時に評価され、メソッドが宣言された場所ではなく、どのオブジェクトが「ドットの前」にあるかに依存します。

実行時に評価されるthisの概念には、長所と短所があります。一方では、関数を異なるオブジェクトに対して再利用できます。他方では、柔軟性の向上により、ミスが発生する可能性が高くなります。

ここでは、この言語設計の決定が優れているか悪いかを判断することはしません。それを使ってどのように機能するか、どのように利点を得て問題を回避するかを理解します。

アロー関数には"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 ); // What's the result?

回答:エラー。

試してみる

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の値は関数全体に対して1つであり、コードブロックやオブジェクトリテラルは影響しません。

したがって、ref: thisは実際には関数の現在のthisを取得します。

関数を書き直し、undefined値を持つ同じthisを返すことができます。

function makeUser(){
  return this; // this time there's no object literal
}

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

ご覧のとおり、alert( makeUser().name )の結果は、前の例からのalert( user.ref.name )の結果と同じです。

これが反対のケースです。

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

let user = makeUser();

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

これで機能します。user.ref()はメソッドだからです。そして、thisの値はドット.の前にあるオブジェクトに設定されます。

重要度:5

3つのメソッドを持つオブジェクトcalculatorを作成します。

  • read()は2つの値を要求し、それぞれabという名前のオブジェクトプロパティとして保存します。
  • 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() { // shows the current step
    alert( this.step );
  }
};

ここで、複数の呼び出しを連続して行う必要がある場合、次のように行うことができます。

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

次のように呼び出しをチェーンできるように、updownshowStepのコードを変更します。

ladder.up().up().down().showStep().down().showStep(); // shows 1 then 0

このようなアプローチは、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().showStep().down().showStep(); // shows 1 then 0

1行につき1つの呼び出しを書くこともできます。長いチェーンの場合、より読みやすくなります。

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

サンドボックスでテストを含む解答を開きます。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善点の提案がある場合は、コメントする代わりにGitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 数行のコードを挿入するには<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合はサンドボックス(plnkrjsbincodepen…)を使用してください。