B-Teck!

お仕事からゲームまで幅広く

【JavaScript】JavaScriptの変数・関数の巻き上げ(ホイスティング)

ホイスティングとは

JavaScriptは、関数内でvarで宣言した全ての変数が先頭で宣言したことになる。
この、宣言が先頭に移動する仕様をホイスティング、または宣言の巻き上げと呼ぶ。
宣言のみが先頭に移動し、代入された変数は移動しないため、
直感的ではない動作をするケースが有る。

変数の巻き上げ

var scope = 'global';

function test() {
  // 関数内のscopeの宣言のみが巻き上がるので
  // undefinedになる
  console.log(scope); 

  // 宣言は巻き上がっているので代入のみ行われる
  var scope = 'local';
  // 代入されたlocalが出力される
  console.log(scope);
}

test();

見た目上は最初のconsole.log(scope)ではglobalが出力されそうだが、
undefinedが出力される。

その原因は、test()が実際には下記のコードと等価になるせいだ。

function test() {
  // 宣言のみ先頭に移動
  var scope;
  // 値は代入されていないのでundefined
  console.log(scope); 

  scope = 'local';
  // 代入されたlocalが出力される
  console.log(scope);
}

ES2015の場合

実は、ES2015以降ではホイスティングを考慮せず記述することができる。
varではなくletまたはconstで宣言を行うと、宣言より前に使用された変数は
ReferenceErrorが出力されるようになった。

var scope = 'global';

function test() {
  // letで変数が宣言された場合、
  // 宣言は巻き上がらずReferenceErrorとなる
  console.log(scope); 

  let scope = 'local';
  console.log(scope);
}

test();

関数の巻き上げ

関数については、function文で宣言したものについては、その定義ごと巻き上がる。
変数に保持した場合は、変数のときと同様の動作となる。

function文で宣言した場合

定義ごと宣言が巻き上がるため、実行することができる。

function hoge() {
  // fuga()の定義も巻き上がっているので、  
  // 使用することができる。
  fuga();
  function fuga() {
    console.log('fuga');
  }
}

hoge();

varで宣言した場合

宣言のみ巻き上がるが、実行不可能。

function hoge() {
  // fugaの宣言のみ巻き上がるが、
  // undefinedのため関数として実行することができず
  // TypeErrorが発生する
  fuga();

  var fuga = function () {
    console.log('fuga');
  }
}
hoge();

letconstで宣言した場合

巻き上がらないため、実行不可能。

function hoge() {
  // letで宣言しているため定義が巻き上がらず、
  // ReferenceErrorが発生する
  fuga();

  let fuga = () => {
    console.log('fuga');
  }
}

hoge();

【JavaScript】最大値と最小値を指定して範囲内の値を持った配列を作成する(ES2015対応版)

この記事は下記の記事をES2015対応機能でリライトしたものです。 beatdjam.hatenablog.com

/** 
* range
* 範囲内の整数値を持った配列を作成する
* @param {number} max 範囲の最大値
* @param {number} min 範囲の最小値(デフォルト値:0)
* @param {function} filer フィルタ関数(デフォルト値:全てtrue)
* @return {array} 作成した配列
*/
function range(max,min = 0,filter=(e)=>{return true}){
    return Array(max-min+1).fill()
                           .map((v,i)=>{return min+i})
                           .filter(filter);
}
console.log(range(5));
// 第一引数までの配列を作成する
// Array [ 0, 1, 2, 3, 4, 5 ]
console.log(range(5,1));
// 第二引数から第一引数までの配列を作成する
// Array [ 1, 2, 3, 4, 5 ]
console.log(range(5,1,e=>{return e%2 === 0}));
// 第二引数から第一引数までのうち、第三引数の条件に合致する配列を作成する
// Array [ 2, 4 ]
console.log(range(-5,-11));
// 負の値も生成可能
// Array [ -11, -10, -9, -8, -7, -6, -5 ]

前回は(最小値、最大値、フィルタ関数)という引数の並びだったけど、
最大値までの連番を作る方が用途として多かったので、
(最大値、最小値、フィルタ関数)という並びに変更した。

処理の概要は、

  • Array(max-min+1)で必要な長さの配列を生成
  • 配列の要素をfill()undefinedにする ※空要素の場合map()で処理がうまく走らないので必要
  • 最小値+indexの値を各要素に格納
  • フィルタ関数が指定されていなければ全てtrue、
    指定されていればフィルタ関数でfilter()する

【JavaScript】thisの種類

JavaScriptthisにはいくつかの種類があり、状況によって動作が変わる。
現在はおおまかに分けて5つ?

コンストラクタ呼出し

function Func1(arg1, arg2){
  this.arg1 = arg1;
  this.arg2  = arg2;
}

let obj1 = new Func1('test', 123456);
// obj1.arg1 => 'test'
// obj1.arg2 => 123456

Functionオブジェクトのコンストラクタをnewで呼び出してインスタンスを生成する場合。
この文脈でのthisインスタンス自身を指す。

