본문 바로가기
JS/바닐라JS

[인사이드 JS] 실행 컨텍스트와 클로저

by 실버dev 2019. 11. 9.

1. 실행 컨텍스트 개념

 

콜 스택 : 함수를 호출할 때 해당 함수의 호출 정보가 차곡차곡 쌓여있는 스택

실행 컨텍스트 : 실행 가능한 자바스크립트 코드 블록이 실행되는 환경. 여기서 코드 블록은 대부분 함수.

 

실행 컨텍스트가 형성되는 경우 세 가지 : 전역 코드, eval() 함수로 실행되는 코드, 함수 안의 코드를 실행할 경우.

 

대부분 함수로 실행 컨텍스트를 만든다.

코드 블록 안에는 변수, 객체, 실행 가능한 코드가 들어있고, 이 코드가 실행되면 실행컨텍스트가 생성된다.

실행 컨텍스트는 스택 안에 하나씩 차곡차곡 쌓이고, 제일 위에 위치하는 실행 컨텍스트가 현재 실행중인 컨텍스트이다.

 

 

 

 

2. 실행 컨텍스트 생성 과정

 

function func1(p1, p2) {
  const a = 1, b = 2
  function func2(){
    return a + b
  }
  return p1 + p2 + func2()
}
console.log(func1(3, 4))

다음 함수를 실행되는 순서는?

 

 

1) 활성 객체 생성

실행 컨텍스트가 생성되면 자바스크립트 엔진은 해당 컨텍스트에서 실행에 필요한 여러가지 정보를 담을 객체(활성 객체)를 생성한다.

이 객체에 매개변수나 사용자가 정의한 변수, 객체를 저장하고, 새로 만들어진 컨텍스트로 접근할 수 있다.

 

2) arguments 객체 생성

1번에서 만든 활성객체는 arguments프로퍼티로 이 객체를 참조한다.

 

3) 스코프 정보 생성

컨텍스트의 유효범위를 나타내는 스코프 정보를 생성한다.

스코프 정보는 현재 실행 중인 실행 컨텍스트 안에서 연결리스트와 유사한 형식으로 만들어진다.

현재 컨텍스트에서 특정 변수나 상위 컨텍스트에 접근해야 하는 경우 이 리스트를 활용함.

이 리스트를 스코프 체인이라고 하고 [[scope]]프로퍼티로 참조된다.

 

4) 변수생성

실행 컨텍스트 내부에서 사용되는 지역 변수의 생성이 이루어진다.

함수인자 p1, p2에 값이 할당되고, 변수 a, b, func2가 생성된다.

단, 변수나 내부함수는 메모리에 생성, 초기화는 표현식이 실행되기 전까지 이루어지지 않는다.

변수 a와 b에는 먼저 undefined가 할당된다.

 

5) this 바인딩

 

6) 코드실행

내부 변수 a, b에 1, 2의 값이 할당된다.

 

 

단 전역 실행 컨텍스트는 arguments 객체가 없고, 전역 객체 하나만을 포함하는 스코프 체인이 있다.

 

 

 

3. 스코프 체인

 

자바스크립트에서는 오직 함수만이 유효범위를 갖는다. (for, if 같은 구문은 X)

스코프체인은 유효범위를 나타내는 스코프로, [[scope]] 프로퍼티로 각 함수 객체 내에서 연결리스트 형식으로 관리된다.

각각의 함수는 [[scope]] 프로퍼티로 자신이 생성된 실행 컨텍스트의 스코프 체인을 참조한다.

실행 컨텍스트는 실행된 함수의 [[scope]] 프로퍼티를 기반으로 새로운 스코프 체인을 만든다.

 

const v0 = "value0"

function func1(){
  const v1 = "value1"
  function func2(){
    const v2 = "value2"
    return v2
  }
  console.log(func2())
}
func1()

 

해당 코드에서 스코프 체인은 다음과 같이 형성된다.

 

전역 실행 컨텍스트 [ v0, func1, this, [[scope]] ( 0 : 전역객체 ) ] =>

 

func1 실행 컨텍스트 [ v1, func2, this, [[scope]] ( 0 : 전역객체, 1 : func1 변수 객체 ) ] =>

 

func2 실행 컨텍스트 [ v2, this, [[scope]] ( 0 : 전역객체, 1 : func1 변수 객체, 2 : func2 변수 객체 ) ]

 

 

