JS06-1 생성자 함수

조예진·2022년 2월 4일
0

JavaScript 스터디

목록 보기
7/16
post-thumbnail

본 시리즈는 모던 자바스크립트 Deep Dive 책을 참고하여 작성하고 있습니다.

생성자 함수로 객체 생성하기

객체는 다양한 방식으로 생성할 수 있다. 앞에서는 객체 리터럴로 객체를 생성하는 방식을 알아보았다.

const obj = {
  name: "kim",
  age: 32
}

이번에는 생성자 함수를 통해서 객체를 생성하는 방식에 대해 알아본다.

const obj = new Object();
const obj2 = new Circle(5);

생성자 함수(constructor)new 연산자와 함께 호출하여 객체(인스턴스)를 생성하는 함수이다. 생성자 함수에 의해 생성된 객체인스턴스(instance)라 한다. 자바스크립트에서 제공하는 빌트인 생성자 함수를 사용할 수도 있고, 직접 생성자 함수를 만들어 쓸 수도 있다. 위 예시의 Object 외에도 String, Number, Boolean, Function, Array, Date, RegExp, Promise 와 같은 빌트인 생성자 함수가 제공된다. Object 빌트인 생성자 함수는 빈 객체를 생성해서 반환한다.

  • 생성자 함수: contructor, new 연산자와 함께 호출하여 객체를 생성하는 함수
  • 빌트인 생성자 함수: built-in constructor, 자바스크립트에서 제공하는 생성자 함수
  • 인스턴스: instance, 생성자 함수에 의해 생성된 객체

객체 리터럴과 생성자 함수 비교

객체 리터럴에 의한 객체 생성

const circle = { 
  radius: 5,
  getDiameter() {
    return 2 * this.radius;
  }
};

circle.getDiameter();
  • 직관적이고 간편
  • 단 하나의 객체만 생성
    • 같은 프로퍼티를 갖는 객체가 여러 개 필요한 경우 같은 프로퍼티를 여러번 기술해야 함, 코드 중복 발생

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

function Circle(radius) {
  this.radius = radius; // this는 미래 생성될 인스턴스를 가리킴
  this.getDiameter = function () {
    return 2 * this.radius;
  }
}

const circle = new Circle(5);
circle.getDiameter();
  • 객체 생성을 위한 템플릿처럼 사용 가능 (Java의 Class처럼)
  • new 연산자를 붙이지 않고 호출하면 일반 함수로 동작함

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

생성자 함수의 역할은 1) 인스턴스를 생성하고 2) 생성된 인스턴스를 초기화하는 것이다. 초기화 과정에서 인스턴스의 프로퍼티를 추가하고 초기값을 할당하는 일을 한다. 생성자 함수는 인스턴스를 반드시 생성해야 하고, 초기화는 할 수도 있고 하지 않을 수도 있다.

그런데 위의 <생성자 함수에 의한 객체 생성> 예시 코드를 보면 생성자 함수 내에서는 인스턴스 초기화를 위한 동작만 하고 있다. 그런데도 인스턴스를 반환받을 수 있는 이유는, new 연산자와 함께 생성자 함수를 호출하면 자바스크립트 엔진이 암묵적 처리를 통해 인스턴스를 생성하고 반환하기 때문이다.

  1. 인스턴스 생성과 this 바인딩

    이 과정은 함수 실행 런타임 이전에 실행된다.

    1. 암묵적으로 빈 객체를 생성 → 추후 반환될 인스턴스 객체
    2. 인스턴스를 this에 바인딩 → 생성자 함수 내부 this가 인스턴스를 가리키게 됨

    바인딩: 식별자와 값을 연결하는 과정. this가 가리킬 객체를 바인딩했다는 뜻이고, 생성자 함수 내부에서의 this는 인스턴스를 가리키게 된다.

  2. 인스턴스 초기화

    생성자 함수 내부 코드가 한 줄씩 실행된다.

    1. 인스턴스에 프로퍼티나 메서드 추가
    2. 초기값을 할당하여 초기화하거나 고정값 할당
  3. 인스턴스 반환

    1. 바인딩된 this가 암묵적으로 반환
    2. 명시적으로 반환된 객체가 있다면 return 문에 명시한 객체를 반환
    3. 명시적으로 반환되는 원시 값이 있다면 원시 값을 무시하고 this 반환
function Circle(radius) {
  this.radius = radius;
	return radius;
}

const circle = new Circle(3);
console.log(circle); // { radius: 3 }

생성자 함수의 기본 동작은 바인딩된 this를 반환하는 것이다. 따라서 다른 값을 반환하는 것은 생성자 함수의 동작 방식을 훼손하는 것이므로 예측을 어렵게 만든다. 따라서 생성자 함수 내부에서는 return 문을 사용하지 않아야 한다.

내부 메서드

