きっかけ
JavaScriptはなんとなく書けてきた。でも「var と let と const の違いをちゃんと説明してください」と聞かれたとき、即答できなかった。スプレッド構文も、動くコードは書けるが「なぜそう書くのか」の説明が曖昧だった。
使えることと理解していることは別だな、と改めて感じたので、自分なりに整理し直してみた。
JavaScriptとDOMの関係
まず前提として、JavaScriptはブラウザ上で動くプログラミング言語だ。ユーザーの操作に応じてページの表示を動的に変えられる。Node.jsを使えばサーバー側でも動く。
ただし、JavaScriptはHTMLを直接操作できない。ブラウザがHTMLを解析すると、その内容をツリー構造のオブジェクトとして構築する。これがDOMだ。JavaScriptはこのDOMを介して初めてHTMLにアクセスできる。
「JavaScriptでHTMLを操作する」という表現が一般的に使われるが、正確には「JavaScriptがDOMを通じてHTMLを操作する」という構造になっている。この区別は最初意識できていなかった。
var / let / const の使い分け
3つの違いを整理するとこうなる。
| var | let | const | |
|---|---|---|---|
| 再宣言 | 可能 | 不可 | 不可 |
| 再代入 | 可能 | 可能 | 不可 |
| スコープ | 関数スコープ | ブロックスコープ | ブロックスコープ |
var は関数スコープなので、if ブロックや for ループの中で宣言しても外から参照できてしまう。意図しない参照が起きやすく、現代のコードではほぼ使わない方向になっている。
基本的には const を使い、後から値を変える必要があるものだけ let を使う、という判断基準が実務では一般的だ。var を新規コードで書く理由はほぼない。
関数の書き方が複数ある理由
JavaScriptには関数の書き方が複数あって、最初は混乱した。
関数宣言
function greet(name) {
return `Hello, ${name}`;
}
関数式(無名関数)
const greet = function(name) {
return `Hello, ${name}`;
};
アロー関数
const greet = (name) => `Hello, ${name}`;
アロー関数は function キーワードを省略した短い記法だ。処理が1行に収まる場合は {} と return も省略できる。
書き方の違いだけでなく、this の扱いに差がある点は別途整理が必要だと感じている(今回は深入りしない)。
コールバック関数の仕組みを整理した
「関数を引数として渡す」という書き方が最初ピンとこなかった。
function hello() {
console.log('hello');
}
function execute(callback) {
callback();
}
execute(hello);
execute(hello) と書いた時点では hello は実行されない。execute の中で callback() が呼ばれて初めて動く。「実行のタイミングを渡す側ではなく受け取る側に委ねる」という発想で、非同期処理や配列メソッド(forEach や map など)で頻繁に出てくる。
クラスとインスタンスの関係
クラスはオブジェクトの設計図で、インスタンスはそこから生成した実体、という整理は他の言語と変わらない。
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const user = new User('太郎', 20);
console.log(user.name); // 太郎
なぜインスタンスを生成するのかという部分も改めて整理した。複数の処理が同じオブジェクトを直接共有してしまうと、互いに干渉してしまう。インスタンスとして別のオブジェクトを生成することで、それぞれが独立したデータを持てる。
余談だが、ES6以前はクラス構文がなく、コンストラクタ関数で擬似的にクラスを表現していた。古いコードを読む機会があると function Person(name) { this.name = name; } のような書き方が出てくるが、これは現代の class に対応するものだと理解しておくと混乱しにくい。
スプレッド構文と参照の話
スプレッド構文は ... を使って配列やオブジェクトを展開する記法だ。
const a = [1, 2, 3];
const b = [...a, 4, 5, 6];
console.log(b); // [1, 2, 3, 4, 5, 6]
配列のコピーにも使える。
const a = [1, 2, 3];
const c = [...a];
c.push(4);
console.log(a); // [1, 2, 3] ← 変わっていない
console.log(c); // [1, 2, 3, 4]
let c = a と書いてしまうと、c は a の参照を持つだけなので、c を変更すると a も変わってしまう。スプレッド構文を使うことで、参照先とは独立した別の配列を作れる。
ただしこれは「シャローコピー(浅いコピー)」であることに注意が必要だ。ネストされたオブジェクトや配列の中身は参照がコピーされるだけなので、深い階層のデータは共有されたままになる。
[...a] でコピーした場合でも、ネストされた配列やオブジェクトはコピーされない。[{name: 'A'}] のような構造を扱うときは別の対処が必要になる。
整理して見えてきたこと
今回言語化できたのは、var/let/const の使い分けの根拠、関数の書き方の違い、コールバックの仕組み、スプレッド構文と参照の関係あたりだ。
まだ自分の理解が薄いと感じているのは、アロー関数と通常の関数における this の違いだ。「動きが変わるのは知っている」レベルで止まっていて、具体的にどういうケースで問題になるかを整理できていない。そこは次の課題にしたい。
この記事を書いた人について
株式会社Flexibilityでエンジニアをしています。
DX推進・システム開発を軸に、エンジニアが自律的に動ける環境を大事にしている会社です。
技術的に面白いことをやっていきたい方や、働き方に柔軟さを求めている方は、
よかったら一度のぞいてみてください。
- 会社サイト: https://www.flexi-inc.com/
- Qiita Organization: https://qiita.com/organizations/flexi-inc