WEB制作、アプリ開発において、調べごとをしている時に、他の人の書いたコードを見る機会があると思いますが、「この構文なんだ??見たことない・・・」となったりしませんか?
JavaScriptは、元々はブラウザ上でちょっとした処理をさせる為だけのものでしたが、様々な機能拡張に伴って、モダンな構文が用意されるようになり、同じ処理内容でもより簡潔に記述することができたり、他のコンパイラ言語のような機能を使用出来たりします。
今回記事では私が初心者のときにモダンな構文を知らず、よく躓いたものを重点的に紹介していきます。
ちょっと小難しい言葉が並ぶけど、使いこなせるようになればめっちゃ便利なものばかりですよ!
let, const
varの問題点
JavaScriptではvar
で変数を宣言・定義することができる…のですが、それは昔の話です。var
を使った変数の宣言・定義は次のような問題があり、よほど小規模の開発でない限り、今は使うべきではありません。
- 意図しない変数の上書き
var
による変数の宣言は、同じ変数名を再度宣言することができるため、意図せず変数を上書きしてしまう可能性があります。
var x = 10;
var x = 20; // エラーにならず、上書きされる
コードが長くなった時等に同じ名前の変数が存在するのに気付かずに再宣言してしまい、バグの原因となることがあります。
- 変数のスコープの問題
スコープとは変数の参照できる範囲の事で、C言語等では中括弧{}
内で宣言された変数はその内部でしか参照できません。
var
で宣言された変数のスコープは関数の内部がスコープとなります。
var foo = 'bar';
if (true) {
var foo = 'baz'; // 1行目で宣言した変数を上書きしてしまう}
console.log(foo); // 'baz' が出力される
この変数foo
のように、if
文やfor
文の中で一時的に使用する為に確保したつもりの変数でも、そのスコープは中括弧{}
の外側と同じなので、前に宣言した変数を上書きしてしまったり、そのブロックを抜けても参照できてしまったりと、バグの原因になりえます。
- 変数の巻上げ
下の例のように、変数の宣言よりも前に、変数を参照しようとするのは通常はエラーとなるべきです。しかし、var
による変数の宣言の場合はエラーとならずに処理が進んでしまいます。
console.log(x); // エラーにならず undefined が出力されるvar x = 10;
このコードは次のコードを実行したのと同様となります。同じスコープ内で宣言している変数は、そのスコープの先頭で暗黙的に変数宣言をしてしまいます。
var x; // 変数xを暗黙で宣言しているconsole.log(x); // undefined
var x = 10;
let, constを使う
これらの問題点があるvar
の代わりに、let
およびconst
が導入されました。let
は再代入可能な変数を宣言するために使われ、const
は再代入不可能な変数を宣言するために使われます。
let count = 10;
count = 20; // OK
const pi = 3.14;
pi = 3.14159; // エラー
let
、const
にはvar
による変数宣言の問題点を解決するために、次のような特徴があります。
- 同一スコープ内での変数の再定義は不可
let a = 10;
let a = 20; // エラー: 変数aはすでに宣言されています
const b = 30;
const b = 40; // エラー: const変数bは再宣言できません
- スコープは中括弧
{}
内部(ブロックスコープ)
if (true) {
// このif文の中括弧内でのみ有効な変数
let x = 10;
const y = 20;
}
console.log(x); // エラー: xは未定義console.log(y); // エラー: yは未定義
- 変数を宣言前に参照しようとするとエラーとなる
console.log(x); // エラー: xは宣言されていないlet x = 10;
アロー関数
アロー関数とは、関数式を手軽に書くことができる記法です。
例えば次のような関数式があります。
const hoge = function (a, b) { return a + b;
}
これをアロー関数に書き換えると、次のように煩わしいfunction
を書かずに済みます。その代わりに引数と関数本体の間にアロー=>
を入れます。
const hoge = (a, b) => {
return a + b;
}
直接単一の式を返すのみの場合は中括弧{}
を省略でき、引数が一つのみの場合は括弧()
を省略できます。
// 単一の式 a + b をreturnする
const hoge = (a, b) => a + b;
// 引数が a のみ
const fuga = a => {
retunr a * 2;
}
分割代入
分割代入を使うと、配列やオブジェクトから複数の値を簡単に取り出すことができます。
通常は配列は添字を用いて、オブジェクトはプロパティを指定して値を取り出します。
const numbers = [1, 2, 3];
const first = numbers[0];
const second = numbers[1];
console.log(first); // 1 が出力される
console.log(second); // 2 が出力される
const person = {
firstName: 'Alice',
lastName: 'Smith'
};
const firstName = person.firstName;
const lastName = person.lastName;
console.log(firstName); // 'Alice' が出力される
console.log(lastName); // 'Smith' が出力される
分割代入を用いるとこれらを少し省略して記述することができます。
代入の左辺にて、配列の場合は角括弧[]
、オブジェクトの場合は中括弧{}
内に変数を宣言し、元の変数からそれぞれ宣言した変数に値を代入します。
配列の場合は先頭から順に、オブジェクトの場合は宣言した変数と同じ名前のプロパティが代入されます。
配列やオブジェクトを作るリテラルの[]
、{}
とは異なるので注意です。
const numbers = [1, 2, 3];
const [first, second] = numbers; // 変数 first に 配列の先頭,変数 second に2番目の値を代入console.log(first); // 1
console.log(second); // 2
const person = {
firstName: 'Alice',
lastName: 'Smith'
};
const { firstName, lastName } = person; // それぞれの変数に、同じプロパティ名の値を代入console.log(firstName); // 'Alice'
console.log(lastName); // 'Smith'
次の例は、引数に配列・オブジェクトを受取り、分割代入を使って値を取り出す関数の例です。
const func = ([arr]) => { // 引数の配列の一番目の要素を取り出して arr に代入
console.log(arr);
}
func([1, 2, 3]) // 1 が出力される
const fullName = ({ first, last }) => { // 引数のオブジェクトのfirst, lastプロパティを取り出し代入
console.log(first + last);
}
fullName({ first: 'Alice', last: 'Smith' }); // 'AliceSmith'が出力される
スプレッド構文・残余引数構文
スプレッド構文は、配列やオブジェクトを展開して別の配列やオブジェクトに統合するのに便利です。
既存の配列を使って新しい配列を作る時、push()
、splice()
、concat()
を使ったり、オブジェクトの場合はObject.assign()
を使用したりする方法がありますが、次のスプレッド構文...
を使うと、既存の配列・オブジェクトをその場で展開するリテラルとして使えます。
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combinedArray = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6] の配列となる
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const combinedObject = { ...obj1, ...obj2 }; // { a: 1, b: 2, c: 3, d: 4 } のオブジェクトとなる
また、関数の呼び出し時に、引数としてスプレッド構文を使用すると、引数として与えた配列の要素がそれぞれ個別の引数になります。
function func (x, y, z) {
console.log(x);
console.log(y);
console.log(z);
}
const array = [1, 2, 3]
func(...array);// コンソール出力
// 1
// 2
// 3
残余引数構文は、スプレッド構文と似た構文で...
を使用しますが、関数定義の最後の引数に使用することで、引数の個数を可変とするこができます。
残余引数構文...
で定義した引数は、呼び出し時に指定した引数と同じ位置以降のものを配列としてまとめます。
function myFun(a, b, ...manyMoreArgs) { console.log("a", a);
console.log("b", b);
console.log("manyMoreArgs", manyMoreArgs);
}
myFun("one", "two", "three", "four", "five", "six");
// コンソール出力
// a, one
// b, two
// manyMoreArgs, ["three", "four", "five", "six"]
async, await
非同期関数?
本題の前に非同期関数についてです。
非同期関数でよく例にあがるものの一つが、setTimeout()
です。
これは引数の一個目に関数(コールバック関数)を指定し、二個目に指定したミリ秒後に実行する、というものです。
JavaScriptコードではsetTimeout()
は「この処理お願いね~」と外部に任せた後、自分の処理を継続して実行します。
setTimeout(() => {
// 1秒後に実行する処理を指定
console.log('Delayed for 1 second.');
}, 1000);
// この後の行はsetTimeoutの処理を待たずに実行できる
console.log('処理を継続')
上のコードを実行すると
コンソールに'処理を継続'
と表示してから1秒後に'Delayed for 1 second.'
が表示されます。
コードの先頭から順に処理するものを同期処理というのに対し、これは非同期処理と呼びます。
このような非同期の関数において、コールバック関数にさらに非同期関数を渡していくとどうなるでしょう?
次の例ではsetTimeout()
を使って、1秒間隔でコンソールに秒数を表示します。
setTimeout(() => {
console.log('1秒'); // '1秒'
setTimeout(() => {
console.log('2秒'); // '1秒'の表示が終わってから1秒後に'2秒'
setTimeout(() => {
console.log('3秒'); // '2秒'の表示が終わってから1秒後に'3秒'
setTimeout(() => {
console.log('4秒');
}, 1000);
}, 1000);
}, 1000);
}, 1000);
ネストが深くなりめっちゃ見づらいですね・・・。これは俗に言うコールバック地獄というものです。
Promiseベースの非同期関数
このような非同期関数の問題点を解決する為に、Promise
オブジェクトを返す非同期関数が存在します。
非同期関数の返すPromise
オブジェクトは、.then()
と.catch()
メソッドを持ち、それぞれの引数には、その非同期関数の完了時の処理と、エラー時の処理をコールバックとして指定します。
さらに、.then()
メソッドに渡すコールバック関数内で、Promise
オブジェクトを返すようにすると.then()
を繋げることができ、コールバック地獄から解放されます。
Promise
オブジェクトを返す非同期関数としてよく例に挙げられる、fetch()
の例を見てみましょう。
fetch()
はネットワークからデータを取得してくるメソッドです。
// 指定したURLからデータを取得してくる
// この時、fetchPromiseには戻り値のPromiseオブジェクトが代入される
const fetchPromise = fetch('https://api.example.com/data');
fetchPromise
.then((response) => {
// データの取得が完了したときの処理
// 引数は取得してきたデータ
if (!response.ok) {
// エラー発生
throw new Error(`HTTP error: ${response.status}`);
}
// 取得したJSON形式のデータをjavascriptオブジェクトに変換
// このメソッドの返値はPromiseオブジェクト
return response.json();
})
.then((data) => {
// json()メソッドが完了したときの処理
// 引数はJSON化したデータ
console.log(data);
})
.catch((error) => {
// fetchPromiseの一連の処理でエラーが発生したときの処理
console.error(`Could not get products: ${error}`);
});
ネストの深さ問題はこれで解決です。
async,awaitを使う
ここでようやく本題のasync
、await
です。これらを使用すると、Promise
オブジェクトを返す非同期関数を、同期的なコードに近い形で呼出しできます。
async
、await
を使うには、
- 非同期処理を含む関数を
async
を付けて定義 Promise
を返す非同期関数をawait 非同期関数()
で実行(完了したら戻り値を取得できる).then()
に渡していた完了時の処理はawait
以降に続けて記述- 非同期関数を実行している箇所を
try{}
で囲い、続けてcatch{}
内にエラー時の処理を記述(無くてもとりあえずは動くけどエラー時の処理ができなくなる) async
を付けた関数は自動的にPromise
を返すようになる(その関数をawait
できるようになる)
fetch()
を使用した例は、次のように書き換えることができます。
async function fetchProducts() { try {
// 指定したURLからデータを取得してくる
// fetchProductsはデータの取得完了、もしくはエラーとなるまでこの行で待機する
// データが取得できたらresponseに代入される
const response = await fetch('https://api.example.com/data'); if (!response.ok) {
// エラー発生
throw new Error(`HTTP error: ${response.status}`);
}
// 取得したJSON形式のデータをjavascriptオブジェクトに変換
// fetchProductsはデータ変換完了、もしくはエラーとなるまでこの行で待機する
// JSON化が完了したらdataに代入される
const data = await response.json(); console.log(data);
} catch (error) {
// fetchProductsの一連の処理でエラーが発生したときの処理
console.error(`Could not get products: ${error}`);
}
}
fetchProducts();
.then()
を使った例より少し読みやすくなりましたね!
尚、先のsetTimeout
で秒数を表示する例をPromise
ベースで書き直すと以下のようになります。(自分でPromise
ベースの非同期関数を実装することは少ないと思いますが…)
下記のresolve()
を実行するとPromise
は「履行 (fulfilled)」となり、.then()
内の処理に移り、reject()
を実行するとPromise
は「拒否 (rejected)」となり、.catch()
内の処理に移ります。
// Promiseオブジェクトを返す関数を実装
const delay = (seconds) => {
return new Promise((resolve,reject) => {
setTimeout(() => {
resolve();
}, seconds * 1000);
});
}
const displayInSeconds = () => {
delay(1)
.then(() => {
console.log('1秒');
return delay(1);
})
.then(() => {
console.log('2秒');
return delay(1);
})
.then(() => {
console.log('3秒');
return delay(1);
})
.then(() => console.log('4秒'))
.catch(error => {
console.error(error);
});
}
displayInSeconds();
更にasync
、await
を用いて書き直すと、以下のようになります。
// Promiseオブジェクトを返す関数
const delay = (seconds) => {
return new Promise((resolve,reject) => {
setTimeout(() => {
resolve();
}, seconds * 1000);
});
}
const displayInSeconds = async () => {
try {
await delay(1);
console.log('1秒');
await delay(1);
console.log('2秒');
await delay(1);
console.log('3秒');
await delay(1);
console.log('4秒');
} catch (error) {
console.error(error);
}
}
displayInSeconds();
import, export
モジュール機能を使用すると、JavaScriptのコードを分割(モジュール化)し、再利用できるコードを作成できます。
全てのブラウザがモジュール機能に対応しているわけではありませんが、モジュールの利用を可能にするライブラリー・フレームワークが多く存在し、それとともに使用することになります。
アプリ制作等においては、インストールしたライブラリをimport
して使用する場面が多いです。
モジュール化した.js
ファイルにて、関数や変数の先頭にexport
を付けてエクスポート指定することで、他のファイルでimport
して使用可能となります。
デフォルトエクスポート
モジュール化した.js
ファイルのうち、1点のみをデフォルトエクスポートすることができます。
インポートが簡潔になり、インポートする変数名も自由に変えることが出来ます。
エクスポートしたい変数・関数等の先頭にexport default
を付けます。
// myFunc.js(モジュール化したファイル)
export default function () {
console.log('hogehoge')
};
デフォルトエクスポートされた値をインポートしたい場合は、コードの先頭でimport インポートする変数 from 'モジュールへのパス'
とします。モジュールへのパスは.js
を付けなくてもOKです。
// main.js(モジュールを読込むファイル)
import myFunc from "myFunc";
myFunc(); // 'hogehoge' と出力される
この時、インポートする変数myFunc
は、自由に変数名を指定できます。
名前付きエクスポート
モジュールから複数の値をエクスポートする場合は名前付きエクスポートをします。
エクスポートしたい変数・関数等の先頭にそれぞれexport
を付けます。
// lib.js(モジュール化したファイル)
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
}
名前付きエクスポートされた値をインポートしたい場合は、コードの先頭でimport {インポートする変数, インポートする変数, ... } from 'モジュールへのパス'
とします。
デフォルトエクスポートと異なり、中括弧{}
が必要です。また、インポートする変数名は、エクスポートされた変数・関数名と同じものを指定します。
// main.js(モジュールを読込むファイル)
import { square, diag } from "lib";
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
名前の衝突を避ける
モジュール機能を使っていると、モジュール間で同じ名前の関数などが出てきたり、自分のコード内で使用している変数名との名前被りが発生する恐れがあります。
変更前の名前 as 変更後の名前
のようにすると、エクスポート・インポートする変数や関数の名前を変更することが出来ます。(インポートする側で名前を変更する機会の方が多いと思います。)
次の二つの例は、どちらもモジュール内にあるhoge
、fuga
をそれぞれnewHoge
、newFuga
へ名前を変更してインポートする例です。
// lib.js(モジュール化したファイル)
export { hoge as newHoge, fuga as newFuga };
// main.js(モジュールを読込むファイル)
import { newHoge, newFuga } from "lib";
// lib.js(モジュール化したファイル)
export { hoge, fuga };
// main.js(モジュールを読込むファイル)
import {
hoge as newHoge,
fuga as newFuga,
} from "lib";
または、次のようにすると、モジュール側でエクスポートしたものを全て一つのオブジェクトにインポートすることができるので、名前の衝突が起きにくくなります。
// main.js(モジュールを読込むファイル)
import * as lib from "lib";
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5
まとめ
これらのように、モダンなJavaScriptの構文を使うことで、コードをより効率的に書き、メンテナンスしやすくすることができます。
より効率的な作業を目指してマスターしてください!