JS基礎いろいろーthisキーワード
全てはこの問題から始まっています。ブラウザー環境だと想定し、出力結果は何でしょうか。
// ブラウザー
var a = 1
;(function() {
console.log(a + this.a)
var a = '2'
console.log(a + this.a)
})()
var name = 1
;(function() {
console.log(name + this.name)
var name = '2'
console.log(name + this.name)
})()
this
の謎
this
キーワードが、JSという言語の中で非常に複雑な概念の一つ(の一つ、他にも色々とある)。経験者でも少しでも注意しないと間違ってしまうところです。this
は一体何を指しているのか、なぜこんなに混乱をもたらすか、今回は少し研究してみたいと思います。
内容について主にこちらの本を参考にしています。
詳細を見る前に、まずはJSのthis
に対するいくつかよくある誤解を先に説明します。
誤解その一:インスタンスを指している
上記の例を見ると、一般的なOOP言語のように、self
とかthis
とかで、インスタンスを指しているので、渡されたコンテキスト(実行対象)もインスタンスなのではないかと。
これがJSのthis
に対するよくある誤解の一つです。JSは本質的にOOP言語ではありません(機能は実現できそうですが)。class
などもES6以降で追加されたシンタクスシュガー(syntax sugar)に過ぎず、結局オブジェクトとなっています。インスタンスを指しているのは、表面的にそう見えるだけです。
class Car {
constructor(maker, model) {
this.maker = maker
this.model = model
}
describe() {
return this.maker + ' ' + this.model
}
}
let myCar = new Car('Honda', 'Civic')
// classが導入される前はコンストラクタ関数という方法で擬似的にクラスを作っています
function Car(maker, model) {
this.maker = maker
this.model = model
// こちらは機能しますが毎回newでオブジェクトを作るときにこの関数を作り直しています
// this.describe = function() {
// return this.maker + ' ' + this.model
// }
}
// classで定義されたメソッドは実際にCarのプロトタイプに追加されています
Car.prototype.describe = function() {
return this.maker + ' ' + this.model
}
var myCar = new Car('Honda', 'Civic')
上記の例のように、class
というキーワードがJS言語設計当初からついているわけではありません。それまでに、擬似的にクラス・インスタンスを実現するには、関数オブジェクトで行われています。JSのclass
は結局、関数オブジェクトに過ぎません。
そのため、this
もインスタンスを指す、というようなことは誤解です。そもそも、OOPで一般的に言われるinstance
という概念は適応できないからです。なぜインスタンスに見えるかは、後のnew
演算子のところで説明します。
誤解その二:実行関数自身を指している
JSのすべての関数がオブジェクトです(もちろん、関数以外もすべてオブジェクト)。インスタンスという概念自体が適応できなければ、this
は実行する関数を指しているのではないかと、推測もできます。
これも誤解です。次の例を見ると:
function foo(num) {
console.log('foo: ' + num)
return this.count++
}
foo.count = 0
var i
for (i = 0; i < 5; i++) {
foo(i)
}
console.log(foo.count) // 0
関数オブジェクトfoo
に、属性count
を0に設定して、foo
を5回実行します。そしてfoo.count
を参照して、foo
が実行された回数を出力します。すると、foo.count
は0になります。もしthis.count
をfoo.count
に修正すると、結果は正しくなります。つまり、this
は関数オブジェクト自身を指しているわけではありません。
誤解その三:実行関数のスコープを指している
これは結論から言うとケースバイケースですが、根本的に言えば、「スコープ」に指すことが不可能です。
例えば:
function foo() {
var a = 2
console.log(this.bar) // ここは一応barを呼び出すことが可能
this.bar()
}
function bar() {
console.log(this.a) // ここのthisを呼び出されたfooの関数のスコープへ指すことが不可能
}
foo()
// function bar() {
// console.log(this.a)
// }
// undefined
foo
関数のスコープには、bar
が存在するため、this.bar
がbar
関数を指しています。this.bar()
で実行されるとき、スコープがfoo
関数となったため、this.a
はfoo
関数内のa
になるはずですが、結果はundefined
となります。
console.log(this.bar)
だけを見ると、スコープじゃないかと言う錯覚がありますね。ただ実際使用中は、同じスコープにある関数を呼び出すときに、this
をつけず、関数の名前そのままで呼び出すのが正しいやり方です。
this
は何を指しているのか
ならthis
の指しているものは動的に変化します。関数の定義場所と関係なく、実行方式と深く関わっており、指向は一定ではありません。関数が呼び出されるときに、一つ実行コンテキスト(execution context)が作られます。そのコンテキストには、関数実行のコールスタック、関数実行の方式(how)、関数の引数などの情報が含まれています。その中に、関数が実行された時点でthis
が指すものとの情報も存在します。
this
を理解する第一歩として、関数の実行場所(call-site)が鍵となる、ということを先に念頭におきましょう。
this
の指向のケース
関数の実行場所(call-site)
関数の実行場所というのは、関数の定義(declared)された場所とは違い、呼び出される時の場所です。
実際のプログラムでは、関数の呼び出しは複雑な環境になっているので、どの関数がどの関数の中で呼び出されたのかがわかりにくいことがあります。少し専門用語的に言えば、コールスタックで実行されている関数の、一個前に実行された関数が、実行中の関数の実行場所となります。具体例を見た方がわかりやすいかもしれません:
function baz() {
// コールスタック: `baz`
// コールサイト: global scope
console.log( "baz" );
bar(); // <-- `bar`のコールサイト: baz
}
function bar() {
// コールスタック: `baz` -> `bar`
// コールサイト: `baz` 関数内
console.log( "bar" );
foo(); // <-- `foo`のコールサイト: bar
}
function foo() {
// コールスタック: `baz` -> `bar` -> `foo`
// コールサイト: `bar` 関数内
console.log( "foo" );
}
baz(); // <-- `baz`のコールサイト: global scope
こちらの例をvscodeのデバグモードで確認することが可能です。
最初のbaz
実行時のコールスタックに、<anonymous>
と書いてありますが、これはこのファイルを実行する名前のない関数となります。この匿名関数が、baz
の実行場所となります。次にbaz
の中でbar
が実行し、スタックにbaz
が追加されます。つまり、bar
の実行場所はbaz
です。他も同様です。
this
のデフォルト値
実行の場所をわかったら、this
の値はどうなるのかをデバグツールで確認することも可能です。
baz
実行時のthis
は、Object
となっていますが、その後の関数はすべてglobal
となっています。
this:Object
の状況について、vscodeのデバグツールがnode.js環境となっていますので、ファイルで実行する場合、ファイル(モジュール)内のthis
はglobal
ではなく、{}
となります。モジュールの中でグローバルオブジェクトへアクセスするには、他にglobalThis
との変数(ブラウザーとNode.js共通)があります。ブラウザー環境では、baz
のところのthis
はwindow
となります。
上記の例を見ると、関数の中でthis
を使うときは、this
がglobal
であることを確認することができます。
ならglobal
とは何か。 MDN によると:
グローバルオブジェクトは、グローバルスコープ上に常時存在するオブジェクトです。
- ウェブブラウザーでは、明示的にバックグランドタスクとして起動されるコードを除き、
Window
がグローバルオブジェクトになります。ウェブにおける JavaScript コードのほとんどはこのケースに該当します。- Worker 内で実行されるコードでは
WorkerGlobalScope
オブジェクトがグローバルオブジェクトになります。- Node.js で実行されるスクリプトの場合、
global
と呼ばれるオブジェクトがグローバルオブジェクトになります。
要するに、jsの実行環境の中で存在する、どこからもアクセス可能(=グローバル)な変数、関数、属性などを持っているオブジェクトです。例えば、ブラウザーだと、setTimeout
とclearTimeout
、setInterval
とclearInterval
、console
などがグローバルオブジェクトに含まれています。これらの関数は、どこからでも呼び出すことができます。vscodeのデバグ環境はNodejsなので、ここのthis
はwindow
ではなく、global
となっています。
話に戻りますが、関数を呼び出すときに、特別な事情がなければ、関数内のthis
は常にグローバルオブジェクト(具体的はjs環境による)を指しています。この状況を一番普遍的で、デフォルトのケースだと認識して良いでしょう。
strict mode
上記のデフォルトケースには、例外があります。strict mode
を使うと、デフォルトのthis
バイディングが禁止されるため、グローバルオブジェクトへ指すことができず、undefined
となります。
function baz() {
"use strict";
console.log( this.a );
}
var a = 2
baz() // Uncaught TypeError TypeError: Cannot read properties of undefined (reading 'a')
ただ、注意したいのは、strict mode
の制限というのは、そのスコープ内しか効きません。例えば:
function baz() {
console.log( this.a );
}
var a = 2
;(function () {
'use strict'
console.log('this in strict mode', this) // undefined
baz() // 2
})()
iife
関数内部では、strict mode
が有効になっているため、this
はundefined
となります。ただ、その外部ではstrict mode
が無効になっているため、this
はグローバルオブジェクトを指しており、this.a
が2となります。
通常ではstrict mode
とノーマルモードの混在は避けるべきですが、サードパーティのパッケージとかを使うときに自分でコントロールできないので、ここの違いも注意する必要があります。
this
バイディング(implicit binding)
暗黙的なデフォルトのthis
とstrict mode
の影響が分かりましたが、実際に関数そのまま呼び出すだけではなく、オブジェクトのメソッドとして呼び出すこともよくあります。もちろん、クラス・インスタンスのメソッドとして呼び出すケースも根本的に同じです。
これはまさにthis
がインスタンスを指す誤解を招くケースですが、まずは次の例を:
var obj = {
a: 2,
b: function () {
console.log( this.a );
}
}
obj.b() // 2
デバグツールを使ってみてみると:
b
メソッドを呼び出すときに、this
がobj
を指していることがわかります。これは、this
バイディングのもう一つよくみられるパターン、つまりオブジェクトの属性として呼び出されたときに、そのオブジェクトへ指すことです。
ただ、このthis
は、複数階層のオブジェクトケースに、一番近い層のオブジェクトを指します:
function b () {
console.log( this.a );
}
var obj2 = {
a: 2,
b: b
}
var obj1 = {
a: 20,
obj2: obj2,
b: b
}
var obj3 = {
c: 0,
obj1: obj1
}
obj1.b() // 20 -> b()のthisがobj1を指す
obj1.obj2.b() // 2 -> b()のthisがobj2を指す
obj3.obj1.b() // 20 -> b()のthisがobj1を指す
上記の例のように、いずれもメソッド実行直前の.で繋いでいるオブジェクトがthisとなります。
this
バイディングの喪失(lost)
暗黙的なこれまでの例を見ると、オブジェクトのメソッドとして呼び出すときに、その呼び出すオブジェクトを指すことから見ると、OOPのインスタンスと非常に似ています。ただ、次のケースはこの表面的な類似性を覆します。
function b () {
console.log( this.a );
}
var obj2 = {
a: 2,
b: b
}
var c = obj.b // メソッドbを、cへ付与
var a = 20
c() // 20
オブジェクトはレファレンスで値を渡しているので、c
はもちろん、obj.b
と同じ関数オブジェクトを指しています。しかし実行するときに、出力が2
ではなく、20
となりました。
また、関数を引数として他の関数に渡す時も、同じく、this
のバイディングが変わることがあります:
function b () {
console.log( this.a );
}
function c ( fn ) {
fn() // fnのコールサイトがcとなり、ここの`this`がglobalかundefined(strict mode)になる
}
var obj = {
a: 2,
b: b
}
var a = 20
c(obj.b) // 20
これは、デフォルトの関数実行ケースと全く同じパターンになってしまい、cを実行するときに、this
がglobal
を指していることになります。つまり、オブジェクトのメソッドとして実行されるときに適応された、暗黙的なthis
バイディングが失っています。
なので、setTimeout
とかのように、コールバックを渡すときに、this
が変なところに行ってしまう落とし穴があります。これを治す方法はいくつかありますが、この節の例を見ると、インスタンスを指してないことが理解できるでしょう。
this
バイディング(explicit binding)
明示的な上記の問題を治すには、要するに、this
を明示的に指定することで、デフォルトのルール(グローバルオブジェクトまたはundefined
)に戻すことを防ぐことです。jsの関数オブジェクトには、共通するメソッドが存在し、その中でこの問題を解決する、call
やapply
メソッドを使うことで、this
を明示的に指定します。
let obj = {
name: 'John',
greet: function () {
console.log(this.name + ' said hi');
}
}
obj.greet.call({name: 'Jack'}); // Jack said hi
call
とapply
は基本的に一緒ですが、違いといえば、call
が任意数の引数を受け入れることができ、apply
は配列を受け入れることができます。上記の例では、引数として{name: 'Jack'}
を渡すときに、obj
のthis
を、{name: 'Jack'}
のthis
に紐付けました。
a.call( b ) // => aのthisをbにバインドする
ただ、call
とapply
のバイディングは、変えられることができません:
function b () {
console.log( this.a );
}
var obj = {
a: 2,
}
var a = 20
var c = function () {
b.call( obj )
}
c() // 2
setTimeout(c, 1000) // 2
c.call(window) // 2
そのため、このようなバイディングを、ハードバイディング(hard binding)とも呼びます。
このハードバイディングを実現するためのヘルパー関数も作られます:
function bind ( fn, context ) {
return function () {
return fn.apply( context, arguments ) // argumentsはこの関数に渡されたすべての引数の配列
}
}
function b (num) {
console.log( this.a + num );
}
var obj = {
a: 2,
}
var a = 20
var c = bind(b, obj)
var d = c(10)
console.log(d) // 12
このパターンがよく使われるため、ES5には関数オブジェクトにbind
メソッドが導入されました。なので、bind
を使うバイディングも一緒で、ハードバイディングとなります。使い方もヘルパー関数とほぼ一緒です。
// ...
var c = b.bind(obj)
var d = c(10)
console.log(d) // 12
また、JS内蔵の一部の関数にも、このようなハードバイディングの対象をオプショナル引数として受け入れることができます。例えば、forEach
の定義を見ると:
// Arrow function
forEach((element) => { /* ... */ } )
forEach((element, index) => { /* ... */ } )
forEach((element, index, array) => { /* ... */ } )
// Callback function
forEach(callbackFn)
forEach(callbackFn, thisArg)
// Inline callback function
forEach(function(element) { /* ... */ })
forEach(function(element, index) { /* ... */ })
forEach(function(element, index, array){ /* ... */ })
forEach(function(element, index, array) { /* ... */ }, thisArg)
この中のthisArg
は、コールバック関数内のthis
を指定のオブジェクトとハードバイドすることです。
function foo(el) {
console.log(el, this.name);
}
var obj = {
name: 'hoge'
}
[1,2,3].forEach(foo, obj) // 1 hoge 2 hoge 3 hoge
本質的には、上記のcall/applyによるハードバイディングと一緒なので、開発者の手間を少し省けることができます。
new
キーワードバイディング
このnew
の存在も一つの理由ですが、よくJSは一般的なOOP言語と同じだと誤解されることがあります。
function Car (maker, model) {
this.maker = maker
this.model = model
}
var myCar = new Car('Honda', 'Civic')
jsでは関数を呼び出す方法として、一般的に関数名の後ろに()
をつけて実行しますが、new
で呼び出すことも可能です。すべての関数はnew
で実行することが可能ですが、通常new
で実行する関数をパスカルケースで書き、コンストラクター関数と呼びます。new
で関数を実行するときに、次のような処理が行われます(MDNにも参照)。
- 一つ新しいオブジェクトが作られる
- 作られたオブジェクトに、コンストラクター関数のプロトタイプとリンク付ける
- 新しく作られたこのオブジェクトが、
this
としてバイドされる - 関数に他のリターン値が指定されていない限り、
new
で実行された関数は、自動で新しく作られたオブジェクトをリターンする
Car
の例でいえば、new
で実行するときに、一つ新しいオブジェクトを作り、そのオブジェクトをthis
としてバイドすることで、それで「インスタンスを指し」ているように見えます。
function Car1 (maker, model) {
this.maker = maker
this.model = model
// リターン値がないが、`this`を返す、`this`がグローバルではなく、{maker, model}オブジェクトとなる
}
function Car2 (maker, model) {
return {maker, model}
}
var myCar1 = new Car1('Honda', 'Civic')
var myCar2 = Car2('Nissan', 'Selena')
var myCar3 = Car1('Honda', 'Vezel')
var myCar4 = new Car2('Toyota', 'Corolla')
myCar4.model = 'Harrier'
console.log(myCar4.model)
console.log(myCar1) // {maker: 'Honda', model: 'Civic'}
console.log(myCar2) // {maker: 'Nissan', model: 'Selena'}
console.log(myCar3) // undefined => `new`演算子使用されていない、かつリターン値がないためundefinedとなる
console.log(myCar4) // {maker: 'Toyota', model: 'Corolla'}
優先順位
これまでthis
のバイドについていくつかのルールをみてきました:
- デフォルト
this
バイディング:グローバルオブジェクトまたはundefined
(strict mode) - 暗黙的な
this
バイディング:メソッドとして実行され、呼び出されたオブジェクトがthis
となる - 明示的な
this
バイディング:bind
またはapply/call
で指定したオブジェクトがthis
となる -
new
バイディング:new
で作られた新しいオブジェクトがthis
となる
どちらの優先順位が高いかが次の問題となります。
まずデフォルトはその名前通り、他に特に指定がなければのフォールバックとなるため、優先順位が一番低いのです。
次に暗黙的なthis
バイディングと明示的なthis
バイディングですが、名前でなんとなく伝わるかもしれません。一応例で見てみます:
function foo() {
console.log( this.a )
}
var obj1 = { a: 1, foo: foo }
var obj2 = { a: 2, foo: foo }
obj1.foo() // 1
obj2.foo() // 2
obj1.foo.call( obj2 ) // 2
obj2.foo.call( obj1 ) // 1
この通り、call
で明示的にバイドするときは優先されます。
次はnew
バイディングと、暗黙的なthis
バイディングとの順位です:
function Car (maker, model) {
this.maker = maker
this.model = model
}
var car1 = {
getCar: Car
}
var car2 = {}
car1.getCar('Honda', 'Civic')
console.log(car1.model) // Civic
car1.getCar.call(car2, 'Honda', 'Vezel')
console.log(car2.model) // Vezel
var car3 = new car1.getCar('Nissan', 'Selena')
console.log(car1.model) // Civic
console.log(car3.model) // Selena
つまり、new
バイディングは暗黙的なthis
バイディングより優先順位が高いのです。
最後は明示的なthis
バイディングと、new
バイディングの順位です:
function Car (maker, model) {
this.maker = maker
this.model = model
}
var car1 = { }
var car2 = Car.bind(car1)
car2('Honda', 'Civic')
console.log(car1.model) // Civic
var car3 = new car2('Nissan', 'Selena')
console.log(car1.model) // Civic
console.log(car3.model) // Selena
これを見る限り、car2の時点でcar1へハードバイドしているのですが、new
で呼び出すときに、this
がcar1
ではなく、なんと変えられました。つまり、new
の優先順位は一番高いことです。
MDNの説明によると:
バインドされた関数は new 演算子でも生成されます。これを行うとターゲット関数が代わりに生成されたようになります。与えられた this の値は無視され、追加された引数はエミュレートされた関数に提供されます。
bind
関数の具体的な実現について、我々が先ほど例で実装したものよりだいぶ複雑です。もしbind
が先ほどの例のように簡単なものだと、確かにnew
でthis
を変えることができません。ただ、MDNの説明のように、new
を使うときは、ハードバンドされたthis
が無視され、new
で生成されたオブジェクトがthis
として入れ替えられます。
ちなみに、本当のbind
実装は下記のようになるようですが、筆者自身もまだ理解が難しいので、今回は深入りしません。
Function.prototype.bind = function (oThis) {
if ( typeof this !== "function") {
throw new TypeError( "Function.prototype.bind - what is trying to be bound is not callable")
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(
this instanceof fNOP && oThis ? this : oThis,
aArgs.concat(Array.prototype.slice.call(arguments))
)
}
fNOP.prototype = this.prototype
fBound.prototype = new fNOP()
return fBound
}
一旦まとめとして、今までの4つのルールの中で、優先順位はnew
演算子 > 明示的this
(call/apply
またはbind
) > 暗黙的this
(オブジェクトのメソッドとして) > デフォルトthis
(上記以外のケース)となります。このくらい分かれば、this
の謎についてもうだいぶ理解できるでしょう。
例外
残念ながら上記のルールと優先順位はすべてのケースをカバーできません。ここでいくつかの例外のケースを説明します。
this
無視されるnull
またはundefined
をcall/apply
またはbind
に渡した場合、this
は無視されます。
function add(a, b) {
return a + b
}
// ...が導入される前に、配列の要素を引数として展開するテクニック
add.apply(null, [2, 3]) // 5
// null以降の引数をadd2に渡す
var add2 = add.bind(null, 2)
console.log(add2(3)) // 5
引数の展開について、今...
演算子を使うことが普通になったので、もうあまり見られないかもしれません。ただ、bind
はまだ代替するものがないのでしばらくは見られる・使われるでしょう。
null
を渡し、任意数の引数をその付与された関数(add2
)に渡すことが可能です。これは関数型プログロミングで言われるcurryingとの操作です。
bind
は割と使われていますので、上記の例のパターンは見たことがあるかもしれません。例えば、reactのコンポーネントのクリックイベントに、引数を渡したいときにこのパターンを使うことができます:
// ...
const clickHandler = (nickname) => { ... }
return (
<ul>
{userList.map((user) => (
<UserCard
key={user.id}
clickHandler={clickHandler.bind(null, user.nickname)}
/>
))}
</ul>
)
いずれにしても、bind
を使うときに、もしthis
のバイディングは関係なければ、null
を入れると良いでしょう。ただ、ここで明示的なバイドではないので、デフォルトのバイドとして、this
がグローバルオブジェクトまたはundefined
となり、万が一どれかのサードパーティのパッケージにthis
が使われているとしたら、非常にデバグしにくい問題になりかねます。
この問題を防ぐために、どうせthis
は誰を指すか関心を持たないなら、一つ無関係のオブジェクトを作り、それをnull
の代わりに渡しておけば良いことです。jsで空のオブジェクトを作りには、Object.create(null)
を使うことができます。
function add(a, b) {
return a + b
}
var emptyObj = Object.create(null)
add.apply(emptyObj, [2, 3]) // 5
var add2 = add.bind(emptyObj, 2)
console.log(add2(3)) // 5
間接的な(indirect)レファレンス
これは特に値を与えるときにうっかりと起こりやすいのです:
function foo() {
console.log(this.a)
}
var a = 20
var o = {a: 10, foo: foo}
var p = {a: 5}
o.foo() // 10
;(p.foo = o.foo)() // 20
p.foo(); // 5
p.foo = o.foo
のリターン値は、foo
関数のレファレンスで、実行する場所はp
でもo
でもなく、ここのthis
はデフォルトのグローバルオブジェクトとなります。このような関数を呼び出す方法をできる限り避けた方が無難です。
ソフトバイディング
明示的なバイディングのときに、ハードバイディングという言葉が出ましたが、ハードがあるなら、ソフトもあるでしょう、と推測が着くかもしれません。
ハードバイディングの欠点といえば、一旦バイドされた後、new
で入れ替えること以外、this
を変えることが不可能になります。
もしglobal
とundefined
以外の値を、デフォルトバイディングの対象として指定できれば、ハードバイディングのようにthis
をバイドしながら、暗黙的なバイディングとハードバイディングの両方を行うことができます。例えば:
Function.prototype.softBind = function(obj) {
var fn = this
var curried = Array.prototype.slice.call(arguments, 1)
var bound = function() {
return fn.apply(
// thisがundefinedまたはglobal/windowの場合、引数のobjをthisにする、それ以外の場合はそのまま
(!this || this === (window || global)) ? obj : this,
curried.concat(curried, arguments)
)
bound.prototype = Object.create(fn.prototype)
return bound
}
}
これを試してみると:
function foo() {
console.log("name: " + this.name)
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind( obj );
obj2.foo(); // name: obj2 => 暗黙的なバイディング適応
fooOBJ.call( obj3 ); // name: obj3 => ハードバイディング適応
setTimeout( obj2.foo, 10 ); // name: obj => デフォルトになっていなく、ソフトバイディング適応
一点だけ注意ですが、softBind
は公式のメソッドではなく、あくまでもカスタムでFunction.prototype
に追加したものです。this
バイディングの理解のためのものなので、実際のコードでは言語自身の仕様変更(Function
にsoftBind
を追加)はあまりお勧めしません。
アロー関数
Es6から導入された関数定義の方法の一つ。上記の4つのルールは適応対象外となります。というのは、アロー関数のthis
は、アロー関数の外層のthis
を継承しているからです。
function foo() {
return (a) => {
console.log(this.a)
}
}
var obj1 = {a:2}
var obj2 = {a:20}
var bar = foo.call(obj1) // ここでbarのthisはobj1へバイド
bar.call(obj2) // 2 => 20ではありません
アロー関数のthis
バイディングはハードバイディング、かつnew
では影響されません。この親からthis
を継承する特性から、よくコールバックのときに使われることがあります:
function foo() {
setTimeout(() => {
console.log(this.a) // ここのthisはfooのthisを継承する
},1000)
}
var obj = {a:2}
foo.call(obj) // 2
実際にアロー関数が導入される前に、より明示的なテクニックがすでに広く使われています:
function foo() {
var self = this // ここでfooスコープ内のthisを保存
setTimeout(function () {
console.log(self.a) // this.aの代わりに、self.aで代用
}, 1000)
}
var obj = {a:2}
foo.call(obj) // 2
仲介役のself
を導入することで、どのスコープ内のthis
を指定するかを決めることができます。悪い実践ではありませんが、根本的にthis
そのものを使わないようにしています。
また、この特性もあるから、アロー関数を使うことは、コードが少なくなるだけではないということを意識しておいた方が良いでしょう。
最初の問題に戻る
IIFE
これで最初の問題に戻りますが、一つ先に説明しておく必要のある概念があります。IIFE(Immediately Invoking Function Expression):
// ブラウザー
var a = 2
(function IIFE( global ) {
var a = 3
console.log(a) // 3
console.log(global.a) // 2
})( window )
console.log(a) // 2
function foo() {...}
に丸いカッコをつけることで、これを関数表現(expression)に変えています。その次に()
をつけることで即時に実行することになります。上記の例では、実行時にグローバルのwindow
オブジェクトを引数として渡しているので、IIFE
関数内ではglobal
という名前でアクセス可能になります。
ただいずれにしても、IIFEがthis
バイディングへの影響がなく、判断基準はこれまでと同じです。例えば上記の例では、グローバルで実行されている関数ですから、IIFE関数内部のthis
もデフォルトのグローバルオブジェクトとなります。
スコープの分離
IIFE
は本来、スコープの汚染(pollution)を解決するためのテクニックです。冒頭の問題に何を影響するかというと、スコープが分けられているところです。
// ブラウザー
var a = 1 // -> ここはグローバルスコープ
;(function() {
// -> ここはIIFE関数1のスコープ
console.log(a + this.a) // この時点でIIFE関数1のスコープ内にはaが存在しますが、値の付与・初期化されてないので、aがundefinedとなる
var a = '2' // IIFE関数1スコープ内のaが初期化され、'2'となる
console.log(a + this.a) // 特に適応するルールがないのでデフォルトとして、thisはグローバルオブジェクトを指す
})()
var name = 1 // -> ここはグローバルスコープ
;(function() {
// -> ここはIIFE関数2のスコープ
console.log(name + this.name) // IIFE関数1と同じ??
var name = '2'
console.log(name + this.name) // IIFE関数1と同じ??
})()
// -> ここはグローバルスコープ
これまで明白なのは、二つのIIFE関数はグローバルスコープ内で実行されているので、その中のthis
は、デフォルトのグローバルオブジェクトを指しているのです。二つのパターンが一緒なので、両方とも出力がundefined + 1 = NaN
と'2' + 1 = '21'
となるのではないかと、思われます。
ブラウザー環境のグローバルオブジェクト
これまで半分正解です。この問題の落とし穴はもう一つあります。name
という変数名です。
最初にこの問題を見たときにおかしいと思ったのは、一つ目の変数名がa
なのに、二つ目ではb
ではなく、なぜかname
となっています。
name
はここで、グローバル変数として宣言されていますが、ブラウザーのwindow
オブジェクトのname
プロパティと同じ名前となっています。すると、var name = 1
という記述は、グローバルスコープで行われた値の付与ですので、実際はwindow.name = 1
と同じ意味になります。
// ブラウザー環境
var foo = 'abc'
console.log(foo === window.foo) // true
また、MDNの記述のように、window.name
は、その付与された値をすべてtoString
メソッドで文字列に変換することになります。
Note: window.name converts all stored values to their string representations using the toString method.
つまり、name= 1
で付与しても、実際はwindow.name = '1'
となっています。
this
の指向は同じく、グローバルオブジェクトですので、this.name
=window.name
、つまりthis.name = '1'
ということです。
そのため、2つ目のIIFEでは、undefined + '1' = 'undefined1'
と'2' + '1' = '21'
となります。
これまでようやく謎が解きました。この問題の正しい解答は以下となります。
NaN
"21"
"undefined1"
"21"
ただ、この解答には、ブラウザー環境という制限があります。もしnode.jsで実行する場合、global
オブジェクトには、global.name
という属性はないため、ブラウザー環境のような謎の結果が見られません。
終わりに
冒頭の問題は、知り合いから投げられて、「これやってみ」と言われたのですが、結局予想と全く違う結果が出て、???と思って、色々と調査し始めました。
最初はthis
が鍵になると思い、this
を中心に考えていましたが、どうやら他の要素も関わっており、特にブラウザー環境という大きな要因があると気づきました。結局this
だけではないことですが、今回はthis
の謎解明のきっかけとして残しておきたいと思います。
this
は本当にJS言語でかなりわかりにくい概念です。JS言語開発の一人、JSON発明者のDouglas Crockford氏が著作 How JavaScript Works の16章でthis
について「問題をもたらすだけですから、this
を使わないこと」、というように完全に否定しています(もっと説明ほしかったのですがこの章だけ数ページだけでした。嫌いだったでしょうね。。)
にしても、完全に棄却する、という判断は、this
の働きが分からない、間違いが怖い、という理由ではなく、理解した上での判断だと思った方が良いでしょう。なので、使わないから知らなくて良いとの意味ではなく、JS言語を理解するために避けられないと私は思っています。
今回の内容にはいくつか軽く触れていた重要な概念、スコープ、プロトタイプとかがありますが、またの機会に詳しく書きたいと思います。
ではでは、今日は一旦これで。
Discussion