JavaScript

10.1. 프로토타입 : 스코프 체인 vs 프로토타입 체인

문정훈 2022. 2. 12. 23:38

10절부터 계속 프로토타입에 대해 정리할 것이며 10절의 첫 주제로 프로토타입과 스코프 체인을 어느 정도 공부한 시점에서 헷갈릴 수 있는 내용인 스코프 체인 vs 프로토 타입 체인 두 개념 구분해 정리할려고 한다. 

 

1. 스코프 체인이란 

자바스크립트에는 4가지 타입의 소스코드가 있다. 

  1. 전역코드
  2. 함수코드
  3. eval코드
  4. 모듈 코드

4가지 소스코드의 타입은 스코프라는 것을 가지는데 예를 들어 함수 코드의 스코프를 설명하면 함수 내부에 선언된 변수 a가 있다고 해보자 이 a가 유효한 범위를 해당 함수의 스코프라고 한다.

 

함수 코드를 생각해보면 함수 선언문이 작성된 스코프(전역 객체라고 하겠다.)의 "평가" 과정 즉 전역 코드의 평가 과정이 이루어지면 함수 선언문의 이름을 key로 하며 그 값으롷 함수 객체를 생성하여 key에 값으로 할당한다.

이런 key와 값은 heap 영역에 저장된다.

함수 객체의 [[Environment]] 내부 슬롯 즉 함수 객체의 상위 스코프를 가리키는 내부 슬롯으로 자신(함수 객체)를 생성시킨 스코프 즉 전역 객체를 함수 객체의 상위 스코프로 가지며 이는 변경되지 않는다.( 이것이 렉시컬 스코프의 개념이다.)

 

그리고 전역 코드가 런타임 (실행)을 하게 되면 싫애 컨텍스트 스택에 전역 실행 컨텍스트가 push되어 실행되다가

전역 코드에서 함수의 호출이 이루어지면 함수의 실행 컨텍스트가 실행 컨텍스트 스택에 push 되면서 

전역 실행 컨텍스트는 잠시 실행을 멈추고 함수 코드가 "평가"되고 "실행"되게 된다. 

여기까지가 실행 컨텍스트에 관한 기본적인 내용이다 (자세한 내용은 7.3절 참고)

 

그래서 하고싶은 말이 뭐냐?

이후 정리하는 프로토타입 체인과 스코프 체인이 상당히 헷갈릴 수 있다.  이 둘을 정확히 비교하는 것이 이 절의 목표이다.

아래 코드를 보자

a = 1;
function foo() {
  //a = 2;
  function bar() {
    console.log(a);
  }

  bar();
}

foo();
//결과 1

스코프 체인은 언제 주로 생각하나?? 위 코드가 실행되고 foo의 호출로 bar가 호출될 때 bar는 자신의 스코프 영역에서 a를 찾으려고한다. 하지만 bar 내부에는 a가 없다.

따라서 bar가 가지는 상위 스코프 [[Environment]]의 값(=foo)에서 a를 찾으려고한다. 

하지만 foo내부에서도 a는 선언되어 있지않다. 따라서 foo의 상위 스코프 [[Environment]]의 값(=global)인 전역 객체에서 a를 찾을려고 할 것이며 1을 출력하게 된다. 

이것이 스코프 체인이다. 

위 코드를 "평가" "실행"과정과 실행 컨텍스트 관점에서 생각해보면

처음 전역 객체가 "평가" 될 때 어디까지 실행되냐?

  • a = undefined
  • heap영역에 foo = 함수 객체 할당 그리고 상위 스코프로 global 저장
  • heap영역에 bar = 함수 객체 할당 그리고 상위 스코프로 foo 저장

여기까지가 전역 코드의 "평가"과정에서 이루어지는 작업들이다.

위 과정(전역 코드의 평가)가 진행되었다면 사실상 foo와 bar의 객체가 이미 heap영역에 만들어져 있고 상위 스코프 또한 모두 결정이 된 상태이다. 

함수 foo, bar가 호출되어 실행 컨텍스트 스택에서 함수의 실행 컨텍스트를 생성하여 함수 렉시컬 환경(function Lexical Environment) 의 외부 렉시컬 환경에 대한 참조(OuterLexical Environment Reference)의 값에 함수(foo, bar)들의 [[Environment]]의 내부 슬롯의 값이 할당 될 때 비로서 스코프 체인이 의미가 있어진다.

의미가 있다는 것이 무슨 말이냐? 아래 코드를 보자

 

r = 1;
function getArea() {
   var r = 2;
}

getArea.prototype.r = 3;

var a = new getArea();
console.log(a.r);

//결과 3

이후 정리할 프로토타입의 개념을 안다면 위 코드의 결과는 당연히 3이라고 받아들여질 것이다.

하지만 스코프 체인과 프로토타입의 체인이 어떻게 생성되며 각각 어떤 의미를 지니는지 명확히 알지 못한다면

위 코드를 이렇게 해석하는 오류를 범할 수 있다..(내가 그랬음..)

 

※ 잘못된 생각

getArea의 함수의 상위 스코프는 전역 코드이다. 

