Play JavaScript

Variable Scoping!

Created by 이항희 / atconsole.com (Team Blog)

목차

  • JavaScript Function
  • Function Scope
  • Scope Chain
  • Closure
  • Tip

JavaScript Function

JavaScript의 함수가 하는 일

  1. 로직 묶음 및 재활용
  2. 변수에 할당 가능
  3. 반환 값으로 사용
  4. new로 새로운 객체 생성
  5. 변수 스코프 지정 (?)

Define Function

Declaration

function helloWorld() { /* Implemenet... */ };

Expression

var helloWorld = function hw() { /* Implemenet... */ };
var helloWorld = function() { /* Implemenet... */ };
(function() { /* Implemenet... */ });
(function helloWorld() { /* Implemenet... */ });

Function Scope

JavaScript의 스코프는 보통 언어와 다른 범위

// Java
public class Scope {
    private void scope() {
        boolean isPass = true;
        String coupang = "쿠팡!";
        if(isPass) {
            String costco = "코스트코";
        }
        System.out.println(coupang + " vs " + costco);
    }
    public static void main(String [] args) {
        new Scope().scope();
    }
}

Cannot Compile...

But JavaScript...?

오로지 함수 범위

함수 정의 영역에서만 범위가 결정

// JavaScript
function scope() {
    var isPass = true;
    var  coupang = "쿠팡!";
    if(isPass) {
        var costco = "코스트코";
    }
    alert(coupang + " vs " + costco);
}
scope();
RUN

그렇다면 이 코드의 실행 결과는...?

// Hmm...?
var coffee = "아메리카노";
function hoist() {
    if(coffee == undefined) {
        var coffee = "카페모카"
    }
    alert(coffee + " 사드릴게요");
};
hoist();
RUN

Hoisting

스코프내의 모든 변수는 실행 전 선언 (컴파일 단계)

변수는 실행되면서 할당 (엔진에 의한 코드 실행 단계)

var a = 2

실제 엔진이 실행할땐 이런식으로

// 실제 인터프리터가 해석하는 방법
var coffee = "아메리카노";
function hoist() {
    var coffee;
    if(coffee == undefined) {
        coffee = "카페모카"
    }
    console.log(coffee + " 사드릴게요");
};
hoist();

JavaScript 는 이 코드를 몇단계로 나누어 생성하고 실행

컴파일러는 코드의 렉싱(lexing)을 거친 뒤 토큰된 문자열을 분석하기 시작함

var 는 선언구문이라 변수를 선언하는 코드를 생성해야 한다

먼저 a가 해당 스코프에 선언되어 있는지 판별하여 없으면 선언, 아니면 그냥 무시한다

그 다음 컴파일러는 a에 변수를 할당하는 코드를 생성한다

나머지 컴파일이 완료되고, 실행될 때가 되면 엔진은 a가 현재 스코프에서 찾을 수 있는지 확인한다

이 과정에서 스코프 체이닝이 발생. 찾아낸 변수 a에 2를 할당

체이닝 결과로 변수를 못찾으면 ReferenceError 발생

이러한 과정에서 중요한 것은 매 과정에 스코프가 깊이 관여하고 있다는 점.

Scope Chain

다시 Function...

함수는 객체!

JavaScript의 Function은 객체이기에 속성을 가진다.

function A() { /* ... */ }
A.coupang = "쿠팡!";
alert(A.coupang);
alert(A.toString()); // 기본으로 상속된 메서드도 있다.
RUN

변수 찾기

객체의 속성에 접근하는 방법은 '.' 연산자로 접근

var obj = { coupang: "쿠팡!" };
obj.coupang = "쿠팡!";
alert(obj.coupang);
alert(obj.costco);
RUN

JavaScript 는 특정 변수를 찾을 때 해당 이름을 키로 객체를 뒤져 찾는다

[[Scope]] Property

함수 객체에만 있는 인터프리터만 사용하는 내부 속성.

함수가 "선언" 되면 인터프리터에 의해 일련의 과정을 거쳐 할당된다

이렇게...

// 전역에 함수를 생성.
function getById(id, prefix) {
    prefix = prefix || "coupang_";
    return document.getElementById(prefix + id);
}
// 다음과 같이 [[Scope]] 속성이 할당.
getById.[[Scope]] = [ {
    getById: getById,
    window: window,
    document: window.document
    /* ...기타 전역변수들... */
} ]