함수 선언문이나 함수 표현식으로 정의한 함수는 일반 함수로 호출할 수도 있고 생성자 함수로서 호출할 수도 있다. 그리고 함수는 객체이므로 일반 객체와 동일하게 동작할 수 있다. 함수는 일반 객체가 가지는 내부 슬롯과 내부 메서드를 가지고, 추가로 함수로서 동작하기 위한 내부 슬롯과 내부 메서드를 가진다.

  • 함수만 가지는 내부 슬롯: [[Environment]], [[FormatParameters]]
  • 함수만 가지는 내부 메서드: [[Call]], [[Construct]]
    • [[Call]] : 함수를 일반 함수로서 호출할 경우
      • 이 메서드를 가지는 함수 객체는 callable
      • 함수로서 기능하는 객체는 반드시 callable이다
    • [[Construct]] : new 연산자와 함께 생성자 함수로 호출할 경우
      • 이 메서드를 가지는 함수 객체는 constructor
      • 이 메서드를 가지지 않는 함수 객체를 non-constructor
      • 모든 함수가 constructor인 것은 아니다

즉, 함수 객체는 callable이면서, constructor이거나 non-constructor이다. 모든 함수 객체는 호출 가능하지만, 생성자 함수로서 호출할 수 있을 수도 있고 없을 수도 있다. 호출할 수 없는 객체는 함수 객체가 아니므로 모든 함수 객체는 반드시 callable이다.

함수가 constructor인지 판단하는 기준은 함수를 정의한 방식이다.

  • constructor: 함수 선언문, 함수 표현식, 클래스 등 일반 함수로 정의된 함수
  • non-constructor: 메서드(ES6 메서드 축약 표현), 화살표 함수
    • ECMAScript 사양에서의 메서드는 ES6의 메서드 축약 표현만을 의미한다.
// 일반 함수로 정의된 경우
function foo() {}
const bar = function () {};
const obj = {
  x: function() {}
};

const ins_1 = new foo();
const ins_2 = new bar();
const ins_3 = new obj.x();

// non-constructor
const arrow = () => {};
const obj_2 = {
  x() {}
};

const ins_arr = new arrow(); // TypeError
const ins_met = new obj_2.x(); // TypeError

new 연산자

new 연산자와 함께 함수를 호출하면 그 함수는 생성자 함수로 동작한다. 이 경우 함수 객체 내부 메서드 [[Construct]]가 호출된다. 이 때 함수는 constructor여야 한다. new 연산자 없이 생성자 함수를 호출하면 일반 함수로 호출된다. 즉, 내부 메서드 [[Call]]이 호출된다.

생성자 함수와 일반 함수 간에 형식적인 차이는 없기 때문에, 일반적으로 생성자 함수의 경우 함수 이름의 첫 문자를 대문자로 기술하여 일반 함수와 구분한다.

new.target

생성자 함수를 일반 함수로 호출하면, 함수 내에서 사용한 this는 전역 객체를 가리킨다. 따라서 new 키워드를 제외할 경우 전역 객체에 의도치 않게 프로퍼티나 메서드를 추가하게 될 수도 있다.

function Circle(radius) {
  this.radius = radius;
  this.getDiameter = function () {
    return this.radius * 2;
  }
}

Circle(2);

console.log(window.radius); // 2
console.log(window.getDiameter()); // 4

이러한 위험을 방지하기 위해 new.target 을 지원한다. 이를 메타 프로퍼티라고 부른다. new.target은 ES6 문법으로, IE에서는 new.target이 지원되지 않는다. 이는 constructor인 함수 내부에서 암묵적인 지역 변수처럼 사용된다.

  • 함수가 생성자 함수로서 호출되면 new.target은 함수 자신을 가리킨다.
  • 함수가 일반 함수로 호출되면 new.target은 undefined이다.
function Circle(radius) {
  // 일반 함수로 호출된 경우
  if (!new.target) {
    return new Circle(radius);
  }

  this.radius = radius;
  this.getDiameter = function () {
    return this.radius * 2;
  }
}

new.target을 사용 불가능한 경우에는 스코프 세이프 생성자 패턴을 사용할 수 있다.

function Circle(radius) {
  // 생성자 함수로 생성되었다면 -> this가 Circle에 바인딩 됨
  // 일반 함수로 생성되었다면 -> this는 전역 객체를 가리킴
  if (!(this instanceof Circle)) {
    return new Circle(radius);
  }

  this.radius = radius;
  this.getDiameter = function () {
    return this.radius * 2;
  }
}

인스턴스는 프로토타입에 의해 생성자 함수와 연결된다. 대부분의 빌트인 생성자 함수도 new 연산자와 함께 호출되었는지 확인한 후 값을 반환한다.

profile
https://oooooroblog.com 으로 이사갔어요

0개의 댓글