メソッド呼出し

let obj = {
  val : 'test',
  func1 : function(){
    console.log(this.val);
    // =>'test'
  }  
}
obj.func1();

オブジェクトの中のメソッドからthisを参照した場合。
メソッドの存在するオブジェクトをthisとする。

関数呼び出し

function func1(arg1, arg2){
  this.arg1 = arg1;
  this.arg2  = arg2;
}

func1('test', 123456);
console.log(window.arg1);
//windowオブジェクトにプロパティがセットされる。
//windowはグローバルオブジェクトのため、プロパティもグローバルになってしまう。

Functionオブジェクトをnewをつけずにそのまま呼び出した場合。
普通に呼び出される関数の中でthisを使用すると、Function内にスコープが限定されず、
windowオブジェクトを参照してしまう。

Strictモードを使用した場合はエラーが発生して利用できなくなるため安全。

'use strict';
function func1(arg1, arg2){
  this.arg1 = arg1;
  this.arg2  = arg2;
}

func1('test', 123456);
console.log(window.arg1);
//Exception: TypeError: this is undefined

関数呼び出しの注意点

let obj = {
  val : 'obj prop',
  func1 : function(){
    // メソッド呼び出し
    // obj.val
    console.log(this.val);
    
    function nestedFunc(){
      // 関数呼び出し
      // window.val
      console.log(this.val);      
    }
    nestedFunc();
  }  
}

window.val = 'window.prop';
obj.func1();

メソッド呼出しの中で関数呼出しを行った場合でも、単体で関数呼出しを行った場合と同様にthiswindowを指してしまう。
ECMASript6(ECMAScript2015)の場合は、後述するアロー関数を用いることで、thisを一意にすることができる。
それ以外の環境の場合、下記のような対処法がある。

  • self
let obj = {
  val : 'obj prop',
  func1 : function(){
    let self = this;
    console.log(self.val);
    // =>'obj prop'
    
    function nestedFunc(){
      console.log(self.val);
    // =>'obj prop'
    }
    nestedFunc();
  }  
}

window.val = 'window.prop';
obj.func1();

参照するべきthisを変数に保持してしまう方法。
慣例的にselfthatthisなどの変数名が利用される。
単純だがわかりやすい。

  • bind()
let obj = {
  val : 'obj prop',
  func1 : function(){
    console.log(this.val);
    
    function nestedFunc(){
      console.log(this.val);      
    };
    // thisをbindしたfunctionを定義
    let bindFunc = nestedFunc.bind(this);
    bindFunc();
    //またはそのまま実行
    nestedFunc.bind(this)();
  }  
}
window.val = 'window.prop';
obj.func1();

参照されるべきthisの値をbind()で束縛した新しいFunctionオブジェクトを作成する。 作成した関数を実行することで別のものを参照しないようにする方法。
部分適用はしやすいが、少し可読性が良くない。

個人的には、どちらかと言えばselfの方が嬉しい気持ちになる。

applyまたはcallで呼び出し時

let obj = {
  val : 'obj prop',
}

function func1(){
  console.log(this.val);
}  

func1();
// => undefined
func1.call(obj);
// => 'obj prop'
func1.apply(obj);
// => 'obj prop'

Function.prototype.apply()Function.prototype.call()は、
引数で与えられたオブジェクトをthisとしてセットして呼び出すことができる。

applycallの違い

let obj = {
  val : 'obj prop',
}

function func1(arg1, arg2){
  console.log(this.val + ' ' + arg1 + ' ' + arg2);
}

func1.call(obj, 'test', 'args');
// => 'obj prop test args'
func1.apply(obj, ['test', 'args']);
// => 'obj prop test args'

両方共第一引数はthisにセットする値だが、下記の点が異なる。
* callは第二引数以降に与えた引数が、呼び出すオブジェクトに引数として渡される。
* applyは第二引数に渡した配列の中身が引数として渡される。

アロー関数で呼び出し時

let obj = {
  val : 'obj prop',
  func1 : function(){
    console.log(this.val);
    // アロー関数は、外側のthisを自身のthisとして扱うため、
    // func1のthis.val = obj.valを参照する。
    let nestedFunc = () => {
      console.log(this.val);      
    }
    nestedFunc();
  }  
}

window.val = 'window.prop';
obj.func1();

ECMASript6(ECMAScript2015)以降では、アロー関数という仕組みが提供されている。 アロー関数は、自分が宣言されているスコープのthisを引き継いで関数内でthisとして扱うため、
直感に反しないthisの扱い方をすることができる。

アロー関数の注意点

window.val = 'window val';
let func = () => {
  console.log(this.val);      
}

let obj2 = {val : 2};
func();
// => windos val
func.call(obj2);
// => windos val
func.apply(obj2);
// => windos val

前述したように、アロー関数のthisは外側のスコープのthisを引き継いでセットされる。
アロー関数の場合、callapplyで呼び出してもこの前提は変わらず、thisを置き換えることが出来ない。