uhyohyo.net

JavaScript初級者から中級者になろう

十六章第三回 代入

ES6では、代入が進化しました。

従来代入といえば、

a = 3;
var b = "foo";

のように変数に代入するか、あるいは

var obj={};
obj.c=true;
obj["d"]=null;

のようにプロパティに代入するかの2通りでしたね。

ES6でも基本は変わりませんが、オブジェクトや配列を展開して代入するということが可能になりました。

destructuring assignment

具体的には、次の例を見てください。

var [a, b] = ["foo", "bar"];

左辺が変数ではなく何か配列っぽい感じになっています。どういう動作になるかは何となく直感的に分かると思います。この例ではa"foo"が入り、b"bar"が入ります。なお、

var [a, b, c] = ["foo","bar"];

のように要素の数が合わない場合、余った変数にはundefinedが入ります。この場合はcですね。逆に右辺の配列のほうが長い場合、余ったところは捨てられます。

また、ここでは右辺に配列リテラルを直接書きましたが、もちろん任意の配列を与えることができます。関数の返り値なども当然指定できますから、配列を用いて関数が擬似的に複数の値を返すとか、そういうことも可能ですね。

また、配列っぽい感じのほかに、オブジェクトっぽい感じの代入も可能です。

var {c,d} = {
  c:"foo",
  d:"bar"
};

オブジェクトっぽい感じで変数名を書いて、右辺に同じ名前のプロパティを持つオブジェクトを渡せば、各変数に入ります。

オブジェクトのプロパティ名とは違う名前の変数に入れたいという場合、次のように書きます。

var {c:foo, d} = {
  c:"foo",
  d:"bar"
};

この場合、cではなくfooという変数にオブジェクトのcプロパティの値が入ります(すなわち、変数fooに"foo"が入り、変数dに"bar"が入ります)。

この場合も、与えられたオブジェクトに該当のプロパティがない場合はundefinedが入ります。

ここまではvar文で説明しましたが、別にvar文でなくても代入可能です。例えば次の文も動作します。

[e,f]=[1,2];

ただし注意しなければいけないのは、次は文法エラーになります。

{g,h}={g:3, h:4};

