GUMI Dev
[JS Tutorial] 변수 - 호이스팅, 스코프체인, 클로저 본문
호이스팅(hoisting)
자바스크립트에서 호이스팅(hoisting)이란, 인터프리터가 변수와 함수의 메모리 공간을 선언 전에 미리 할당하는 것을 의미한다.
var로 선언한 변수의 경우 호이스팅 시 undefined로 변수를 초기화한다.
반면 let과 const로 선언한 변수의 경우 호이스팅 시 변수를 초기화하지 않는다.
호이스팅을 설명할 때는 주로 "변수의 선언과 초기화를 분리한 후, 선언만 코드의 최상단으로 옮기는" 것으로 말하곤 한다. 따라서 변수를 정의하는 코드보다 사용하는 코드가 앞서 등장할 수 있다. 다만 선언과 초기화를 함께 수행하는 경우, 선언 코드까지 실행해야 변수가 초기화된 상태가 됨을 주의하자.
함수 호이스팅
function 키워드로 선언한 모든 식별자도 호이스팅된다.
먼저 함수가 정의되는 방식 네 가지를 살펴보자
// 1. 함수 선언문
// 함수 이름은 생략 불가능
function gyumi(x, y) {
return x + y
}
// 2. 함수 표현식
// 함수 이름 생략 가능
var gyumi = function(x, y) {
return x + y
}
// 함수 이름 작성 시,
// var gyumi = function plus(x, y) {
// return x + y
// }
// 3. Function 생성자 함수
var gyumi = new Function('x', 'y', 'return x + y')
// 4. 화살표 함수
var gyumi = (x, y) => x + y
위에서 함수 선언문과 표현식의 호이스팅 결과를 보자.
// 함수 참조
console.dir(add) // output: f add(x, y)
console.dir(sub) // output: undefined
// 함수 호출
console.log(add(2, 5)) // output: 7
console.log(sub(2, 5)) // output: Uncaught TypeError: sub is not a function
// 함수 선언문
function add(x, y) {
return x + y
}
// 함수 표현식
var sub = function(x, y) {
return x + y
}
"함수 선언문"의 경우, 런타임 이전에 자바스크립트 엔진에서 먼저 실행되어, 함수 자체를 호이스팅 시킬 수 있다.
반면, "함수 표현식"은 변수 호이스팅과 같이, 런타임 이전에 해당 값을 undefined로 초기화만 시키고, 런타임에서 해당 함수 표현식이 할당되어 그때 객체가 된다.
자바스크립트는 함수의 코드를 실행하기 전에 함수 선언에 대한 메모리부터 할당한다.
덕분에 함수를 호출하는 코드를 함수 선언보다 앞서 배치할 수 있다.
// 함수 선언
function catName(name) {
console.log("제 고양이의 이름은 " + name + "입니다");
}
// 함수 호출
catName("호랑이")
/*
결과: "제 고양이의 이름은 호랑이입니다. "
*/
위 코드 조각은 일반적으로 코드를 작성하는 순서다.
이번에는 함수를 선언하기 전에 먼저 호출해보았다.
catName("클로이"); // 함수 호출
function catName(name) { // 함수 선언
console.log("제 고양이의 이름은 " + name + "입니다");
}
/*
결과: "제 고양이의 이름은 클로이입니다"
*/
함수 호출이 함수 자체보다 앞서 존재하지만, 그럼에도 불구하고 이 코드 역시 동작한다.
이것이 자바스크립트에서 실행 맥락이 동작하는 방식이다.
호이스팅은 다은 자료형과 변수에도 잘 작동한다. 변수를 선언하기 전에 먼저 초기화하고 사용할 수 있는 것이다.
* 무엇이든 그것의 "선언"만 호이스팅 대상이다.
자바스크립트는 "초기화"를 제외한 "선언"만 호이스팅한다.
변수를 먼저 사용하고 그 후에 선언 및 초기화가 나타나면, 사용하는 시점의 변수는 기본 초기화 상태
(var 선언시 undefined, 그 외에는 초기화하지 않음)이다.
console.log(num); // 호이스팅한 var 선언으로 인해 undefined 출력
var num; // 선언
num = 6; // 초기화
반면 아래 예제는 선언없이 초기화만 존재한다. 따라서 호이스팅도 없고, 변수를 읽으려는 시도에서는 ReferenceError 예외가 발생한다.
console.log(num); // ReferenceError
num = 6; // 초기화
다음은 호이스팅 예제다.
/* 예제 1 */
/* y만 호이스팅 대상 */
x = 1;
/* x 초기화. x를 선언하지 않은 경우 선언.
그러나 명령문에 var가 없으므로 호이스팅이 발생하지 않음 */
console.log(x + " " + y); // '1 undefined'
/* 자바스크립트는 선언만 호이스팅하므로, 윗줄의 y는 undefined */
vat y = 2;
/* y를 선언하고 초기화 */
/* 예제 2 */
/* 호이스팅은 없지만, 변수 초기화는 (아직 하지 않은 경우) 변수 선언까지 병행하므로
변수를 사용할 수 있음 */
a = '크랜'; // a 초기화
b = '베리'; // b 초기화
console.log(a + "" + b); // '크랜베리'
* var hoisting
변수 선언은 코드가 실행되기 전에 처리되기 때문에 코드의 아무 곳에서나 변수를 선언하는 것은 변수를 맨 위에서 선언하는 것과 같다. 이것은 또한 변수가 선언되기 전에 사용된 것처럼 보일 수 있음을 의미한다. 변수 선언이 함수 또는 전역 코드의 맨 위로 이동하는 것처럼 보이기 때문에 이 동작을 "호이스팅"이라고 한다.
bla = 2;
var bla;
// ...is implicitly understood as:
var bla;
bla = 2;
이러한 이유로 해당 범위의 맨 위(전체 코드의 맨 위와 함수 코드의 맨 위)에 변수를 선언하는 것이 좋다. 그러면 어떤 변수가 함수 범위(로컬)이고 어떤 변수가 * 스코프 체인(scope chain)에서 확인되는지 명확히 할 수 있다.
스코프
스코프(scope, 유효 범위)란 식별자(변수명, 함수명, 클래스명 등)의 유효범위를 뜻하며, 선언된 위치에 따라 유효범위가 달라진다. 전역에 선언된 변수는 전역 스코프를, 지역에 선언된 변수는 지역 스코프를 갖는다.
전역 변수는 어디에서든지 참조가 가능한 값이다. 반면, 지역 변수는 함수 몸체 내부를 말한다. 따라서 지역 변수는 자신의 지역 스코프와 그 하위 지역 스코프에서 유효하다.
※ 한 가지 주의해야 할 점은, 자바스크립트에서 모든 코드 블록(if, for, while, try/catch 등)이 지역 스코프를 만들며, 이러한 특성을 블록 레벨 스코프라 한다. 하지만 var 키워드로 선언된 변수는 오로지 함수의 코드 블록만을 지역 스코프로 인정한다. 이를 함수 레벨 스코프라 한다.
함수 레벨 스코프
자바스크립트에서 var는 블록 스코프(block scope)가 아닌 함수 스코프를 사용하므로, 함수 내에 정의된 변수는 해당 함수 내에서만 사용할 수 있고 유효하다.
함수 스코프라는 것은 함수 내에서 정의된 변수는 그 함수의 전체에 걸쳐서 유효하다는 뜻이다. 쉽게 설명하면, 스코프는 "유효 범위"라는 뜻이므로 함수 스코프는 선언된 함수 내에서라면 어디에서든 유효하다는 것이다. 이는 변수가 미처 선언되기도 전에 유효하다는 뜻이고, 이러한 현상을 호이스팅(hoisting)이라고 한다.
아래 예제에서 함수 f 내에 지역변수 scope이 선언되었으므로, 이 변수는 함수 몸체 내부의 맨 꼭대기로 끌어올려진다. 다만 할당까지 끌어올려지지는 않으므로, 윗 줄의 콘솔 로그는 undefined를 출력하고 마지막 줄의 콘솔 로그는 local을 출력한다.
var scope = "global";
function f() {
console.log(scope); // undefined
var scope = "local";
console.log(scope); // local
}
아래 예제에서는 함수가 아닌 곳에 var 키워드를 이용해 a를 선언했기 때문에 전역 변수로 취급한다.
기존에 있던 a 변수가 중복 선언되면서, 최하단의 console에서도 출력 값이 5로 바뀐 것을 확인할 수 있다.
해당 예제는 코드가 짧아 어디에서 문제가 일어났는지 알 수 있지만, 실무에서는 어려울 수 있다.
전역 변수로 인해 재할당이 발생하거나, 전역 스코프를 공유하기 때문에 어딘가 동일한 이름이 있다면 예상치 못한 결과를 가져올 수 있는 위험이 있다.
var a = 1
if (true) {
var a = 5
}
console.log(a) // output: 5
따라서 오로지 함수 코드 블록 만을 지역 스코프로 인정하는 var 대신, 블록 레벨 스코프를 지원하는 const와 let을 사용할 것을 권장한다.
var, let, const의 차이
앞서 발견한 var 키워드의 문제점은 크게 세 가지가 존재한다.
- 변수 중복 선언 가능하여, 예기치 못한 값을 반환할 수 있다.
- 함수 레벨 스코프로 인해 함수 외부에서 선언한 변수는 모두 전역 변수로 된다.
- 변수 선언문 이전에 변수를 참조하면 언제나 undefined 를 반환한다.
ES6에서 나온 let과 const 키워드는 위의 세 가지 문제점을 해결했다.
1. 변수 중복 선언 불가
1) let
let 키워드로는 변수 중복 선언이 불가하지만, 재할당은 가능하다.
// 선언과 할당
let name = 'angyumi'
console.log(name) // output: angyumi
// 중복 선언
let name = 'ang' // output: Uncaught SyntaxError: Identifier 'name' has already been declared
// 재할당
name = 'ang'
console.log(name) //output: ang
2) const
const가 let과 다른 점이 있다면, 반드시 선언과 초기화가 동시에 진행되어야 한다는 것이다.
const name; // output: Uncaught SyntaxError: Missing initializer in const declaration
const name = 'ang'
const도 let과 마찬가지로 재선언이 불가하며, 더 나아가 재할당도 불가하다.
재할당의 경우, 원시 값은 불가능하지만, 객체는 가능하다.
const 키워드는 재할당을 금지할 뿐, '불변'을 의미하지는 않는다.
// 원시값의 재할당
const name = 'ang'
name = 'angyumi' // output: Uncaught TypeError: Assignment to constant variable.
// 객체의 재할당
const name = {
eng: 'ang',
}
name.eng = 'angyumi'
console.log(name) // output: { eng: "angyumi" }
2. 블록 레벨 스코프
let, const 키워드로 선언한 변수는 모두 코드 블록(ex. 함수, if, for, while, try/catch 문 등)을 지역 스코프로 인정하는 블록 레벨 스코프를 따른다.
var a = 1
if (true) {
var a = 5
}
console.log(a) // output: 5
let a = 1
if (true) {
let a = 5
}
console.log(a) // output: 1
var 키워드로 선언한 경우 5가 나왔지만, let 키워드로 선언한 경우 if문 안에 있는 것은 지역 스코프를 가져 전역에서 console을 입력하면, 전역에 있는 a가 결과값으로 출력된다. (const 키워드도 let 키워드와 동일하게 동작한다. )
3. 변수 호이스팅
1) let
let 키워드로 선언한 변수는 선언 단계와 초기화 단계가 분리되어 진행된다. 즉, 런타임 이전에 자바스크립트 엔진에 의해 선언 단계가 실행되지만, 초기화 단계가 실행되지 않았을 때 해당 변수에 접근하려고 하면 참조 에러(Uncaught ReferenceError: name is not defined)가 뜬다.
console.log(name) // output: Uncaught ReferenceError: name is not defined
let name = 'ang'
따라서 let 키워드로 선언한 변수는 스코프의 시작 지점부터 초기화 단계 시작 지점까지 변수를 참조할 수 없는 일시적 사각지대(Temporal Dead Zone: TDZ) 구간에 존재한다.
2) const
const 키워드는 선언 단계와 초기화 단계가 동시에 진행된다.
console.log(name) // output: Uncaught ReferenceError: Cannot access 'name' before initialization
const name = 'ang'
let 키워드로 선언한 경우, 런타임 이전에 선언이 되어 자바스크립트 엔진에 이미 존재하지만 초기화되지 않았기 때문에
"Uncaught ReferenceError: name is not defined"라는 문구가 떴다. 하지만 const 키워드로 선언한 경우, 선언과 초기화가 동시에 이루어져야 하지만 런타임 이전에는 실행될 수 없다. 따라서 초기화가 진행되지 않은 상태이기 때문에 "Cannot access 'name' before initialization"라는 에러 문구가 뜬다.
기본적으로 변수의 스코프는 최대한 좁게 만드는 것을 권장한다. 따라서, var 키워드 보다는 let과 const 키워드를 사용하며, 변경하지 않는 값(상수)이라면 let 보다는 const 키워드를 사용하는 것이 안전하다.
스코프 체인
자바스크립트에서 전역 변수는 전역 객체의 프로퍼티다. (ECMAScript 명세에서 정의) 지역 변수는 그런 규정이 없지만, 변수를 각 함수 호출과 연관된 객체(call object)의 프로퍼티로 생각할 수 있다.
지역 변수를 어떤 객체의 프로퍼티로 생각한다면, 자바스크립트의 모든 코드는 스코프 체인을 가지고 있다.
스코프 체인은 해당 코드의 유효 범위(in scope) 안에 있는 변수를 정의하는 객체인 체인, 리스트다.
자바스크립트가 변수 값을 얻으려고 할 때 (variable resolution, 변수 해석) 스코프 체인에서 변수를 찾는다.
스코프 체인은 위에서 말했다시피 객체의 리스트이므로, 스코프 체인은 객체의 리스트이므로, 첫 번째 객체에서 해당 변수를 찾고 없으면 그 다음 객체에서 해당 변수를 찾고 또 없으면 그 다음 객체에서 찾는 식이다. 리스트의 끝까지 검색했는데도 불구하고 그 변서가 없다면 reference error가 발생한다.
최상위 자바스크립트 코드(어떠한 함수에도 속하지 않는 코드)의 스코프 체인에는 하나의 객체만 있고, 그것이 전역 객체다.
중첩되지 않은 함수의 스코프 체인은 2개의 객체로 이루어진다. 하나는 함수의 매개변수와 지역변수를 정의하는 객체고, 다른 하나는 전역 객체다.
함수가 "정의"될 때, 함수는 스코프 체인을 저장한다.
힘수가 "호출"될 때, 함수는 지역 변수를 보관하는 새로운 객체를 만들고 그 객체를 기존에 만들어둔 스코프 체인에 추가한다.
클로저
클로저란 함수와, 함수의 변수가 해석되는 스코프를 아울러 말한다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(lexical scoping)를 먼저 이해해야 한다.
* lexical scoping(어휘적 범위 지정)
lexical scoping(어휘적 범위 지정)이란, 함수가 정의된 시점의 스코프 체인을 사용하여 함수가 실행된다는 뜻이다. 여기서 함수가 호출된 시점이 아니라 정의된 시점이라는 것이 중요하다.
function init() {
var name = "Mozilla"; // name은 init에 의해 생성된 지역 변수이다.
function displayName() { // displayName()은 내부 함수이며, 클로저다.
alert(name); // 부모 함수에서 선언된 변수를 사용한다.
}
displayName();
}
init();
init()은 지역 변수 name과 함수 displayName()을 생성한다. displayName()은 init()안에 정의된 내부 함수이며 init() 함수 본문에서만 사용할 수 있다. 여기서 주의할 점은 displayName()내부에는 자신만의 지역 변수가 없다는 점이다. 그런데 함수 내부에서 외부 함수의 변수에 접근할 수 있기 때문에 displayName() 역시 부모 함수 init()에서 선언된 변수 name에 접근할 수 있다. 만약 displayName()가 자신만의 name 변수를 가지고 있었다면, name 대신 this.name을 사용했을 것이다.
function init() {
var name = "Mozilla"; // name 은 init에 의해 만들어진 지역 변수다.
function displayName() { // displayName() 은 내부 함수이며, 클로저다.
alert (name); // displayName()은 부모 함수에서 선언된 변수를 사용한다.
}
displayName();
}
init();
위 코드를 실행하면 displayName()함수 내의 alert()문이 부모 함수에서 정의한 변수 name의 값을 성공적으로 출력한다. 이 예시를 통해 함수가 중첩된 상황에서 파서가 어떻게 변수를 처리하는지 알 수 있다. 이는 어휘적 범위 지정(lexical scoping)의 한 예다. 여기서 "lexical"이란, 어휘적 범위 지정(lexical scoping) 과정에서 변수가 어디에서 사용 가능한지 알기 위해 그 변수가 소스코드 내 어디에서 선언되었는지 고려한다는 것을 의미한다. 단어 "lexical"은 이런 사실을 나타낸다. 중첩된 함수는 외부 범위(scope)에서 선언한 변수에도 접근할 수 있다.
* 클로저(Closure)
function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
// myFunc변수에 displayName을 리턴한다.
// 유효범위의 어휘적 환경을 유지한다.
myFunc();
// 리턴된 displayName 함수를 실행(name 변수에 접근)
이 코드는 displayName()함수가 실행되기 전에 외부함수인 makeFunc()로부터 리턴되어 myFunc 변수에 저장된다는 것이다. 한 눈에 봐서는 이 코드가 여전히 작동하는 것이 직관적으로 보이지 않을 수 있다.
makeFunc() 실행이 끝나면(displayName 함수가 리턴되고 아면) name 변수에 더 이상 접근할 수 없게 될 것으로 예상하는 것이 일반적이다.
실용적인 클로저
https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures
'Web > JavaScript' 카테고리의 다른 글
[JS Tutorial] var, let, const (0) | 2021.11.23 |
---|---|
[JS Tutorial] 세미콜론 (0) | 2021.11.23 |
[JS Tutorial] 자바스크립트 출력 (0) | 2021.11.22 |
[JS Tutorial] 자바스크립트 위치 (0) | 2021.11.22 |
[threejs] Creating a scene (0) | 2021.08.01 |