console.log로 r을 호출하는데 getArea 함수의 상위 스코프는 전역 코드니깐 console.log(a.r)의 결과는 1이 아닌가??

아니 그럼 프로토타입 체인과 스코프 체인 중 누굴 더 먼저 참고하나?? 

이런 아주 아주 말도 안되는 논리를 생각할 수 있다.  (내가 그랬다...)

이것 외에도 스코프 체인과 프로토타입 체인을 같은 level의 체인으로 인식해버리는 순간 잘못된 다른 생각을 할 가능성이 크며 이 둘 체인이 어떻게 생성는지 잘 알아야한다.

 

※ 예시 코드를 옳바르게 해석해보면

getArea의 상위 스프는 전역 코드의 평가 과정에서 객체로 heap 영역에 생성되므로 getArea의 [[Environment]]는 global이 맞다.

그리고 getArea의 프로토타입 객체에 r 값을 3으로 지정하여 

현재 getArea는 r=3이라는 프로퍼티를 상속받은 것이다. 

 

위 코드를 실행 컨텍스트의 관점에서 생각해보면 실행 컨텍스트 스택에는 최초에 전역 코드의 실행 컨텍스트가 push되고 이후에는 어떠한 실행 컨텍스트도 만들어지지 않는다. 따라서 함수의 스코프 체인으로써 생각을 하는 것은 애초부터 틀린 접근 방식이다. 

단순히 위 코드만 보면 만약 getArea가 생성자 함수가 아닌 일반 함수로 호출되는 문이 있었다면 getArea의 실행 컨텍스트가 생성되고  함수의 렉시컬 환경(function Lexical Environment) 의 외부 렉시컬 환경에 대한 참조(OuterLexicalEnvironmentReference)의 값이 할당되면서 만약 함수 내부에 a가 없다면 상위 스코프인 전역 객체에서 a를 찾을 것이다. 

 

 

2.  프로토타입 체인

예시 코드1)

r = 1;
function getArea() {
   var r = 2;
}

getArea.prototype.r = 3;

var a = new getArea();
console.log(a.r);

//결과 3

 

일반 함수의 호출과 스코프 체인과의 관계 vs 함수로 생성자 함수의 호출과 프로토타입의 체인

이 둘을 구분해야한다. 

전자에 대해 설명을 했으므로 후자에 대해 설명하면 우선

생성자 함수로 객체를 생성하는 것은 heap영역에 저장된 생성자 함수의 객체(빵틀)로 heap영역에 생성자 함수로 생성된 객체(만들어진 빵)가 생성된다. 

그리고 그렇게 만들어진 객체는 prototype이라는 필드를 가지며 이는 부모 객체로 생각하면 된다. 

prototype 객체에 필드 또는 메소드를 작성하는 것으로 getArea(자식 객체)는 부모 객체(prototype)의 필드와 메소드를 상속받게 되고 사용할 수 있게 된다.

이처럼 getArea와 getArea.prototype의 연결 관계를 프로토타입 체인이라고 부른다. 

따라서 위 코드에서 r을 생각할때 스코프 체인은 애초부터 생각을 할 필요가 없다. (전혀 관련이 없는 내용이기 때문)

heap영역에 있는 a라는 객체는 r을 찾을려고하는데 a라는 객체에는 r이라는 프로터가 없다. 따라서 프로토타입 체인에 따라 getArea.prototype에서 a를 찾으며 3을 반환하게 된다. 

 

※ 또 착각할만한 내용

a.r을 호출하는것으로 var r = 2; 이 코드가 있는데 그럼 getArea에 r이 있는 것이 아닌가? 

따라서 결과가 2가 나와야하는 것이 아닌가?

이런 잘못된 생각을 할 수 있다. 

이것은 생성자 함수의 동작 원리를 정확히 이해하지 못해서 발생하는 오류이다. 

getArea의 생성자 함수에서 new 연산자를 통해 생성된 객체는 var r =2;라는 것을 가질 수 없다.

자바에서 클래스의 맴버는 필드, 메소드, 생성자인데 지역변수가 맴버가 될 수 있는가? 이것과 똑같은 개념이다. 

아래 코드를 보자. 

 

예시 코드2)

function getArea() {
    var a = 10;
    this.file1 = 20;
    this.method = function() {}
}

var GA = new getArea();

GA라는 객체에는 filed1이라는 필드와 method 메소드를 객체의 맴버로 가진다. 

var a = 10의 값은 객체의 맴버가 될 수 없다. 

 

그렇다면 예시 코드1을 아래와 같이 변경해보면?

r = 1;
function getArea() {
   this.r = 2;
}

getArea.prototype.r = 3;

var a = new getArea();
console.log(a.r);

//결과 2

이 경우는 이제 2가 출력되게 된다.

getArea 생서자 함수로 생성된 객체는 r을 필드로 가지며 이 객체의 프로토타입 (부모 객체)의 필드로 r이 있는 상횡이 된다.  a.r을 호출하게 되면 a객체의 r= 2라는 필드를 먼저 참조(프로토타입 체인에 의해)하게 되므로 출력 결과는 2가 되게 된다. 

 

 

다음 절 부터 본격적으로 프로토타입에 대해 정리한다.