실행 문맥과 클로저, this

·2022년 1월 31일
2

JavaScript

목록 보기
2/4
post-thumbnail

머리말

“JS는 인터프리터를 쓰기 때문에 한 줄 한 줄 바로 실행한대.”
사실 JS는 '처음 보는 코드'는 실행 직전 컴파일한다.
그리고 인터프리터로 실행하며, 캐싱한 코드는 최적화 컴파일러가 실행한다.
이 부분을 더 알고 싶으면 다른 포스팅을 참고하자.

console.log(num);
const num = 5;

console.log를 실행할 때 자바스크립트 엔진은 변수 num에 대해 아무것도 모를까?

prints();

function prints() {
	console.log("이거 출력될까요?");
}
prints();

const prints = () => {
	console.log("이건 어떨까요?");
}

function은 실행되고 화살표 함수는 에러가 난다. 왜?

실행 문맥(Execution context)

우리는 일할 때 계획을 먼저 짜고, 작업을 하고, 중간중간 문서화도 하고, 작업을 마무리하면 퇴근한다.

실행 문맥은 비슷한 역할을 한다.
코드를 실행하기 전에 코드를 어떻게 쓸지 개요를 만들고, 실행하고, 마무리하면 사라진다. (원래는 변수 객체, 활성 객체, 스코프 체인, this 정보가 담긴다는 설명이 유명하다. ECMAScript 버전을 갱신하면서 지금은 표현이 조금 달라졌다.)

ECMAScript의 실행 문맥 소개.

An execution context is a specification device that is used to track the runtime evaluation of code by an ECMAScript implementation.
At any point in time, there is at most one execution context per agent that is actually executing code. This is known as the agent's running execution context.
The execution context stack is used to track execution contexts. The running execution context is always the top element of this stack.
실행 문맥은 평가한 코드를 따라갈 때 쓰는 특수한 장치입니다.
어느 시점에든, 코드를 실행하는 대행자(agent)는 실행 문맥을 가집니다.
작동중인 실행 문맥(running execution context)이라고도 부릅니다.
콜 스택은 실행 문맥을 따라가고, 작동중인 실행 문맥은 콜 스택 맨 위에 있습니다.

간단히 말하면 실행 문맥 콜 스택에서 GEC를 실행하다가 함수 코드를 만나면 FEC를 콜 스택에 올려서 실행하는 것을 말한다.

What's Agent?

An agent comprises a set of ECMAScript execution contexts, an execution context stack, a running execution context, an Agent Record, and an executing thread. Except for the executing thread, the constituents of an agent belong exclusively to that agent.
대행자(agent)는 실행 문맥 + 콜 스택 + 대행자 기록(필드 집합) + 실행 스레드입니다. 실행 스레드만 제외하고 나머지 요소들은 대행자가 독점합니다.

  • 첨언 1
    agentproxy는 둘 다 대리인이라고 번역하는데, 차이가 있다.
    agent는 직접 행위를 하는 대행자, proxy는 둘 사이를 중개하는 중개인 개념으로 바라보면 된다.

  • 첨언 2
    예컨대 navigator 객체의 userAgent는 사용자를 대신해서 행사하는 프로그램이다. 브라우저의 userAgent는 브라우저다. 관련 오류로는 구글 로그인 실패 요인 중 하나인 403 disallowed_useragent가 있다.
    프록시 서버는 캐싱해서 클라이언트와 서버의 빠른 통신을 돕는데, 그야말로 중개다.

실행 문맥 종류

세 종류가 있지만 두 종류만 보는 편이다.

  • Global execution Context (GEC)
    코드 전체 문맥. JS실행할 때 생성.

  • Functional execution context (FEC)
    함수 문맥. 함수실행할 때 생성.

  • Eval execution context
    eval 함수를 실행할 때 생성.
    eval 함수는 파싱을 지연하는 용도로서 쓰곤 했는데 어려운 디버깅, 컴파일, 캐시 미지원 등의 이유로 이젠 사용이 권장되지 않으므로 생략한다. eval is evil 이란 관용어까지 있다.

실행 문맥 2단계

실행 문맥은 다음과 같이 두 단계를 거쳐서 만들어진다.

  1. 생성 단계(Creation phase)
    변수의 '선언'을 읽는다.

  2. 실행 단계(Execution phase)
    변수의 '값'을 읽는다.

