자바스크립트 실행 문맥과 클로저

dahyeon·2022년 12월 30일
1

자바스크립트

목록 보기
3/7
post-thumbnail

본 포스팅은 책 <모던 자바스크립트 입문>의 내용을 바탕으로 작성되었으며, 내용 보충이 필요한 부분들은 참고자료에 있는 문서들을 참고해서 작성하였음을 알려드립니다.


실행 문맥

자바스크립트 엔진은 실행 가능한 코드를 만나면 그 코드를 평가해서 실행 문맥으로 만든다.

실행 가능한 코드는 다음과 같다.

  • 전역 코드
  • 함수 코드
  • eval 코드: eval 코드는 렉시컬 환경이 아닌 별도의 동적 환경에서 실행됨.

실행 문맥의 구성

💡 실행 문맥이란?
실행 가능한 코드가 실제로 실행되고 관리되는 영역. 실행에 필요한 모든 정보를 컴포넌트 여러 개가 나누어 관리하도록 만들어져 있음.

✔️ 컴포넌트의 종류

🔶 렉시컬 환경(LexicalEnvironment) 컴포넌트

자바스크립트 코드를 실행하기 위한 자원을 모아 둔 곳. 함수 또는 블록의 유효 범위 안에 있는 식별자와 그 결괏값이 저장되는 곳.

🤔 식별자(identifier)란? 변수/함수의 이름을 뜻함.

렉시컬 환경 컴포넌트의 구성요소

  • 환경 레코드: 유효 범위 안에 포함된 식별자를 기록하고 실행하는 영역
    • 선언적 환경 레코드(Declarative Environment Record) 함수와 변수, catch 문의 식별자와 실행 결과가 저장되는 영역. 식별자와 그 실행 결과를 키와 값의 쌍으로 관리함.
    • 객체 환경 레코드(Object Environment Record) 실행 문맥 외부에 별도로 저장된 객체의 참조를 가져와서 데이터를 읽거나 씀.
  • 외부 렉시컬 환경 참조: 함수를 둘러싸고 있는 코드가 속한 렉시컬 환경 컴포넌트의 참조가 저장되는 곳. 중첩된 함수 안에서 바깥 코드에 정의된 변수를 읽거나 써야할 때, 외부 렉시컬 환경 참조를 따라 한 단계씩 렉시컬 환경을 거슬러 올라가서 변수를 검색함. JS는 dynamic scrope가 아닌 lexical scope를 따르므로, 외부 환경 참조 값은 함수가 호출된 위치가 아닌, 선언된 위치에 따라 결정된다.

🔶 디스 바인딩(This Binding) 컴포넌트

그 함수를 호출한 객체의 참조가 저장되는 곳. 이것이 가리키는 값이 곧 해당 실행 문맥의 this가 됨.

❗ 컴포넌트의 종류에는 변수 환경(VariableEnvironment) 컴포넌트라는 컴포넌트가 더 있다. 하지만 <모던 자바스크립트 입문>에서 다음과 같은 이유로 렉시컬 환경 컴포넌트로 통일해서 설명하고 있기 때문에 본문에서도 구분하지 않았다.

렉시컬 환경 컴포넌트와 변수 환경 컴포넌트는 앞으로 설명할 렉시컬 환경 타입의 컴포넌트입니다. 두 컴포넌트는 타입이 같고 실제로 with 문을 사용할 때를 제외하면 내부 값이 같으므로 똑같이 취급해도 무리가 없습니다.

🤔 환경 레코드의 선언적 환경 레코드 vs 객체 환경 레코드

  • 선언적 환경 레코드 함수와 변수, catch 문의 식별자와 실행 결과가 저장되는 영역. 아래 예시에서 a, b, c는 모두 선언적 환경 레코드의 바인딩이다.
    function foo(a) {
      var b = 10;
      function c() {}
    }
    아래 코드의 catch 문에서, e가 선언적 환경 레코드의 바인딩이 된다.
    try {
      ...
    } catch (e) { 
      ...
    }
  • 객체 환경 레코드 전역 환경 또는 with 문 안의 변수와 함수를 바인딩하는 곳.

전역 환경과 전역 객체의 생성

자바스크립트 인터프리터는 시작하자마자

  • 렉시컬 환경 타입의 전역 환경(global environment)을 생성
    • 전역 환경의 외부 렉시컬 환경 참조는 null
  • 전역 객체(Window)를 생성
  • 전역 환경의 객체 환경 레코드의 bindObject 프로퍼티에 전역 객체의 참조를 대입

프로그램의 평가와 전역 변수

자바스크립트 엔진은 전역 코드를 평가할 때

  • 최상위 레벨에 선언된 변수와 함수를 전역 환경의 객체 환경 레코드에 기록
    • 전역 변수의 실체는 전역 객체의 프로퍼티 또는 전역 객체의 실행 문맥에 들어있는 환경 레코드(객체 환경 레코드)의 프로퍼티
      ✅ 호이스팅의 원리: 최상위 레벨에 선언된 함수와 변수는 프로그램을 평가하는 시점에 환경 레코드에 추가되므로 코드의 어느 위치에 작성해도 전체 프로그램이 참조할 수 있는 것.
  • 함수 안에 선언된 지역 변수와 중첩 함수의 참조는 그 함수가 속한 실행 환경의 환경 레코드(선언적 환경 레코드)의 프로퍼티

