[javascript] JavaScript 클로저는 어떻게 작동합니까?

함수, 변수 등으로 구성된 개념을 알고 있지만 클로저 자체를 이해하지 못하는 사람에게 JavaScript 클로저를 어떻게 설명 하시겠습니까?

내가 본 구성표 예를 위키 백과에 제공을하지만, 불행히도 그것은 도움이되지 않았다.



답변

클로저는 다음의 쌍입니다.

  1. 기능
  2. 해당 함수의 외부 범위에 대한 참조 (어휘 환경)

어휘 환경은 모든 실행 컨텍스트 (스택 프레임)의 일부이며 식별자 (예 : 로컬 변수 이름)와 값 사이의 맵입니다.

JavaScript의 모든 함수는 외부 어휘 환경에 대한 참조를 유지합니다. 이 참조는 함수가 호출 될 때 작성된 실행 컨텍스트를 구성하는 데 사용됩니다. 이 참조는 함수 내부의 코드가 함수 호출시기 및 위치에 관계없이 함수 외부에서 선언 된 변수를 “볼”수있게합니다.

함수가 함수에 의해 호출 된 후 다른 함수에 의해 호출 된 경우 외부 어휘 환경에 대한 참조 체인이 작성됩니다. 이 체인을 스코프 체인이라고합니다.

다음 코드에서 변수를 닫으면 호출 inner될 때 작성된 실행 컨텍스트의 어휘 환경으로 클로저를 형성 합니다 .foosecret

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에 클로저가 없다면 함수간에 더 많은 상태를 명시 적으로 전달해야 하므로 매개 변수 목록이 길어지고 소음이 커집니다.

따라서 함수가 항상 개인 상태에 액세스하도록하려면 클로저를 사용할 수 있습니다.

… 우리 종종 상태를 함수와 연결하려고합니다. 예를 들어, 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모두 이상 종료 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  = function() { console.log(x) }
  x = x+1
  return inner
}
var f = foo()
f() // logs 43

실시 예 2

다음 코드, 세 가지 방법에서 log, increment그리고 update같은 어휘 환경에 대한 모든 가깝습니다.

그리고 매번 createObject호출 될 때마다 새로운 실행 컨텍스트 (스택 프레임)가 생성되고 완전히 새로운 변수가 생성되고이 새로운 변수 xlog근접한 새로운 기능 세트 등이 생성됩니다.

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는 게양됩니다. 이는 letand 의 도입으로 인해 최신 JavaScript에서 문제가 훨씬 적습니다 const.

