Ch2 렉시컬 스코프
- 스코프는 두 가지 방식으로 작동한다. 첫 번째 방식은 렉시컬 스코프, 두 번째 방식은 동적 스코프이다. 이 장에서는 렉시컬 스코프만 다룬다.
2.1 렉스타임
- 렉시컬 스코프는 렉싱 타임에 정의되는 스코프다. 프로그래머가 코드를 짤 때 변수와 스코프 블록을 어디서 작성하는가에 기초해서 렉서가 코드를 처리할 때 확정된다.
function foo(a) {
var b = a * 2
function bar(c) {
console.log(a, b, c)
}
bar(b * 3)
}
foo(2) // 2,4,12
- 위 예제 코드에는 3개의 중첩 스코프가 있다. 스코프 버블은 스코프 블록이 쓰이는 곳에 따라 결정되는데, 스코프 블록은 서로 중첩될 수 있다.
- 어떤 함수의 버블도 동시에 다른 두 스코프 버블 안에 존재할 수 없다. 어떤 함수도 두 개의 부모 함수 안에 존재할 수 없는 것처럼.
2.1.1 검색
- 스코프는 목표와 일치하는 대상을 찾는 즉시 검색을 중단한다. 여러 중첩 스코프 층에 걸쳐 같은 확인자 이름을 정의할 수 있다. 이를 '섀도잉'이라 한다. 더 안쪽의 확인자가 더 바깥쪽의 확인자를 가리는 것이다.
- 함수가 어떻게 호출되는지와 상관없이 렉시컬 스코프는 함수가 선언된 위치에 따라 정의된다.
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()이 호출되는 시점에 원래 있던 코드인 것처럼 처리된다. 위 코드를 실행하면 실제로 foo() 안에 변수 b를 생성하여 바깥 스코프에 선언된 변수 b를 가린다.
- Strict Mode 프로그램에서 eval()을 사용하면 eval()은 자체적인 렉시컬 스코프를 이용한다. 현재 위치의 스코프를 실제로 수정하지 않는다.
2.2.2 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;
- o2 객체를 인자에 넘기면 o2안에 a라는 프로퍼티가 없기 때문에 o2.a는 undefined이다. 이때 특이한 부작용이 있는데, 대입문 'a=2'가 글로벌 변수 a를 생성한다는 점이다.
- with 문은 속성을 가진 객체를 받아 마치 하나의 독립된 렉시컬 스코프처럼 취급한다. 위 예에서 o2가 스코프로 적용되면 그 스코프에는 a 확인자가 없으므로 이후 작업은 일반적인 LHS 확인자 검색 규칙에 따라 진행된다. 결국 o2의 스코프, foo의 스코프, 글로벌 스코프에서도 a는 찾을 수 없다. 따라서 1장에서 확인한 것처럼 글로벌 변수 a가 생성된다.
2.2.3 성능
- 자바스크립트 엔진은 컴파일레이션 단계에서 상당수의 최적화 작업을 진행한다.
- 그러나 eval()이나 with가 코드에 있다면 엔진은 미리 확인해둔 확인자의 위치가 틀릴 수도 있다고 가정해야 한다.
- eval()이나 with가 코드에 있다면 대다수 최적화가 의미 없어져서 아무런 최적화도 하지 않은 것이나 마찬가지가 되어 버린다.
- 따라서 단순히 코드 어딘가에서 eval()이나 with를 사용했다는 사실 하나만으로 그 코드는 거의 확실히 더 느리게 동작할 것이다.
Ch3 함수 vs 블록 스코프
3.1 함수 기반 스코프
- 각각의 선언된 함수는 저마다의 스코프를 생성하지만 다른 어떤 자료 구조도 자체적인 스코프를 생성하지 않는다는 사실은 거짓이다.
- 함수 스코프 내부는 스코프 바깥에서는 접근할 수 없다.
3.2 일반 스코프에 숨기
- 함수의 스코프로 둘러싸서 변수와 함수를 숨길 수 있다. 스코프를 이용해 숨기는 방식을 사용하는 이유는 '최소 권한의 원칙'과 관련이 있다. 이것은 모듈/객체의 API와 같은 소프트웨어를 설계할 때 필요한 것만 최소한으로 남기고 나머지는 숨겨야 한다는 것이다.
- 접근 가능한 확인자는 의도적이든 아니든 생각지 못한 방식으로 사용될 수 있다.
3.2.1 충돌 회피
- 숨기는 것의 또 다른 장점은 같은 이름을 가졌지만 다른 용도를 가진 두 확인자가 충돌하는 것을 피할 수 있다는 점이다.
function foo() {
function bar(a) {
i = 3
console.log(a + i)
}
for (var i = 0; i < 10; i++) {
bar(i * 2)
}
}
foo()
- bar 내부의 대입문 i=3은 for 반복문을 위해 선언된 변수 i를 덮어쓴다. 그 결과로 이 코드는 무한 반복을 하게 된다.
- bar 내부의 대입문은 어떤 확인자 이름을 고르든 지역 변수로 선언해서 사용해야 한다.
글로벌 네임스페이스
- 비공개 변수와 함수가 적절하게 숨겨져 있지 않은 여러 라이브러리를 한 프로그램에서 불러오면 라이브러리들은 서로 쉽게 충돌한다.
- 네임스페이스를 통해 최상위 스코프의 확인자가 아니라 속성 형태로 라이브러리의 모든 기능이 노출된다.
var MyReallyCollLibrary = {
awesome: "stuff",
doSomething: function() {},
doAnotherThing: function() {},
}
3.3 스코프 역할을 하는 함수
var a = 2
function foo() {
var a = 3
console.log(a) // 3
}
foo()
console.log(a) // 2
- 위 예제처럼 하면 스코프는 제대로 동작 하겠지만 이상적인 방식은 아니다. 첫째로, foo()라는 이름의 함수를 선언해야 하는데, 이것은 글로벌 스코프를 오염시킨다. 실제로 foo라는 함수를 직접 호출해야만 실제 감싼 코드를 실행할 수 있다.
var a = 2
;(function foo() {
var a = 3
console.log(a) // 3
})()
console.log(a) // 2
3.3.1 익명 vs 기명
- function 구문이 시작 위치에 있다면 함수 선언문이고, 다른 경우는 함수 표현식이다.
- 함수 표현식은 이름 없이 사용할 수 있지만, 함수 선언문에는 이름이 빠져서는 안된다. 이름 없는 함수 선언문은 자바스크립트 문법에 맞지 않다.
- 이 책에서는 함수 표현식을 사용할 때 이름을 항상 쓰는 것이 좋다고 한다.
3.3.2 함수 표현식 즉시 호출하기
- 즉시 호출 함수 표현식(IIFE)는 함수 선언문을 괄호로 감싸고, 마지막에 ()로 감싼 함수를 호출하는 방식으로 도착한다.
var a = 2
;(function foo() {
var a = 3
console.log(a) // 3
})()
console.log(a) // 2
3.4 스코프 역할을 하는 블록
- 블록 스코프의 목적은 변수를 최대한 사용처 가까이에서 최대한 작은 유효 범위를 갖도록 선언하는 것이다.
- 외견상 자바스크립트는 블록 스코프를 지원하지 않는다. 물론 파고들면 방법은 있다.
3.4.2 try/catch
- try/catch문 중 catch 부분에서 선언된 변수는 catch 블록 스코프에 속한다.
try {
undefined()
} catch (err) {
console.log(err) // works
}
console.log(err) // ReferenceError
변수 err은 오직 catch 문 안에만 존재하므로 다른 곳에서 참조하면 오류가 발생한다.
3.4.3 let
- ES6에서 새로운 키워드 let이 채택됐다. 키워드 let은 선언된 변수를 둘러싼 아무 블록의 스코프에 붙인다. 바꿔 말해, 명시적이진 않지만 let은 선언한 변수를 위해 해당 블록 스코프를 이용한다고도 말할 수 있다.
- let을 사용한 선언문은 속하는 스코프에서 호이스팅 효과를 받지 않는다. 따라서 let으로 선언된 변수는 실제 선언문 전에는 명백하게 존재하지 않는다.
가비지 콜렉션 (Garbage Collection)
function process(data) {
//do something
}
{
let someReallyBigData = {}
process(someReallyBigData)
}
- 위 예에서 처럼 명시적으로 블록을 선언하여 변수의 영역을 한정 지어서 많은 메모리를 먹는 자료구조인 someReallyBigData를 수거하도록 할 수도 있다.
let 반복문
- let은 for 반복문에서 특히 유용하게 사용할 수 있다.
- let은 i를 반복문에 묶고, 전역 변수를 생성하지 않고 해당 스코프에서만 동작한다.
for (let i = 0; i < 10; i++) {
console.log(i)
}
console.log(i) // ReferenceError
3.4.4 const
- const 역시 블록 스코프를 생성하지만, 선언된 값은 고정된다. 즉, 상수로 선언하는 것이다.
'JavaScript' 카테고리의 다른 글
[You don`t Know JS 정리] ch.3 객체 (0) | 2020.08.16 |
---|---|
[You don`t Know JS 정리] ch.1 this란 무엇인가 (0) | 2020.08.15 |
[You don`t Know JS 정리] ch.1 스코프란 무엇인가 (0) | 2020.08.13 |
[You don`t Know JS 정리] ch.5 문법 (0) | 2020.08.12 |
[You don`t Know JS 정리] ch.4 강제변환 (0) | 2020.08.10 |