🫠

"レガシー"と言われないためのJavaScript再入門

2023/10/07に公開
4

追記: 10/11 ハテブでバズっているようで、色々指摘があったので追記

  • getElement*は動作が早いのでIDやクラス名が自明の場合はgetElement*を使う方がいいと言う意見もあり、また、ページの表示で大量に呼び出されるわけではないからボトルネックにはならないと言う意見もある。

  • getElement*で返されるオブジェクトは動的な変化に対応しており、querySelector*は動的な変化に対応していないため、場合によってはgetElement*を使うといい。このサイトで遊んでみよう。
    https://ja.javascript.info/searching-elements-dom#ref-263

  • for await ... ofは非推奨なので Promise.allを現代的な書き方にした

  • 顧客先のブラウザが古い場合も考慮して、あえてレガシーな書き方もする場合があるらしい。現に、codeSandboxで動作確認をしていたとき、動かないものもあった(どの構文が動かないか忘れた)

  • ChatGPTの有料版に入っていても、JSのコードを出力させるといまだに古い書き方を出力される場合があるので、"現代的なJSの書き方で"のような文言を入れると現代的なJSの書き方で出力してくれる

  • 元記事のタイトルは『"オジさん"と言われないための〜』だったがハテブコメで否定的な多く意見があったので『"レガシー"と言われないための〜』に変更。自虐と釣りタイトルを考慮してこの言葉を安直に使ってしまった。不快に思わせてしまいすみません。

  • ChatGPTには次のような文言を書けば似たようなのが出てきます。一回の出力で3,4個しか出力されないので、「もっと」 と打てばもっと出力してくれます

レガシーなJavaScriptと現代風に追加されたJavaScriptの機能を比較して記事を書いてみましょう。以下の条件に従ってください。

*伝統的に使われている機能を使い続けていて最新のJavaScriptの機能を知らない人に向けた記事です。
* 「レガシーなコード」と「現代風なコード」のサンプルコードを書いてください
* 各機能の現代風のコードを書いた後、何が変わったのか、変わって便利になった点などを300文字程度でコメントを書いてください。

ChatGPTにコードリーディングをしてもらっていると「これは伝統的な書き方ですが、もっと現代風の書き方がありますよ」といった旨を言われて悲しくなってしまった。要は新しい情報をキャッチアップできてないおじさんと言われたようなものだ。"現代風"のJavaScriptの書き方を調べるのは大変なのでChatGPTにいろいろリストアップしてもらったので共有したい。ちなみに

ここでは詳細なことには踏み込まず、流し読みしてもらうためにざっくり書いている。気になる箇所があればその都度ググって調べて欲しい。

DOM要素の取得: getElement* vs. querySelector

  • 昔のスタイル (getElement* メソッド):
    const elementById = document.getElementById('myId');
    const elementsByClass = document.getElementsByClassName('myClass');
    const elementsByTag = document.getElementsByTagName('div');
    
  • 現代風 (querySelectorquerySelectorAll):
const elementById = document.querySelector('#myId');
const elementsByClass = document.querySelectorAll('.myClass');
const elementsByTag = document.querySelectorAll('div');
// こんな指定もできる
const elements1 = document.querySelectorAll('div.parent > span#child'); // parentというクラスめいのdiv子要素のうち、childというidを持ったspan要素
const elements1 = document.querySelectorAll("#parent li:nth-child(odd)"); // parentというidを持った子孫の要素のうち、奇数番目のli要素
  • 変わった点: querySelectorquerySelectorAll を使用することで、CSSセレクタの構文を使ってDOM要素を取得できるようになりました。これにより、要素の選択がより直感的で柔軟になり、一つのメソッドで複数の取得方法をカバーすることができるようになりました。
  • それでもgetElement*を使う場合 (パフォーマンス編) :
    getElement*querySelector*に比べてかなり速い。呼び出し回数にも依存するが、数十倍-100倍程度さが開く。そう聞くとものすごい差に思えるが単位がミリ秒だから1万回呼び出すだけならボトルネックにはならない。しかし十万回以上呼び出す場合はquerySelectorは避けた方が吉。
    参考として、呼び出した回数におけるそれぞれの経過時間を表示しておく。
呼び出し回数 getElementByClassName の時間 (ミリ秒) querySelectorAll の時間 (ミリ秒)
1,000 回 0(1m秒以下) 48
10,000 回 3 359
100,000 回 12 3421
1,000,000 回 78 33991

以下に100万回呼び出すコードを書いておくので DevToolsを開いて動かしてみてほしい。

