JavaScript学習2

thisが難しい...

Javascriptは実行コンテキストのオブジェクトが引数thisとしてメソッドへ暗黙的に渡させるのは理解できる。 (Ruby も同じだから)

const Bob = {
  name: "Bob",
  greeting() {
    console.log(this);
    console.log(`Hello! My name is ${this.name}`);
  },
};

Bob.greeting();

// { name: 'Bob', greeting: [Function: greeting] }.  thisとして渡されているのが分かる!!
// Hello! My name is Bob

しかし、JavaScriptではメソッド以外からでもthisを呼ぶことができるし、thisを呼び出し側から任意のオブジェクトに 指定して関数を実行することもできる。

callメソッド

func.call([thisArg[, arg1, arg2, ...argN]])

// thisArg: funcが呼び出された際、thisとして渡される値。
// arg1..argN: 引数

applyメソッド

func.apply(thisArg, [ argsArray])

// thisArg: funcが呼び出された際、thisとして渡される値。
// argArray:  配列として渡される引数
function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price); // this = Food {}
  this.category = 'food';
}

console.log(new Food('cheese', 5).name); //cheese

const Person = function (name) {
  this.name = name;

  return this;
};

const Kazu = Person.call({ gender: "m" }, "Kazu"); // { gender: "m", "Kazu"}をthisとして渡す
console.log(Kazu);

上の挙動を見るとthisを変数ではなく、呼び出し側から渡される引数と考えるのが自分の学習していた本では イメージし易いと書かれていていた。

よって、call()やapply()を使わずに呼ばれた場合の暗黙的に渡されたthisが示しているのは、
その実行コンテキストのオブジェクトと考えるのが素直に理解できるらしい...

thisの4つのパターン

new演算子をつけて呼び出した時 => 新規制生成されるオブジェクト

new 演算子 - JavaScript | MDN

this - JavaScript | MDN

関数に対して実行した場合、その関数のprototypeを継承する、新しいオブジェクトを生成する。 次にそれを関数に暗黙の引数thisとして渡し、最後にその関数がreturn thisで終わっていない場合は代わりにそれを実行する。

> const foo = function () { console.log('this is', this); };
> const bar = new foo();  // foo {} が暗黙的に渡される
this is foo {}
undefined
> bar
foo {}
> foo.prototype
{}
> foo === bar // アドレスを共有しない新しいオブジェクト
false

メソッドとして実行された時 => その所属するオブジェクト

メソッドと実行された場合、そのアクセス演算子.の前のオブジェクトがthisとして渡される。

this - JavaScript | MDN

const foo = {
  name: "Foo Object",
  dump() {
    console.log(this);
  },
};

foo.dump(); // { name: 'Foo Object', dump: [Function: dump] }

それ以外の関数 [非ストリクトモード] => グローバルオブジェクト

Node.jsであればglobal オブジェクト。 Global object (グローバルオブジェクト) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN

> this
<ref *1> Object [global] {
  global: [Circular *1],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
  },
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
  }
}

ブラウザであればwindowオブジェクト

Window - Web API | MDN

Image from Gyazo

それ以外の関数[ストリクトモード] => undefined

非ストリクトモードだと下記のように簡単にグローバルオブジェクトが汚染される。

> const Person = function (name) { this.name = name; return this }
> Person('somebody') // new演算子なしで実行
<ref *1> Object [global] {
  global: [Circular *1],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
  },
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  se3tImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Function (anonymous)]
  },
  name: 'somebody' // グローバルオブジェクトが汚染!!
}
> name
'somebody'

この安全性を解消するための仕様として、「Strcit モード」が導入された。 Strict モード - JavaScript | MDN

関数でStrictモードを呼び出すには、関数本体の最初に'user strict'を記述する。

> const Person = function (name) { 'use strict'; this.name = name; return this; }
> Person('somebody')
Uncaught TypeError: Cannot set property 'name' of undefined  // thisがundefinedになる!!
    at Person (REPL6:1:58)

クラス構文では自動的にstrictモードが有効になっており、そのコンストラクトもnew演算子をつけないと 実行できないようになっている。

> class Foo { constructor() { console.log('this is', this); }}
> Foo() // error
Uncaught TypeError: Class constructor Foo cannot be invoked without 'new'
> new Foo()
this is Foo {}
Foo {}

このstrictモードで安全性が増した反面、thisがその関数の実行コンテキストのオブジェクトであるという仕様を 崩してしまったということでもある。

class構文でのthisの挙動の問題点と対処法

問題点

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    const doIt = function () {
      console.log(`Hi, I'm ${this.name}`);
    };
    doIt();  // この関数内で呼び出されるthisはstcirtモードの為、undefinedになる。
  }
}

const minky = new Person("Momo");
minky.greet()  // TypeError: Cannot read property 'name' of undefined

エラーが起きる理由としてはメソッドgreet()内で定義された関数はただの関数で、そのオブジェクトの実行コンテキスト内にない。 そして、クラス構文のためstrictモードが有効になっており、関数doItでのthisへのアクセスはundefinedになる。

このthisを期待するオブジェクト({ name: 'xxx' })にするには以下の4つの方法がある。

bind()で関数にthisを束縛する。

Function.prototype.bind() - JavaScript | MDN

bind(thisArg, arg1, arg2, ..., argN)

thisArg: バインドされた値が呼び出される際、this引数として渡される。
argN: thisArgと一緒に渡される引数
class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    const doIt = function () {
      console.log(`Hi, I'm ${this.name}`);
    };
    const bindDoIt = doIt.bind(this); // bind()に{ name: 'xxx' }をthisとして渡す。
    bindDoIt();
  }
}

const minky = new Person("Momo");
minky.greet(); // 'Hi Momo"

call(), apply()を使用して、thisを指定して実行する。

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    const doIt = function () {
      console.log(`Hi, I'm ${this.name}`);
    };
    doIt.call(this);
  }
}

const minky = new Person("Momo");
minky.greet();

thisを一時変数に代入する

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    let _this = this;  // 一時変数に代入
    const doIt = function () {
      console.log(`Hi, I'm ${_this.name}`);
    };
    doIt();
  }
}

const minky = new Person("Momo");
minky.greet();

アロー関数で定義する

アロー関数は暗黙の引数としてのthisを持たず、thisを参照すると関数の外のスコープのthisの値を参照する。

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    const doIt = () => {         // アロー関数で定義
      console.log(`Hi, I'm ${this.name}`);
    };
    doIt();
  }

  greet_1 = () => {  // メソッド自身もアロー関数で定義
    const doIt = () => {
      console.log(`Hi, I'm ${this.name}`);
    };
    doIt();
  };
}

const minky = new Person("Momo");
minky.greet();

結論

  • thisはクラス構文でしか使用しない。
  • クラス構文では、メソッドを含めたあらゆる関数の定義をアロー関数で行う。