모던 자바스크립트 스터디 17장

lamPolar·2024년 3월 22일
0

javascript

목록 보기
5/5
post-thumbnail

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

이번장에서 꼭 알아야 하는 것!

  1. 다양한 객체 생성 방식 중에 생성자 함수를 사용해 객체를 생성하는 방식
  2. 객체 리터럴을 사용해 객체를 생성하는 방식 vs 생성자 함수를 사용해 객체를 생성하는 방식 (장단점, 차이)

17.1 Object 생성자 함수

new 연산자와 함께 Object 생성자 함수를 호출하면, 빈객체 {}를 생성해서 반환한다.

const person = new Object();
console.log(person); // {}

하지만, 반드시 Object 생성자 함수를 이용해서 빈 객체를 생성해야하는 것은 아니다. 오히려 객체 리터럴이 더 간편하므로, 특별한 이유가 없다면 Object 생성자 함수를 이용하지 않는 것을 권장한다.

Q. 그렇다면, 특별한 이유는 어떨때??

아래에서 알려주겠음~~

생성자 함수 constructor 란 ?

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

자바스크립트는 기본적으로 Object, String, Number, Boolean, Function, Array, Date, RegExp, Promise등의 빌트인 생성자 함수를 제공한다.
const strObj = new String('Lee');
console.log(typeof strObj); //object
console.log(strObj); //String {'Lee'}

이처럼, 생성자 함수에 의해 생성하면 결과로 반환되는 건 원시값이 아닌 객체이다.
왜?? 객체를 생성하는 함수를 호출했으니까!

17.2 생성자 함수

객체 리터럴에 의한 객체 생성방식의 문제점 : 한번에 한 객체만 만드는 것

객체 리터럴에 의한 객체 생성 방식은 직관적이고, 간편하지만 단 하나의 객체만 생성한다.그래서 동일한 프로퍼티 구조를 갖는 여러개의 객체를 만들 때는 매번 같은 프로퍼티와 메서드를 기술해야 하므로 중복적인 코드를 쓰게되고, 비효율적이다.

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

위의 예제처럼, 프로퍼티 구조가 동일한 두개의 객체를 만들때,
생성자 함수를 써준다!

왜 ? 생성자 함수에 의한 객체 생성 방식의 장점이 있으니까!

마치 객체(인스턴스)를 생성하기 위한 클래스(템플릿)처럼 생성자 함수를 사용하여 프로퍼티 구조가 동일한 객체 여러개를 간편하게 생성할 수 있다.
실제로 클래스처럼 동작한다는 건 아니다!

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

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

Q. return이 없는데, 객체가 반환된다..?

Q. new가 없으면 어떻게 될까..? 에러가 날까?

짜잔! 한번에 두개나 생성이 가능하다구~~ 멋지지!
하지만 단점없는 기술은 없는법

생성자 함수의 작동방식

자바와 같은 클래스 기반 객체지향 언어의 생성자와 다르게, 형식이 정해져있지 않고, 일반 함수와 동일한 방법으로 함수를 정의하고 new 연산자와 함께 호출하면 해당 함수는 생성자 함수로 동작한다.

이게 무슨 뜻이냐면, 놀랍게도

const circle3 = Circle(15);
// 이렇게 new없이 호출하면 생성자함수가 아닌 일반 함수로서 호출이 된다는 것이다. 미쳤다. 
console.log(circle3); //undefined
// 하지만 반환문이 없으니, 암묵적으로 undefined를 반환한다.

그리고, this가 가리키는 객체가 반환할 인스턴스가 아닌 전역객체가 되어, radius가 전역객체의 프로퍼티가 되는 일이 발생한다! 미친거같아

Q. 그러면, 어떻게해야해? 아예 쓰지 말아야하나? 그럴수는 없잖슴!

이건 밑에서 알려줌~~

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

생성자 함수는 프로퍼티 구조가 동일한 인스턴스를 생성하기 위한 템플릿(클래스)로서 동작해야하므로, 함수 몸체 내에서
1. 인스턴스를 생성하고
2. 생성된 인스턴스를 초기화 (프로퍼티 추가 및 프로퍼티 초기값 할당)
을 해야한다.

다시 위의 Circle 생성자 함수를 보면, 프로퍼티 초기화 및 초기값 할당 부분을 보이지만, 인스턴스를 생성하고 반환하는 코드는 보이지 않는다.

function Circle(radius){
  //인스턴스 초기화 및 초기값 할당
  this.radius = radius;
  this.getDiameter = function (){
    return 2 * this.radius;
  };
}

