본문 바로가기

Study/JavaScript

Scope, Execution context, Closure

Before Start

"scope"란 범위, 영역을 뜻하는 용어이다. 이번 chapter에서는 scope가 어떤 범위와 영역을 말하는 것인지부터 호이스팅, 클로저, 실행 컨텍스트의 개념 등에 대해 함께 다루어 볼 것이다. 이 개념들은 scope라는 개념을 필요로 한다는 점에서 공통된 속성을 가지고 있기 때문이다. 

 

+) global scope은 코드의 전체 범위, 즉 어떤 변수를 전역에서 참조할 수 있음을 말한다.

+) 반면 local scope은 어떤 변수를 특정한 지역 내에서만 참조할 수 있음을 말한다. 이 때 특정한 지역은 { 코드 블럭 }또는 함수를 말한다. 코드 블럭은 중괄호를 사용하는 대부분의 문(statement)을 말하는데, if( ) { 조건문 }, for( ) { 반복문 } 등이 있다.

Scope

The scope is the current context of execution in which values and expressions are "visible" or can be referenced.
If a variable or expression is not in the current scope, it will not be available for use. Scopes can also be layered in a hierarchy, so that child scopes have access to parent scopes, but not vice versa.

=>  scope란, 값과 표현식이 보여지거나 ​​참조될 수 있는 현재 실행 컨텍스트를 말한다.
=> 만약 어떤 변수나 표현식이 현재 scope에 존재하지 않으면, 그 변수나 표현식에 접근할 수 없다(not available).
=> scope에는 계층이 있기 때문에 자식 스코프가 부모 scope에 접근할 수 있다. 하지만 그 반대(부모가 자식에 접근)는 적용되지 않는다. 즉, "코드 블럭 내부에서는 바깥을 볼 수 있고, 밖에서는 안을 볼 수 없다"

=> 간단히 말해 scope란, 변수를 인식할 수 있는(변수의 값에 접근할 수 있는) 범위이다. 

1. Global Scope

The default scope for all code running in script mode.

2. Module Scope

The scope for code running in module mode.

3. Function Scope

The scope created with a function.

=> 하나의 함수는, 하나의 scope를 만든다.
=> 따라서 그 함수 내부에 선언된 변수를 함수 외부에서나, 다른 함수에서 참조할 수 없다. 

 

let myDepartment='front-end';

function change(department) {
    department = 'back-end';
    return department;
}

change(myDepartment); // 'back-end'
myDepartment; // 'front-end'

 

change의 매개 변수 department는 함수 내부에서만 참조가 가능한 값이기 때문에, 함수의 실행이 끝난 뒤에는, 즉 함수의 바깥에서는 다시 원래의 값을 출력한다. 

 

function getEggsNumber() {
  let eggs = 6;
  return eggs;
}

getEggsNumber(); // 6
console.log(eggs); // ref error!

 

위의 예시에서 함수를 실행 한 뒤에 eggs 변수에 접근해서 값을 읽으려고 하자 에러가 발생한다. 함수 블럭 안에서 선언된 변수는 그 함수의 내부에서만 접근 가능하기 때문에, 함수의 실행이 종료되면 더이상 블럭 내부에 접근할 수 없게 된다. 

 

let eggs = 0;

function getEggsNumber() {
  eggs = 6;
  return eggs;
}

eggs; // 0
getEggsNumber(); // 6
eggs; // 6 forever change!

 

반면 위의 예시에서는 함수가 전역(global scope)에 선언되어있는 eggs 변수에 직접 접근해서 값을 재할당하고 있다. 따라서 함수의 실행이 종료된 이후에도 eggs 변수에 접근할 수 있다. 뿐만 아니라 전역에 선언되어있던 변수의 값을 직접 변경했기 때문에(0=>6), 그 값이 함수가 종료된 이후에도 값이 바뀌어있는 것을 확인할 수 있다. 

 

let eggs = 0;

function getEggsNumber() {
  let eggs = 6;
  return eggs;
}
console.log(getEggsNumber()); // 6
console.log(eggs); // 0

 

