자바스크립트는 함수 지향적인 언어이다.

함수의 특징

- 동적으로 만들수 있다.
- 다른 변수에 복사 될 수 있다.
- 다른 함수에 인자로 넘겨질수 있다.
- 완전히 다른 장소에서 나중에 호출될 수도 있다.

그래서 closure가 뭔데?
클로저는 독립적인 변수(자유변수)를 가리키는 함수이다. 그리고 클로저안에 정의된 함수는 만들어진 환경을 기억한다.

Lexical environment

자바스크립트에서, 모든 동작하는 함수 코드블럭 {...} 과 스크립트 전체는 내부적(숨겨진)으로 연관된 오브젝트를 Lexical environment로 가지고 있다.

Lexical environment Object는 두가지 부분을 가지고 있다.

  1. environment Record : 모든 지역 변수를 프로퍼티고 갖고 있는 오브젝트(그리고 this의 값과 같은 다른 정보들도)
  2. outer lexicla environment 에 대한 참조, 외부 코드에 관련된것.

그래서 변수는 특별한 내부 오브젝트(환경기록, Environment Record)의 프로퍼티 이다. 변수를 가져오거나 변경하는 것은 그 오브젝트의 프로퍼티를 가져오거나 변경하는 것을 의미한다.

예를 들면 아래의 코드는 단하나의 Lexical environment를 가진다.

이것은 global Lexical environment이라고 불리는 것이다. 전체 스크립트에 연관되어 있다.

위의 사진에서, 사각형은 Environment Record(변수 저장소)를 의미한다. 그리고 화살표가 의미하는 것은 외부 참조인 것이다. global Lexical environment는 외부 참조를 가지고 있지 않다. 그래서 global Lexical environment는 null을 가리킨다.

여기 변수 parse의 값이 변경될 때, 어떠한 일이 일어나는 지 보자

오른쪽 사각형들은 global Lexical environment이 실행 동안 어떻게 변화하는지에 대한 묘사를 하고 있다.

  1. 스크립트가 시작할때, Lexical environment는 비어 있다.

  2. let phrase의 정의가 나타난다. 아무란 값이 할당되어 있지 않아서 undefined가 저장되어 있다.

  3. parse값을 할당 받는다.phrase = "hello"

  4. parse값을 바꾼다.phrase = "Bye"

요약 하자면

  • 변수는 특별한 내부 오브젝트의 프로퍼티 이다. 특별한 내부 오브젝트는 현재 실행중인 블록/ 함수/ 스크립트와 연관되어 있다.
  • 변수들이 작동하는 것은 실제로 그 오브젝트의 프로퍼티들이 작동하는 것이다.

함수 선언

let 변수와는 다르게, 함수는 완전히 초기화 된다. 프로그램이 실행되어 함수에 접근할 때 초기화되는 것이 아니라 그 이전에 Lexical environment가 만들어 질 때 함수는 초기화 된다.

최상위 레벨의 함수들은 스크립트가 시작되는 순간에 초기화되는데 이것은 함수가 정의되기 전에 함수 선언을 호출할 수 있는 이유이다.

아래의 코드는 Lexical environment가 시작부터 비어 있는 상태가 아니라는 것을 나타낸다. 아래에서 Lexical environment는 함수 say 를 가지고 있다. 왜냐하면 say는 함수 선언 이기 때문이다. 그리고 이후에 Lexical environment은 let 으로 선언된 phrase를 갖게 된다.

inner/outer Lexical environment

함수가 외부의 변수에 접근할 때 어떤일이 일어나는지 살펴보자.

함수 호출 동안, say() 는 외부 변수 phrase를 사용한다.
먼저, 함수가 실행될 때, 새로운 함수 Lexical environment이 자동으로 만들어진다. 모든 함수에 대한 일반적인 규칙이다. Lexical environment은 지역 변수들과 함수 호출 시의 파라미터를 저장하기 위하여 사용된다.

예를 들면 say("Olive")의 경우는, 아래의 그림처럼 보인다.(실행을 아래의 그림에서 화살표로 표기된 곳에서 하고 있다.)

함수 호출동안, 2개의 Lexical environment을 가지고 있다. 함수 호출을 위한 inner Lexical environment와 전역 변수를 위한 outer Lexical environment을 가지고 있다.

  • inner Lexical environment는 say의 현재 실행에 대응된다.
  • inner Lexical environment는 name이라는 하나의 프로퍼티(함수 인자)를 가지고 있다. say("Olive")를 호출하였고, name 의 값은 "Olive"가 된다.
  • outer Lexical environment은 global Lexical environment이며, phrase변수와 함수 그 자체를 가지고 있다.

