티스토리 뷰

728x90

👉 var, let, const

var

  • 한번 선언된 변수를 다시 선언할 수 있다.
  • var는 선언하기 전에 사용할 수 있다.
    • 선언과 초기화 단계가 동시에 일어나기 때문에, 호이스팅이 일어날 때 변수가 undefined로 초기화 되어 변수의 할당 전에 변수를 사용하더라도 에러가 나지 않음
  • 함수 스코프를 가진다.: 유일하게 벗어날 수 없는 스코프가 함수이다
///
if (age < 14) {
	var txt = '어린이' // 반면 let이나 const 변수는 해당 if 문 안에서만 사용이 가능함 (블록스코프)
}
///
console.log(txt); // var는 함수 스코프이므로 txt 변수 사용 가능
function add(num1, num2) {
	var result = num1 + num2;
}

add(2,3);
console.log(result); // var는 함수 스코프이므로 함수 내에서 선언한 변수는 밖에서 사용하지 못함

 

let

  • 한번 선언된 변수를 다시 선언할 수 없다. 하지만 값을 재할당 하는 것은 가능!
  • 선언 단계와 초기화 단계가 따로 일어난다. 호이스팅이 일어나면서 선언 단계가 이루어지지만, 초기화는 실제 코드에 도달했을 때 일어나기 때문에 ReferenceError가 발생하게 된다.
    • 스코프의 시작 지점부터 초기화 시작 지점까지의 구간을 TDZ(Temporal Dead Zone)이라고 함
  • 블록 스코프 (함수, if문, for문, while문, try/catch문): 코드 블록 내에서 선언된 변수는 코드 블록 내에서만 유효하며, 외부에서는 접근할 수 없음

 

const

  • 한번 선언된 변수를 다시 선언할 수 없고, 값을 재할당하는 것도 불가능하다. 하지만 변수의 값이 원시값이 아닌 객체인 경우에는 재할당이 가능합니다.
  • const 또한 let과 동일하게 호이스팅이 일어나지만, 선언 단계와 초기화 단계가 따로 일어납니다. 
  • 코드 내에서 선언, 초기화, 할당이 모두 동시에 일어나야 한다.
    • 선언하면서 할당이 일어나지 않으면 에러가 나게 된다!
  • 블록 스코프
  • let과 const를 사용하면 예측 가능한 결과를 내고 버그를 줄일 수 있다

 

변수의 생성 과정
  1. 선언 단계
  2. 초기화 단계
  3. 할당 단계

 

호이스팅

스코프 내부 어디서든 변수 선언과 함수 선언은 최상위에 선언된 것처럼 행동하는 현상을 의미합니다.

함수 표현식이 아니라 함수 선언문일 경우에는 식 자체가 통째로 끌어올려집니다. 함수 표현식의 경우 함수를 할당할 변수의 선언은 호이스팅 되지만, 함수를 할당(=초기화)하는 것은 호이스팅 되지 않습니다.

변수 선언이 함수 선언보다 높은 우선 순위를 가집니다.

 

 

👉 함수 선언문과 함수 표현식

함수 선언문

  • 어디서든 호출 가능
    • 식 자체가 통째로 호이스팅 되므로 함수의 할당 전에도 호출이 가능해짐.
  • 더 자유롭고 편하게 코딩할 수 있음
sayHello(); // 정상 동작
function sayHello() {
	console.log('Hello');
}

 

함수 표현식

  • 코드에 도달하면 함수가 생성되기 때문에 위의 예제에서 첫번째 호출에 대해서는 정상적으로 동작하지 않음
showError(); // ReferenceError 발생

let showError = function(){
	console.log('error');
}

 

+) 화살표 함수

  • function 키워드가 사라지고 간결하게 함수를 표현할 수 있음
  • ES6 이후 등장한 방식
  • 함수 자체의 this, arguments, super, new.target 바인딩을 갖지 않는다.
    • 화살표 함수 자체의 this 바인딩을 갖지 않기 때문에, 화살표 함수 내부에서 this를 참조하면 상위 스코프의 this를 참조한다.

 

👉 스코프 (Scope)란?

변수 이름, 함수 이름, 클래스 이름과 같은 식별자가 본인이 선언된 위치에 따라 다른 코드에서 자신이 참조될 수 있을지 없을지 결정되는 것

  • 함수 레벨 스코프(var)와 블록 레벨 스코프(let, const)
  • 동적 스코프: 함수가 호출되는 시점에 결정
  • 정적 스코프(렉시컬 스코프): 함수가 정의되는 시점에 상위 스코프가 결정됨.
    • 자바스크립트는 정적 스코프를 따르므로 함수가 선언되자마자 자신의 상위 스코프를 알 수 있음

 

스코프 체인

함수의 중첩이 일어나면 함수의 지역 스코프 또한 중첩이 일어나게 됨

스코프가 계층적으로 연결되어 있는 것

