もやもやエンジニア

IT系のネタで思ったことや技術系のネタを備忘録的に綴っていきます。フロント率高め。

TypeScriptを試してみた 3

前回に引き続きTypeScriptの話

1回目
http://rei19.hatenablog.com/entry/2013/07/14/183311

2回目
http://rei19.hatenablog.com/entry/2013/07/23/023008

今回は関数についてです。

関数定義の基本

変数に型を付けられるようになったことで、関数の引数と返り値の型定義も出来るようになりました。

TypeScript

function funcTest(arg1 : string, arg2? : number) : boolean{
    console.log(typeof arg1 + ' : ' + arg1);
    console.log(typeof arg2 + ' : ' + arg2);
    console.log(typeof this);
    return true;
}
// ①引数全指定
console.log(funcTest('ひきすう', 0));
出力結果
// string : ひきすう
// number : 0
// object
// true

// ②引数必須のみ指定
console.log(funcTest('ひきすう'));
// 出力結果
// string : ひきすう
// undefined : undefined
// object
// true

// ③データ型の違う引数を指定
console.log(funcTest('ひきすう', '0'));
// コンパイルエラー

コンパイル

function funcTest(arg1, arg2) {
    console.log(typeof arg1 + ' : ' + arg1);
    console.log(typeof arg2 + ' : ' + arg2);
    console.log(typeof this);
    return true;
}
console.log(funcTest('ひきすう', 0));
console.log(funcTest('ひきすう'));

では、言語仕様の解説をします。

まず、「変数名 : 型」 で変数にデータ型を定義出来るようになったのは前回書いた通りです。
関数を呼びだしているところの③をみると2個目の引数にstringを渡しているので型変換でコンパイルエラーとなります。

2個目の引数の後ろに「?」がくっついていますが、これはオプショナルの引数であるという意味です。
関数を呼び出しているところの②を見て下さい。1個目の引数だけで関数を呼んでいますがコンパイルエラーにはなりません。既存のJavaScriptの仕様で定義されていない変数はundefinedになります。
なお初期値を設定する場合は「arg1 : string == 'でふぉると'」となりますが、「arg1? : string == 'でふぉると'」とするとコンパイルエラーになります。これは初期値が設定されているのでオプショナルのわけがなく、不要なコードであるのでエラーになるということでしょう。

次に関数の引数の定義の後の「: boolean」ですが、これは関数の返り値の型を定義しています。
サンプルの関数は最後に「return true」をしていますが、文字列やNumberを指定した場合はコンパイルエラーとなります。

アロー関数の登場

TypeScriptのアロー関数は無名関数の構文で本家のJavaScriptはES6から投入されるようですが、TypeScriptでは先行して導入されています。

先ほどの関数をアロー関数で書き換えてみます。

TypeScript

var arrowTest = (arg1 : string = '11', arg2? : number) : boolean => {
    console.log(typeof arg1 + ' : ' + arg1);
    console.log(typeof arg2 + ' : ' + arg2);
    console.log(typeof this);
    return true;
}
console.log(arrowTest('ひきすう', 0));
console.log(arrowTest('ひきすう'));

コンパイル

var _this = this;
var arrowTest = function (arg1, arg2) {
    if (typeof arg1 === "undefined") { arg1 = '11'; }
    console.log(typeof arg1 + ' : ' + arg1);
    console.log(typeof arg2 + ' : ' + arg2);
    console.log(typeof _this);
    return true;
};
console.log(funcTest('ひきすう', 0));
console.log(funcTest('ひきすう'));

アロー関数の特徴は2つです。
・new できない。
・thisの固定化。

new できないということはコンストラクタではないということです。通常のfunction演算子で作られたfunctionオブジェクトは関数でもあり、オブジェクトのプロトタイプたるコンストラクタでもあるのですが、=>で作られた関数はコンストラクタとしての機能は持っていないということです。

問題はthisについて。JavaScriptにおけるthisは呼ばれるところで参照先が異なります。
※ここで触れるとちょっと長くなるので割愛。このへん読むと面白いです。
http://qiita.com/vvakame/items/74005adacc0e8e2a3cab
http://qiita.com/KDKTN/items/0b468a07410d757ac609

サンプルコードに戻ってみます。構文としては「(引数) => {処理}」で、アロー関数のオブジェクトが返ります。そしてコンパイル後のコードをみると1行目に「var _this = this;」という記述があります。これが最大の特徴です。上のコードだとちょっとわかりづらいので、利点がわかるように書き直してみます。

var obj = {
    cnt : 0,
    arrowTest : function(){
        // 普通の関数をリテラルのプロパティとしてセット
        if(typeof this.cnt == 'number'){
            this.cnt++;
        }
        console.log(this.cnt);
    },
    arrowTest2 : () =>{
        // アロー関数をリテラルのプロパティとしてセット
        if(typeof this.cnt == 'number'){
            this.cnt++;
        }
        console.log(this.cnt);
    },
    arrowTest3 : function(){
        // プロパティ内に無名関数を定義
        (function(){
            if(typeof this.cnt == 'number'){
                this.cnt++;
            }
            console.log(this.cnt);
        })();
    },
    arrowTest4 : function(){
        // プロパティ内にアロー関数を定義
        (() => {
            if(typeof this.cnt == 'number'){
                this.cnt++;
            }
            console.log(this.cnt);
        })();
    }
}
obj.arrowTest();
obj.arrowTest2();
obj.arrowTest3();
obj.arrowTest4();

このコードですが、順当に全部動くと1 2 3 4と表示されるように見えますが、実際には以下の通り出力されます。

1
undefined
undefined
2

コンパイル後のコードを見てみましょう。ちょっと解説を加えました。

var _this = this;    // arrowTest2でアロー関数を使ったため定義時点のthisを_thisとして退避
var obj = {
    cnt: 0,
    arrowTest: function () {
        // ここは普通にobj=thisなのでthis.cntは有効
        if (typeof this.cnt == 'number') {
            this.cnt++;
        }
        console.log(this.cnt);
    },
    arrowTest2: function () {
        // ここはアロー関数で定義されたのでコンパイル後はthisはオブジェクト外の_thisに置き換わっている
        if (typeof _this.cnt == 'number') {
            _this.cnt++;
        }
        // _thisはcntを持っていないのでundefined
        console.log(_this.cnt);
    },
    arrowTest3: function () {
        // ここでのthisはobjではなくarrowTest3内の無名関数を指しているためcntは無い
        (function () {
            if (typeof this.cnt == 'number') {
                this.cnt++;
            }
            console.log(this.cnt);
        })();
    },
    arrowTest4: function () {
        // ここではobjを指しているthisを _thisに退避して、無名関数内では退避した_thisを指しているためcntが有効。
        // ここでのthis→無名関数
        // ここでの_this→obj
        var _this = this;
        (function () {
            if (typeof _this.cnt == 'number') {
                _this.cnt++;
            }
            console.log(_this.cnt);
        })();
    }
};
obj.arrowTest();
obj.arrowTest2();
obj.arrowTest3();
obj.arrowTest4();

という感じで実行時に決まるはずのthisをallow functionを使うと固定できるという仕組みです。
コンパイル前後のコードを比較するとわかりやすいかなと思います。
JavaScriptだとコンパイル後のコードのようにthisをselfとか_thisとかに退避するのが慣例でしたが、アロー関数を使うと幸せになれますね!

次回に続く

WEB+DB PRESS Vol.75

WEB+DB PRESS Vol.75