Next.js と TypeScript で、クロージャーを理解する

TwitterFacebookHatena

TL;DR

このページでは、クロージャーの実装方法について解説しますね。一言でいうとクロージャーとは、関数が作成された環境を「記憶」する仕組みです。これにより、関数はその外部スコープの変数にアクセスできます。

開発環境 バージョン
Next.js 13.4.4
TypeScript 5.0.4
Emotion 11.11.0
React 18.2.0

「クロージャー」とは?

クロージャーは、プログラミングの重要な考え方で、特に JavaScript や TypeScript などの言語では頻繁に見られます。

まずクロージャーの考え方を、おもちゃの箱を使った例え話で説明します。

まず、大きな箱(関数)があります。この箱の中には、小さなおもちゃ(変数やデータ)が入っています。そして、この箱の中には、小さな箱(別の関数)も入っています。小さな箱が親の大きな箱から出て行ったときでも、小さな箱は親の箱からおもちゃ(データ)を持ち出すことができます。この小さな箱がまさしくクロージャーです。小さな箱(クロージャー)は、親の大きな箱から離れていても、親の箱にあるおもちゃ(変数やデータ)を自由に使うことができます。

それでは、この考え方をもとに、簡単なクロージャーのコードを見てみましょう。

function createCounter() {
  let count = 0

  return function () {
    count++
    console.log(count)
  }
}

const counter = createCounter()

counter() // "1" が出力される
counter() // "2" が出力される

ここでは、createCounterという大きな箱(関数)があります。そして、この中にはcountというおもちゃ(変数)があります。また、この中には別の小さな箱(関数)も存在します。

小さな箱(関数)はcreateCounterから出て行き、counterという新しい名前をつけて使われます。そしてcounterを呼び出す度に、countの値が 1 ずつ増えていきます。これは、小さな箱が親の大きな箱にあるおもちゃ(ここではcount)を使っているからです。

このように、クロージャーとは、関数とその関数が作られた環境(スコープ)との関係を表しているのです。

つまり、クロージャーは、関数が自身が定義されたスコープ(レキシカルスコープ)を「覚えて」いる状態を指します。これにより、関数は定義された場所の外部にある変数にアクセスでき、その値を操作することができます。

例えば、次のようなコードがあります。

function outerFunction() {
  let outerVariable = 'outer'

  function innerFunction() {
    console.log(outerVariable)
  }

  return innerFunction
}

const myInnerFunction = outerFunction()
myInnerFunction() // "outer" をログに表示

この例では、outerFunction 関数内に innerFunction という別の関数があります。innerFunctionouterVariable という変数を参照していますが、この変数は outerFunction のスコープ内に定義されています。しかし、innerFunctionouterFunction から返され、myInnerFunction として呼び出される時には、元々定義された outerFunction のスコープからは外れています。にも関わらず、innerFunction はまだ outerVariable にアクセスでき、その値をログに出力します。これがクロージャーの働きです。

レキシカルスコープ

レキシカルスコープ(静的スコープ)とは、プログラムの変数がどの範囲で参照できるか(スコープ)を、ソースコードを書く時点で決定するスコーピングの方法を指します。

通常、JavaScript や TypeScript では、関数やブロックによって新しいスコープが作られ、そのスコープ内で宣言された変数はそのスコープ内でのみ参照可能となります。これがレキシカルスコープの基本的なルールです。レキシカルスコープは「静的スコープ」とも呼ばれ、これはコードが実行される前、つまり静的な段階でスコープが決定されることを示しています。

たとえば以下のようなソースコードを考えてみましょう。

let outerVar = 'I am outside!' // この行はグローバルスコープにあります。

function outerFunc() {
  let innerVar = 'I am inside!' // この行はouterFuncのスコープにあります。

  function innerFunc() {
    console.log(innerVar) // この行はinnerFuncのスコープにあります。
    console.log(outerVar) // この行もinnerFuncのスコープにあります。
  }

  innerFunc()
}

outerFunc()

このコードを実行すると、innerFunc関数内で宣言されていないinnerVarouterVarが出力されます。これはレキシカルスコープの性質によります。innerFuncのスコープ内では、直接innerVarouterVarは宣言されていませんが、レキシカルスコープのルールにより、この関数は外側のスコープ(この場合はouterFuncとグローバルスコープ)を「見る」ことができます。

したがって、innerVarouterVarinnerFunc内で利用可能となり、その結果これらの変数の値がコンソールに出力されます。これがレキシカルスコープの一例です。

なお、JavaScript では var や function で宣言された変数や関数は、その宣言が所属する最も近い関数のスコープに束縛されます。一方、let や const で宣言された変数は、ブロックスコープ(if 文や for 文などの {} で囲まれた範囲)に束縛されます。これもレキシカルスコープの一部として理解することができます。

