[EffectiveJavaScript輪読会3]関数、メソッド、コンストラクタの、呼び出しの違いを理解する

GAS

どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。

前回のシリーズでは、第2章に相当する考察をお届けしました。

引き続き、第3章をお送りします。

目次と日程

第3章は「関数の扱い」です。いよいよJavaScriptの真髄に迫る感じです。

  • 第1章 JavaScriptに慣れ親しむ
  • 第2章 変数のスコープ
  • 第3章 関数の扱い
  • 第4章 オブジェクトとプロトタイプ
  • 第5章 配列とディクショナリ
  • 第6章 ライブラリとAPI設計
  • 第7章 並行処理

LT大会は2021年9月26日です。がんばりましょう。

今日はさっそく1回目で、「関数、メソッド、コンストラクタの、呼び出しの違いを理解する」 をお届けします。

テキスト第3章「関数の扱い」の項目18に対応しています。

今日のアジェンダ

  • 関数の利用パターン1(呼び出し)
  • 関数の利用パターン2(thisを使ったグローバル関数)
  • 関数の利用パターン3(コンストラクタ)

わたしもぼんやり思っていたことですが、他の言語と比べて、JavaScriptはなんでもかんでも関数に仕事をさせています。

たとえばVBAでは、戻り値を返すプロシージャのことを「関数」と呼び、処理をするだけのプロシージャとは区別しています。

ほかにも、Javaのクラスである、コンストラクタ(インスタンスを生成する処理)、メソッドなどは、関数とは別の処理です。

JavaScriptの関数は、具体的にどんな仕事をしているのでしょうか。

「そんなのあたりまえじゃないか」と思うことが多いと思いますが、その先に待ってるJavaScriptの重要な仕様と戦うための基礎固めです。

関数の利用パターン1(呼び出し)

JavaScrtiptのメソッドは、関数であるオブジェクトのプロパティにすぎません。

メソッド定義も、プロパティに無名関数が代入されていることの、糖衣構文(シンタクティックシュガー)です。

function myFunction3_18_02() {

  const obj = {

    userName: 'Bob',

    hello: function () {
      return `hello, ${this.userName}`;
    },

    greeting() {
      return `How are you, ${this.userName}`;
    }
  };

  console.log(obj);
  // { userName: 'Bob',
  //  hello: [Function: hello],
  //  greeting: [Function: greeting] }
 
}

メソッドHelloから、objのプロパティ[userName]にアクセスするために、thisを付けました。

thisは、書かれている場所で、オブジェクト自分自身を表しますが、このように、オブジェクトをコピーすると、興味深い結果が返ります。

つまり、書かれている場所ではなく、呼び出された場所、とも言えそうです。

function myFunction3_18_03() {

  const obj = {

    hello() {
      return `hello, ${this.userName}`;
    },
    userName: 'Bob'

  };


  const obj2 = {
    hello: obj.hello,
    userName: 'Tom'
  }

  console.log(obj2.hello()); //	hello, Tom

}

これは、関数の呼び出し式(Call Expression)という仕組みからきています。

メソッドを呼び出すと、Call Expressionがthisの結合を決めています 。

thisに結合される値は、レシーバ(receiver)とも呼ばれます。値[Tom]単体ではなく、オブジェクト[obj2]がレシーバです。

ルックアップという言葉はスコープチェーンの動きと同様に、名前結合をするために参照先を探しにいく とイメージしてください。

obj.hello()を実行したときの流れをみてみましょう。

  • objのhelloプロパティをルックアップする → オブジェクト[obj]のなかに、プロパティ[hello]があるかどうか探しにいく
  • thisがあるので、レシーバとしてthisをオブジェクト[obj]に結合する
  • レシーバを呼び出す

同様に、obj2.hello()を実行したときの流れをみてみましょう。

  • obj2のhelloプロパティをルックアップする → オブジェクト[obj2]のなかに、プロパティ[hello]があるかどうか探しにいく
  • プロパティ[hello]には、値[obj.hello]が定義されている
  • obj.hello(戻り値に[hello, ${this.userName}]をもつ関数)を返す
  • thisがあるので、レシーバとしてthisをオブジェクト[obj2]に結合する
  • レシーバを呼び出す

