【React】React開発のためのJavaScript必須知識

こちらの書籍で学ばせていただいた内容をまとめました。

書籍の説明

書籍名: Reactハンズオンラーニング 第2版 ―Webアプリケーション開発のベストプラクティス
著者: Alex Banks 著
出版社: オライリージャパン 発行
発売日: 2021/8/6

let

ES2015によるvarからletへの変更により、JavaScriptにレキシカルスコープが導入されました。

レキシカルスコープとはなんなのか、どのような挙動になるのかスコープを持たないvarとレキシカルスコープを持つletの違いから以下で見ていきましょう!

元来、変数のスコープは関数によってのみ作ることができました。

それが、ES2015より、関数でなくてもスコープを作ることができるようになりました。

スコープとは、変数がどの場所から参照できるかを定義する概念、つまり有効範囲のことをいいます。

varによりスコープを持たない例

var topic = "JavaScript";

if (topic) {
  var topic = "React";
  console.log("block", topic);
}

console.log("global", topic);

>
block React
global React

ifの外ではtopicには”JavaScript”を入れているため、globalは”JavaScript”が表示されるはずですが、varはスコープを持たないため、ifの内外ともに”React”が表示されます。

varはスコープを持たない!

letによりレキシカルスコープが導入された例

var topic = "JavaScript";

if (topic) {
  let topic = "React";
  console.log("block", topic);
}

console.log("global", topic);

>
block React
global JavaScript

letを使うことで、外からifの中を参照できなくなり、topicは書き換えられること無く、globalは”JavaScript”となっています。

つまり、レキシカルスコープとは、変数letの定義時にスコープが作られるということです。

関数宣言と関数式

呼び出しの結果が全く同じとなるこの2つについて見ていきます。

この2つの違いはこちらです。

関数よりも前で呼び出せるか(関数の巻き上げが起こるか)どうか

関数宣言の場合は、前で呼び出すことができるが、関数式は後でしかできない。

つまり、関数式は宣言前の変数を参照できない。

それぞれの書き方がこちらです。

関数宣言

function logCompliment() {
  console.log("You're doing great!")
}
logCompliment()

この場合、宣言前に呼び出せる。

関数式

const logCompliment = fuction() {
  console.log("You're doing great!")
}

logCompliment()

この場合、宣言後にしか呼び出せない。

デフォルト引数

関数には、関数呼び出しの際に引数が与えられなかった場合のデフォルト値を設定することができます。

function logActivity(name = "Shinji", activity = "surfing") {
  console.log(`${name} loves ${activity}`);
}
logActivity()

>
Shinji loves surfing

アロー関数

アロー関数はJavaScriptの基本のキですが、アロー関数がどう生まれたのか新たに学んだのでまとめました。

アロー関数は、関数式の進化版だった!

関数式

const lordify = function(firstName) {
  return `${firstName} of Canterbury`
}

アロー関数

const lordify = firstName => `${firstName} of Canterbury`;

・戻り値が単一の式であればreturnは不要

・引数が一つであれば、( )の省略が可能

JavaScriptのコンパイル

最新の構文をブラウザで実行可能とするために、コンパイルの実行が必要となる。

こちらのBabal REPLというサイトで、どのようにコンパイルされているかを確認することができる。

Babel · The compiler for next generation JavaScript
The compiler for next generation JavaScript

デストラクチャリング(和名:分割代入)

オブジェクトの型も、他の値と同じように変数に代入することができます。

そのオブジェクトから必要なプロパティのみを選択するときにデストラクチャリング(分割代入)を使用することでスッキリ書くことができます。

// オブジェクトをsandwichという変数に代入
const sandwich = {
  bread: "dutch crunch",
  meat: "tuna",
  cheese: "swiss",
  toppings: ["lettuce", "tomato", "mustard"]
}

// sandwichを分割代入し、必要なプロパティ(bread, meat)のみを選択
const { bread, meat } = sandwich;

console.log(bread, meat)

>
dutch crunch tuna

関数のデストラクチャリング

デストラクチャリングを使わない場合

const lordify = regularPerson => {
  console.log(`${regularPerson.firstname} of Caterbury`)
}

const regularPerson = {
  firstname: "Bill",
  lastname: "Wilson"
}