스코프체인이 만들어지면 이것으로 식별자 인식이 이루어진다.

식별자 인식은 스코프체인의 첫번째 변수 객체부터 시작하여, 식별자와 대응되는 이름을 가진 프로퍼티가 있는지 확인한다.

없으면 찾을때까지 다음 객체로 이동한다.

 

 

스코프체인을 이해하면 호이스팅의 원리에 대해서도 알 수 있다.

전역에서 선언된 함수는 스코프 체인 생성과정에서 전역 실행 컨텍스트의 변수객체에 저장되므로, 함수를 호출한 뒤에 해당 함수 선언문을 정의하더라도 이를 참조 할 수 있다.

표현식은 변수에 함수를 할당하는 것이 코드 실행 과정에 일어나므로, 호출 전에 정의를 안하면 참조할 수 없다.

 

 

 

 

 

4. 클로저

 

1) 클로저 개념

function outerFunc() {
  const a = 10
  const innerFunc = function(){
    console.log(a)
  }
  return innerFunc
}
const inner = outerFunc()
inner() // 10

 

inner()는 outerFunc()가 실행이 종료 된 후에 실행된다.

그런데 실행이 끝난 컨텍스트의 변수 a를 참조하였다.

 

outerFunc의 실행 컨텍스트는 사라졌지만, outerFunc 변수 객체는 여전히 남아있고, innerFunc의 스코프 체인으로 참조된다.

이를 클로저라고 한다.

 

클로저 : 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수

 

위 코드의 innerFunc이 클로저이다.

outerFunc를 외부함수, a를 자유 변수라고 한다.

 

참고) 클로저로 접근하는 변수 대부분이 스코프 체인의 첫번째 객체가 아닌 그 뒷쪽의 객체이므로 이곳에 접근 하는 것은 성능 문제를 유발시킬 수 있는 여지가 있다. 클로저를 사용한 코드는 그렇지 않은 코드보다 메모리 부담도 많아진다.

클로저는 자바스크립트의 강력한 기능이지만 영리하게 사용해야 할 필요가 있다.

 

 

 

2) 클로저의 활용

 

클로저를 왜 쓰는가?

 

- 특정 함수에 사용자가 정의한 객체의 메서드 연결하기

 

function HelloFunc(func) {
  this.greeting = "hello"
}

HelloFunc.prototype.call = function(func){
  func ? func(this.greeting) : this.func(this.greeting)
}

const userFunc = function(greeting) {
  console.log(greeting)
}

const objHello = new HelloFunc()
objHello.func = userFunc
objHello.call() // hello


function saySomething(obj, methodName, name) {
  return (function(greeting) {
    return obj[methodName](greeting, name)
  })
}

function newObj(obj, name) {
  obj.func = saySomething(this, 'who', name)
  return obj
}

newObj.prototype.who = function(greeting, name){
  console.log(greeting + " " + (name || "everyone"))
}

const obj1 = new newObj(objHello, "kim")
obj1.call() // hello kim

userFunc() 함수를 정의하여 HelloFunc.func()에 참조시킨 뒤, HelloFunc()의 지역변수인 greeting을 화면에 출력시킨다.

 

 

 

- 함수의 캡슐화

전역변수에 노출시키면 충돌 가능성이 있다.

클로저를 활용하여 추가적인 스코프에 값을 캡슐화 하여 이를 해결할 수 있음.

 

 

-setTimeout()에 지정되는 함수의 사용자 정의

function callLater(obj, a, b){
  return (function() {
    obj["sum"] = a + b
    console.log(obj["sum"])
  })
}

const sumObj = {
  sum : 0
}

const func = callLater(sumObj, 1, 2)
setTimeout(func, 500)

setTimeout에 첫번째 인자로 그냥 함수의 참조를 넘겨준다면, 함수에 인자를 줄 수 없다.

이를 클로저로 해결한다.

 

 

 

3) 클로저를 활용할 때 주의사항

 

- 클로저의 프로퍼티값이 쓰기 가능하므로 그 값이 여러번 호출로 항상 변할수 있음을 유의해야한다.

 

- 하나의 클로저가 여러 함수 객체의 스코프체인에 들어갈 수도 있다.

function func(){
  let a = 1
  return{
    func1 : function(){console.log(++a)},
    func2 : function(){console.log(--a)}
  }
}
const exam = func()
exam.func1() // 2
exam.func2() // 1