아직 함수는 실행되지 않았다는 사실에 주의.
단지 생성되어 있다.

엔진이 실행시 이 속성을 사용

함수 실행 시 이 속성을 바탕으로 변수를 식별

그런데 잘 보시면, key-value 배열 형태이다. 이 배열의 요소들을 변수 객체라고 부른다

변수 객체는 현재 접근할 수 있는 변수로 초기화된다.

여기부터 중요.

getById("someElement");

함수가 실행되면 실행 컨텍스트가 생성된다.

실행 컨텍스트는 변수를 식별하기 위해 [[Scope]] 를 사용하여 스코프 체인을 초기화.

이 상태라면 아마도 이런 구조일 것이다.

[ExecutionContext::getById].ScopeChain = [ {
    A: A,
    window: window,
    document: window.document
    /* ...기타 전역변수들... */
} ]

이어서...

이어서 실행 컨텍스트는 활성 객체(Activation Object)라고 부르는 객체를 하나 만든다.

이 객체는 앞서 설명한 호이스팅된 객체들이 매핑된다

추가로, this 변수와 arguments 변수도 초기화된다

이 상태라면 아마도 이런 구조일 것이다.

[ExecutionContext::getById].ScopeChain = [
    {
        id: "someElement",
        prefix: undefined,
        this: window,
        arguments: [ "someElement" ]
    },
    {
        A: A,
        window: window,
        document: window.document
        /* ...기타 전역변수들... */
    }
]

이제 실행된다.

인터프리터는 변수를 만날 때마다 ScopeChain 속성에서 배열 첫번째 부터 차례대로 찾는다

function getById(id, prefix) {
    prefix = prefix || "coupang_";
    return document.getElementById(prefix + id);
}
[ExecutionContext::getById].ScopeChain = [
    {
        id: "someElement",
        prefix: undefined,
        this: window,
        arguments: [ "someElement" ]
    },
    {
        A: A,
        window: window,
        document: window.document
        /* ...기타 전역변수들... */
    }
]

실행이 끝나면...

실행 컨텍스트는 파괴되고, 생성된 활성 객체도 사라진다.

그런데 예외가 있다

바로 Closure라고 부르는 영역이 등장할 때이다.

Closure

단어의 뜻은 폐쇄.
뭘 폐쇄한다는 뜻일까요?

보통은

실행 컨텍스트와 활성 객체는 운명을 같이 .
하지만 예외가 발생할 때가 있다.

함수는 말했지만 "생성" 시 [[Scope]] 를 초기화.

[[Scope]] 는 현재 접근 가능한 변수객체로 초기화된다.

함수가 중첩될 때...

function A(val) {
    function B() {
        var b = "B";
        return val + b;
    }
    return B;
}
var b = A("A");

A를 실행하면 A안이 실행되면서 B가 선언되고,
반환하고 있다.

반환된 함수의 [[Scope]] 는 어떤 구조일까요?

// b의 [[Scope]] 속성 구조.
// 뭔가 하나 늘었다...
b.[[Scope]] = [
    {
        B: B,
        val: "A",
        arguments: [ "A" ],
        this: window
    },
    {
        A: A,
        window: window,
        document: window.document
        /* ...기타 전역변수들... */
    }
]

b를 실행하면...

ScopeChain 이 초기화되며 다음과 같을 것이다.

// 스코프체인의 변수객체가 3개.
[ExecutionContext::b].ScopeChain = [
    {
        b: "B",
        arguments: [],
        this: window
    },
    {
        B: B,
        val: "A",
        arguments: [ "A" ],
        this: window
    },
    {
        A: A,
        window: window,
        document: window.document
        /* ...기타 전역변수들... */
    }
]

이런식으로 함수가 자신을 생성해준 스코프 체인을 유지하는 것

예제 #1

var foo = 'foo';
function fn7() {
    return foo;
}

function fn8(fn) {
    var foo = 'huk?';
    alert(fn()); // ?
}
fn8(fn7);
RUN

예제 #2

function privateClosure(name, age) {
    var myName = name;
    var myage = age;
    return {
        profile: function() {
            return myName + " / " + myage;
        }
    };
}
try {
    var pr = privateClosure("javarouka", "26");
    alert(pr.profile());
    pr.myName = "이항희";
    alert(pr.profile());
}
catch(e) { alert("Error! > " + e); }
RUN

감사

Happy Coding!