Skip to content

 You Don't Know JS Scope & Closures

Yongkwan Lim edited this page May 2, 2019 · 2 revisions

넌 JS를 모른다: 스코프 & 클로저

원문:  You Don't Know JS Scope & Closures

제 2장에서 경험한 바와 같이, 스코프는 식별자(변수, 함수)가 선언 된 컨테이너 또는 버킷으로 동작하는 일련의 "버블"(bubble)로 구성된다. 이 버블들은 서로의 내부에 깔끔히 자리잡고 있고, 이 중첩은 작성자 시간에 정의된다.

하지만 정확히 무엇이 새로운 버블을 만들까? 함수만 만들 수 있을까? Javascript의 다른 구조가 스코프 버블을 만들 수 있을까?

함수의 스코프

이러한 질문에 대한 가장 일반적인 대답은 Javascript가 함수 기반 스코프를 가지고 있다는 것이다. 선언한 각 함수는 자체적으로 버블을 생성하지만 다른 구조는 자체 스코프 버블을 생성하지 않는다. 조금만 더 보면 알 수 있듯이, 이것은 사실이 아니다.

첫째로 함수 스코프를 살펴보고 구현해 보자.

아래 코드를 고민해 보자:

function foo(a) {
  var b = 2;
  // some code
  function bar() {
    // ...
  }
  // more code
  var c = 3;
}

이 스니펫에서, foo(...)함수의 스코프 버블은 식별자 a,b,c 그리고 bar를 포함한다. 선언(declaration)이 스코프의 어느 위치에 나타나든, 변수나 함수는 해당 스코프 버블에 속한다. 다음 장에서 이것이 정확히 어떻게 작동하는지 알아볼 것이다.

bar(...)는 자체 스코프 버블을 가진다. 그리고 글로벌 스코프는 foo라는 하나의 식별자만 가진다.

a,b,c, barfoo(...) 함수의 스코프 버블에 속하기 때문에, foo(...) 밖에서는 해당 식별자에 접근할 수 없다. 따라서 해당 식별자들은 글로벌 스코프에서 사용이 불가능 하기 때문에 아래의 코드는 ReferenceError가 발생한다.

bar(); // fails
 
console.log( a, b, c ); // all 3 fail

그러나, 이 식별자들(a,b,c, bar)은 foo(...)함수 내부에서 접근이 가능하고, 또한 bar(...)함수에서도 접근할 수 있다. ( bar(...) 함수 내부에 그림자 식별자가 없는 가정하에)

그림자 식별자(shadow identifier)

var foo; // The outer one
function example() {
 var foo; // The inner one -- this "shadows" the outer one, making the
           // outer one inaccessible within this function
  // ...
}

외부의 선언된 식별자 명과 내부에 선언된 식별자 명이 동일할 때, 내부에 선언된 해당 식별자를 그림자 식별자라고 한다.

함수 스코프는 모든 변수가 함수에 속한다는 개념을 가진다. 따라서 함수 전체에서 사용/재사용이 가능하다.(실제로 중첩 된 스코프에서도 접근 할 수 있음). 이는 매우 유용하기에, Javascript 변수의 "동적(dynamic)" 특성을 최대한 활용하여 필요에 따라 다른 유형의 값을 취할 수 있다.

반면, 주의하지 않으면 스코프 전체에 존재하는 변수가 예상치 못한 함정을 초래할 수 있다.

기본 스코프에서의 숨김 (Hiding In Plain Scope)

함수에 대한 전통적인 사고 방식은 함수를 선언한 다음 그 안에 코드를 추가하는 것이다. 그러나 반대로 생각해도 강력하고 유용하다. 작성한 코드의 임의의 부분을 가져와서 함수 선언으로 감싸면, 사실상 코드를 "숨기는 것"이다.

