자바스크립트 딥다이브 - 클로저

ChoiYongHyeun·2023년 12월 16일
0

모던 자바스크립트 딥다이브 교재로 이해하고

MDN 까지 한 번 슈우우욱 훑고나서 정리하는 내용이다.

이전 파트의 실행 컨텍스트와 렉시컬 환경에 대해서 잘 이해했는지 , 어려운 개념인 것 같음에도 불구하고

이해가 좀 빠륵게 되었다.

클로저 살펴보기

클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.

MDN 에서 클로저에 대해 정의한 내용이다.

어떤 식으로 작동하는건지 코드를 통해 먼저 살펴보자

function MakeCounter() {
  let num = 0;

  return {
    increase() {
      num += 1;
    },
    decrease() {
      num -= 1;
    },
    value() {
      return num;
    },
  };
}

const counter = MakeCounter();
console.log(counter.value()); // 0
counter.increase(); 
counter.increase();
console.log(counter.value()); // 2
counter.decrease();
console.log(counter.value()); // 1
console.log(counter.num); // undefined
console.log(num); // ReferenceError: num is not defined

생긴 코드를 살펴보면 MakeCounter 라는 함수 안에 num 이란 지역 변수를 만들어 두고 {increase, decrease, value} 함수들이 담긴 객체를 반환한다.

해당 객체를 counter 라는 식별자에 담아 메소드인 increase , decrease 들을 몇 번 실행하고 value를 통해 값을 확인해보니 값이 사용한 메소드에 맞춰 num값이 바뀐 것을 볼 수 있다.

그럼 num 값이 어디에 생겼나 ? 보려고 하니

counter 객체에도 존재하지않고 전역에도 존재하지 않고 있다.

이처럼 클로저 를 이용하면 외부에서 참조 할 수 없는 프로퍼티를 다룰 수 있는 함수를 말한다.

이처럼 클로저는 모듈 패턴과 깊은 연관이 있다.

모듈 패턴

모듈 패턴(Module Pattern)은 소프트웨어 디자인 패턴 중 하나로, JavaScript에서 코드를 모듈화하고 캡슐화하기 위한 방법을 제공합니다. 이 패턴은 코드의 유지보수성을 향상시키고 전역 스코프의 오염을 방지하는 데 도움이 됩니다.

모듈 패턴의 주요 특징은 다음과 같습니다:

  • 캡슐화 (Encapsulation): 모듈 패턴은 변수와 함수 등을 하나의 단일 단위로 묶어 캡슐화합니다. 이로써 모듈 내부의 세부 사항을 감추고 외부에서 직접 접근할 수 없도록 보호할 수 있습니다.

  • 네임스페이스 (Namespace): 모듈 패턴은 전역 네임스페이스를 사용하여 변수나 함수의 충돌을 방지합니다. 모듈 내부에서 선언된 변수와 함수는 해당 모듈의 스코프 내에서만 유효하므로 다른 모듈과 충돌할 위험이 줄어듭니다.

  • 재사용성 (Reusability): 모듈은 독립적으로 개발되고 테스트되기 때문에 재사용성이 높아집니다. 다른 프로젝트에서 필요한 모듈을 가져와 사용하거나, 현재 프로젝트에서 여러 곳에서 동일한 모듈을 사용할 수 있습니다.

유지보수성 (Maintainability): 코드를 모듈 단위로 나누면 각 모듈을 독립적으로 관리할 수 있어 유지보수가 용이해집니다. 특정 모듈만 업데이트하거나 수정할 수 있어 전체 코드베이스에 영향을 덜 주게 됩니다.

함수의 렉시컬 환경

모든 소스 코드는 소스 코드 별로 실행 컨텍스트 가 존재하며 실행 컨텍스트렉시컬 환경 에 맞춰 관리 된다고 하였다.

전역 소스 코드는 전역 렉시컬 환경을 생성하고 함수 소스 코드는 함수 렉시컬 환경을 생성한다.

