[JS] scope

Mia:)·2021년 1월 13일
0

스코프

특정 장소에 변수를 저장하고 나중에 그 변수를 찾는 데는 잘 정의된 규칙이 필요하다는 점. 이러한 규칙을 스코프라고 한다.

컴파일러 이론

자바스크립트는 일반적으로 동적 또는 인터프리터 언어로 분류하나 사실은 컴파일러 언어다. 물론 자바스크립트가 전통적인 많은 컴파일러 언어처럼 코드를 미리 컴파일하거나 컴파일한 결과를 분산 시스템에 이용할 수 있는 것은 아님. 하지만 자바스크립트 엔진은 전통적인 컴파일러 언어에서 컴파일러가 하는 일의 상당 부분을 우리가 아는 것 보다 세련된 방식으로 처리. 전통적인 컴파일러 언어의 처리과정에서는 프로그램을 이루는 소스 코드가 실행되기 전에 보통 3단계를 거치는데, 이를 컴파일레이션이라고 한다.

토크나이징/ 렉싱

문자열을 나누어 토큰이라 불리는 의미있는 조각으로 만드는 과정. 예를들어 var a = 2; 이 코드는 var, a, = ,2 ,; 로 토큰을 나눌 수 있디.

파싱

토큰 배열을 프로그램의 문법 구조를 반영하여 중첩 원소를 갖는 트리 형태로 바꾸는 과정이다. 파싱의 결과로 만들어진 트리를 AST(추상 구문 트리)라 부른다. var a = 2; 의 트리는 먼저 변수 선언이라는 부르는 최상위 노드에서 시작하고, 최상위 노드는 'a'의 값을 가지는 확인자와 대입수식이라 부르는 자식 노드를 가진다. 대입 수식 노드는 2라는 값을 가지는 숫자 리터럴을 자식노드로 가진다.

코드생성

AST를 컴퓨터에서 실행 코드로 바꾸는 과정이다. 이 부분은 언어에 따라 또는 목표하는 플랫폼에 따라 크게 달라진다. 코드 생성에 대한 세부 사항을 보며 끙끙대기보다는 일단 앞서 말한 var a =2;를 나타내는 AST를 기계어 집합으로 바꾸어 실제로 a라는 변수를 생성(메모리를 확보하는 일 등)하고, 값을 지정.
반면에 자바스크립트 엔진은 이 세가지 단계뿐 아니라 많은 부분에서 다른 프로그래밍 언어의 컴파일러보다 훨씬 복잡하다. 예컨대, 자바스크립트 엔진은 파싱과 코드 생성 과정에서 불필요한 요소를 삭제하는 과정을 거쳐 실행 시 성능을 최적화한다. 자바스크립트 엔진이 기존 컴파일러와 다른 점은 자바스크립트 컴파일레이션을 미리 수행하지 않아서 최적화할 시간이 많지 않다.

간단히

어떤 자바스크립트 조각이라도 실행되려면 먼저 컴파일되어야 한다는 것. 즉 자바스크립트 컴파일러는 프로그램 var a =2; 를 받아 컴파일하여 바로 실행될 수 있게 한다.

스코프

var a =2 ;
엔진: 컴파일레이션의 시작부터 끝까지 전 과정과 자바스크립트 프로그램 실행을 책임진다.
컴파일러: 엔진의 친구로, 파싱과 코드 생성의 모든 잡일을 도맡아 한다.
스코프: 엔진의 또 다른 친구로, 선언된 모든 확인자(변수) 검색 목록을 작성하고 유지한다. 또한 엄격한 규칙을 가제하여 현재 실행코드에서 확인자의 적용 방식을 정한다.

과정

var a =2;
이 프로그램에서 컴파일러가 할 첫 번째 일은 렉싱을 통해 구문을 토큰으로 쪼개는 것이다. 그 후 토큰을 파싱해 트리 구조를 만든다.
변수를 위해 메모리를 할당하고 할당된 메모리를 a라 명명한 후 그 변수에 값을 2를 넣는다.
좀 더 자세히 서술하면,
1. 컴파일러가 var a를 만나면 스코프에게 변수 a가 특정한 스코프 컬렉션 안에 있는지 묻는다. 변수 a가 이미 있다면 컴파일러는 선언을 무시하고 지나가고, 그렇지 않으면 컴파일러는 새로운 변수 a를 스코프 컬렉션내에 선언하라고 요청한다.
2. 그 후 컴파일러는 'a=2' 대입문을 처리하기 위해 나중에 엔진이 실행할 수 있는 코드를 생성한다. 엔진이 실행하는 코드는 먼저 스코프에게 a라 부르는 변수가 현재 스코프 컬렉션 내에 접근할 수 있는지 확인한다. 가능하다면 엔진은 변수 a를 사용하고 아니라면 엔진은 다른 곳(중첩 스코프 부분을)을 살핀다.

