#Javascriptのthisについて
Javascriptの学習をしていてthisのパターンがいくつかあったのでメモ的な感じでこの記事を書くことにしました!Web上にはたくさんこれに関する記事があり、車輪の再発明みたいになってしまうのですが一応自分の頭の整理と言うことで。
何か間違いがあったり、曖昧な箇所があればご指摘いただけると嬉しいです!
関数内のthis
普通に定義された関数内でのthisです。これはグローバルオブジェクトを指します。
var greeting = 'Hello world';
function sayHello(){
console.log(this.greeting);
}
sayHello(); // Hello world
実際にデベロッパーツールのコンソールで見てみるとwindowオブジェクト(=グローバルオブジェクト)の中に変数greetingが入っているのがわかります。(ES6で導入されたletとconstで変数を定義するとグローバルオブジェクトの中に入りません。そのため上記でgreetingをletやconstで定義してwindowオブジェクトの中を見てもその変数は入っていません。実行するとundefinedとなります。)
##メソッド内のthis
オブジェクトの中で定義された関数を上のような普通の関数と区別してメソッドと呼びます。
メソッドの中でのthisはそのオブジェクト自身を指します。
var animal = {
animalName: 'Lion',
getName: function() {
return this.animalName;
}
}
console.log(animal.getName()); // Lion
ここでのthisはその関数を保持しているオブジェクトであるanimal自身を指しています。
thisが決まるタイミング
以下のコードを見てください。
var animal = {
animalName: 'Lion',
getName: function() {
return this.animalName;
}
}
var getAnimal = animal.getName;
console.log(getAnimal()); // undefined
結果はundefinedとなりました。このことから何がわかるかというとthisが指すものはそれが呼び出された時に決まるということです。定義された時ではありません。上の例だと、animal.getNameはgetAnimalと言う変数に格納されています。つまりこの時点でもはやgetAnimalはメソッドではなくただの関数になってしまいます。コードで表すとこんな感じです。
function getAnimal(){
return this.animalName;
}
関数内のthisは常にグローバルオブジェクト(=windowオブジェクト)を指しています。よってこれが実行されるとthisはグローバルオブジェクトを指すようになります。しかしグローバルオブジェクトにanimalNameと言う変数はどこにも定義されていません。結果、undefinedとコンソールに表示されます。
下記のような場合もcryはメソッドではなく関数なので中のthisはグローバルオブジェクトを指します。
var animal = {
animalName: 'Lion',
getName: function() {
function cry(){
console.log(`${this.animalName}は叫んだ。`);
}
cry();
return this.animalName;
}
}
animal.getName(); //undefinedは叫んだ。
thisがある時にはそれが関数なのか、メソッドなのかを意識すると良いです。
##コンストラクタ内でのthis
Javascriptではnewして使う関数をコンストラクタと言います。
コンストラクタ内でのthisが指す対象は呼び出し元のインスタンスです。
function Book(title, author) {
this.title = title;
this.author = author;
}
var book1 = new Book('イノベーションのジレンマ', 'クリステンセン'); // 呼び出し元インスタンス
console.log(book1.title); // イノベーションのジレンマ
console.log(book1.author); // クリステンセン
var book2 = new Book('ハリーポッター','J.K.ローリング'); // 呼び出し元インスタンス
console.log(book2.title); // ハリーポッター
console.log(book2.author); // J.K.ローリング
newした結果はオブジェクトで返ってくるので通常のオブジェクトと同じように使えます。
thisはそれぞれのインスタンスを指しています。
call, apply
callとapplyを使うことによって自分でthisが指すものを設定できるようになります。
###call
callは引数に対象となるオブジェクト(thisが指す対象)を指定します。
var animal = {
animalName: 'Lion',
getName: function() {
return this.animalName;
}
}
var smallAnimal = {
animalName: 'kitty'
}
console.log(animal.getName.call(smallAnimal)); // kitty
getNameを実行する際にcallでオブジェクト(smallAnimal)を指定しています。これによりgetNameの中のthisがsmallAnimalを指すようになり、実行結果はkittyとなります。
###apply
これをapplyに変えても同じ結果が得られます。
var animal = {
animalName: 'Lion',
getName: function() {
return this.animalName;
}
}
var smallAnimal = {
animalName: 'kitty'
}
console.log(animal.getName.apply(smallAnimal)); // kitty
それではcallとapplyで何が違うのかと言うと関数(またはメソッド)に引数がある時にその違いが現れます。
var person = { name: '山田' };
function sayHello(message1, message2){
console.log(`${message1}, ${this.name}。 ${message2}`);
}
sayHello.call(person, 'こんにちは', 'よろしく!'); // こんにちは, 山田。 よろしく!
sayHello.apply(person, ['こんばんわ', 'よろしく!!!']); // こんばんわ, 山田。 よろしく!!!
callの第二引数以降は呼び出し元の関数の引数を入れることができます。ただしcallは普通に第二引数以降に引数を指定していくのに対して、applyは第二引数に配列で全ての引数を指定します。
実際どっちを使ったほうがいいのかなどは状況にもよると思いますし自分も経験が浅いので具体的なシュチュエーションなどは思い浮かびませんでした。申し訳ないです。。
##bind
bindは関数(またはメソッド)とbindの引数で指定したオブジェクトを紐づけて新しい関数を返してくれます。
var dog = { name: 'candy' };
function cry() {
console.log(`${this.name}が吠えた!`);
}
var wanwan = cry.bind(dog);
wanwan(); // candyが吠えた!
もともとcryのthisはグローバルオブジェクト(=windowオブジェクト)でしたがbindで束縛(バインド)することによりdogを指すようになりました。
これは例えば関数の引数になるコールバック関数に、あるオブジェクトのメソッドを使いたい時などに利用することができます。
var child = {
age: 5,
grow: function() {
console.log(this.age += 1);
}
}
function invokeFunction(func){
func();
}
var growChild = child.grow;
invokeFunction(growChild);
console.log(child.age); // 5 ⇦ ageが更新されていない
var growChildWithBind = child.grow.bind(child);
invokeFunction(growChildWithBind);
console.log(child.age); // 6
この場合、growChildはただの関数になるのでその中のthisはグローバルオブジェクトを指します。invokeFunction内ではそれが実行されていますがthis.ageはundefinedなのでundefined += 1のような形になり結果はNaNになります。よってageが更新されることはなくchild.ageは5となります。
一方でbindを使うと先ほどもやったように束縛されます。これによりgrow内部のthisとchild自身がバインドされた新しい関数を返してくれるので上記のようにコールバックとして渡された後もうまく機能しています。
##アロー関数でのthis
これまでやってきたように通常thisは実行時にその値が決まります。
しかし新しくES6で導入されたthisは定義時にもう値が決まってしまいます。
var Dog = function(name) {
this.dogName = name;
}
var cry1 = function() {
console.log(`${this.dogName}が叫んだ!きゃんきゃん!`);
}
var cry2 = () => {
console.log(`${this.dogName}が叫んだ!わんわん!`);
}
Dog.prototype.cry1 = cry1;
Dog.prototype.cry2 = cry2;
var dog = new Dog('candy');
dog.cry1(); // candyが叫んだ!きゃんきゃん!
dog.cry2(); // undefinedが叫んだ!わんわん!
上記のように通常の関数は呼び出し時にthisの値が決まるのに対し、アロー関数は定義時に決まるのでcry2の宣言してる箇所を見てみましょう。するとこの時のthisの値はグローバルオブジェクトを指しているのでその後でcry1とcry2をprototypeに追加したとしてもcry2が指すものはグローバルオブジェクトのままです。
よって、そこではdogNameなんて変数は定義されていないので最後の行のようにundefinedとなってしまう。
なお、prototypeについては以下のリンクを参照してください。
https://itsakura.com/js-prototype
おまけ
アロー関数は周りの文脈に合わせてthisに値を代入してくれるようです。まずは以下の例を見てください。
function Orange() {
this.stock = 0;
}
Orange.prototype.addStock = function() {
setTimeout(function() {
this.stock++;
console.log('オレンジが追加されました');
}, 300);
};
const orange = new Orange();
orange.addStock();
結果は次のようになります。
console.log(orange.stock); // 0
setTimeout内部の関数は通常の関数になるのでthis.stockはundefiedとなります。undefinedに対して演算を行なっているので結果はNaNとなり、stockが更新されることはありません。
次にアロー関数を使った場合を見てみましょう。
function Orange() {
this.stock = 0;
}
Orange.prototype.addStock = function() {
setTimeout(() => {
this.stock++;
console.log('オレンジが追加されました');
}, 300);
};
const orange = new Orange();
orange.addStock();
結果は次のようになります。
console.log(orange.stock); // 1
orangeのstockは更新され、1となりました。
ここからわかることはアロー関数がその外側のthisの値を継承していると言うことです。今回の例でいうとアロー関数が定義されている外側はOrangeのaddStockメソッドです。よって、アロー関数内のthisはOrangeを指すことになり、Orangeはstockというプロパティを確かに持っているので無事にstockが更新されたということになります。
しかしここでaddStock自体もアロー関数で定義してしまうとうまくいきません。
function Orange() {
this.stock = 0;
}
Orange.prototype.addStock = () => {
setTimeout(() => {
this.stock++;
console.log('オレンジが追加されました');
}, 300);
};
const orange = new Orange();
orange.addStock();
console.log(orange.stock); // 0
理由はaddStockメソッドもその外側からthisを継承してしまうからです。今回だと外側はグローバルオブジェクトなのでstockという変数はなく、いくらaddStockを呼び出しても更新されません。結果、0が返ってきます。
##最後に
Qiita初投稿でした!自分の書いたものを世の中に出すという経験をしたことがなかったのですごい新鮮な気持ちです。また学習をしていく内に何か整理したいことが出てきたら記事を書こうと思います。
最後までありがとうございました!