개발/JavaScript

[JavaScript] 실행 영역(Execution Context), Scope, Closure

2016. 11. 21. 15:33

JavaScript는 다른 프로그래밍 언어들보다 조금 더 유연한 실행 영역(Execution Context)과 Scope를 갖고 있다.

 

Execution Context(실행 영역)

주로 Context라고 불리는 Execution Context는 JavaScript에서 실행 가능 코드(Executable Code)인 Global code(스크립트 실행), Function code, Eval code가 호출될 때마다 그에 해당하는 Execution Context가 하나씩 생성된다. 각각의 context들은 Global Context부터 호출된 순서대로 Call Stack에 쌓이게 되며, Stack 영역의 LIFO 규칙에 따라 현재 활성화된 Context(Active Context)가 Stack의 최상위에 위치하게 된다.

 

Context 구성

각각의 Context들은 아래와 같이 3가지로 구성되어 있다.

 - 구성환경객체(Lexical Environment Object): 해당 Context안의 함수, 변수 등을 저장, 또한 바로 상위 단계의 정보도 저장

 - 변수환경객체(Variable Environment Object): 함수, 변수 등을 저장한다는 점에서 Lexical Environment와 같지만, Lexical Environment의 값은 실행 중 변하는 반면, Variable Environment의 값은 변하지 않는다는 차이점이 있다.

 - This Binding Object: 현재 Context가 참조하고 있는 객체를 가리키는 this object를 저장. Execution Context에서 사용자가 유일하게 접근 가능한 부분이다. This 가 가리키는 object는 해당 함수가 어떻게 호출되었는지에 따라 달라진다.

 

Variable Scope, Scope Chain

Variable Scope은 일반적으로 Scope으로 불리며, 변수에 접근이 가능한 유효 범위를 의미한다. 즉 해당 Context의 life-cycle이라고도 할 수 있다. Scope은 Global scope와 Local Scope로 구분할 수 있는데, Global Scope은 스크립트 실행 시 생성되는 context의 scope이고, local scope은 function 호출 시 생성되는 context의 scope이다. JavaScript에서 Scope은 오직 function의 {}로만 생성되며, C에서와 같이 함수의 {} 뿐만 아니라 if, for 등의 {}로는 별도의 scope 구분을 하지 않는다. 다만 with문, try-catch 문으로 scope을 구분할 수 있다.

Scope Chain은 해당 Context에서 접근 가능한 식별자들의 범위를 나타내는 list 객체이다. [[Scopes]]로 표현된다. 특정 Context의 scope 뿐만 아니라 접근 가능하도록 binding 되어있는 상위 Context의 scope의 변수들도 포함되어 있다. 즉, Scope Chain을 통해 Global scope의 변수들까지 접근할 수 있다. 

var globalStr = "global";
function depth1Func() {
  var depth1Str = "depth1";
  function depth2Func() {
    var depth1Str = "depth2";
    var depth2Str = "depth2";
    console.log("depth2: " + globalStr); // depth2: global
    console.log("depth2: " + depth1Str); // depth2: depth2
    console.log("depth2: " + depth2Str); // depth2: depth2
  }
  depth2Func();
  console.log("depth1: " + globalStr); // depth1: global
  console.log("depth1: " + depth1Str); // depth1: depth1
  console.log("depth1: " + depth2Str); // RefereenceError
}
depth1Func();
console.log("global: " + globalStr); // global: global
console.log("global: " + depth1Str); // ReferenceError
console.log("global: " + depth2Str); // ReferenceError

depth2Func()에서는 해당 function의 scope 뿐만 아니라 상위 scope인 depth1Func()의 scope와 Global scope의 변수들까지 접근이 가능하다. 이 때 접근 순서는 현재 scope에서 변수를 먼저 찾고, 현재 scope에 없을 경우 상위 scope에서 찾는 식으로 올라가게 된다. 따라서 depth1Func()과 depth2Func()에서 동일한 이름으로 선언한 depth1Str의 경우 depth2Func()에서 선언한 값을 표시하게 된다. depth1Func()의 경우 해당 scope에 속한 depth1Str과 상위 scope에 속한 globalStr에는 접근 가능하지만, 하위(sub routine) scope에 속한 depth2Str은 접근할 수 없다. 마찬가지로 global scope에서는 globalStr에만 접근이 가능하다.
이 때 주의해야 할 점은 Scope은 함수가 호출될 때 정해지는 것이 아니라, 함수가 선언된 위치에 따라 결정된다는 것이다.