이 때 각 실행 컨텍스트는 선언되는 상황에 맞춰 실행 컨텍스트와 렉시컬 환경이 생성되고

선언된 컨텍스트로부터 상위 컨텍스트를 연결하는 스코프 체인 으로 이뤄져있다고 했다.

렉시컬 환경

변수와 함수의 식별자를 기억하고 관리하는 역할을 한다.
각 실행 컨텍스트는 자신만의 렉시컬 환경을 가지며, 이는 해당 컨텍스트의 식별자와 메소드들에 대한 정보를 담고 있다.

스코프 체인

스코프 체인은 각 렉시컬 환경에서 상위 렉시컬 환경으로 연결된 것을 말한다. 이는 변수나 함수를 찾을 때 사용되는 메커니즘이며, 현재 렉시컬 환경에서 찾지 못한 경우 상위 렉시컬 환경으로 이동하여 검색을 계속한다.

간단히 코드를 통해 이해해보자

const x = 1; // 전역 렉시컬 환경에 존재하는 식별자

function foo() {
  const x = 10; // foo 렉시컬 환경에 존재하는 식별자
  function bar() {
    console.log(x); 
  }

  bar();
}

foo(); // 10
console.log(x); // 1

foo 내부에서 선언된 barbar의 렉시컬 환경 을 가진다. 이 때 bar 내부에는 x 라는 프로퍼티가 없기 때문에 선언된 위치의 상위 렉시컬 환경인 foo의 렉시컬 환경 으로 이동하여 프로퍼티 x : 10 를 찾아 console.log 한다.

전역에서 x 를 찾으려 하면 전역 렉시컬 환경 에 존재하는 프로퍼티 x : 1 을 찾아 로그한다.

이처럼 함수 들은 선언 될 때 본인만의 렉시컬 환경 을 가지며, 선언된 위치를 본인의 상위 렉시컬 환경 으로서 스코프 체인을 연결한다.

[[Environment]]

객체들은 다양한 내부 프로퍼티와 내부 메소드를 갖는다고 하였다.

함수들도 일급 객체이기 때문에 내부 프로퍼티, 내부 메소드를 갖는데

상위 렉시컬 환경에 대한 내용을 [[Environment]]에 담는다.

즉 위 예시에서 전역 공간에서 선언된 foo 함수의 [[Environment]] 는 전역 객체인 window or global 이며 foo의 렉시컬 공간 에서 선언된 bar 함수의 [[Environemt]]foo의 렉시컬 공간 이다.

이전에 함수의 렉시컬 환경에 존재하는 객체인 FunctionEnvironment 에는 함수의 매개 변수 및 argument , 지역 변수 들이 선언된다고 하였고 OuterLexicalEnvironmentRefrence 에는 상위 렉시컬 환경에 존재하는 객체들 (상위 객체의 FunctionEnvironemtn , OuterLexicalEnvironmentRefeence) 등이 담긴다고 하였다.

결국 OuterLexicalEnvironmentRefrence 은 내부 프로퍼티인 [[Environment]] 가 가리키고 있는 렉시컬 환경의 객체들을 참조한다.

챗 지피티는 OuterLexicalEnvironmentRefeence 라는 객체명이 자바스크립트 명세서에서 정확하게 쓰는 용어가 아니라고 한다.
내가 공부하고 있는 모던 자바스크립트 딥다이브 교재에선느 해당 객체로 설명을 이어나가기 때문에 나는 사용하도록 하겠다.
중요한 점은 함수는 본인이 선언된 위치를 상위 렉시컬 환경으로 [[Environment]] 에 지정하고 본인만의 렉시컬 환경도 구성한다는 점이다.

클로저와 렉시컬 환경

function outer() {
  let num = 0;

  function increaseInner() {
    num += 1;
    console.log(num);
  }

  return increaseInner;
}

const increaser = outer();
console.log(increaser);
increaser(); // 1
increaser(); // 2
increaser(); // 3

해당 코드를 살펴보면 outer 라는 함수 안에 increaseInner 가 선언되어 있고 선언된 해당 함수가 반환된다.