생성 단계(creation phase)

생성 단계에서는 여러 가지 상태 컴포넌트(state component)가 만들어진다.
코드 평가, 함수, 렐름, ScriptOrModule 등이 있지만 일반적인 개념이 아니므로 넘기겠다.
유명한 개념만 알면 된다.

  • 어휘적 환경(Lexical Environment)
    • Environment Records (환경 기록)
      • Outer Environment Reference (외부 환경 참조)
  • 변수 환경(Variable Environment)

어휘적 환경은 환경 기록을 가진다.
기록(record)이란 컴퓨터 용어로 필드(field)의 집합을 말한다.

  • 여담
    어휘적 환경 등은 이론상 존재하는 객체다.
    이론대로 동작하지만, 실제로 직접 조작할 순 없다.

어휘적 환경과 환경 기록

주의 : 환경 기록, OuterEnv만 알아도 충분하다. 가볍게 보길 추천한다.

Environment Record is a specification type used to define the association of Identifiers to specific variables and functions, based upon the lexical nesting structure of ECMAScript code. Usually an Environment Record is associated with some specific syntactic structure of ECMAScript code such as a FunctionDeclaration, a BlockStatement, or a Catch clause of a TryStatement. Each time such code is evaluated, a new Environment Record is created to record the identifier bindings that are created by that code.
Every Environment Record has an [[OuterEnv]] field, which is either null or a reference to an outer Environment Record. This is used to model the logical nesting of Environment Record values. The outer reference of an (inner) Environment Record is a reference to the Environment Record that logically surrounds the inner Environment Record. An outer Environment Record may, of course, have its own outer Environment Record.
환경 기록이란 어휘가 중첩되는 구조(lexical nesting structure)임을 이용해 변수, 함수들의 식별자 연결을 정의합니다.
환경 기록은 특정한 구문 구조(함수 선언문, 블록 구문, Try의 Catch 절 등)와도 연관이 깊습니다. 이런 코드를 평가할 때 새 환경 기록을 만들며 해당 코드의 식별자 바인딩을 기록합니다.
모든 환경 기록에는 null, 혹은 외부 환경 기록을 참조하는 [[OuterEnv]] 필드가 있습니다. 환경 기록의 중첩을 모델링하고, 외부 환경들을 가리킵니다.

[[outerEnv]] 는 흔히 배우는 Scope chain 개념이다.

outerEnv는 해당 스코프 외부의 환경 기록들을 가리켜서 외부 식별자를 참조하게 돕는다.
환경 기록은 아래와 같이 세분화된다.

  • Environment Records
    • declarative Environment Record
      • Function Environment Records
      • module Environment Records
    • object Environment Record
    • global Environment Record

선언적 환경 기록과 ReferenceError

선언적 환경 기록에는 GetBindingValue라는 추상 메소드가 있다. (JavaScript가 내부적으로만 사용하는 메소드를 뜻함. 개발자는 사용 못한다.) 이 메소드는 환경 기록에 들어가는 식별자가 바인딩할 게 없거나, uninitialized를 바인딩하면 ReferenceErrorthrow한다.

function tester() {
  let testing = "outer";
  function innerFunc() {
    // 에러나는 코드
    let testing = testing;
    return testing;
  }
  return innerFunc;
}

const inner = tester();

innerFunc는 외부의 testing을 참조하지 않는다.
만약 JS 엔진이 '요령껏' 외부 식별자를 참조하면 에러가 나기 쉬울 것이다. 컴퓨터는 언어의 불확실한(ambiguous) 성질을 싫어한다. 이를 해결하기 위해 많은 규칙을 가지지만, 완벽하진 않다.

innerFunctesting은 선언문에서 자기 스스로를 참조한다. 자기 참조는 안티 패턴으로서 지양할 부분이다. 아무튼 초기화가 안 일어나기 때문에 ReferenceError가 난다.

function tester() {
  let testing = "outer";
  function innerFunc() {
    return testing;
  }
  return innerFunc;
}

const inner = tester();

이 코드는 정상 작동하는데, innerFunc의 환경 기록에는 testing 식별자가 없기 때문에 외부 환경 기록에 있는 testing을 참조하기 때문이다.