위의 예시에서는 전역에 이미 선언되어있는 변수의 이름과 똑같은 변수를 함수 내부에서 다시 선언했을 때를 나타냈다. 이 때 이미 선언되어있는 변수라는 에러가 발생하지 않는다. 함수 내부에 선언된 변수는 함수 안에서만 참조할 수 있을 뿐, 외부에 영향을 끼치지 않기 때문이다. 

4. Block Scope

In addition, variables declared with let or const can belong to an additional scope, Block scope. The scope created with a pair of curly braces (a block).

=> var로 선언된 변수는 블럭 내부에 선언되어 있다고 하더라도, 블럭 스코프를 갖지 않는다. 즉,  블럭 밖에서도 접근할 수 있다. 
=> 함수 내부에 var로 선언된 변수는 함수 스코프를 갖는다. 즉, 함수 외부에서 접근할 수 없다. 

 

// loop statement
for(let i=0; i<5; i++) {
    console.log(i); // 0, 1, 2, 3, 4
}
console.log(i); // ref err! 

// conditional statement
if(true) {
    let number = 0;
    console.log(number); // 0 
}
console.log(number); // ref err!

 

for 문에서 자주 사용되는 i, 조건문 내부의 변수 number는 모두 해당 블럭 안에서만 접근 가능하다. 

 

ex) 

const a = 1 {
// { global scope, Lexical Env = {Env Rec: 'a = 1', 부모 block: null} }
  const a = 2 {
  //{ block1 scope, Lexical Env = {Env Rec: 'a = 2', 부모 block: global scope} }
	const a = 3
    // { block2 scope, Lexical Env = {Env Rec: 'a = 3', 부모 block: block1 scope} }
    console.log(a) // 어떤 값이 출력될까요? 
    }
  }

 

여기서 부모 block에 저장되는 정보들이 각 블럭의 가장 가까운 부모를 가리키는 것을 알 수 있는데, 이를 "scope chain" 이라고 부른다. 

자. 그래서 console.log(a)에는 어떤 값이 출력될까? 자바스크립트 엔진은 이 코드를 만나면 해당하는 블럭의 Lexical Env 객체에서 a라는 변수를 찾는다. a=3이라는 정보가 저장되어 있으므로, console에는 3이 출력된다. 만약 현재 블럭에서 a라는 변수가 저장되어 있지 않다면 어떻게 될까? 현재 렉시컬 환경의 부모 block 으로 이동한다. 그런 다음 block1의 Lexical Env에서 a 변수를 찾아 볼 것이다. 'a=2'를 찾게되므로, 이 때에는 2가 출력될 것이다.

 

+) 자바스크립트 엔진이 chain을 통해 바쁘게 돌아다니길 원치 않는다면, "필요한 곳에 변수를 선언하는 것이 중요하다"는 결론에 이른다.

Execution Context & Lexical Environment | 링크에 연결된 article 번역

+) scope vs. context 

scope pertains to the variable access of a function when it is invoked and is unique to each invocation.
Context is always the value of the this keyword, which is a reference to the object that “owns” the currently executing code.

Execution Context

실행 컨텍스트란 다음을 일컫는다. 

 

Simply put, an execution context is an abstract concept of an environment where the Javascript code is evaluated and executed. Whenever any code is run in JavaScript, it’s run inside an execution context.

=> js 코드가 평가되고 실행되는 추상적 공간을 실행 컨텍스트라고 한다. 
=> js에서 실행되는 모든 코드는 언제나 실행 컨텍스트 내에서 실행된다. 

 

이와 같은 실행 컨텍스트는 총 3가지로 나누어 볼 수 있다. 

 

  1. global execution context | 기본 값(the default or base execution context), 함수 내부에 있지 않은 모든 코드는 global execution context에서 평가되고 실행된다. 
    • 전역 실행 컨텍스트는 전역 객체(global object, 브라우저에서는 window가 된다)를 만들고, this에 전역 객체를 값으로 할당한다. 
    • JS엔진이 우리가 작성한 코드를 실행하기 시작했을 때, stack에 가장 먼저 들어오는 context이다.
    • 우리는 execution context stack 이라는 이름에서 이 context가 가장 나중에 나갈 것임을 예상해 볼 수 있다.
    • 실제로 이 컨텍스트는 코드가 종료될 때 까지 남아있다. 
  2. functional execution context | 함수를 실행할 때 생성되는 컨텍스트 
    1. 어떤 함수를 실행하면, 그 함수의 실행 컨텍스트가 새롭게 만들어진다. 
    2. JS엔진이 함수를 실행하는 코드를 만날 때 마다 stack에 저장하는 context로, 함수가 종료되면 stack에서 나간다. 
  3. eval function execution context | eval 함수를 통해 실행되는 코드가 갖는 컨텍스트, 자주 사용되지는 않는다. 

