Post

[모던 자바스크립트 Deep Dive]17장 생성자 함수에 의한 객체 생성

17.1 Object 생성자 함수

  • 생성자 함수(constructor): new 연산자와 함께 호출해 객체를 생성하는 함수
    • Object, String, Number, Boolean, Function, Array, Date, RegExp, Promise
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
      // String 생성자 함수에 의한 String 객체 생성
      const strObj = new String('Lee');
      console.log(typeof strObj); // object
      console.log(strObj);        // String {"Lee"}
        
      // Number 생성자 함수에 의한 Number 객체 생성
      const numObj = new Number(123);
      console.log(typeof numObj); // object
      console.log(numObj);        // Number {123}
        
      // Boolean 생성자 함수에 의한 Boolean 객체 생성
      const boolObj= new Boolean(true);
      console.log(typeof boolObj); // object
      console.log(boolObj);        // Boolean {true}
        
      // Function 생성자 함수에 의한 Function 객체(함수) 생성
      const func = new Function('x', 'return x * x');
      console.log(typeof func); // function
      console.dir(func);        // ƒ anonymous(x)
        
      // Array 생성자 함수에 의한 Array 객체(배열) 생성
      const arr = new Array(1, 2, 3);
      console.log(typeof arr); // object
      console.log(arr);        // [1, 2, 3]
        
      // RegExp 생성자 함수에 의한 RegExp 객체(정규 표현식) 생성
      const regExp = new RegExp(/ab+c/i);
      console.log(typeof regExp); // object
      console.log(regExp);        // /ab+c/i
        
      // Date 생성자 함수에 의한 Date 객체 생성
      const date = new Date();
      console.log(typeof date); // object
      console.log(date);        // Mon May 04 2020 08:36:33 GMT+0900 (대한민국 표준시)
    
  • 인스턴스(instance): 생성자 함수가 생성한 객체
  • new 연산자와 함께 Object 생성자 함수를 호출하면 빈 객체를 생성하여 반환

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
      // 빈 객체의 생성
      const person = new Object();
        
      // 프로퍼티 추가
      person.name = 'Lee';
      person.sayHello = function () {
        console.log('Hi! My name is ' + this.name);
      };
        
      console.log(person); // {name: "Lee", sayHello: ƒ}
      person.sayHello(); // Hi! My name is Lee
    
    • 객체 리터럴을 사용하는 방법보다 번거롭다.

17.2 생성자 함수

17.2.1 객체 리터럴에 의한 객체 생성 방식의 문제점

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const circle1 = {
  radius: 5,
  getDiameter() {
    return 2 * this.radius;
  }
};

console.log(circle1.getDiameter()); // 10

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

console.log(circle2.getDiameter()); // 20
  • 직관적이고 간편하지만, 단 하나의 객체만 생성한다.
  • 프로퍼티 구조가 동일한 여러 개의 객체를 생성할 때 매번 같은 코드를 작성해야 한다.

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

  • 생성자 함수: 일반 함수로 동일한 방법으로 정의하고 new 연산자와 함께 호출
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 생성자 함수
function Circle(radius) {
  // 생성자 함수 내부의 this는 생성자 함수가 생성할 인스턴스를 가리킨다.
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}

// 인스턴스의 생성
const circle1 = new Circle(5);  // 반지름이 5인 Circle 객체를 생성
const circle2 = new Circle(10); // 반지름이 10인 Circle 객체를 생성

console.log(circle1.getDiameter()); // 10
console.log(circle2.getDiameter()); // 20

// new 연산자와 함께 호출하지 않으면 생성자 함수로 동작하지 않는다.
// 즉, 일반 함수로서 호출된다.
const circle3 = Circle(15);

// 일반 함수로서 호출된 Circle은 반환문이 없으므로 암묵적으로 undefined를 반환한다.
console.log(circle3); // undefined

// 일반 함수로서 호출된 Circle내의 this는 전역 객체를 가리킨다.
console.log(radius); // 15
  • 인스턴스를 생성하기 위한 클래스처럼 이용할 수 있어 구조가 동일한 여러 객체를 간편하게 생성할 수 있다.

