uhyohyo.net

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

十六章第三回 代入

このページの最終更新日:

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

従来、代入といえば、


a = 3;
var b = "foo";

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


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

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

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

destructuring assignment

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


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

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

つまり、代入の左辺に配列のように変数を並べたものを置いてやり、右辺に配列が来た場合、配列の形に合わせて各変数にまとめて代入されるのです。

なお、

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

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

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

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


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

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

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

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

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

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


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

ここまでは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文で添字付きで配列を回すのが簡単になります。

さらに、これは関数の引数にも使うことができます。次のように宣言した関数を考えます。この関数は、第1引数が変数名ではなくオブジェクトの分解代入の形になっています。


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

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

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

結果として3 "bar" falseのようにコンソールに表示されることでしょう。引数として渡されたオブジェクトが分解され3つのローカル変数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に入ったことになります。これにより、任意個の引数をとる関数などが書きやすくなります。これを使えば以前紹介したargumentsを使う必要もなくなりますね。

さらに、ES2015では関数の引数に初期値を設定できるようになりました。これは、引数が与えられなかった場合、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を代入しています。このときbには初期値が設定されているため、渡されたオブジェクトにプロパティbが無かった場合は初期値が変数bに代入されます。

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


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文はif文の中にありますが関係ありません。関数内に書いたvar文はどこに書いてあろうとも関数全体をスコープとして変数が初期化されます。ただし値が実際に代入されるのはvar文が実行された時点なので、この時点では変数xが作られただけで、その値はundefinedです。aが真のときはif文に入り、xに5が代入されます。

つまり、上の関数は次のように書いたのと同じということです。


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

なので、fooは引数aが真のときは5を表示し、偽のときundefinedを表示するでしょう。

一方、let宣言の場合は、スコープがブロック内に狭められています。ブロックとは、{ }で囲まれたコードです。厳密には関数宣言の{ }はブロックではありませんが、let宣言の場合はこれも同様に扱います。よって、letで作った変数はvarで作った変数と同様、関数の外に漏れることはありません。なお、例えばif文の{ }もブロックです。そこで、前の関数のvarをletに変えた場合を考えます。


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

この場合、fooを実行するとconsole.logのところでエラーが発生します。なぜなら、このlet宣言により作られた変数xはif文のところのブロックのスコープに存在する、すなわちこのブロックの中でのみ有効だからです。ブロックの外に変数xは存在しないので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文の中で使われている変数xは関数のスコープでletで宣言されたxを指します。


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

let宣言により、変数のスコープをより細かく管理することができます。基本的に、変数のスコープがブロックに制限されていても困ることはありません。広いスコープが欲しければ、それ相応の位置で宣言すればいいのです。あなたは今let宣言を知ってしまったので、今後varを使う必要はもはや無いでしょう。

varを書くことができる場所は全てletを書くことができます。例えば、for文でvarを書くことがありましたが、それもletにできます。


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

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

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


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

しかし、letはできません。for文が終了した時点で変数iが有効なスコープの外に出てしまうからです。

ついでに、これはいままで紹介していなかった気がするのでここで解説していますが、if文などがなくてもブロックを作ることができます。


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

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

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


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

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

const宣言

さて、let宣言の仲間としてconst宣言というのもあります。これは、letをconstに変えただけで、letと同様にブロックスコープの変数を作ります。

constとは定数のことですね。constの特徴は、const宣言により作られた変数は再代入できないということです。一度変数を作ったあとは、再び代入しようとしても何も起きません(strictモードのときはエラーになります)

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}という結果になるでしょう。constはあくまで変数への代入(obj = なんとか)を制限するのみで、変数に対するその他の操作は一切制限されないのです。

また、constで変数を作るときは必ず初期化する必要があります。つまり、const x;のようにその場で代入しない宣言はできません。これは当然ですね。一度作ったら再代入できないのだから、最初に代入しないと何の意味もない変数になります。

constなんで何に使うんだと思うかもしれませんが、実践的なプログラムを書くときは意外とconstが使われる機会が多いです。一度変数に値を代入したあと、再度代入する機会は実は多くありません。必要がないのに再代入可能なのはバグの元なので、再代入する気がない変数はconstで宣言しましょう。そうすると、ものにもよりますが、プログラムの変数宣言の9割くらいはconstになります。

変数の再宣言

letとconstには、varとは違う特徴がもう1つあります。それは、変数の再宣言ができないということです。

実は、varでは同じ変数を何回も宣言することができました。


var a = 3;
console.log(a);
var a = 5;
console.log(a);
var a = 10;
console.log(a);

このようにvarで何回も変数を宣言した場合、2回目以降のvarは無視されます。つまり、上のプログラムはこれと同じ意味です。


var a = 3;
console.log(a);
a = 5;
console.log(a);
a = 10;
console.log(a);

一方、letやconstでは、同じスコープで同じ変数を宣言するのはエラーになり、できません。同じ変数を何度も宣言するなんてことはふつうはやりませんしバグの元なので、このほうがいいですね。

なお、もちろん違うスコープなら同じ名前の変数を宣言できます。次の例はOKです。


let a = 3;
if (a > 0){
  let a = 5;
  console.log(a);
}else{
  const a = 10;
  console.log(a);
}

この講座でも以降はletやconstが登場してきます。