중첩스코프

스코프는 확인자 이름으로 변수를 찾기 위한 규칙의 집합이다. 하나의 블록이나 함수는 다른 블록이나 함수 안에 중첩될 수 있으므로 스코프도 다른 스코프안에 중첩될 수 있다. 따라서 대상 변수를 현재 스코프에서 발견하지 못하면 엔진은 다음 바깥의 스코프로 넘어가는 식으로 변수를 찾거나 글로벌 스코프라 부르는 가장 바깥 스코프에 도달할때 까지 계속한다.

function foo(){
    console.log(a+b); //12 
}
b = 10;
foo(2); 

중첩 스코프의 규칙

엔진은 현재 스코프에서 변수를 찾기 시작하고, 찾지 못하면 한단계 씩 올라간다.
최상위 글로벌 스코프에 도달하면 변수를 찾았든, 못 찾았든 검색을 멈춘다.

TypeError

null, undefined 값을 참조할 때 엔진은 TypeError를 발생

정리

스코프는 어디서 어떻게 변수를 찾는가를 결정하는 규칙의 집합이다. 변수를 검색하는 이유는 변수에 값을 대입(LHS)하거나 변수의 값을 얻어오기 위해서(RHS)이다. LHS 참조는 대입 연산 과정에서 일어나고, 스코프와 관련된 대입 연산은 '=' 연산자가 사용되가너 인자를 함수의 인자로 넘겨줄 때 일어난다.

자바스크립트 엔진은 코드를 실행하기 전에 먼저 컴파일을 하는데, 이 과정에서 엔진은 var a =2;와 같은 구문을 독립된 두 단계로 나눈다.
1. var a는 변수 a를 해당 스코프에 선언하고, 이 단계에서는 코드 실행 전에 처음부터 수행된다.
2. a =2는 변수 a를 찾아 값을 대입한다.

LHS,RHS 참조 검색은 모두 현재 실행중인 스코프에서 시작하며, 그리고 대상 변수를 찾지 못했을 경우, 한 번에 한 스코프의 상위 스코프로 넘어가면 확인자를 찾는다. 이 작업은 글로벌 스코프에 이를 때까지 계속하고 대상을 찾았든, 못찾았든 작업을 중단한다.

RHS 참조가 대상을 찾지 못하면 ReferenceError가 발생된다. LHS 참조가 대상을 찾지 못하면 자동적, 암시적으로 글로벌 스코프에 같은 이름의 새로운 변수가 생성. 스트릭 모드일 경우에는 ReferenceError.

렉시컬 스코프

렉시컬 스코프는 일반적이고 다수의 프로그래밍 언어가 사용하는 방식이다. 이 방식을 렉시컬 스코프라고 부른다.

렉스 타임

렉싱 처리 과정에서는 소스 코드 문자열을 분석하여 상태 유지 파싱의 결과로 생성된 토큰에 의미를 부여한다. 렉시컬 스코프는 렉싱 타임에 정의되는 스코프이다. 렉시컬 스코프는 개발자가 코드를 짤 때 변수와 스코프 블록을 어디에서 작성하는가에 기초해서 렉서가 코드를 처리할때 확정한다.

function foo(a){ //1
    var b = a * 2;   //1
    function bar(c){ //3 
        console.log(a,b,c)
    }
    bar(b *3 );
}
foo(2) // 2, 4, 12 

1은 글로벌 스코프를 감싸고 있고, 해당 스코프 안에는 오직 하나의 확인자만 있다.
2는 foo의 스코프를감싸고 있고, 해당 스코프는 a, bar, b를 포함한다.
3은 bar의 스코프를 감싸고 있고, 해당 스코프는 하나의 확인자만을 포함한다.