이렇게 외부 스코프로 타고 나가는 걸 scope chaining이라 부르고, OuterEnv의 리스트를 scope chain이라 부른다. scope chain은 개발자 도구에서 [[Scopes]] 필드로 확인할 수 있다. 이따 클로저 예시들에서 확인해보자.

this

W3C에서는 this를 다음과 같이 간결히 설명한다.

In JavaScript, the this keyword refers to an object.
Which object depends on how this is being invoked (used or called).
In an object method, this refers to the object.
Alone, this refers to the global object.
In a function, this refers to the global object.
In a function, in strict mode, this is undefined.
In an event, this refers to the element that received the event.
Methods like call(), apply(), and bind() can refer this to any object.
자바스크립트에서 this 지정어는 객체를 참조합니다.
누가 this를 호출하느냐에 따라 객체가 다르게 지정됩니다.
객체 내부 메소드에서 this는 그 객체를 참조합니다.
그냥 this를 쓰면 Global 객체를 참조합니다.
함수 안에서 this는 Global 객체를 참조합니다.
strict 모드에서 함수의 this는 undefined입니다.
이벤트에서 this는 이벤트를 받는 요소를 참조합니다.

call(), apply(), bind() 메소드는 this가 다른 객체를 참조하게 해줍니다.

applycallbind
인수배열쉼표로 구분객체
예시apply ( [a,b,c] )call ( a,b,c )bind ( obj )
특이사항this를 이 객체로 고정하겠다

간단한 예시 몇 가지를 들어보겠다.

const obj = {
  str: "this is obj",
  innerFunc() {
    console.log(this);
  },
};

const outerFunc = obj.innerFunc;

const nextObj = {
  str: "this is nextObj",
  finalFunc: outerFunc,
};

function test() {
  outerFunc();
}

function test2(callback) {
  callback();
}

obj.innerFunc(); // this === obj
outerFunc(); // this === Global
test(); // this === Global
test2(obj.innerFunc); // this === Global
test2(obj.innerFunc.bind(obj)); // this === obj
test2(obj.innerFunc.bind(nextObj)); // this === nextObj
nextObj.finalFunc(); // this === nextObj

  • obj.innerFunc()obj가 호출했기 때문에 this === obj
  • outerFunc()obj의 메소드를 가져오긴 했지만, 전역에서 호출하므로 this === Global
  • test() 내부에서 실행하는 outerFunc()는 호출하는 주체가 test 함수이다. 함수에서 thisGlobal.
  • test2() 내부에서 실행하는 obj.innerFunc도 어쨌든 test2() 함수에서 호출하므로 thisGlobal
  • bind 메소드는 this를 고정시키는 메소드다. test2()thisobj, nextObj로 바인딩해서 넘겨주면 바인딩된 객체 obj, nextObj를 출력한다.
  • nextObjfinalFuncinnerFunc를 복사한 것이지만 호출하는 주체는 nextObj이므로 this === nextObj.

Global로 지정되는 경우는 strict mode를 쓰면 undefined로 바뀐다.
별 쓸모없는 잡담이지만 strict mode를 안 쓰는 기본 모드는 sloppy mode(조잡한 모드)란 이름이다. (참고로 Class, module을 사용하면 자동으로 strict mode가 적용된다.)

이렇듯 thisbind만 쓰지 않으면 호출을 누가 하느냐에 따라 값이 동적으로 결정된다.
화살표 함수는 좀 다른데, ECMAScript에서는 this 관련으로 아래와 같이 설명한다.
(결론만 봐도 된다.)

A function Environment Record is a declarative Environment Record that is used to represent the top-level scope of a function and, if the function is not an ArrowFunction, provides a this binding. If a function is not an ArrowFunction function and references super, its function Environment Record also contains the state that is used to perform super method invocations from within the function.
함수 환경 기록은 선언 환경 기록의 하나로서 함수의 최상단(top-level) 스코프를 나타내는 용도로 쓰는 편입니다. 화살표 함수가 아니면 this 바인딩을 합니다.
화살표 함수가 아니고 super를 참조한다면 그 함수의 함수 환경 기록에는 super 메소드 호출을 수행하는데 쓸 상태들도 포함합니다.

  • 함수 환경 기록에는 [[thisValue]], [[ThisBindingStatus]] 필드 등이 있다.
  • 함수 환경 기록에는 BindThisValue( ) 메소드 등이 있다.
  • 화살표 함수는 [[ThisBindingStatus]] === lexical이다.
  • BindThisValue 및 여러 바인딩 메소드는 ThisBindingStatus === lexical이면 falsereturn하고 this를 따로 바인딩하지 않는다. 바인딩할 필요가 없다. 왜냐하면 아래 MDN의 설명 때문이다.

