k-tokitoh

2024-12-25

ずっとthisがわからない

ふつうに呼び出すと、インスタンスに bind される。

class C {
	v = 'C';

	m() {
		console.log(this?.v);
	}
}

const c = new C();
c.m(); // C

コールバックは通常、this の値が undefined で(オブジェクトに関連付けずに直接)呼び出されます。 https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/this

class C {
	v = 'C';

	m() {
		console.log(this?.v);
	}
}

const c = new C();
[0].forEach(c.m); // undefined

通常はこれを避けるためにアロー関数をつかえばいい。

class C {
	v = 'C';

	m() {
		console.log(this?.v);
	}
}

const c = new C();
[0].forEach(() => c.m()); // C

しかし今回ある件でコードを書いていて、再帰的にrequestAnimationFrame()を実行したく、呼び出しごとに引数となるアロー関数を生成する処理コストを抑えたいと思った。(実際どれくらいパフォーマンス影響があるかは分かっていない。)

以下は呼び出し側でアロー関数による bind をつかわない、という制約のもとでの話をする。


今回はあるクラスのインスタンスメソッドから、別のインスタンスメソッドを callback として利用したかった。その場合はどうなるか。

class C {
	v = 'C';

	m() {
		console.log(this?.v);
	}

	n() {
		console.log(this?.v);
		[undefined].forEach(this.m);
	}
}

const c = new C();
c.n(); // C undefined

同一クラスのインスタンスメソッドだとしても、コールバックとして扱う限り this は undefined になってしまうらしい。


対応を 3 つ示す。

1. 呼び出す側で bind する

「this の指定は呼び出し側が決定する」という原則に沿った対応。

class C {
	v = 'C';

	m() {
		console.log(this?.v);
	}

	n() {
		console.log(this?.v);
		[0].forEach(this.m.bind(this));
	}
}

const c = new C();
c.n(); // C C

ただし毎回 bind()を呼び出してしまう点が気になる。

2. 呼び出される側で bind する

予め bind しておく。ただしメソッドの宣言ではできないので constructor に書く。

class C {
	v = 'C';

	constructor() {
		this.m = this.m.bind(this);
	}

	m() {
		console.log(this?.v);
	}

	n() {
		console.log(this?.v);
		[0].forEach(this.m);
	}
}

const c = new C();
c.n(); // C C

m の面倒を constructor がみるという点に、処理が散在している印象を受ける。できれば m の宣言部分のみで完結させたい。

3. メソッドではなくアロー関数をプロパティとする

そしたらインスタンスを bind してくれるよう。

class C {
	v = 'C';

	m = () => {
		console.log(this?.v);
	};

	n() {
		console.log(this?.v);
		[0].forEach(this.m);
	}
}

const c = new C();
c.n(); // C C

ただしこれだと、インスタンスを生成するたびに関数も生成されてしまうというデメリットがある。

const c1 = new C();
const c2 = new C();
console.log(c1.m === c2.m); // false
console.log(c1.n === c2.n); // true

今回はパフォーマンス懸念がある話だったので、pros/cons 見比べて 2 にしてみた。

this はずっとわからない。