그렇다면 outerincreaseInner[[Environment]] (상위 렉시컬 환경) 는 무엇을 가리키고 있을까 ?

Function [[Environment]]
outer Global Lexical Environment
increaseInner outer Lexical Environment

반환되는 increasInner 함수는 outer 을 상위 렉시컬 환경으로 가리키고 있는 함수 객체이다.

const increaser = outer() 으로 increaseInner를 할당하는 것인데

할당 이후 outer 함수의 생명 주기는 끝남에도 불구하고 outer 내부에서 선언된 increaseInner 함수의 생명주기는 끝나지 않고 사용 가능하다.

또한 outer 의 생명주기가 끝났음에도 불구하고 increaseInner 함수는 outer 내부에서 정의된 num 에 대해서 접근이 가능하다.

outer 의 생명주기가 끝났음에도 불구하고 increaseInner 함수가 상위 렉시컬 환경으로 outer 를 가리키고 있기 때문에 outer 내에 존재하는 지역 변수에 접근 가능하다.

이처럼 중첩함수(increaseInner)가 외부함수(outer) 보다 생명 주기가 길고, 외부 함수의 생명주기가 종료되었음에도 불구하고 참조 가능한 함수를 클로저 라고 한다.

이제 위에서 설명한

클로저는 함수와 그 함수가 선언한 렉시컬 환경과의 조합이다.

라는 문구가 좀 이해가 된다.

그러면 클로저 함수 왜 사용해야 할까 ?

클로저를 사용해야 하는 이유

비공개 데이터 보호

let num = 0;

function Counter() {
  return {
    increase() {
      num += 1;
    },
    decrease() {
      num -= 1;
    },
    value() {
      console.log(num);
    },
  };
}

const counter = Counter();

counter.increase();
counter.increase();
counter.value(); // 2

num = 9999;

counter.value(); // 9999

만약 전역 변수에 존재하는 num의 값을 참조하여 변경하는 Counter 함수가 존재 할 때

전역 변수인 num 은 값의 재할당이 일어날 수 있다.

하지만 클로저를 사용하면 외부에서 직접 접근하지 못하게 하고, 변수의 상태를 함수 내부에서만 조작, 외부에서는 읽기 전용으로 가능하게 할 수가 있다.

function Counter() {
  let num = 0;

  return {
    increase() {
      num += 1;
    },
    decrease() {
      num -= 1;
    },
    value() {
      console.log(num);
    },
  };
}

const counter = Counter();

counter.increase();
counter.increase();
counter.value(); // 2
console.log(num); // ReferenceError: num is not defined

함수 팩토리

클로저를 사용하면 함수를 동적으로 생성하는 함수 팩토리를 만들 수 있다.

이를 통해 비슷한 기능을 하는 함수를 여러 개 만들 수 있고, 각 함수는 자신만의 렉시컬 스코프를 가지게 된다.

function MakeCounter(aux) {
  let num = 0;

  return function () {
    num = aux(num);
    return num;
  };
}

function increas(x) {
  x += 1;
  return x;
}

function decrease(x) {
  x -= 1;
  return x;
}

const increaser = MakeCounter(increas);
const decreaser = MakeCounter(decrease);

console.log(increaser()); // 1
console.log(increaser()); // 2

console.log(decreaser()); // -1
console.log(decreaser()); // -2

자바스크립트에서 함수는 일급 객체이기 때문에 다른 함수의 인수로 넣을 수 있다고 했다.

그렇기 때문에

function MakeCounter(aux) {
  let num = 0;

  return function () {
    num = aux(num);
    return num;
  };
}

해당 함수에서 aux 인수로 함수를 받고, MakeCounter 렉시컬 환경 내 지역 변수인 num 의 값을 aux로 값을 계속 변환시킨 후 변환 시킨 num 을 반환하는 함수를 만들었다.

그렇다면 MakeCounter(increase)의 경우 반환되는 함수 객체는 다음처럼 생겼다.

function () {
    num = increase(num);
    return num;
  }

