본문 바로가기

JavaScript

[You don`t Know JS 정리] ch.2 렉시컬 스코프

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 역시 블록 스코프를 생성하지만, 선언된 값은 고정된다. 즉, 상수로 선언하는 것이다.