//생성자 함수 호출
const circle4 = Circle(100);

보이지않으면 뭐다? 암묵적으로 생성한거다. 여기서 자바스크립트 엔진의 도움이 들어가게 된다.
new 연산자와 함께 생성자 함수를 호출하면, 자바스크립트 엔진은 암묵적인 처리를 통해 암묵적으로 인스턴스를 생성하고 인스턴스를 초기화하고 암묵적으로 인스턴스를 반환한다.

  1. 암묵적 인스턴스 생성과 this 바인딩 (자바스크립트 엔진)
    new 연산자와 함께 생성자 함수를 호출하는 순간, 암묵적으로 빈객체 {}를 만든다.
    그리고, 이 빈 객체를 this에 바인딩한다.
    (생성자 함수 내부의 this가 생성자 함수가 생성할 인스턴스를 가리키는 이유)

  2. 인스턴스 초기화 (개발자)
    함수 몸체 내부에 기술된 코드가 실행되어, this에 바인딩 되어있는 인스턴스를 초기화한다.
    프로퍼티나 메서드를 추가하고, 생성자 함수가 인수로 전달받은 초기값을 인스턴스 프로퍼티에 할당하여 초기화하거나 고정값을 할당한다.

  3. 인스턴스 반환 (자바스크립트 엔진 / 개발자)
    생성자 함수 내부의 모든 처리가 끝나면, 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다.
    (이게 아까 질문의 대답! return이 없는데, 객체가 반환되는 이유!)

function Circle(radius){
  // 1. 암묵적으로 빈객체 생성, this에 바인딩.
  console.log(this); // Circle {}
  
  // 2. this에 바인딩 된 인스턴스 초기화
  this.radius = radius;
  this.getDiameter = function(){
    return 2 * this.radius;
  };
  
  // 3. 인스턴스 반환
  // 1. 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환.
  // return {}; -> 2. 명시적으로 빈객체 반환
  // return 1; -> 3. 명시적으로 원시값 반환 => this 반환
}

const circle = new Circle(1);
console.log(circle); 
// Circle {radius: 1, getDiameter: f} -> 1, 3
// {} -> 2. 명시적으로 빈객체를 반환한 경우

반환되는 값은 무조건 2개 중 하나!
1. return문을 명시하지 않은 경우 -> 바인딩 된 this 객체
2. 명시적으로 다른 객체를 반환한 경우 -> 다른 객체
3. 명시적으로 원시값을 반환한 경우 -> 바인딩 된 this 객체
명시적으로 this가 아닌 다른 값을 반환하면, 생성자 함수의 기본 동작을 훼손하기 때문에, 생성자 함수 내에서는 무조건 return문을 생략해야한다!!

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

함수 선언문 / 함수 표현식으로 정의한 함수는 일반적인 함수로서 호출 + 생성자 함수로서 호출이 둘 다 가능하다

  • 함수는 객체이므로 일반 객체와 동일하게 동작할 수 있다.
    -> 일반 객체의 내부 슬롯, 내부 메서드를 가지고 있다.
  • 일반 객체는 호출할 수 없지만 함수 객체는 호출 할 수 있다.
    -> 함수로 동작하기 위해 [[Environment]], [[FormalParameters]]등의 내부 슬롯과, [[Call]], [[Construct]]같은 내부 메서드를 추가로 가지고 있다.

함수는 호출되는 방식에 따라 호출되는 내부 메서드가 다르다.
일반함수로서 호출되면 [[Call]]이 호출되고,
생성자 함수로서 호출되면 [[Construct]]가 호출된다.

function foo(){} // 함수 선언문으로 생성한 함수
foo(); // 일반 함수 호출 -> Call
new foo(); // 생성자 함수 호출 -> Constructor
  • 내부 메서드 [[Call]]은 호출할 수 있는 객체를 뜻하며, 모든 함수가 가지고 있고, [[Call]]메서드를 가지면 Callable이라고 한다.

  • 내부 메서드 [[Construct]]는 생성자함수로서 호출할 수 있는 함수만 가지고 있다.
    [[Construct]]를 가진 함수객체는 일반함수, 생성자 함수로서 호출할 수 있는 함수객체이고 constructor라고 하며,
    [[Construct]]를 갖지 않는 함수객체는 일반 함수로서만 호출할 수 있는 함수객체로, non-constructor라고 한다.

결론적으로 함수 = callable이고,
함수 중 일부는 constructor, 나머지는 모두 non-constructor이다.