해당 함수를 실행 시킬 때 마다 num 값을 1증가 시킨후 반환하게 되는데 이 때 가져오는 num 값은 선언 할 때 설정한 MakeCounter(aux) 내부에 존재하는 let num =0 을 가져오게 된다.

값이 변환되면 MakeCounter(aux) 내부의 num 값도 1씩 증가하게 될 것이다.

여기서 포인트는 MakeCounter(increase)MakeCounter(decrease) 의 렉시컬 스코프가 독립적으로 작동한다는 것인데

이는 [[Environment]] 에 들어가는 상위 렉시컬 환경은 할당 때마다 생성되기 때문이다.

const increaser = MakeCounter(increas); // increaser 가 참조하고 있는 상위 렉시컬 환경 1 생성
const decreaser = MakeCounter(decrease); // decreaser 가 참조하고 있는 상위 렉시컬 환경 2 생성

좀 더 자세히 살펴보자

1. 렉시컬 스코핑

함수가 정의 될 때 함수 내부에서 사용하는 변수들은 함수가 정의된 스코프에 대한 참조를 갖는다.
함수가 실행 될 때가 아닌, 정의 될 때의 스코프를 기억한다.

function MakeCounter(aux) {
  let num = 0;

  return function () {
    num = aux(num);
    return num;
  };
}

MakeCounter(aux) 내에서 정의된 let num = 0;MakeCounter 함수 내부 ~ 하위 객체에서만 접근이 가능하다.

2. 클로저의 생성

클로저는 함수와 함수가 정의된 렉시컬 환경의 조합이다.
함수가 다른 함수 내부에서 정의되면 내부 함수는 외부 함수의 렉시컬 환경을 기억하며 , 외부 함수의 렉시컬 환경을 상위 렉시컬 환경 이라고 한다.

function MakeCounter(aux) {
  let num = 0;

  return function () {
    num = aux(num);
    return num;
  };
}

에서 return function() {...} 에 정의된 함수는 상위 렉시컬 환경으로 MakeCounter(aux) 를 가지며, MakeCounter(aux) 에서 정의된 num 에 접근 가능하다.

3. 함수 팩토리의 동작

const increaser = MakeCounter(increas); // increaser 가 참조하고 있는 상위 렉시컬 환경 1 생성
const decreaser = MakeCounter(decrease); // decreaser 가 참조하고 있는 상위 렉시컬 환경 2 생성

에서 매 호출마다 새로운 렉시컬 환경을 생성한다.

렉시컬 스코프 환경을 정의하는 것과 렉시컬 환경을 생성하는 것은 다르다.
increaser가 따르고 있는 렉시컬 환경을 decreaer 나 전역에서 조회하는 것은 불가능하다.

각 함수가 동작 할 때 마다 ex : increaser() increaser는 선언 때 생성된 렉시컬 환경 내에서 동작한다.

4. 독립적인 상태 유지

서로 자신만의 렉시컬 환경을 가지고 있기 때문에 상태 변화가 독립적으로 이뤄진다.

렉시컬 환경은 독립적이지만 렉시컬 환경의 스코프 체인의 형태는 동일하다.

비동기 작업 처리

클로저를 사용하면 비동기 작업에서 현재 상태를 유지하면서 이후에도 해당 상태를 이용할 수 있다.

이는 주로 콜백 함수에서 상태를 유지하고 활용하는데 유용하다.

function asyncOperation() {
  let result;

  setTimeout(function () { // 클로저
    result = "Operation completed";
    console.log(result);
  }, 1000);

  console.log("Operation started");
}

asyncOperation();

해당 비동기 작업을 통해 result 변수가 1초 후에 작업이 완료되었다고 외부 값을 변경해주는 코드이다.

중첩 함수로 클로저를 사용하였으며, 클로저를 이용하여 외부 렉시컬 환경의 변수의 최신 상태를 유지하고 활용 할 수 있다.

이처럼 클로저는 상태가 의도치않게 변경되지 않도록 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용한다.

독립적인 렉시컬 환경을 갖지 않기 위해서

