Scope

스코프는 변수와 상수, 매개변수가 언제 어디서 정의되는지 결정한다. 함수 매개변수가 함수 바디 안에서만 존재하는 것도 스코프의 한 예이다.

function f(x) {
  return x * 3
}

f(5) // 15
x // ReferenceError: x is not defined

스코프의 존재

가시성이라고도 불리는 스코프는 프로그램의 현재 실행 중인 부분, 즉 실행 컨택스트에서 현재 보이고 접근할 수 있는 식별자들을 말한다. 반면 존재한다는 말은 그 식별자가 메모리가 할당된 무언가를 가리키고 있다는 뜻이다.

정적 스코프와 동적 스코프

function f1() {
  return console.log('one')
}

function f2() {
  return console.log('two')
}

f2()
f1()
f2()

정적으로 보면 위의 코드는 단순히 위에서 아래로 읽어내리는 문의 연속이다. 하지만 이 코드의 실행 흐름은 읽는 순서와는 다르다. f1 이 f2 보다 먼저 정의됐지만, f2 의 함수 바디가 실행된 다음 f1 으로, 다시 f2 로 넘어간다.

기본적으로 자바스크립트의 스코프는 정적이다. 정적 스코프는 어떤 변수가 함수 스코프 안에 있는지 함수를 정의할 때 알 수 있다는 뜻이다. 호출할 때 알 수 있다는 것은 아니다.

const x = 3

function f() {
  console.log(x)
  console.log(y)
}

{
  // 새로운 스코프
  const y = 5
  f()
}

변수 x 는 함수 f 를 정의할 때 존재하지만, y 는 다른 스코프에 존재한다. 다른 스코프에서 y 를 선언하고 그 스코프에서 f 를 호출하더라도, f 를 호출하면 x 는 그 바디안의 스코프에 있지만 y 는 그렇지 않다. 이것이 정적 스코프이다. 함수 f 는 자신이 정의될 때 접근할 수 있었던 식별자에는 여전히 접근할 수 있지만, 호출할 때 스코프에 있는 식별자에 접근할 수는 없다.

자바스크립트의 정적 스코프는 전역 스코프, 블록 스코프, 그리고 함수 스코프에 적용된다.

전역 스코프

프로그램을 시작할 때 암시적으로 주어지는 스코프가 필요하다. 이 스코프를 전역 스코프라고 한다. 자바스크립트는 프로그램을 시작할 때, 즉 어떤 함수도 호출하지 않았을 때 실행 흐름은 전역 스코프에 있다. 바꿔말해, 전역 스코프에서 선언한 것은 무엇이든 프로그램의 모든 스코프에서 접근 가능하다.

전역 스코프에서 선언된 것들을 전역 변수라고 한다.

let name = 'Sun' // 전역
let age = '27' // 전역

function greet() {
  console.log(`Hello ${name}`)
}
function getBirthYear() {
  return new Date().getFullYear() - age + 1
}

WARNING

위의 코드 문제는 함수가 호출하는 스코프에 의존적이라는 것이다. 프로그램 어디에서든 name 과 age 의 값은 바꿀 수 있으므로, 프로그램의 다른 부분에서 name 과 age 의 값을 정확히 가지고 있는 것이 좋다.

let user = { name: 'Sun', age: '27' }

function greet(user) {
  console.log(`Hello ${user.name}`)
}
function getBirthYear(user) {
  return new Date().getFullYear() - user.age + 1
}

블록 스코프

let 과 const 는 식별자를 블록 스코프에서 선언한다.

console.log('before block')
{
  console.log('inside block')
  const x = 3
  console.log(x) // 3
}
console.log(`outside block; x=${x}`) // ReferenceError: x is not defined

위의 코드에서 x 는 블록 안에서 정의됐고, 블록을 나가는 즉시 x 도 스코프 밖으로 사라지므로 블록 바깥에서의 x 는 정의되지 않은 것으로 간주된다.

