[JS] 생성자 함수에 의한 객체 생성

vSsongv·2021년 11월 28일
0

JS

목록 보기
15/40
post-thumbnail

객체는 객체 리터럴 이외의 생성자 함수를 사용하여 객체를 생성할 수 있다.

✅ Object 생성자 함수

  • new 연산자와 함께 Object 생성자 함수를 호출하면 빈 객체를 생성하여 반환한다.
  • 빈 객체를 생성한 이후 프로퍼티 또는 메서드를 추가하여 객체를 완성할 수 있다.
const person = new Object();

person.name = 'Ssong';

📌 생성자 함수 : new 연산자와 함께 호출하여 객체를 생성하는 함수를 말한다.
📌 인스턴스 : 생성자 함수에 의해 생성된 객체

🔎 JS는 Object 생성자 함수 이외에도 String, Number, Boolean, Function등 다양한 빌트인 생성자 함수를 제공한다.

const strObj = new String('Ssong');
const Number = new Number(123);
const function = new Function('x', 'return x + x');

❗ 반드시 Object 생성자 함수를 사용해 빈 객체를 생성해야 하는 것은 아니다. -> 빈 객체 만들려면 리터럴 쓰는거랑 다를 바가 없다.
❗❗ 객체를 생성하는 방법은 객체 리터럴을 사용하는 것이 더 간편하다.

✅ 생성자 함수

🔰 객체 리터럴 생성 방식의 문제점

  • 객체 리터럴에 의한 객체 생성 방식은 단 하나의 객체만 생성한다.
  • 만약 동일한 프로퍼티를 갖는 객체를 여러 개 생성해야 하는 경우에는 매번 같은 프로퍼티를 기술해야 하기 때문에 비효율적이다.
	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

❗ 프로퍼티 구조가 동일함에도 불구하고 매번 같은 프로퍼티와 메서드를 기술해야 한다. 만약 수십 개의 객체를 생성해야 한다면 문제는 커진다.

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

  • 위 코드를 생성자 함수 방식을 사용하면 아래처럼 된다.
	// 생성자 함수
	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 circle =  Circle(5);
	console.log(circle); //undefined -> Circle이 일반 함수로 호출되었을 때는, Circle내에 return문이 없으므로 undefined를 반환한다.

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

  • 생성자 함수의 역할은 프로퍼티 구조가 동일한 객체를 생성하기 위한 템플릿으로서 동작하여 인스턴스를 생성하는 것(필수)과 생성된 인스턴스를 초기화하는 것(옵션)이다.
	function Circle(radius) {
	  // 인스턴스 초기화
	  this.radius = radius;
	  this.getDiameter = function () {
	    return 2 * this.radius;
	  };
	}
	
	// 인스턴스 생성
	const circle1 = new Circle(5);

📌 new 연산자와 함께 함수를 호출하면 자바스크립트 엔진은 암묵적으로 인스턴스를 생성하고 반환한다.

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

  • 암묵적으로 빈 객체가 생성되고 이 객체가 생성자 함수가 생성한 인스턴스이다.
  • 그리고 이 인스턴스는 this에 바인딩이 된다. 이 모든 처리들은 런타임 이전에 실행된다.

    ❓ 바인딩

    • 식별자와 값을 연결하는 과정.
    • 예를 들어, 변수 선언은 변수 이름과 확보된 메모리 공간의 주소를 바인딩 하는 것이다.
    • this 바인딩은 this와 this가 가리킬 객체를 바인딩하는 것이다.
    • 할당은 개발자가 할 수 있지만 바인딩은 오직 JS엔진만이 할 수 있다.

2. 인스턴스 초기화

  • 생성자 함수에 기술되어 있는 코드가 한 줄씩 실행되어 this에 바인딩되어 있는 인스턴스를 초기화한다.
  • this에 바인딩되어 있는 인스턴스에 프로퍼티나 메서드를 추가하고 초기값을 프로퍼티에 할당해 주는 것이다.

3. 인스턴스 반환

  • 함수 내부의 모든 처리가 끝나면 인스턴스가 바인딩된 this가 암묵적으로 반환된다.
function Circle(radius) {
  // 1. 암묵적으로 인스턴스가 생성되고 this에 바인딩된다.
  
  // 2. this에 바인딩되어 있는 인스턴스를 초기화한다.
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
  
  // 3. 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다. return this
}

