new F()와 같은 생성자 함수를 이용하면 새로운 객체를 만들 수 있다는 걸 앞서 배운 바 있습니다.

그런데 F.prototype이 객체면 new 연산자는 F.prototype을 사용해 새롭게 생성된 객체의 [[Prototype]]을 설정합니다.

주의:

자바스크립트가 만들어졌을 때는 프로토타입 기반 상속이 주요 기능 중 하나였습니다.

그런데 과거엔 프로로타입에 직접 접근할 방법이 없었습니다. 그나마 믿고 사용할 수 있었던 방법은 이번 챕터에서 설명할 생성자 함수의 "prototype" 프로퍼티를 이용하는 방법뿐이었죠. 많은 스크립트가 아직 이 방법을 사용하는 이유가 여기에 있습니다.

F.prototype에서 "prototype"F에 정의된 일반 프로퍼티라는 점에 주의해 주시기 바랍니다. 앞서 배웠던 ‘프로토타입’ 객체와 같아 보이지만 F.prototype에서 "prototype"은 이름만 같은 일반 프로퍼티입니다.

예시:

let animal = {
  eats: true
};

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

Rabbit.prototype = animal;

let rabbit = new Rabbit("White Rabbit"); //  rabbit.__proto__ == animal

alert( rabbit.eats ); // true

Rabbit.prototype = animal은 "new Rabbit을 호출해 만든 새로운 객체의 [[Prototype]]animal로 설정하라."라는 것을 의미합니다.

그림으로 나타내면 다음과 같습니다.

그림에서 가로 화살표는 일반 프로퍼티인 "prototype"을, 세로 화살표는 [[Prototype]]을 나타냅니다. 세로 화살표는 rabbitanimal을 상속받았다는 것을 의미합니다.

F.prototypenew F를 호출할 때만 사용됩니다.

F.prototype 프로퍼티는 new F가 호출될 때만 사용됩니다. new F를 호출해 새롭게 만든 객체의 [[Prototype]]을 할당해 주죠. 할당 후엔 F.prototype과 새 객체의 연관 관계가 사라집니다. F.prototype는 '딱 한 번만 쓸 수 있는 이용권’이라고 생각하면 됩니다.

새로운 객체가 만들어진 후에 F.prototype 프로퍼티가 바뀌면(F.prototype = <another object>) new F로 만들어지는 새로운 객체는 또 다른 객체()를 [[Prototype]]으로 갖게 됩니다. 다만, 기존에 있던 객체의 [[Prototype]]은 그대로 유지됩니다.

함수의 prototype 프로퍼티와 constructor 프로퍼티

개발자가 특별히 할당하지 않더라도 모든 함수는 "prototype" 프로퍼티를 갖습니다.

기본 프로퍼티인 "prototype"constructor 프로퍼티 하나만 있는 객체를 가리키는데, 이 constructor 프로퍼티는 함수 자신을 가리킵니다.

이 관계를 코드와 그림으로 나타내면 다음과 같습니다.

function Rabbit() {}

/* 기본 prototype
Rabbit.prototype = { constructor: Rabbit };
*/

아래 코드를 실행해 직접 확인해봅시다.

function Rabbit() {}
// 기본 prototype:
// Rabbit.prototype = { constructor: Rabbit }

alert( Rabbit.prototype.constructor == Rabbit ); // true

특별한 조작을 가하지 않았다면 Rabbit을 구현한 객체 모두에서 [[Prototype]]을 거쳐 constructor 프로퍼티를 사용할 수 있습니다.

function Rabbit() {}
// 기본 prototype:
// Rabbit.prototype = { constructor: Rabbit }

let rabbit = new Rabbit(); // {constructor: Rabbit}을 상속받음

alert(rabbit.constructor == Rabbit); // true (프로토타입을 거쳐 접근함)

constructor 프로퍼티를 사용하면 기존에 있던 객체의 constructor를 사용해 새로운 객체를 만들 수 있습니다.

아래와 같이 말이죠.

function Rabbit(name) {
  this.name = name;
  alert(name);
}

let rabbit = new Rabbit("White Rabbit");

let rabbit2 = new rabbit.constructor("Black Rabbit");

