[러닝 자바스크립트] - 스코프

이수빈·2022년 8월 7일
0

자바스크립트

목록 보기
6/7
post-thumbnail

러닝 자바스크립트 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;
}

위 코드의 문제점은 nameage라는 식별자가 전역으로 선언됐기 때문에 프로그램 어디서든 이 값을 변경 할 수 있고, greetgetBirthYear함수가 이 전역 변수에 의존한다는 것이다.

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');
}
profile
내가 나중에 보려고

0개의 댓글