Intro

"자바스크립트는 프로토타입 기반의 언어이다."
자바스크립트를 공부하는 사람은 한번쯤 접했을 혹은 접할 문구이다. 그만큼 프로토타입은 자바스크립트에서 중요한 개념이지만 더불어 자바스크립트를 시작하는 사람들에게 혼돈의 카오스를 선사하는 개념으로 유명하기도 하다. 나 역시 그 혼돈의 카오스를 겪어본 자바스크립트를 시작하는 사람으로써 조금이지만 이해한 바를 나누고 싶은 마음에 글을 쓴다.

목차

  • 모든 객체는 proto를 가지고 있고 함수는 prototype도 가지고 있다.
  • 프로토타입 식별자 룩업
  • 프로토타입을 통해 객체를 생성하면 메모리 효율을 높일 수 있다.
  • 프로토타입을 이용해 객체를 만드는 방법 첫번째 생성자
  • 프로토타입을 이용해 객체를 만드는 방법 두번째 Object.create()
  • 프로토타입 변경시 주의사항
  • 번외 : 모든 prototype의 type은 객체이다. Function()의 prototype만 빼고, 왜일까?

모든 객체는 proto를 가지고 있고 함수는 prototype도 가지고 있다.

구글에 "what is prototype in javascript definition(프로토타입 정의가 뭐임?)" 라고 검색하면 가장 첫줄에 이 문장이 나온다.

