Lexical Scope | 렉시컬 스코프

스코프는 엔진이 변수 이름을 현재 스코프 또는 중첩 스코프에서 변수를 찾을때 사용하는 규칙의 집합이라고 정의한다.

스코프는 2가지 방식으로 작동한다.

첫 번쨰는 일반적이고 다수가 사용하는 렉시컬 스코프이며

두 번쨰는 Perl에서 사용하는 동적 스코프이다.

렉스타임

일반적인 컴파일러는 첫 단계를 전통적으로 토크나이징 또는 렉싱이라는 불리는 작업으로 시작한다.

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

이 개념이 렉시컬 스코프가 무엇인지, 어원이 어디인지를 알게 해주는 바탕이 된다.

렉시컬 스코프는 렉싱 타임에 정의되는 스코프다.

렉시컬 스코프는 프로그래머가 코드를 짤 때 변수와 스코프 블록을 어디서 작성하는가에 기초해서 렉서가 코드를 처리할때 확정된다.

function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c);
}
bar(b * 3);
}
foo(2);

이 예제는 3개의 중첩 스코프가 있다.

스코프를 겹쳐진 버블이라고 가정하면 이해하기 쉽다.

버블 1은 글로벌 스코프를 감싸고 있고 해당 스코프 안에 오직 하나의 확인자(foo)만 있다.

버블 2는 foo의 스코프를 감싸고 있고 해당 스코프는 3개의 확인자(a,bar,b)를 포함한다.

버블 3는 bar의 스코프를 감싸고 있고 해당 스코프는 하나의 확인자(c)를 가지고 있다.

스코프 버블은 스코프 블록이 쓰이는 곳에 따라 결정되는데 스코프 블록은 서로 중첩될 수 있다.

bar 버블은 foo의 버블 내부에 완전히 포함된다. 바로 foo의 내부에서 bar 함수를 정의했기 때문이다.

어떤 함수의 버블도 동시에 다른 두 스코프 버블안에 존재할 수없다.

검색

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

앞쪽 코드를 보면 엔진은 console.log 구문을 실행하고 3개의 참조된 변수 a,b,c를 검색한다.

검색은 가장 안쪽인 스코프 버블인 bar에서 시작한다.

여기서 a를 찾지 못하면 다음 가장 가까운 스코프 버블인 foo의 스코프로 올라가고 이곳에서 a를 찾아서 사용한다.

똑같은 방식이 b에도 적용된다.

변수 c가 bar와 foo 내부에 모두 존재한다고 가정하면 console.log 구문은 bar 내부에 있는 c를 찾아서 사용하고 foo에 있는 c를 찾지 않는다.

스코프는 목표와 일치하는 대상을 찾는 즉시 검색을 중단한다.

더 안쪽의 확인자가 더 바깥쪽의 확인자를 가리는 것을 쉐도잉이라고 한다.

새도잉과 상관없이 스코프 검색은 항상 실행 시점에서 가장 안쪽 스코프에서 시작하여 최초 목표와 일치하는 대상을 찾으면 멈추고, 그전까지는 바깥/위로 올라가면서 수행한다.

함수가 어디서 어떻게 호출되는지 상관없이 함수의 렉시컬 스코프는 함수가 선언된 위치에 따라 정의된다.

렉시컬 속이기

자바스크립트에서는 렉시컬 스코포를 속일 수 있는 두 가지 방법이 있다.

렉시컬 스코프를 속이는 방법은 성능을 떨어뜨린다.

eval

eval 함수는 문자열을 인자로 받아들여 실행 시점에 문자열의 내용을 코드의 일부분 처럼 처리한다.

즉 처음 작성한 코드에 프로그램에서 생성한 코드를 집어넣어 마치 처음 작성될 때부터 있던 것처럼 실행한다.

eval이 실행된 후 코드를 처리할 때 엔진은 지난 코드가 동적으로 해성되어 렉시컬 스코프를 변경스켰는지 알 수도 없고 관심도 없다.

엔진은 평소처럼 렉시컬 스코프를 검색을 한다.

function foo(str, a) {
eval(str);
console.log(a, b);
}
var b = 2;
foo("var b = 3", 1);

문자열 var b = 3은 eval이 호출되는 시점에 원래 있던 코드처럼 된다.

이 코드는 새로운 변수 b를선언하면서 이미 존재하는 foo의 렉시컬 스코프를 수정한다.

실제로 foo 안에 변수 b를 생성하여 바깥 스코프에 선언된 b를 가린다.

console.log가 호출될 때 a와 b 모두 foo의 스코프에서 찾을 수 있으므로 바깥의 b는 찾지 않는다.

