ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JS] Scope, Execution context, Closure
    programing/Language 2019. 8. 3. 18:30

    안녕하세요, Einere입니다.

    (ADblock을 꺼주시면 감사하겠습니다.)


    해당 포스트는 JavaScript : Scope 이해를 읽고 간단하게 정리한 포스트입니다.

    해당 내용은 ES3 기준이라는 제보가 있어, 참고에 ES5 이후 글을 링크해두겠습니다.

     

     

    1. 유효범위(Scope)

    유효범위(이하 스코프)는 변수와 매개변수의 접근성과 생존기간을 의미합니다.

     

    스코프는 다음과 같이 두가지가 있습니다.

    • 전역(global) 스코프
      • 전역 스코프는 스크립트 내부 어디서도 참조가 가능합니다.
    • 지역(local) 스코프, 함수(functional) 스코프
      • 특정 함수 내부에서만 참조가 가능합니다.

    위 그림에서, 전역 스코프에 해당하는 값들은 global_scopeA함수입니다. A함수의 지역 스코프에 해당하는 값들은 local_scope_aB함수입니다. B함수의 지역 스코프에 해당하는 값들은 local_scope_b와 익명함수입니다. 익명함수의 지역스코프에 해당하는 값은 local_scope_c입니다.

     

     

    스코프의 특징

    JS에서 스코프는 다음과 같은 특징들이 있습니다.

    • 블록 단위 스코프와 함수 단위 스코프
    • 변수명 중복 허용
    • 변수 선언 키워드의 생략
    • 렉시컬 특성

     

    블록 단위 스코프와 함수 단위 스코프

    function scopeTest() {  
        var a = 0;
        if (true) {
            var b = 0;
            for (var c = 0; c < 5; c++) {
                console.log("c=" + c);
             }
             console.log("c=" + c);
        }
        console.log("b=" + b);
    }
    scopeTest();  
    //실행결과
    /*
    c = 0  
    c = 1  
    c = 2  
    c = 3  
    c = 4  
    c = 5  
    b = 0  
    */

    다른 프로그래밍 언어들은 스코프의 단위가 블록 단위이기 때문에 위의 코드와 같은 if문, for문 등 구문들이 사용되었을 때, 중괄호 밖의 범위에서는 그 안의 변수를 사용할 수 없습니다.

    하지만 JavaScript의 스코프는 함수 단위이기 때문에 예제코드의 변수 a, b, c모두 같은 유효범위를 갖습니다. 그 결과, 실행화면을 보면 알 수 있듯이 구문 밖에서 그 변수를 참조합니다.

     

    function scopeTest2() {  
        let a = 0;
        if (true) {
            let b = 0;
            for (let c = 0; c < 5; c++) {
                console.log("c=" + c);
             }
             console.log("c=" + c);
        }
        console.log("b=" + b);
    }
    scopeTest2();  
    
    // 실행 결과
    /*
    c=0
    c=1
    c=2
    c=3
    c=4
    Uncaught ReferenceError: c is not defined
    */

    그러나, 함수 스코프를 가지는 특성은 var만의 특징이며, letconst는 위와 같이 블록 스코프를 가집니다. 따라서, for문 밖에서 c를 참조할 수 없습니다.

     

    변수명 중복 허용

    const scope = 10;  
    function scopeExam(){  
        const scope = 20;
        console.log("scope = " +scope);
    }
    scopeExam();  
    //실행결과
    /*
    scope =20
    */

    위와 같이 스코프가 다른 경우, 같은 변수명이 허용됩니다.

    단, 이름이 같은 변수를 참조할 때, 가장 가까운 범위의 변수를 참조합니다. 위의 코드 실행화면을 보면 함수 내에서 scope를 호출했을 때 전역 변수 scope를 참조하는 것이 아니라 같은 함수 내에 있는 지역변수 scope를 참조합니다.

     

    변수 선언 키워드의 생략 (암묵적 전역, implicit global)

    function scopeExam(){  
        scope = 20;
        console.log("scope = " +scope);
    }
    
    function scopeExam2(){  
        console.log("scope = " + scope);
    }
    scopeExam();  
    scopeExam2();  
    //실행결과
    /*
    scope=20  
    scope=20  
    */

    변수 선언을 생략하면, 무조건 전역 변수로 취급됩니다.

    // 브라우저의 개발자 도구의 콘솔
    
    var foo = 1;
    // 출력 : undefined
    if(true) {
        bar = 2;
    }
    // 출력 : 2
    window.foo
    // 출력 : 1
    window.bar
    // 출력 : 2

    더 자세히 말하자면, 전역 객체의 프로퍼티가 됩니다. 이 것은 전역 스코프에서 var로 선언한 변수도 동일합니다. 따라서 엄밀하게는 변수가 아니며, delete 키워드로 제거할 수 있습니다. 또한 호이스팅이 되지 않습니다.

    좋은 습관은 아니니 반드시 constlet을 사용하는 습관을 들이는 것이 좋습니다. (혹은 스트릭트 모드를 적용하면 암시적 전역 변수를 허용하지 않습니다.)

     

    렉시컬 특성

    lexical scope is the set of rules about how the Engine can look-up a variable and where it will find it. The key characteristic of lexical scope is that it is defined at author-time, when the code is written (assuming you don't cheat with eval() or with).

    You Don't Know Js에 따르면, 렉시컬 스코프는 기본적으로 엔진이 필요한 변수를 어디서 어떻게 찾는지를 정한 규칙의 집합이라고 합니다. 또한, 렉시컬 스코프의 핵심 특징은 실행시가 아닌, 정의시에 결정된다는 것입니다. (eval이나 with를 사용하지 않은 경우 한정)

     

    function f1(){  
        const a= 10;
        f2();
    }
    function f2(){  
        return console.log("호출 실행");
    }
    f1();
    
    //실행결과
    /*
    호출실행
    */
    
    
    function f3(){  
        const b= 10;
        f4();
    }
    function f4(){  
        return b;
    }
    f3();
    
    //실행결과
    /*
    Uncaught ReferenceError: b is not defined
    */

    f3내부에서 f4가 호출되기 때문에 f4내부에서 f3의 변수를 참조할 수 있을 것 같지만, 참조할 수 없습니다. 왜냐하면 f4가 정의되었을 때, 스코프 내에 b가 선언되지 않았기 때문입니다. (이것은 함수 표현식을 사용한 경우에도 마찬가지입니다.)

     

     

     

    2. 실행 문맥(Execution context)

    실행 문맥이란, 실행에 필요한 정보들을 의미합니다. JS에는 호출 스택(call stack)이 있는데, 이 호출 스택에는 함수가 실행될 때 마다, 실행 문맥들이 차곡차곡 쌓이게 됩니다. 호출스택의 top이 현재 실행중인 함수의 실행 문맥이 됩니다.

     

    console.log("전역 컨텍스트 입니다");  
    function func1(){  
        console.log("첫 번째 함수입니다.");
    };
    function func2(){  
        func1();
        console.log("두 번째 함수입니다.");
    };
    func2();
    
    // 실행 결과
    /*
    전역 컨텍스트 입니다
    첫 번째 함수입니다.
    두 번째 함수입니다.
    */

    호출 스택과 실행 문맥

    스크립트가 실행이 되면 콜 스택에 전역 컨텍스트가 쌓입니다. 위의 코드에서 함수 func2의 실행 문구가 나와 함수가 실행이 되면 그 위에 func2의 실행 컨텍스트가 쌓입니다. func2가 실행되는 도중 함수 func1이 실행이 되면서 콜 스택에는 func2 실행 컨텍스트위에 func1의 실행 컨텍스트이 쌓이죠. 그렇게 func1이 종료가되고 func2가 종료가 되면서 차례로 컨텍스트들이 스택에서 빠져나오게됩니다. 마지막으로 스크립트가 종료가 되면 전역 컨텍스트가 빠져나오게 되는 구조입니다.

     

     

    실행 문맥의 구조

    실행 문맥 객체의 구조

    실행 문맥은 크게 세가지 정보를 가지고 있습니다.

    • 활성화 객체
      • 실행에 필요한 여러 가지 정보들을 담을 객체입니다. 여러 가지 정보란 arguments 객체와 변수등을 말합니다.
    • 유효 스코프
      • 현재 실행 문맥의 유효 스코프를 나타냅니다.
      • 해당 실행 문맥의 활성화 객체에서 필요한 변수(혹은 값)을 찾지 못한다면, 유효 스코프를 타고 가서 찾습니다.
    • this
      • 현재 실행 문맥을 포함하는 객체입니다.

    스크립트가 시작되면 우선 최상위 실행 문맥이 생성되고, abcFunction이 실행되면 호출 스택에 새로운 실행 문맥이 생성되어 쌓이게 됩니다.

     

    실행 문맥의 생성

    실행 문맥의 생성 순서는 다음과 같습니다.

    1. 활성화 객체 생성
    2. arguments 객체 생성
    3. 유효 스코프 정보 생성
    4. 변수 생성
    5. this 객체 바인딩
    6. 실행

    arguments 객체는 함수가 실행될 때 들어오는 매개변수들을 모아놓은 유사 배열 객체입니다.

    Scope Chain은 유효범위 정보를 담는 일종의 배열이며, 0번 인덱스는 전역 변수 객체를 참조합니다. Scope Chain에 대해서는 뒤에 다시 한 번 설명하겠습니다.

    변수들은 위의 코드의 지역변수와 매개변수 a, b, c 입니다. 매개변수 ab는 실행 문맥 생성단계에서 초기화 값이 들어가지만, c의 경우 생성 후 실행 단계에서 초기화가 되기 때문에 undefined란 값을 가지고 생성됩니다.

     

    호이스팅

    호이스팅은 선언을 상단으로 끌어올리는 것을 의미합니다.

    호이스팅이 되는 대상으로는 var 키워드로 선언한 변수와 함수 선언식이 있습니다. 다만, 변수 같은 경우에는 선언만 끌어올려질 뿐, 초기화할당은 끌어올려지지 않기 때문에 조심해야 합니다.

     

    // 크롬 개발자 도구 콘솔에서
    
    function func1() {
        console.log('foo1', foo);
        var foo = 1;
        console.log('foo2', foo);
    }
    // 출력 : undefined
    function func2() {
        console.log('bar1', bar);
        let bar = 2;
        console.log('bar2', bar);
    }
    // 출력 : undefined
    
    func1();
    // 출력 : foo1 undefined
    // 출력 : foo2 1
    // 출력 : undefined
    
    func2();
    // 출력 : Uncaught ReferenceError: Cannot access 'bar' before initialization
    //    at func2 (<anonymous>:2:17)
    //    at <anonymous>:1:1
    

    위 코드처럼, var로 선언한 변수인 foo는 호이스팅으로 인해 선언 이전에도 에러가 나지 않습니다. (다만 초기화는 호이스팅 되지 않아 undefined로 출력됩니다.) 그러나, let으로 선언한 변수인 bar는 호이스팅이 되지 않아 에러가 납니다.

     

    유효 스코프 체인

    유효 스코프 체인이란, 함수가 중첩 선언되어 있는 경우 내부 함수가 외부 함수의 범위까지 가지는 것을 의미합니다. 더 자세히 말하자면, 내부 함수가 실행될 때 내부 함수에서 필요한 변수(혹은 값)을 찾지 못한다면 외부함수에서도 찾습니다.

     

    간단한 실행 컨텍스트 예시

    위에서 말했듯이, 함수가 실행될 때 실행 컨텍스트를 생성하며, 실행 컨텍스트 내부에 유효 스코프 정보를 등록하는 기준은 정의될 때를 기준으로 합니다. 특정 함수 내부에 정의된 함수가 호출될 때, 해당 함수를 호출한 부모 함수의 활성화 객체를 Scope Chain 배열에 추가합니다.

     

    실행 컨텍스트의 자세한 예시

    우선, 전체 스크립트를 포함하는 익명 함수 실행 컨텍스트가 생성됩니다. 활성화 객체가 a, window를 가진 객체를 가리키게 합니다. (스코프 체인과 this바인딩은 원글에 언급되어 있지 않아서, 표현을 생략하였습니다. 아마 제 생각에는 스코프 체인 0번 인덱스에 자기 자신을 추가하고, 자기 자신을 this바인딩 할 것 같습니다.)

    outterFunction이 실행되면 해당 실행 컨텍스트가 생성됩니다. 활성화 객체가 b를 가진 변수 객체를 가리키게 합니다. 스코프 체인 0번 인덱스에 전역 변수 객체를 할당합니다.

    innerFunction이 실행되면 해당 실행 컨텍스트가 생성됩니다. 활성화 객체가 c를 가진 변수 객체를 가리키게 합니다. 스코프 체인 0번 인덱스에 전역 변수 객체를 할당하며, 1번 인덱스에는 부모인 outterFunction 실행 컨텍스트의 활성화 객체를 할당합니다.

    만약 innerFunction내부에 deepInnerFunction이라는 함수가 정의되고 실행된다면, 스코프 체인 0번 인덱스에는 전역 변수 객체가, 1번 인덱스에는 b를 가진 변수 객체가, 2번 인덱스에는 c를 가진 변수 객체가 할당될 것입니다.

     

     

     

    3. 클로저(Closure)

    클로저는 JS의 스코프 체인을 이용하여, 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 방법입니다. 외부 함수의 실행이 끝났더라도(호출 스택에서 제거되었더라도), 내부함수가 실행되면 내부함수에서 참조하는 외부함수는 닫히지 못하며, 내부함수에 의해서 닫히게 되어 클로저라 불립니다. 따라서 클로저란 외부에서 내부 변수에 접근할 수 있도록 하는 함수입니다.

     

    function outerFunc(){  
        let a = 0;
        return {
            innerFunc1 : function(){
                a += 1;
                console.log(`a = ${a}`);
            },
            innerFunc2 : function(){
                a += 2;
                console.log(`a = ${a}`);
            }
        };
    }
    const out1 = outerFunc();  
    const out2 = outerFunc();  
    out1.innerFunc1();  
    out1.innerFunc2();  
    out2.innerFunc1();  
    out2.innerFunc2();  
    
    //실행결과
    /*
    a = 1  
    a = 3  
    a = 1  
    a = 3  
    */

    outerFunc의 실행으로 innerFunc1innerFunc2를 가진 객체가 반환되며, 각 함수는 outerFunc에 선언된 변수 a를 참조할 수 있습니다. 왜냐하면 innerFunc1innerFunc2는 정의될 때, 스코프 체인에 의해 outerFunc의 변수 객체를 참조할 수 있기 때문입니다.

    또한 out1out2는 생성될 때 서로 독립적인 컨텍스트를 가지며, 이에 따라 out1의 함수의 실행과 out2의 함수의 실행은 서로 영향을 미치지 않습니다.

     

     

    클로저의 사용

    클로저를 사용하는 이유는 다음과 같습니다.

    • 전역 변수 사용의 최소화
    • 변수의 공유
      • 위 예시에서 innerFunc1innerFunc2는 변수 a를 공유합니다.
    • private한 변수 선언 (캡슐화)
      • 위 예시에서, 오로지 innerFunc1innerFunc2만이 변수 a를 참조할 수 있습니다.
      • 이를 이용해, setter와 getter를 만들 수 있습니다.

     

     

    클로저 사용시 주의할 사항

    var를 이용한 스코프

    블록 스코프와 함수 스코프의 예시와 같이, 반복문의 반복변수로서 var를 사용하는 것은 지양해야 합니다.

     

    클로저의 오남용

    function Foo(){
        this.bar = 1;
        this.baz = function() {
            console.log(this.bar);
        };
    }
    
    const foo1 = new Foo();
    const foo2 = new Foo();
    foo1.baz();
    foo2.baz();
    

    상속에 대해 잘 모르시는 분들은 위와 같이 필드와 메소드를 선언하는 경우가 종종 있습니다. 이 경우, 모든 Foo 인스턴스들은 똑같은 동작을 하는 함수를 개별적으로 가지게 됩니다.

    따라서 메모리의 낭비가 심하기 때문에, 다음과 같이 prototype을 이용하는 것이 좋습니다.

     

    function Foo() {
    	this.bar = 1;
    }
    Foo.prototype.baz = function() {
        console.log(this.bar);
    };

    baz함수가 어떻게 this를 참조하는지 궁금하실 텐데, 생성자 함수인 Foonew를 이용하여 생성된 인스턴스는 자동으로 this바인딩이 이루어져, 자신의 bar인 1을 출력할 수 있습니다.

     

    this와 arguments는 참조 불가능

    function f1(){  
        function f2(){
            console.log(arguments[0]);
        }
        return f2;
    }
    const exam = f1(1);  
    exam();
    
    //실행결과
    /*
    undefined  
    */

    변수 exam에는 함수 f2가 할당되어 있습니다. 그리고 함수 f2arguments객체를 참조합니다. 함수 f1을 호출할 때 인자로 1을 주었으니, 1이 출력될 것 같지만 그렇지 않습니다. 왜냐하면 스코프 체인을 통해 참조가 가능한 것은 오로지 변수 객체이기 때문입니다.

    따라서 f1arguments[0]에는 1이 할당되어 있지만, f2arguments[0]에는 아무것도 할당되지 않았기 때문에 undefiend가 출력됩니다.

     

    Function 생성자

    const x = 10;
    
    function createFunction1() {
        const x = 20;
        return new Function('return x;'); // this x refers global x
    }
    
    function createFunction2() {
        const x = 20;
        function f() {
            return x; // this x refers local x above
        }
        return f;
    }
    
    const f1 = createFunction1();
    const f2 = createFunction2();
    console.log(f1()); // 10
    console.log(f2()); // 20
    Functions created with the Function constructor do not create closures to their creation contexts;
    they always are created in the global scope.
    When running them, they will only be able to access their own local variables and global ones, not the ones from the scope in which the Function constructor was created.

    MDN에 의하면, Function생성자로 생성된 Function객체는 생성된 곳에서 클로저를 만들지 않으며, 항상 전역 스코프를 가진다고 합니다.

    따라서 이 Function객체가 실행될 때, 오로지 자신의 변수 객체전역 변수 객체만 참조할 수 있습니다.

     

     

     

    참고

    http://www.nextree.co.kr/p7363/

     

    JavaScript : Scope 이해

    JavaScript는 단순한 언어로 여겨져 왔습니다. 그래서 여러 개발자분들이 JavaScript를 배우기도 쉽고 간단히 쓸 수 있다는 편견을 가지고있습니다. 하지만, 최근 JavaScript의 관심이 늘어나면서 JavaScript는 더이상 '쉬운 언어'가 아닌 깊은 이해를 필요로 하는 언어라는 인식이 생기고있습니다. 저는 JavaScript에 대한 깊은 이해를 하기 위해서는 클로저(Closure)에 대해 알아야 되며 이를 알기 위해서는

    www.nextree.co.kr

    https://meetup.toast.com/posts/129

     

    자바스크립트 함수(3) - Lexical Environment : TOAST Meetup

    자바스크립트 함수(3) - Lexical Environment

    meetup.toast.com

     

    'programing > Language' 카테고리의 다른 글

    [Functional] join, find, flatten  (0) 2019.08.12
    [JS] Date 객체 사용시 주의할 점  (0) 2019.08.04
    [JS] SVG에 대해서  (0) 2019.08.02
    [JS] CanvasRenderingContext2D 정리 1  (0) 2019.08.01
    [JS] 태스크 큐와 이벤트 루프  (0) 2019.07.31

    댓글

Designed by black7375.