Programming/javascript

자바스크립트 기초부터 모던 자바스크립트까지 - 생성자편

KOOCCI 2022. 7. 20. 21:43

목표 : 자바스크립트에서는 생성자를 어떻게 표현하는지 알아본다.


앞선 포스트에서 객체가 어떻게 생성되는지 알아본 적이 있다.

물론 객체 리터럴이 가장 생성하기 쉬운 방식이지만, Object 생성자 함수를 사용해 생성하는 방식에 대해 알아보자.

 

생성자 함수

생성자 함수(constructor)new 연산자와 함께 호출하여 객체(인스턴스)를 생성하는 함수다.

그리고, 생성자 함수에 의해 생성된 객체를 인스턴스(Instance)라고 한다.

const person = new Object();

person.name = 'Lee';
person.sayHello = function() {
    console.log('Hi! My name is ' + this.name);
}

console.log(person); // {name: 'Lee', sayHello: ƒ}
console.log(person.sayHello()); // Hi! My name is Lee

자바스크립트는 Object 생성자 함수 말고도, String, Number, Boolean, Function, Array, Date, RegExp, Promise 등의 빌트인(Built-in) 생성자 함수를 제공한다.

 

객체 리터럴로 생성하는게 훨씬 편하지 않은가?

맞다.

굳이, Object 생성자 함수로 객체를 생성할 필요가 없어 보인다.

그러나 불편한 점이 한가지 떠오른다.

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

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

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

console.log(circle2.getDiameter()); // 14

객체 리터럴로 생성하는 방식은 단 하나의 객체만 생성할 수 있다.

위와 같이, 데이터 프로퍼티 값만 바뀌었고 구조가 동일한데, 두번이나 작성해야하는 번거로움이 생긴다.

이제 이를 생성자 함수를 호출하는 것으로 바꿔보자.

function Circle(radius) {
    // 생성자 함수 내부의 this는 생성자 함수가 생성할 인스턴스를 가리킨다.
    this.radius = radius;
    this.getDiameter = function() {
    	return 2 * this.radius;
    };
}

const circle1 = new Circle(5);
const circle2 = new Circle(7);

console.log(circle1.getDiameter()); // 10
console.log(circle2.getDiameter()); // 14
더보기

this는 자바스크립트에서 중요한 키워드 중 하나이므로, 언제 호출되냐에 따라 가리키는 값이 달라진다.

우선 생성자 함수 내부에서는 위 주석처럼 생성자 함수가 생성할 인스턴스를 가리키는 것으로 넘어가고, 이후에 다시 알아보도록 하자.

다시, 위 예시로 넘어가서 new 연산자로 인스턴스를 생성하는 것을 볼 수 있다.

자바스크립트는 자바와 같은 언어와 다르게 특별한 형식을 가지고 인스턴스를 생성하는 것이 아니라, 생성자 함수를 정의하고 new 연산자로 호출하면 해당 함수는 생성자 함수로서 동작하며, new 연산자 없이 호출한다면, 일반 함수와 동일하게 동작하게 된다.

 

조금만 더 상세히 생성되는 과정을 작성해보자.

function Circle(radius) {
    // 1. 암묵적으로 빈 객체가 생성되고 this에 바인딩된다.
    console.log(this); // Circle {}
    // 2. this에 바인딩되어 있는 인스턴스를 초기화한다.
    this.radius = radius;
    this.getDiameter = function() {
    	return 2 * this.radius;
    };
    
    // 3. 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다.
}

const circle = new Circle(1);
console.log(circle); // Circle {radius: 1, getDiameter: f}
  1. 향후 생성자 함수가 생성한 인스턴스가 될 빈 객체가 생성된다. 그리고 이 빈 객체(인스턴스)는 this에 바인딩된다. (this가 생성자 함수의 인스턴스를 가리키는 이유)
  2. 생성자 함수에 기술되어 있는 코드가 한 줄씩 실행되어 this에 바인딩되어 있는 인스턴스를 초기화한다. (개발자가 기술한 내용)
  3. 생성자 함수 내부의 처리가 끝나면, 인스턴스가 바인딩된 this가 암묵적으로 반환된다.