lordify(regularPerson);

> Bill of Caterbury

デストラクチャリングを使った場合

デストラクチャリングによりregularPersonからfirstnameを取り出していることで、console部の呼び出しをスッキリ書くことができています。

const lordify = ({ firstname }) => {
  console.log(`${firstname} of Caterbury`);
};

const regularPerson = {
  firstname: "Bill",
  lastname: "Wilson",
};

lordify(regularPerson);

> Bill of Caterbury

配列のデストラクチャリング

配列の先頭の要素をローカル変数に代入する

const [firstAnimal] = ["Horse", "Mouse", "Cat"];
console.log(firstAnimal);

> Horse

配列の3番目の要素をローカル変数に代入する

const [, , thirdAnimal] = ["Horse", "Mouse", "Cat"];
console.log(thirdAnimal);

> Cat

オブジェクトリテラル

オブジェクトについてES2015によりこちらの変更がされています。

・プロパティ名を省略できる

・オブジェクト内の関数はfunctionキーワードを省略できる

ES5(従来の記法)

var skier = {
  sound: sound,
  powderYell: function () {
    var yell = this.sound.toUpperCase();
    console.log(`${yell}!!!`);
  },
  speed: function (mhp) {
    this.speed = mph;
    console.log("speed:", mph);
  },
};

ES2015(新しい記法)

  • soundの省略
  • functionの省略
var skier = {
  sound,
  powderYell() {
    let yell = this.sound.toUpperCase();
    console.log(`${yell}!!!`);
  },
  speed(mhp) {
    this.speed = mph;
    console.log("speed:", mph);
  },
};

async/await

こちらのrandomuser.meという、randomにuser情報を生成してくれているapiを使って使用例を見てみます。

Random User Generator | Home
Random user generator is a FREE API for generating placeholder user information. Get profile photos, names, and more. It's like Lorem Ipsum, for people.

const getFakePerson = async () => {
  const res = await fetch("https://randomuser.me/api/");
  const { results } = await res.json();
  console.log(results);
};
getFakePerson();

> 0: {gender: 'female', name: {…}, location: {…}, email: 'sylvia.crawford@example.com', login: {…}, …}
length: 1

awaitを書くことで、それ以降のコードはPromiseが成功するまで処理を待たせることができる。

fetchできなかった場合の例外処理を書く場合、tryとcatchを使います。

const getFakePerson = async () => {
  try {
    const res = await fetch("https://randomuser.me/api/");
    const { results } = await res.json();
    console.log(results);
  } catch (error) {
    console.log(error);
  }
};
getFakePerson();

クラス宣言

classを使うと何ができるのか、classを使う場合と、classを使わずに同じ定義をカスタムクラスで行う場合を比較します。

classを使用せずカスタムクラスを定義する方法

function Vacation(destination, length) {
  // コンストラクタ関数内で初期化
  this.destination = destination;
  this.length = length;
}
Vacation.prototype.print = function () {
  console.log(this.destination + " | " + this.length + " days");
};
// new演算子でインスタンス化
var maui = new Vacation("Maui", 7);
maui.print();

> Maui | 7 days

classキーワードによるクラス宣言

class Vacation {
  constructor(destination, length) {
    this.destination = destination;
    this.length = length;
  }
  print() {
    console.log(`${this.destination} will take ${this.length} days.`);
  }
}
const trip = new Vacation("Chile", 7);
trip.print();

> Chile will take 7 days.

ECMAScriptモジュール

モジュールとは、再利用可能で、他のファイルからインポートして利用できるもののことです。

モジュールは、ファイルの括りで格納されているため、別のモジュールと変数名が重複しても問題ないです。

exportして、他のモジュールからimportしての使用も可能です。

export const print = (message) => log(message, date());

export const log = (message, timestamp) =>
  console.log(`${timestamp.toString()}: ${message}`);

import側

import { print, log } from "./text-helpers";

print("printing a message");
log("logging a message");

イミュータブルなデータ

イミュータブルとは、変異(mutate)しない、つまり変更できない状態を指します。

オブジェクトの例

緑色(lawn)を表す以下のオブジェクトを例にデータをイミュータブルとする方法を見ていきます。