객체가 있는데 이 객체를 만들 때 어떤 생성자가 사용되었는지 알 수 없는 경우(예: 객체가 서드 파티 라이브러리에서 온 경우), 이 방법을 유용하게 쓸 수 있습니다.

어느 방식을 사용해 객체를 만들든 "constructor"에서 가장 중요한 점은 다음과 같습니다.

자바스크립트는 알맞은 "constructor" 값을 보장하지 않습니다.

함수에 기본으로 "prototype" 값이 설정되긴 하지만 그게 전부 입니다. "constructor"에 벌어지는 일 모두는 전적으로 개발자에게 달려있습니다.

함수의 기본 "prototype" 값을 다른 객체로 바꾸면 이 객체엔 "constructor"가 없을 겁니다.

예시:

function Rabbit() {}
Rabbit.prototype = {
  jumps: true
};

let rabbit = new Rabbit();
alert(rabbit.constructor === Rabbit); // false

이런 상황을 방지하고 알맞은 constructor를 유지하려면 "prototype" 전체를 덮어쓰지 말고 기본 "prototype"에 원하는 프로퍼티를 추가/제거해야 합니다.

function Rabbit() {}

// Rabbit.prototype 전체를 덮어쓰지 말고
// 원하는 프로퍼티는 그냥 추가하세요.
Rabbit.prototype.jumps = true
// 이렇게 하면 기본 Rabbit.prototype.constructor가 유지됩니다.

constructor 프로퍼티를 수동으로 다시 만들어주는 것도 대안이 될 수 있습니다.

Rabbit.prototype = {
  jumps: true,
  constructor: Rabbit
};

// 수동으로 추가해 주었기 때문에 알맞은 constructor가 유지됩니다.

요약

이번 챕터에선 생성자 함수를 이용해 만든 객체에 [[Prototype]]을 설정해 주는 방법에 대해 간략히 알아보았습니다. 이 방법을 기반으로 하는 고급 프로그래밍 패턴에 대해선 추후 학습할 예정입니다.

몇 가지 사항만 명확하게 이해하고 있으면 지금까지 배운 것들은 복잡하지 않습니다.

  • F.prototype 프로퍼티는 [[prototype]]과는 다릅니다. F.prototypenew F()를 호출할 때 만들어지는 새로운 객체의 [[Prototype]]을 설정합니다.
  • F.prototype의 값은 객체나 null만 가능합니다. 다른 값은 무시됩니다.
  • 지금까지 배운 내용은 생성자 함수에 "prototype"를 설정하고, 이 생성자 함수를 new를 사용해 호출할 때만 적용됩니다.

일반 객체에 "prototype" 프로퍼티를 사용하면 아무런 일이 일어나지 않습니다.

let user = {
  name: "John",
  prototype: "Bla-bla" // 마술은 일어나지 않습니다.
};

모든 함수는 기본적으로 F.prototype = { constructor : F }를 가지고 있으므로 함수의 "constructor" 프로퍼티를 사용하면 객체의 생성자를 얻을 수 있습니다.

과제

중요도: 5

아래 코드에선 new Rabbit를 만들고 Rabbit"prototype"을 변겅합니다.

시작 코드는 다음과 같습니다.

function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

let rabbit = new Rabbit();

alert( rabbit.eats ); // true
  1. 아래와 같은 코드를 추가(강조된 줄)하면 얼럿창엔 무엇이 출력될까요?

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    Rabbit.prototype = {};
    
    alert( rabbit.eats ); // ?
  2. 아래와 같이 코드를 변경하면 얼럿창엔 무엇이 출력될까요?

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    Rabbit.prototype.eats = false;
    
    alert( rabbit.eats ); // ?
  3. 아래와 같이 delete를 사용하면 얼럿창엔 무엇이 출력될까요?

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    delete rabbit.eats;
    
    alert( rabbit.eats ); // ?
  4. 마지막 코드를 실행하면 얼럿창엔 무엇이 출력될까요?

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    delete Rabbit.prototype.eats;
    
    alert( rabbit.eats ); // ?

