[JS] 모던 Javascript Deep Dive 17장 - 생성자 함수에 의한 객체 생성
업데이트:
이전에 중괄호를 사용하여 객체 리터럴을 만드는 방법을 배운 적이 있다. 이번 챕터는 객체 생성 방식 중에서 생성자 함수를 사용하여 객체를 생성하는 방식을 알아볼 것이다.
여기서 생성자 함수란 무엇일까??
쉽게 말하면 new 키워드와 함께 호출하여 객체를 생성하는 함수이다.
이때 만들어진 객체는 인스턴스라고 한다.
new 키워드를 이용하면 생성자 함수를 만들 수 있고 기본적으로 내장되어 있는 생성자 함수(=빌트인 생성자 함수)도 사용할 수 있다.
기본적으로 내장되어 있는 생성자 함수는 대표적으로 String, Number, Boolean, Function, Array, Date, RegExp 등이 있다.
먼저 기본적으로 빈 객체를 만들어주는 Object 생성자부터 알아보자.
[01] Object 생성자 함수
const person = new Object(); // 객체 생성
person.name = 'minhye'; // 프로퍼티 추가
person.sayHello = function (){ // 메서드 추가
console.log('Hi! My name is' + this.name);
};
console.log(person); // {name: "minhye", sayHello: f}
person.sayHello(); // Hi! My name is minhye
처음 new 키워드와 함께 Object 생성자 함수를 만들면 프로퍼티가 없는 빈 객체가 만들어진다. 프로퍼티나 메서드를 추가하여 객체를 만들어 나가야 한다.
그렇기 때문에 객체 리터럴을 사용하는 것이 더 간편해보이고 Object 생성자 함수가 있다는 것만 알아두고 넘어가는 편이 좋겠다.
[02] 생성자 함수
그렇다면 생성자 함수는 왜 사용할까?? 객체 리터럴의 단점과 생성자 함수의 장점을 알면 이해할 수 있다.
객체 리터럴과 생성자 함수의 객체 생성 방식 비교
🥕 객체 리터럴 방식
객체 리터럴의 객체 생성 방식은 간편하지만 하나의 객체만 생성할 수 있다.
그렇기 때문에 아래 코드처럼 프로퍼티의 구조가 똑같아도 프로퍼티 값이 다르면 두개의 객체를 생성해 주어야 하는 단점이 있다.
const circle1 = {
radius: 5,
getDiameter() {
return 2 * this.radius;
}
};
console.log(circle.getDiameter());
const circle2 = [
radius: 10,
getDiameter(){
return 2 * this.radius;
}
};
console.log(circle2.getDiameter());
// 두 객체가 radius 값만 다르고 같은 구조인데 두개의 객체가 불필요하게 생성됨.
🥕 생성자 함수 방식
생성자 함수 방식으로 객체를 만들면 프로퍼티 구조가 같은 객체 여러개를 한개의 생성자 함수만으로 간편히 생성할 수 있다.
function Circle(radius){
this.radius = radius;
this.getDiameter = function(){
return 2 * this.radius;
};
}
//인스턴스 생성
const circle1 = new Circle(5);
const circle2 = new Circle(10);
console.log(circle1.getDiameter());
console.log(circle2.getDiameter());
생성자 함수의 인스턴스 생성 과정
인스턴스 생성 과정을 이해하려면 this의 개념도 어느정도 알아두어야 한다.
🔍 this
📢 객체 자신의 프로퍼티나 메서드를 참조하기 위한 자기 참조 변수다.
인스턴스가 생성되면 this에 바인딩된다. 바인딩은 식별자와 값을 연결하는 과정이다.
this 바인딩은 this와 this가 가리킬 객체를 바인딩하는 것이다.
🎯 인스턴스 생성 과정
- 암묵적으로 빈 객체 생성, this에 바인딩
- 생성된 인스턴스를 초기화(인스턴스 프로퍼티 추가 및 초기값 할당)
- 암묵적으로 인스턴스 반환
function User(name) {
// this = {}; (빈 객체가 암묵적으로 만들어지고 this에 바인딩된다)
console.log(this); // User{}
// this에 바인딩되어 있는 인스턴스 초기화
this.name = name;
this.isAdmin = false;
//return this; (this가 암묵적으로 반환)
}
const user = new User("민혜");
console.log(user); // User{name: "민혜", isAdmin: false}
// 실제로 함수에 반환문이 없지만 this가 암묵적으로 반환됨
console.log(user.name); // 민혜
console.log(user.isAdmin); // false
return 문이 없으면 암묵적으로 this가 반환되는데 return문이 존재하면 어떻게 될까??
function Circle(radius){
this.radius = radius;
this.getDiameter = function(){
return 2 * this.radius;
};
return {};
}
const circle = new Circle(1);
console.log(circle); // {}
// this가 무시되고 빈 객체를 반환한다.
위 코드를 보면 함수에서 {}를 반환한 바람에 this가 무시되고 빈 객체가 반환되었다….
+) 원시값을 반환되면 오히려 원시값이 무시되고 this가 반환된다고 한다.
결론적으로 생성자 함수 내부에서 this가 아니라 다른 값을 반환하면 생성자 함수의 기본 동작을 훼손하는 것이므로 return 문은 생략해야 한다 !!
내부 메서드 [[Call]] 과 [[Counstruct]]
프로퍼티 어트리뷰트 단원에서 모든 객체는 내부 슬롯과 메서드를 갖는다는 것을 배웠다.
함수도 객체이지만 함수는 일반 객체와 다르게 호출할 수 있다는 특징을 가지고 있다.
그래서 함수 객체는 함수 객체만을 위한 [[ Call ]] , [[Construct]]와 같은 내부 메서드를 추가로 가지고 있다.
function foo(){}
// 일반적인 함수 호출: [[Call]] 호출
foo();
// 생성자 함수 호출 : [[Counstruct]] 호출
new foo();
일반 함수로 호출되면 [[Call]]이 호출되고 생성자 함수로 호출되면 [[Counstruct]]가 호출된다.
- callable : 호출할 수 있는 객체나 함수, 내부 메서드 [[Call]]을 갖는 함수
- constructor : 생성자 함수로서 호출할 수 있는 함수, 내부 메서드 [[Construct]]를 갖는 함수
- non-constructor : 객체를 생성자 함수로서 호출할 수 없는 함수
함수는 호출 가능하므로 함수 객체는 반드시 callable일 것이다. 그런데 모든 함수가 constructor은 아니다. 모든 함수 객체를 생성자 함수로 호출할 수는 없다는 뜻이다.
정리하자면 함수는 모두 호출 가능한데 그 중 생성자 함수로 호출할 수 있는 객체와 아닌 객체로 나뉜다.
constructor와 non-constructor의 구분
그렇다면 constructor와 non-constructor는 어떻게 구분할까??
자바스크립트 엔진은 함수 정의 방식에 따라 이 둘을 구별한다.
- constructor : 함수 선언문, 함수 표현식, 클래스
- non-constructor : 메서드(ES6 메서드 축약 표현), 화살표 함수
//constructor
function foo(){} // 함수 선언문
new foo(); // foo{}
const bar = function() {}; // 함수 표현식
new bar(); // bar{}
const baz = {
x : function() {}
};
new baz.x(); // x {}
non-constructor 의 경우 new 연산자와 함께 생성자 함수로 호출했을 때 오류 발생
// non-constructor
const arrow = () => {};// 화살표 함수
new arrow(); // TypeError: arrow is not a constructor
const obj = {
x() {}
}; // 메서드
new obj.x(); //TypeError: obj.x is not a constructor
이 constructor 속성을 이용하여 나중에 인스턴스가 어떤 생성자 함수에 의해 생겨났는지 알 수 있다고 한다.
new. target
만약 new 연산자 없이 생성자 함수가 호출되면 일반 함수로 호출되어 버린다.
그렇게 되면 [[Construct]]가 호출되는 것이 아니라 [[Call]]이 호출된다.
function Circle(radius){
this.radius = radius;
this.getDiameter = function(){
return 2 * this.radius;
}
}
const circle = Circle(2); //new 연산자와 함께 호출되지 않았다.
console.log(circle); // undefined
//함수 내부의 this는 전역객체 window를 가리킨다.
console.log(radius); //2
console.log(getDiameter()); //4
circle.getDiameter();
//TypeError: Cannot read property 'getDiameter' of undefined
이것을 방지하기 위해 ES6에서 new.tartget이라는 메타 프로퍼티를 지원한다.
아래 코드를 이해하면 new.target이 어떻게 사용되는지 알기 쉬울 것이다.
function Circle(radius){
// new 연산자와 생성자 함수로서 호출했는지 확인하는 코드 추가
if(!new.target){
return new Circle(radius);
}
this.radius = radius;
this.getDiameter = function(){
return 2 * this.radius;
}
}
const circle = Circle(2); //new 연산자와 함께 호출되지 않았다.
console.log(circle.getDiameter()); // 4
//new.target이 없었다면 원래는 오류가 남.
new 연산자와 함께 호출되면 new.target은 함수 자신을 가리키는데 new 연산자 없이 일반 함수가 호출되면 new.target은 undefined 값을 갖는다.
new 연산자와 함께 호출되지 않은 경우 new 연산자와 함께 재귀 호출을 통해 생성자 함수로서 호출할 수 있다.
방어 코드로 사용하기도 한다.
댓글남기기