let color_lawn = {
  title: "lawn",
  color: "#00FF00",
  rating: 0,
};

これに変更を加え、ratingプロパティの値を書き換えます。

function rateColor(color, rating) {
  color.rating = rating;
  return color;
}

console.log(rateColor(color_lawn, 5).rating);
> 5

console.log(color_lawn.rating);
> 5

関数rateColorの実行結果が変更されているのは良いですが、color_lawn.ratingを確認すると、元オブジェクトのratingまで5に書き換わってしまっています。

これに変更が加わらないよう、つまりイミュータブルにします。

function rateColor(color, rating) {
  // 第一引数に空オブジェクト{}を渡すことで、新規に作成されたオブジェクトに対し変更を加えられる
  return Object.assign({}, color, { rating: rating });
}

console.log(rateColor(color_lawn, 5).rating);
> 5

console.log(color_lawn.rating);
> 0

イミュータブルとすることができました!

これを、アロー関数を使って、スプレッド構文も使うと更に簡単に書くことができます。

const rateColor = (color, rating) => ({
  ...color,
  rating,
});

console.log(rateColor(color_lawn, 5).rating);
> 5

console.log(color_lawn.rating);
> 0

配列の例

次に、配列のデータをイミュータブルとする方法についてです。

色の情報を格納したこちらの配列を例に見ていきます。

let list = [{ title: "Red" }, { title: "Yellow" }, { title: "Green" }];

これに対して色を追加します。

pushを使用した場合

const addColor = function (title, colors) {
  colors.push({ title: title });
  return colors;
};

console.log(addColor("Blue", list).length);
> 4

console.log(list.length);
> 4

どちらも出力結果が4であることから、元の配列も変更してしまっていることがわかります。

元の配列をイミュータブルにするには、Array.concatを使います。

const addColor = (title, array) => array.concat({ title });

console.log(addColor("Blue", list).length);
> 4

console.log(list.length);
> 3

Array.concatは配列のコピーを作成した上で追加をするため、元の配列はそのままになります。

オブジェクトのときと同様、スプレッド構文を使用すると以下のように書くことができる。

const addColor = (title, list) => [...list, { title }];

データの変換

JavaScripに用意されているビルトイン関数を使ってデータを変換する例を見ていきます。

今回データ変換に使用する関数はこちらの4つ

  • join
  • filter
  • map
  • reduce

Array.join

指定した区切り文字(今回の場合は”, “)で配列の全要素を結合して、一続きの文字列に変換することができる。

const schools = ["Waseda", "Keio", "Jochi"];

console.log(schools.join(", "));

> Waseda, Keio, jochi

Array.filter

filter関数は真偽値を返す。

このような、配列の要素を引数にとり、真偽値を返すコールバック関数をpredicateという。

const wSchools = schools.filter(school => school[0] === "W")

console.log(wSchools)

> ['Waseda']

school[0]で、イニシャルがWである要素を探し出すので、trueとなるWasedaが取得できます。

文字列にインデックスが使われているのが不思議な感じがしますが、これでイニシャルを取得できます。

以下のようにschoolを取り出してみるとわかります。

  const schools = ["Waseda", "Keio", "Jochi"];
  const wSchools = schools.filter(
    (school) => console.log(school, school[0])
  );
  console.log(wSchools);

配列から要素を削除する(取り除く)用途で使う場合

const cutSchool = (cut, list) => list.filter(school => school !== cut)

console.log(cutSchool("Waseda", schools).join(", "))

> Keio, Jochi

条件に一致しない要素で配列を作っています。

Array.map

配列の要素の数だけ呼び出され、各要素が引数として渡される。

戻り値は新しい配列に追加される。

const highSchools = schools.map(school => `${school} High School`)

console.log(highSchools)

> (3) ['Waseda High School', 'Keio High School', 'Jochi High School']

文字列配列をオブジェクト配列に変換する

const highSchools = schools.map(school => ({ name: school }))

console.log(highSchools)

>
[
  {name: 'Waseda'}
  {name: 'Keio'}
  {name: 'Jochi'}
]

Array.reduce

配列を単一の数値に変換する

