앞서 함수 섹션들에서도 살펴본 바와 같이 객체를 생성 함에 있어서 객체 리터럴 및 함수 리터럴을 사용하여 객체를 생성하는 방법도 있었는데요. 단순히 객체 리터럴을 이용해 객체를 생성하는 방식은 직관적이고 간편하다는 장점을 가지고 있으나 여러 객체 간의 공통된 프로퍼티가 존재하더라도 또 그것을 객체들에 일일이 작성을 해주어야 한다는 단점 또한 가지고 있습니다.
즉 공통된 작업을 수행하는 코딩을 일일이 작성하는 행위를 함수를 만들어 대체하여 최대한의 효율성을 확보하는 것처럼 공통된 프로퍼티를 가지고 있는 객체들에 대해서도 마찬가지로 최소한의 코드 작성을 회피하여 효율적으로 작성하기 위해 우리는 '생성자 함수' 라는 것을 사용할 수 있습니다.
생성자 함수란 간단히 말해 '객체를 생성하는 함수' 라고 봐도 무방하며, 이때 이 생성자 함수에 의해 생성된 객체들을 통칭 '인스턴스(instance)' 라고 부르죠.
이 생성자 함수는 일반 함수와의 구분을 짓기 위해 네이밍을 파스칼 케이스로 정의합니다. 또한 개체들 간의 공통적인 프로퍼티 외에도 개체들 마다 다른 프로퍼티들 또한 this 키워드를 사용하여 프로퍼티를 생성하는 작업 또한 가능합니다.
그리고 생성된 생성자 함수를 호출하여 인스턴스를 생성하기 위해서 new 키워드와 함께 호출 된다는 특징도 가지고 있죠.
// ① 생성자 함수 선언 예시) function Person(name, age){ // 함수를 선언하듯 작성하되 this.name = name; // 자기 참조 변수인 this 키워드를 이용해 생성하는 객체의 현재 name과 age 변수의 값을 인자로 넘어온 name과 age 값으로 할당합니다. this.age = age; // 여기서 this 키워드는 생성자 함수를 통해 생성하고 있는 현재의 객체를 가리킨다고 생각하시면 됩니다. this.introduce = function(){ console.log(`안녕하세요~ 저는 ${this.name} 이고, 나이는 ${this.age}입니다!`) } } const person1 = new Person("Re_Go", 30); // person1 객체를 new 연산자와 생성자 함수를 작성하여 Person 함수에서 정의된 프로퍼티와 값을 가지는 객체로 생성합니다. const person2 = new Person("김유연", 22); // 이렇게 생성된 person1과 person2는 서로 독립된 개체로서 어느 한쪽에서 값을 변경하더라도 같은 생성자 함수로 생성된 나머지 객체들의 프로퍼티에 영향을 주지 않게 됩니다.
- 여기서 생성자 함수 안의 this의 역할은 생성자 함수에 의해 암묵적으로 인스턴스가 생성될 때 생성된 인스턴스를 바인딩 하는 역할로 사용됩니다.
즉 this 뒤의 프로퍼티들은 "지금 이 객체의 프로퍼티는" 을 의미하며 이때 넘겨 받은 인자값을 저장하고 있는 매개변수의 값들을 해당 객체의 프로퍼티의 값으로 초기화 합니다.
다시 말해 this 키워드는 생성자 함수가 매번 실행될 때마다 인스턴스를 초기화하고, 현재 생성하는 객체의 속성을 설정하는 역할을 수행합니다. 이렇게 this 키워드로 생성된 각각의 객체들은 해당 프로퍼티의 값을 고유하게 갖게 되는 것이며, 최종적으로 변수에 할당 됨으로서 사용자가 해당 인스턴스에 접근 및 제어할 수 있는 것이죠.
이러한 생성자 함수를 통해 인스턴스를 생성해내는 코드를 작성시 return 키워드는 생략하는데, 이는 자바스크립트 엔진에서 암묵적으로 해당 생성자 함수를 통해 만들어진 객체를 암묵적으로 반환하는 코드를 자동적으로 기입하므로 굳이 사용자가 해당 인스턴스의 반환에 대한 키워드를 작성할 필요가 없는데,
만약 사용자가 return 키워드 뒤에 빈 값을 포함한 객체를 반환값으로 작성하게 된다면, 앞전의 this 키워드로 바인딩 된 객체(인스턴스)는 무시되고, 해당 객체만 반환이 되고, 반대로 원시 값을 작성하게 되면 원시 값에 대한 반환은 무시되고 암묵적으로 this에 의해 바인딩 된 객체(인스턴스)가 정상적으로 반환됩니다.
// ① return 키워드를 명시적으로 작성하여 객체를 반환시. function Person(name, age) { // 생성자 함수 내에서 this를 사용하여 객체의 프로퍼티를 설정 this.name = name; this.age = age; // return { // 생성자 함수에서 명시적으로 객체를 반환합니다. greeting: `안녕하세요! 나는 ${this.name}이고, ${this.age}살입니다.`, }; } // 생성자 함수를 사용하여 객체 생성 const personObj = new Person('John', 30); console.log(personObj); // { greeting: '안녕하세요! 나는 John이고, 30살입니다.' } 출력 console.log(personObj.name); // undefined 출력. // ② return 키워드를 명시적으로 작성하여 객체 이외의 타입 값(원시 타입) 반환시. function Person(name, age) { // 생성자 함수 내에서 this를 사용하여 객체의 프로퍼티를 설정 this.name = name; this.age = age; // 생성자 함수에서 원시값을 반환 return 'Hello, world!'; } // 생성자 함수를 사용하여 객체 생성 const personObj = new Person('John', 30); console.log(personObj); // 출력: Person { name: 'John', age: 30 }
자바스크립트에서 모든 함수는 내부 슬롯에 [[Call]] 메서드를 가지고 있습니다. 이 메서드에 의해 모든 함수들은 호출이 되어 특정한 동작을 수행하거나, 실행되도록 할 수 있는 것이지요. 우리는 이러한 객체의 특성을 Callable이라고 부릅니다.
하지만 모든 함수가 내부 슬롯의 메서드 중 하나인 [[Construct]] 메서드를 가지고 있는 것은 아닙니다. 그래서 해당 메서드를 가지고 있는 함수들을 가리켜 '생성자 함수(Constructor)' 라고 부르고 있죠. 흔히 new 키워드와 같이 호출되어 인스턴스를 생성하는 함수 선언문이 대표적이고, Class 등이 이에 해당합닏.
정리하자면, 내부 슬롯에 [[Constructor]] 메서드를 가지고 있는 함수들은 생성자 함수이고, 해당 메서드가 없는 함수들은 일반 함수(비생성자 함수 | Non-Constructor) 라고 정의할 수 있는 것이죠.
가장 쉽게 이 둘을 구분 짓는 방법은 함수 선언의 종류에 의한 방법으로, constructor는 함수 선언문, 함수 표현식, 클래스를 의미하고, non-constructor는 ES6 메서드의 축약 표현에 의한 메서드, 화살표 함수가 해당 됩니다.
//① 함수 선언문에 의한 constructor 생성 예시) function Person(name, age) { this.name = name; this.age = age; } const person1 = new Person("Re_Go", 30); console.log(person1); // Person { name: 'Re_Go', age: 30 } // ② 함수 표현식에 의한 constructor 생성 예시) const Person = function(name, age) { this.name = name; this.age = age; }; const person1 = new Person("Re_Go", 30); console.log(person1); // Person { name: 'Re_Go', age: 30 } // ③ 클래스에 의한 constructor 생성 예시) class Person { constructor(name, age) { this.name = name; this.age = age; } } const person1 = new Person("Re_Go", 30); console.log(person1); // Person { name: 'Re_Go', age: 30 } // ④ 화살표 함수에 의한 constructor 생성 시도시) const Person = (name, age) => { this.name = name; this.age = age; } const person1 = new Person("Re_Go", 30); // 에러 발생!
앞 섹션에서는 생성자 함수와 일반 함수의 차이에 대해서 설명을 드렸는데요. [[Constructor]] 메서드를 가지고 있는 함수가 호출 된다고 해서 인스턴스 생성을 목적으로 호출이 되는 것은 아닙니다.
무슨 말이냐면, [[Constructor]] 메서드를 가지고 있는 함수가 새로운 인스턴스 생성(메모리에 적재) 을 위해 new 키워드의 연산자와 함께 호출이 될때에만 생성자 함수로서 호출이 되어 인스턴스를 생성하는 코드를 실행한다는 것이지, 아무리 [[Constructor]] 메서드를 가지고 있는 함수라고 해도 new 키워드 없이 호출이 된다면 그것은 [[Call]] 메서드가 실행 되어 호출 된 Callable 함수(일반 함수) 로서 호출이 된다는 것이지요.
이처럼 어떠한 함수가 생성자 함수인지를 구별해내는대에 있어서 사용자의 주의가 다소 필요한데요. 보통의 방법으로는 생성자 함수의 목적으로 작성 된 함수의 경우 네이밍을 파스칼 케이스로, 일반 함수의 목적으로 작성 된 함수의 경우 네이밍을 카멜 케이스로 구분을 짓고 이습니다.하지만 개발자도 사람이기에 네이밍을 보고도 실수를 하는 경우가 발생 될 수 있으므로, ES6에서는 이러한 상황을 방지하고자 현재 실행중인 함수가 new 키워드와 함께 호출 되었는지를 확인 시켜주는 new.target 메타 프로퍼티를 활용하여
만약 해당 함수가 new와 함께 호출되지 않은 상태라면 다시 new 키워드와 함께 재귀 호출되는 코드문을 구현함으로 해당 함수가 생성자 함수로서의 역할을 수행할 수 있게 할 수 있습니다.
function Person(name, age) { if(new.target === undefined){ //만약 이 함수가 new 연산자와 함께 호출되지 않았다면 new.target은 undefined 입니다. 참고로 위의 조건은 !new.target 한 문장으로도 설정이 가능합니다. return new Person(name,age); // 조건이 참일 경우, 즉 new 연산자 없이 호출 된 함수일 경우 new 키워드와 함께 다시 호출되도록 하여 생성자 함수로서의 역할을 수행하게 합니다. } this.name = name; this.age = age; } const person1 = new Person("Constructor is here", 30); // 일반적인 인스턴스 생성 const person2 = Person("Recursive is here", 100); // new 키워드 없이 함수 호출 console.log(person1); // Person { name: 'Re_Go', age: 30 } // 정상적으로 인스턴스 생성 console.log(person2); // Person { name: 'Recursive is here', age: 100 } // 재귀 함수(Person(name, age) 가 new 키워드와 함께 발동 되어 코드문을 수행한 후 식별자에 반환 됩니다.
★ 참고로 대부분의 빌트인 함수는 new 연산자와 함께 호출되어는지를 확인한 후 적절한 값을 반환하기 때문에 new 연산자 없이 생성 되더라도 자동으로 new 연산자와 함께 호출되기에 굳이 재귀 함수 구현으로 new 키워드 구문을 생성하지 않아도 되지만,
String, Number, Boolean 빌트인 함수로 객체를 생성할 때 new 연산자 없이 호출할 경우 각각의 인자값을 그에 맞는 '문자열', '숫자형', '불리언형' 으로 반환하기 때문에 이러한 형질을 이용해 특정 변수의 값이나 리터럴 값들을 형변환 하는대에도 사용합니다.
// ① Object 빌트인 함수로 new 키워드 없이 인스턴스를 생성할 경우의 예시) const obj = Object(); console.log(typeof obj); // 출력: 'object' // 내부적으로는 new 연산자와 함께 호출되었다고 처리 const objWithNew = new Object(); console.log(typeof objWithNew); // 출력: 'object' // ② String 빌트인 함수로 new 키워드 없이 인스턴스를 생성할 경우의 예시) const strObject = String(42); console.log(typeof strObject); // 출력: 'string' const strLiteral = 'Hello'; console.log(typeof strLiteral); // 출력: 'string'