簡単に言えば、レキシカルスコープとは「変数が参照できる範囲」のことを指します。その「範囲」はコードを書く時点、つまりソースコードがどのように書かれているかによって決まります。

さらに詳しく言うと、関数やブロック(if 文、for 文、while 文などの{}で囲まれた部分)ごとに新たなスコープが作られ、そのスコープ内で宣言された変数はそのスコープ内でのみ参照可能となります。そして、内部のスコープからは外部のスコープの変数を参照できますが、その逆はできません。

このように、変数がどの範囲で参照可能かを決定するこの仕組みがレキシカルスコープです。これにより、プログラム内で変数の名前が衝突することを防いだり、変数の有効範囲を制限してプログラムの安全性を高めるなどの役割を果たしています。

アロー関数

アロー関数とクロージャーは、JavaScript と TypeScript のコーディングにおいて重要な役割を果たします。特に、アロー関数はクロージャーを作成する際に非常に便利なツールです。それでは、アロー関数とクロージャーの関係について詳しく見ていきましょう。

アロー関数は、短い構文で関数を定義できる機能です。アロー関数は、通常の関数とはいくつかの重要な違いがあります。その一つが「レキシカルスコープ」または「静的スコープ」と呼ばれる特性です。これは、アロー関数内部で変数を参照するときに、その変数が存在する場所を決定するためのルールです。

この特性により、アロー関数内部から外部の変数を参照することが可能となります。この振る舞いがまさしく「クロージャー」です。クロージャーとは、一つの関数が自身の外部スコープにある変数を「覚えている」状態を指します。

それでは具体的なコードを見てみましょう。

let value = 0

const incrementValue = () => {
  value++
  console.log(value)
}

incrementValue() // 1 が出力される
incrementValue() // 2 が出力される

この例では、incrementValueというアロー関数があります。このアロー関数は外部の変数valueにアクセスし、その値を増加させています。これはクロージャーの一例で、アロー関数が自身の外部スコープにある変数valueを「覚えている」からです。

アロー関数を使用することで、簡潔な構文でクロージャーを作成し、関数が特定のスコープの変数を「記憶」することが可能となります。この特性は、関数の振る舞いを柔軟にコントロールすることを可能にし、様々なプログラミングパターンを実現します。

Next.js で、クロージャーを実装

Next.js と TypeScript を使って、クロージャーをどのように使うのかを見ていきましょう。今回は、クロージャーを使ってカウンターを作る例を見てみましょう。

pages/index.tsx

import { useState } from 'react'

type Props = {
  initialCount: number
}

const IndexPage = ({ initialCount }: Props) => {
  const [count, setCount] = useState(initialCount)

  const increment = () => {
    setCount((prevCount) => prevCount + 1)
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  )
}

export default IndexPage

ここでのポイントは、setCount 関数内にある関数です。この関数は prevCount という現在の状態を引数に取り、新しい状態(つまり現在のカウントに 1 を足したもの)を返します。この関数は、実際には setCount が呼ばれたときのスコープ内で実行されます。それにもかかわらず、この関数は count の値にアクセスし、それを更新できます。これがクロージャーの一例です。

クロージャーを実装

クロージャーは、より高度なテクニックでも活用できます。例えば、状態をカプセル化するためや、関数をメモ化するために使用できます。

次の例では、関数をメモ化するための簡単な memoize 関数を作成します。この関数は、引数として取った関数の結果をキャッシュし、同じ引数で再度呼ばれたときにはキャッシュから結果を返すという動作をします。

function memoize(func) {
  const cache = {}

  return (...args) => {
    const argStr = JSON.stringify(args)
    if (cache[argStr]) {
      return cache[argStr]
    }

    const result = func(...args)
    cache[argStr] = result
    return result
  }
}

const slowFunction = (x) => {
  // 何か時間のかかる処理
}

const memoizedSlowFunction = memoize(slowFunction)

この memoize 関数は、まず空のキャッシュ(cache)を作成します。次に、新たな関数を返します。この関数は任意の数の引数(args)を受け取り、それらをキーとしてキャッシュをチェックします。キャッシュに結果が存在する場合、その結果が返されます。存在しない場合、引数を使って元の関数(func)が実行され、その結果がキャッシュに保存されて返されます。この挙動により、slowFunction が同じ引数で複数回呼ばれても、時間のかかる処理は一度しか実行されず、その後はキャッシュから結果が返されます。

このように、クロージャーは JavaScript と TypeScript の強力な特性であり、様々なケースで役立ちます。より詳しく学ぶためには、具体的なコードを書きながら挙動を理解していくと良いでしょう。

Next.js と TypeScript で、クロージャーを理解する