その理由は、最初に{が現れた時点でブロック文と解釈され、=のところでエラーが生じるからです。ブロック文と誤認されないように例えば次のように書けば動作します。

({g,h}={g:3, h:4});

なお、=は演算子という話を以前にしました。このような代入文の場合でも、演算子の返り値は右辺の値になります。

このように代入演算子の左辺に配列っぽいものやオブジェクトっぽいものを持ってくる方法をdestructuring assignment(分解代入?)といいます。

さらに、配列やオブジェクトをネストさせることが可能です。同じ形のオブジェクトを渡せばそのとおりに値が入ります。

var [a, b, [c, d]] = [0, 1, [2, 3]];

また、左辺が配列っぽい形のときは、右辺は実はiterableである必要があります。iterableでなければエラーが発生します。逆に言えば、iterableならば配列でなくてもOKです。例えば文字列がiterableであることを利用すると、次のようなことが可能です。

[a,b,c,d,e] = "foobar";

他にも、変数名を書かないことでその部分を飛ばすことができます。

[a,,b]= [1,2,3];

この例ではaには1が、bには3が入ります。もっとも、このように,を連続させて配列リテラルを構成することは、代入文に限らずもともと可能です。例えば、

console.log([1,,,2]);

としてみれば分かりますが、これは[1,undefined,undefined,2]という配列と同等です。

最後に、配列の「残りの部分」をまとめて得る方法というのがあります。これは次のように書きます。

[a,b,...c] = [1,2,3,4,5];

こうすると、aには1が、bには2が、cには[3,4,5]という配列が入ります。このように配列っぽいやつの最後に...をつけた変数名を書くと、そこに配列が入ります。何も残っていない場合は空の配列ですね。なお、右辺が配列ではないiterableの場合でも、ここで得られるのは配列です。

destructuring assignmentの応用

以上で説明したことは、単なる代入文以外の場面でも使う機会があります。例えばfor-in文やfor-of文です。

var arr=["foo","bar","baz"];
for(var [i,val] of arr.entries()){
  console.log(i+": "+val);
}

この結果は次のようになります。

0: foo
1: bar
2: baz

前回紹介したArray#entriesと組み合わせることで、for-of文で添字付きで配列を回すのが簡単になります。

さらに、これは関数の引数にも使うことができます。次のように宣言した関数を考えます。

function foo({a, b, c}){
  console.log(a, b, c);
}

この関数fooは次のように使うことができます。

foo({
  a: 3,
  b: "bar",
  c: false
});

結果として3 "bar" falseのようにコンソールに表示されることでしょう。このように、関数宣言の引数に配列っぽいものやオブジェクトっぽいものを入れてやると、その部分に渡された引数が展開されて入ります。上の例では、変数a,b,cは通常の引数と同様にローカル変数になります。

ほかに、destructuring assignmentとは直接関係ありませんが、先ほど紹介した...の記法は引数にも使うことができます。引数の最後をこの記法にすると、残りの引数が配列として得られます。

function bar(a,b,...c){
  console.log(a,b,c);
}
bar(1,2,3,4,5);

このコードの場合は、1 2 [3,4,5]と表示されるでしょう。3つ目以降の引数が配列としてまとめられて引数cに入ったことになります。これにより、任意個の引数をとる関数などが書きやすくなります。

さらに、ES6では関数の引数に初期値を設定できるようになりました。これは、引数が与えられなかった場合、undefinedが入る代わりに初期値として指定しておいた値を入れるものです。引数名のあとに=を続けて書くことで初期値を指定します。

function baz(a = 5,b = 3){
  console.log(a,b);
}
baz(0);

この例では、関数bazに2つ目の引数bが与えられなかったので、初期値の3が入ります。よって結果は0 3となります。なお、引数としてundefinedを渡した場合は引数が渡されなかった扱いになり初期値が適用されます。nullの場合はされません。

もちろん、一部の引数にだけ初期値を設定することも可能です。いかなる場合も、関数に与えられた引数は前の引数から順に適用されます。

ここでやっと話が戻るのですが、この初期値はdestructuring assignment的な引数と組み合わせることができます。すなわちこうです。

function quux({a,b=3,c} ={}){
  console.log(a,b,c);
}
quux({a:5});

結果は5 3 undefinedです。ここでは初期値が二段構えになっていることに注意してください。まず{a,b=3,c}に対する初期値が{}です。これにより、quuxが引数なしで呼び出された際に、引数{}で呼び出されたのと同様になります。さらに、その引数を展開して変数a,b,cを代入しています。

さらに話を戻すと、実はこの初期値の指定は関数の引数に限らず行うことができます。すなわち、

var {a,b=3,c} = obj;
のようなことができます。

let宣言とconst宣言

さて、ここで新しい文法を紹介します。それはlet宣言(Let declaration)です。これはvar文とほぼ同様の文法を持ちます。varをletに変えるとlet宣言になります。

let宣言はvar文と同様にローカル変数を宣言するのですが、そのスコープ(範囲)が違います。

var文は、そのスコープは関数内ということになっています。つまり、関数内ならどこにvar文を書いても関数内全体で通用します(そして、関数の外には影響を与えません)。例えば、

function foo(a){
  if(a){
    var x=5;
  }
  console.log(x);
}

この関数は、aがtrueなら5を、aがfalseならundefinedを出力します。

関数に入った時点でその中のvar文が全て調べられて、その関数内をスコープとして変数が初期化されます。この場合は変数xですね。ただし値が実際に代入されるのはvar文が実行された時点なので、この時点では変数xが作られただけで、その値はundefinedです。aが真のときはif文に入り、xに5が代入されます。この関数の動作はこのように説明できます。

さて、let宣言の場合は、スコープがブロック内に狭められています。ブロックとは、{ }で囲まれたコードです。関数宣言の{ }は少し違いますが、letで作った変数も関数の外に漏れることはありません。他にも、if文の{ }がブロックです。そこで、前の関数のvarをletに変えた場合を考えます。

function foo(a){
  if(a){
    let x=5;
  }
  console.log(x);
}

この場合、fooを実行するとconsole.logのところでエラーが発生します。let文でxを宣言した場合はそのスコープはif文のところのブロックにとどまり、その外には見えないのでconsole.logのところではxは宣言されていないことになるからです。次の例を考えましょう。

function foo(a){
  let x=3;
  if(a){
    let x=5;
  }
  console.log(x);
}

この場合、aが真だろうと偽だろうと、表示されるのは3です。こえは、if文のところのブロックの中のlet宣言で宣言されたxが、外のxに影響を及ぼさないからです。

let文により、変数のスコープをより細かく管理することができます。

ちなみに、if文などがなくてもブロックを作ることができます。


let x=3;
{
  let x=5;
}
console.log(x);

これがブロックと呼ばれるもので、文と同様に扱われます。ブロックの働きはもちろん、中の文を全て実行することです。

本来if文やfor文などは処理部分を1文しか書くことができません。その1文にブロックを当てはめることで複数の文を処理させているのです。if文には

if(a==null)a={};

のようにブロックを使わない書き方もありますが、これはそういう理由によるものです。


さて、let宣言の他にconst宣言というのもあります。これは、letをconstに変えただけです。

constとは定数のことで、const宣言により作られた変数は値が変わらなくなります。すなわち、値を代入しても変わりません(strictモードのときはエラーになります)。スコープはlet宣言と同じです。

const宣言にはある変数が定数であると分かりやすくする効果があります。

function foo(){
  const a=3;
  a=5;
  console.log(a);
}
foo();

これの実行結果は3となります。

なお、注意すべきなのは、オブジェクトをconstで宣言してもオブジェクトの中身は変わる可能性があるということです。

function bar(){
  const obj={a:3,b:5};
  obj.a=6;
  console.log(obj);
}
bar();

こうすると、{a:6,b:5}という結果になるでしょう。これは、obj自体に代入しなければOKだからです。

なお、for文でvarの代わりにletを使うことも可能です。

for(let i=1;i<=10;i++){
  console.log(i);
}

こうすると、1から10まで表示されます。

このletは特別で、スコープはそのfor文の中になります。すなわち、let i=1;i<=10i++というforを構成する各式及び{ console.log(i); }という本体部分にわたってこのiが使用可能です。その外にはもれないので、for文が終わるとiはなくなります。これはvarの場合と対照的です。

for(var i=1;i<=10;i++){
}
console.log(i); //11が表示される

varの場合は、for文で宣言したものであってもスコープはやはり関数内となり、for文が終わっても変数を参照可能です。

ちなみに、ここもletの代わりにconstを使うことができますが、特に意味はないでしょう。