this

  • 객체 자신의 프로퍼티나 메서드를 참조하기 위한 자기 참조 변수(self-referencing variable)
  • this 바인딩은 함수 호출 방식에 따라 동적으로 결정

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
      // 함수는 다양한 방식으로 호출될 수 있다.
      function foo() {
        console.log(this);
      }
        
      // 일반적인 함수로서 호출
      // 전역 객체는 브라우저 환경에서는 window, Node.js 환경에서는 global을 가리킨다.
      foo(); // window
        
      // 메서드로서 호출
      const obj = { foo }; // ES6 프로퍼티 축약 표현
      obj.foo(); // obj
        
      // 생성자 함수로서 호출
      const inst = new foo(); // inst
    
    • 일반 함수로서 호출 ⇒ 전역 객체
    • 메서드로서 호출 ⇒ 메서드를 호출한 객체(마침표 앞의 객체)
    • 생성자 함수로서 호출 ⇒ 생성자 함수가 생성할 인스턴스

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

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

1
2
3
4
5
6
7
8
9
function Circle(radius) {
  // 1. 암묵적으로 빈 객체가 생성되고 this에 바인딩된다.
  console.log(this); // Circle {}

  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}
  • 암묵적으로 빈 객체(인스턴스) 생성
  • 인스턴스는 this에 바인딩 됨
  • 런타임 이전에 실행

2. 인스턴스 초기화

1
2
3
4
5
6
7
8
9
function Circle(radius) {
  // 1. 암묵적으로 인스턴스가 생성되고 this에 바인딩된다.

  // 2. this에 바인딩되어 있는 인스턴스를 초기화한다.
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}
  • this에 바인딩된 인스턴스에 프로퍼티, 메서드를 추가
  • 생성자 함수가 인수로 전달받은 초깃값을 할당하여 초기화 또는 고정값을 할당

3. 인스턴스 반환

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Circle(radius) {
  // 1. 암묵적으로 인스턴스가 생성되고 this에 바인딩된다.

  // 2. this에 바인딩되어 있는 인스턴스를 초기화한다.
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };

  // 3. 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다
}

// 인스턴스 생성. Circle 생성자 함수는 암묵적으로 this를 반환한다.
const circle = new Circle(1);
console.log(circle); // Circle {radius: 1, getDiameter: ƒ}
  • 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환
  • 명시적으로 객체를 반환하면 암묵적 this 반환 무시

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
      function Circle(radius) {
        // 1. 암묵적으로 인스턴스가 생성되고 this에 바인딩된다.
        
        // 2. this에 바인딩되어 있는 인스턴스를 초기화한다.
        this.radius = radius;
        this.getDiameter = function () {
          return 2 * this.radius;
        };
        
        // 3. 암묵적으로 this를 반환한다.
        // 명시적으로 객체를 반환하면 암묵적인 this 반환이 무시된다.
        return {};
      }
        
      // 인스턴스 생성. Circle 생성자 함수는 명시적으로 반환한 객체를 반환한다.
      const circle = new Circle(1);
      console.log(circle); // {}
    
  • 명시적으로 원시 값을 반환하면 이를 무시하고 암묵적 this 반환

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
      function Circle(radius) {
        // 1. 암묵적으로 인스턴스가 생성되고 this에 바인딩된다.
        
        // 2. this에 바인딩되어 있는 인스턴스를 초기화한다.
        this.radius = radius;
        this.getDiameter = function () {
          return 2 * this.radius;
        };
        
        // 3. 암묵적으로 this를 반환한다.
        // 명시적으로 원시값을 반환하면 원시값 반환은 무시되고 암묵적으로 this가 반환된다.
        return 100;
      }
        
      // 인스턴스 생성. Circle 생성자 함수는 명시적으로 반환한 객체를 반환한다.
      const circle = new Circle(1);
      console.log(circle); // Circle {radius: 1, getDiameter: ƒ}
    
  • 생성자 함수의 기본 동작을 위해 return문은 반드시 생략해야 한다.

