Programming/Javascript

[Javascript] You Don't Know JS (렉시컬스코프)

알로그 2020. 4. 6. 00:52
반응형

Chapter 2. 렉시컬 스코프

1장에서 '스코프'를 엔진이 확인자 이름으로 현재 스코프 또는 중첩 스코프 내에서 변수를 찾을 때 사용하는 '규칙의 집합'이라고 정의했음

두 가지 방식 스코프

  • 다른 방식보다 훨씬 더 일반적이고 다수의 프로그래밍 언어가 사용하는 방식(렉시컬 스코프)
  • Bash scripting이나 Perl의 일부 모드와 같은 몇몇 언어에서 사용하는 방식(동적 스코프)

2.1 렉스타임

렉싱 처리 과정에서는 소스 코드 문자열을 분석하여 상태 유지 파싱의 결과로 생성된 토큰에 의미를 부여함

렉시컬 스코프는 렉싱 타임에 정의되는 스코프임
렉시컬 스코프는 개발자가 변수와 스코프 블록을 어디에 작성하는가에 기초해서 렉서가 코드를 처리할 때 확정됨

functio foo(a){
    var b = a * 2;

    function bar(c){
        console.log(a, b, c);
    }

    bar(b * 3);
}

foo(2); //2, 4, 12

이 예제에는 3개의 중첩 스코프가 있음

  • 글로벌 스코프, 해당 스코프 안에는 오직 하나의 확인자 foo만 있음
  • foo의 scope을 감싸고 있고, 해당 스코프 안에는 3개의 확인자 a, bar, b를 포함
  • bar의 scope을 감싸고 있고, 해당 스코프는 하나의 확인자 c를 포함

bar의 버블은 foo의 버블 내부에 완전히 포함됨

중첩 버블간 경계가 엄밀하게 정해져 있음, 어떤 함수의 버블도 동시에 다른 스코프 버블 안에 존재할 수 없음

2.1.1 검색

엔진은 스코프 버블의 구조와 상대적 위치를 통해 어디를 검색해야 확인자를 찾을 수 있는지 알 수 있음

console.log 구문을 살펴보면 3개의 참조된 변수 a, b, c를 검색함
검색은 가장 안쪽 스코프 버블인 bar()함수의 스코프에서 시작함
여기서 a를 찾지 못하면 다음으로 가까운 foo()의 스코프로 올라가고 이곳에서 a를 찾아 사용함

변수 c가 bar()와 foo() 내부에 모두 존재한다고 가정하면 bar() 내부에 있는 c를 찾아서 사용하고 foo()에는 찾으러 가지 않음

스코프의 목표와 일치하는 대상을 찾는 즉시 검색을 중단함
여러 중첩 스코프 층에 걸쳐 확인자 이름을 정의할 수 있는데, 이를 섀도잉이라고 함

글로벌 변수는 자동으로 웹 브라우저의 window 같은 글로벌 객체에 속함

2.2 렉시컬 속이기

렉시컬 스코프는 개발자가 작성할 때 함수를 어디에 선언했는지에 따라 결정됨
그렇다면 런타임 때 어떻게 렉시컬 스코프를 수정할 수 있을까?

자바스크립트에서 렉시컬 스코프를 속일 수 있는 방법이 두 가지 있음
이런 방법은 성능을 떨어뜨리니 권장하지 않음

2.2.1 eval

자바스크립트의 eval()함수는 문자열을 인자로 받아들여 실행 시점에 문자열의 내용을 코드의 일부분처럼 처리함

function foo(str, a){
    eval(str);
    console.log(a, b);
}

var b = 2;
foo("var b = 3;", 1); //1, 3

문자열 "var b = 3;"은 eval()이 호출되는 시점에 원래 있던 코드인 것처럼 처리됨
이 코드는 새로운 변수 b를 선언하면서 이미 존재하는 foo()의 렉시컬 스코프를 수정함
foo() 안에 변수 b를 생성하여 바깥 스코프에 선언된 변수 b를 가리게 됨