위 파트에서 렉시컬 환경의 구조는 동일해도, 생성 될 때 마다 독립적인 렉시컬 환경을 갖는다고 하였다.

그렇다면 독립적이지 않고 서로 참조 가능하게 하려면 어떻게 해야 할까 ?

위 카운터 예시에서 increase, decrease 함수가 같은 렉시컬 환경에서 num을 참조하게 하고 싶다.

그러면 렉시컬 환경을 하나만 만들면 된다.

function MakeCounter() {
  let num = 0;

  return {
    increase() {
      num += 1;
    },
    decrease() {
      num -= 1;
    },
    value() {
      console.log(num);
    },
  };
}

const counter = new MakeCounter();
counter.increase();
counter.increase();
counter.value(); // 2
counter.decrease();
counter.value(); // 1

구우웃 ~

new 를 이용하여 새로운 객체라고 명확히 명시해주는 이유는 가독성 뿐이 아니라 this 를 명확히 바인딩 하기 위함도 있다.

캡슐화와 정보 은닉

캡슐화 는 프로퍼티나 메소드등을 하나의 객체로 묶는 것을 의미하고

정보 은닉 은 특정 프로퍼티나 메소드를 감추기 위해 캡슐화를 이용하는 것을 말한다.

대부분의 객체 지향 프로그래밍 언어에서는 클래스를 정의하고 그 안에서 public , private , protected 등의 접근제한자를 이용하여 공개 범위를 한정 할 수 있지만

자바스크립트에는 해당 기능이 없다.

하지만 클로저를 이용하여 비슷하게 구현 할 수는 있다.

자바스크립트로 정보 은닉 구현하기

function Person(name, age) {
  this.name = name; // 정보 공개
  const _age = age;

  this.introduce = function introduce() {
    console.log(`hi i am ${this.name} and ${_age} years old`);
  };
}

let tom = new Person('tom', 20);

tom.introduce(); // hi i am tom and 20 years old
console.log(tom.name); // tom
console.log(tom.age); // undefined
console.log(tom._age); // undefined

Person 함수 내부에 _age 라는 지역 변수에 인수 age 를 집어 넣어줬다.

이후 Person 내에 정의된 메소드인 introduce 는 상위 렉시컬 스코프로 Person에 접근 할 수 있기 때문에 _age 에는 접근 가능하기에 해당 메소드는 실행 가능하다.

하지만 _ageagetom 의 프로퍼티가 아닐 뿐더러 new Person 이 아닌 Person 에서만 접근 가능한 프로퍼티이다.

그럼 this.introduce 를 프로토타입으로 돌릴 수 있을까 ?

function Person(name, age) {
  this.name = name; // 정보 공개
  const _age = age;
}

Person.prototype.introduce = function introduce() {
  console.log(`hi i am ${this.name} and ${_age} years old`);
};

let tom = new Person('tom', 20);

tom.introduce(); // ReferenceError: _age is not defined

참조 에러가 발생한다.

tom.introduce 는 사실 tom.prototype.introduce 에 해당하는데 결국 Person._age 에 접근하려고 하는 것이다.

이 때 _agePerson 안에서 선언된 지역 변수일 뿐, Person의 프로퍼티가 아니라 참조 에러가 발생한다.

즉시 실행 함수 이용해서 정보 은닉 해보기

나는 곧죽어도 프로퍼티로 넣고 싶어 어떻게 해

ㅋㅋ 즉시 실행 함수 쓰세요

const MakePerson = (function IIFE() {
  let _age = 0;

  function Person(name, age) {
    this.name = name;
    _age = age;
  }

  Person.prototype.introduce = function introduce() {
    console.log(`hi i am ${this.name} and ${_age} years old`);
  };

  return Person;
})();

let tom = new MakePerson('tom', 20);
tom.introduce(); // hi i am tom and 20 years old
let jerry = new MakePerson('jerry', 40);
jerry.introduce(); // hi i am jerry and 40 years old

으아악 이 코드 이해하는데 한시간이나 걸렸다.