Execution Stack 

함수의 실행 순서를 저장하는 call stack이 있었다. 이와 비슷하게  Execution stack 이라는 장소가 있다. JS 엔진이 코드를 한줄 한줄 실행하면서 만나는 Execution context(실행 컨텍스트)를 저장하는 장소이다. 

 

  1. js 파일을 처음 실행하면, js 엔진은 global execution stack을 만들고 이것을 current execution stack에 집어넣는다. 
  2. js 엔진이 함수 실행 코드를 만나면, 이 함수를 위한 execution context를 새롭게 만들고, execution stack의 가장 위(top)에 이것을 쌓는다(stack). 
  3. js 엔진은 execution stack의 가장 위에 놓인 함수를 실행한다.
  4. 함수의 실행이 끝나면, 이 함수는 execution stack에서 제거된다(pop off).

How is the Execution Context created created? 

실행 컨텍스트(execution context)는 두 단계를 거쳐 생성된다. 1) creation phase(생성 단계)와 2) 실행 단계이다. 

 

1) creation phase | Lexical Environment, Variable Environnment

ExecutionContext = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref. to VariableEnvironment in  memory>,
}

 

The identifier is only used to identify an entity uniquely in a program at the time of execution whereas, a variable is a name given to a memory location, that is used to hold a value.

 

먼저 렉시컬 환경(Lexical environment)이란 identifier와 variable의 연결(mapping)을 가지고 있는 객체 데이터이며, 다음과 같은 값을 저장하고 있다.

 

  1. environment record | 변수와 함수의 선언이 저장되는 장소
  2. other lexical environment reference
    1. 바깥 lexical environment에 접근할 수 있다. 
    2. 이 덕분에 우리는 "안에서 바깥을 볼 수 있다".
    3. 즉, 현재 렉시컬 환경에 존재하지 않는 함수나 변수가 있다면, 그 바깥 환경에 이 함수나 변수가 존재하는지 확인할 수 있다. 
  3. this 
    1. global execution context에서 this는 전역 객체를 가리킨다. 즉 브라우저에서는 window가 된다. 
    2. function execution context에서 this는 함수를 호출하는 방식에 따라 달라진다.
      • 만약 객체의 메서드로 함수를 호출하고 있다면, 함수 실행 컨텍스트의 this는 그 객체를 가리킨다.
      • 함수를 일반적으로 호출하고 있다면 함수 실행 컨텍스트의 this는 global object 또는 undefined(strict mode)을 가리킨다. 

 

함수 실행 컨텍스트의 pseudo 코드는 다음과 같이 표현할 수 있다. 

 

FunctionExectionContext = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
    }
    outer: <Global or outer function environment reference>,
    this: <depends on how function is called>
  }
}

 

+) Variable Environment | 렉시컬 환경이 let, const로 선언된 변수화 함수 선언을 저장한다면 variable environment은 var로 선언된 변수만 저장한다. 

 

2) execution phase | 모든 변수에 값이 할당 되고, 코드가 실행된다.

ex1) 

let a = 20;
const b = 30;
var c;

function multiply(e, f) {
 var g = 20;
 return e * f * g;
}

c = multiply(20, 30);

 

+) 곱하기 함수의 실행이 완료되면, 변수 c에 값이 할당되고 global execution context 객체가 업데이트 될 것이다(c: undefined => 값). 

 

GlobalExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      a: 20, // creation 단계에서는 not initialized 저장
      b: 30,
      multiply: < func >
    }
    outer: <null>,
    ThisBinding: <Global Object>
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // Identifier bindings go here
      c: undefined,
    }
    outer: <null>,
    ThisBinding: <Global Object>
  }
}

FunctionExectionContext = {
LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      Arguments: {0: 20, 1: 30, length: 2},
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>,
  },
VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // Identifier bindings go here
      g: 20 // creation 단계에서는 undefined 저장 
    },
    outer: <GlobalLexicalEnvironment>,
    ThisBinding: <Global Object or undefined>
  }
}

 

ex2) 

function bankRobbery() {
  const heroes = ['captain marvel', 'spiderman', 'wonder womyn', 'iron man'];
  function cryForHelp() {
    heroes.forEach((hero) =>
      console.log(`PLEASE HELP US, ${hero.toUpperCase()}`)
    );
  }
  cryForHelp(); // here  
}

bankRobbery();

 

bankRobbery라는 함수 내부에 선언되어있는 cryForHelp에서 heroes 배열에 접근할 수 있는 것은 이 함수의 렉시컬 환경에 저장되어있는 outer를 통해, bankRobbery에 접근할 수 있기 때문이다. 

Garbage Collector 

모든 변수를 global 변수로 설정하면 아무데서나 접근이 가능하니까 좋을 것 같지만, memory의 관점에서는 그렇지 않다. 

 

우리가 global 변수에 data를 할당하면, JS엔진은 그 data를 app이 실행되는 순간부터 종료되는 순간까지 메모리에 저장해 둔다. 반면 local 변수에 저장된 data의 경우, 해당 변수가 사용되고 있는 코드 블럭을 실행할 때, 데이터가 메모리에 저장되었다가 블럭이 종료되면 메모리에서 삭제된다. 모든 코드 블럭이 종료될 때 마다 이런 일이 일어나는 것은 아니고, 자바스크립트 엔진에 내장된 "Garbage Collector"가 app이 background 상태일 때 더이상 참조되지 않는 변수들을 삭제함으로써 메모리를 정리한다. 

 

따라서 변수를 선언할 때에는 전역에 선언하는 것이 아니라, 필요한 블럭 안에 (블럭 안에서도 필요한 블럭에) 선언하는 것이 중요하다. 

Closure

모든 실행 컨텍스트는 Lexical environment라는 객체 데이터를 가지고 있고, 이 데이터에는 부모 컨텍스트에 대한 정보가 저장되어있다. 이 chian 덕분에 내부에서 외부 블럭의 렉시컬 환경으로 접근하는 것이 가능하다.

closure는 바로 이 개념의 하위 개념이다. closure는 어떤 함수 내부에 있는 함수가, 자신을 감싸고 있는 함수의 렉시컬 환경에 접근할 수 있는 것을 말한다. 

 

function makeCounter() {
  let count = 0;
  function increase() {
    count++;
    console.log(count);
  }
  return increase;
}

 

즉 increase 함수는 자신을 둘러싸고 있는 makeCounter함수의 렉시컬 환경에 접근할 수 있으므로, count 변수 값에 접근할 수 있다. 이것은 outer와 다를 바 없는 개념처럼 느껴진다. 그렇다면 왜 모든 사람들이 closure에 대해 이야기하는 걸까? 그 비밀은 makeCounter함수의 실행 결과, 즉 return 값에 있다. 

 

const increase = makeCounter();
increase();

 

우리는 makeCounter가 반환한 increase 함수를 increase 변수에 할당한 뒤, 이를 실행함으로써 count의 값을 업데이트 할 수 있다. count의 값을 "직접" 수정할 수 있는 방법은 없고, 반환된 increase함수를 통해서만 count의 값을 수정할 수 있다. class의 encapsulation 개념과 동일하다. 

class에서 어떤 property(state)를 private하게 만들고, public 함수를 통해서만 이 property에 접근할 수 있게 만드는 것, 이것이 closure의 핵심개념이다. 선후 관계만을 따지면 class의 핵심 개념이 closure라고 해야 할지도 모르겠다. 

 

class Counter {
  count=0;
  increase() {
    this.count++
    console.log(this.count)
    }
}

const counter = new Counter();
counter.increase(); // 1
counter.count++; // this is bad

 

'Study > JavaScript' 카테고리의 다른 글

This  (0) 2022.04.05
OOP | Prototype, Class  (0) 2022.04.05
Asynchronous | callBack, promise, async & await  (0) 2022.04.02
Call stack & Task que  (0) 2022.04.01
Modularization  (0) 2022.03.31