// 仮の要素とクラスを作成
const div = document.createElement('div');
div.className = 'test-class';
document.body.appendChild(div);

// getElementByClassName のパフォーマンス測定
let start = performance.now();
for (let i = 0; i < 1_000_000; i++) {
    document.getElementsByClassName('test-class');
}
let end = performance.now();
console.log(`getElementByClassName took ${end - start} milliseconds.`);

// querySelectorAll のパフォーマンス測定
start = performance.now();
for (let i = 0; i < 1_000_000; i++) {
    document.querySelectorAll('.test-class');
}
end = performance.now();
console.log(`querySelectorAll took ${end - start} milliseconds.`);

// 仮の要素を削除
document.body.removeChild(div);
  • それでもgetElement*を使う場合 (動的か静的か) :

Array.prototype.includes(): 配列内の要素の存在チェック

  • 昔のスタイル:
    const arr = [1, 2, 3, 4];
    const hasTwo = arr.indexOf(2) !== -1; // true
    
コピペで動かす
const arr = [1, 2, 3, 4];
let result;

if (arr.indexOf(2) !== -1) {
    result = "2はある";
} else {
    result = "2はない";
}

console.log(result); // 2はある

  • 現代風:
    const arr = [1, 2, 3, 4];
    const hasTwo = arr.includes(2); // true
    
コピペで動かす
const arr = [1, 2, 3, 4];
let result;

if(arr.includes(2)) {
    result = "2はある";
} else {
    result = "2はない";
}

console.log(result); // 2はある
  • 変わった点: includesメソッドを使用することで、配列内に特定の要素が存在するかどうかを簡単にチェックできるようになりました。これにより、コードの可読性が向上し、意図が明確になりました。

オブジェクトリテラル: Enhanced Object Literals

  • 昔のスタイル:
    const key = "name";
    const age = 20
    const gender = "male"
    const obj = { key: key, age: age, gender: gender }; // { key: 'name', age: 20, gender: 'male' }
    
コピペで動かす
const key = "name";
const age = 20
const gender = "male"
const obj = { key: key, age: age, gender: gender };

console.log(obj); // { key: 'name', age: 20, gender: 'male' }
  • 現代風:
    const key = "name";
    const age = 20
    const gender = "male"
    
    const obj = { key, age, gender }; // { key: 'name', age: 20, gender: 'male' }
    
コピペで動かす
const key = "name";
const age = 20
const gender = "male"

const obj = { key, age, gender };
console.log(obj); // { key: 'name', age: 20, gender: 'male' }
  • 変わった点: 強化されたオブジェクトリテラルを使用することで、プロパティ名と変数名が同じ場合のオブジェクトの定義が簡潔になりました。

文字列操作: String Concatenation vs. Template Literals

  • 昔のスタイル:
    const name = "Alice";
    const greeting = "Hello, " + name + "!"; // Hello, Alice!
    
    
  • 現代風:
    const name = "Alice";
    const greeting = `Hello, ${name}!`; // Hello, Alice!
    
  • 変わった点: テンプレートリテラルを使用することで、文字列内に変数を埋め込むことが簡単になりました。これにより、コードの可読性が向上し、文字列の結合エラーを減少させることができます。

非同期処理: Callbacks vs. Promises vs. Async/Await

  • 昔のスタイル (Callbacks):
    function fetchData(callback) {
      // データ取得後
      callback(data);
    }
    fetchData(function(data) {
      console.log(data);
    });
    
  • 少し新しいスタイル (Promises):
    function fetchData() {
      return new Promise((resolve, reject) => {
        // データ取得後
        resolve(data);
      });
    }
    fetchData().then(data => console.log(data));
    
  • 現代風 (Async/Await):
    async function fetchData() {
      // データ取得
      return data;
    }
    const data = await fetchData();
    console.log(data);
    
  • 変わった点: 非同期処理の方法が、コールバックからプロミス、そしてasync/awaitへと進化しました。これにより、非同期コードの書き方がより直感的で読みやすくなり、エラーハンドリングも効果的に行えるようになりました。

配列操作: Manual Iteration vs. Array Methods

  • 昔のスタイル (Manual Iteration):
    const numbers = [1, 2, 3, 4];
    const doubled = [];
    for (let i = 0; i < numbers.length; i++) {
      doubled.push(numbers[i] * 2);
    } // [2, 4, 6, 8]
    
  • 現代風 (Array Methods):
    const numbers = [1, 2, 3, 4];
    const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8]
    
  • 変わった点: 組み込みの配列メソッドを使用することで、配列の操作が簡潔になりました。これにより、コードの可読性が向上し、手動の繰り返しに関連するミスを減少させることができます。