var str = "global";
function getStr() {
  return str;
}
function depth1Func() {
  var str = "depth1";
  function depth2Func() {
    var str = "depth2";
    console.log(str); // depth2
    console.log(getStr()); // global
  }
  depth2Func();
  console.log(str); // depth1
  console.log(getStr()); // global
}
depth1Func();

depth1Func() 및 depth2Func()에서 getStr()을 호출한 결과를 보면, getStr()이 선언된 위치의 scope에서 변수를 참조하는 것을 볼 수 있다.

 

Closure

JavaScript는 위에서 나열한 Context와 Scope 덕분에 Closure라는 녀석이 가능하다. MDN에서는 Closure에 대해서 다음과 같이 설명한다. 

클로저는 독립적인 (자유) 변수를 가리키는 함수이다. 또는, 클로저 안에 정의된 함수는 만들어진 환경을 '기억한다'.

다시 말하면 closure는 함수와 그 함수를 둘러싼 환경(Lexical Environment)을 저장하고 있는 녀석으로, 함수를 생성하는 시점에 생성된다. 함수를 생성하는 시점에 생성되기 때문에 함수를 호출하는 위치가 아니라 선언된 위치에 의해 결정된다.

 

closure가 사용된 기본적인 예시를 살펴보자.

var name = "global";
function outerFunc() {
  var name = "outer";
  function innerFunc() {
    console.log(name);
  }
  return innerFunc;
}
var f = outerFunc();
f(); // outer

global 영역에서 외부함수 outerFunc()를 호출했는데, 해당 함수는 내부함수 innerFunc()를 반환하고 있다. 다시 global 영역에서 f()를 통해 반환받은 innerFunc()을 호출하였는데, innerFunc()의 scope에는 name이 존재하지 않는다. 따라서 scope chain을 따라 outerFunc()에서 name을 찾고 outer를 출력하게 된다.

위 예제의 var f = outerFunc();에서 함수를 호출하고 실행이 종료되어 outerFunc()의 scope이 끝났음에도, f();를 통해 내부함수를 호출하면서 실행이 종료된 outerFunc()의 scope을 참조하고 있다. 원래대로라면 이미 종료된 scope을 참조하는 것은 불가능하지만, innerFunc()의 outer environment인 outerFunc()의 scope를 closure가 "참조"하고 있어 접근이 가능하다. 이론상 모든 함수는 closure이지만 실제로는 그렇게 취급하지 않고, 위와 같이 밖에서 외부함수에 접근할 수 있는 내부함수들을 closure라고 한다. 여기에서 closure가 outer environment를 저장하지 않고 참조한다고 한 이유는 값을 변경할 수 있기 때문이다.

function makeCounter() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }   
};
var Counter1 = makeCounter();
var Counter2 = makeCounter();
Counter1.increment();
Counter1.increment();
Counter1.decrement();
console.log(Counter1.value()); // 1
console.log(Counter2.value()); // 0

위에서 Counter1과 Counter2는 별개로 동작한다. Counter1의 각 함수들을 생성하는 시점인 makeCounter()를 호출하는 시점에 생긴 closure와 Counter2의 각 함수들을 생성하는 시점에 makeCounter()를 호출한 closure는 서로 다른 closure인 것이다. 반면 Counter1의 각 함수들은 동일한 closure를 공유하며, closure의 변경된 값 역시 공유한다.

for (var i=0; i<10; i++) {
  setTimeout(function timer() { console.log(i); }, 100);
}

위 코드는 0~9까지를 출력하고자 했으나 10만 10번 찍히는 결과를 출력한다. 이유는 timer는 closure로 i값을 물어보지만, closure는 environment를 참조만 하기 때문에 이미 timer가 터지고 i 값을 참조하는 시점에는 i값이 10까지 증가된 상태이기 때문이다. 위의 코드를 의도대로 0~9까지 출력하려면 즉시실행함수(Immediately Invoked Function Expression)를 사용하여 새로운 scope를 추가하여 값을 따로 저장해야 한다.

for (var i=0; i<10; i++) {
  (function(j){
    setTimeout(function timer() { console.log(j); }, 100);
  })(i);
}

 

함수가 호출되고 scope가 종료되면서 해당 context는 garbage collector의 대상이 되어 사라져야 하지만, 위와 같이 closure로 참조된 경우에는 GC의 대상에서 제외된다. 따라서 closure를 사용하는 경우에는 사용이 끝난 후에 초기화를 해서 memory 관리가 정상적으로 이루어지도록 해야한다.

 

'개발 > JavaScript' 카테고리의 다른 글

XMLHttpRequest  (0) 2014.11.05
Java script & DOM  (0) 2013.07.30