생성자 함수, 프로토타입

아래 내용은 학원 수업과 "모던자바스크립트 Deep Dive : 이웅모 저"를 읽고 정리한 내용입니다.

1. 생성자 함수에 의한 객체 생성

객체를 생성하기 위해서는 객체 리터럴을 사용하는 경우가 가장 많다. 하지만 이 방법은 치명적인 단점이 존재하는데, 한번에 단 하나의 객체만 생성한다는 것이다. 동일한 프로퍼티를 가지고 있지만, 각 프로퍼티의 값은 수 많은 객체를 만들었고, 이 후 각각의 객체에 동일한 프로퍼티를 추가해야 한다고 생각해보자. 몇 개인지 모를 많은 객체들에 프로퍼티를 하나 하나 추가하는 것은 메모리가 낭비되는 것은 물론이고, 비효율적이며, 오타로 인한 실수를 발생시킬 가능성도 높아진다. 이런 단점을 해결하기 위해서는 생성자 함수를 사용하여 객체(인스턴스)를 생성하는 방법이 있다.

생성자 함수에 의한 객체 생성 방식의 장점

생성자 함수란 동일한 형태(프로퍼티 구조가 같은)를 가진 객체(인스턴스)를 생성하기 위한 템플릿처럼 사용할 수 있는 함수를 말한다. 예를 들어 쿠키틀(생성자 함수)을 이용하면 같은 모양(프로퍼티 키가 같은)을 하고 있는 다양한 맛(프로퍼티 값)의 쿠키를 빠르게 만들어 낼 수 있는 있는 것과 같다.

생성자 함수는 일반 함수와 동일하게 정의하고, new연산자와 함께 호출하면 객체를 생성한다. 이렇게 생성된 객체는 인스턴스라고 한다. 각각의 인스턴스마다 달라야 하는 프로퍼티 값은 생성자 함수의 매개변수를 통해 전달받고, 이 값을 생성자 함수에 작성된 메서드에서 사용하기 위해서는 this.프로퍼티키처럼 this라는 키워드를 사용하여 참조해야 한다. this는 함수가 호출되는 방식에 따라서 달라지게 되는데, 생성자 함수로 호출했을 경우에는 생성자 함수가 생성하게 될 인스턴스를 가르킨다.

중요한 것은 생성자 함수로 사용하기 위해 정의한 함수도 일반 함수로 호출이 가능하다는 것이다. 때문에 생성자 함수로 사용하기 위해서는 반드시 new연산자와 함께 호출해야 한다는 것을 명심해야 한다.

// 생성자 함수
function Cookie(flavor, size) {
    this.size = size;
    this.flavor = flavor;
    this.getWeight = function () {
    	return this.size * 1.5;
    }
}

// 인스턴스 생성
const chocolateCookie = new Cookie('chocolate', 5);
const vanillaCookie = new Cookie('vanilla', 3);

생성자 함수의 인스턴스 생성 과정

  1. 인스턴스 생성과 this 바인딩
    생성자 함수가 런타임 이전에 빈 객체(인스턴스)를 생성하고, 생성자 함수 내부의 this와 바인딩 됨

  2. 인스턴스 초기화
    this에 바인딩 되어 인스턴스에 프로퍼티와 메서드를 추가하고, 매개변수를 통해 전달받은 값을 인스턴스의 프로퍼티 값에 할당하여 초기화하거나 고정값을 할당한다.

  3. 인스턴스 반환
    완성된 인스턴스가 바인딩된 this(생성자 함수가 생성한 인스턴스)가 암묵적으로 반환된다.

유의점

일반 함수에서는 return을 작성하지 않으면 undefined가 반환되기 때문에 반드시 return을 작성해야 한다고 했다. 하지만 생성자 함수에서는 return을 생략해야 한다. 생성자 함수 내부에서 return키워드를 통해 원시값을 작성할 경우 이 원시값은 무시되고, 객체를 작성할 경우에는 생성자 함수를 통해서 생성하려고 했던 인스턴스가 아닌 return키워드를 통해 작성한 객체가 반환된다. 하지만 생성자 함수는 인스턴스를 생성해서 반환하기 위한 것이므로 반드시 return을 생략해야 한다.

new.target

자바스크립트는 일반 함수와 생성자 함수가 별도로 구분되지 않다. 생성자 함수로 사용하기 위해서 정의했다고 하더라도 일반 함수로 호출할 수 있고, 이런 실수를 방지하기 위해서 파스칼 케이스로 작성하는 것을 일반적인 컨벤션으로 지키는 것이 좋다. 하지만 이 방법은 오류를 근본적으로 방지할 수 있는 방법은 아니다.

ES6에서는 이런 실수를 방지하기 위해서 new.target을 지원한다. 함수 내부에서 new.target을 사용하면 new연산자와 함께 생성자 함수로 호출되었는지 확인할 수 있다. new연산자와 함께 생성자 함수로서 호출되면 함수 내부의 new.target은 함수 자신을 가리킨다. new연산자 없이 일반 함수로서 호출된 함수 내부의 new.targetundefined다. 단, new.target은 IE를 지원하지 않는다.

