JavaScript 클로징은 어떤 개념(예를 들어 함수, 변수 등)을 알고 있지만 클로징 자체는 이해하지 못하는 사람에게 어떻게 설명할 수 있습니까?
Wikipedia에 있는 Scheme의 예를 보았습니다만, 유감스럽게도 도움이 되지 않았습니다.
질문에 대한 답변
폐쇄는 다음 요소의 조합입니다.
- 기능 및
- 해당 기능의 외부 범위(렉시컬 환경)에 대한 참조
어휘 환경은 모든 실행 컨텍스트(스택 프레임)의 일부이며 식별자(로컬 변수 이름)와 값 사이의 맵입니다.
JavaScript의 모든 함수는 외부 어휘 환경에 대한 참조를 유지합니다.이 참조는 함수를 호출할 때 생성되는 실행 컨텍스트를 구성하기 위해 사용됩니다.이 참조를 통해 함수가 호출된 시기와 위치에 관계없이 함수 내부의 코드가 함수 외부에 선언된 변수를 볼 수 있습니다.
함수에 의해 함수가 호출되고, 다른 함수에 의해 호출된 경우, 외부 어휘 환경에 대한 참조 사슬이 생성됩니다.이 체인을 스코프 체인이라고 합니다.
다음 코드에서는inner
다음 경우에 생성된 실행 컨텍스트의 사전 환경과 함께 폐쇄를 형성합니다.foo
호출되어 변수 위로 닫힙니다.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`
즉, JavaScript에서 함수는 개인 “상태 상자”에 대한 참조를 전달하며, 이들(및 동일한 어휘 환경 내에서 선언된 다른 함수)만 액세스할 수 있습니다.이 상태의 상자는 함수의 발신자에게는 보이지 않기 때문에 데이터 숨기기 및 캡슐화를 위한 뛰어난 메커니즘을 제공합니다.
또한 JavaScript의 함수는 변수(퍼스트 클래스 함수)처럼 전달될 수 있습니다.즉, C++에서 클래스의 인스턴스를 전달하는 것과 같은 기능과 상태의 쌍이 프로그램에 전달될 수 있습니다.
JavaScript에 closes가 없다면 함수 간에 더 많은 상태를 명시적으로 전달해야 하므로 파라미터 목록이 더 길고 코드 노이즈가 더 많이 발생합니다.
따라서 함수가 항상 개인 상태에 액세스할 수 있도록 하려면 닫기를 사용할 수 있습니다.
…그리고 우리는 종종 상태를 함수와 연관짓고 싶어합니다.예를 들어 Java 또는 C++에서는 개인 인스턴스 변수와 메서드를 클래스에 추가할 때 상태를 기능과 관련짓습니다.
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
둘 다 닫다fn
그리고.args
.
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
close over 변수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>
모듈화
다음 예제에서는 모든 구현 세부 정보가 즉시 실행된 함수 식 안에 숨겨져 있습니다.기능tick
그리고.toString
작업을 완료하기 위해 필요한 개인 상태 및 기능을 닫습니다.폐쇄로 인해 우리는 코드를 모듈화하고 캡슐화할 수 있게 되었습니다.
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
다음 코드에서는 세 가지 방법이 있습니다.log
,increment
,그리고.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의 도입으로 인해 훨씬 덜 문제가 됩니다.let
그리고.const
.
다음 코드에서는 루프를 돌 때마다 새로운 함수가 생성됩니다.inner
생성되어 닫힙니다.i
근데 왜냐하면var i
이 내부 함수는 모두 같은 변수 상에서 닫힙니다.즉, 이 값은 루프의 최종값입니다.i
(3)은 3회 인쇄됩니다.
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에서 함수가 선언될 때마다 closure가 생성됩니다.
- 반환하다
function
외부 함수의 실행이 완료된 후에도 외부 함수의 상태가 반환된 내부 함수에 암묵적으로 사용 가능하기 때문에 다른 함수의 내부가 닫힘의 전형적인 예입니다. - 사용할 때마다
eval()
함수 내부에서 폐쇄가 사용됩니다.당신의 텍스트eval
함수의 로컬 변수를 참조할 수 있습니다.또, 비표준 모드에서는, 를 사용해 새로운 로컬 변수를 작성할 수도 있습니다.eval('var foo = …')
. - 사용할 때
new Function(…)
(함수 생성자) 함수 내에서는 사전 환경을 닫지 않고 글로벌 컨텍스트를 닫는다.새 함수는 외부 함수의 로컬 변수를 참조할 수 없습니다. - JavaScript의 클로징은 함수 선언 지점에서 스코프에 대한 참조(복사가 아님)를 유지하는 것과 같습니다.이것에 의해 스코프 체인 상단의 글로벌오브젝트에 대한 참조가 유지됩니다.
- 함수가 선언되면 닫힘이 생성됩니다.이 닫힘은 함수가 호출될 때 실행 컨텍스트를 구성하는 데 사용됩니다.
- 함수를 호출할 때마다 새로운 로컬 변수 세트가 생성됩니다.
링크
- 더글라스 크록포드의 시뮬레이션된 개인 속성과 폐쇄를 이용한 개인 방법.
- 주의하지 않으면 IE에서 메모리 누설이 발생할 수 있는 폐기에 대한 자세한 설명입니다.
- JavaScript Closes에 관한 MDN 문서.
JavaScript의 모든 함수는 외부 어휘 환경에 대한 링크를 유지합니다.어휘 환경은 범위 내의 모든 이름(예: 변수, 매개변수)과 그 값의 맵입니다.
그래서 여러분들이 볼 때마다function
키워드, 함수 내부의 코드는 함수 외부에 선언된 변수에 액세스할 수 있습니다.
function foo(x) {
var tmp = 3;
function bar(y) {
console.log(x + y + (++tmp)); // will log 16
}
bar(10); }
foo(2);
이것은 로그에 기록됩니다.16
기능하기 때문에bar
파라미터를 닫습니다.x
및 변수tmp
둘 다 외부 함수의 어휘 환경에 존재한다.foo
.
기능.bar
, 기능의 어휘 환경과의 링크와 함께foo
끝장이야.
닫힘을 만들기 위해 함수를 반환할 필요는 없습니다.단순히 선언에 의해 모든 함수는 둘러싸인 어휘 환경에서 닫히고 닫힙니다.
function foo(x) {
var tmp = 3;
return function (y) {
console.log(x + y + (++tmp)); // will also log 16
} }
var bar = foo(2); bar(10); // 16 bar(10); // 17
위의 함수도 16을 기록합니다.왜냐하면,bar
아직 논거를 참조할 수 있다x
및 변수tmp
단, 더 이상 직접 적용범위가 아닌 경우에도 마찬가지입니다.
하지만, 그 이후로는tmp
아직도 안에서 맴돌고 있다bar
가 닫힙니다.증가할 수 있습니다.호출할 때마다 증가합니다.bar
.
폐쇄의 가장 간단한 예는 다음과 같습니다.
var a = 10;
function test() {
console.log(a); // will output 10
console.log(b); // will output 6 } var b = 6; test();
JavaScript 함수가 호출되면 새로운 실행 컨텍스트가 실행됩니다.ec
작성됩니다.함수 인수 및 타깃오브젝트와 함께 이 실행 컨텍스트는 호출 실행 컨텍스트의 사전 환경 링크도 받습니다.즉, 외부 어휘 환경에서 선언된 변수(위의 예에서는 양쪽 모두)를 의미합니다.a
그리고.b
)는, 다음의 사이트에서 입수할 수 있습니다.ec
.
모든 함수는 외부 어휘 환경에 대한 링크를 가지고 있기 때문에 모든 함수는 폐쇄를 생성합니다.
변수 자체는 복사가 아닌 폐쇄 내에서 볼 수 있습니다.
서문: 이 답변은 다음과 같은 경우에 작성되었습니다.
앨버트가 말한 것처럼, “만약 6살짜리에게 설명할 수 없다면, 당신은 정말로 그것을 이해하지 못할 것입니다.”JS의 폐쇄에 대해 27년 된 친구에게 설명하려다 완전히 실패했어요.
내가 6살이고 그 과목에 이상하게 관심이 있다고 생각할 수 있는 사람이 있나요?
제가 첫 질문을 문자 그대로 받아들이려고 했던 유일한 사람 중 하나라고 확신합니다.그 이후로, 그 질문은 여러 번 변이되어 왔기 때문에, 제 대답은 이제 믿을 수 없을 정도로 어리석고 적절하지 않은 것처럼 보일지도 모릅니다.이 이야기의 전반적인 아이디어가 일부 사람들에게는 여전히 재미있기를 바란다.
저는 어려운 개념을 설명할 때 유추와 은유를 매우 좋아하기 때문에 이야기를 해보겠습니다.
옛날 옛적:
공주가 있었는데…
function princess() {
그녀는 모험으로 가득한 멋진 세상에서 살았다.그녀는 그녀의 매력적인 왕자를 만났고, 유니콘을 타고 그녀의 세계를 일주했고, 용과 싸웠고, 말하는 동물들과 마주쳤고, 그리고 다른 많은 환상적인 것들을 만났다.
var adventures = [];
function princeCharming() { /* ... */ }
var unicorn = { /* ... */ },
dragons = [ /* ... */ ],
squirrel = "Hello!";
/* ... */
하지만 그녀는 항상 지루한 집안일과 어른들의 세계로 돌아가야 할 것이다.
return {
그리고 그녀는 종종 그들에게 공주로서의 최근의 놀라운 모험에 대해 이야기하곤 했다.
story: function() {
return adventures[adventures.length - 1];
}
}; }
하지만 그들이 볼 수 있는 건 어린 소녀뿐이었어…
var littleGirl = princess();
마법과 환상에 대한 이야기를 들려주죠
littleGirl.story();
어른들이 진짜 공주를 알더라도 유니콘이나 용을 볼 수 없기 때문에 믿지 않을 것이다.어른들은 어린 소녀의 상상 속에만 존재한다고 말했다.
하지만 우리는 진실을 알고 있어요. 공주와 함께 있는 어린 소녀는…
공주님 안에 어린 여자애가 있는 것 같아요
이 질문을 진지하게 받아들이면서 우리는 일반적인 6살짜리 아이가 인지적으로 무엇을 할 수 있는지 알아내야 한다. 하지만 JavaScript에 관심이 있는 아이는 그렇게 전형적이지 않다.
아동 발달: 5~7년간 다음과 같이 기술되어 있습니다.
자녀는 2단계 지시를 따를 수 있습니다.예를 들어, 아이에게 “주방에 가서 쓰레기 봉투를 가져와”라고 말하면 그 방향을 기억할 수 있습니다.
이 예를 사용하여 폐쇄를 다음과 같이 설명할 수 있습니다.
주방은 지역 변수가 있는 폐쇄입니다.
trashBags
주방에 ‘라는 기능이 있습니다.getTrashBag
쓰레기봉투 하나 받고 돌려주는 거죠
JavaScript에서는 다음과 같이 코드화할 수 있습니다.
function makeKitchen() {
var trashBags = ['A', 'B', 'C']; // only 3 at first
return {
getTrashBag: function() {
return trashBags.pop();
}
}; }
var kitchen = makeKitchen();
console.log(kitchen.getTrashBag()); // returns trash bag C console.log(kitchen.getTrashBag()); // returns trash bag B console.log(kitchen.getTrashBag()); // returns trash bag A
폐쇄가 흥미로운 이유를 설명하는 추가 사항:
- 매번
makeKitchen()
호출되고, 새로운 폐쇄가 생성되며, 그 자체와 함께trashBags
. - 그
trashBags
변수는 각 부엌의 안쪽에 있어 외부에서 접근할 수 없지만 내부 함수는 다음과 같습니다.getTrashBag
속성이 액세스할 수 있습니다. - 모든 함수 호출이 닫힘을 생성하지만 닫힘 내부에 액세스할 수 있는 내부 함수를 닫힘 외부에서 호출할 수 없는 한 닫힘을 유지할 필요가 없습니다.와 함께 오브젝트 반환
getTrashBag
함수는 여기서 그렇게 합니다.
빨대맨
버튼을 몇 번 클릭했는지 알아야 하며 3번 클릭할 때마다 작업을 수행해야 합니다.
지극히 명백한 솔루션
// Declare counter outside event handler's scope var counter = 0; var element = document.getElementById('button');
element.addEventListener("click", function() {
// Increment outside counter
counter++;
if (counter === 3) {
// Do something every third time
console.log("Third time's the charm!");
// Reset counter
counter = 0;
} });
<button id="button">Click Me!</button>
이 방법은 작동하지만 카운트를 추적하는 것이 유일한 목적인 변수를 추가하여 외부 범위를 잠식합니다.경우에 따라서는, 외부 애플리케이션에 이 정보에 액세스 할 필요가 있는 경우가 있기 때문에, 이 방법이 바람직할 수 있습니다.단, 이 경우 세 번 클릭할 때마다 변경되기 때문에 이벤트핸들러에 이 기능을 포함시키는 것이 좋습니다.
이 옵션을 고려합니다.
var element = document.getElementById('button');
element.addEventListener("click", (function() {
// init the count to 0
var count = 0;
return function(e) { // <- This function becomes the click handler
count++; //
and will retain access to the above `count`
if (count === 3) {
// Do something every third time
console.log("Third time's the charm!");
//Reset counter
count = 0;
}
}; })());
<button id="button">Click Me!</button>
여기서 몇 가지 점에 주의해 주십시오.
위의 예에서는 JavaScript의 closure 동작을 사용하고 있습니다.이 동작에 의해, 모든 함수는, 작성한 범위에 무제한으로 액세스 할 수 있습니다.이것을 실제로 적용하기 위해서, 나는 즉시 다른 함수를 반환하는 함수를 호출하고, 내가 반환하는 함수는 (위에서 설명한 닫힘 동작 때문에) 내부 카운트 변수에 액세스할 수 있기 때문에, 결과적으로 함수에 의한 전용의 범위가 됩니다.그렇게 간단하지 않아요?희석해서…
한 줄의 간단한 폐쇄
//
_______________________Immediately invoked______________________ //
//
Scope retained for use
___Returned as the____
//
only by returned function
value of func
//
//
v
v
v
v
v
v var func = (function() { var a = 'val'; return function() { alert(a); }; })();
반환된 함수 이외의 모든 변수는 반환된 함수에서 사용할 수 있지만 반환된 함수 개체에서는 직접 사용할 수 없습니다.
func();
// Alerts "val" func.a;
// Undefined
알 수 있습니까? 이 주요 예에서는 카운트 변수가 폐쇄 내에 포함되어 이벤트 핸들러가 항상 사용할 수 있으므로 클릭할 때마다 상태가 유지됩니다.
또한 이 개인 변수 상태는 판독치와 개인 범위 변수에 할당 모두에서 완전히 액세스할 수 있습니다.
자, 이제 이 동작을 완전히 캡슐화했습니다.