HTML5テクニカルノート
TypeScript: 変数の宣言
- ID: FN1701010
- Technique: HTML5 / JavaScript
- Package: TypeScript 2.2
TypeScript公式Handbook「Variable Declarations」をもとにした解説です。TypeScriptにおける変数の宣言についてご説明します。JavaScriptに備わるver
宣言に加えて、TypeScriptはECMAScript 2015の仕様からlet
とconst
を採り入れました。また、分割代入とスプレッド演算子も使えます。
01 varとlet宣言
01-01 ブロックスコープ
var
宣言はブロックスコープをもちません。関数の中でvar
宣言すると、ローカル変数になるので関数の外からは参照できません。けれど、関数本体の中であれば、どこからでも値が得られます。
function f(condition: boolean) { if (condition) { var x = 10; } return x; } console.log(f(true)); // 10 console.log(f(false)); // undefined
let
宣言にはブロックスコープがあります。関数本体の中であっても、ブロック{}
の中で宣言された変数は、外から参照できません。
function f(condition: boolean) { if (condition) { let x = 10; } return x; // 変数がないというエラー }
01-02 宣言と参照の順序
var
宣言した変数は、そのステートメントの前でも参照が得られます(値の代入は、ステートメントの順序にしたがいます)。
a = 1; console.log(a); // 1 var a: number;
let
の場合は、宣言する前に変数は扱えません。操作しようとすれば、エラーになります。
a = 1; // 宣言する前に操作はできないのでエラー let a: number;
ただ、現行のTypeScriptでは、関数からはlet
宣言する前の変数が参照できます。けれど、宣言は予め行っておくのがよいでしょう。
function f() { a = 1; return a; } console.log(f()); // 1 let a: number;
01-03 変数宣言の重複
var
宣言は重複しても、とくにエラーにはなりません。
function f(x: number) { var x = 1; var x = 2; if (x > 0) { var x = 3; } return x; } console.log(f(0)); // 3
let
宣言は同じブロックに同じ変数があることを許しません(MDN「let」の「Temporal dead zone と let に関するエラー」参照)。
function f(x: number) { let x = 1; // 引数と重複しているのでエラー } function g() { let x = 1; var x = 2; // 同じブロックに変数が重複しているのでエラー }
プロックが異なれば、それぞれの変数は別に扱われます。
function f(x: boolean) { if (x) { let x = 100; return x; } return x; } console.log(f(true)); // 100 console.log(f(false)); // false
01-04 スコープの参照
スコープの処理が終わっても、その外の変数に参照を与えておけば、スコープの中身は消えることなく保たれます。つまり、あとからスコープの中の変数値を調べることもできるということです。
function f() { let g; if (true) { let x = 1; g = function() { return x; } } return g; } let func = f(); console.log(func()); // 1
02 ブロックスコープが役立つ例
02-01 for文でカウンタ変数を参照する
for
文のカウンタ変数にvar
宣言を用いると、ループごとのブロック{}
の中で同じ変数として参照されます。たとえば、つぎのようにfor
ループの中でwindow.setTimeout()
メソッドを定め、時間差でコールバックからconsole.log()
によりカウンタ変数(i)の値をコンソールに示すとしましょう。
for (var i: number = 0; i < 5; i++) { setTimeout(function(): void { console.log(i); }, 100 * i); } console.log('i = ' + i);
window.setTimeout()
メソッドを定めたときのカウンタ変数(i)の値は、ループごとに異なります。けれども、ブロックスコープがありませんから、同じグローバル変数の扱いです。したがって、コールバックが呼び出されたときは、その同じ変数値を参照することになります。コンソールには、つぎのように値が示されるのです。
i = 5 5 5 5 5 5
ループごとに異なる値をもたせたいとき、JavaScriptでは即時関数(名前のない関数を直ちに呼び出す)を用いる手法があります。つぎのようにfor
ループの中で関数を定め、変数(i)値を引数に渡せば、関数ごとのローカル変数として別扱いにできるのです。
for (var i: number = 0; i < 5; i++) { (function(i: number): void { setTimeout(function(): void { console.log(i); }, 100 * i); })(i); }
コンソールには、即時関数の引数に渡した値が順に連番整数で示されます。
0 1 2 3 4
カウンタ変数(i)をつぎのようにlet
宣言すれば、for
文のブロック{}
の中はループするごとに別の空間として扱われます。したがって、コンソールには上記とおなじ整数連番が表れます。
// for (var i: number = 0; i < 5; i++) { for (let i: number = 0; i < 5; i++) { setTimeout(function(): void { console.log(i); }, 100 * i); }
02-02 入れ子のfor文に同じカウンタ変数を使う
入れ子のfor
文にvar
宣言で同じカウンタ変数を使うのは避けるべきです。前述01-03「変数宣言の重複」のとおり、エラーを告げることもなく、同じ変数として扱われてしまうからです。たとえば、つぎのような2次元の入れ子配列を定めます(「多次元配列」参照)。入れ子なので型づけの角かっこ[]
はふたつです。親配列の中に子配列を納めることにより、多次元の行列が表せます。
var matrix: number[][] = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]
この入れ子配列のエレメント(行列成分)すべてを足し合わせます。その関数(sumMatrix())を以下のように定めて、引数には上記の2次元配列を渡すと、初めの子配列([1, 2, 3])の合計(6)しか得られません。
console.log(sumMatrix(matrix)); // 6
function sumMatrix(matrix: number[][]): number { var sum: number = 0; for (var i: number = 0; i < matrix.length; i++) { var currentRow: number[] = matrix[i]; for (var i:number = 0; i < currentRow.length; i++) { sum += currentRow[i]; } } return sum; }
関数(sumMatrix())は、入れ子のfor
文で親と同じ名前のカウンタ変数(i)をvar
宣言しています。すると、ブロックスコープがありませんので、同じ変数として扱われてしまうのです。その結果、つぎのコードと同じ処理になります。初めの子配列(currentRow)を子のfor
文が処理し終えたときに、カウンタ変数(i)の値は親for
ループの継続条件(i < matrix.length)を外れてしまったのです。
var sum: number = 0; var i: number = 0; var currentRow: number[] = matrix[i]; for (i = 0; i < currentRow.length; i++) { sum += currentRow[i]; } console.log(i); // 3 console.log(sum); // 6
for
文のカウンタ変数にvar
宣言を使うのでしたら、入れ子のカウンタ変数は名前を変えなければなりません。けれど、カウンタ変数をlet
宣言すれば、入れ子のfor
ループはブロックスコープが異なるので、別の変数として扱われます。関数の中でひとつしか宣言のない変数は、var
を用いても構いません。ただ、let
宣言なら、ブロックの外との重複など気にしなくて済みます。あえてvar
宣言を使わなければならない場合を除いて、let
を用いるのがよいでしょう。
console.log(sumMatrix(matrix)); // 45
function sumMatrix(matrix: number[][]): number { // var sum: number = 0; let sum: number = 0; // var宣言でも問題はなし // for (var i: number = 0; i < matrix.length; i++) { for (let i: number = 0; i < matrix.length; i++) { // var currentRow: number[] = matrix[i]; let currentRow: number[] = matrix[i]; // var宣言でも問題はなし // for (var i:number = 0; i < currentRow.length; i++) { for (let i:number = 0; i < currentRow.length; i++) { sum += currentRow[i]; // 外でlet宣言された変数は参照できる } } return sum; }
03 const宣言
const
宣言は定数を定めます。あとからデータの書き替えはできません。そのため、宣言とともに値を与えなければなりません。
const numLivesForCat = 9; // numLivesForCat = 10; // エラー // numLivesForCat++; // エラー
データの書き替えができないだけで、納めたオブジェクトの状態は変えられます。イミュータブル(immutable)とは異なることにご注意ください。
const numLivesForCat = 9; const kitty = { name: "Aurora", numLives: numLivesForCat, } /* エラー kitty = { name: "Danielle", numLives: numLivesForCat }; */ // エラーなし kitty.name = "Kitty"; kitty.numLives--; console.log(kitty); // { name: 'Kitty', numLives: 8 }
ブロックスコープをもつことは、let
宣言と同じです。データを書き替えないときは、const
を用いるのがよいでしょう。コードを見ただけで、書き替えが行われないとわかります。なお、クラスのプロパティ(メンバー)に、const
宣言はできません。readonly
修飾子を使うのがよいでしょう。
04 分割代入
分割代入(destructuring assignment)構文は、配列またはオブジェクトからデータを取り出して別の変数に代入する式です。ECMAScript 2015で備わりました。
04-01 配列の分割代入
変数を配列のかたちで宣言して、配列の要素の値を与えます。
let input = [1, 2]; let [first, second] = input; console.log(first, second); // 1 2
この分割代入は、つぎのJavaScriptコードにコンパイルされます。
var first = input[0], second = input[1];
分割代入は、宣言済みの変数に用いることもできます。変数値の入れ替えに使うと便利です。
[first, second] = [second, first]; console.log(first, second); // 2 1
関数の引数にも、分割代入の構文が使えます[*1]。
function f([first, second]: [number, number]): void { console.log(first, second); // 1 2 } f([1, 2]);
スプレッド演算子...
を用いれば、代入されなかった値が変数に配列エレメントとして納められます。
let [first, ...rest] = [1, 2, 3]; console.log(first, rest); // 1 [ 2, 3 ]
値を受け取る変数の数が足りなくても、エラーは起こりません。逆に、変数に納める値がないときはエラーになります。
let [first] = [1, 2, 3]; console.log(first); // 1 // let [one, two] = [1]; // 値が足りないのでエラー
変数は間引くことができます。
let [, second, , fourth] = [1, 2, 3, 4]; console.log(second, fourth); // 2 4
[*1] Handbook「Variable Declarations」の「Array destructuring」のサンプルコードでは、関数の引数に変数(input)を渡しています。その場合には、 引数と変数の型を合わせなければなりません。
// let input = [1, 2]; // 型をnumber[]とみなされてエラー let input: [number, number] = [1, 2]; function f([first, second]: [number, number]) { console.log(first, second); } f(input);
関数の引数の側の型づけを合わせることもできます。
let input = [1, 2]; // function f([first, second]: [number, number]) { function f([first, second]: number[]) { console.log(first, second); } f(input);
04-02 オブジェクトの分割代入
オブジェクトを使って分割代入することもできます。代入する変数はプロパティで判別されます。
let o = { a: 'apple', b: 2, p: 'pen' } let {a, p} = o; console.log(a, p); // apple pen
配列の場合と同じく、宣言した変数の値をあとから書き替えるのに用いることもできます。このとき式は、丸かっこ()
でくくってください。JavaScriptは{
を、ブロックの始まりと解釈してしまうからです。
({a, p} = {a: 'apple-pen', p: 'pineapple-pen'}); console.log(a, p); // apple-pen pineapple-pen
オブジェクトでも、スプレッド演算子...
が使えます。代入されなかったプロパティをもつオブジェクトが変数に納められます。
let {a, ...rest} = o; console.log(a, rest); // apple { b: 2, p: 'pen' }
代入先変数のあとにコロン:
を添えて名前が変えられます。型づけではありませんので、間違えないようにしてください。
let {a: ap, p: pp} = o; console.log(ap, pp); // apple pen // console.log(a, p); // 変数がないのでエラー
この代入は、つぎのJavaScriptコードにコンパイルされます。
var ap = o.a, pp = o.p;
型づけにはオブジェクト型リテラルをお使いください。
let {a, b}: {a: string, b: number} = o;
変数にはデフォルト値が与えられます。
let {ap, pp = 'pineapple'} = {ap: 'apple'}; console.log(ap, pp); // apple pineapple
04-03 関数の引数への利用
分割代入は、関数の引数に用いることもできます。つぎのコードは、引数に分割代入を使い、型の別名(type alias)で型づけしました。また、引数のひとつ(b)は省けます(「TypeScript入門 06: メソッド引数のデフォルト値と省略および定数を定める」02「メソッドの引数が省かれたときの処理を定める」参照)。
type C = {a: string, b?: number}; function f({a, b}: C): void { console.log(a, b); } f({a: 'apple'}); // apple undefined f({a: 'apple', b: 2}); // apple 2
分割代入で定めた引数にも、デフォルト値が与えられます(「TypeScript入門 06: メソッド引数のデフォルト値と省略および定数を定める」01「メソッドの引数にデフォルト値を与える」参照)。ただし、関数を引数なしで呼び出した場合に用いられるので、渡すオブジェクトのプロパティを省くとエラーになります。
function f({a, b} = {a: 'apple', b: 0}): void { console.log(a, b); } f(); // apple 0 // f({a: 'apple'}) // bがないのでエラー f({a: 'apple-pen', b: 2}); // apple-pen 2
前項04-02で述べたとおり、分割代入先の変数にもデフォルト値は定められます。
function f({a, b = 0} = {a: 'apple'}): void { console.log(a, b); } f(); // apple 0 f({a: 'apple-pen'}) // apple-pen 0 // f({b: 2}); // aがないのでエラー f({a: 'apple-pen', b: 2}); // apple-pen 2 // f({}); // aがないのでエラー
デフォルト値と引数省略の組み合わせにより、エラーになる場合が変わります。
function f({a = 'apple', b = 0}: {a?: string, b?: number}): void { console.log(a, b); } // f(); // 引数がないのでエラー f({a: 'apple-pen'}) // apple-pen 0 f({b: 2}); // apple 2 f({a: 'apple-pen', b: 2}); // apple-pen 2 f({}); // apple 0
組み合わせ方を工夫すれば、対応の幅は広げられます。ただ、コードが見づらくなり、結果が捉えにくくなる面もあります。それらも考え合わせたうえで、使い方を決めるのがよいでしょう。
function f({a = 'apple', b = 0}: {a?: string, b?: number} = {a:'apple'}): void { console.log(a, b); } f(); // apple 0 f({a: 'apple-pen'}) // apple-pen 0 f({b: 2}); // apple 2 f({a: 'apple-pen', b: 2}); // apple-pen 2 f({}); // apple 0
04-04 スプレッド演算子を用いる
スプレッド演算子...
を用いると、分割とは逆にデータがまとめられます。変数のデータはコピーされますので、もとの変数の値を書き替えても、変わることはありません。
let first = [1, 2]; let second = [3, 4]; let bothPlus = [0, ...first, ...second, 5]; first[0] = 100; console.log(first); // [ 100, 2 ] console.log(bothPlus); // [ 0, 1, 2, 3, 4, 5 ]
スプレッド演算子...
は、プロパティとその値を代入と同じように扱います。同じプロパティがあってもエラーにはならず、あとの値で上書きされるのです。
let defaults = {food: 'spicy', price: '$$', ambiance: 'noisy'}; let search = {food: "rich", ...defaults}; console.log(search); // { food: 'spicy', price: '$$', ambiance: 'noisy' }
したがって、デフォルト値を定めるプロパティは先に置くのがよいでしょう。
let defaults = {food: 'spicy', price: '$$', ambiance: 'noisy'}; let search = {...defaults, food: 'rich'}; console.log(search); // { food: 'rich', price: '$$', ambiance: 'noisy' }
クラスのインスタンスをスプレッド演算子...
で加えることもできます。ただし、メソッドは含められないことにご注意ください。
class C { p = 1; m() { return this.p; } } let c = new C(); let clone = { ...c }; console.log(c.m()); // 1 console.log(clone.p); // 1 // clone.m(); // メソッドは含まれないのでエラー console.log(clone); // { p: 1 }
作成者: 野中文雄
作成日: 2017年1月31日
Copyright © 2001-2017 Fumio Nonaka. All rights reserved.