// 생성자 함수
function Cookie(flavor, size) {

	if (!new.target) {
    	return new Cookie(flavor, size);
    }
    
    this.size = size;
    this.flavor = flavor;
    this.getWeight = function () {
    	return this.size * 1.5;
    }
}

// 인스턴스 생성
const chocolateCookie = new Cookie('chocolate', 5);
const vanillaCookie = new Cookie('vanilla', 3);

2. 프로토타입

객체지향 프로그래밍

사람은 어떤 사물에 대해서 인지를 하고, 구분할 때 그 사물의 특징이나 속성을 가지고 구분을 한다. 예를 들어서 사람 두명이 나란히 서 있을 때, 외쪽의 사람은 여자이며 안경을 썼고, 이름은 김규리이다. 오른쪽은 남자이며 안경을 쓰지 않았고, 이름은 홍길동이다. 와 같은식으로 말이다. 이러한 인지 방식을 프로그래밍에 접목하려는 것에서 시작된 것이 객체지향 프로그래밍 언어이다. 단, 프로그래밍에서는 모든 특징을 사용하는 것이 아니라 필요한 특징만 사용하게 되는데, 이렇게 필요한 특징만 간추려서 표현하는 것을 추상화라고 한다. 그리고 이렇게 필요한 특성만 모아서 자료구조로 만든 것을 객체라고 한다.

이 때 간추려진 특성들 중에서 상태를 나타내는 것을 프로퍼티라고 하고, 객체의 내부에서 값을 구하는 동작을 메서드라고 한다. 객체는 또 다른 객체와 연결되어 관계성을 가질 수도 있다.

상속과 프로토타입

상속이란 어떤 객체가 다른 객체의 프로퍼티 또는 메서드를 이어 받아서 사용할 수 있도록 하는 것을 말한다. 이 때 자바스크립트의 상속은 프로토타입을 기반으로 하고, 이를 통해서 불필요한 코드의 중복을 제거한다. 이쯤에서 생성자 함수를 다시 생각해 볼 필요가 있다. 생성자 함수는 코드를 반복해서 작성하는 수고를 줄여주지만, 생성자 함수를 통해서 생성된 인스턴스는 결국 공통된 코드를 각각 가지고 있기 때문에 코드를 재사용 한다는 관점에서는 여전히 단점을 가지고 있는 것이다.

그렇다면 이렇게 공통된 코드를 중복하여 작성하지 않고 재사용할 수 있을까? 바로 프로토타입을 통해서 상속받아서 사용하는 것이다. 일반적으로 상속이란 부모의 것을 자식이 물려 받는 것을 말하는데, 프로토타입도 이와 같이 생각할 수 있다. 프로토타입은 부모의 역할을 하는 객체이고, 자식의 역할을 하는 객체는 부모의 역할을 하는 객체인 프로토타입의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 쓸 수 있다. 하지만 부모의 역할을 하는 객체는 자식의 역할을 하는 객체의 프로퍼티를 사용할 수 없다.

이렇게 프로퍼티를 찾아서 사용하고자 할 때 검색하는 메커니즘을 프로토타입 체인이라고 하고, 이 프로토타입 체인 역시 스코프 체인과 마찬가지로 단방향 링크트 리스트 형태를 가지고 있다.

// 생성자 함수
function Cookie(flavor, size) {   
    this.size = size;
    this.flavor = flavor;
}

// 프로토타입
Cookie.prototype.getWeight = function () {
	return this.size * 1.5;
}

// 인스턴스 생성
const chocolateCookie = new Cookie('chocolate', 5);
const vanillaCookie = new Cookie('vanilla', 3);

console.log(chocolateCookie.getWeight());

3. this

객체는 프로퍼티와 메서드로 구성된다. 이 때 메서드는 자신이 속해 있는 객체의 프로퍼티를 참조하고 변경할 수 있는데, 그러기 위해서는 내가 속한 객체가 어디인지를 알아야 한다.

생성자 함수는 자신이 생성할 인스턴스를 참조할 수 있어야 해당 인스턴스에 프로퍼티 또는 메서드를 추가할 수 있다. 그런데 인스턴스를 생성하기 위해서는 먼저 생성자 함수를 정의해야 한다. 하지만 생성자 함수를 정의할 때에는 아직 인스턴스가 생성되기 전이기 때문에 생성자 함수가 인스턴스를 직접 가리킬 수 없고, 이것을 해결하기 위해서 this라는 특수한 식별자를 제공한다.

this는 자기 참조 변수(self-referencing variable)로서 자신이 속한 객체 또는 자신이 생성할 인스턴스의 프로퍼티나 메서드를 참조할 수 있게 해준다. 그런데 이 this는 함수를 호출하는 방식에 따라서 this가 가르키는 객체가 달라진다. (this 바인딩이 동적으로 결정된다.)

함수 호출 방식this가 가르키는 객체 (this바인딩)
일반 함수전역 객체
메서드메서드를 호출한 객체
생성자 함수생성자 함수가 (미래에)생성할 객체

좋은 웹페이지 즐겨찾기