변수 숨기기

{
  // 외부 블록
  let x = { color: 'blue' }
  let y = x // 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"; x와 y는 같은 객체를 가리킴
}
console.log(typeof x) // "undefined"

스코프를 떠나지 않아도 새 스코프에 진입할 수 있다. 스코프의 계층적인 성격 때문에 어떤 변수가 스코프에 있는지 확인하는 스코프 체인 이라는 개념이 생겼다. 현재 스코프 체인에 있는 모든 변수는 스코프에 있는 것이며, 숨겨지지 않았다면 접근할 수 있다.

함수, 클로저, 정적 스코프

함수가 특정 스코프에 접근할 수 있도록 의도적으로 그 스코프에서 정의하는 경우가 많다. 이것을 클로저라고 부른다. 스코프를 함수 주변으로 좁히는 것이라 생각하면 쉽다.

let globalFunc // 정의되지 않은 전역 함수
{
  let blockVar = 'a' // 블록 스코프에 있는 변수
  globalFunc = function() {
    console.log(blockVar)
  }
}
globalFunc() // 'a'

globalFunc 은 블록 안에서 값을 할당 받았다. 이 블록 스코프와 그 부모인 전역 스코프가 클로저를 형성한다. globalFunc 를 어디서 호출하든, 이 함수는 클로저에 들어있는 식별자에 접근할 수 있다.

즉시 호출하는 함수 표현식

함수 표현식을 사용하여 즉시 호출하는 함수 표현식(IIFE)을 만들 수 있다.

;(function() {
  // IIFE
})()
const message = (function() {
  const secret = "I'm a secret!"
  return `The secret is ${secret.length} characters long.`
})()
console.log(message) // The secret is 13 characters long.

변수 secret 은 IIFE 의 스코프 안에서 안전하게 보호되며 외부에서 접근할 수 없다. IIFE 는 함수이므로 무엇이든 반환할 수 있다.

함수 스코프와 호이스팅

ES6 에서 let 을 도입하기 전에는 var 를 써서 변수를 선언했고, 이렇게 선언된 변수들은 함수 스코프라 불리는 스코프를 가졌다.(var 로 선언한 전역 변수는 명시적인 함수 안에 있지는 않지만 함수 스코프와 똑같이 동작한다.)

let 으로 변수를 선언하면, 그 변수는 선언하기 전에는 존재하지 않는다. var 로 선언한 변수는 현재 스코프 안이라면 어디서든 사용할 수 있으며, 선언하기 전에도 사용되어질 수 있다.

// let을 사용하면, 변수를 선언하기 전에 사용하려고 하면 에러가 발생한다.

x // ReferenceError: x는 정의되지 않았습니다
let x = 3
x // 3

// var로 변수를 선언하면, 선언하기 전에도 에러를 발생시키지 않는다.

x // undefined
var x = 3
x // 3

var 로 선언한 변수는 호이스팅이라는 메커니즘을 따른다. 자바스크립트는 함수나 전역 스코프 전체를 살펴보고 var 로 선언한 변수를 맨 위로 끌어올린다.

WARNING

호이스팅시에 선언만 끌어올려질뿐, 할당은 끌어올려지지 않는다.

함수 호이스팅

var 로 선언된 변수와 마찬가지로, 함수 선언도 스코프 맨 위로 끌어올려진다. 따라서, 함수를 선언하기 전에 호출할 수 있다.

f()
function f() {
  console.log('f')
}

WARNING

변수에 할당한 함수 표현식은 끌어올려지지 않는다.

f() // ReferrenceError: f 는 정의되지 않았습니다.
let f = function() {
  console.log('f')
}

스트릭트 모드

스트릭트 모드에서는 암시적 전역 변수를 허용하지 않는다. "use strict"를 코드 맨 앞에 쓰면 스트릭트 모드가 실행된다.

;(function() {
  'use strict'
  // some code...
})