オブジェクトのメソッドを呼び出すと、オブジェクトのプロパティをルックアップし、そのときのオブジェクトがメソッドのレシーバとして使われます。

レシーバとして結合したオブジェクトに、結合する相手がいなかったときの、悲しいundefinedを理解しましょう。

function myFunction3_18_04() {

  const obj = {

    hello() {
      return `hello, ${this.userName}`;
    },
    userName: 'Bob'

  };


  const obj2 = {
    hello: obj.hello,
    // userName: 'Tom'
  }

  console.log(obj2.hello()); //	hello, undefined

}

関数の利用パターン2(thisを使ったグローバル関数)

このthisのはたらきを利用して、どこからでも呼び出せる関数を定義し、thisを置いてみます。

共通する処理を関数化するということで、便利かもしれません。

ただし、最終行のように関数単体で呼び出すと、undefinedが返ってしまうので、少々問題ありですね。

function myFunction3_18_05() {

  function hello() {
    return `hello, ${this.userName}`;
  }


  const obj1 = {
    hello: hello,
    userName: 'Bob'
  }

  const obj2 = {
    hello: hello,
    userName: 'Tom'
  }

  console.log(obj1.hello()); //	hello, Bob
  console.log(obj2.hello()); //	hello, Tom

  console.log(hello()); //hello, undefined

}

先ほど、単体で呼び出した関数では、レシーバにグローバルオブジェクトが結合されます。

グローバルオブジェクトにuserNameがあれば、結合しますが、このような書き方は、グローバル汚染につながり、決してよくありません。

function myFunction3_18_06() {

  //宣言をしないことで、グローバルオブジェクトのプロパティに格納しています
  userName = 'John';

  function hello() {
    return `hello, ${this.userName}`;
  }

  console.log(hello()); //hello, John

}

‘use strict’モードでは、thisのデフォルトの結合先がundefinedとなります。

function myFunction3_18_07() {

'use strict'

  //宣言をしないことで、グローバルオブジェクトのプロパティに格納しています
  userName = 'John';

  function hello() {
    return `hello, ${this.userName}`;
  }

  console.log(hello()); //ReferenceError: userName is not defined

}

use strictのリストに追加しておきましょう。

strictモードで効く主な厳格

  1. 暗黙的なグローバル変数の禁止
  2. 代入不可なプロパティへの代入の禁止
  3. 削除できないプロパティの削除の禁止
  4. 関数の引数名の重複の禁止
  5. 幾つかの識別子は予約語にするため使用禁止(staticとか)
  6. 8進数表記の禁止
  7. eval 変数、arguments 変数の宣言禁止
  8. with 禁止
  9. ブロックスコープ内の関数定義
  10. thisのデフォルト結合はundefined ← New

関数の利用パターン3(コンストラクタ)

JavaScriptのコンストラクタは、メソッドや関数と同じように、functionキーワードで定義できます。

new演算子によってインスタンスを生成するクラスは、V8以前の講座で習いましたね。

function myFunction3_18_08() {

  function User(name, passwordHash) {
    this.name = name;
    this.passwordHash = passwordHash;
  }

  const name = 'Ivy';
  const passwordHash = '0e0101';

  const u = new User(name, passwordHash);
  console.log(u); //{ name: 'Ivy', passwordHash: '0e0101' }

}

コンストラクタが実行されると、できたてホヤホヤのオブジェクトをthisとして渡します。

変数[u]には、Userから生成されたオブジェクトが格納されていますが、元々thisとして書かれていた場所には、できたてホヤホヤのオブジェクトが結合されています。

クラス構文が登場してから、JavaScriptのクラスと関数の関係性が見え辛くなりましたが、もとはこのような書き方をしていました。

そんなクラス構文も、functionキーワードで記述していた関数のただの糖衣構文であり、JavaScriptがクラスベースになったわけではありません。

JavaScriptはプロトタイプベースですが、あとでたっぷり仲良くなりましょう。

まとめ

以上で、「関数、メソッド、コンストラクタの、呼び出しの違いを理解する」をお届けしました。

thisは仲良くなりづらいものです。

しかし、このように関数と一緒に考えると、少し理解が深まった気がしますね。

むかし習ったことの復習もでてきて、楽しかったです。

次回は、「高階関数を快適に使えるようにしよう」 をお届けします。

このシリーズの目次

タイトルとURLをコピーしました