const circle = new Circle(1);
console.log(circle); //Circle {radius: 1, gerDiameter: f}
  • 만약 this가 아닌 다른 객체를 명시적으로 반환하면 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가 반환된다.
function Circle(radius) {
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
  
  return 100; // 명시적으로 원시 값을 반환
}

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

❗❗ 이처럼 생성자 함수 내부에서 명시적으로 this가 아닌 다른 값을 반환하는 것은 생성자 함수의 기본 동작을 훼손하기 때문에, 생성자 함수 내부에서는 return 문을 반드시 생략해야 한다.

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

  • 함수는 일반 객체가 가지고 있는 내부 슬롯과 내부 메소드들은 물론, 함수 객체만을 위한 [[Enviroment]], [[FormalParameters]]등의 내부 슬롯과 [[Call]], [[Construct]] 같은 내부 메소드를 추가로 가지고 있다.
  • 함수가 일반 함수로서 호출되면 함수 객체의 내부 메서드 [[Call]] 이 호출되고, new 연산자와 함께 생성자 함수로서 호출되면 내부 메서드 [[Construct]] 가 호출된다.
function foo() {}
foo(); // 일반적인 함수로서 호출 , [[Call]] 호출
new foo(); // 생성자 함수로서 호출, [[Construct]]호출
  • callable : 내부 메서드 [[Call]]을 갖는 함수 객체, 함수 객체는 반드시 callable이어야 한다.
  • constructor : 내부 메서드 [[Constructor]]를 갖는 함수 객체. 생성자 함수로서 호출할 수 있는 함수다.
  • non-constructor : 내부 메서드 [[Constructor]]를 갖지 않는 객체. 생성자 함수로서 호출할 수 없는 함수다.
  • ❗❗ 모든 함수 객체는 callable이면서 constructor이거나, callable이면서 non-constructor이다.

🔰 constructor vs non - constructor

  • constructor : 함수 선언문, 함수 표현식, 클래스
  • non-constructor : 메서드 축약 표현, 화살표 함수
function foo() {}
const bar = function () {};
//프로퍼티 x의 값으로 할당된 것은 일반 함수로 정의된 함수다. 이는 메소드로 인정하지 않는다.
const baz = {
  x: function() {}
}; 

new foo(); -> foo {}
new bar(); -> bar {}

new bar.x(); // x {}

//화살표 함수
const arrow = [] => {};
new arrow(); // ❌ Typeerror : arrow is not a constructor
//메소드 정의 : 축약 표현한 메소드로 인정한다.
const obj = {
  x() {}
}; 
new obj.x(); // ❌ TypeError : obj.x is not a constructor
  • 함수를 프로퍼티 값으로 사용하면 일반적으로 메소드로 통칭한다.
  • 하지만 ECMAScript 사양에서 메소드란 ES6의 메소드 축약 표현만을 의미한다.
  • 함수 정의 방식에 따라 constructor와 non-constructor를 구분한다.
  • 함수 선언문과 함수 표현식으로 정의된 함수만이 constructor이고, 화살표 함수와 축약 표현으로 정의된 함수는 non-constructor이다.
  • 함수를 일반 함수로서 호출하면 [[Call]] 이 호출되고, new연산자와 함께 생성자 함수로서 호출하면 [[Constructor]]가 호출된다.
  • ❌ non-constructor인 함수 객체를 생성자 함수로서 호출하면 에러가 발생한다.
function foo() {}
foo(); //일반 함수로서 호출, [[Call]] 이 호출된다.
new foo(); //생성자 함수로서 호출, [[Constructor]]가 호출된다. 

💠 new 연산자

  • new 연산자와 함께 함수를 호출하면 해당 함수는 생성자 함수로 동작한다. -> 함수 객체의 내부 메서드 [[Construct]]가 호출된다. (constructor 여야 한다.)
function add(x, y) {
    return x + y;
}
//생성자 함수로서 정의하지 않은 일반 함수를 new 연산자와 함께 호출
let inst = new add();
//함수가 객체를 반환하지 않으므로 반환문이 무시된다. 빈 객체 반환
console.log(inst); // {}

//객체를 반환하는 일반 함수
function user(name, role) {
    return { name, role };
}
//일반 함수를 new연산자와 함께 호출
inst = new user('Song', 'admin');
//함수가 생성한 객체를 반환한다.
console.log(inst); //{ name: 'Song', role: 'admin' }
  • new 연산자와 함께 호출하지 않으면 생성자 함수가 아니라 일반 함수로 동작한다.
