30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

ES2015の関数(アロー関数、this、残余引数など)

ES2015で加わった、関数に関する仕様について。

引数のデフォルト値

関数の定義時に、引数のデフォルト値を指定できるようになった。
関数を呼び出す際に引数が渡された場合はそれを、渡されなかった場合はデフォルト値を、使用する。

function addFunc(x = 1, y = 2){
    console.log(x + y);
};

addFunc();  // 3
addFunc(4, 5);  // 9
addFunc(4); // 6

rest parameters

rest parametersは、残余引数とも呼ばれる。
これは、不特定多数の引数を受け取るための構文。受け取った引数は配列になる。
引数の頭に...を付けると、その引数は残余引数になる。

function myFunc(...para){
    console.log(para);
};

myFunc(1);  // [ 1 ]
myFunc(1,2,3);  // [ 1, 2, 3 ]
myFunc('a','b');  // [ 'a', 'b' ]

function addFunc(...para){
    let sum = 0;
    for(let i=0; i < para.length; i++){
        sum += para[i];
    }
    console.log(sum);
};

addFunc(1,2,3,4,5); // 15
addFunc(11,22,33);  // 66

argumentsに似ているが、argumentsと違って残余引数は配列である。
そのため、配列のメソッドを使用することが可能。
また、argumentsは渡された全ての引数を含むが、残余引数は対象となっている引数のみを含む。

function myFunc(a, ...b){
    console.log( arguments[0] );
    console.log( b[0] );
    console.log( 'push' in arguments );
    console.log( 'push' in b );
};

myFunc('hoge', 'fuga');
// hoge
// fuga
// false
// true

lengthでの扱い

全ての関数は、lengthというプロパティを持つ。
これは、その関数に定義されている引数の数を示す。実際に渡された数ではなく、定義時の数である。
残余引数は、lengthではどのように扱われるのか。

function myFunc(a,b,c){
    console.log(myFunc.length);
};
function myFunc2(a, ...rest){
    console.log(myFunc2.length);
};
function myFunc3(a, b, ...rest){
    console.log(myFunc3.length);
};
function myFunc4(...rest){
    console.log(myFunc4.length);
};

myFunc();   // 3
myFunc2();  // 1
myFunc3();  // 2
myFunc4();  // 0

残余引数の数はカウントされないようだ。
そのため、残余引数しかないmyFunc4では、length0になっている。

アロー関数

ES2015で、新しい関数リテラルの書き方が加わった。
この書き方は、アロー関数と呼ばれている。

function(){};    // 従来の記法
() => {}; // アロー関数

省略記法

アロー関数では様々な省略記法が用意されている。

引数が1つのときは、()を省略できる。

let myFunc = x => { console.log('受け取った引数は '+x); };
myFunc('hoge');  // 受け取った引数は hoge

returnを返すだけの場合は、return{}を省略できる。
但し、オブジェクトリテラルを返す場合は、()で囲まないと、正しく認識されない。

let myFunc3 = (x,y) => x + y;
console.log(myFunc3(1,2));  // 3

let myFunc4 = (x,y) => [x,y];
console.log(myFunc4(1,2));  // [ 1, 2 ]

let myFunc5 = (x,y) => {x:y};
console.log(myFunc5(1,2));  // undefined

let myFunc6 = (x,y) => ({x:y});
console.log(myFunc6(1,2));  // { x: 2 }

即時関数

// SyntaxError
() => { console.log('hoge'); }();

// SyntaxError
( () => { console.log('hoge'); }() );

// hoge
( () => { console.log('hoge'); } )();

即時関数の記法が厳密に決まっており、3番めの記法でないとエラーになる。

argumentsの挙動

アロー関数においては、argumentsは存在はするのだが、従来とは異なる挙動をする。
そのため、アロー関数で可変長の引数を用いる場合は、argumentsではなく、先述の残余引数で対応する。

thisの整理