정답:

  1. true

    Rabbit.prototype에 무언가를 할당하면 그 값이 새로운 객체의 [[Prototype]]이 됩니다. 다만 이미 만들어진 객체엔 이 규칙이 적용되지 않습니다.

  2. false

    객체는 참조에 의해 할당됩니다. Rabbit.prototype이 참조하는 객체는 단 하나뿐인데, 이 객체는 Rabbit.prototyperabbit[[Prototype]]을 사용해 참조할 수 있습니다.

    따라서 둘 중 하나의 참조를 사용해 객체의 내용을 변경하면 다른 참조를 통해서도 변경 내용을 볼 수 있습니다.

  3. true

    delete 연산은 객체에 직접 적용됩니다. delete rabbit.eatsrabbit에서 eats 프로퍼티를 제거하는데, rabbiteats가 없습니다. 따라서 delete는 아무런 영향을 주지 않습니다.

  4. undefined

    프로퍼티 eats가 프로토타입에서 삭제되었기 때문에 eats는 더이상 존재하지 않습니다.

중요도: 5

생성자 함수가 하나 있고, 이 생성자 함수를 사용해 만든 임의의 객체 obj가 있다고 가정해봅시다. 지금은 이 생성자 함수를 사용해 새로운 객체를 만들어야하는 상황입니다.

정체를 모르는 생성자 함수를 사용해 새로운 객체를 만드는게 가능할까요?

let obj2 = new obj.constructor();

위와 같은 코드를 사용해 객체를 만들 수 있게 해주는 생성자 함수를 작성해보세요. 여기에 더하여 위와 같은 코드가 동작하지 않도록 하는 예시도 하나 만들어보세요.

"constructor" 프로퍼티에 제대로 된 값이 저장되어있다면 위와 같은 접근법이 가능합니다.

기본 "prototype"를 변경하지 않았다면 아래 예시는 의도한 대로 동작합니다.

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

let user = new User('John');
let user2 = new user.constructor('Pete');

alert( user2.name ); // Pete (잘 동작하네요!)

User.prototype.constructor == User이기 때문에 위 예시는 제대로 동작합니다.

그런데 누군가가 User.prototype를 덮어쓰고 User를 참조하는 constructor를 다시 만들어주는 걸 잊었다면 문제의 접근법은 실패합니다.

예시:

function User(name) {
  this.name = name;
}
User.prototype = {}; // (*)

let user = new User('John');
let user2 = new user.constructor('Pete');

alert( user2.name ); // undefined

user2.nameundefined가 될까요?

그 이유는 new user.constructor('Pete')가 아래와 같이 동작하기 때문입니다.

  1. new user.constructor('Pete')user에서 constructor를 찾는데 아무것도 찾지 못합니다.
  2. 객체에서 원하는 프로퍼티를 찾지 못했기 때문에 프로토타입에서 검색을 시작합니다. user의 프로토타입은 User.prototype인데, User.prototype은 빈 객체입니다.
  3. User.prototype은 일반 객체 {}이고, 일반 객체의 프로토타입은 Object.prototype입니다. Object.prototype.constructor == Object이므로 Object가 사용됩니다.

결국에 let user2 = new user.constructor('Pete');let user2 = new Object('Pete')가 됩니다. 그런데 Object의 생성자는 인수를 무시하고 항상 빈 객체를 생성합니다. 따라서 let user2 = new Object('Pete')let user2 = {}와 같다고 생각할 수 있습니다. user2.nameundefined인 이유가 여기에 있습니다.

튜토리얼 지도

댓글

댓글을 달기 전에 마우스를 올렸을 때 나타나는 글을 먼저 읽어주세요.
  • 추가 코멘트, 질문 및 답변을 자유롭게 남겨주세요. 개선해야 할 것이 있다면 댓글 대신 이슈를 만들어주세요.
  • 잘 이해되지 않는 부분은 구체적으로 언급해주세요.
  • 댓글에 한 줄짜리 코드를 삽입하고 싶다면 <code> 태그를, 여러 줄로 구성된 코드를 삽입하고 싶다면 <pre> 태그를 이용하세요. 10줄 이상의 코드는 plnkr, JSBin, codepen 등의 샌드박스를 사용하세요.