17.2.4 내부 메서드 [[Call]][[Construct]]

  • 함수는 객체이므로 일반 객체와 동일하게 일반 객체의 내부 슬롯과 내부 메서드를 가지고 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
      // 함수는 객체다.
      function foo() {}
        
      // 함수는 객체이므로 프로퍼티를 소유할 수 있다.
      foo.prop = 10;
        
      // 함수는 객체이므로 메서드를 소유할 수 있다.
      foo.method = function () {
        console.log(this.prop);
      };
        
      foo.method(); // 10
    
  • 함수는 함수로 동작하기 위해 일반 객체와는 달리 함수 객체만을 위한 [[Environment]], [[FormalParameters]] 등의 내부 슬롯과 [[Call]][[Construct]] 등의 내부 메서드를 추가로 가지고 있다.
  • 함수가 일반 함수로서 호출되면 [[Call]]이 호출되고, new 연산자와 함께 생성자 함수로서 호출되면 [[Construct]]가 호출된다.

    1
    2
    3
    4
    5
    6
    7
    
      function foo() {}
        
      // 일반적인 함수로서 호출: [[Call]]이 호출된다.
      foo();
        
      // 생성자 함수로서 호출: [[Construct]]가 호출된다.
      new foo();
    
  • callable: 내부 메서드 [[Call]]을 갖는 함수 객체
    • 호출할 수 있는 객체, 즉 함수
    • 함수 객체는 무조건 callable이어야 한다.
  • constructor: [[Construct]]를 갖는 함수 객체
    • 생성자 함수로서 호출할 수 있는 함수
  • non-constructor: [[Construct]]를 갖지 않는 함수 객체
    • 생성자 함수로서 객체를 호출할 수 없는 함수

17.2.5 constructor와 non-constructor의 구분

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 일반 함수 정의: 함수 선언문, 함수 표현식
function foo() {}
const bar = function () {};
// 프로퍼티 x의 값으로 할당된 것은 일반 함수로 정의된 함수이기에 메서드로 인정하지 않는다.
const baz = {
  x: function () {}
};

// 일반 함수로 정의된 함수만이 constructor이다.
new foo();   // -> foo {}
new bar();   // -> bar {}
new baz.x(); // -> x {}

// 화살표 함수 정의
const arrow = () => {};

new arrow(); // TypeError: arrow is not a constructor

// 메서드 정의: ES6의 메서드 축약 표현만을 메서드로 인정한다.
const obj = {
  x() {}
};

new obj.x(); // TypeError: obj.x is not a constructor
  • constructor: 함수 선언문, 함수 표현식, 클래스
  • non-constructor: 메서드(ES6 메서드 축약 표현), 화살표 함수
    • ECMAScript 사양에서 메서드는 ES6의 메서드 축약 표현만을 의미한다.
    • 생성자 함수로서 호출하면 에러가 발생한다.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      
        function foo() {}
              
        // 일반 함수로서 호출
        // [[Call]]이 호출된다. 모든 함수 객체는 [[Call]]이 구현되어 있다.
        foo();
              
        // 생성자 함수로서 호출
        // [[Construct]]가 호출된다. 
        // 이때 [[Construct]]를 갖지 않는다면 에러가 발생한다.
        new foo();
      

17.2.6 new 연산자

  • 생성자 함수로서 정의하지 않은 일반 함수를 new 연산자와 함께 호출하는 경우
    • [[Construct]] 호출
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
      // 생성자 함수로서 정의하지 않은 일반 함수
      function add(x, y) {
        return x + y;
      }
        
      // 생성자 함수로서 정의하지 않은 일반 함수를 new 연산자와 함께 호출
      let inst = new add();
      // 함수가 객체를 반환하지 않았으므로 반환문이 무시된다. 
      // 따라서 빈 객체가 생성되어 반환된다.
      console.log(inst); // {}
        
      // 객체를 반환하는 일반 함수
      function createUser(name, role) {
        return { name, role };
      }
        
      // 생성자 함수로서 정의하지 않은 일반 함수를 new 연산자와 함께 호출
      inst = new createUser('Lee', 'admin');
      // 함수가 생성한 객체를 반환한다.
      console.log(inst); // {name: "Lee", role: "admin"}
    
  • 생성자 함수를 new 연산자 없이 호출하는 경우
    • [[Call]] 호출
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
      function Circle(radius) {
        this.radius = radius;
        this.getDiameter = function () {
          return 2 * this.radius;
        };
      }
        
      // new 연산자 없이 생성자 함수 호출하면 일반 함수로서 호출된다.
      // 일반 함수 내부의 this는 전역 객체 window를 가리킨다.
      // 따라서 radius 프로퍼티와 getDiameter 메서드는 전역 객체의 프로퍼티와 메서드다.
      const circle = Circle(5);
      console.log(circle); // undefined
        
      // 일반 함수 내부의 this는 전역 객체 window를 가리킨다.
      console.log(radius); // 5
      console.log(getDiameter()); // 10
        
      circle.getDiameter();
      // TypeError: Cannot read property 'getDiameter' of undefined
    
  • 생성자 함수는 일반적으로 파스칼 케이스로 명명해 일반 함수와 구별한다.