실제 결과는 해당 코드 주위에 스코프 버블을 만드는 것이며, 이는 해당 코드의 모든 선언(변수 또는 함수)이 이전 스코프가 아닌 새롭게 감싼 함수의 스코프와 결합됨을 의미한다. 다른 말로 표현하자면 변수와 함수를 함수의 스코프로 둘러싸서 "숨기기" 할 수 있다.

변수와 함수를 숨기는 것이 왜 유용할까?

스코프-기반의 숨김을 사용하는데에는 여러 가지 이유가 있다. 소프트웨어 디자인 원칙 "최소 특권의 원칙"[최저 권한]의 이유인데, "최소 권한"또는 "최소 노출"이라고도 한다. 이 원칙은 모듈/객체를 위한 API 처럼 소프트웨어 설계에서 최소한 필요한 것만 노출하고 다른 모든 것을 "숨겨야"한다고 명시한다.

이 원칙은 변수와 함수를 포함할 스코프를 선택하는 데까지 확장된다. 모든 변수와 함수가 전역 스코프에 있다면, 어떤 중첩된 스코프 범위에서도 접근할 수 있을 것이다. 하지만 이건 비공개로 유지되어야할 많은 변수/함수를 노출하므로, 변수/함수가 올바르게 사용되는 것을 저해하기에 "최저 원칙"을 위반한다.

예를 들어:

function doSomething(a) {
  b = a + doSomethingElse( a * 2 );
 
  console.log( b * 3 );
}
 
function doSomethingElse(a) {
  return a - 1;
}
 
var b;
doSomething( 2 ); // 15

이 스니펫에서, doSomething(...) 함수는 변수 bdoSomethingElse(...) 함수를 사용하여 동작에 대한 세부사항을 "비공개" 처리한 것 처럼 보인다. 그러나 주어진 스코프에서 bdoSomethingElse(...) 에 "접근"은 불필요할 뿐만아니라 "위험"하다. 이것들은 예상치 않은 방법이거나 의도적으로 사용될수 있다는 점에서, doSomething(...)의 사전 조건 가정을 위반할 수 있다.

보다 "적절한" 디자인은 다음과 같은 doSomething(...)의 스코프 내에서 코드를 숨길 것이다.:

function doSomething(a) {
  function doSomethingElse(a) {
    return a - 1;
  }
 
  var b;
 
  b = a + doSomethingElse( a * 2 );
 
  console.log( b * 3 );
}
 
doSomething( 2 ); // 15

이제, bdoSomethingElse(...) 는 바깥에서 접근할 수 없고, 오직 doSomething(...)에 의해서만 제어된다. 기능성과 최종 결과는 변함없지만, 소프트웨어 설계는 코드를 private하게 유지하므로, 보통 더 나은 소프트웨어로 간주된다.

충돌 방지

스코프 내에서 변수와 기능을 "숨기는" 또 다른 장점은 이름이 같지만 용도가 다른 두 개의 다른 식별자 사이에 의도하지 않은 충돌을 방지하는 것이다. 충돌은 종종 예상치 못한 값의 덮어쓰기를 초래한다.

예를 들어:

function foo() {
  function bar(a) {
    i = 3; // changing the `i` in the enclosing scope's for-loop
    console.log( a + i );
  }
 
  for (var i=0; i<10; i++) {
    bar( i * 2 ); // oops, infinite loop ahead!
  }
}
 
foo();

bar(...) 함수 내의 i = 3으로 할당한 값은 foo(...)함수 내의 for-loop에 선언 되었던 i을 덮어 쓴다. 이 경우, i는 항상 3으로 고정되고 평생 <10 로 남기에 무한 루프에 빠진다.

bar(...) 함수 내의 할당은 식별자의 이름에 신경쓰지 않도록 로컬 변수로 선언할 필요가 있다. var i = 3으로 로컬 변수로 선언한다면 문제는 해결된다. (이전에 언급한 "그림자 변수" 선언을 작성하는 것이다.) 추가로 다른 방법은 var j = 3과 같이 아예 다른 변수 명을 선택하는 것이다. 그러나 소프트웨어 설계에서는 당연히 동일한 식별자 이름을 사용할 수 있으므로 스코프를 활용하여 내부 선언을 "숨기는" 것이 최선의 선택이다.