デフォルトパラメータ: Undefined Checks vs. Default Parameters

  • 昔のスタイル:
    function greet(name) {
      name = name || "Guest";
      console.log("Hello, " + name + "!");
    }
    
  • 現代風:
    function greet(name = "Guest") {
      console.log(`Hello, ${name}!`);
    }
    
  • 変わった点: かつてのJavaScriptでは、未定義の引数にデフォルト値を提供するために、OR || 演算子を使って条件を設定していました。このアプローチは、引数が0falseの場合などに問題を引き起こす可能性がありました。デフォルトパラメータを使用することで、この問題を回避し、関数定義がより直感的でエラーを防ぐことができます。

Optional Chaining: 手動での存在チェック vs. Optional Chaining

  • 昔のスタイル:
    if (user && user.address && user.address.street) {
      console.log(user.address.street);
    }
    
コピペで動かす
var user = {
  address: {
    street: "123 Main St"
  }
};

if (user && user.address && user.address.street) {
  console.log(user.address.street);  // "123 Main St"
}
  • 現代風:
    console.log(user?.address?.street);
    
コピペで動かす
const user = {
  address: {
    street: "456 Elm St"
  }
};

console.log(user?.address?.street);  // "456 Elm St"
console.log(user?.address?.postcode); // undefined

  • 変わった点: Optional Chainingを使用することで、ネストされたオブジェクトのプロパティに安全にアクセスできるようになりました。これにより、多くの条件文やエラーチェックを省略でき、コードが大幅に簡略化されました。

配列とオブジェクトの分割代入: Manual Extraction vs. Destructuring

  • 昔のスタイル:

    var arr = [1, 2, 3];
    var first = arr[0]; // 1
    var second = arr[1]; // 2
    
  • 現代風:

    const arr = [1, 2, 3];
    const [first, second] = arr; // first は 1, secondは 2
    
コピペで動かす
const arr = [1, 2, 3];
const [first, second] = arr;

console.log(first); // 1
console.log(second); // 2


const[ichi, ...ni] = arr;
console.log(ichi); // 1
console.log(ni); // [ 2, 3 ]
  • 変わった点: 分割代入を用いることで、配列やオブジェクトから要素やプロパティを一度に抽出して変数に代入することができます。これにより、コードが簡潔になり、変数の初期化や再代入の際の手間やエラーが削減されます。

正規表現も分割代入できる

  • 昔のスタイル:
    var orderDate = "2023年1月1日"     
    var orderDateNumbers = orderDate.match(/\d+/g);
    var year = orderDateNumbers[0]; //2023
    var month = orderDateNumbers[1]; // 1
    var day = orderDateNumbers[2]; //1
    
  • 現代風:
const orderDate = "2023年1月1日"
const [year, month, day] = orderDate.match(/\d+/g); // yearには2023, monthには1, dayには1 が代入されている
 ```
  • 変わった点: ES6 (ES2015) から導入されたディストラクチャリング (destructuring) という機能を使用することで、配列やオブジェクトから要素やプロパティを簡潔に抽出できるようになりました。この機能により、コードが短くなり、可読性が向上しました。

Private Class Fields: 真のプライベートフィールド、プライベートメソッドの登場

  • 昔のスタイル:
class User {
    constructor(name, age) {
        this.name = name;
        this._age = age;  // アンダースコアを使用してプライベートを模倣
    }
    
    isAdult() {
        return this._age > 20; 
    }
    
    _isKanreki() {  // アンダースコアを使用してプライベートメソッドを模倣
        return this._age > 60;
    }
}

コピペで動かす
class User {
    constructor(name, age) {
        this.name = name;
        this._age = age;
    }
    
    isAdult() {
        return this._age > 20;
    }
    
    _isKanreki() {
        return this._age > 60;
    }
    
    publicIsKanreki() {
        return this._isKanreki();
    }
}

const tenno = new User("hironomiya-tenno", 63);

console.log(tenno.name); // "hironomiya-tenno"
console.log(tenno.isAdult());      // true
console.log(tenno.publicIsKanreki()); // true

console.log(tenno._age);  // 63 ※外部からアクセスできてしまう
console.log(tenno._isKanreki());   // true ※外部からアクセスできてしまう
  • 現代風:
class User {
    name;
    #age;  // 真のプライベートフィールド
    
    constructor(name, age) {
        this.name = name;
        this.#age = age; 
    }
    
    isAdult() {
        return this.#age > 20; 
    }
    
    #isKanreki() {  // 真のプライベートメソッド
        return this.#age > 60;
    }
}

コピペで動かす
class User {
    name;
    #age;
    
    constructor(name, age) {
        this.name = name;
        this.#age = age;
    }
    
    isAdult() {
        return this.#age > 20;
    }
    
    #isKanreki() {
        return this.#age > 60;
    }
    
    publicIsKanreki() {
        return this.#age > 60;
    }
}

const tenno = new User("hironomiya-tenno", 63);

console.log(tenno.isAdult()); // true
console.log(tenno.isKanreki); // undefined ※プライベートメソッドは外部からアクセスできない
console.log(tenno.publicIsKanreki()); // true

console.log(tenno.name); // hironomiya-tenno
console.log(tenno.age); // undefined ※プライベートフィールドは外部からアクセスできない
  • 変わった点: 古い書き方ではプライベートフィールド、プライベートメソッドを表すために名前の前に_を書いていました。しかしこれは単なる慣習なので、実際は外部からアクセスできてしまいます。現代的な方法として、プライベートフィールドおよびプライベートメソッドが導入されました。これらの機能は、フィールド名やメソッド名の前に # を付けることで使用できます。#をついたフィールドやメソッドは外部からアクセスできません。

非同期イテレーション: for await...of vs Promise.all

  • 昔のスタイル:
// 同時に複数のユーザを同期的に作りたいとする
const USER_DATA_LIST = [
    { userId: 1, userName: "User1" }
    { userId: 2, userName: "User2" }
    { userId: 3, userName: "User3" }
    { userId: 4, userName: "User4" }
    { userId: 5, userName: "User5" }
]

for await (const userData of USER_DATA_LIST) {
    await createUser(userData.userId, userData.userName)
}
  • 現代風:
// 同時に複数のユーザを同期的に作りたいとする
const USER_DATA_LIST = [
    { userId: 1, userName: "User1" }
    { userId: 2, userName: "User2" }
    { userId: 3, userName: "User3" }
    { userId: 4, userName: "User4" }
    { userId: 5, userName: "User5" }
]

Promise.all(
    USER_DATA_LIST.map(userData => createUser(userData.userId, userData.userName))
)
  • 変わった点: for await...of ループと Promise.all の主な違いは、前者はシーケンシャル(順番に)処理を行い、後者は並列処理を行う点です。Promise.all を使用することで、複数の非同期処理を効率的に並列に実行し、全ての処理が完了するまで待ってから結果を取得することができます。これにより、プログラムのパフォーマンスが向上し、コードの可読性も保たれます。また、エラーハンドリングが一箇所に集中し、エラー処理が簡単になるという利点もあります。

サンプルコードはこちらの記事からお借りしました
https://zenn.dev/takeru0430/articles/9dcd9d70e4ec92

Spread/Rest 演算子: 手動での配列/オブジェクトの操作 vs. Spread/Rest 操作

  • 昔のスタイル:
    var arr1 = [1, 2, 3];
    var arr2 = arr1.concat([4, 5, 6]);
    
コピペで動かす
var arr1 = [1, 2, 3];
var arr2 = arr1.concat([4, 5, 6]);

console.log(arr2); // [ 1, 2, 3, 4, 5, 6 ]
  • 現代風:
    const arr1 = [1, 2, 3];
    const arr2 = [...arr1, 4, 5, 6];
    
コピペで動かす
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5, 6];

console.log(arr2); // [ 1, 2, 3, 4, 5, 6 ]
  • 変わった点: スプレッド構文を使用することで、配列やオブジェクトを簡単に展開・結合することができるようになりました。これにより、コードの行数が減少し、可読性が向上しました。

オブジェクトリテラルの計算されたプロパティ名

  • 昔のスタイル:
    var key = "name";
    var obj = {};
    obj[key] = "Taro";
    
  • 現代風:
    const key = "name";
    const obj = {
      [key]: "Taro"
    };
    
  • 変わった点: 計算されたプロパティ名を使って、動的にオブジェクトのキーを設定することが直接可能になりました。これにより、コードが簡潔かつ直感的になりました。

Nullish Coalescing Operator: Undefined/Null チェック vs. Nullish Coalescing

  • 昔のスタイル:
    const inputData = 0;
    const value = inputData || "defaultの値"; // "defaultの値"
    
コピペで動かす
let inputData1; // 初期値を書いてないときは undefined
const value1 = inputData1 || "あああ";
console.log(value1);  // "あああ"

let inputData2 = 0;
const value2 = inputData2 || "あああ";
console.log(value2);  // "あああ"
  • 現代風:
    const inputData = 0;
    const value = inputData ?? "defaultの値"; // 0
    
コピペで動かす
let inputData1; // undefined
const value1 = inputData1 ?? "あああ";
console.log(value1);  // "あああ"

let inputData2 = 0;
const value2 = inputData2 ?? "あああ";
console.log(value2);  // 0
  • 変わった点: Nullish Coalescing Operator (??) を使用することで、undefinednullの場合だけデフォルト値を設定できるようになりました。これにより、0falseなどのfalsyな値を誤ってデフォルト値で上書きする問題を防ぐことができます。

Array.prototype.flat() と flatMap(): ネストされた配列の平坦化

  • 昔のスタイル:
    const nested = [[1, 2], [3, 4]];
    const flattened = [].concat.apply([], nested);
    
コピペで動かす
const nested = [[1, 2], [3, 4]];
const flattened = [].concat.apply([], nested);

console.log(flattened); // [ 1, 2, 3, 4 ]
  • 現代風:
    const nested = [[1, 2], [3, 4]];
    const flattened = nested.flat();
    
コピペで動かす
const nested = [[1, 2], [3, 4]];
const flattened = nested.flat();

console.log(flattened); // [ 1, 2, 3, 4 ]
  • 変わった点: .flat()メソッドを使用することで、ネストされた配列を簡単に平坦化できるようになりました。これにより、コードが簡潔になり、配列の操作が直感的になりました。

Numeric Separators: 大きな数字の可読性の向上

  • 昔のスタイル:
    const billion = 1000000000; // 1000000000
    
  • 現代風:
    const billion = 1_000_000_000; // 1000000000
    
  • 変わった点: 数字のセパレータとしてアンダースコア(_)を使用することで、大きな数字の可読性が向上しました。これにより、数字の桁を迅速に把握し、ミスを減少させることができます。

Optional Catch Binding: エラーのバインドをオプションに

  • 昔のスタイル:
    try {
      // エラーがないときに動くコード
    } catch (error) {
      console.error("An error occurred!");
    }
    
  • 現代風:
    try {
      // エラーがないときに動くコード
    } catch {
      console.error("An error occurred!");
    }
    
  • 変わった点: エラーオブジェクトをバインドすることなく、catchを使用することが可能になりました。これにより、エラーオブジェクトが不要な場合のコードが簡潔になりました。

Rest Properties for Object Destructuring: オブジェクトの残りのプロパティの取得

  • 昔のスタイル:
    const obj = { a: 1, b: 2, c: 3, d: 4 };
    const a = obj.a
    const b = obj.b;
    const rest = { c: obj.c, d: obj.d };
    
コピペで動かす
const obj = { a: 1, b: 2, c: 3, d: 4 };
const a = obj.a
const b = obj.b;
const rest = { c: obj.c, d: obj.d };

console.log(a); // 1
console.log(b); // 2
console.log(rest); // { c: 3, d: 4 }
  • 現代風:
    const obj = { a: 1, b: 2, c: 3, d: 4 };
    const { a, b, ...rest } = obj;
    
コピペで動かす
const obj = { a: 1, b: 2, c: 3, d: 4 };
const { a, b, ...rest } = obj;

console.log(a); // 1
console.log(b); // 2
console.log(rest); // { c: 3, d: 4 }
  • 変わった点: Rest propertiesを使用することで、オブジェクトから一部のプロパティを簡単に取り出し、残りのプロパティを別のオブジェクトとして取得できるようになりました。これにより、オブジェクトの操作がより柔軟かつ簡潔になりました。

Object.values() と Object.entries(): オブジェクトの値とエントリーの取得

  • 昔のスタイル:
    const obj = { a: 1, b: 2 };
    const values = Object.keys(obj).map(key => obj[key]); // [1, 2]
    
  • 現代風:
    const obj = { a: 1, b: 2 };
    const values = Object.values(obj); // [1, 2]
    const keys = Object.keys(obj); // ["a", "b"]
    
  • 変わった点: Object.values()Object.entries()を使用することで、オブジェクトの値やキーと値のペアを簡単に取得できるようになりました。これにより、オブジェクトのデータの操作が簡潔かつ効率的になりました。

Discussion

iwbjpiwbjp

ChatGPTが生成するJavaScriptのコードは
正しいとは限らないので、Zennに投稿するなら
本当に正しいかコピペだけで済ませずに、実行するくらいはしてほしい。

例えば以下のコードは明らかに間違い。

const safeString = html`<div>Hello</div>`;
ひげひげ

動作確認をせずに投稿してしまい申し訳ありません
今からサンプルコードを実行して動くように訂正します!