프로그램 실행과 실행 문맥

프로그램이 평가된 다음에는 프로그램이 실행되며, 프로그램은 실행 문맥 안에서 실행된다.

  • 실행 문맥은 스택이라는 구조로 관리
  • 실행 문맥은 프로그램 실행 중에 스택에 push되어 실행
    • 전역 코드가 가장 먼저 실행됨
  • 함수를 호출하면 현재 실행 중인 코드의 작업을 일시적으로 멈추고 실행 문맥 영역을 생성
    • 실행하는 함수가 특정 함수 내부에 정의된 중첩 함수라면, 중첩 함수의 실행 문맥을 새로 만들어서 스택에 push
    • 함수의 실행 문맥, 렉시컬 환경, 환경 레코드가 생성되면 실행 문맥에 있는 디스 바인딩 컴포넌트에 그 함수를 호출한 객체의 참조를 저장.
      this는 동적이며 함수를 호출하는 상황에 따라 가리키는 객체가 바뀜.
  • 함수가 종료되어 제어권이 호출한 코드로 돌아가면 실행 문맥과 함께 렉시컬 환경 컴포넌트가 메모리에서 지워짐. 단, 함수의 참조가 환경 레코드에 있는 경우에는 지워지지 않음.

식별자 결정

자바스크립트는 식별자를 결정할 때 좀 더 안쪽 코드에 선언된 변수를 사용한다.

  • 함수의 환경 레코드(선언적 환경 레코드)에 있는지 찾고
  • 없다면 외부 렉시컬 환경 참조를 따라 환경 레코드 탐색

클로저

클로저란?

클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합이다.

  • 클로저는 함수가 생성될 때 같이 생성된다.

렉시컬 스코핑(Lexical Scoping)이란?

렉시컬 스코핑이란 함수가 중첩된 상황에서 파서가 변수 이름을 결정하는 스코핑 방식으로, 이 스코프는 함수를 호출할 때가 아니라 함수를 어디에 선언하였는지에 따라 결정된다.

다음의 예시에서,

function init() {
  var name = 'Mozilla'; // name is a local variable created by init
  function displayName() {
    // displayName() is the inner function, a closure
    console.log(name); // use variable declared in the parent function
  }
  displayName();
}
init();
  • displayName 함수 내부에는 name 변수가 없다.
  • 하지만 displayName 함수를 둘러싸고 있는 외부 함수 initname이 정의되어 있다.
  • 이렇듯 displayName 함수 내부의 변수 name의 값을 결정할 때 우선 해당 함수 내부에 정의되어 있는지 탐색하고, 없을 경우 외부 렉시컬 환경 참조를 따라가면서 결정된다.
  • nameinit 함수 내부에서만 접근 가능하며, 내부에 중첩된 함수(displayName)에서도 접근 가능하다.

클로저 활용하기

클로저를 잘 활용하면 현재 상태를 기억하고 변경된 최신 상태를 유지할 수 있다.

예를 들어, 카운터를 구현한다고 생각해보자.

특정 버튼을 클릭할 때 카운터 값이 하나씩 증가하게 하고 싶다면, 어딘가에서 현재 카운터 값을 기억하고 있어야 한다. 전역변수를 사용하는 방법도 있겠지만, 의도치 않게 값이 변경될 수 있다는 위험이 있다. 이럴 때 다음과 같이 클로저를 사용하여 counter 변수를 기억할 수 있다.

const increase = (() => {
      // 카운트 상태를 유지하기 위한 자유 변수
      let counter = 0;
      // 클로저를 반환
      return function () {
        return ++counter;
      };
    }()
);

변수 뿐만 아니라 함수 또한 클로저를 활용해서 private하게 만들 수 있다.

클로저는 ‘캡슐화된 객체’라고 할 수 있다.

다음과 같은 코드에서 출력값은 어떻게 될까?

function makeCounter(){
	var count = 0;
	return function() {
		return count++;
	}
}
	
var counter1 = makeCounter();
var counter2 = makeCounter();
console.log(counter1());
console.log(counter2());
console.log(counter1());

정답은 0, 0, 1이다. 그 이유는 makeCounter()를 호출할 때마다 makeCounter렉시컬 환경 컴포넌트가 새로 생성되기 때문이다. 따라서 각 클로저는 서로 다른 내부 상태를 저장한다.


참고 자료

<모던 자바스크립트 입문> 이소 히로시 지음
[JS]Execution Context와 Call Stack - 어? 쓰흡... 하아.... | Dev X
ECMA-262-5 in detail. Chapter 3.2. Lexical environments: ECMAScript implementation.
Closures - JavaScript | MDN
Closure | PoiemaWeb

profile
https://github.com/dahyeon405

0개의 댓글