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演算子をつけて呼び出した時 => 新規制生成されるオブジェクト
関数に対して実行した場合、その関数の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
として渡される。
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
オブジェクト
それ以外の関数[ストリクトモード] => 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
はクラス構文でしか使用しない。- クラス構文では、メソッドを含めたあらゆる関数の定義をアロー関数で行う。