러닝 자바스크립트 7장에 해당되는 부분이며, 읽으면서 자바스크립트에 대해 새롭게 알게된 것과 기록하고 싶은 부분을 정리한 내용입니다.
스코프
는 변수와 상수, 매개변수가 언제 어디서 정의되는지 결정하는 범위의 개념이다.
변수
가 스코프 안에 있지 않다면, 이 변수는 스코프 안에 있지 않다
이지 존재하지 않다
는 것은 아니다. 스코프는 실행 컨텍스트
에서 현재 보이고 접근할 수 있는 식별자를 말한다. 존재한다는 것은 그 식별자가 메모리가 할당된 무언가를 가리키고 있다는 뜻이다.
자바스크립트의 스코프는 정적 스코프
이다. 정적 스코프는 어떤 변수가 함수 스코프 안에 있는지 함수를 호출할 때가 아닌, 함수를 정의할 때 알 수 있다는 뜻이다.
const x = 3;
function f() {
console.log(x);
console.log(y);
}
// 새로운 스코프
{
const y = 5;
f();
}
x
는 함수f
를 정의할 때 존재하지만, y
는 다른 스코프에 존재한다. 다른 스코프에서 f
를 호출하게 되면, x
는 함수가 정의될 때 접근할 수 있었기 때문에 접근할 수 있지만 y
는 호출할 때 스코프에 있는 식별자이기 때문에 접근할 수 없다.
자바스크립트의 정적 스코프는
전역 스코프
,블록 스코프
,함수 스코프
에 적용된다.
전역 스코프
는 프로그램을 시작할 때 암시적으로 주어지는 스코프이다. 자바스크립트 프로그램을 시작핼 때 실행 흐름은 전역 스코프에 있다. 즉, 전역 스코프에서 선언한 것은 프로그램의 모든 스코프에서 볼 수 있다. 이런 이유 때문에 전역 스코프를 남용하는 것은 좋지 않다.
let name = "Irena"; // 전역
let age = 25; // 전역
function greet() {
console.log(`Hello ${name}!`);
}
function getBirthYear() {
return new Date().getFullYear() - age;
}
위 코드의 문제점은 name
과 age
라는 식별자가 전역으로 선언됐기 때문에 프로그램 어디서든 이 값을 변경 할 수 있고, greet
와 getBirthYear
함수가 이 전역 변수에 의존한다는 것이다.
function greet(user) {
console.log(`Hello ${user.name}!`);
}
function getBirthYear(user) {
return new Date().getFullYear() - user.age;
}
함수가 명시적으로 user
을 전달받게해 전역 변수(객체)에 의존하지 않게 하고, 식별자를 단일 객체에 보관하게 고칠 수 있다.
let과 const는 식별자를 블록 스코프
에서 선언한다. 블록 스코프는 그 블록의 스코프에서만 보이는 식별자를 의미한다.
console.log('before block');
{
console.log('inside block');
const x = 3;
console.log(x); // 3
}
console.log(`x=${x}`); // ReferenceError
x
는 블록 안에서 정의 됐고, 블록을 나가는 즉시 사라지기 때문에 정의되지 않은 것으로 간주된다.
스코프가 중첩되어 이름이 같은 변수나 상수가 존재하게 되면 변수 숨김
이 적용된다.
{
// 외부 블록
let x = 'blue';
console.log(x); // 'blue'
{
//내부 블록
let x = 3;
console.log(x); // 3
}
console.log(x); // 'blue'
}
console.log(typeof x); // undefined
내부 블록의 x
는 외부 블록에서 정의한 x
와 이름이 같은 변수이기 때문에 외부 블록의 x
를 숨기는 효과가 있다. 실행 흐름이 내부 블록으로 들어가 x
라는 새 변수를 정의하게 되면, 외부 블록에 있는 변수 x
에 접근할 방법이 없다.
{
// 외부 블록
let x = { color: 'blue'};
let y = x;
let z = 3;
{
// 내부 블록
let x = 5; // 외부 x 가려짐
console.log(x); // 5
console.log(y.color) // 'blue'
y.color = 'red';
console.log(z); // 3
}
console.log(x.color); // 'red'
console.log(y.color); // 'red'
console.log(z); // 3
}
내부 블록에서 x
는 5라는 값으로 가려지게 되고, y
는 외부에서 x
와 같은 객체를 갖게 선언 되었기 때문에 외부 블록에서 x
가 가리키는 객체 color
가 내부 블록에도 존재해 수정이 가능하다.
외부 스코프에서 정의된 변수를 내부 스코프에서 숨기게 되면 그 변수에 해당하는 이름으로는 절대 접근할 수 없다.
자바스크립트에서 함수를 전역에서 정의하고 함수 안에서 전역 스코프를 참조하지 않도록 코드를 작성하면 상관 없지만, 최신 자바스크립트에서는 함수가 필요한 곳에서 즉석으로 정의하고 할당, 반환하는 경우가 많다.
이럴 때 함수가 특정 스코프에 접근할 수 있도록 의도적으로 그 스코프에서 정의하는 경우가 많은데 이런 것을 클로저
라고 한다.
let globalFunc; // 전역 함수
{
let blockVar = 'a'; // 블록 스코프 변수
globalFunc = function() {
console.log(blockVar);
}
}
globalFunc(); // 'a'
globalFunc
함수는 전역으로 선언 되었지만 블록 내부에서 값을 할당받았다. 이 블록 스코프와 그 부모인 전역 스코프가 클로저를 형성하게 되고, 어디서 호출되든 이 함수는 클로저에 들어있는 식별자에 접근할 수 있다.
let f; // 정의되지 않은 전역 함수
{
let o = { note : "safe" };
f = function () {
return o;
}
}
let oRef = f();
oRef.note = 'Not safe';
즉, 위의 코드처럼 스코프 안에서 함수를 정의하게 되면 해당 스코프는 더 오래 유지되고, 일반적으로 스코프 바깥쪽에 있는 접근할 수 없는 것에 접근할 수 있게 된다.
ES6 이전에서 let
대신 var
를 사용해 변수를 선언했는데 선언된 변수들은 함수 스코프
라는 스코프를 가진다. let
으로 선언한 변수는 선언하기 전에는 존재하지 않지만, var
로 선언한 변수는 선언하기도 전에 사용이 가능하다. 선언되지 않은 변수와 값이 undefined인 변수는 다르다.
x; // ReferenceError
let x = 3;
y; // undefined
var y = 3;
y; // 3
var
로 선언된 변수는 끌어올린다는 뜻의 호이스팅
이라는 메커니즘을 따른다. 자바스크립트는 var
로 선언된 변수를 맨 위로 끌어올리는데, 중요한 것은 선언만 끌어올려진다는 점이다.
// 원래 코드
y; // undefined
var y = 3;
y; // 3
//자바스크립트가 해석한 코드
var y;
y;
y = 3;
y;
let
으로 가능했던 변수 숨김도 var
로 선언하게 되면 함수나 전역 스코프 안에서는 새 변수를 만들 수 없기 때문에 불가능하다.
var x = 3;
if(x === 3) {
var x = 2;
console.log(x); // 2
}
console.log(x); // 2
저자는 var에는 let보다 나은 점이 전혀 없으며, let이 언젠가 var를 완전히 대체할 것으로 예상한다. var와 호이스팅을 이해해야 하는 이유는 기존 자바스크립트 코드 대부분이 ES5로 작성되어 아직은 var가 어떻게 동작하는지 이해해야하고, 함수 선언 역시 끌어올려지기 때문이라고 한다.
var로 선언된 변수와 마찬가지로 함수 선언도 스코프 맨 위로 끌어올려진다. 따라서 함수를 선언하기 전에 호출할 수 있지만, 변수에 할당한 함수 표현식
은 변수의 스코프 규칙을 그대로 따르기 때문에 끌어올려지지 않는다.
f(); // 'f'
function f() {
console.log('f');
}
g(); // ReferenceError
let g = function() {
console.log('g');
}