In arrow functions, this retains the value of the enclosing lexical context's this
화살표 함수의 this는 자신을 둘러싼 어휘 문맥의 this를 유지합니다.

결론

  • 리터럴 함수의 this는 함수를 호출하는 객체를 가리킨다.
  • 화살표 함수의 this는 자신이 정의된 스코프의 this를 가리킨다.

변수 환경과 호이스팅

변수 환경은 이름과 다르게 var 전용 공간이다. 이는 let, const, var의 변수 할당 과정이 다르기 때문이다.

In ECMAScript 2015, let and const are hoisted but not initialized
JavaScript Hoisting refers to the process whereby the interpreter appears to move the declaration of functions, variables or classes to the top of their scope, prior to execution of the code.
let과 const는 호이스팅됩니다. 초기화를 안 할 뿐.
호이스팅은 인터프리터가 코드를 실행하기 전에 함수, 변수, 클래스 등의 선언을 스코프에서 참조하는 작업입니다.(마치 그것들을 해당 스코프 맨 위에 끌어올린 것처럼).

JS에서 변수는 Declaration(선언) -> Initialization(초기화) -> Assignment(할당) 3단계를 거친다.

실행 문맥을 만들 때 var는 선언과 초기화를 동시에 해서 undefined이 바인딩된다.
let, const는 선언까지만 해서 uninitialized가 바인딩된다.

  • let, const는 어떤 차이가 있을까?
letconst
호이스팅 시 선언만 된다.호이스팅 시 선언만 된다.
초기화 따로, 할당 따로 가능초기화, 할당 동시에 해야 함
let num;
num = 2;

const str = "string";
  • 리터럴 함수는 선언, 초기화, 할당 3단계가 한꺼번에 된다.

  • 변수 값이 초기화되기 전의 구간을 TDZ(Temporal Dead Zone, 일시적 사각지대)라고 부른다. 호이스팅했을 때 letconst 등은 선언 단계이므로 TDZ에 속하지만 var는 초기화가 됐으므로 해당 사항이 없다.

어휘적 환경과 변수 환경은 변수 할당이 다르게 구분되지만 그 외에 돌아가는 원리 자체는 똑같다.

클로저

클로저를 말할 때 단골로 나오는 유명한 말이 하나 있다.

JS에서 함수는 1급 객체(first-class-object)이다.

1급 객체는 뭘까?

  • 인자로 넘길 수 있고,
  • 함수 return값으로 쓸 수 있고,
  • 변수로 할당할 수 있다.

2급 객체도 있을까? 당연히 있다.

  • 인자로 사용 가능
  • return 불가
  • 변수 할당 불가.

3급 객체도 있다.

  • 인자로 못 씀
  • return 불가
  • 변수 할당 불가

자바스크립트에서 함수는 1급 객체 조건을 다 만족한다. 그래서 클로저는 함수가 그 점을 이용한다. 함수를 만들 때 함수에서 사용하는 변수가 함수 외부에 있다면, 그 변수는 함수의 환경 기록에서 참조된다.

클로저의 가장 대표격인 예시는 함수가 내부 함수를 return하는 경우인데 MDN에서 소개하는 클로저는 다음과 같다.

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).
a closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.
클로저는 함수와 함수가 참조하는 어휘적 환경을 묶은 것입니다.
클로저를 쓰면 내부 함수는 외부 함수 스코프에 접근 가능합니다. 클로저는 함수를 만들 때마다 생성됩니다.

