🎒

後から学ぶJavaScript Primer

2024/04/16に公開
2

どういうもの

基礎がおざなりになっている,Vue.jsなどの発展形のフレームワークから入った人が,基礎的なjsを座学することによるとどういう学びがあるのかと言うもの。
jsを1から勉強する記事ではないので,そういう趣旨の方は下のリンク先を熟読しましょう。
https://jsprimer.net/

背景

述べたように,最初にJS/TSの発展形であるフレームワークから入ってきて,基本がおざなりになっているというパターンです。

  1. Vue.js/Nuxt.jsを学習し,色々とコードを書いてきた。
  2. 他のエンジニアと交流していく中で,インプット<<アウトプットの比重になっていたことが分かり,基本的な部分の座学の必要性を感じた。
  3. このサイトを勧められた。

学んだもの/気づき

分割代入

律儀にconst a = array[0]の形式でこれまで書いていたのですが,AtCoderなどで扱う要素が3つくらいの単純な配列ならこれのほうがすっきりしてて良さそうです。

本文より抜粋
const array = [1, 2];
// aには`array`の0番目の値、bには1番目の値が代入される
const [a, b] = array;
console.log(a); // => 1
console.log(b); // => 2

一方でオブジェクトの分割代入は,変数名がオブジェクトのプロパティ名で固定されるのが,例えばAPIのレスポンスのプロパティ名が英語圏寄りのニュアンスだったりそもそも分かりにくいことがあったりする時に困りそうに思います。

PS.コメントで頂きましたが,オブジェクトの分割代入でも別名指定が可能で,そちらのほうが可読性が良くなりそうなので以下に記載します。

オブジェクトの分割代入でも別名指定する
const warframeObject = {
// 前後に色々ある
"active": true, //元のママ
"archwing": true
}

const {
    active: isActive,
    archwing: isArchwingAvailable
    } = warframeObject //もう少しマシな整え方がありそうだけど変に整えるとそもそも機能しなくなりそうで分からない

別名使わない場合
const warframeObject = {
// 前後に色々ある
"active": true, //元のママ
"archwing": true
}
//改めて下記のように書いたほうが読みやすそう
const isActive = warframeObject.active
const isArchwingAvailable = warframeObject.archwing

someメソッド

愚直にfor(let i = 0;i < array.length;i++)const someFlag = falseの組み合わせをしなくても配列の中に条件を満たすあるかどうかを判定できるシンプルが方法があると知りました。
例えば下の問題なんかは,flagの変数を用意しなくてもsomeメソッドを使えばすっきりと書けます。(Atcoder Extension使用)
https://atcoder.jp/contests/abs/tasks/abc081_b

コード
answer
import * as fs from "fs";

// util for input
// prettier-ignore
const lineit = (function* () { for (const line of fs.readFileSync(process.stdin.fd, "utf8").split("\n")) yield line.trim(); })();
// prettier-ignore
const wordit = (function* () { while (true) { let line = lineit.next(); if (line.done) break; for (const word of String(line.value).split(" ")) yield word; } })();
// prettier-ignore
const read = () => String((wordit.next()).value);

// main

const isOdd = (num: number) =>{
  return num % 2 === 1;
}

const main = function () {
  // TODO edit this code, this code is for https://atcoder.jp/contests/practice/tasks/practice_1

  // param
  let n: number = Number(read());
  let a: number[] = [];
  for (let i = 0; i < n; i++) {
    a.push(Number(read()));
  }
  let count: number = 0;
  while(a.some(isOdd) === false){
    a = a.map(num => num / 2)
    count++;
  }
  console.log(count)

  return;
};
main();

破壊的なメソッド

fetchしたデータの使い方などから,感覚的にあるコードにおいて,左辺の変数のみが作成/変更されるものだと思っていて,AtCoderで配列操作をする際になぜか右辺の配列も変更されてしまってバグることが多発していたのですが,これが破壊的なメソッドによるものだと分かりました。
直感的な操作をするためには,

  • 多用するspliceについてはtoSplicedを用いる。
  • pop,shiftなどのto◌◌edが用意されていないものは,array.slice()でコピーを作ってから操作する

文字数のカウントとUnicodeと絵文字