변수를 참조할 때 자바스크립트 엔진은 스코프 체인을 따라 참조하게 됨.

최상위 스코프인 전역 스코프에도 참조하고자 하는 변수가 없다면 ReferenceError를 반환하게 됨

 

  1. 함수 호출
  2. 호출된 함수의 실행 컨텍스트를 생성하고, 실행 컨텍스트 스택에 쌓는다.
  3. 렉시컬 환경을 생성한다.
  4. 실행이 끝나면 실행 컨텍스트 스택에서 꺼낸다.

 

👉 클로저 (Closure)

  • 함수와 그 함수의 렉시컬 환경의 조합을 의미한다.
  • 외부 함수의 실행이 끝난 이후에도 내부 함수가 외부 함수의 지역변수 값을 참조할 수 있는데, 이러한 메커니즘을 클로저라고 한다.
  • 클로저는 함수를 구성하는 코드와 함수가 생성될 당시의 렉시컬 환경을 알고 있기 때문에, 함수가 생성될 당시의 모든 변수를 기억해 두었다가 함수가 호출 되었을 때 사용할 수 있습니다.
var makeClosure = function() {
  var name = 'zero';
  return function () {
    console.log(name);
  }
};
var closure = makeClosure(); // function () { console.log(name); }
closure(); // 'zero';

해당 예제에서 makeClosure() 함수의 결과 값인 익명 함수가 closure 변수에 담긴다.

코드의 실행이 closure() 함수 실행으로 넘어오면 makeClosure() 함수의 실행이 끝났기 때문에 이 함수의 지역 변수인 name은 소멸되는 것이 자연스럽다. 하지만 closure() 함수를 실행하면 'zero' 라고 name 변수가 출력되게 되고, 이는 지역변수 name이 소멸되지 않았다는 것을 의미한다. 

 

function() {console.log(name)} 은 name 변수나 name 변수가 있는 스코프에 대해 클로저라고 부를 수 있습니다.

 

 

👉 렉시컬 환경 (어휘적 환경, Lexical Environment)

  • 실행할 스코프 범위 안에 있는 변수와 함수를 프로퍼티로 저장하는 객체이다.
  • 자신의 변수와 함수를 포함하는 레코드와, 자신이 참조하는 Outer Lexical Environment를 기억하고 있습니다.
  • 소스 코드를 실행하면서 참조가 필요한 변수의 값을 Lexical Environment라는 객체에서 식별자 이름을 키로 찾는다고 생각하면 된다.
  • 코드가 실행되면 코드에서 선언된 변수와 함수들이 전역 Lexical 환경으로 올라가게 됨.
  • 코드 내의 함수가 실행되면, 해당하는 함수의 내부 Lexical 환경이 생성됨. 그리고 전역 Lexical 환경을 참조하게 됨.
  • 내부 Lexical에서 참조했을 때 값이 없으면, 참조하는 외부 Lexical 환경을 차례로 참조하여 값을 찾음

 

👉 실행 컨텍스트 (Execution Context)

함수의 실행, 호이스팅, 렉시컬 환경, 클로저 같은 개념을 관통하는 하나의 큰 개념이다.

코드의 실행 환경이라고 할 수 있다.

  • 코드를 실행하면 모든 것을 포함하는 전역 컨텍스트가 생성됩니다.
  • 함수를 호출할 때마다 함수 컨텍스트가 생깁니다.
  • 컨텍스트가 생성될 때 컨텍스트 안에는 변수 객체(arguments, variable), scope chain, this가 생성됩니다.
    • 실행 컨텍스트가 생성될 때 함수 내의 변수 상태를 렉시컬 환경이라는 객체에 저장해두고, 변경이 있을 때마다 업데이트하고 필요할 때 접근해서 갖다 쓰는 것!
    • 모든 변수가 컨텍스트 생성 단계에서 렉시컬 환경에서 초기화 되기 때문에 자바스크립트 엔진은 변수들의 존재를 모두 인지하게 되고, 이것이 호이스팅이 발생하는 이유가 된다.
  • 컨텍스트 생성 후 함수가 실행되는데, 사용되는 변수들은 변수 객체 안에서 값을 찾고, 없다면 스코프 체인을 따라 올라가며 찾습니다.
  • 함수 실행이 마무리되면 해당 컨텍스트는 사라집니다. (클로저 제외) 페이지가 종료되면 전역 컨텍스트가 사라집니다.
  • 코드를 실행했을 때 생성되는 전역 컨텍스트와 함수 컨텍스트는 실행 컨텍스트 스택이라는 자료구조에 저장되고 관리된다.
var name = 'zero'; // (1)변수 선언 (6)변수 대입
function wow(word) { // (2)변수 선언 (3)변수 대입
  console.log(word + ' ' + name); // (11)
}
function say () { // (4)변수 선언 (5)변수 대입
  var name = 'nero'; // (8)
  console.log(name); // (9)
  wow('hello'); // (10)
}
say(); // (7)

 

