변수 선언 방법에 따라 스코프가 달라집니다.
스코프는 변수가 유효한 범위를 의미한다고 볼 수 있으며, 참조가 가능한지 판단하는 기준이 됩니다.
스코프는 다음과 같이 분류됩니다.
대부분의 프로그래밍 언어는 코드 블록 단위({ }) 를 지역으로 보는
블록 레벨 스코프 를 사용했는데, 자바스크립트는 특이하게 함수 레벨 스코프
(함수 내의 변수가 함수 외부에서 유효하지 ❌) 를 따르며 선언자 var 를 사용했습니다.
그래서 지역이 블록이 아닌 함수로 결정되다보니 if문이나 반복문에서 선언한 변수를
그 밖에서도 사용할 수 있었죠.
그러나 ES6 부터 블록 레벨 스코프 를 따르는 선언자 let 과 const 를 지원합니다.
let, const 로 선언된 변수는 블록 레벨 스코프입니다.
즉, 코드 블록 { } 안에서 선언한 변수는 그 내부에서만 사용할 수 있습니다.
let, const 는 블록 범위{
let message = "안녕하세요."; // 블록 내에서만 변숫값을 얻을 수 있음
alert(message); // 안녕하세요.
}
{
// 위의 message 와는 연관 ❌
// 블록 {} 이 끝나는 지점에서, 위 블록의 message 는 그 외에 영향을 끼치지 못함
let message = "안녕히 가세요.";
alert(message);
}
// 블록 내부의 변수를 외부에서 탐지 ❌
alert(message); // ReferenceError: message is not defined
var 로 선언한 변수와 함수 선언문으로 선언한 함수 는 함수 레벨 스코프입니다.
let 이나 const 처럼 블록을 기준으로 스코프를 생성하지 않습니다.
함수 레벨 스코프는 함수 내의 식별자가 함수 내부에서만 사용가능함을 의미합니다.
함수가 아닌 그냥 블록 레벨 { } 에서 선언하면, 지역이 아니라 전역 변수가 되며
브라우저의 경우 window 객체의 프로퍼티가 됩니다.
따라서 조건문, 반복문 등의 블록에서 var 로 선언한 변수를 사용하면 의도와 다른 동작이 발생합니다.
if (true) {
var test = true;
}
alert(test); // true(if 문이 끝났어도 변수에 여전히 접근 가능)
function sayHi() {
if (true) {
var phrase = "Hello";
}
alert(phrase); // 제대로 출력됨
}
sayHi();
alert(phrase); // Error: phrase is not defined
자바스크립트 함수는 선언 위치에 따라 스코프를 결정하는데,
이를 렉시컬 스코프, 혹은 정적 스코프라고 합니다.
호출 위치에 따라 스코프를 동적으로 변경하는 게 아니라 선언 때 한 번 정해지면
그때의 환경을 계속 참고하기 때문에 정적 스코프인 것이죠.
따라서, 함수를 호출하는 위치는 함수의 스코프 결정에 영향을 주지 않습니다.
함수는 자신이 선언된 당시의 스코프를 기억하고 그걸 사용하기 때문이죠.
var x = 1;
function foo() {
var x = 10;
bar();
}
function bar() {
console.log(x);
}
foo(); // 1
bar(); // 1
위를 보면, foo 에서 호출한 bar 는 더 가까운 위치에 변수 x 가 있음에도
전역의 x 를 참조하는 것을 확인할 수 있습니다.
이는 bar 함수가 전역에서 선언되어 그 때의 스코프인 전역의 변수 x 를 참조하기 때문입니다.
실행 컨텍스트는 실행할 코드에 대한 세부 정보를 담고 있는 내부 데이터 구조로,
제어 흐름의 현재 위치, 변수의 현재 값, this 의 값 등 상세 내부 정보를 저장합니다.
코드가 실행되는 환경을 나타낸다고 볼 수 있으며, 실행하는 코드가 필요로 하는 정보를 저장합니다.
실행 컨텍스트는 두 가지로 분류됩니다.
global execution context: 스크립트 실행 시 기본적으로, 제일 먼저 생성function execution context: 함수 호출 시 생성❗실행 컨텍스트를 생성하는 건
전역 공간, eval, 함수, 모듈뿐입니다.
거의 안 쓰이는eval을 제외하면, 함수만이 실행 컨텍스트를 생성 한다고 보면 됩니다.
객체 리터럴, 블록 구문... 등은 실행 컨텍스트를 생성하지 않습니다.
대신 블록 구문은 렉시컬 환경을 정의하고 상위 실행 컨텍스트가 잠시 자신의 렉시컬 환경을 참조하도록 합니다. 상위 컨텍스트를 이용하여 동작하는 것이죠.
실행 컨텍스트는 실행 컨텍스트 스택이라는 자료구조에 저장합니다.
이는 콜 스택(call stack) 이라고도 부르며, 스택 자료구조이므로 선입후출 로 동작합니다.
기본적으로 스크립트를 실행하면 스택에 global execution context 를 저장하고,
그 외에 함수가 실행될 때 function execution context 를 글로벌 위에 저장합니다.
함수의 실행이 끝나면 함수의 실행 컨텍스트가 스택에서 제거되며, 스크립트가 끝나면 글로벌 실행 컨텍스트가 제거되고 프로그램이 마무리됩니다.
그러므로, 실행중인 함수의 실행 컨텍스트는 항상 스택의 최상위 에 위치하게 됩니다.
함수 호출 한 번 당 하나의 실행 컨텍스트를 생성하는데요, 재귀함수를 사용하면 반복적으로 생성된 실행 컨텍스트가 계속해서 콜 스택에 쌓이게 되겠죠.
따라서 너무 많은 재귀 함수 호출은 콜 스택의 최대 사이즈를 초과하게 되는 오류를 발생시킵니다.
실행 컨텍스트는 다음과 같은 정보를 가집니다.
this 에 대한 객체globe 의 경우 기본적으로 window 가 this 정리하면 다음과 같습니다.
왜 this 는 호출할 때마다 달라지지❓
이 의문에 대한 답도 여기서 얻을 수 있습니다.
함수가 호출될 때 실행 컨텍스트를 생성하고, 그와 함께 this 를 바인딩하기 때문입니다.
그래서 this 는 호출 시점에 함수와 연관된 객체로 결정되는 것이죠.
ES6 부터 나온 화살표 함수는 기존의 함수와 좀 다른 점이 있습니다.
바로 자체 바인딩을 하지 않는다는 것입니다. (this, arguments, super, new.target 모두 ❗)
❓ 화살표 함수와 this 바인딩
보통 함수는 호출과 함께 this 를 바인딩하지만, 화살표 함수는 그렇지 않습니다.
대신, 화살표 함수는 this 에 대한 참조를 렉시컬 스코핑으로 정한 외부 환경에 의존합니다.
렉시컬 스코핑에 의하면 화살표 함수의 외부 환경은 선언된 시점에 고정되죠?
이때 화살표 함수의 외부에 어떤 함수가 있었다면... 그 함수의 this 를 참조합니다.
객체 리터럴 메서드에 바로 화살표 함수를 할당하면 화살표 함수는 전역 컨텍스트를 참조하겠죠?
(왜냐하면 바로 상위인 객체 리터럴은 컨텍스트 ❌ 니까...)
그래서 this 가 window 가 나옵니다.
그러나, 메서드로 일반 함수를 주고 그 안에서 화살표 함수를 선언하여 호출한다면?
화살표 함수의 this 는 그 객체가 됩니다.
외부가 함수라서 컨텍스트를 가지기 때문이죠. 화살표 함수는 바로 그 외부를 참조할 수 있고,
그 외부 함수(상위 스크프의 실행 컨텍스트)의 this 는 객체입니다!
변수 환경 (VariableEnvironment) 과 렉시컬 환경 (LexicalEnvironment) 은 대상의 환경 레코드 (Environment Record) 를 식별해줍니다.
환경 레코드는 대상 내부의 변수나 함수에 대한 식별자 참조 정보를 담고 있습니다.
함수 선언, 블록 구문 등의 코드가 평가될 때 그 컨텍스트에서 선언된 식별자 정보를 기록하기 위해 환경 레코드가 생성됩니다.
환경 레코드가 모든 식별자에 대한 정보를 수집하여 기록해놓는 것이죠.
환경 레코드는 다음과 같은 정보를 담고 있습니다.
[[OuterEnv]] : 외부 환경 레코드에 대한 참조함수의 경우 [[ThisBindingStatus]] 라는 필드도 가지는데, 이 필드의 값이 lexical 이면 이 함수가 화살표 함수임을 의미하며 화살표 함수는 자체 this 가 없습니다.
렉시컬 환경은 식별자 정보와 외부 환경에 대한 참조를 지닌 객체입니다.
실행 중인 함수, 코드 블록 {}, 스크립트 전체는 렉시컬 환경을 가지게 됩니다.
렉시컬 환경은 다음으로 구성됩니다.
환경 레코드(environment record)외부 렉시컬 환경(outer lexical environment) 에 대한 참조스코프 체이닝(scope chaining) :스크립트가 시작되면 함수와 변수의 선언을 수집하여 기록합니다.
이 때, 환경 레코드에 기록된 변수는 특수 내부 상태를 가집니다.
스크립트 시작 당시 변수의 상태는 uninitialized 로, 엔진이 변수를 인지하긴 하지만 참조 불가능한 상태입니다.
변수 선언문 코드를 만나게 되면 상태가 undefined 로 바뀌고, 참조 가능한 상태가 됩니다.
이후 값이 할당되면 할당된 값을 참조합니다.
함수 표현식으로 쓰인 경우 변수처럼 취급하여 선언, 할당 이전에 참조 및 사용이 불가능합니다.
그러나 함수 선언문 으로 선언한 함수는 선언과 동시에 할당이 이뤄지므로,
선언이 수집됨과 동시에 값이 할당됩니다.
따라서 함수 선언문은 스크립트 실행시 함수가 바로 초기화되어 선언 코드 이전에도 사용 가능합니다.
uninitialized : 참조 불가능undefined : 참조 가능그래서 함수가 외부의 변수를 참고하려 할 때 이런 현상이 일어납니다.
let cat = 10;
function bar(){
alert(cat); // 외부의 cat 변수 참조
};
function foo(){
alert(cat); // (*)
let cat = 100;
alert(cat);
};
bar(); // 10
foo(); // Cannot access 'cat' before initialization 에러!
bar 함수는 전역의 cat 변수를 참조하지만 foo 는 그렇지 않습니다.
왜냐하면 foo 함수가 실행되며 환경 레코드가 내부의 식별자 정보를 다 수집하기 때문이죠.
이때 foo 의 내부에는 cat 이란 변수가 존재하네요.
따라서 foo 는 전역의 cat 이 아니라 자기가 갖고 있는 내부의 cat 을 사용합니다.
그런데 (*) 에선 아직 선언문 도달 전이라 uninitialized 상태입니다.
이러면 참조가 불가능하죠? 그래서 에러가 납니다.
이렇게 내부의 정보를 미리 다 수집하는 것이 호이스팅입니다.
선언문이 가장 앞으로 끌어당겨지는 듯한 현상을 호이스팅 이라고 합니다.
이는 렉시컬 환경이 생성되며 환경 레코드가 식별자 정보를 수집함에 따라 일어나는 현상입니다.
어떤 동작을 수행하기 전에 환경 레코드가 먼저 선언 정보를 수집하기 때문에
마치 선언문이 전부 가장 위에 위치한 것처럼 동작하죠.
그래서 함수 선언문으로 작성한 함수는 선언문 전에 사용할 수 있고,
var 로 선언한 변수를 선언 전에 참조할 수 있습니다.
let 이나 const 로 선언한 변수도 호이스팅은 되지만 var 과는 다르게 동작합니다.
var 로 선언한 변수는 undefined 상태지만, let 이나 const 로 선언한 변수는 uninitialized 상태로 존재합니다.
따라서, var 로 선언하면 선언문 전에 참조 가능(값은 undefined 가 나오지만...) 하고,
let 이나 const 로 선언하면 참조 불가능합니다.
function sayHi() {
alert(phrase); // undefined
var phrase; // 선언은 함수 시작 시 처리
phrase = "Hello"; // 할당은 실행 흐름이 해당 코드에 도달했을 때 처리
}
sayHi();
클로저는 함수와 함수가 선언되었을 당시의 맥락적 환경(렉시컬 환경)의 조합으로, 생성 시점의 유효 범위 내에 있는 모든 지역 변수로 구성됩니다.
간단히 말하면 클로저는 선언되었을 때의 렉시컬 환경 을 기억하고 있는 함수입니다.
생성 당시의 외부 변수를 기억하고, 그 변수에 접근 가능한 함수이죠.
함수는 숨김 프로퍼티인 [[Environment]] 를 이용하여 생성된 외부 환경을 기억하고 그에 접근합니다.
자바스크립트에선 몇 예외를 제외한 모든 함수가 클로저입니다.
외부 함수가 내부 함수를 반환한 후에 외부 함수가 종료되어 그 함수의 실행 컨텍스트가 콜 스택에서 사라져도, 내부 함수가 그 정보를 참조하는 한 외부 함수의 정보가 유효한 값으로 유지되어 참조 가능합니다.
즉, 외부 함수는 반환된 상태라도 이를 참조하는 내부 함수가 있는 한 외부 함수의 변수는 계속 유지됩니다. 이 때, 내부 함수가 참조하는 변수는 복사본이 아닌 실제 외부 함수의 변수입니다.
클로저는 전역 변수를 사용해야하는 상황에서 이를 외부 환경 변수로 대체하도록 만들 수 있다는 점에서 유용합니다.