여기서, 암묵적으로 this가 반환되지만, 명시적으로 객체를 반환하면 암묵적인 this 반환이 무시되며, 원시 값을 반환하면 암묵적인 this값이 반환된다. (생성자 함수 내부에 return문이 반드시 생략되야 하는 이유)

function Circle(radius) {
    this.radius = radius;
    this.getDiameter = function() {
    	return 2 * this.radius;
    };
    
    // return {}; // this를 무시해버리고, {}를 반환한다.
    // return 100; // 100을 무시해버리고, this를 반환한다.
}

const circle = new Circle(1);
console.log(circle); // {} or Circle {radius: 1, getDiameter: f}

그럼 결국 객체, 함수, 생성자 함수 셋의 관계가 어떻게 되는거지?

함수는 객체다. 그렇지만, 모든 객체는 함수가 아니다.

다시 말해보자.

함수 선언문 또는 함수 표현식으로 정의한 함수 (이 제한을 놓치지 말자!)일반적인 함수로서 호출할 수도 있고, new 연산자와 함께 생성자 함수로도 호출할 수 있다.

함수는 객체이므로, 일반 객체와 동일하게 동작할 수 있다. 일반 객체가 가지고 있는 내부 슬롯과 내부 메서드를 함수 객체도 가지고 있기 때문이다.

그렇지만, 일반 객체는 다르다.

일반 객체는 호출할 수 없지만, 함수는 호출할 수 있다.

함수에는 함수 객체만을 위한 [[Environment]], [[FormalParameters]] 같은 내부 슬롯과 [[Call]], [[Construct]] 같은 내부 메서드를 갖고 있기 때문이다.

 

함수가 일반 함수로서 호출이 되면, Call 메서드가 호출이 되고, new 연산자로 호출이 되면, Constuct가 호출이 되는 것이다.

function foo() {};

foo(); // 일반 함수 : [[Call]] 호출

new foo(); // 생성자 함수 : [[Construct]] 호출

함수는 호출할 수 있어야 하므로, [[Call]] 내부 메서드를 무조건 가지고 있다. (Callable이라고 한다)

그치만, [[Construct]]는 항상 갖고 있는 것은 아니다.

즉, 일반 함수로서만 호출할 수 있는 함수 객체가 있다는 것이다. (non-constructor라고 하며, [[Construct]]가 있다면 constructor라고 한다.)

  • constructor : 함수 선언문, 함수 표현식, 클래스(클래스도 함수다)
  • non-constructor : 메서드(ES6 함수 축약 표현), 화살표 함수
const arrow = {} => {};
new arrow(); // TypeError: arrow is not a constructor

const obj = {
	x() {} // ES6 함수 축약 표현으로, 메서드라고 할 때는 ES6의 축약 표현에서만 메서드라고 명명한다.
}

new obj.x(); // TypeError: obj is not a constructor

차근히 다 배울 내용이니, 예제로 대신하고 넘어가도록 하자.

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

const circle = Circle(5); // 일반 함수로서 호출하면 어떻게 되는지 보자.
console.log(circle); // undefined

console.log(radius); // 5
console.log(getDiameter()); // 10

console.log(circle.getDiameter()); // Uncaught TypeError: Cannot read properties of undefined (reading 'getDiameter')

재밌는 건 마치 생성자 함수처럼 만들어둔 함수를 그냥 일반 함수처럼 호출을 하면 위 예시와 같이 진행된다.

또한, Circle 함수를 일반 함수로 호출했을 때, this전역 객체 window를 가리키게 되어, 그 값을 쓸 수 있게 된다.

 

