変数の宣言
変数の宣言
let
とconst
は、比較的新しい形式のJavaScriptの変数宣言になります。
前の章で述べたように、let
はvar
に近しいものですが、
開発者がJavaScriptを動かす際に、"はまりやすい"問題を避ける手助けとなってくれます。
const
はlet
を強力にしたものであり、
変数への値の再割当てを禁止します。
TypeScriptはJavaScriptのスーパーセットであるため、
当然let
とconst
を言語としてサポートします。
ここで、これらの新しい宣言が、何故var
よりも推奨されるのかを詳しく説明します。
もし、あなたがJavaScriptを粗野に扱っているのであれば、
次のセクションはあなたの考えをリフレッシュする良い機会になるかもしれません。
もし、JavaScriptのvar
宣言の癖を完璧に理解しているのであれば、
次のセクションはスキップしても構いません。
var宣言
JavaScriptにおける変数の宣言には、常にvar
キーワードが使用されてきました。
var a = 10;
ご覧のとおり、これは10の値のa
という変数を宣言しているだけです。
また、関数内部に変数を宣言することも可能です。
function f() {
var message = "Hello, world!";
return message;
}
更に、別の関数内の変数に対してもアクセスすることが可能です。
function f() {
var a = 10;
return function g() {
var b = a + 1;
return b;
}
}
var g = f();
g(); // returns '11'
上記の例でg
には、f
の内部で宣言された変数が割り当てられています。
どの時点であれ、g
が呼び出されると、a
の値はf
内のa
の値に紐付けられます。
f
の実行が完了したすぐ後にg
が呼び出れさたとしても、
a
のアクセスと変更は可能になります。
スコープの規則
var
宣言は、他の言語のそれと比べて幾つか奇妙なスコープの規則を持ちます。
下記の例をご覧ください。
function f(shouldInitialize: boolean) {
if (shouldInitialize) {
var x = 10;
}
return x;
}
f(true); // returns '10'
f(false); // returns 'undefined'
一部の読者は、この例に驚いてもう一度見なおしてしまうかもしれません。
一般的には、変数x
がif
ブロック内で宣言されたのであれば、
外部のブロックからは、これにアクセスすることは出来ないでしょう。
これはvar
宣言が、含まれる関数、モジュール、名前空間、グローバルスコープ、
これら全ての含まれるブロックに関係なく、後からどこからでもアクセス可能であるためです。
一部の人はこれをvarスコープ(var-scoping)、または関数スコープ(function-scoping)と呼びます。
引数もまた関数にスコープされます。
これらのスコープ規則が、ある類のミスを招きます。 実際に事態を悪化させる要因に、複数回同じ変数を宣言してもエラーにならないことが挙げられます。
function sumMatrix(matrix: number[][]) {
var sum = 0;
for (var i = 0; i < matrix.length; i++) { //iを宣言
var currentRow = matrix[i];
for (var i = 0; i < currentRow.length; i++) { //ここでもiを宣言
sum += currentRow[i];
}
}
return sum;
}
i
は同じ関数内のスコープの変数を参照するため、内側のforループは意図せずi
変数を上書きしてしまいます。
経験豊富な開発者であればご存知だと思いますが、
このような種のバグはコードレビューをすり抜け、我々のフラストレーションの原因になることがあります。
奇妙な変数への割り当て
下記のスニペットが何を出力するのが、即答してみてください。
for (var i = 0; i < 10; i++) {
setTimeout(function() {console.log(i); }, 100 * i);
}
不慣れな方のためにアドバイスすると、setTimeout
は指定されたミリ秒後に関数の実行を試みます。
(何かを待つ間、実行を停止します。)
それでは結果を見てみましょう。
10
10
10
10
10
10
10
10
10
10
多くのJavaScript開発者はこの挙動には慣れているのでしょうが、 あなたが驚いていたとしても、それは無理もないことです。 多くの人は次のような出力を期待したのではないでしょうか。
0
1
2
3
4
5
6
7
8
9
先程説明した変数割り当てのことを思い出してください。
どの時点であれ、g
が呼び出されると、a
の値はf
内のa
の値に紐付けられます。
この状況について少し考えてみましょう。
setTimeout
は数秒後に実行されますが、
これはfor
ループが実行を停止した後でもあります。
for
ループの実行が完了すると、i
の値は10になります。
そのため、与えられた関数が実行されるタイミングでは、出力される値は10になります。
一般的な解決策として、IIEF(Immediately Invoked Function Expression - 即時実行関数)を使用して、
各繰り返し時にi
を割り当ててしまいます。
for (var i = 0; i < 10; i++) {
// 現在の値で関数を実行することで、現在の'i'の状態を割り当てます。
(function(i) {
setTimeout(function() { console.log(i); }, 100 * i);
})(i);
}
奇妙なパターンに見えますが、実際にはよく見かけるものです。
引数のi
は実際にはfor
ループ内で宣言されたi
に紐付きますが、
同じ名前を付けているため、ループの本文を変更する必要はありませんでした。
let宣言
ここまでで、あなたはvar
が持つ幾つかの問題と、
変数を定義する新しい方法であるlet
文が何故必要かについて理解したものと思います。
使用方法はさておき、let
文はvar
文と同じ方法で書かれます。
let hello = "Hello!";
違いの核心は文法ではなく、その意味付け(semantics)にあります。 これから、その核心について触れていきます。
ブロックのスコープ
let
を使用して宣言された変数は、「レキシカルスコープ」または「ブロックスコープ」と呼ばれるものを使用します。
var
で宣言された変数のスコープは、それが含まれる関数外に漏れてしまいますが、
let
のブロックスコープの変数は直近のブロックまたはforループの外からアクセスすることは出来ません。
function f(input: boolean) {
let a = 100;
if (input) {
// aの参照はOK
let b = a + 1;
return b;
}
// エラー: bはここには存在しない
return b;
}
ここに、a
とb
の2つのローカル変数があります。
a
のスコープはf
の本文内に限定され、
一方b
のスコープはif
文のブロック内に限定されます。
catch
節の中で宣言された変数も、同様のスコープ規則を持ちます。
try {
throw "oh no!";
}
catch (e) {
console.log("Oh well.");
}
// エラー: 'e'はここには存在しない
console.log(e);
ブロックスコープされた変数のその他の特徴として、実際に変数が宣言される前には、
読み込み・書き込みが出来ないことが挙げられます。
そういった変数がスコープで"提示"されている区間は、宣言されるまでは一時的なデッドゾーンとなってしまいます。
端的に言えば、let
文の前ではアクセス出来ないということですが、
幸いにもTypeScriptはそのことを伝えてくれます。
a++; // illegal to use 'a' before it's declared;
let a;
ただし、宣言される前にブロックスコープされた変数にアクセス出来てしまうケースがひとつだけ存在するので注意が必要です。 それは、宣言前の非合法な関数の呼び出しです。 もしES2015が対象であれば、最新のランタイムはエラーをスローしますが、 現状のTypeScriptはこれを許可し、エラーを報告しません。
function foo() {
// 'a'へのアクセス可能
return a;
}
// 非合法な宣言前の'foo'の呼び出し
// ランタイムはここでエラーをスローするべき
foo();
let a;
一時的なデッドゾーンの詳細については、 Mozilla Developer Networkの関連記事を参照してください。
再宣言とシャドーイング
var
宣言については、前述したとおり同じ変数を何回宣言しても問題にはなりません。
function f(x) {
var x;
var x;
if (true) {
var x;
}
}
上記の例では、全てのx
の宣言は実際には同じx
を参照し、全く問題のない有効な処理文です。
ただし、これがバグの発生源になることが度々あります。
幸いなことに、let
宣言はこれを許しません。
let x = 10;
let x = 20; // error: can't re-declare 'x' in the same scope
TypeScriptが問題があることを我々に伝えるために、必ずしも両方の変数がブロックスコープされる必要はありません。
function f(x) {
let x = 100; // error: interferes with parameter declaration
}
function g() {
let x = 100;
var x = 100; // error: can't have both declarations of 'x'
}
ブロックスコープされた変数が、関数スコープされた変数で宣言出来ないというわけではありません。 ブロックスコープされた変数は、明確に異なるブロックで宣言される必要があるということです。
function f(condition, x) {
if (condition) {
let x = 100;
return x;
}
return x;
}
f(false, 0); // returns '0'
f(true, 0); // returns '100'
より深いスコープで新しい名前が割り当てられることを、シャドーイング(shadowing)と呼びます。
これは特定のバグを防ぐ一方で、シャドーイング自身のイベントで、意図しない特定のバグを生んでしまう可能性があるという点で、
諸刃の刃の要素を持ちます。
例えば、先の例で書いたsumMatrix
関数にlet
変数を使用したとしましょう。
function sumMatrix(matrix: number[][]) {
let sum = 0;
for (let i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (let i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}
ここでのループは内部ループのi
が外部ループからシャドー(shadow)されるため、
実際に正しく動作します。
綺麗なコードを保つために、シャドーイングの使用は通常であれば避けるべきです。 何らかの状況でこれが相応しいケースもあるかもしれないので、それはあなたの判断のもとに使用されるべきでしょう。
ブロックスコープされた変数の捕捉
我々はまず初めに、var
宣言された変数の振る舞いについて、
その変数がどのように捕捉されるのかについて学習しました。
これをより直感的に言い表すと、スコープが実行される度に変数の「環境」を作り出している、と言えます。
その環境と補足された変数は、そのスコープ内の実行が完了した後も存在し続けることができます。
function theCityThatAlwaysSleeps() {
let getCity;
if (true) {
let city = "Seattle";
getCity = function() {
return city;
}
}
return getCity();
}
その環境内でcity
を捕捉しているため、
if
ブロックの実行が完了しているにも関わらず、それにアクセスすることが可能です。
先ほどのsetTimeout
の例を思い出してください。
for
ループの各繰り返し時点での変数の状態を補足するために、最終的にはIIFE(即時実行関数)を使用する必要がありました。
実際に我々は、変数を補足するための新しい変数の環境を作成したということです。
少々煩雑ではありましたが、幸運なことにTypeScriptがあればこのようなことを行う必要はありません。
let
宣言は、ループ時に宣言される際の挙動が根本的に異なります。
ループ自身に新しい環境を導入するのでは無く、
繰り返し時に毎回新しいスコープを作成します。
今まではIIFE(即時実行関数)を使用してこれを行っていたので、
古いsetTimeout
の例をlet
宣言を使用したものに変更してみましょう。
for (let i = 0; i < 10 ; i++) {
setTimeout(function() {console.log(i); }, 100 * i);
}
期待通りに出力されるでしょう。
0
1
2
3
4
5
6
7
8
9
const宣言
const
は、これまでの変数宣言とは異なるものです。
const numLivesForCat = 9;
let
宣言のようですが、一度値が割り当てられると、それを変更することはできません。
言い換えると、let
と同じスコープ規則を持ちますが、それらへの再割当てはできません。
参照値が不変(immutable)であるという考え方と混同しないでください。
const numLivesForCat = 9;
const kitty = {
name: "Aurora",
numLives: numLivesForCat,
}
// エラー
kitty = {
name: "Danielle",
numLives: numLivesForCat
};
// 全てOK
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;
あなたがそれを避けるための特定の評価方法を取らない限り、
const
変数の内部の状態の変更は可能なままです。
let vs const
我々には同様のスコープ付けを持つ2つの種類の宣言が与えられましたが、 どちらを使用するかは当然自分たちで決める必要があります。 このような質問に関する答えは、決まって「場合による」です。
「権限を最小にする」という信念に従うのであれば、変更する予定の無い全ての宣言にはconst
を使用するべきです。
基本的には、もし変数への書き込みが不要だった場合、
同じコード上で他の動作がオブジェクトへの自動的な書き込みをするべきではないのであれば、
その変数への再割当てが本当に必要かどうかを考慮する必要があるでしょう。
const
を使用することで、データのフローを判断する際にコードがより予測しやすいものになります。
一方で、let
があれば、もはやvar
を使用する理由はなく、
多くのユーザーはこの簡潔さを気に入ることでしょう。
このハンドブックの大半は、この恩恵を受けるためにlet
宣言を使用しています。
あなたのベストな判断に従い、可能であるならば、チームの残りのメンバーとこのことについて話合ってみてください。
分割代入
TypeScriptが持つECMAScript2015の別の機能に分割代入(destructuring)があります。 詳細については、Mozilla Developer Networkの記事を参照してください。 このセクションでは、手短かに概要を説明します。
配列の分割代入
分割代入の最もシンプルな形式は、配列の分割代入です。
let input = [1, 2];
let [first, second] = input;
console.log(first); // 1を出力
console.log(second); // 2を出力
これはfirst
とsecond
という2つの新しい変数を作成しています。
これはインデックスを使用した場合と同じですが、それに比べるとはるかに便利でしょう。
first = input[0];
second = input[1];
既に宣言された変数への代入分割も同様です。
// 変数の交換
[first, second] = [second, first];
また、関数への引数としても使用できます。
function f([first, second]: [number, number]) {
console.log(first);
console.log(second);
}
f(input);
...name
文法を使用して、リスト内の残りの項目のための変数を作成することができます。
let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // 1 を出力
console.log(rest); // [ 2, 3, 4 ] を出力
これはJavaScriptであるため、当然残りの要素を気にせずに無視することも可能です。
let [first] = [1, 2, 3, 4];
console.log(first); // 1 を出力
他の要素のために次のようにすることも可能です。
let [, second, , fourth] = [1, 2, 3, 4];
オブジェクトへの分割代入
オブジェクトへの分割代入も可能です。
let o = {
a: "foo",
b: 12,
c: "bar"
}
let {a, b} = o;
これは、o.a
とo.b
から、新しい変数であるa
とb
を作成します。
必要が無ければc
をスキップすることが可能です。
配列の分割代入のように、宣言無しで割り当てることも可能です。
({a, b} = {a: "baz", b: 101});
この文を括弧で囲んでいることに注目してください。
JavaScriptは通常、ブロックの始まりとして{
を解析します。
...
の構文を使用して、オブジェクト内の残りの項目の変数を作成することができます。
let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;
プロパティの改名
また、プロパティに異なる名前を割り当てることも可能です。
let {a: newName1, b: newName2} = o;
文法が紛らわしくなってきましたね。
これは、a: newName1
は「a
をnewName1
とする」と読むことができます。
左から右への書き方をするとすれば、下記と同義になります。
let newName1 = o.a;
let newName2 = o.b;
紛らわしいですが、ここでのコロンは型を示すものではありません。 もし型を指定したいのであれば、現状ではまだ分割代入の後に書く必要があります。
let {a, b}: {a: string, b: number} = o;
デフォルト値
デフォルト値は、プロパティが未定義(undefined
)の場合にデフォルトの値を指定するものです。
function keepWholeObject(wholeObject: {a: string, b?: number}) {
let {a, b = 1001} = wholeObject;
}
ここでは、keepWholeObject
はb
がundefined
であっても、
変数wholeObject
はa
だけでなく、a
とb
を持ちます。
関数宣言
また、分割代入は関数の宣言でも動作します。 分かりやすいように、単純な例を見てみましょう。
type C = {a: string, b?: number}
function f({a, b}: C): void {
// ...
}
ただし、引数のためのデフォルト値を指定することはより一般的であり、 分割代入によってデフォルト値を右辺より取得するのは、扱いづらい傾向があるでしょう。 まず第一に、デフォルト値を割り当てる前に、型を指定することを忘れないようにする必要があります。
function f({a, b} = {a: "", b: 0}): void {
// ...
}
f(); // ok, default to {a: "", b: 0}
次に初期化の処理の代わりとなるプロパティへの分割代入で、
任意のプロパティに対してデフォルト値を指定することを覚えておく必要があります。
C
はb
を任意プロパティとして定義していたことを思い出してください。
function f({a, b = 0} = {a: ""}): void {
// ...
}
f({a: "yes"}) // OK, bにはデフォルト値0を代入
f() // OK, デフォルトは{a: ""}となり、bにはデフォルト値0を代入
f({}) // エラー, 引数を指定するのであれば'a'は必須
分割代入は注意して使用するようにしてください。 前述した例のように、分割代入の式はシンプルなどころか厄介なケースが数多く存在します。 特に、これは入れ子の深い分割代入のケースで当てはまり、 改名、デフォルト値、型アノテーションが無かったとしても解析するのは本当に困難です。
分割代入の式は、小さくシンプルにすることを心がけてください。
スプレッド(Spread)演算子
スプレッド(Spread)演算子は分割代入とは正反対のものです。
これは、配列を別の配列へ、またはオブジェクトを別のオブジェクトへ、広げるような(展開する)ことを可能にしてくれます。
下記はその例になります。(訳注: ...
がスプレッド演算子)
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
bothPlus
には[0, 1, 2, 3, 4, 5]
の値が割り当てられます。
スプレッド(spread)すると、first
とsecond
のシャローコピーが作成されます。
first
とsecond
は、スプレッドによって変更されません。
また、オブジェクトをスプレッドすることも可能です。
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };
search
は、{ food: "rich", price: "$$", ambiance: "noisy" }
になります。
オブジェクトのスプレッドは、配列よりも複雑です。
配列のスプレッドと同様に左から右に進みますが、あくまでもオブジェクトのままです。 つまり、スプレッドのオブジェクトの後に来るプロパティは、先に来たプロパティを上書きします。 そのため、末尾にスプレッドが来るように先程の例を修正すると、
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };
default
のfood
プロパティは、food: "rich"
を上書きし、我々が望む結果とは異なる結果になります。
オブジェクトのスプレッドには他にも驚くべき制限がいくつかあります。 1つ目はそれが自身の列挙可能なプロパティのみを含むということです。 要するに、オブジェクトのインスタンスをスプレッドすると、メソッドを失うこということです。
class C {
p = 12;
m() {
}
}
let c = new C();
let clone = { ...c };
clone.p; // ok
clone.m(); // error!
2つ目はTypeScriptのコンパイラが、ジェネリクス関数からの型パラメーターのスプレッドを許さないことです。 この機能は、将来のバージョンでサポートされる可能性があります。
© https://github.com/Microsoft/TypeScript-Handbook
このページは、ページトップのリンク先のTypeScript-Handbook内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。
- ドキュメントの情報が古い可能性があります。
- "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。