다음 코드에서는 루프를 돌 때마다 새 함수 inner가 작성되어 닫힙니다 i. 그러나 var i루프 외부에 들어 있기 때문에 이러한 모든 내부 함수는 동일한 변수에 대해 닫히므로 i(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로 선언 될 때마다 클로저가 작성됩니다.
  • 반환 function외부 함수 내부 상태를 외부 함수가 실행을 완료 한 후에도, 반환 내부 기능을 암시 사용할 수 있으므로 다른 함수 내부에서 것은 클로저의 고전적인 예이다.
  • eval()함수 안에서 사용할 때마다 클로저가 사용됩니다. eval함수의 로컬 변수를 참조 할 수 있는 텍스트 이며, 엄격하지 않은 모드에서는을 사용하여 새 로컬 변수를 만들 수도 있습니다 eval('var foo = …').
  • 함수 내에서 new Function(…)( Function constructor ) 를 사용하면 어휘 환경을 닫지 않고 전역 컨텍스트를 닫습니다. 새 함수는 외부 함수의 로컬 변수를 참조 할 수 없습니다.
  • 자바 스크립트의 클로저는 참조 (유지처럼 NOT 상단의 전역 객체를 차례로 등등의 외부 범위에 대한 참조를 유지하고, 함수 선언의 시점에서 모든 방법을 범위에 사본) 스코프 체인
  • 함수가 선언되면 클로저가 생성됩니다. 이 클로저는 함수가 호출 될 때 실행 컨텍스트를 구성하는 데 사용됩니다.
  • 함수가 호출 될 때마다 새로운 로컬 변수 세트가 작성됩니다.

연결


답변

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을 기록합니다. 내부 코드 는 더 이상 직접 범위에 있지 않더라도 barargument x와 variable 을 계속 참조 할 수 있기 때문 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가 작성됩니다. 함께 함수 인수 및 타겟 오브젝트이 실행 컨텍스트 또한, 위의 예에서는 외측 어휘 환경에서 선언 된 변수 (즉, 호출 실행 콘텍스트의 사전 환경에 대한 링크를 수신 모두 ab )를 사용할 수 있습니다 ec.

모든 함수에는 외부 어휘 환경에 대한 링크가 있으므로 모든 함수는 클로저를 작성합니다.

변수 자체 는 복사본이 아닌 클로저 내에서 볼 수 있습니다 .


답변

서문 :이 답변은 질문이 다음과 같은 경우에 작성되었습니다.

알버트가 말한 것처럼 “만약 당신이 6 살짜리 아이에게 설명 할 수 없다면, 당신은 그것을 스스로 이해하지 못한다.”

아무도 내가 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함수 와 함께 객체를 반환하면 여기서 수행합니다.

답변

밀짚 맨

버튼을 몇 번 클릭했는지 알고 세 번째 클릭마다 무언가를해야합니다 …

상당히 명백한 해결책

// 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의 클로저 동작을 사용하고 있습니다. 이 동작을 통해 모든 함수가 작성된 범위에 무기한으로 액세스 할 수 있습니다.실제로 이것을 적용하기 위해 즉시 다른 함수를 반환하는 함수를 호출하고 반환하는 함수가 내부 카운트 변수에 액세스 할 수 있기 때문에 (위에서 설명한 클로저 동작 때문에) 결과로 사용할 수있는 개인 범위가됩니다. 기능 … 그렇게 간단하지 않습니까? 희석 시키자 …

간단한 한 줄 폐쇄

//          _______________________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

알 겠어요? 따라서 기본 예제에서 count 변수는 클로저 내에 포함되어 있으며 항상 이벤트 핸들러에서 사용할 수 있으므로 클릭 간 상태를 유지합니다.

또한이 개인 변수 상태는 완전히 모두 판독과의 개인 범위의 변수에 할당, 접근.

당신은 간다; 이제이 동작을 완전히 캡슐화했습니다.

전체 블로그 게시물 (jQuery 고려 사항 포함)


답변

클로저는 모든 사람들이 직관적으로 어쨌든 작동 할 것으로 예상되는 동작을 만드는 데 사용되기 때문에 설명하기가 어렵습니다. 나는 그것들을 설명하는 가장 좋은 방법과 그들이 하는 것을 배운 방법을 찾았 습니다.

    var bind = function(x) {
        return function(y) { return x + y; };
    }

    var plus5 = bind(5);
    console.log(plus5(3));

JavaScript 클로저를 알지 못하면 어떻게됩니까? 마지막 줄의 호출을 메소드 본문 (기본적으로 함수 호출이 수행하는 것)으로 바꾸면 다음과 같은 결과가 나타납니다.

console.log(x + 3);

이제 정의는 어디에 x있습니까? 현재 범위에서 정의하지 않았습니다. 유일한 해결책은 그 범위 (또는 부모의 범위)를 plus5 옮기는 것입니다. 이 방법 x은 잘 정의되어 있으며 값 5에 바인딩됩니다.


답변

좋아, 6 살짜리 클로저 팬. 가장 간단한 폐쇄 예를 듣고 싶습니까?

다음 상황을 상상해 봅시다 : 운전자가 차에 앉아 있습니다. 그 차는 비행기 안에 있습니다. 비행기가 공항에 있습니다. 운전자가 자동차 외부의 물건에 접근 할 수 있지만 비행기 내부가 공항을 떠나더라도 비행기 내부에서는 접근 할 수 없습니다. 그게 다야. 27 세가되면 더 자세한 설명을보십시오 이나 아래 예를보십시오.

비행기 이야기를 코드로 변환하는 방법은 다음과 같습니다.

var plane = function(defaultAirport) {

  var lastAirportLeft = defaultAirport;

  var car = {
    driver: {
      startAccessPlaneInfo: function() {
        setInterval(function() {
          console.log("Last airport was " + lastAirportLeft);
        }, 2000);
      }
    }
  };
  car.driver.startAccessPlaneInfo();

  return {
    leaveTheAirport: function(airPortName) {
      lastAirportLeft = airPortName;
    }
  }
}("Boryspil International Airport");

plane.leaveTheAirport("John F. Kennedy");