フォームなど文字数制限を加えたい際に,lengthを特に何も考えずに使っていましたが,一部の漢字や絵文字🦀は1と出力してくれないそうです。それはlengthがUTF-16を用いて表されているCode Unitの数を数えている仕組になっていて,人間が見た時の文字数を正確に調べる場合はUnicodeの文字のIDであるCode Pointを使う必要があります。
てっとり早いのはArray.fromを用いることだそうです。その際絵文字ZWJシーケンスが用いられている場合はゼロ幅接合子(U+200D)が絵文字と絵文字の間に入ることによって一つの絵文字として表現されているため,この方法でも正確に見た目の文字数をカウントすることは出来ません。

console.log("🦀".length); // 2
const crab = Array.from("🦀");
console.log(crab.length); // 1

console.log("👨🏾‍🦳".length); // 7
const person = Array.from("👨🏾‍🦳");
console.log(person.length) // 4となってしまう
console.log(person) // [ '👨', '🏾', '‍', '🦳' ]

見た目の文字数を表現したい場合は,

  • 肌色の絵文字のみが(多分)唯一ゼロ幅接合子なしで接続できる絵文字であり,種類も5種類と少ないので,これを指定して除外する。
  • ゼロ幅接合子の次の絵文字を除外する
  • ゼロ幅接合子とVariation Selector-16を除外する

で出来るかと思います。

varの巻き上げとvarを使わない理由

日常的に変数を定義する場合はconstで,再代入可能にするにはRef(),computed()(Vueの場合)かlet(AtCoderなどでtsを使う場合)を用いていたので,varを使うことは全くありませんでしたが,これはJS側の狙い通りでした。
varには巻き上げという厄介な機能があり,次のような構文でもエラーを出さないそうです。

// var x とここに書かれているような挙動をする
console.log(x); // undefined,ReferenceErrorは吐かない
var x = "hoge" // x = "hoge" のような挙動をする

直感的に宣言していない変数を参照するとエラーになると考えているので,この巻き上げは非常にコードの流れや仕組みを追うのに苦労する上,デバッグも大変になってしまう要素であると思います。

MapとObject

APIのデータのやり取りや,AtCoderで頻繁に使う連想配列を扱うのに無意識にObjectを使ってきましたが,特に後者の操作においてはMapのほうが良いみたいです。

Mapの利点

  • prototypeなどの既定のキーがなく,真っ新な状態からスタートできる
    キーの名前が衝突してしまったり,ユーザーがキーと値を提供した場合に,prototypeの値を上書きすることでインジェクション攻撃が出来てしまうリスクが発生する。
  • 追加,削除する際のパフォーマンスが良いらしい
  • 大きさをsizeという直感的なコードで表現できる
    Objectの場合はObject.keys.length

Objectの利点

  • JSON.stringify()JSON.parse()による,JSONとの連動性がある
    Mapにはないらしい
  • 多くの関数でObjectを渡される設計となっている

ユーザーが関与する余地のない場合やJSON化/JSONから受け取る場合はObjectを用いる一方,jsprimer内の例でもあるようなカートへの商品の追加のように頻繁にデータが追加削除される場合や,ユーザーがMapに値を追加する場合はMapのほうが良いとされています。
AtCoderに関してはMapでよさそうです。

終わりに

実際に自分でモノを作りながら学ぶ一方で,改めて基礎を座学することで見えてきた仕様などの気づきも多くありました。特に破壊的なメソッドの存在については衝撃的でした。
基礎を学ぶことで,こうした微妙だが重要な違いを生む仕様[1]への理解と,何かエラーがあったり分からないコードがあった時の調べ先として,この基礎の書かれたサイトを脳内インデックスに登録できる,つまりどこに書いてあったのかは把握している状態を作れて,作業が捗るかと思いますので,自分みたいに何かを作ることから先に入ってきた人は是非学んでみましょう。
次はサバイバルTypescriptも読んでみようと思います。

何か理解や説明に間違いがありましたらコメント頂けると助かります。

脚注
  1. タイミングを逃すというようなもの ↩︎

Discussion

NPG418NPG418

オブジェクトの分割代入は別名を指定することもできますよ

const { active: isActive, archwing: isArchwingAvailable } = warframeObject
// 以降isActiveやisArchwingAvailableでアクセスできる

ただ可読性とかは個人の好みによる気がします

nemunyannemunyan

コメントありがとうございます。オブジェクトの分割代入についても別名指定が可能だったのは初耳でした。
以下のように改行を駆使してObjectやJSONっぽくまとめれば,ブロックの存在も相まって,本文中のように列挙するより可動性の部分でもより高くなりそうです。

const {
    active: isActive,
    archwing: isArchwingAvailable
    } = warframeObject