inner Lexical environment은 outer Lexical environment 에대한 참조를 가지고 있다.

코드에서 변수에 접근할 때, inner Lexical environment이 먼저 검색된다. 그리고 outer Lexical environment을 검색한다. 그후 더욱 outer Lexical environment를 거쳐 global Lexical environment까지 도달한다.

만일 어디에서도 변수가 발견되지 않는다면, strict mode에서는 에러가 표기된다.use strict 표기 없이는, 이전 버전에 대한 호환성을 위하여 undefined변수를 새로 할당하여 전역 변수를 만들어낼 것이다.

예제에서 검색과정이 어떻게 진행되는지 살펴보자.

  • say 내부의 alertname에 접근하기 원할때, say함수는 함수 Lexical environment내부에서 name이라는 이름을 즉시 찾아본다.
  • sayphrase에 접근하기 원할 때는, phrase는 local Lexical environment에는 존재 하지 않는다. 그래서 둘러싸는(enclosing) Lexical environment를 따라가고 outer Lexical environment에서 phrase를 찾아낸다.

위의 과정을 통해 함수는 현재 외부 변수에 접근이 가능하다는 것을 알수 있다. 하지만 가장 가까운 값을 사용한다.

함수가 오래된 변수 값들을 참조하길 원할 때, 함수는 자기 자신의 Lexical environment환경 또는 outer Lexical environment에서 지금의 값을 가져온다.

let name = "John";