constructor와 non-constructor는 함수 정의방식에 따라 구분된다.
함수 선언문, 함수 표현식, 클래스 = constructor
메서드 (ES6메서드 축약표현), 화살표 함수 = non-constructor

👀 주의할 점
ES6의 메서드 축약 표현만이 메서드로 인정된다.
함수가 어디 할당되어있는지가 아니라 함수 정의방식에 따라 구분하기 때문

function foo(){} // constructor
const bar = function (){}; // constructor
const baz = {
  x : function (){}, // (주의!!) constructor
  //-> 이건 일반함수로 정의되었다고 판단 -> 메서드 인정 xx
  y() {}, // non-constructor
};
const arrow = () => {}; // non-constructor
new foo(); // foo {}
new bar(); // bar {}
new baz.x(); // x {}
new arrow(); // TypeError
new obj.x(); // TypeError

non-constructor인 함수 객체는 내부 메서드 [[Construct]]를 갖지 않기 때문에 에러가 발생함.

주의 !!!! 따라서, 생성자 함수가 아닌 일반함수를 기대하고 생성한 함수가 생성자 함수처럼 호출될 경우 생성자 함수처럼 동작할 수 있음을 알고 있어야함!!

// 함수를 정의할 때 의도는 생성자 함수가 아니었지만,
function add(x, y){
  return x + y;
}
// 함수 선언문으로 생성했기 때문에 생성자 함수처럼 호출이 가능
let inst = new add();
// 반환문이 원시값이므로 무시되고 암묵적으로 빈객체가 생성되어 반환됨.
console.log(inst); // {}

이런 상황이 발생할 수도 있다.

반대로, 생성자 함수로 기대하고 만든 함수가 일반함수처럼 불렸을 때도 동작할 수 있음을 알고 있어야함!!

// 위에서 생성한 Circle 생성자 함수가 있다고 해보자.
function Circle(radius){
  ...
}
// new 없이 호출했을 경우, 생성자함수가 아닌 일반함수처럼 호출이 된다.
const circle = Circle(5);
console.log(circle); // 반환값이 없으므로 undefined가 암묵적으로 반환된다.
console.log(radius) // 5
console.log(getDiameter()); // 10
console.log(circle.getDiameter()); // TypeError

전혀 예측하지 않은대로 동작하게 된다.
인스턴스의 프로퍼티가 되길 바랐던 radius, getDiameter는 전역객체의 프로퍼티가 되는것.

따라서, 생성자함수는 일반적으로 첫 문자를 대문자로 기술하는 파스칼 케이스로 명명해 일반 함수와 구별할 수 있도록 하자.

하지만, 모든걸 사람에게 맡기면 난리가 나니까.
-> ES6에서는 new.target을 지원한다.
new.target은 this와 유사하게 constructor인 모든 함수 내부에서 암묵적인 지역 변수와 같이 사용된다.

new.target을 사용할 경우 new연산자와 함께 호출되었는지를 판별할 수 있다.

  • new 연산자와 함께 호출되었을 경우 (생성자 함수로서의 호출)
    => new.target = 함수 자신을 가리킴
  • new 연산자 없이 호출되었을 경우 (일반 함수로서의 호출)
    => new.target = undefined
function Circle(radius){
  if (!new.target){
    return new Circle(radius)}
  .....
}

이처럼 new 연산자와 함께 호출되지 않았을 경우 생성자함수를 재귀 호출하는 방식을 선택하면, 생성자함수는 늘 일반함수로서의 호출이 아닌 생성자함수로서의 호출을 보장할 수 잇다.

ES6이전에는 스코프 세이프 생성자 패턴이라는걸 썼음

function Circle(radius){
  if (!this instanceof Circle){
    return new Circle(radius)}
  ....
}

this가 전역객체인지, Circle의 인스턴스인지를 판별해서 재귀적으로 호출하는것

그래서, 이런 문제가 있는 생성자 함수지만, 대부분의 빌트인 생성자함수는 new연산자와 함께 호출되었는지를 확인한 뒤에 적절한 값을 반환하지만,

String, Number, Boolean생성자함수의 경우 new 연산자 없이 호출하면 원시값을 반환한다.

const str = String(123);
console.log(str, typeof str); //123 string

각각의 래퍼 객체 별로 생성자 함수로서 호출되지 않았을 경우에 대한 처리가 다르므로 가급적 사용하지 않는것을 권장한다.

profile
불안을 안고 구르는 작은 모난 돌

0개의 댓글