위의 말을 보면 알겠지만 일반적인 함수는 전부 클로저를 만든다. new Function을 제외하고.

  • 리터럴 함수 : 외부 렉시컬 스코프 참조, this가 동적
  • 화살표 함수 : 외부 렉시컬 스코프 참조, this가 정적
  • new Function : 외부 렉시컬 스코프 참조 불가능. 전역 렉시컬 스코프 참조.

new Functioneval과도 연관이 있는데 현재 포스팅 주제와 사뭇 다르니 넘기겠다.

function tester() {
  let testing = "outer";
  function innerFunc() {
    console.log(testing);
    let testing = "inner";
    return testing;
  }
  return innerFunc;
}

const inner = tester();
inner();

inner()를 실행하면 console에는 뭐가 찍힐까? 직관적으론 outer가 출력될 것 같다. 하지만 에러가 난다.

아래부터 나오는 예제 코드들은 예전에 작성했던 예시로 변수 객체, 활성 객체, scope chain, this로 이루어지는 실행 문맥을 의사 코드(pseudocode)로 작성한 것이다.
활성 객체(AO)는 FEC의 식별자 정보, 변수 객체(VO)는 GEC의 식별자 정보를 참조한다.

innerFunc FEC는 아래와 같이 생성됐을 것이다.

innerFuncExecutionObj = {
    scopeChain: [tester, Global],
    activationObject: {
      	testing: <uninitialized>,
    }
  	this: undefined
}

호이스팅은 변수들을 해당 스코프의 맨 위에 올려두고 참조하는 개념이다. innerFunc에서 testingtester에서 선언한 testing이 아니라 안쪽에서 선언한 testing을 참조하고 있다. 그래서 에러가 난다.

function tester() {
  let testing = "outer";
  function innerFunc() {
    console.log(testing);
    return testing;
  }
  return innerFunc;
}

const inner = tester();
inner();

innerFunclet testing 구문을 지우면 outer가 출력된다.

원래 FEC는 함수가 할 일을 다하고 나면 메모리를 회수하며 소멸한다. 다시 말해 inner에 값을 할당하면 tester()FEC와 변수들은 소멸해야 한다. 하지만 inner()를 실행하면 tester의 변수값을 확인할 수 있다. 클로저 때문이다.
클로저는 함수가 생성되는 순간의 함수 자신과 함수를 둘러싼 유효 환경의 합집합이다. inner는 클로저에서 tester의 어휘 환경을 참조하고, tester 스코프 내의 원소들을 사용할 수 있다.
이 때 tester 스코프 내의 원소들을 복사해서 사용하는 게 아니라 원본 그대로 쓰기 때문에 변경도 자유롭게 가능하다.

화면으로 이해해보자.

해당 코드를 라이브 서버로 실행하고 디버깅하는 모습.
Global은 전역 객체(Global Object)이고 ScriptHTML 파일이 Script 태그를 읽으면서 만든 것이다.


잠시만 전역 환경 기록과 전역 식별자를 짚고 넘어가자.

  • 전역 환경 기록
    • [[ObjectRecord]] : 전역 함수, var, 제너레이터를 보관 (Global)
    • [[DeclarativeRecord]] : let, const, class 등 보관 (Script)
    • [[GlobalThisValue]] : 전역 this

하지만 전역 환경 기록 내부가 여러 환경 기록으로 또 세부화된단 뜻은 아니다.

A global Environment Record is logically a single record but it is specified as a composite encapsulating an object Environment Record and a declarative Environment Record.
전역 환경 기록은 이론적으론 단일 기록이지만, 캡슐화 처리한 객체 환경 기록과 선언 환경 기록의 조합으로 명시합니다.

내부적으로 별도 처리를 한다고만 인식하자. 너무 깊게 파면 안 된다.

sub.js에선 main.jstest 함수를 별도로 import하지 않아도 쓸 수 있다.
그러므로 전역 공간을 사용하는 건 조심할 필요가 있다.

let myVar = "안녕하세요";
globalThis.myVar = "자바스크립트입니다.";

console.log(myVar);
console.log(globalThis.myVar);
// 이 코드를 실행하면 재밌는 출력이 나올 것이다.

이제 다시 클로저로 넘어와보자.

innerFunc 클로저가 tester에 있던 변수 testing : 'outer'을 참조하는 모습을 확인할 수 있다.