우선 해당 즉시 실행 함수를 이용하면 왜 prototype 이 가능해지는걸까 ?

(function IIFE() {...})() 부분을 통째로 이해해보자

해당 함수가 실행되면 즉시 실행 함수 내부에 _age 라는 지역 변수가 우선 생성된다.

그리고 함수 안에서 Person 함수가 name , age 라는 매개변수를 받아 namePerson 으로 인해 생성될 객체의 name 프로퍼티로 설정된다.

age 값은 IIFE() 함수 내부에 존재하는 _age 의 값을 변경 시킨다.

이후 Person.prototype.introduce 를 설정해주는데 이 때 this.name 을 통해 이름은 찾을 수 있지만 _agePerson 으로 인해 만들어진 객체 내부에서는 찾을 수 없다.

하지만 PersonIIFE를 상위 렉시컬 환경으로 가리키고 있기 때문에 _age 를 찾아 프로토타입으로 설정해줄 수 있다.

이전에는 안됐던게 되는 이유

function Person(name, age) {
  this.name = name; // 정보 공개
  const _age = age;
}
Person.prototype.introduce = function introduce() {
  console.log(`hi i am ${this.name} and ${_age} years old`);
};
let tom = new Person('tom', 20);
tom.introduce(); // ReferenceError: _age is not defined

이전에 했던 것에선 Person_age 를 프로퍼티로 설정해주지 않고 지역변수로만 설정하여 은닉해줬다.
하지만 지역 변수로만 설정해주었기 때문에 Person 이 생성하는 객체에서 찾을 수 없었다.
하지만 IIFE 로 한 번 더 검싸주어 상위 렉시컬 환경을 생성해준 후 상위 렉시컬 환경에서 _age 를 지역 변수로 사용하기 때문에 찾을 수 있었다.

그리고 Person 함수를 반환한다.

Person 함수는 IIFE 함수 내부에 존재하는 클로저 함수인 것이다.

