tech.junha.kr

스코프: 크롬 디버거로 보기, 블록스코프, 함수스코프, 렉시컬스코프

스코프란, 변수 이름, 함수 이름, 클래스 이름과 같은 식별자가 본인이 선언된 위치에 따라 다른 코드에서 자신이 참조될 수 있을지 없을지 결정하는 것이다. 식별자에 대한 유효범위라고도 간단히 말한다.

크롬 디버거로 보는 스코프

var globalVariable1 = 1;
const globalVariable2 = 2;

const outerConst = function () {
  console.log();
};

var outerVar = function () {
  console.log();
};

function outerFunc2() {
  console.log();
}

function outerFunc1() {
  let outerVariable = 999;
  function innerFunc() {
    const innerVariable = 1;
    outerVariable += 1;
  }
  inner();
}

스코프는 컨텍스트에서 접근할 수 있는 식별자들을 나타내는 것이다. 위의 사진은 innerFunc() 컨텍스트에서 접근할 수 있는 식별자들을 스코프별로 모아놓은 것이다. (anonymous) 컨텍스트는 전역 컨텍스트를 뜻한다.

내가 궁금했던 것

Q. 컨텍스트가 생길때마다 Global, Script, Local 식별자가 생기는데, Global, Script는 컨텍스트마다 동일한 것을 바라보고 Local 식별자는 컨텍스트마다 다른 건가?

A. Scope에 나와있는 Global, Script, Local은 식별자라기 보단, 컨텍스트마다 접근할 수 있는 식별자들을 크롬 디버거가 스코프별로 모아놓은 분류명으로 보는 것이 적절하다. 따라서 어떤 컨텍스트건 Global, Script로 분류해놓은 식별자는 전역 컨텍스트의 VO안에 있기 때문에 동일할 것이다. 따라서 Global은 window객체의 프로퍼티&메소드**(var)가 들어가있고, Script는 스크립트 안에서 생성한 식별자(const, let)**가, Local은 해당 컨텍스트에서 생성된 식별자가 들어간다.

함수 스코프

  • 새로운 함수가 생성되면 새로운 스코프가 발생한다.
  • 함수에서 선언한 변수들은 해당 함수 내에서만 접근할 수 있다.
  • 자바스크립트는 var 키워드로 선언된 변수는 오로지 함수 코드블록만을 지역 스코프로 인정한다.
if(5 > 4) {
	var secret = '12345';
}
secret // '12345'
function a() {
	var secret = '12345';
}
secret; //ReferenceError

a함수에 대한 새로운 실행 컨텍스트가 생성되고, 이 실행 컨텍스트 내부에 존재하는 변수환경(variable environment)에 secret변수가 저장된다. 따라서 함수 외부에서 secret에 접근하려고 할 경우 스코프가 다르기 때문에 해당 변수에 접근이 불가능하다. 함수외부는 global scope이고, 함수 내부는 a함수의 scope인데 부모스코프는 자식스코프에서 간섭할 수 없기 떄문에 접근이 불가능하다.

블록 스코프

  • 블록이 생성될때 새로운 스코프가 생성된다.
  • 함수뿐만아니라, if문, for문...에서도 스코프가 생성된다.
  • 자바스크립트는 원래 함수스코프를 따르지만, let과 const의 등장이후로 블록 스코프 형성도 가능해졌다.
function  loop() {
	for (var j = 0; j < 3; j++) {
		console.log(j);
	}
	console.log('final', j);
}

loop();

// 0
// 1
// 2
// final 3
function  loop() {
	for (let j = 0; j < 3; j++) {
		console.log(j);
	}
	console.log('final', j);
}

loop();

// 0
// 1
// 2
// Uncaught ReferenceError: j is not defined

let으로 선언한 변수는 **for문 내(블록 스코프)**에서만 종속되며, for문 외부에서는 접근할 수 없다.

Lexical Scope

렉시컬 스코프는 함수의 호출 위치와 상관 없이, 함수가 어디서 선언되었는지에 따라 상위 스코프를 결정한다. 실행 컨텍스트가 생성될 때, 스코프 체인에 상위 스코프를 저장한 후에 실행되기 때문이다.

렉시컬 스코프는 코드가 쓰여진 순간 스코프는 이미 정해지기 때문에 정적 스코프라고도 불린다.

예제 코드1)

var x = 1; // global

function first() {
  var x = 10;
  second();
}

function second() {
  console.log(x);
}

first(); // ?
second(); // ?

렉시컬 스코프에소는 동적 스코프의 특징인 "함수를 어디에서 호출하였는지"는 스코프 결정과 전혀 무관하다. 그렇기 때문에, second() 함수가 first() 함수 안에서 호출된 것과 상관없이, second() 함수는 전역 컨텍스트에서 선언되어 있으므로, 전역 컨텍스트의 변수 x의 값 1이 두 번 출력된다.

예제 코드2)

const x = 1

function outer() {
    const x = 10
    const inner = function() {
        console.log(x)
    }
    return inner;
}

const junha = outer()

junha() // ?

inner함수가 정의되는 시점에 스코프 체인의 상위 스코프는 outer 컨텍스트를 가리키고 있기 때문에, x는 outer의 x를 먼저 참조해 10을 출력한다. outer가 콜스택에서 사라졌음에도 불구하고, inner가 outer의 x를 참조하고 있기 때문에 inner는 클로저이다.

예제 코드3)

var c = 999

let arr = []

for(let i= 0 ; i<5 ; i++) {
	let c = i * 2
    arr.push(function inner() {console.log(c)})
}

arr.forEach(func => func())

// ?

ES6에서는 블럭이 생성되면서도 실행 컨텍스트가 생성된다. inner함수가 정의되는 시점은(= inner 실행컨텍스트가 생성되는 시점은) 이 블럭에서 선언되기 때문에, inner의 실행 컨텍스트의 스코프체인은 블록을 참조하게 된다. 따라서 inner함수가 참조하는 c는 블록안의 c가 가장 먼저 스코핑이 된다.

만약, 블록안에 지역변수 c가 없었다면?? inner함수의 스코핑 단계는 VO(inner) -> SC(블록) -> SC(전역)을 통해 전역변수 c를 참조하여, 999를 5번 출력한다.