전역 "네임스페이스" (Global "Namespaces")

특히 변수의 충돌은 주로 글로벌 스코프에서 발생한다. 프로그램에 로드 된 여러 라이브러리는 내부/private 함수 및 변수를 제대로 숨기지 않으면 서로 쉽게 충돌할 수 있다.

그러한 라이브러리는 일반적으로 글로벌 스코프에서 충돌할 위험이 없는 고유한 이름을 가진 단일 변수 선언(객체)을 생성한다. 그런 다음 객체는 해당 라이브러리의 "네임스페이스"로 사용되며, 여기서 모든 기능의 노출은 해당 객체(네임스페이스)의 속성으로 이루어진다.

예제:

var MyReallyCoolLibrary = {
  awesome: "stuff",
  doSomething: function() {
    // ...
  },
  doAnotherThing: function() {
    // ...
  }
};

모듈 관리

충돌 회피의 또 다른 방법은 다양한 의존성 관리자를 이용한 "모듈"을 사용하는 것이다. 이러한 의존성 관리자를 이용하면, 그 어떤 라이브러리도 글로벌 스코프에 식별자를 추가하지 않는대신 종속성 관리자의 다양한 메커니즘을 사용하여 식별자를 다른 특정 스코프에 명시적으로 가져오도록 해야 한다. (import)

모듈 관리자는 렉시컬 스코프 규칙에서 면책되는 신기한 기능이 없음을 알아야한다. 단지 여기에 설명 된 스코프 지정 규칙을 사용하여 식별자가 공유된 스코프에 주입되지 않도록하고, 충돌하지 않는 private 스코프에 보관하여 스코프 충돌을 방지한다.

따라서, 의존성 관리자를 사용하지 않고 사용하지 않고 방어적인 코드를 작성하면 동일한 결과를 얻을 수 있다. 모듈 패턴에 대한 자세한 내용은 5 장을 참조하라.

스코프로서의 함수

우리는 코드 주위에 함수를 감싸서, 함수의 내부 스코프 안에 있는 변수 또는 함수 선언을 외부 스코프에서 접근하지 못하도록 효과적으로 "숨긴다"

var a = 2;
 
function foo() { // <-- insert this
  var a = 3;
  console.log( a ); // 3
} // <-- and this
foo(); // <-- and this
 
console.log( a ); // 2

이 테크닉은 "작동" 하지만 이상적인 것은 아니다. 몇 가지 문제가 있다. 첫 번째는 이름이 있는 함수 foo()를 선언해야한다는 것이다. 즉, 식별자 이름 foo 자체가 둘러싸는 스코프(이 경우 글로벌)를 "오염"시킨다는 것을 의미한다. 또한 둘러쌓여진 코드가 실제로 실행되도록 foo() 으로 함수를 명시적으로 호출해야 한다.

함수에 이름이 필요하지 않으면 (또는 이름이 둘러쌓인 스코프를 오염시키지 않으면) 함수가 자동으로 실행될 수 있는게 이상적이다.

다행히도, 자바스크립트는 두 가지 문제에 대한 해결책을 제시한다.

var a = 2;
 
(function foo(){ // <-- insert this
  var a = 3;
  console.log( a ); // 3
})(); // <-- and this
 
console.log( a ); // 2

여기서 일어나는 일을 쪼개보자.