eval()은 흔히 동적으로 생성된 코드를 실행할 때 사용됨

Strict Mode 프로그램에서 eval()을 사용하면 자체적인 렉시컬 스코프를 이용함
eval()내에서 실행된 선언문은 현재 위치의 스코프를 실제로 수정하지 않음

function foo(str){
    "use strict";
    eval(str);
    console.log(a); //ReferenceError: a is not defined
}

foo( "var a = 2" );

eval()과 비슷한 효과를 내는 다른 방법들이 있지만, 동적으로 생성한 코드를 프로그램에서 사용하는 경우는 굉장히 드물고 성능 저하를 감수할만큼 활용도가 높지 않음

2.2.2 with

렉시컬 스코프를 속일 수 있는 자바스크립트의 또 다른 기능 (권장x, 곧 없어질 예정)
with는 일반적으로 한 객체의 여러 속성을 참조할 때 객체 참조를 매번 반복하지 않기 위해 사용하는 일종의 속기법

var obj = {
    a: 1,
    b: 2,
    c: 3
};

obj.a = 2;
obj.b = 3;
obj.c = 4;

with (obj){
    a = 3;
    b = 4;
    c = 5;
}

with는 단순히 객체 속성을 편하게 접근할 수 있는 속기법 이상의 효과가 있음

function foo(obj){
    with(obj){
        a = 2;
    }
}

var o1 = {
    a: 3
};

var o2 = {
    b: 3
};

foo(o1);
console.log(o1.a); //2

foo(o2);
console.log(o2.a);  //undefined
console.log(a);     //2-Oops, leaked global! 

객체 o1, o2가 있고, 하나는 a 속성이 있고 다른 하나는 그런 속성이 없음
o1을 인자로 넘기면 "a = 2" 대입문 처리과정에서 o1.a를 찾아 값 2를 대입함
o2를 인자로 넘길 때는 o2에 a 속성이 없으므로 새로이 속성이 생성되지 않고 o2.a는 undefined로 남음

이때 "a = 2"가 글로벌 변수 a를 생성한다는 점에 주목하자

with문은 속성을 가진 객체를 받아 마치 하나의 독립된 렉시컬 스코프를 취급함
따라서 객체의 속성은 모두 해당 스코프 안에 정의된 확인자로 간주됨

with 블록 안에서 var 선언문이 수행될 경우 선언된 변수는 with 블록이 아니라 with를 포함하는 함수의 스코프에 속함

따라서 o1을 넘겨받은 with문은 o1이라는 스코프를 생성하고 그 스코프는 o1.a 속성에 해당하는 확인자를 가짐
o2가 스코프로 사용되면 그 스코프에는 a 확인자가 없으므로 이후 작업은 일반적인 LHS 확인자 검색규칙에 따라 진행됨

2.2.3 성능

런타임에 스코프를 수정하거나 새로운 렉시컬 스코프를 만드는 방법으로 eval()과 with 모두 작성된 렉시컬 스코프를 속임

자바스크립트 엔진은 컴파일레이션 단계에서 상당수의 최적화 작업을 진행함
핵심 작업은 렉싱된 코드를 분석하여 모든 변수와 함수 선언문이 어디에 있는지 파악하고 실행 과정에서 확인자 검색을 더 빠르게 하는 것임

2.3 정리

렉시컬 스코프는 개발자가 코드를 작성할 때 함수를 어디에 선언하는지에 따라 정의되는 스코프를 말함
컴파일레이션의 렉싱 단계에서는 모든 확인자가 어디서 어떻게 선언됐는지 파악하여 실행 단계에서 어떻게 확인자를 검색할지 예상할 수 있도록 도와줌

  • eval() : 하나 이상의 선언문을 포함하는 코드 문자열을 해석하여 렉시컬 스코프가 있다면 런타임때 수정
  • with : 객체 참조를 하나의 스코프로, 속성을 확인자로 간주하여 런타임에 완전히 새로운 렉시컬 스코프를 생성함

eval()과 with는 최적화 작업을 무산시키니 사용하지 말자

반응형