이처럼 함수 바깥에 있지만 사용 가능한 변수를 자유 변수(free variable)라고 한다.
자유 변수 반대는 묶인 변수(bound variable)라고 부른다.
예 : 모든 전역 변수 => 자유 변수 (필요 조건)

처음에 클로저가 1급 객체가 상관이 있는 것으로 설명했는데, 만약 함수가 2~3급 객체였다면 위와 같이 함수 안에서 함수를 return하는 코드를 짤 수 없기 때문이다.

예시를 하나만 더 보자.

function counts() {
  let count = 0;
  function addCount() {
    count++;
    return count;
  }
  return addCount;
}

let closure1 = counts();
// count : 1
let count1 = closure1();

let closure2 = closure1;
// count : 2
let count2 = closure2();

console.log(count1, count2);
// 1, 2

closure2closure1을 참조해서 만들기 때문에 count가 1인 클로저를 가져온다.
더 자세히 이해하고 싶으면 아래 사이트에 접속해서 과제를 풀어보자.

https://ko.javascript.info/closure

전역 변수와 클로저와 메모리 회수

  • 클로저와 메모리는 상관이 깊다. 함수를 다 실행한 뒤에도 그 함수 스코프의 변수들을 사용할 수 있다는 것은, 메모리 회수를 안 했기 때문이다.

  • 클로저를 만들었다고 해서 모든 코드가 클로저를 계속 사용한단 법은 없는데, 안 쓰는 클로저가 쌓이면 클로저를 보관하는 메모리도 누적된단 얘기다.
    따라서 클로저를 만드는 코드를 null 처리하는 방법 등으로 가비지 컬렉터가 메모리를 회수하도록 간접 처리를 해야 한다. C++같은 특수를 제외하면 JS를 포함한 어지간한 언어는 가비지 컬렉션 직접 조작이 불가능하다. (직접 조작이 가능하면 마냥 좋은 게 아니다. 문제가 나기 쉽다는 뜻이기도 하다.)

  • 자동으로 메모리를 관리하는 언어들은 보통 Mark & Sweep 알고리즘으로 가지가 안 닿는(사용을 안 하는) 메모리를 회수한다. GC의 원리가 그렇기 때문에 안 쓰는 클로저 함수를 null 처리하면 GC가 해당 메모리를 처리하는 것. 다만 가벼운 프로젝트에서 이 부분을 예민하게 신경써야할 만큼 우리의 컴퓨터 메모리가 빡빡하진 않으므로 초보라면 다른 기초를 더 신경쓰길 권한다.

  • 전역 변수는 메모리 회수 대상이 아니다. 이로 인해서 발생할 수 있는 간단한 이슈로는 DOM 조작이 있겠다. 삭제할 노드를 전역 변수로 선언하고 삭제 처리하면 화면에선 그 노드가 사라져도 메모리에는 남기 때문. 콜백 함수 내부에서 삭제할 노드를 불러내고 처리하는 게 낫다.

백문이 불여일견, 매우 간단한 예시를 하나 보자.

  • 먼저 button1 노드를 전역 변수로 선언한다.
  • lexicalEnvironment() 함수를 쓰면 button1은 제거된다.
  • lexicalEnvironment() 함수는 printf 함수를 return하면서 클로저를 생성한다. 클로저에는 자유 변수 freeVariable이 포함될 것이다.
  • lexicalEnvironment() 함수의 결과물을 closure라는 변수에 담아낸다.
  • closure에 콜백함수를 전달해본다. 클로저가 정상 작동하면 freeVariable이 잘 참조될 것이다.
  • 전역변수 btn1과 클로저는 사용이 끝났지만 메모리에 계속 남을 것이다.

  • closure 변수에 내용이 담기기 전 단계.
  • 클로저가 정상적으로 생성되어 closure 변수에 할당될 것이다.
  • btn1은 이미 삭제 처리가 됐음에도 Script에 남아있는 모습을 볼 수 있다.

모든 작업이 다 끝났지만 사용할 일이 없는 btn1과 클로저가 메모리에 남아있는 모습을 볼 수 있다. 이를 해결하려면 어떻게 할까?

  • 한 번만 쓸 거라면 즉시 실행 함수(IIFE)를 사용해도 좋다.
  • 삭제할 노드는 함수 내부에서 선언한다.
