How do JavaScript closures work? Ask Question

How do JavaScript closures work? Ask Question

How would you explain JavaScript closures to someone with a knowledge of the concepts they consist of (for example functions, variables and the like), but does not understand closures themselves?

I have seen the Scheme example given on Wikipedia, but unfortunately it did not help.

ベストアンサー1

A closure is a pairing of:

  1. A function and
  2. A reference to that function's outer scope (lexical environment)

A lexical environment is part of every execution context (stack frame) and is a map between identifiers (i.e. local variable names) and values.

Every function in JavaScript maintains a reference to its outer lexical environment. This reference is used to configure the execution context created when a function is invoked. This reference enables code inside the function to "see" variables declared outside the function, regardless of when and where the function is called.

If a function was called by a function, which in turn was called by another function, then a chain of references to outer lexical environments is created. This chain is called the scope chain.

In the following code, inner forms a closure with the lexical environment of the execution context created when foo is invoked, closing over variable secret:

function foo() {
  const secret = Math.trunc(Math.random() * 100)
  return function inner() {
    console.log(`The secret number is ${secret}.`)
  }
}
const f = foo() // `secret` is not directly accessible from outside `foo`
f() // The only way to retrieve `secret` is to invoke `f`

In other words: in JavaScript, functions carry a reference to a private "box of state", to which only they (and any other functions declared within the same lexical environment) have access. This box of the state is invisible to the caller of the function, delivering an excellent mechanism for data-hiding and encapsulation.

And remember: functions in JavaScript can be passed around like variables (first-class functions), meaning these pairings of functionality and state can be passed around your program, similar to how you might pass an instance of a class around in C++.

If JavaScript did not have closures, then more states would have to be passed between functions explicitly, making parameter lists longer and code noisier.

So, if you want a function to always have access to a private piece of state, you can use a closure.

...and frequently we do want to associate the state with a function. For example, in Java or C++, when you add a private instance variable and a method to a class, you are associating the state with functionality.

C や他の一般的な言語では、関数が返された後、スタック フレームが破棄されるため、すべてのローカル変数にアクセスできなくなります。JavaScript では、関数を別の関数内で宣言すると、外側の関数のローカル変数は、その関数から返された後もアクセス可能なままになります。このように、上記のコードでは、secret関数オブジェクト はinnerから返された後もfoo引き続き使用できます。

クロージャの使用

クロージャは、関数に関連付けられたプライベートな状態が必要な場合に便利です。これは非常に一般的なシナリオです。覚えておいてください: JavaScript には 2015 年までクラス構文がなく、プライベート フィールド構文もまだありません。クロージャはこのニーズを満たします。

プライベートインスタンス変数

次のコードでは、関数はtoString車の詳細を閉じます。

function Car(manufacturer, model, year, color) {
  return {
    toString() {
      return `${manufacturer} ${model} (${year}, ${color})`
    }
  }
}

const car = new Car('Aston Martin', 'V8 Vantage', '2012', 'Quantum Silver')
console.log(car.toString())

関数型プログラミング

次のコードでは、関数はinnerの両方を閉じますfnargs

function curry(fn) {
  const args = []
  return function inner(arg) {
    if(args.length === fn.length) return fn(...args)
    args.push(arg)
    return inner
  }
}

function add(a, b) {
  return a + b
}

const curriedAdd = curry(add)
console.log(curriedAdd(2)(3)()) // 5

イベント指向プログラミング

次のコードでは、関数onClickは変数を閉じますBACKGROUND_COLOR

const $ = document.querySelector.bind(document)
const BACKGROUND_COLOR = 'rgba(200, 200, 242, 1)'

function onClick() {
  $('body').style.background = BACKGROUND_COLOR
}

$('button').addEventListener('click', onClick)
<button>Set background color</button>

モジュール化

次の例では、実装の詳細はすべて、すぐに実行される関数式の中に隠されています。関数ticktoString関数は、作業を完了するために必要なプライベート状態と関数を閉じます。クロージャにより、コードをモジュール化してカプセル化できるようになりました。

let namespace = {};

(function foo(n) {
  let numbers = []

  function format(n) {
    return Math.trunc(n)
  }

  function tick() {
    numbers.push(Math.random() * 100)
  }

  function toString() {
    return numbers.map(format)
  }

  n.counter = {
    tick,
    toString
  }
}(namespace))

const counter = namespace.counter
counter.tick()
counter.tick()
console.log(counter.toString())

例1

この例では、ローカル変数がクロージャにコピーされないことを示しています。クロージャは元の変数自体への参照を維持します。外側の関数が終了した後も、スタックフレームがメモリ内に残っているかのようです。

function foo() {
  let x = 42
  let inner = () => console.log(x)
  x = x + 1
  return inner
}

foo()() // logs 43

例2

次のコードでは、、の 3 つのメソッドlogincrementすべてupdate同じ語彙環境を閉じます。

そして、が呼び出されるたびに、新しい実行コンテキスト (スタック フレーム) が作成され、この新しい変数を閉じるcreateObjectまったく新しい変数xと新しい関数のセット (など) が作成されます。log

function createObject() {
  let x = 42;
  return {
    log() { console.log(x) },
    increment() { x++ },
    update(value) { x = value }
  }
}

const o = createObject()
o.increment()
o.log() // 43
o.update(5)
o.log() // 5
const p = createObject()
p.log() // 42

例3

を使用して宣言された変数を使用している場合はvar、どの変数を閉じるのかを慎重に理解してください。 を使用して宣言された変数は巻き上げられます。およびvarの導入により、最近の JavaScript ではこの問題はほとんど発生しなくなりましたletconst

次のコードでは、ループを回るたびに、innerを閉じる新しい関数が作成されますi。しかし、var iはループの外側に持ち上げられているため、これらの内部関数はすべて同じ変数を閉じることになり、(3) の最終値がi3 回印刷されることになります。

function foo() {
  var result = []
  for (var i = 0; i < 3; i++) {
    result.push(function inner() { console.log(i) } )
  }

  return result
}

const result = foo()
// The following will print `3`, three times...
for (var i = 0; i < 3; i++) {
  result[i]() 
}

最終ポイント:

  • JavaScript で関数が宣言されるたびに、クロージャが作成されます。
  • 別の関数内からを返すことは、functionクロージャの典型的な例です。これは、外側の関数の実行が完了した後でも、外側の関数内の状態が、返された内側の関数で暗黙的に使用できるためです。
  • eval()関数内で使用する場合は常にクロージャが使用されます。 テキストを使用するとeval関数のローカル変数を参照でき、非厳密モードでは を使用して新しいローカル変数を作成することもできますeval('var foo = …')
  • new Function(…)関数コンストラクタ) を関数内に挿入すると、その関数の字句環境は閉じられません。代わりにグローバル コンテキストが閉じられます。新しい関数は、外側の関数のローカル変数を参照できません。
  • JavaScript のクロージャは、関数宣言の時点でスコープへの参照(コピーではない)を保持するようなもので、関数宣言によってその外側のスコープへの参照が保持され、スコープ チェーンの最上部にあるグローバル オブジェクトまでそれが続きます。
  • 関数が宣言されるとクロージャが作成されます。このクロージャは、関数が呼び出されたときの実行コンテキストを構成するために使用されます。
  • 関数が呼び出されるたびに、新しいローカル変数のセットが作成されます。

リンク

おすすめ記事