console.log()구문을 실행하고, 3개의 참조된 변수 a,b,c를 검색한다. 검색은 가장 안쪽의 스코프 버블인 bar() 함수의 스코프에서 시작한다. 여기서 a를 찾지 못하면 다음으로 가장 가까운 스코프 버블인 foo()의 스코프로 한 단계 올라가고, 이곳에서 a를 찾아 사용한다. b,c도 마찬가지. 하지만 c의 경 bar()내부에서 찾을 수 있다. 그래서 더이상 foo()로 위로 올라가서 찾지 않는다.
스코프는 목표와 일치하는 대상을 찾는 즉시 검색을 중단한다. 스코프 검색은 항상 실행 시점에서 가장 안쪽 스코프에서 시작하여, 최초 목표와 일치하는 대상을 찾으면 멈추고, 그 전까지는 바깥/위로 올라가면서 수행한다.

글로벌 변수는 자동으로 웹 브라우저의 window 같은 글로벌 객체에 속하고, 따라서 글로벌 변수를 직접 렉시컬 이름으로 참조하는 것 뿐만 아니라 글로벌 객체의 속성을 참조해 간접적으로 참조. window.a

함수의 렉시컬 스코프는 함수가 선언된 위치에 따라 정의된다. 렉시컬 스코프 검색 과정은 a,b,c 와 같은 일차 확인자 검색만 적용된다. 코드에서 foo, bar, baz 참조를 찾는다고 하면 렉시컬 스코프 검색은 foo 확인자를 찾는 데 사용되지만, foo를 찾고 나서는 객체 속성 접근 규칙을 통해 bar와 bax의 속성을 가져 온다.

정리

렉시컬 스코프란 개발자가 코드를 작성할 때 함수를 어디에 선언하는지에 따라 정의되는 스코프를 말함. 컴파일레이션의 렉싱 단계에서 모든 확인자가 어디서 어떻게 선언됐는지 파악하여 실행 단계에서 어떻게 확인자를 검색할지 예상할 수 있도록 도와준다.

함수 기반 스코프

앞의 코드에서 foo() 함수안에 a,b,c, bar 모두 foo()에 속했다. 그래서 바깥에서는 이들이 접근할 수 없다. 이렇께 감싸진 코드 안에 잇는 모든 변수 또는 함수 선언문은 만약 새로운 함수를 만든다면, 기존의 스코프가 아닌 새로이 코드를 감싼 함수의 스코프에 묶인다. 그러므로 함수의 스코프로 둘러싸서 변수와 함수를 숨길 수 있다는 것이다.

소프트웨어 디자인 원칙인 최소 권한의 원치과도 관련이 있는데. 이 원칙은 모듈 객체의 api와 같은 소프트웨어를 설계할 대 필요한 것만 최소한으로 남기고 나머지는 숨겨야한다는 것이다.

뿐만 아니라 변수와 함수를 스코프 안에 숨기는 것의 장점은 같은 이름을 가졌지만 다른 용도를 가진 두 확진자가 충돌하는 것을 피할 수 있다.

오염된 스코프

var a =2;
function foo(){
    var a =3 ;
    console.log(a); //3 
}
foo()

console.log(a) //2

해결방법1

var a =2;
(function foo(){
    var a =3 ;
    console.log(a) //3 
})();

console.log(a) // 2

함수 자신의 내부 스코프에 묶여서, 바깥스코프에서 발견되지 않는다. 그래서 함수 이름 foo를 자기 내부에 숨기면 함수를 둘러싼 스코프를 불필요하게 오염시키지 않음.

스코프 역할을 하는 블록

대표적인 블록 스코프 방식은

for(var i =0 ; i<10 ; i++){
    console.log(i)
}

변수 i를 for 반복문의 시작부에 선언하는 이유는 보통 i를 오직 for 반복문과 관련해서 사용하려고이다. 그리고는 변수 i가 실제로는 둘러싼 스코프에 포함된다는 것을 무시한다. 블록 스코프의 목적이 바로 이것이다. 변수를 최대한 사용자 가까이에서 최대한 작은 유효 범위를 갖도록 선언하는 것이다. 하지만 자바스크립트는 블록 스코프를 지원하지 않는다.

let

하지만 ES6에서는 let을 var 과 같이 변수를 선언하면서, 키워드 let은 선언된 변수를 둘러싼 아무 블로의 스코프에 붙인다. let은 선언한 변수를 위해 해당 블록 스코프를 이용한다고 말 할 수 있다.

let 반복문

for (let i =0; i <10 ; i++){
    console.log(i);
}
console.log(i) // ReferenceError

let 선언문은 둘러싼 함수 스코프가 아니라 가장 가까운 임의의 블록에 변수를 붙인다.

const

블록 스코프를 생성하지만, 선언된 값은 고정된다. 선언된 후 const값을 변경하려면 오류가 발생된다.

참고: you don't know JS

0개의 댓글