첫째, (function...로 시작되는 감싸진 함수 선언문은, 사소한 변경 처럼 보일수도 있지만 실제로는 큰 변화이다. 함수를 표준 선언으로 처리하는 대신 함수는 함수 표현식으로 처리된다.

참고: 선언(declaration)과 표현(expression)을 구별하는 가장 쉬운 방법은 문장에서 "function"이라는 단어의 위치이다. 만약 "function"이 문장에서 가장 먼저 나온 것이라면, 그것은 함수 선언식이다. 그렇지 않으면 함수 표현식이다.

함수 선언과 함수 표현식 사이의 주요 차이점은 그 이름이 식별자로 묶인 곳과 관련이 있다.

이전 두 개의 코드 예제를 비교해 보자. 첫 번째 예제에선 foo라는 이름이 스코프에 묶여 있으며, foo()로 직접 호출한다. 두 번째 예제에선 foo라는 이름이 스코프에 묶이지 않고, 그 대신 자신의 함수 내부에만 묶인다.

즉, 표현식(function foo(){ .. })은 식별자 foo가 외부 스코프가가 아닌 ..가 나타내는 스코프에서만 발견 됨을 의미합니다. 그 안에 foo라는 이름을 숨기면 불필요하게 둘러싸는 스코프를 오염시키지 않는다. (foo는 외부 스코프에서 접근할 수 없다는 뜻)

익명 vs 기명 (Anonymous vs. Named)

콜백 파라미터로 사용되는 함수 표현식에 가장 익숙할 것이다.

setTimeout( function(){
  console.log("I waited 1 second!");
}, 1000 );

fucntion()에는 이름 식별자가 없기 때문에 이것을 "익명 함수 표현식(anonymous function expression)"이라고 부른다. 함수 표현은 익명이 될 수 있지만 함수 선언엔 이름을 빠뜨릴 수 없다.

익명 함수 표현식은 빠르고 타자가 쉬우며, 많은 라이브러리는 이러한 관용적인 코드 스타일을 장려하는 경향이 있다. 그러나 다음과 같은 몇 가지 단점을 고려해야 한다.

  1. 익명 함수에는 stack trace에 표시할 이름이 없으므로 디버깅을 더욱 어렵게 만들 수 있다.
  2. 이름 없는 함수가 자체, 재귀 등을 위해 참조해야 하는 경우, 유감스럽게도 더 이상 사용되지 않는 arguments.callee 참조가 필요하다. 자가 참조가 필요한 또 다른 예는 이벤트 핸들러 함수가 실행된 후 스스로 unbind 되기를 원할 때 이다.
  3. 함수의 이름은 코드를 이해하는데에 도움을 주고, 문서화하는 데 도움이된다.

인라인 함수 표현식은 강력하고 유용하다. 함수 표현에 대한 이름을 제공하면 이러한 모든 단점을 효과적으로 해결할 수 있고 뚜렷한 단점도 없다. 가장 좋은 방법은 항상 함수 표현식에 이름을 지정하는 것이다.

setTimeout(function timeoutHandler(){ // <-- Look, I have a name!
  console.log( "I waited 1 second!" );
}, 1000 );

즉시 실행 함수표현식

var a = 2;
 
(function foo(){
  var a = 3;
  console.log( a ); // 3
})();
 
console.log( a ); // 2

이제 우리는 ( )의 쌍으로 함수를 감싸는 표현식을 알기 때문에, (function foo(){ .. })()와 같이 표현식 마지막에 또다른 ( )를 추가하여 함수를 실행할 수 있다. 첫번째로 함수를 감싸는 ( )는 함수 표현식을 만들고, 두번째 ( )는 함수를 실행한다.

이 패턴은 매우 일반적이다. 줄여서 IIFE라고도 부른다. (Immediately Invoked Function Expression.)

물론, IIFE는 이름이 필요하지 않다. 가장 일반적인 형태의 IIFE는 익명 함수 표현식을 사용하는 것이다. 하지만 기명 IIFE는 익명 함수 표현보다 앞서 언급한 모든 이점을 가지고 있으므로 사용하는 것을 추천한다.

var a = 2;
 
(function IIFE(){
  var a = 3;
  console.log( a ); // 3
})();
 
console.log( a ); // 2

전통적인 IIFE 형식(function(){ .. }())엔 약간의 다른 점이 있는데, 일부는 이를 선호한다. 그 차이를 자세히 보아라. 첫 번째 형태에서는 함수 식을 ( )로 감싼 다음, 마지막 호출을 위한 ( ) 쌍이 바깥쪽에 있다. 하지만 전통적인 IIFE 형식의 호출을 위한 ( ) 쌍은 내부에 존재한다.

이 두 가지 형태는 기능 면에서 동일하다. 순전히 선호하는 방식에 따라 다르다.

IIFE의 또 다른 바리에이션은 함수 호출과 동시에 arguments(매개변수)를 전달할 수 있다는 것이다.

var a = 2;
 
(function IIFE( global ){
  var a = 3;
  console.log( a ); // 3
  console.log( global.a ); // 2
})( window );
 
console.log( a ); // 2

window 참조 객체를 전달하지만, 매개변수 이름을 global 지정하여 글로벌 참조와 비 글로벌 참조에 대한 명확한 스타일 적 설명을 제공한다. 물론, 원하는 주변 스코프에서 어떤 것이든 전달할 수 있고, 다른 이름을 할당할 수도 있다.

이 패턴은 기본 undefined 식별자에 값을 잘못 덮어 썼을때 예기치 않은 결과를 초래할 수 있다 우려를 해결한다 . 매개 변수의 이름을 지정 undefined 하지만 해당 인수에 값을 전달하지 않으면 undefined 코드 블록에서 식별자가 실제로 정의되지 않은 값이라는 것을 보장 할 수 있다.

undefined = true; // setting a land-mine for other code! avoid!
(function IIFE( undefined ){
	var a;
	if (a === undefined) {
		console.log( "Undefined is safe here!" );
	}
})();

IIFE의 또 다른 변화는 순서에 관한 것이다. 함수를 매개 변수로 전달 한 뒤 실행을 시키는 것이다. 이 패턴은 UMD(Universal Module Definition) 프로젝트에서 사용된다. 어떤 사람들은 조금 더 복잡하지만 이해하는 것이 좀 더 편하다고 생각한다.

var a = 2;
 
(function IIFE( def ){
	def( window );
})(function def( global ){
	var a = 3;
	console.log( a ); // 3
	console.log( global.a ); // 2
});

def 함수 표현식은 스니펫의 후반에서 정의되며, IIFE 함수의 매개 변수로 전달된다. (def) 마지막으로 매개변수 def(함수)가 호출되어 windowdef 함수global매개변수 로 전달 된다.

블록 스코프

함수가 가장 공통적인 스코프의 단위이며, 대다수의 JS의 디자인 방식 중 가장 널리 보급되어 있지만, 다른 스코프 단위를 사용하면 더 나은 결과를 얻을 수 있고, 코드를 유지보수 하기가 쉽다.

자바스크립트가 아닌 많은 언어가 블록 스코프를 지원하므로, 이러한 언어의 개발자들은 사고방식에 익숙하지만, 자바스크립트에서만 주로 사용하는 개발자는 그 개념이 약간 어색할 수 있다.

그러나 블록 스코프 방식으로 코드를 작성한 적이 없더라도 자바스크립트에서 매우 일반적으로 발견할 수 있다,

for (var i=0; i<10; i++) {
  console.log( i );
}

for-loop 문의 머리 부분에 변수 i를 직접적으로 선언했다. 왜냐하면 우리의 의도는 for-loop의 컨텍스트 내에서만 i를 사용해야 하고, 변수가 실제로 해당 범위 (함수 또는 전역)로 자신을 범위 지정한다는 사실을 본질적으로 무시하기 때문이다.

이것이 바로 블록 스코프의 전부이다. 가능한 사용할 곳으로 부터 가깝거나 로컬 변수를 선언하라. 다른 예시:

var foo = true;
 
if (foo) {
  var bar = foo * 2;
  bar = something( bar );
  console.log( bar );
}

if 문의 컨텍스트에서만 bar 변수를 사용하기 때문에 if 블록 내에서 bar 변수를 선언 할 수 있습니다. 그러나 bar 사용하여 변수를 선언하는 위치는 상관이 없다. 왜냐하면 항상 변수가 감싸여진 스코프에 속하기 때문이다. 이 스니펫은 기본적으로 "가짜" 블럭 스코프이며 변수 bar를 다른 장소에서 사용하지 않기 떄문이다.

블록 스코프는 코드 상의 블록 안에서 정보를 숨기는 것이며 함수내에서 정보를 숨기는 것이다. 즉 이전의 "최소 노출 원칙"을 확장시키는 도구이다.

for-loop 예제를 다시한 번 보자.

for (var i=0; i<10; i++) {
  console.log( i );
}

i 변수는 오직 for-loop 문에서만 사용하면서 왜 함수의 전체 스코프를 오염시킬까?

그러나 더 중요한 것은 개발자가 바깥 스코프에서 의도하지 않은 변수를 (재)사용하여 실수로 (다시) 자신 을 확인 하는 것이다. 예를 들어 잘못된 위치에서 알수없는 변수를 사용하려고하면 오류가 발생한다. i 변수에 대한 블록 스코프 지정은 i가 for-loop 문에서만 사용 가능하게 되어 함수의 다른 곳에서 i 변수에 접근하는 경우 오류가 발생한다. 이는 변수가 혼란 스럽거나 유지하기 힘든 방식으로 재사용되지 않도록 보장한다.

하지만 슬픈 현실은 표면적으로는 자바스크립트에 블록 스코프가 없다는 것이다.

그러니까, 조금 더 알아보기 전까지 말이다.

with

우리는 2장에서 with에 대해 배웠다. with는 블록 스코프의 한 예이다. 객체에서 생성된 스코프는 with 문의 생명주기에서만 존재하며, 주변 스코프에는 존재하지 않는다.

try/catch

try/catch매우 ES3의 자바스크립트에서 알려진 사실이다. try/catch문의 catch안에 선언된 변수는 catch의 블록 스코프안에만 존재한다.

예를 들면:

try {
  undefined(); // illegal operation to force an exception!
}
catch (err) {
  console.log( err ); // works!
}
console.log( err ); // ReferenceError: `err` not found

보다시피, errcatch문에만 존재하며 다른 곳에서 참조하려고하면 오류가 발생한다.

참고 : 사실상 모든 표준 JS 환경 (예전의 IE 제외)에서는 동작하지만, 많은 문법 검사기들이 catch문을 동일한 스코프에 두 개 또는 그 이상을 만들면 각각 동일한 이름으로 error 변수를 선언 하면 많은 lint 경고를 발생시킨다. 변수가 안전하게 블록 스코프 안에 있기 때문에, 이것은 실제로 충돌할 일이 없어 보이지만, 문법 검사기는 여전히 성가신 듯이 사실에 대해 경고를 출력한다.

이러한 불필요한 경고를 방지하기 위해 일부 개발자들은 catch 변수에다 err1, err2등 새로운 이름을 부여한다. 어떤 개발자들은 그냥 중복된 변수 이름을 검사하는 린트 체크를 꺼버린다.

catch의 블록 스코프는 쓸모없는 것처럼 보일 수 있지만, 유용성에 대한 자세한 정보는 부록 B를 참조하라.

let

지금까지 자바스크립트는 블록 스코프 기능을 노출하는 이상한 틈새 동작만을 포함한다는 것을 알았다. 오랜 세월 동안 자바스크립트가 가진 전부 였다면, 블록 스코프는 자바스크립트 개발자에게 유용하지 않았을 것이다.

다행스럽게도 ES6에서는 이를 변경하고 변수를 선언하는 var대신 또 다른 방법 인 새 키워드 let를 도입한다.

let키워드는 어떤 블록 (일반적으로의 범위에 변수 선언 부착 { .. }이에 포함 된 것 쌍). 즉, let암시 적으로 그 변수 선언에 대한 모든 블록의 범위를 공중 납치.

let키워드는 변수 선언을 모든 블록 스코프에 첨부한다(일반적으로 { .. }). 즉, let은 변수 선언에 대한 블록의 스코프를 암묵적으로 탈취한다.

var foo = true;
 
if (foo) {
  let bar = foo * 2;
  bar = something( bar );
  console.log( bar );
}
 
console.log( bar ); // ReferenceError

let을 사용하여 기존 블록에 변수를 연결하는 것은 어느정도 내포된 것이다. 어떤 블록이 어떤 변수에 스코프 처리를 하였는지 세심한 주의를 기울이지 않으면 코드를 개발할때 블록을 옮기거나 다른 블록으로 감싸는 경우가 있다.

블록 스코프 지정을 위한 명시적 블록을 생성하면 이러한 우려 중 일부를 해결할 수 있어 변수가 어디에 연결되는지 여부가 더욱 명확해진다. 일반적으로 명시적 코드는 암묵적 코드나 미묘한 코드보다 바람직하다. 이러한 명시적인 블록 스코프 스타일은 보기가 쉽고, 다른 언어로 블록 스코핑이 작동하는 방식과 더 자연스럽게 어울린다.

var foo = true;
 
if (foo) {
  { // <-- explicit block
    let bar = foo * 2;
    bar = something( bar );
    console.log( bar );
  }
}
 
console.log( bar ); // ReferenceError

문장이 유효한 문법인 곳이면 어디에서나 { .. } 쌍을 포함시킴으로써 let을 이용해 바인딩을 하기 위한 임의의 블록을 만들 수 있다. 이 예제의 경우, if-statement 내부에 명시적인 블록을 만들었다.

참고: 명시적인 블록 범위를 표현하는 다른 방법은 부록 B를 참조하라.

4장에서는 호이스팅을 설명할 예정이다. 호이스팅은 호이스팅이 발생하는 전체 스코프에 대해 존재하는 선언에 대해 이야기한다.

그러나 let으로 선언한 내용은 그들이 나타나는 블록의 전체 스코프에 호이스팅 되지 않는다. 선언문이 나올 때까지 블록에 "존재"하지 않을 것이다.

{
  console.log( bar ); // ReferenceError!
  let bar = 2;
}

Garbage Collection

블록 스코프 지정의 또 다른 이유는 메모리를 되찾기 위해 클로저 및 가비지 콜렉터와 관련이 있다. 여기선 간단히 설명하겠지만, 5장에서 자세히 설명한다.

function process(data) {
  // do something interesting
}
 
var someReallyBigData = { .. };
 
process( someReallyBigData );
 
var btn = document.getElementById( "my_button" );
 
btn.addEventListener( "click", function click(evt){
  console.log("button clicked");
}, /*capturingPhase=*/false );

클릭 핸들러의 콜백인 click 함수는 someReallyBigData 변수가 필요 없다. 그 말인 즉슨 process(..)실행 후 큰 메모리가 필요한 데이터 구조는 가비지 컬렉터에 의해 회수 될 수 있다. 그러나 JS 엔진은 구조 click 함수가 전체 스코프에서 존재하기 때문에 여전히 구조를 유지해야 할 가능성이 매우 높다. (구현에 따라 다름) .

블록 스코프는 이러한 우려를 해결할 수 있으므로 엔진에 someReallyBigData를 주변에 유지할 필요가 없다는 것을 분명히 한다.

function process(data) {
  // do something interesting
}
 
// anything declared inside this block can go away after!
{
  let someReallyBigData = { .. };
  process( someReallyBigData );
}
 
var btn = document.getElementById( "my_button" );
 
btn.addEventListener( "click", function click(evt){
  console.log("button clicked");
}, /*capturingPhase=*/false );

변수를 명시 적으로 블록에 선언하면 좋다.

let Loops

let은 이전에 논의한 것처럼 for-loop 경우에 강력하다.

for (let i=0; i<10; i++) {
  console.log( i );
}
 
console.log( i ); // ReferenceError

for-loop 헤더에 있는 let은 for-loop 바디에 있는 i바인딩 할 뿐만 아니라, 각 루프의 반복 때마다 재-바인딩을 한다. 따라서 이전 루프의 반복 종료 후 값을 재할당하도록 해야한다.

{
  let j;
  for (j=0; j<10; j++) {
    let i = j; // re-bound for each iteration!
    console.log( i );
  }
}

이 반복 바인딩이 흥미로운 이유는 5장 클로저를 논의 할 때에서 명확해질 것이다.

let선언이 바깥 쪽 함수의 스코프(또는 글로벌)가 아니라 블록에 연결되기 때문에, 기존의 var 선언의 함수 스코프에 숨겨진 의존이 경우, varlet으로 교체하는 것에 대해 추가 리팩토링이 필요할 수도 있다.

var foo = true, baz = 10;
 
if (foo) {
  var bar = 3;
 
  if (baz > bar) {
    console.log( baz );
  }
 
  // ...
}

이 코드는 다음과 같이 쉽게 다시 리팩토링된다.

var foo = true, baz = 10;
 
if (foo) {
  var bar = 3;
 
  // ...
}
 
if (baz > bar) {
  console.log( baz );
}

그러나 블록 스코프 변수를 사용할 때 이러한 변경 사항을 주의해야한다.

var foo = true, baz = 10;
 
if (foo) {
  let bar = 3;
 
  if (baz > bar) { // <-- don't forget `bar` when moving!
    console.log( baz );
  }
}

이러한 시나리오에서 보다 견고한 유지 보수/리팩토링 코드를 보다 쉽게 ​​제공할 수 있는 블록 스코프 지정의 대체 스타일은 부록 B를 참조하라.

const

let과 함꼐 ES6에서 소개된 const 역시 블록 스코프 변수를 만들지만, 그 값은 고정되어 있다(상수). 나중에 이 값을 변경하려고하면 오류가 발생한다.

var foo = true;
 
if (foo) {
	var a = 2;
	const b = 3; // block-scoped to the containing `if`
 
	a = 3; // just fine!
	b = 4; // error!
}
 
console.log( a ); // 3
console.log( b ); // ReferenceError!

요약

함수는 JavaScript에서 가장 일반적인 스코프의 단위이다. 다른 함수 내에서 선언 된 변수와 함수는 근본적으로 좋은 소프트웨어의 의도적 인 설계 원리인 스코프에서 "숨겨"진다.

그러나 함수는 결코 유일한 스코프의 단위가 아니다. 블록 스코프는 변수와 함수가 둘러쌓여진 함수가 아닌 임의의 블록 (일반적으로 { .. } 쌍)에 속할 수 있다는 개념을 나타낸다 .

ES3부터 try/catch구조체는 catch절에 블록 스코프를 가진다.

ES6에서는 임의의 코드 블록에서 변수를 선언 할 수 있도록 let키워드 (var와 유사한)가 도입되었다. if (..) { let a = 2; }는 변수 a를 선언하고 if{ .. }블록에 연결 할 것이다.

일부는 그렇게 믿는 것처럼 보이지만 블록 스코프를 var 함수 스코프를 완전히 대체 한 것으로 받아 들여서는 안된다 . 두 기능이 공존하며, 개발자는 더 나은,보다 읽기 쉽고/유지 보수가 가능한 코드를 생성하기에 적절한 각각의 경우, 함수 스코프와 블록 스코프 기술을 모두 사용할 수 있어야한다.

Clone this wiki locally