전역 컨텍스트 (Global Execution Context)

위의 예시에서 전역 컨텍스트가 아래와 같이 생성됩니다.

전역 컨텍스트는 함수의 인자인 argument가 존재하지 않고, variable은 해당 스코프의 변수들입니다.

스코프 체인은 자기 자신인 전역 변수 객체이고, this는 따로 설정되어 있지 않으면 window를 의미합니다.

'전역 컨텍스트': {
  변수객체: {
    arguments: null,
    variable: ['name', 'wow', 'say'],
  },
  scopeChain: ['전역 변수객체'],
  this: window,
}

코드를 실행하면 wow와 say는 선언과 동시에 할당이 됩니다. 또한 var 변수인 name 또한 선언과 동시에 할당이 됩니다.

variable: [{ name: 'zero' }, { wow: Function }, { say: Function }]

 

함수 컨텍스트

그 후에 7번 줄에서 say()함수를 호출하면 새로운 say 함수 컨텍스트가 아래와 같이 생성됩니다.

this는 따로 설정한 적이 없으니 window로 설정됩니다.

'say 컨텍스트': {
  변수객체: {
    arguments: null,
    variable: ['name'], // 초기화 후 [{ name: 'nero' }]가 됨
  },
  scopeChain: ['say 변수객체', '전역 변수객체'],
  this: window,
}

10번째 줄에서 wow 함수가 호출되어 wow 함수 컨텍스트가 생성됩니다.

여기서 자바스크립트는 lexical scoping을 따르기 때문에, wow 함수의 스코프 체인은 함수를 선언할 때 이미 정해졌습니다.

따라서 say 스코프는 wow 컨텍스트의 scope chain이 아닙니다.

'wow 컨텍스트': {
  변수객체: {
    arguments: [{ word : 'hello' }],
    variable: null,
  },
  scopeChain: ['wow 변수객체', '전역 변수객체'],
  this: window,
}

wow 함수의 컨텍스트까지 생성된 후 wow 함수가 실행됩니다.

console.log(word + ' ' + name);

word는 wow 함수 컨텍스트의 argument에서 찾을 수 있고, name 변수는 wow 함수의 컨텍스트에 존재하지 않으므로 scope chain을 따라 전역 변수 컨텍스트에서 찾게 됩니다. 전역 스코프에 variable에 name이 zero로 되어 있습니다.

따라서 "hello zero"가 콘솔에 찍히게 됩니다.

wow 컨텍스트는 say 컨텍스트와 일절 관련이 없었다는 점!

 

클로저를 왜 사용하는가?

cntPlus() 함수를 실행시켰을 때만 cnt 변수의 값을 변경시킬 수 있다고 가정해봅시다.

그런데 클로저를 사용하지 않고 전역 변수로 cnt를 선언하게 되는 아래의 경우를 생각해봅시다.

이렇게 될 경우 마지막 console.log로 101이 나오게 됩니다.

이 경우 우리가 가정한 cntPlus() 함수만으로 cnt 값을 구현할 수 없게 됩니다.

let cnt = 0;
function cntPlus() {
	cnt = cnt + 1;
}
console.log(cnt);
cntPlus();
console.log(cnt);

// 1억개의 코드
cnt = 100;
// 1억개의 코드

cntPlus();
console.log(cnt);

아래와 같이 전역변수가 아니라 클로저를 사용하게 되는 경우를 살펴봅시다!

이렇게 클로저를 사용하면 전역변수를 사용하지 않고 우리가 원하는 가정을 완벽하게 구현할 수 있습니다.

function closure() {
	let cnt = 0;
    function cntPlus() {
    	cnt = cnt + 1;
    }
    function setCnt(value) {
    	cnt = value;
    }
    function printCnt() {
    	console.log(cnt);
    }
    return {
    	cntPlus,
        setCnt,
        printCnt
    }
}

const cntClosure = closure(); // closure() 함수에서 반환한 함수들이 담긴 객체가 리턴됨
console.log(cntClosure);
cntClosure.printCnt();
cntClosure.cntPlus();
cntClosure.printCnt();
cntClosure.setCnt(100);
cntClosure.printCnt();
// closure 함수 내부의 cnt 값은 함수의 외부에서 접근할 수 없음
cnt = 100; // 이 코드는 실행 불가능함.
  • 상태 유지: 현재 상태를 기억하고 변경된 최신 상태를 유지할 수 있다.
  • 전역 변수의 사용 억제: 상태 변경이나 가변 데이터를 피하고, 오류를 피하는 안정성을 증가시킬 수 있다.
    • 위의 예제를 통해 이해할 수 있습니다.
  • 정보의 은닉: 클래스 기반 언어의 private 키워드를 흉내낼 수 있다.

 

References

 

728x90
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함