function Circle(radius) {
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}

// 일반 함수로서 호출된다.
const circle = Circle(15);

// 반환문이 없으므로 undefined를 반환한다.
console.log(circle); // undefined

// 일반 함수로 호출된 Circle 내 this는 전역 객체 window를 가리킨다.
console.log(radius); // 15
console.log(getDiameter()); // 30

circle.getDiameter(); // TypeError: Cannnot read property 'getDiameter' of undefined
  • Circle은 일반 함수로서 호출되었기 때문에 Circle 함수 내부의 this는 전역 객체 window를 가리킨다. 즉, radius와 getDiameter는 전역 객체의 프로퍼티와 메서드가 된다.

📌 일반적으로 생성자 함수는 첫 문자를 대문자로 기술하여 일반 함수와 구별할 수 있도록 한다.

첫 문자를 대문자로 기술하는 파스칼 케이스로 명명했다 할지라도 실수는 언제나 발생할 수 있다. 이런 실수를 방지하기 위해 ES6에서는 new.target을 지원하고 있다.

const person(name) {
    if (!(this instanceof Person)) {
        return new Person(name);
    } //아래의 경우처럼 new를 붙이지 않고 호출하는 것을 대비하여 방어 코드를 만들자.
}

const me = Person('Ssong'); //new가 붙지 않은 상태로 호출되었다. 일반 함수로 호출되었다.

💠 new.target

  • 생성자 함수가 new 연산자 없이 호출되는 것을 방지하기 위해 ES6에서는 new.target을 지원한다.
  • new.target은 this와 유사하게 constructor인 모든 함수 내부에서 암묵적인 지역 변수와 같이 사용되며 메타 프로퍼티라고 부른다.
  • 📌 함수 내부에서 new.target을 사용하면 생성자 함수로서 호출되었는지 확인할 수 있다.
  • new 연산자와 함께 생성자 함수로서 호출되면 함수 내부의 new.target은 함수 자신을 가리킨다. 일반 함수로서 호출된 함수 내부의 new.target의 경우에는 undefined다.
function Circle(radius) {
  // new 연산자와 함께 호출되지 않았다면 new.target은 undefined다.
  if(!new.target) {
    // 생성자 함수를 재귀 호출하여 생성된 인스턴스를 반환
    return new Circle(radius);
  }
  
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}

// 일반 함수로서 호출되더라도 new.target을 통해 생성자 함수로서 호출된다.
const circle = Circle(5);
console.log(circle.getDiameter()); // 10

❗ new.target은 IE에서 지원하지 않는다. 이때 스코프 세이프 생성자 패턴을 사용할 수 있다.

function Circle(radius) {
  // 이 함수가 new 연산자와 함께 호출되지 않았다면 이 시점의 this는 전역 객체 window를 가리킨다.
  // this와 Circle은 연결되지 않는다.
  if(!(this instanceof Circle)) {
    // new 연산자와 함께 호출하여 생성된 인스턴스를 반환
    return new Circle(radius);
  }
  
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}

const circle = Circle(5);
console.log(circle.getDiameter()); // 10
  • 생성자 함수가 new 연산자와 함께 호출되면 함수의 선두에서 빈 객체를 생성하고 this에 바인딩되어 인스턴스와 생성자 함수는 서로 연결된다.
  • 이를 이용해 new 연산자와 함께 호출되었는지 확인할 수 있다.

💠 빌트인 생성자 함수

  • Object, Function 등의 대부분의 빌트인 생성자 함수는 기본적으로 new 연산자와 함께 호출되었는지를 확인하고 적절한 값을 반환한다.
  • 즉, new 연산자 없이 호출해도 new 연산자와 함께 호출했을 때와 동일하게 동작한다.
let obj = new Object();
console.log(obj); // {}

obj = Object();
console.log(obj); // {}


let f = new Function('x', 'return x ** x');
console.log(f); // f anonymous(x) { return x ** x }

f = Function('x', 'return x ** x');
console.log(f); // f anonymous(x) { return x ** x }

❗ 하지만 String,Number,Boolean 생성자 함수는 new 연산자 없이 호출하면 문자열, 숫자, boolean 값을 반환한다.

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

<모던 자바스크립트 deepdive와, 추가 탐구한 내용을 정리한 포스트입니다.>

profile
wanna be bright person✨ Front-End developer

0개의 댓글