function lexicalEnvironment() {
  // 삭제할 노드를 함수 내부에서 선언
  const btn1 = document.getElementById("button1");
  const freeVariable = "자유 변수";
  btn1.remove();
  function printf(callback) {
    const str = freeVariable + "입니다.";
    callback(str);
  }
  return printf;
}

let closure = lexicalEnvironment();

closure((str) => console.log(str));

// 클로저를 해제.
closure = null;
const num = 1;

btn1과 클로저가 말끔하게 비워진 모습을 볼 수 있다.

  • 원래 내부 함수는 외부 변수를 자유 변수로서 쓰기 때문에, 내부 함수는 열린 함수이다. (자유 변수를 쓰는 함수를 열린 함수라 한다.)

  • 하지만 클로저는 메모리 회수 대상이 아니다. 클로저에 있는 변수는 보이지는 않지만 계속 쓸 수 있다. 클로저로 인해 내부 함수는 외부 환경과 한 덩어리로 묶여서 닫힌 함수가 된다. 열려있던 것을 닫는 행위. 그래서 closure 이다.

"굳이 전역 변수를 남발하지 말고, 코드 정리 잘하자." 정도로만 받아들이자. 기본에 충실한 게 좋다. 사실 나도 클로저 비워서 메모리 관리해야겠단 생각도, 시도도 한 적 없다. 위에도 적었듯이 메모리 관리는 문제가 터지기 쉽다.

확실한 근거 없이 가비지 컬렉션을 다루면 나중에 문제 터졌을 때 원인도 못 찾고 X될 확률이 높다. 우리의 메모리는 그렇게까지 나약하지 않으니 너무 기우 가지지 말자.
유명한 말 세 가지를 소개하며 TMI는 여기까지만 하겠다.

효율이라는 명분 하에 저질러지는 죄악이 많다. (제대로 하지도 못하면서)
-William A. Wulf

웬만하면 머리에서 효율이란 말을 지워라. 섣부른 최적화가 만악의 근원이다.
-Donald E. Knuth

“최적화는 아래의 두 규칙만 따르면 된다.”
1. 하지마.
2. (네가 전문가라면) 아직 하지마. 완벽, 명쾌하지 않으면 하지마라.
-M. A. Jackson

실행 단계(Execution phase)

각 변수에 값을 할당한다. GEC의 실행 단계는 다음과 같이 이뤄진다.
실행 문맥은 스택 형태로 쌓여서 맨 위에 있는 문맥부터 실행한다.

globalExecutionObj = {
    scopeChain: [],
    variableObject: {
        ten: 10,
     	two: 2,
      	one: 1,
    },
    this: window
}

추가 예시

GEC, FEC를 같이 예시로 보자.

let firstName = 'Zelda';

function nameMaker(name){
  let lastName = 'Link';
  let fullName = firstName + lastName;
}  
  
nameMaker(firstName);  
// 생성 단계 GEC
globalExecutionObj = {
    scopeChain: [],
    variableObject: {
        firstName: <uninitialized>,
     	nameMaker: func,
    },
    this: window
}
// 실행 단계 GEC
globalExecutionObj = {
    scopeChain: [],
    variableObject: {
        firstName: 'Zelda',
     	nameMaker: pointer to function nameMaker,
    },
    this: window
}

GEC에서 함수는 생성 단계에서 함수라고 인식하고, 실행 단계에서 해당 함수의 포인터를 부여한다.

// 생성 단계 FEC
nameMakerExecutionObj = {
    scopeChain: Global,
    activationObject: {
       arguments: {
            0: name,
            length: 1
        },
        name: 'Zelda',
      
      	lastName: <uninitialized>,
      	fullName: <uninitialized>,
    },
  // 'use strict' 모드로 하면 this가 undefined로, 안 쓰면 Global로 나온다.
    this: Global or undefined
}
// 실행 단계 FEC
nameMakerExecutionObj = {
    scopeChain: Global,
    activationObject: {
       arguments: {
            0: name,
            length: 1
        },
        name: 'Zelda',
      
      	lastName: 'Link',
      	fullName: 'ZeldaLink',
    },
    this: Global or undefined
}

참조

profile
모르는 것 투성이

0개의 댓글