13장_스코프
모든 식별자는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효 범위가 결정되고, 이것을 스코프라고 한다.
즉 스코프는 식별자가 유효한 범위를 말한다.
var x = 'global';
function foo(){
var x = 'local';
console.log(x); //local
}
foo();
console.log(x); //global
코드의 가장 바깥영역과 foo 함수 내부에 같은 x 라는 변수를 선언했다.
자바스크립트 엔진은 이름이 같은 두 개의 변수 중에서 어떤 변수를 참조해야 할 것이지를 결정해야하는데 이를 식별자 결정이라고 한다.
식별자 결정을 할 때 자바스크립트 엔진은 스코프를 통해 어떤 변수를 참조해야 할 것인지를 결정한다.
자바스크립트 엔전은 코드를 실행할 때 코드의 문맥을 고려한다.
가장 바깥에서 선언된 x는 어디서든 참조할 수 있지만 foo 함수 내부에서 선언된 x 변수는 foo 함수 내부에서만 참조할 수 있다. 이렇게 두개의 x변수는 스코프가 다른 별개의 변수이다.
- var 키워드로 선언한 변수의 중복선언
- var 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언을 허용한다
- 아래 변수 선언문은 자바스크립트 엔진에 의해 var 키워드가 없는 것처럼 동작한다
function foo(){ var x = 1; var x = 2; console.log(x); //2 }
foo();
하지만 let 이나 const 로 선언된 변수는 같은 스코프 내에서 중복선언을 허용하지 않는다
```js
function bar(){
let x = 1;
//let 이나 const 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언을 허용하지 않는다
let x = 2;
}
bar();
스코프의 종류
- 전역 변수 : 전역에서 선언된 변수는 전역 스코프를 갖는 전역 변수
- 전역 변수는 어디서든지 참조할 수 있다.
- 어디서든 참조할 수 있으므로 함수 내부에서도 참조할 수 있다.
- 지역 변수 : 지역 스코프를 갖는 지역변수
- 지역 : 함수 몸체 내부를 말한다.
- 지역변수는 자신의 지역 스코프와 하위 지역 스코프에서 유효하다.
위 예제에서 local_one 함수 내부에서 선언된 c는 지역변수이다.
변수 c 는 자신의 지역 스코프인 local_one 과 그 하위 지역 스코프인 local_two 에서 참조할 수 있다.
local_two 에서 선언된 a도 지역변수이다.
local_two 함수 내부에서 선언된 a 가 있고, 전역 변수 a 가 존재한다.
이때 local_two 내부에서 a 변수를 참조하면 전역변수를 참조하는 것이 아니라 local_two 내부의 지역변수 a 를 참조한다.
이는 자바스크립트 엔진이 스코프 체인을 통해 참조할 변수를 검색 했기 때문이다.
스코프 체인
중첩 함수
함수 몸체 내부 에서 함수가 정의된 것을 '함수의 중첩' 이라고 하고, 함수의 몸체 내부에서 정의한 함수를 중첩 함수, 중첩 함수를 포함하는 함수를 외부 함수라고 한다.
함수가 중첩될 수 있으니 함수의 지역스코프도 중첩될 수 있다. 중첩 함수의 지역 스코프는 중첩 함수를 포함하는 외부함수의 지역 스코프와 계층적 구조를 갖는다. 이때 외부 함수의 지역 스코프를 중첩 함수의 상위 스코프라고 한다.
local_two는 local_one 함수의 중첩함수이다.
local_one이 만든 지역 스코프는 local_two 지역 스코프의 상위 스코프이다. 그리고 local_one 함수의 지역 스코프의 상위 스코프는 전역 스코프이다.
이렇게 모든 스코프는 계층적 구조로 연결되는 것을 스코프 체인 이라고 하며, 모든 지역 스코프의 최상위 스코프는 전역스코프이다.
변수를 참조할 때 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작해 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다.
위 그림에서 4번을 살펴보면
a 변수를 참조하는 코드의 스코프인 local_two함수의 지역 스코프에서 a 변수가 선언되었는지 검색한다. x는 local_two 내부에서 선언되었기 때문에 변수를 참조하고 검색을 종료한다.
5에서 동일하게 local_two 내부에서 b 변수가 선언되었는지 찾고, 없기 때문에 상위 스코프인 local_one 함수의 지역 스코프로 이동한다. 상위 지역스코프에서도 찾을 수 없었기 때문에 더 상위인 전역스코프로 이동하여 b변수를 찾아내고 이를 참조하고 검색을 종료한다.
- 자바스크립트 엔진은 점차 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다.
- 절대 하위 스코프로 내려가면서 식별자를 검색하는 일은 없다.
함수 레벨 스코프
지역은 함수 몸체 내부를 말하고 지역은 지역 스코프를 만든다고 했는데, 이는 코드 블록이 아닌 함수에 의해서만 지역 스코프가 생성된다는 의미이다.
대부분의 프로그래밍 언어는 함수 몸체만이 아니라 모든 코드블록이 지역 스코프를 만든다. 이런 특성을 블록 레벨 스코프라고 한다.
하지만 var 키워드로 선언된 변수는 오직 함수의 코드블록만을 지역 스코프로 인정한다. 이런 특성을 함수 레벨 스코프라고한다.
var x = 1;
if(true){
var x = 10
}
console.log(x); //10
전역변수 x 가 선언되었고 if 문의 코드 블록 내에도 x 변수가 선언되었다.
이때 if 문의 코드블록 내에서 선언된 x변수는 전역변수이다. var 키워드로 선언된 변수는 함수 레벨 스코프만 인정하기 때문이다.
ES6에서 도입된 let, const 키워드는 블록 레벨 스코프를 지원한다.
렉시컬 스코프
var x = 1;
function foo() {
var x = 10;
bar();
}
function bar() {
console.log(x);
}
foo(); // ?
bar(); // ?
위 예제의 실행 결과는 함수 bar의 상위 스코프가 무엇인지에 따라 결정된다
- 함수를 어디서 호출하였는지에 따라 상위 스코프를 결정.
- 함수 bar의 상위 스코프 : 함수 foo와 전역
- 함수를 어디서 선언하였는지에 따라 상위 스코프를 결정.
- 함수 bar의 스코프 : 전역일 것이다.
프로그래밍 언어는 이 두가지 방식 중 하나의 방식으로 함수의 상위 스코프를 결정한다. 첫번째 방식을 동적 스코프(Dynamic scope)라 하고, 두번째 방식을 렉시컬 스코프(Lexical scope) 또는 정적 스코프(Static scope)라 한다. 자바스크립트를 비롯한 대부분의 프로그래밍 언어는 렉시컬 스코프를 따른다.
렉시컬 스코프는 함수를 어디서 호출하는지가 아니라 어디에 선언하였는지에 따라 결정된다. 자바스크립트는 렉시컬 스코프를 따르므로 함수를 선언한 시점에 상위 스코프가 결정된다. 함수를 어디에서 호출하였는지는 스코프 결정에 아무런 의미를 주지 않는다. 위 예제의 함수 bar는 전역에 선언되었다. 따라서 함수 bar의 상위 스코프는 전역 스코프이고 위 예제는 전역 변수 x의 값 1을 두번 출력한다.
동적스코프
정적스코프 (=렉시컬 스코프)
함수가 호출되는 시점에 결정된다.
14장_변수의 문제점
변수의 생명주기
변수는 선언에 의해 생성되고 할당을 통해 값을 갖는다. 그리고 언젠가 소멸한다. 즉 변수는 생명주기가 있다. 변수에 생명 주기가 없다면 한번 선언된 변수는 프로그램을 종료하지 않는 한 영원히 메모리 공간을 점유하게 된다.
function foo(){
var x = 'local';
console.log(x);
return x;
}
foo();
console.log(x);
지역 변수 x는 foo 함수가 호출되기 이전까지는 생성되지 않는다. foo 함수를 호출하지 않으면 함수 내부의 변수 선언문이 실행되지 않기 때문이다.
변수 선언은 선언문이 어디에 있든 상관 없이 가장 먼저 실행된다. 이 설명은 엄밀히 말하면 전역 변수에 한정된 것이다. 함수 내부에서 선언한 변수는 함수가 호출된 직후에 함수 몸체의 코드가 한 줄씩 순차적으로 실행되기 이전에 자바스크립트 엔진에 의해 먼저 실행된다.
foo 함수를 다시 보면, foo 함수를 호출하면
- 함수 몸체의 다른 문들이 순차적으로 실행되기 이전에 x 변수의 선언문이 자바스크립트 엔진에 의해 가장 먼저 실행되어 x 변수가 선언되고 Undefined 로 초기화된다.
- 그 후, 함수 몸체를 구성하는 문들이 순차적으로 실행되기 시작하고 변수 할당문이 실행되면 x 변수에 값이 할당된다.
- 그리고 함수가 종료하면 x 변수도 소멸되어 생명주기가 종료된다.
따라서 함수 내부에서 선언된 지역변수 x 는 foo 함수가 호출되어 실행되는 동안에만 유효하다.
함수 몸체 내부에서 선언된 지역 변수의 생명 주기는 함수의 생명 주기와 대부분 일치하지만 지역 변수가 함수보다 오래 생존하는 경우도 있다.
변수
하나의 값을 저장하기 위해 학보한 메모리 공간 자체 또는 그 메모리 공간을 식별하기 위해 붙인 이름이다.
함수 내부에서 선언된 지역 변수
함수가 생성한 스코프에 등록된다. (함수가 생성한 스코프는 렉시컬 환경이라 부르는 물리적인 실체가 있다고 했다) 따라서 변수는 자신이 등록된 스코프가 소멸될 때 까지 유효하다. 할당된 메모리 공간은 더이상 그 누구도 참조하지 않을 때 가비지 콜렉터에 해제되어 가용 메모리 풀에 반환된다.
일반적으로 함수가 종료하면 함수가 생성한 스코프도 소멸하지만 누군가가 스코프를 참조하고 있다면 스코프는 해제되지 않고 생존하게 된다.
var x = 'global';
function foo(){
console.log(x); //1
var x = 'local';
}
foo();
console.log(x);
foo 함수 내부에서 선언된 지역변수 x는 이미 1 시점에 선언되었고 undefined 로 초기화 되어있다. 따라서 전역변수 x 를 참조하는 것이 아니라 지역변수 x를 참조해 값을 출력한다.
이처럼 호이스팅은 스코프를 단위로 동작한다. 지역변수의 호이스팅은 지역 변수의 선언이 지역 스코프의 선두로 끌러올려진 것처럼 동작한다. 따라서 지역 변수는 함수 전체에서 유효하다.
전역 변수의 생명주기
함수와 달리 전역 코드는 명시적인 호출 없이 실행된다. 전역 코드는 함수 호출과 같이 전역 코드를 실행하는 특별한 진입점이 없고 코드가 로드되자마자 곧바로 해석되고 실행된다. 전역코드에는 반환문을 사용할 수 없으므로 마지막 문이 실행되어 더 이상 실행할 문이없을 때 종료한다.
전역객체
전역 객체는 코드가 실행되기 이전에 자바스크립트 엔진에 의해 어떤 객체보다도 먼저 생성되는 특수한 객체다. 전역 객체는 클라이언트 사이드 환경(브라우저) 에서는 window, 서버사이드 환경(Node.js)에서는 Global 객체를 의미한다. 전역객체는 표준 빌트인 객체(Object,Number,Function,Array) 와 환경에 따른 호스트 객체(클라이언트 Web API 또는 Node.js의 호스트 API) 그리고 var 키워드로 선언한 전역변수와 전역함수를 프로퍼티로 갖는다.
브라우저 환경에서 전역객체는 window 이므로 브라우저 환경에서 var 키워드로 선언한 전역 변수는 전역 객체 window 의 프로퍼티다. 전역객체 window 는 웹페이지를 닫기 전까지 유효하다. 따라서 브라우저 환경에서 var 키워드로 선언한 전역변수는 웹페이지를 닫을 때까지 유효하다.
전역 변수의 문제점
암묵적 결합
- 전역변수의 목적 : 코드 어디서든 참조할 수 있는 변수를 사용하겠다.
- 이는 모든 코드가 전역변수를 참조하고 변경할 수 있는 암묵적 결합을 허용하는 것이다.
- => 변수의 유효범위가 크면 클수록 의도치 않게 상태가 변경될 수 있는 위험성도 높아진다.
긴 생명주기
전역변수는 생명 주기가 길다. 따라서 메로리 리소스도 오랜 기간 소비한다. 또 var 키워드는 변수의 중복 선언을 허용하므로 생명 주기가 긴 전역 변수는 변수 이름이 중복될 가능성이 있다.
스코프 체인 상에서 종점에 존재
젼역변수는 스코프 체인 상에서 종점에 존재한다. 따라서 변수를 검색할 때 전역변수가 가장 마지막에 검색되어 검색 속도가 느리다
전역 변수의 사용을 억제하는 방법
전역 변수를 반드시 사용해야 할 이유를 찾지 못한다면 지역변수를 사용해야한다. 변수의 스코프는 좁을 수록 좋다.
아래는 전역변수의 사용을 억제할 수 있는 방법이다.
즉시실행함수
모든 코드를 즉시 실행함수로 감싸면 모든 변수는 즉시 실행함수의 지역변수가 된다.
- 이 방법을 사용하면 전역변수를 만들지 않으므로 라이브러리 등에 자주 사용된다. 즉시 실행 함수는 즉시 실행되고 그 후 전역에서 바로 사라진다.
(function () { var MYAPP = {}; MYAPP.student = { name: 'Lee', gender: 'male' }; console.log(MYAPP.student.name); }());
console.log(MYAPP.student.name);
etc.
# 15장_let, const 키워드와 블록 레벨 스코프
## var 키워드로 선언한 변수의 문제점
### 1. 변수 중복 선언 허용
var 키워드로 선언한 변수는 중복 선언이 가능하다
```js
var x = 1;
var y = 1;
// var 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언을 허용한다.
// 초기화문이 있는 변수 선언문은 자바스킙트 엔진에 의해 var 키워드가 없는 것처럼 동작한다.
var x = 100;
// 초기화문이 없는 변수 선언문은 무시된다.
var y;
console.log(x); //100
console.log(y); //1
위 예제의 var 키워드로 선언한 x,y 변수는 중복 선언되었다. 이렇게 var 키워드로 선언한 변수를 중복선언하면 초기화문의 유무에 따라 다르게 동작한다. 초기화문이 있는 변수 선언문은 자바스크립트 엔진에 의해 var 키워드가 없는 것처럼 동작하고 초기화문이 없는 변수 선언문은 무시된다. 이때 에러는 발생하지 않는다.
2. 함수 레벨 스코프
- 함수의 코드 블록만을 스코프로 인정한다. 따라서 전역 함수 외부에서 생성한 변수는 모두 전역 변수이다. 이는 전역 변수를 남발할 가능성을 높인다.
var x = 1; if(true){ //x는 전역 변수다. 이미 선언된 전역 변수 X 가 있으므로 X 변수는 중복 선언된다. //이는 의도치 않게 변수 값이 변경되는 부작용을 발생시킨다. var x = 10; }
console.log(x); //10
- for 문의 변수 선언문에서 선언한 변수를 for 문의 코드 블록 외부에서 참조할 수 있다.
```js
var i = 10;
//for문에서 선언한 i는 전역변수다. 이미 선언된 전역변수 i가 있으므로 중복선언 된다.
for(var i =0; i<5; i++){
console.log(i); //0 1 2 3 4
}
//의도치않게 i 변수의 값이 변경됨
console.log(i); //5
3. 변수 호이스팅
- 변수를 선언하기 이전에 참조할 수 있다.
var 키워드로 변수를 선언하면 호이스팅으로 인해 변수 선언문 이전에 변수를 참조할 수 있게 된다. 단 이 경우 언제나 undefined 를 반환한다.
let 키워드
블록 레벨 스코프
모든 코드 블록(함수, if 문, for 문, while 문, try/catch 문 등) 내에서 선언된 변수는 코드 블록 내에서만 유효하며 코드 블록 외부에서는 참조할 수 없다. 즉, 코드 블록 내부에서 선언한 변수는 지역 변수이다.
var foo = 123; // 전역 변수
console.log(foo); // 123
{
var foo = 456; // 전역 변수
}
console.log(foo); // 456
블록 레벨 스코프를 따르지 않는 var 키워드의 특성 상, 코드 블록 내의 변수 foo는 전역 변수이다. 그런데 이미 전역 변수 foo가 선언되어 있다. var 키워드를 사용하여 선언한 변수는 중복 선언이 허용되므로 위의 코드는 문법적으로 아무런 문제가 없다. 단, 코드 블록 내의 변수 foo는 전역 변수이기 때문에 전역에서 선언된 전역 변수 foo의 값 123을 새로운 값 456으로 재할당하여 덮어쓴다.
ES6는 블록 레벨 스코프를 따르는 변수를 선언하기 위해 let 키워드를 제공한다.
let foo = 123; // 전역 변수
{
let foo = 456; // 지역 변수
let bar = 456; // 지역 변수
}
console.log(foo); // 123
console.log(bar); // ReferenceError: bar is not defined
let 키워드로 선언된 변수는 블록 레벨 스코프를 따른다. 위 예제에서 코드 블록 내에 선언된 변수 foo는 블록 레벨 스코프를 갖는 지역 변수이다. 전역에서 선언된 변수 foo와는 다른 별개의 변수이다. 또한 변수 bar도 블록 레벨 스코프를 갖는 지역 변수이다. 따라서 전역에서는 변수 bar를 참조할 수 없다.
변수 중복 선언 금지
var foo = 123;
var foo = 456; // 중복 선언 허용
let bar = 123;
let bar = 456; // Uncaught SyntaxError: Identifier 'bar' has already been declared
var 키워드로는 동일한 이름을 갖는 변수를 중복해서 선언할 수 있었다. 하지만, let 키워드로는 동일한 이름을 갖는 변수를 중복해서 선언할 수 없다. 변수를 중복 선언하면 문법 에러(SyntaxError)가 발생한다.
호이스팅
let 키워드로 선언된 변수는 선언 단계와 초기화 단계가 분리되어 진행된다. 즉, 스코프에 변수를 등록(선언단계)하지만 초기화 단계는 변수 선언문에 도달했을 때 이루어진다. 초기화 이전에 변수에 접근하려고 하면 참조 에러(ReferenceError)가 발생한다. 이는 변수가 아직 초기화되지 않았기 때문이다. 다시 말하면 변수를 위한 메모리 공간이 아직 확보되지 않았기 때문이다. 따라서 스코프의 시작 지점부터 초기화 시작 지점까지는 변수를 참조할 수 없다. 스코프의 시작 지점부터 초기화 시작 지점까지의 구간을 ‘일시적 사각지대(Temporal Dead Zone; TDZ)’라고 부른다.
// 스코프의 선두에서 선언 단계가 실행된다.
// 아직 변수가 초기화(메모리 공간 확보와 undefined로 초기화)되지 않았다.
// 따라서 변수 선언문 이전에 변수를 참조할 수 없다.
console.log(foo); // ReferenceError: foo is not defined
let foo; // 변수 선언문에서 초기화 단계가 실행된다.
console.log(foo); // undefined
foo = 1; // 할당문에서 할당 단계가 실행된다.
console.log(foo); // 1
let foo = 1; // 전역 변수
{
console.log(foo); // ReferenceError: foo is not defined
let foo = 2; // 지역 변수
}
위 예제의 경우, 전역 변수 foo의 값이 출력될 것처럼 보인다. 하지만 ES6의 선언문도 여전히 호이스팅이 발생하기 때문에 참조 에러(ReferenceError)가 발생한다.
ES6의 let으로 선언된 변수는 블록 레벨 스코프를 가지므로 코드 블록 내에서 선언된 변수 foo는 지역 변수이다. 따라서 지역 변수 foo도 해당 스코프에서 호이스팅되고 코드 블록의 선두부터 초기화가 이루어지는 지점까지 일시적 사각지대(TDZ)에 빠진다. 따라서 전역 변수 foo의 값이 출력되지 않고 참조 에러(ReferenceError)가 발생한다.
const 키워드
const는 상수(변하지 않는 값)를 위해 사용한다. 하지만 반드시 상수만을 위해 사용하지는 않는다. const의 특징은 let과 대부분 동일하다.
선언과 초기화
let은 재할당이 자유로우나 const는 재할당이 금지된다.
const FOO = 123;
FOO = 456; // TypeError: Assignment to constant variable.
const는 반드시 선언과 동시에 할당이 이루어져야 한다. 그렇지 않으면 다음처럼 문법 에러(SyntaxError)가 발생한다.
const FOO; // SyntaxError: Missing initializer in const declaration
또한, const는 let과 마찬가지로 블록 레벨 스코프를 갖는다.
{
const FOO = 10;
console.log(FOO); //10
}
console.log(FOO); // ReferenceError: FOO is not defined
상수
// 10의 의미를 알기 어렵기 때문에 가독성이 좋지 않다.
if (rows > 10) {
}
// 값의 의미를 명확히 기술하여 가독성이 향상되었다.
const MAXROWS = 10;
if (rows > MAXROWS) {
}
조건문 내의 10은 어떤 의미로 사용하였는지 파악하기가 곤란하다. 하지만 네이밍이 적절한 상수로 선언하면 가독성과 유지보수성이 대폭 향상된다.
const와 객체
const는 재할당이 금지된다. 이는 const 변수의 타입이 객체인 경우, 객체에 대한 참조를 변경하지 못한다는 것을 의미한다. 하지만 이때 객체의 프로퍼티는 보호되지 않는다. 다시 말하자면 재할당은 불가능하지만 할당된 객체의 내용(프로퍼티의 추가, 삭제, 프로퍼티 값의 변경)은 변경할 수 있다.
const user = { name: 'Lee' };
// const 변수는 재할당이 금지된다.
// user = {}; // TypeError: Assignment to constant variable.
// 객체의 내용은 변경할 수 있다.
user.name = 'Kim';
console.log(user); // { name: 'Kim' }
객체의 내용이 변경되더라도 객체 타입 변수에 할당된 주소값은 변경되지 않는다. 따라서 객체 타입 변수 선언에는 const를 사용하는 것이 좋다. 만약에 명시적으로 객체 타입 변수의 주소값을 변경(재할당)하여야 한다면 let을 사용한다.
var vs. let vs. const
변수 선언에는 기본적으로 const를 사용하고 let은 재할당이 필요한 경우에 한정해 사용하는 것이 좋다. 원시 값의 경우, 가급적 상수를 사용하는 편이 좋다. 그리고 객체를 재할당하는 경우는 생각보다 흔하지 않다. const 키워드를 사용하면 의도치 않은 재할당을 방지해 주기 때문에 보다 안전하다.
var와 let, 그리고 const는 다음처럼 사용하는 것을 추천한다.
- ES6를 사용한다면 var 키워드는 사용하지 않는다.
- 재할당이 필요한 경우에 한정해 let 키워드를 사용한다. 이때 변수의 스코프는 최대한 좁게 만든다.
- 변경이 발생하지 않는(재할당이 필요 없는 상수) 원시 값과 객체에는 const 키워드를 사용한다. const 키워드는 재할당을 금지하므로 var, let 보다 안전하다.
변수를 선언하는 시점에는 재할당이 필요할지 잘 모르는 경우가 많다. 그리고 객체는 의외로 재할당을 하는 경우가 드물다. 따라서 변수를 선언할 때에는 일단 const 키워드를 사용하도록 하자. 반드시 재할당이 필요하다면(반드시 재할당이 필요한지 한번 생각해 볼 일이다.) 그때 const를 let 키워드로 변경해도 결코 늦지 않는다.
'Javascript' 카테고리의 다른 글
[JS] 모던 자바스크립트 Deep Dive 북스터디 Week3 (0) | 2023.09.13 |
---|---|
[JS] 모던 자바스크립트 Deep Dive 북스터디 Week2 (0) | 2023.08.11 |
[JS] 모던 자바스크립트 Deep Dive 북스터디 Week1 (0) | 2023.08.04 |
[Javascript] Optional chaining (?.) (0) | 2023.02.20 |
[Javascript] Generator (0) | 2022.12.03 |