function sayHi(){
  alert(`hi, ${name})
}

name = "Pete"; // (*)

sayHi(); // Pete

위의 코드 흐름은 다음과 같다.

  1. global Lexical environment은 name: "John"을 갖고 있다.
  2. (*)로 표기된 줄에서, 전역 변수가 변경된다. 이젠 global Lexical environment는 name: "Pete"를 갖는다.
  3. sayHi()가 실행되고 name 변수 값을 외부에서 가져온다. name 변수는 global Lexical environment에서 가지고 오게 되는데, global Lexical environment의 name"Pete"이다.

하나의 호출에 하나의 Lexical environment
: 반드시 알아둬야 할것은 새로운 함수의 Lexical environment은 함수가 실행되는 각각의 시간마다 한번씩 만들어 진다는 것이다. 그리고 만일 함수가 여러번 호출되면, 각 호출이 그 호출을 위한 지역 변수들과 파라미터를 가진 자신만의 Lexical environment을 가질 것이다.

Lexical environment은 상술객체 이다.
: 이 오브젝트를 코드에서 가져올 수도 없고 직접 수정할 수도 없다. 자바스크립트 엔진은 아마 이 객체에 대한 최적화를 진행할 것이다. 메모리를 아끼기 위해 사용되지 ㅇ낳는 변수를 제거하고 다른 내부적인 트릭도 사용할 것이다.

중첩된 함수들

함수가 다른 함수 내부에서 생성됐을 때, 우리는 이함수를 "중첩된"함수라고 부른다.

중첩된 함수를 사용한 코드를 살펴보자

function sayHiBye(firstName, lastName) {  
  // helper nested function to use below
  function getFullName() {
    return firstName + " " + lastName;
  }
  
  alert( "Hello, " + getFullName() );
  alert( "Bye, " + getFullName() );
}

여기에 중첩된 함수 getFullName()은 편의를 위해 만들어졌다. 이 함수는 외부의 변수에 접근할 수 있다. 그래서 fullName을 반환할 수 있다. 중첩된 함수는 자바스크립트에서는 꽤 일반적인 코드 이다.

더욱 흥미로운 것은, 중첩된 함수가 반환될 수 있다는 것이다. 하나의 새로운 오브젝트의 새로운 프로퍼티(만약 외부 함수가 메소드와 함께 오브젝트를 만들때) 또는 그 자체의 결과로 리턴 가능하다. 위치는 상관없이, 중첩된 함수는 여전히 같은 외부 변수들에 대한 참조자를 가지고 있다.

예를 들면, 생성자 함수에 의해 중첩된 함수가 새로운 오브젝트에 할당되는 코드가 있다.

function User(name) {
  this.sayHi = function() {
    alert(name);
  };
}

let user = new User("John");
user.sayHi();

그리고 여기에서 "counting"함수를 만들고 반환한다.

function makeCounter() {
  let counter = 0;
  
  return function() {
    return count++;
  };
}

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2

makeCounter는 매 호출 시에 다음 숫자를 반환하는 "counter" 함수를 만든다. 간단함에도 불구하고, 코드는 약간의 수정으로 변형되어 실용적으로 사용되고 있다.

어떻게 counter가 내부적으로 작동할까?

내부 함수가 작동할 때, count++안에 있는 변수는 안에서 밖으로 찾는다. 예를 들면 위의 예제에서 코드가 진행되는 순서는 다음과 같다.

  1. 중첩된 함수의 Local Lexical Environment에 접근.
  2. 외부 함수의 변수들에 접근.
  3. 전역 변수에 닿을 때까지 계속하여 반복.

해당 코드에서 count는 2번째 단계에서 발견된다. 외부 변수가 수정됐을 때, 외부 변수는 발견된 곳에서 변경된다. 그래서 count++는 이 코드가 속한 Lexical Environment 내부로 부터 외부 변수를 찾고 외부 변수를 증가 시킨다.

환경 자세히 보기

1. 스크립트가 시작됐을 때는, 오직 전역 어휘 환경만이 존재.


위 시작점에서는, 오직 makeCounter 함수만이 존재한다. 왜냐하면 이건 함수 선언이기 때문이다다. 아직 실행되지 않았다.

모든 함수들은 "태어나는 순간(on birth)"에 함수 생성의 어휘 환경 참조를 가진 숨겨진 프로퍼티 [[Environment]]를 받는다.

여기, makeCounter가 전역 어휘 환경에서 만들어진다. 그래서 [[Environment]]는 전역 어휘 환경에 대한 참조를 갖고 있다.

다시 말해서, 함수는 함수가 생겨난 위치의 어휘 환경과 함께 찍혀나온다("imprinted")는 것이다. 그리고 [[Environment]]는 그 참조를 가진 숨겨진 함수 프로퍼티이다.


2. 코드가 실행되고, 새로운 전역 변수인 counter가 선언되고, 값을 위해 makeCounter()가 호출된다. 여기에 makeCounter() 내부의 코드 첫째 줄 실행 순간 스냅샷이 있다.

makeCounter()를 호출하는 순간에, 변수와 인자를 보관하기 위한 어휘 환경이 만들어 진다..

모든 어휘 환경처럼, counter는 2가지를 저장한다..

  1. 지역 변수를 저장하는 환경 레코드. count가 유일한 지역 변수이다.

  2. 함수의 [[Environment]]로 세팅된 외부 어휘 참조. 여기에 makeCounter[[Environment]]가 전역 어휘 환경을 참조한다.

그래서 이제 두 개의 어휘 환경을 갖고 있다

1. 첫번째 어휘 환경은 전역.
2. 두번째 어휘 환경은 현재의 전역으로의 외부 참조와 함께 makeCounter 호출을 위한 것이다.

3. makeCounter()의 실행 동안, 작은 중첩된 함수가 만들어진다.

함수가 만들어질 때, 함수 선언인지 함수 표현식인지는 중요하지 않다. 모든 함수는 그 함수들이 만들어진 어휘적 환경을 참조하는 [[Environment]] 프로퍼티를 갖는다.

예제에서는 새로운 중첩된 함수에 대해, [[Environment]]의 값은 makeCounter()의 현재의 어휘 환경 (함수가 만들어진 위치) 이다.

이 단계에서 내부 함수가 만들어졌었다. 하지만 아직 호출되지 않았다. function() { return count ++; } 내부의 코드는 아직 동작하지 않았다.


4. 실행이 진행되며, makeCounter()로의 호출은 끝이난다. 그리고 결과(작고 중첩된 함수)는 전역 변수 counter에 할당된다.

그 함수는 오직 하나의 라인만 갖는다. return count++, 이 함수를 실행시킬 때, 실행될 것이다.


5. counter()가 호출됐을 때, 함수 호출을 위한 "빈(empty)" 어휘 환경이 만들어진다. 이 어휘 환경은 그 자체로 어떤 지역 변수도 가지고 있지 않는다. 하지만 counter[[Environment]]는 이 어휘 환경의 외부 참조로서 사용된다. 그래서 이 어휘 환경은 이전 makeCounter() 호출의 변수에 접근할 수 있다.

지금, 만일 이 어휘 환경이 변수에 접근한다면, 처음에는 비어있는 자기 자신의 어휘 환경부터 뒤질것이다. 그 후에는 이전의 makeCounter() 호출의 어휘 환경을 뒤지고 그 이후에는 전역 어휘 환경을 뒤진다.

이 어휘 환경이 count를 찾을 때, 이 어휘 환경은 가장 가까운 외부 어휘 환경인 makeCounter의 변수 사이에서 counter를 발견할 것이다.

여기서 알아야할 사항은 메모리 관리가 어떻게 동작하는지 이다. makeCounter() 호출이 얼마 전에 끝났음에도 불구하고, 어휘 환경은 그대로 메모리에 보관되어 있다. 왜냐하면 그 어휘 환경을 참조하는 [[Environment]]를 가진 중첩 함수가 존재하기 때문이다.

일반적으로, 하나의 어휘 환경 오브젝트는 어떤 함수가 그 어휘 환경을 사용하는 한 계속하여 살아있다. (메모리에서 지워지지 않는다.) 오직 그 어휘 환경을 사용하는 함수가 존재하지 않을 때에만 메모리에서 지워진다.


6. counter() 호출은 count의 값만 반환하는 것이 아니라 증가도 시킨다. 이런 수정사항이 "그 공간에서" 일어난다는 것을 알아 두어야 한다. count의 값은 정확히 그 변수가 발견된 환경 내부에서 수정된다.

결과적으로 유일한 변화였던 새로운 count 값을 지니고 이전 단계로 돌아온다. 뒤에 이어지는 호출들도 모두 같은 일을 한다.


7. 다음 counter()를 호출하고 같은 작업을 한다.
아래의 코드에서 work() 함수는 원래 장소로 부터 외부 어휘 환경 참조를 통하여 name을 사용한다.

그래서, 결과 값은 "Pete"가 나온다.

makeWorker() 함수 내부의 let name이 없었다면, 검색 과정에서 바깥 어휘 환경으로 진입하여 우리가 위의 체인에서 볼 수 있는 전역 변수를 들고 왔을 것입니다. 이번 경우에는 그 전역 변수는 "John" 이다.

Closures : 일반적인 프로그래밍 용어 중 "Closure"가 있다.

closure는 외부의 변수를 기억하고 접근할 수 있는 함수다. 몇몇 언어에서는, 이러한 구현이 불가능 하다. 또한 이러한 일이 일어나게 하기 위해서는 함수를 턱별한 방법으로 작성해야 한다. 하지만 위에 기재된대로, 자바스크립트에서는 , 모든 함수가 자연적으로 closure이다.

자바스크립트의 함수들은 자신이 생성된 위치를 숨겨진 [[Environment]] 프로퍼티를 이용하여 기억한다. 그리고 그렇게 만들어진 모든 함수들은 외부 변수에 접근이 가능하다.

코드 블럭과 루프, 즉시 실행 함수(IIFE)

위의 예제들은 함수라는 것에 집중했다. 하지만 어휘 환경이란 것은 어떤 코드 블럭 {...} 에도 존재한다.

어휘 환경은 코드 블럭이 실행되고 블럭-지역 변수를 가질 때, 만들어 진다.

if

user변수는 오직 if 블럭 안에만 존재한다.

실행 컨텍스트가 if 블럭 안으로 들어갈 때, 블럭을 위한 새로운 "if-only"어휘 환경이 만들어 진다.

이 어휘 환경은 외부 어휘 환경으로의 참조를 갖고 있다. 그래서 phrase를 찾을 수 있다. 하지만 if문 내부에 선언된 모든 변수와 함수 표현식들은 그 어휘 환경 내부에 존재한다. 그리고 바깥에서 접근될 수 없다.

for, while

for루프 문은 모든 반복이 독립된 어휘환경을 지닌다. 만일, 변수가 for내부에서 선언됐다면 그 변수 또한 어휘환경의 지역 변수 이다.

for(let i =0 ; i< 10; i++){
  // 각각의 루프가 자신의 어휘 환경을 가짐
  {i:value}
}

alert(i) // Error, no such variable

각 루프의 반복(iteration)이 i를 내부에 지닌 자신의 어휘 환경을 갖는다.

코드 블럭들

우리는 변수를 "지역 스코프"로 독립시키기 위해서 코드블럭을 사용할 수도 있다.

예를 들면, 웹 브라우저에서, 모든 스크립트 (type="module"을 제외한)가 같은 전역 영역을 공유한다. 그래서 만일 한 스크립트 내부에서 전역 변수를 만들면, 이 전역 변수는 다른 스코프에서도 접근 가능하다. 하지만 만일 두 스크립트가 같은 변수 이름을 사용하고 서로를 덮어씌운다면, 전역 변수는 충돌의 원인이 된다.

만일 이러한 문제를 피하고 싶다면, 전체 스크립트 또는 일부를 독립시키기 위해서 코드블럭을 사용할 수 있습니다.

{
  // do some job with local variables that should not be seen outside

  let message = "Hello";

  alert(message); // Hello
}

alert(message); // Error: message is not defined

블럭 바깥의 코드(또는 다른 스크립트 내부의 코드)는 블럭 내부의 코드를 볼 수 없다. 왜냐하면 그 블럭은 자신만의 어휘 환경을 가졌기 때문이다.

IIFE

즉시 호출되는 함수 표현식(Immediately-Invoked Function Expression) : IIFE.

(function() {

  let message = "Hello";

  alert(message); // Hello

})();

여기 함수 표현식이 만들어졌고 즉시 호출된다. 그래서 코드는 즉시 실행되고 자신의 private 변수들을 가진다.

함수 표현식은 괄호로 둘러싸여 있다. (function {...}), 왜냐하면 자바스크립트가 메인 코드 흐름에서 "function"을 만났을 때, 자바스크립트는 이것을 함수 선언의 시작으로 이해한다. 하지만 함수 선언은 이름을 가져야 한다. 그래서 아래의 코드는 에러를 방출한다.

function() { // <-- Error: Unexpected token (

  let message = "Hello";

  alert(message); // Hello

}();

이름을 다시 정의 한다고 해도, 여전히 작동하지 않는다. 자바스크립트는 함수 선언이 즉시 호출되는 것을 허락하지 않는다.

// syntax error because of parentheses below
function go() {

}(); // <-- can't call Function Declaration immediately

그래서, 함수 주변에 괄호들을 붙이는 것은 함수가 다른 표현식의 컨텍스트에 만들어지는 것을 자바스크립트에게 보여주기 위한 트릭이다. 그래서 이것은 함수 표현식이 된다. 이 함수 표현식은 이름도 필요 없고 즉시 불려질 수 있다.

괄호 말고도 자바스크립트에게 함수 표현식을 쓰려고 했던 것이라고 말할 수 있는 다른 방법들도 있다.

// IIFE 만드는 방법
(function() {
  alert("Parentheses around the function");
})();

!function() {
  alert("Bitwise NOT operator starts the expression");
}();

+function() {
  alert("Unary plus starts the expression");
}();

위의 함수들은 함수 표현식을 선언하고 즉시 실행한다.

가비지 컬렉션

주로 어휘 환경은 함수 실행 이후에 clean-up되고 지워진다.

function f(){
  let value1 = 123;
  let value2 = 456;
}

f();

f()가 끝나고, 그 어휘 환경이 접근 불가능하게 된 후에, 이 어휘 환경이 메모리에서 지워진다.

하지만 만일 f의 끝에 여전히 접근 가능한 중첩된 함수가 존재한다면, 그 때 이 [[Environment]] 참조는 외부 어휘 환경을 다음과 같이 살려 놓는다.

function f() {
  let value = 123;
  function g() { alert(value); }
  
  return g;
}

let g = f(); 
// g는 f 내부의 value에 접근 가능하여 외부 어휘 환경을 메모리에 유지시킨다.

알아두기!
만일 f()가 많이 호출됐고, 결과 함수들이 저장됐다면, 그 때 상응하는 어휘 환경 오브젝트들 또한 메모리에 남게될 것이다. 아래 코드에서 3개 전부에 해당한다.

function f() {  
  let value = Math.random();
  
  return function() {alert(value);};
}

// 배열 안에 3개의 함수가 있고
// 각각은 각각의 어휘 환경과 연결된다.  
let arr = [f(), f(), f()];

하나의 어휘 환경 오브젝트는 접근 불가능할 때(다른 오브젝트들과 같이), 죽는다. 다른 말로 하면, 하나의 중첩된 함수라도 참조하고 있는 동안에만 존재한다.

아래의 코드에서, g가 접근 불가능하게 된 이후에, 에워싼 어휘 환경이 메모리에서 사라진다.

function f() {
  let value = 123;
  function g() {alert(value);}
  return g;
}

let g = f(); // g가 살아있는 동안
// 상응하는 어휘 환경이 살아있다.

g = null; // 이젠 메모리에서 사라짐

현실 최적화

함수가 살아있는 동안에는, 모든 외부 변수들이 유지되고 있다.

하지만, 실제로 자바스크립트 엔진은 그것을 최적화 하려고 하낟. 자바스크립트 엔진은 변수사용을 분석하고 만일 외부 변수가 사용되지 않는 것을 쉽게 파악할 수 있다면, 지워버린다.

V8 (Chrome, Opera)에서의 중요한 부작용은 이러한 변수를 디버깅할 때, 볼 수 없다는 것이다


참고

profile
어제보다는 오늘 더 나은

0개의 댓글