이런 전역으로 생성되버리는 것을 막고자 나온 ES6 문법이 new.target이다.

function Circle(radius) {
    if(!new.target) {
        return new Circle(radius);
    }
    this.radius = radius;
    this.getDiameter = function() {
    	return 2 * this.radius;
    };
    
}

const circle = Circle(5);
console.log(circle.getDiameter()); // 10

만약, new로 생성되지 않았다면, new.target은 undefined가 되어 위와 같은 형태로 생성자 함수가 동작하도록 설정할 수 있다.

 

일급 객체는 한번 알고 가자.

사실 그냥 넘어가긴 했지만, 이전 포스팅에서도 일급 객체를 언급했었다.

앞서 함수가 객체라고 하는 이유이므로 알아보고 넘어가자.

 

일급 객체에는 조건이 있다.

자바스크립트 함수가 일급 객체 조건을 몇개나 만족하는지 보도록 하자.

  1. 무명의 리터럴로 생성할 수 있다. 즉, 런타임에 생성이 가능하다.
  2. 변수나 자료구조(객체, 배열 등)에 저장할 수 있다.
  3. 함수의 매개변수에 전달할 수 있다.
  4. 함수의 반환값으로 사용할 수 있다.
// 무명의 리터럴로 생성
const increase = function(num) {
  return ++num;
}

const decrease = function(num) {
  return --num;
}

// 객체에 저장
const predicates = { increase, decrease };

// 매개변수로 함수 전달
// 반환값으로 함수 반환
function makeCounter(predicate) {
  let num = 0;
  return function() {
    num = predicate(num);
    return num;
  };
}

// 매개변수로 함수 전달
const increaser = makeCounter(predicates.increase);
console.log(increaser()); // 1
console.log(increaser()); // 2

// 매개변수로 함수 전달
const decreaser = makeCounter(predicates.decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2

모든 조건을 만족하고 있고, 따라서 자바스크립트에서 함수는 객체다.

앞서 말했듯이, 함수는 일반 객체랑은 다르다.

함수는 호출이 가능하지만, 일반 객체는 호출할 수 없다.

그리고 함수 객체에는 고유의 프로퍼티가 있다.

 

함수 객체의 프로퍼티

argument, caller, length, name, prototype 프로퍼티는 모두 함수 객체 고유의 데이터 프로퍼티다.

다른 부분은 별도로 찾아보는 것으로 넘어가도 되지만, prototype 프로퍼티는 한번 보고 넘어가자.

 

prototype 프로퍼티 생성자 함수로 호출 할 수 있는 함수 객체. 즉, constructor 만이 소유하는 프로퍼티다.

따라서, non-constructor에는 prototype 프로퍼티가 없다.

함수가 객체를 생성하는 생성자 함수로 호출될 때, 생성자 함수가 생성할 인스턴스의 프로토타입 객체를 가리킨다.

 

다시 말하면, 모든 함수는 non-constructor가 아니라면(화살표 함수, 메서드 등) 생성자 함수로 호출될 수 있다.

이 때, 인스턴스가 생성될 수 있고 이 인스턴스의 프로토타입 객체를 생성자 함수가 prototype이라는 프로퍼티로 가지고 있는 것이다.

그러나, 일반 객체는 생성자 함수가 될 수 없기 때문에, prototype을 갖고 있을 수 없다.

 

__proto__ 프로퍼티 [[Prototype]] 이라는 내부 슬롯이 가리키는 프로토타입 객체에 접근하기 위한 접근자 프로퍼티이며, 상속의 개념이 필요하므로 다음에 설명할 프로토타입에 대해 잘 알아보도록 하자.

 

결론

자바스크립트에서의 생성자에 대해 알아보았다.

이제 자바스크립트 생성자와 함수의 차이, 객체 생성의 차이를 좀더 명확히 할 수 있게 되었다.

이 생성자를 토대로, 다음은 프로토타입에 대해 진행해보도록 하자