const ages = [21, 18, 42, 40 , 64, 63, 34]
const maxAge = ages.reduce((max, age) => {
  console.log(`${age} > ${max} = ${age > max}`)
  if (age > max) {
    return age
  } else {
    return max
  }
}, 0)

console.log("maxAge", maxAge)

>
21 > 0 = true
18 > 21 = false
42 > 21 = true
40 > 42 = false
64 > 42 = true
63 > 64 = false
34 > 64 = false
maxAge 64

これを三項演算子で書くと以下のようにシンプルに書くことができる。

const max = ages.reduce((max, value) => (value > max ? value : max), 0)

再帰

再帰とは、関数の中から自分自身を呼び出すテクニック

数値が0より大きければ1減らした上で再帰的に自分自身を呼び出す例を見ていきます。

const countdown = (value, fn) => {
  fn(value);
  return value > 0 ? countdown(value - 1, fn) : value;
};

countdown(10, (value) => console.log(value));

>
10
9
8
7
6
5
4
3
2
1
0
0

時間の概念を追加し非同期処理とした場合

const countdown = (value, fn, delay = 1000) => {
  fn(value);
  return value > 0
    ? setTimeout(() => countdown(value - 1, fn, delay), delay)
    : value;
};

const log = (value) => console.log(value);

countdown(10, log);

>
10
2
9
8
7
6
5
4
3
2
1
0

delayを加えたことで、1秒おきにデクリメントされる。

デクリメントとは、数値を1減らすことを言います。

アプリケーションの構築

ここまで学習してきた内容を応用して、デジタル時計を実装します。

const oneSecond = () => 1000;
const getCurrentTime = () => new Date();
const clear = () => console.clear();
const log = (message) => console.log(message);

// 時間のオブジェクトに変換
const serializeClockTime = (date) => ({
  hours: date.getHours(),
  minutes: date.getMinutes(),
  seconds: date.getSeconds(),
});

// 午前午後を意識した時刻に変換
const civilianHours = (clockTime) => ({
  ...clockTime,
  hours: clockTime.hours > 12 ? clockTime.hours - 12 : clockTime.hours,
});

// AMPMを追加
const appendAMPM = (clockTime) => ({
  ...clockTime,
  ampm: clockTime.hours >= 12 ? "PM" : "AM",
});

// 高階関数
// 時刻を表示する関数を戻り値として返す。
const display = (target) => (time) => target(time);

// 高階関数
// 実際の数値に変換する関数を戻り値として返す。
const formatClock = (format) => (time) =>
  format
    .replace("hh", time.hours)
    .replace("mm", time.minutes)
    .replace("ss", time.seconds)
    .replace("tt", time.ampm);

// 10より小さければ0をつける
const prependZero = (key) => (clockTime) => ({
  ...clockTime,
  [key]: clockTime[key] < 10 ? "0" + clockTime[key] : "" + clockTime[key],
});

const compose =
  (...fns) =>
  (arg) =>
    fns.reduce((composed, f) => f(composed), arg);

// 関数の合成
const convertToCivilianTime = (clockTime) =>
  compose(appendAMPM, civilianHours)(clockTime);

// prependZeroを合成
const doubleDigits = (civilianTime) =>
  compose(
    prependZero("hours"),
    prependZero("minutes"),
    prependZero("seconds")
  )(civilianTime);

// 全ての関数を合成
// 出来上がった関数をsetIntervalにセットすることで、1秒おきに呼び出される。
const startTicking = () =>
  setInterval(
    compose(
      clear,
      getCurrentTime,
      serializeClockTime,
      convertToCivilianTime,
      doubleDigits,
      formatClock("hh:mm:ss tt"),
      display(log)
    ),
    oneSecond()
  );

startTicking();

こんな時計を作ることができました!

高階関数とは、関数を引数に取る、関数を返すもののことを言います。

27行目でアロー(=>)が一行に2つ使われています。

これはどういう意味なのでしょうか?

これは、引数を一つとり、引数を一つとる関数を返します。

const display = (target) => (time) => target(time);

つまり、この例の場合、引数にtargetをとり、引数にtimeをとる関数target(time)を返します。

こう書くとわかりやすいのかな?

const display = (target) => {
  return (time) => {
    return target(time)
  }
}

以上、読んでいただきありがとうございました。

コメント