When a function is created in JavaScript, JavaScript engine adds a prototype property to the function. (https://hackernoon.com/prototypes-in-javascript-5bba2990e04b)

함수가 생성될 때 자바스크립트 엔진은 그 함수의 property로 프로토타입을 추가한다고 한다. 그럼 함수를 만들어보자.

function Human(name) {
  console.log(`I am ${name}`);
}

그리고 이 함수를 펼쳐보면 아래와 같이 프로토타입 property가 추가된 것을 볼 수가 있다. 그리고 이렇게 생성된 프로토타입은 객체라는 것도 아래를 통해 확인할 수 있다.
post1.png
혹시 나처럼 "property는 객체가 갖는 건데 어떻게 함수가 가지고 있지?" 라는 의문이 드는 사람이 있다면 자바스크립트의 함수는 객체라는 점을 기억하도록 하자. 이 실험을 통해 우리는 자바스크립트 엔진이 함수가 생성될때 프로토타입이라는 객체를 property로 추가한다는 것을 알았다.

이제 우리는 자바스크립트에서 객체가 생성되는 방법에 대해 생각해볼 필요가 있다. 자바스크립트는 객체를 생성하는 방식은 두가지가 있다. 객체 리터럴 방식과 생성자 함수를 통해 생성하는 것이다.

var objectMadeByLiteral = {};  //객체 리터럴 방식
var objectMadeByConstructor = new Object(); // 생성자 함수 방식

두번째 라인은 Object의 생성자 함수를 통해 객체를 생성하고 있고 첫번째 라인의 객체 리터럴 방식은 두번째 라인의 일종의 숏컷일 뿐이다. 즉, 자바스크립트는 객체 리터럴을 사용해도 생성자 함수를 통해 객체를 생성한다. 이렇게 생성된 객체는 생성자 함수의 프로토타입을 공유하게 되는데 이때 사용되는 것이 proto이다. 다시 말해서 객체는 생성자 함수의 프로토타입을 참조하는 링크 정보를 갖게 되고 그것이 proto이며 이를 통해 프로토타입에 접근하게 된다. 이 점이 프로토타입의 가장 큰 장점인 높은 메모리 효율의 요인이 된다. 앞서 프로토타입 역시 객체라고 설명했다. 그렇다면 프로토타입도 proto를 가지고 있을까? 그렇다. 프로토타입역시 proto를 가지고 있고 따로 이에 대한 제어를 하지 않았다면 이 proto는 Object 생성자 함수의 프로토타입을 링크하고 있게 된다. 이러한 프로토타입의 연결을 프로토타입 체인이라고 한다.

프로토타입 식별자 룩업

자바스크립트가 식별자를 룩업하는 방법은 두가지가 있다. 스코프 룩업과 프로토타입 룩업인데 여기서는 프로토타입 룩업에 대해 설명하고자 한다. 앞서 모든 객체는 proto라는 property를 가지고 있고 그 객체의 생성자 함수의 프로토타입을 링크하고 있다고 했다. 자바스크립트는 식별자를 찾을때 먼저 해당 객체에서 찾아 보고 없으면 proto 링크를 통해 이 객체의 생성자함수의 프로토타입에서 식별자를 찾는다. 즉 프로토타입 룩업이란 프로토타입 체인을 통해 자바스크립트가 식별자를 찾아가는 것을 말한다. 모든 프로토타입 체인의 끝은 Object.prototype이다. 그리고 Object.prototype의 proto는 null을 갖고 있으므로 여기서 룩업을 종료하게 된다.

아래 코드에서 c 객체가 attr1속성을 찾아가는 과정을 이해해보자.

var a = {
    attr1: 'a'
};

var b = {
    __proto__: a,
};

var c = {
    __proto__: b,
};

console.log(c.attr1); // 'a'

앞서 설명했듯이 proto는 해당 객체의 prototype을 참조한다. 즉, 이 코드에서 c의 프로토타입은 b가 되고 b의 prototype은 a가 되며 a의 prototype은 객체 리터럴 방식으로 a가 선언되었으므로 Object 생성자함수의 프로토타입이 된다. 참고로 proto는 최대한 직접 접근을 하지 않는 것이 좋다. 이 예제는 이해를 돕기 위해서만 사용한다.

  1. attr1을 c 객체에서 찾는다. => 없다.
  2. c의 proto를 통해 c의 프로토타입인 b 객체에서 attr1을 찾는다. => 없다.
  3. b의 proto를 통해 b의 프로토타입인 a에서 attr1을 찾는다. => 있다.
  4. 'a'를 출력한다.

그렇다면 a에도 없는 속성인 attr0를 찾는다고 가정하면 어떻게 될까. 3번까지는 동일한 과정이 진행되고 a에 객체에도 attr1이 없으므로 a의 prototype인 Object.prototype에서 찾게되고 거기도 없으므로 proto를 따라가려고 하지만 Object.prototype엔 proto가 없어서 결국 undefined를 리턴하게 된다.

  1. attr0을 c 객체에서 찾는다. => 없다.
  2. c의 proto를 통해 c의 프로토타입인 b 객체에서 attr0을 찾는다. => 없다.
  3. b의 proto를 통해 b의 프로토타입인 a에서 attr0을 찾는다. => 없다
  4. a의 proto를 통해 a의 프로토타입인 Object.prototype에서 attr0를 찾는다. => 없다
  5. Object.prototype의 proto를 찾아보았으나 없다.
  6. undefined 리턴

이렇게 프로토타입 체인을 따라서 식별자를 찾아가는 것을 프로토타입 룩업이라고 하며 자바스크립트가 가지는 중요한 특징 중 하나이다.

프로토타입을 통해 객체를 생성하면 메모리 효율을 높일 수 있다.

프로토타입은 메모리 복사가 아닌 proto라는 참조 변수를 통해 연결되므로 한 메모리 공간만을 차지하게 된다. 따라서 함께 사용하는 식별자가 있을 때는 프로토타입 체인을 연결함으로써 메모리 효율을 높일 수가 있다.

프로토타입을 이용해 객체를 만드는 방법 첫번째 생성자

생성자를 통해 객체를 만들게 되면 생성자 함수의 프로토타입이 생성된 객체의 프로토타입 체인으로 연결된다.

//constructor
function Parent(name) {
    this.name = name;
}

Parent.prototype.getName = function() {
    return this.name;
};

var p = new Parent('myName');
p.getName();

Parent라는 생성자가 있고 이 생성자의 prototype에 getName이라는 메소드를 추가하였다. Parent 생성자 함수를 통해 생성한 p는 proto를 통해 Parent의 프로토타입에 프로토타입 체인으로 연결돼 있어서 getName 메소드를 프로토타입 룩업을 통해 사용 가능하다.

프로토타입을 이용해 객체를 만드는 방법 두번째 Object.create()

Object.create() API는 객체를 인자로 받아서 그 객체가 프로토타입 체인으로 연결된 새로운 객체를 리턴해준다. 위에서 프로토타입 룩업을 설명하기 위해 proto를 직접 접근해서 프로토타입 체인을 만들었지만 피해야할 방법이고 Object.create를 통해 프로토타입 체인을 연결해주는 것이 좋다.

var a = {
    attr1: 'a'
};

const b = Object.create(a);

const c = Object.create(b);

console.log(c.attr1); // 'a'

프로토타입 변경시 주의사항

객체를 생성할때 프로토타입이 결정된다. 결정된 프로토타입은 다른 임의의 객체로 변경될 수도 있다. 즉, 부모 객체의 프로토타입이 변경될수도 있다는 말이다. 만약 프로토타입 변경시 constructor를 삭제하게 되면 그때 예상하지 않은 값이 나올 수도 있다. 아래 예제를 보자.

function Person(name) {
  this.name = name;
}

var foo = new Person('Lee');

// 프로토타입 객체의 변경
Person.prototype = { gender: 'male' };

var bar = new Person('Kim');

console.log(foo.gender); // undefined
console.log(bar.gender); // 'male'

console.log(foo.constructor); // ① Person(name)
console.log(bar.constructor); // ② Object()

foo의 proto에 연결된 프로토타입은 Person의 프로토타입이다. 이것은 foo가 생성되는 시점이 결정이 된다. foo의 prototype이 결정된 이후에 Person의 프로토타입을 constructor가 삭제된 일반 객체로 바꾸게 되면 그 다음 이 Person으로 bar 객체를 생성할때 Person은 constructor property가 없으므로 프로토타입 체인을 통해 Object.prototype.constructor를 사용하게 되면서 bar의 constructor는 Object가 되어버린다.

Person의 프로토타입을 새로운 일반 객체로 생성하게 되면서 bar 는 새로 생성된 프로토타입에 연결되고 foo는 이전에 있었던 Person의 프로토타입에 연결되므로 foo에는 gender가 없고 bar는 gender가 있게 된다.

번외 : 모든 prototype의 type은 객체이다. Function()의 prototype만 빼고, 왜일까?

이 부분은 프로토타입을 따라가던 중 발견한 현상에 대한 단순한 호기심을 해결한 부분이다. 별로 중요하진 않다고 생각한다.

  1. ES5 spec에선 아래와 같이 말하고 있다.

    The Function prototype object is itself a Function object (its [[Class]] is "Function") that, when invoked, accepts any arguments and returns undefined.

  2. ECMAScript 2015에선 아래와 같이 말하고 있다.

    The Function prototype object is the intrinsic object %FunctionPrototype%. The Function prototype object is itself a built-in function object.
    The Function prototype object is specified to be a function object to ensure compatibility with ECMAScript code that was created prior to the ECMAScript 2015 specification

  3. 이에 따르면 ECMAScript와의 compatibility를 위해서 Function 의 prototype은 Function의 object로 그 스스로 function이 된다.

참고