アロー関数では、thisの挙動も従来の関数とは異なる。
それについて述べる前にまず、これまでのthisの挙動がどのようなものであったかを、整理しておく。

従来のthisの最大の特徴は、それが何を指し示すかは文脈によって異なる、ということである。
thisは様々な文脈で呼び出され、その度に、その内容は変化する。

thisapply()の詳細は、下記を参照。
this(), call(), apply(), bind()

自身を呼び出しているオブジェクトを指す

thisは、グローバルスコープで呼び出された場合はグローバルオブジェクトを指し、オブジェクトのメソッドのなかで呼びされた場合はそのオブジェクトを指す。
プロトタイプに配置されているメソッドにおいても同様で、そのメソッドにアクセスしたオブジェクト(インスタンス)が、thisになる。

コンストラクタでの使用

コンストラクタとして関数を使った場合、つまりnew演算子インスタンスを作った場合は、コンストラクタのなかにあるthisは、生成されるインスタンスのことを指す。

call,apply,bind

これらのメソッドを使うことで、thisとなるオブジェクトを指定することが出来る。

入れ子になっている関数

これはほとんどバグと言っていい仕様だが、関数のなかに関数を入れると、内側の関数におけるthisは、なぜかグローバルオブジェクトを指す。

global.prop = 'これはグローバルプロパティです。';
let obj = {
    prop: 'これはobjのプロパティです。',
    func: function(){
        console.log(this.prop);  // これはobjのプロパティです。
        (function(){
            console.log(this.prop);  // これはグローバルプロパティです。
        })();
    }
};

obj.func();

文脈への依存

thisの使われ方は、上記のようなパターンに分類される。そしてどのようにthisを呼び出すかで、thisの内容が変わる。

繰り返しになるが、文脈によって内容が変わる、というのが従来のthisの最大の特徴である。
JavaScriptでは例えば、変数のスコープは定義時に決定し、呼び出す文脈には影響を受けない。だがthisにおいては、定義時には内容が決まらず、文脈によって決まる。

アロー関数におけるthis

アロー関数のなかのthisは、文脈に依存せず、定義時にその中身が決まる。
これが、アロー関数最大の特徴である。

具体的には、定義しているスコープのthisを引き継ぐことになる。

これは特に、先述の関数の入れ子において意味を持つ。
アロー関数は、入れ子にしてもthisが切り替わらない。

global.prop = 'これはグローバルプロパティです。';
let obj = {
    prop: 'これはobjのプロパティです。',
    func: function(){
        console.log(this.prop);  // これはobjのプロパティです。
        (function(){
            console.log(this ===  global);   // true
            console.log(this.prop);// これはグローバルプロパティです。
        })();
        (()=>{
            console.log(this.prop);  // これはobjのプロパティです。
        })();
    }
};

obj.func();

// アロー関数では関数が入れ子になってもthisが変わらないことが分かる。

また、定義時にthisが決まるという性質上、コンストラクタとしては使えない。
new演算子を使うと、エラーになる。

let Func = (x) => {
    this.x = x;
};

let ins = new Func(1);
// TypeError: Func is not a constructor

callapplybindを使っても、thisの中身は変わらない。
ただ、これらのメソッドを使ってもエラーにはならない。strictモードでも同様の挙動。

let normarFunc = function(){
    console.log(this);
};
let arrowFunc = ()=>{
    console.log(this);
};

let obj = {
    prop: 'prop'
};

normarFunc.call(obj);   // { prop: 'prop' }
arrowFunc.call(obj);    // グローバルオブジェクト
let normarFunc = function(){
    console.log(this);
};
let arrowFunc = ()=>{
    console.log(this);
};

let obj = {
    prop: 'prop'
};

let bindFunc = normarFunc.bind(obj);
bindFunc(); // { prop: 'prop' }

bindFunc = arrowFunc.bind(obj);
bindFunc(); // グローバルオブジェクト