오케이 ~~ const MakePerson = (function IIFE() {...} 까지는 이해했다.

그럼 실행은 어떻게 되는지 보자

let tom = new MakePerson('tom', 20);
tom.introduce(); // hi i am tom and 20 years old

이 부분에서 new MakePerson('tom', 20)은 사실 new Person('tom', 20) 과 같다.

이 때의 personIIFE() 를 상위 렉시컬로 가리키고 있는 Person이다.

new Person('tom', 20) 이 실행되면서

function Person(name, age) {
    this.name = name;
    _age = age;
  }

  Person.prototype.introduce = function introduce() {
    console.log(`hi i am ${this.name} and ${_age} years old`);
  };

이런 프로퍼티를 갖고 있는 객체를 tom 에 할당해주었는데

할당해주는 동안 Person 함수 내부의 _age = age 가 작동되며 IIFE 에 존재하는 _age 의 값도 들어간 인수인 20 으로 변경된다.

그 다음 !

let jerry = new MakePerson('jerry', 40);
jerry.introduce(); // hi i am jerry and 40 years old

를 실행하면 let jerry = new MakePerson('jerry', 40); 이부분 역시

let jerry = new Person('jerry', 40); 과 같으며 현재 호출된 Person 함수 역시 이전의 IIFE 를 상위 렉시컬 환경으로 가지는 클로저이다.

이 때 IIFE_age 값은 0이 아닌 이전에 설정한 20이다.

그 이유는 클로저의 외부 함수는 실행 될 때 마다 독립적인 렉시컬 환경을 생성하지만 즉시 실행 함수는 단 한번만 실행 되었기 때문에 같은 렉시컬 환경을 공유 한다.

이것 또한 Person 함수 내부의 _age = age 가 작동되며 IIFE 내부에 존재하는 _age 의 값이 이전에 설정한 20 에서 40 으로 변경된다.

값이 변경되며 jerryintroduce 는 잘 설정되었지만 이후에 tom 을 설정하면

tom.introduce(); // hi i am tom and 40 years old

tom 이 참조하고 있는 IIFE_age 값이 재할당되었기 때문에 값이 변경된다.

정리

위에서 즉시 실행 함수 를 이용하여 캡슐화와 은닉을 시행하였지만 즉시 실행 함수는 단 한번만 사용되어 렉시컬 환경을 하나만 생성한다.
렉시컬 환경을 생성하여, 생성자 함수 외부에 지역 변수를 생성하여 은닉한 채로 프로토타입 설정이 가능하였다.
하지만 렉시컬 환경을 하나만 공유하고, 인스턴스들이 같은 렉시컬 환경을 공유하여 지역 변수의 값이 변경된다는 단점이 있었다. (바뀐 값을 다른 인스턴스들도 공유)

개빡치쥬 ?

그런데 최근 버전에선 private 필드 정의 제안 이 가능하다고 한다.
좀 더 진도 나가고 배워보자

너무 어려워서 지피티랑 더블체크 했다.

너만 믿는다

자주 발생하는 실수

var funcs = [];

for (var i = 0; i < 3; i += 1) {
  funcs[i] = function () {
    return i;
  };
}

for (var j = 0; j < funcs.length; j += 1) {
  console.log(funcs[j]());
} // ??

코드를 보기만 해도 불편해진다.

반복문에서 함수 레벨 스코프를 갖는 var 를 사용하다니

하지만 코드를 실행하면 0 , 1 , 2 가 나올 것만 같다.

하지만 이 출력값은 3 ,3 ,3 이 나온다.

왜 ?

var funcs = [];

for (var i = 0; i < 3; i += 1) {
  funcs[i] = function () {
    return i;
  };
}

가 실행 될 때 funcs 배열에 들어가는 것은 i 값이 차례로 0 , 1 , 2 인 원시값이 담길 것 같지만 그것이 아닌 식별자 i 가 들어간다.

이 때 식별자 i 는 전역 변수로서 반복문이 실행됨에 따라 값이 재할당 된다.

funcs[i] 안에 들어있는 변수들은 모두 재할당 된 ireturn하게 되어있기 때문에 각 배열은 모두 최종적으로 재할당 된 3 을 출력하게 된다.

클로저를 이용해 해결하는 방법

var funcs = [];

for (var i = 0; i < 3; i += 1) {
  funcs[i] = (function (id) {
    return function () {
      return id;
    };
  })(i);
}

console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2

즉시 실행 함수를 이용하여 넣어주었다.

i 값이 들어올 때 id 라는 매개변수를 받아 즉시 function(){return id} 값을 반환하는

즉시 실행 함수를 시행 시켜주었다.

funcs 배열에 들어간 함수들은 function(){return id} 인데 반복문이 실행 될 때 마다 즉시 실행 함수가 실행되어 funcs 에 들어간 함수들은 모두 동일하게 생긴 `function(){return id} 이지만 각자의 상위 렉시컬 환경이 다르다.

첫 번째 function(){return id}i 가 0일때 시행된 즉시 실행 함수를 렉시컬 환경으로 가리키고 두 번째는 i 가 1일 때 시행된 즉시 실행 함수를 렉시컬 환경으로 갖는다.

이처럼 전역 변수를 다루는 경우엔 렉시컬 환경을 다르게 만든 후 클로저를 이용하여 구할 수 있다.

더 쉽게 해결하는 방법

이런 문제를 해결하기 위해선 렉시컬 환경을 새롭게 구성 해야 한다.

방금은 즉시 실행 함수를 이용하여 골치아프게 해줬는데

반복문에서 렉시컬 환경을 새롭게 구성하는 방법은 매우 쉽다.

var funcs = [];

for (let i = 0; i < 3; i += 1) {
  funcs[i] = function () {
    return i;
  };
}
console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2

그건 바로 블록 레벨 스코프를 갖는 let 을 이용해주는 것이다.

반복문이 시행 될 때 마다 function() {return i} 가 가리키고 있는 상위 렉시컬 환경이 생성되기 때문에 쉽게 해결된다.

let 최고 키킥

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글