とんちむ日記

RubyとJavaScriptと猫が好きです

今さら聞けないJavaScriptのややこしいところ(スコープ、巻き上げ、this)

これはQiitaに昔書いてたやつを引っ越したものです。

①スコープについて

これについては基本なので、ご存知の方は多いと思いますが折角なのでES2015以降の場合も併せて書こうと思います。

変数のスコープはブロックではない(※variable文の場合)

varによって宣言された変数は、通常の{}で作るブロックではスコープを作りません。ifだろうがforだろうが作らないったら作りません。

{
  var a = "foo";
}

if(true){
  var b = "bar";
}

for(var i = 0; i < 1; i++){
  var c = "buz";
}

console.log(a); //=> "foo"
console.log(b); //=> "bar"
console.log(c); //=> "buz"

ではどうしたらスコープを作ることができるのかというと関数定義の{}ならスコープが作られます。

(function(){

  var a = "foo";

})();

console.log(a);
// ReferenceError: a is not defined
  

このように即時関数を最初に記述しておいてその中に処理を記述しているのを見たことがあると思います。ブラウザがJavaScriptのファイルを複数読み込んだ時に宣言した変数や関数が衝突しないように即時関数を使っているんですね。

ES2015以降だとletとconstが使用でき、それによって宣言された変数と定数では関数定義でなくてもブロックでスコープが作られます。

{
  let   a = "foo";
  const B = "bar";
}

console.log(a);
// ReferenceError: a is not defined
console.log(B);
// ReferenceError: B is not defined

BabelやTypeScriptなどでES2015以降の記法が許される環境であればvarによる宣言は使わなくていいでしょうね。

②変数と関数の巻き上げ(ホイスティング)

関数を定義する方法については2種類ありますね。

var func = function(){}; // 関数リテラルの代入

function func(){}; // 関数宣言

この二つですがきちんと使い分けないと思わぬところでハマります。

func(); // TypeError: func is not a function

var func = function(){
  console.log("hello");
};

上の関数リテラルの場合、読み込み時に代入の処理は行われませんから、funcは関数ではないと言われています。呼び出し時はまだundefinedですね。

func(); //=> hello

function func(){
  console.log("hello");
};

対してこちらの例ではfuncの呼び出しに成功しています。 代入などの処理は順に行われますが、宣言についてはそのスコープの最初で行われるからです。これを巻き上げ(ホイスティング)と呼びます。

巻き上げについては変数も同様です。先ほどの関数リテラルの場合のエラーがReferenceError: func is not definedではないことからもわかります。

var a = "foo";

function func(){
  console.log(a); // 1回目
  var a = "bar";
  console.log(a); // 2回目
}

func();

この場合、1回目の出力結果はfooではなくundefinedとなります。funcの最初でaが再度宣言されているためです。上のコードは下のコードのように動作しているわけです。

var a = "foo";

function func(){
  var a; // ここで宣言されている
  console.log(a);
  a = "bar";
  console.log(a);
}

func();

③5つのthis

JavaScriptのthisは状況によって指し示すものが異なります。 その例を見ていきましょう。

1.コンストラクタ呼び出し

function Person(name, age){
  this.name = name;
  this.age  = age;
}

var tom = new Person('tom', 20);

この場合Personのthisは戻り値となるオブジェクトを指します。

2.関数呼び出し

先ほどのPersonを関数として呼び出すとどうでしょう。

Person('hoge', 20);

console.log(window.name); //=> "hoge"

windowオブジェクトにnameというプロパティを追加してしまいました。これからわかることは関数呼び出し時のthisはグローバルオブジェクトを指しているということです。こういった意図しない挙動を防ぐにはstrictモードを使うといいですよ。

function Person(name, age){
  'use strict';
  this.name = name;
  this.age  = age;
}

Person('hoge', 20); //=> TypeError: Cannot set property 'name' of undefined

3.メソッド呼び出し

メソッド呼び出しというのはレシーバのある呼び出し方です。(jsでレシーバという言い方をするのかわかりませんが・・)要はobject.method();といった呼び出し方ですね。

var obj = {
  name: 'obj_name',
  method: function(){ console.log(this.name) }
};

obj.method(); //=> "obj_name"

この場合はobj自身がthisとなります。一番わかりやすい挙動ですねー。

4.applyかcallを使用する場合

Function.prototype.apply()Function.prototype.call()は呼び出し元の関数のthisと引数を置き換えて呼び出すことができるという面白い関数です。

var obj = { name: 'foo' };

function func(){
  console.log(this.name);
}

func.call(obj); //=> "foo"
func.apply(obj); //=> "foo"

関数呼び出しの際はthisはwindowなのでthis.nameundefinedになりそうですが、この例ではthisがwindowからobjに入れ替えられているため呼び出しに成功します。

ちなみに Function.prototype.bind() というのもありますが、これは呼び出し元の関数のthisや引数を置き換えた新しい関数を返すものです。 部分適用した関数を作ることも出来る面白い関数ですよ。(部分適用とよく混同される概念にカリー化というものがあって・・・・・長くなるのでやめます)

5.アロー関数内で定義されたthis

アロー関数はES2015で登場した関数の新しい定義方法です。

function Person(name){
  this.name = name;  
  this.show = () => {
    console.log(this.name)
  }
}

let p1 = new Person("foo");
let p2 = new Person("bar");
p1.show.call(p2); //=> "foo"

このコードの実行結果はp1.show()と呼び出したときと同じです。アロー関数式でのthisは定義するコンテキストのthisに固定されます。なので実行時にthisが変わることがありません。もうbind(this)しなくてもいいんです。 ただし、さきほどのcall(),apply(),bind()で変更することもできません。

おしまい。