기본적으로 코드 문자열이 하나 이상의 변수 또는 함수 선언문을 포함하면 eval이 그 코드를 실행하면서 eval이 호출된 위치에 있는 렉시컬 스코프를 수정한다.

eval은 프로그래머가 작성했던 때의 렉시컬 스코프를 런타임에서 수정할 수 있다.

strict mode에서 eval을 사용하면 eval은 자체적인 렉시컬 스코프를 이용한다.

즉 eval내에서 실행된 선언문은 현재 위치의 스코프를 수정하지 않는다.

setTimeout과 setInterval은 첫 번째 인자로 문자열을 받을 수 있고 문자열의 내용은 동적 생성된 함수 코드 처럼 처리 된다.

함수 생성자 new Function 도 비슷한 방식으로 코드 문자열을 마지막 인자로 받아서 동적으로 생성된 함수로 바꾼다.

with

with는 렉시컬 스코프를 속일 수 있는 자바스크립트의 또 다른 기능이다.

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;
}
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo(o1);
console.log(o1.a);
foo(o2);
console.log(o2.a);
console.log(a);

foo는 객체 참조 obj를 인자로 받아서 with(obj) {}를 호출하고 with 블록 내부에서 변수 a에 대한 평범한 렉시컬 참조를 수행한다.

이는 LHS 참조로 변수 a를 찾아 값 2를 대입하는 작업이다.

o1을 인자로 넘기면 o1.a를 찾아 값 2를 대입한다.

하지만 o2에는 a라는 속성이 없어서 새로 속성이 생기지 않고 o2.a는 undefined로 남는다.

하지만 a = 2가 글로벌 변수 a를 생성한다는 점이다.

with문은 속성을 가진 객체를 받아 마치 하나의 독립된 렉시컬 스코프처럼 취급한다.

따라서 객체의 속성은 모두 해당 스코프안에 정의된 확인자로 간주한다.

with 블록안에 일반적인 var 선언문이 수행될 경우 선언된 변수는 with 블록이 아닌라 with를 포함하는 함수의 스코프에 속한다.

eval은 인자로 받은 코드 문자열에 하나 이상의 선언문이 있을 경우 이미 존재하는 렉시컬 스코프를 수정할 수 있지만

with문은 넘겨진 객체를 가지고 사실상 하나의 새로운 렉시컬 스코프를 생성한다.

o2가 스코프로 사용되면 그 스코프 안에 a 확인자가 없으므로 일반적인 LHS 검색에 따라 진행된다.

글로벌 스코프에서도 확인자를 찾을 수 없어서 a = 2가 수행되면 자동으로 그에 해당하는 글로벌 변수가 생성된다.

성능

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

자바스크립트 엔진은 컴파일레이션 단계에서 상당수 최적화 작업을 진행한다.

이 최적화의 일부분이 하는 핵심 작업은 렉싱된 코드를 분석해서 모든 변수와 함수 선언문이 어디에 있는지 파악하고 실행하는 과정에서 확인자 검색을 더 빠르게 하는 것이다.

그러나 eval이나 with가 코드에 있으면 엔진은 미리 확인해둔 확인자의 위치가 틀릴 수도 있다고 가정해야한다.

엔진은 렉싱 타임때 eval에 어떤 코드가 전달되어 렉시컬 스코프가 수정될지 알 수 없고 with에 넘긴 객체의 내용에 따라 새로운 렉시컬 스코프가 생성될 수 있기 때문이다.

즉 eval이나 with가 코드에 있으면 대다수 최적화가 의미 없어진다.

정리하기

렉시컬 스코프란 프로그래머가 코드를 작성할때 함수를 어디에 선언하는지에 따라 정의되는 스코프를 말한다.

컴파일레이션의 렉싱 단계에서는 모든 확인자가 어디서 어떻게 선언됐는지 파악하여 실행 단계에서 어떻게 확인자를 검색할지 예상할 수 있도록 도와준다.

자바스크립트에서는 렉시컬 스코프를 속이는 방법이 2가지 있는데 eval과 with이다

eval은 하나이상의 선언문을 포함하는 코드 문자열을 해석하여 렉시컬 스코프가 있다면 런타임에 수정한다.

with는 객체 참조의 하나의 스코프로 속성을 확인자로 간주하여 런타임에 새로운 렉시컬 스코프를 생성한다.

이런 방식의 단점은 eval과 with가 엔진이 컴파일 단계에서 수행한 스코프 검색에 관련된 최적화 작업을 무산시킨다.

엔진은 최악의 경우를 대비해 진행한 최적화 결과가 무효화됐다고 가정해야 하기 때문이다.