17.2.7 new.target

  • ES6에서 지원
  • 생성자 함수가 new 연산자 없이 호출되는 것을 방지하기 위해 사용
  • this와 유사하게 constructor인 모든 함수 내부에서 암묵적인 지역 변수와 같이 사용
    • 메타 프로퍼티라고 부른다.
  • new.target가 가리키는 대상
    • new 연산자와 함께 호출 ⇒ 함수 자신
    • 일반 함수로서 호출 ⇒ undefined
  • 생성자 함수로 호출되기 위해서 재귀 호출을 이용 가능

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
      // 생성자 함수
      function Circle(radius) {
        // 이 함수가 new 연산자와 함께 호출되지 않았다면 new.target은 undefined다.
        if (!new.target) {
          // new 연산자와 함께 생성자 함수를 재귀 호출하여 생성된 인스턴스를 반환한다.
          return new Circle(radius);
        }
        
        this.radius = radius;
        this.getDiameter = function () {
          return 2 * this.radius;
        };
      }
        
      // new 연산자 없이 호출하여도 new.target을 통해 생성자 함수로서 호출된다.
      const circle = Circle(5);
      console.log(circle.getDiameter());
    
  • new.target을 지원하지 않는 경우(IE) 스코프 세이프 생성자 패턴을 사용

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
      // Scope-Safe Constructor Pattern
      function Circle(radius) {
        // 생성자 함수가 new 연산자와 함께 호출되면 함수의 선두에서 빈 객체를 생성하고
        // this에 바인딩한다. 이때 this와 Circle은 프로토타입에 의해 연결된다.
        
        // 이 함수가 new 연산자와 함께 호출되지 않았다면 
      	// 이 시점의 this는 전역 객체 window를 가리킨다.
        // 즉, this와 Circle은 프로토타입에 의해 연결되지 않는다.
        if (!(this instanceof Circle)) {
          // new 연산자와 함께 호출하여 생성된 인스턴스를 반환한다.
          return new Circle(radius);
        }
        
        this.radius = radius;
        this.getDiameter = function () {
          return 2 * this.radius;
        };
      }
        
      // new 연산자 없이 생성자 함수를 호출하여도 생성자 함수로서 호출된다.
      const circle = Circle(5);
      console.log(circle.getDiameter()); // 10
    

다른 빌트인 생성자 함수의 경우

  • Object, Function 생성자 함수는 new 연산자 없이 호출해도 new 연산자와 함께 호출한 것처럼 동작

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
      let obj = new Object();
      console.log(obj); // {}
        
      obj = Object();
      console.log(obj); // {}
        
      let f = new Function('x', 'return x ** x');
      console.log(f); // ƒ anonymous(x) { return x ** x }
        
      f = Function('x', 'return x ** x');
      console.log(f); // ƒ anonymous(x) { return x ** x }
    
  • String, Number, Boolean 생성자 함수는 new 연산자 없이 호출하면 객체가 아닌 문자열, 숫자, 불리언 값을 반환

    1
    2
    3
    4
    5
    6
    7
    8
    
      const str = new String(123);
      console.log(str, typeof str); // String {'123'} 'object'
        
      const num = new Number('123');
      console.log(num, typeof num); // Number {123} 'object'
        
      const bool = new Boolean('true');
      console.log(bool, typeof bool); // Boolean {true} 'object'
    
    • 이를 이용해 데이터 타입 변환 가능

      1
      2
      3
      4
      5
      6
      7
      8
      
        const str = String(123);
        console.log(str, typeof str); // 123 string
              
        const num = Number('123');
        console.log(num, typeof num); // 123 number
              
        const bool = Boolean('true');
        console.log(bool, typeof bool); // true boolean
      
This post is licensed under CC BY 4.0 by the author.