일반적으로 객체 리터럴 방식으로 객체를 많이 생성하지만, 생성자 함수를 통해서도 객체를 생성할 수 있다.
new 연산자와 함꼐 Object 생성자 함수를 호출하면 빈 객체를 생성하여 반환한다. 빈 객체를 생성한 이후 프로퍼티 또는 메서드를 추가하여 객체를 완성할 수 있다.
const person = new Object()
person.name = "lee"
person.hello = function(){
console.log("hello world")
}
console.log(person) // { name: 'lee', hello: [Function (anonymous)] }
person.hello() // hello world
즉, 객체를 생성하는 방법으로 person = {}
가 가능하지만 person = Object()
도 가능하다.
여기서, 생성자 함수란 new 연산자와 함께 호출하여 객체를 생성하는 함수를 만든다. 생성자 함수에 의해 생성된 객체를 인스턴스라고 한다.
자바스크립트는 Object 함수 이외에도 String, Number, Boolean, Function, Array, Date, RegExp , Promise 등 빌트인 생성자 함수를 제공한다.
const strObj = new String("lee")
console.log(typeof strObj) // object
console.log(strObj) // [String: 'lee']
const numObj = new Number(123)
console.log(typeof numObj) // object
console.log(numObj) // [Number: 123]
const boolObj = new Boolean(true)
console.log(typeof boolObj) // object
console.log(boolObj) // [Boolean: true]
const funcObj = new Function(true)
console.log(typeof funcObj) // function
console.log(funcObj) // [Function: anonymous]
const arrayObj = new Array(1,2,3)
console.log(typeof arrayObj) // object
console.log(arrayObj) // [ 1, 2, 3 ]
const regExp = new RegExp(/ab+c/i)
console.log(typeof regExp)
console.log(regExp) // /ab+c/i
const date = new Date()
console.log(typeof date) // object
console.log(date) // 2021-07-26T14:38:39.870Z
반드시 Object 생성자를 사용해 빈 객체를 생성해야 하는 것은 아니다. 객체 리터럴을 사용하는 것이 훨씬 더 편하다. 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
객체는 프로퍼티를 통해 객체 고유의 상태를 표현한다. 그리고 메서드를 통해 상태 데이터인 프로퍼티를 참조하고 조작하는 동작을 표현한다. 따라서, 프로퍼티는 객체마다 프로퍼티 값이 다를 수 있지만, 메서드는 내용이 동일한 경우가 일반적이다.
위의 예제를 보면 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()) // 10
console.log(circle2.getDiameter()) // 20
Circle이라는 생성자 함수를 만들었고, new 라는 키워드로 객체를 만들어내어 circle1, circle2에 할당해 주었다.
문법이 아리송하겠지만, 객체 리터럴에 비해 같은 객체 선언을 두 번하지 않아도 된다는 장점만을 기억하도록 하자
함수 호출 방식------------------this가 가리키는 값(this 바인딩) |
일반 함수로서 호출 : 전역 객체 |
메서드로서 호출 : 메서드를 호출한 객체(마침표 앞의 객체) |
생성자 함수로서 호출 : 생성자 함수가 (미래에) 생성할 인스턴스 |
다음의 예제를 분석하면 다음과 같다.
function foo(){
console.log(this)
}
//일반적인 함수로서 호출
foo();
const obj = { foo } // es6 프로퍼티 축약 표현
// 메서드로서 호출
obj.foo() // obj
// 생성자 함수로서 호출
const inst = new foo() // inst
어떤 결과가 나오는 지는 각자 확인해보길 바란다.
this는 이후에 더 살펴보도록 하자
생성자 함수는 이름 그대로 객체(인스턴스)를 생성하는 함수이다. 하지만 자바와 같은 클래스 기반 객체지향 언어의 생서앚와는 다르게 그 형식이 정해져 있는 것이 아니라, 일반 함수와 동일한 방법으로 생성자 함수를 정의하고, new 연산자와 함께 호출하면 해당 함수는 생성자 함수로 동작한다.
만약 new 연산자와 함께 생성자 함수를 호출하지 않으면 생성자 함수가 아니라, 일반 함수로 동작한다.
function Circle(radius){
this.radius = radius
this.getDiameter = function(){
return 2 * this.radius
}
}
const circle3 = Circle(15) // 일반 함수 호출
console.log(circle3) // undefined
console.log(radius) // 15
console.log(getDiameter()) // 30
중요한 것은 new 연산자를 붙이지 않으면 생성자 함수가 작동되지 않는다는 것이다.
따라서 이는 일반 함수와 동일하기 때문에 this 바인딩이 전역 객체에 할당된다.
console.log(circle3)
가 undefined가 나오는 이유도 바로 이런 이유 때문이다. 생성자 함수에서 객체를 생성해서 반환해주는 것이 아니라, 전역 객체에 radius와 getDiameter 를 할당해버렸기 때문이다.
위에서 생성자 함수를 실컷 말했는데, 어떻게 되는 건지 설명을 안했다.
생성자 함수를 만드는 조건은 매우 간단하다.
그러나 코드를 잘보면, return해주는 코드가 없을 것이다.
여기서 헷갈릴 수 있지만 이는 암묵적인 처리를 통해 인스턴스를 생성하고 반환하는 것이다.
즉, 자바스크립트 엔진은 암묵적인 과정으로 인스턴스를 생성하고 인스턴스를 초기화한 후 암묵적으로 인스턴스를 반환한다.
생성자 함수에서는 암묵적으로 빈 객체가 생성된다. 이 빈 객체가 바로 아직 완성된 것은 아니지만 생성자 함수가 생성한 인스턴스이다. 그리고 암묵적으로 생성된 빈 객체, 즉, 인스턴스는 this에 바인딩된다. 생성자 함수 내부의 this가 생성자 함수가 생성할 인스턴스를 가리키는 이유가 바로 이것이다.
인스턴스 초기화, 생성자 함수에 기술되어 있는 코드가 한 줄씩 실행되어 this에 바인딩되어 있는 인스턴스를 초기화한다. 즉, this에 바인딩되어 있는 인스턴스에 프로퍼티나 메서드를 추가하고 생성자 함수가 인수로 전달받은 초기값을 인스턴스 프로퍼티에 할당하여 초기화하거나 고정값을 할당한다. 이처리는 개발자가 기술한다.
인스턴스 반환은 생성자 함수 내부의 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다.
그러나, 명시적으로 반환값을 지정하면 암묵적인 객체의 반환은 이루어지지 않는다.
function Circle(radius){
this.radius = radius
this.getDiameter = function(){
return 2 * this.radius
}
return {}
}
const circle3 = new Circle(15)
console.log(circle3) // {}
만약 Circle의 return 문을 지우면 Circle의 this에 바인딩된 객체가 생성될 것이다.
따라서, 명시적인 return 값을 주는 것은 생성자 함수를 해치는 요소가 되므로 사용하지 않아야 한다.
함수 선언문, 함수 표현식으로 정의한 함수는 일반적인 함수로서 호출할 수 있는 것은 물론, 생성자 함수로서 호출할 수 있다. 즉, new와 함께 객체를 생성할 수 있다는 것을 의미한다.
함수는 객체이므로, 일반 객체(ordinary object)와 동일하게 동작할 수 있다. 함수 객체는 일반 객체가 가지고 있는 내부 슬롯과 내무 메서드를 모두 가지고 있기 때문이다.
function foo() {
}
foo.prop = 10
foo.method = function(){
console.log(this.prop)
}
foo.method() // 10
함수는 객체이지만, 일반 객체와는 다르다. 일반 객체는 호출할 수 없지만 함수는 호출이 가능하기 때문이다.
따라서, 함수 객체는 일반 객체가 가지고 있는 내부 슬롯과 내부 메서드는 물론, 함수로서 동작하기 위해 함수 객체만을 위한 [[Environment]], [[FormalParameters]]
등의 내부 슬롯과 [[Call]] , [[Construct]]
같은 내부 메서드를 추가로 가지고 있다.
함수가 일반 함수로서 호출되면 함수 객체의 내부 메서드 [[Call]]
이 호출되고 new 연산자와 함께 생성자 함수로서 호출되면 내부 메서드 [[Construct]]
가 호출된다.
이를 통해 같은 function으로 만들어진 함수인데도 불구하고, 어떤 것은 생성자 함수의 역할을 어떤 것은 일반 함수의 역할을 할 수 있는 것이다.
function foo(){
}
foo() // 일반함수로서 [[Call]]을 호출
new foo() // 생성자 함수로서 [[Construct]]가 호출된다.
내부 메서드 [[Call]]
을 갖는 함수 객체를 callable이라 하며, 내부 메서드 [[Construct]]
를 갖는 함수 객체를 constructor, [[Construct]]
를 non-constructor 라고 부른다.
callable은 호출할 수 있는 객체, 즉 함수를 말하며 constructor는 생성자 함수로서 호출할 수 있는 함수, non-constructor는 객체를 생성자 함수로서 호출할 수 없는 함수를 의미한다.
위의 예시에서 생성자 함수도 일반 함수처럼 호출이 가능하다고 하였다. 이는 생성자 함수에도 [[Call]]
이 있기 때문에 callable이라는 것이다.
반면, 일반 함수는 [[Construct]]
가 없기 때문에 생성자가 아니다. 따라서 non-constructor이지만, [[Call]]
가능하므로 callable이다.
즉, 함수 객체는 callable이면서 constructor이거나, callable이면서 non-constructor이라는 것이다.
정리하자면 모든 함수 객체가 callable하지만, 모든 함수 객체가 constructor인 것은 아니다.
자바스크립트 엔진 어떻게 constructor와 non-constructor를 구분하는 살펴보자,
자바스크립트 엔진은 함수 정의를 평가하여 함수 객체를 생성할 때 함수 정의 방식에 따라 함수를 constructor와 non-constructor로 구분한다.
function foo(){} // 함수 선언문
const bar = function () {} // 함수 표현식
//프로퍼티 x는 메서드가 아닌, 일반 함수이다.
const baz = {
x : function () {}
}
//일반함수로 정의된 것들만이 constructor이다.
console.log(new foo()) // foo {}
console.log(new bar()) // bar {}
console.log(new baz.x()) // bar {}
// 화살표 함수
const arrow = () => {};
console.log(new arrow()) // TypeError: arrow is not a constructor
// 메서드 정의 : ES6의 메서드 축약 표현만 메서드로 인정한다.
const obj = {
x() {}
}
console.log(new obj.x()) // TypeError: obj.x is not a constructor
실제 코드 상의 예제는 다음과 같다.
함수 프로퍼티 값으로 사용하면 일반적으로 메서드로 통칭한다. 하지만 ECMAScript 사양에서 메서드란 ES6의 메서드 축약 표현만을 의미한다.
다시 말해 함수가 어디에 할당되어 있는 지에 따라 메서드인지를 판단하는 것이 아니라, 함수 정의 방식에 따라 constructor와 non-constructor를 구분한다.
따라서, 위 예제와 같이 일반 함수, 즉 함수 선언문과 함수 표현식으로 정의된 함수만이 constructor이고 es6의 화살표 함수와 메서드 축약 표현으로 정의된 함수는 non-constructor이다.
함수를 일반 함수로서 호출하면 함수 객체의 내부 메서드 [[Call]]
이 호출되고, new 연산자와 함께 생성자 함수로서 호출하면 내부 메서드 [[Construct]]
가 호출된다.
non-constructor인 함수 객체는 내부 메서드 [[Construct]]
를 갖지 않는다. 따라서 non-constructor인 함수 객체를 생성자 함수로서 호출하면 에러가 발생하는 것이다.
한가지 주의할 것은 생성자 함수로서 호출될 것을 기대하고 정의하지 않은 일반 함수(callable이면서 constructor)에 new 연산자를 붙여 호출하면 생성자 함수처럼 동작할 수 있다는 것이다.
여태까지 이야기 한 것과 마찬가지로 new 키워드가 함수 객체 앞에 붙으면 [[Construct]]
를 호출한다. 만약 non-constructor이면 에러가 발생하니 주의하도록 하자
그러나, 이는 컨벤션이기 때문에 강제성이 없다.
ES6에서는 new.target을 지원하여 이러한 문제를 해결하려했다.
new.target은 this와 유사하게 constructor인 모든 함수 내부에서 암묵적인 지역 변수와 같이 사용되며 메타 프로퍼티
라고 부른다. 참고로 IE에서는 new.target을 지원하지 않으므로 주의하도록 하자
함수 내부에서 new.target을 사용하면 new 연산자와 함께 생성자 함수로서 호출되었는지 확인할 수 있다.
new 연산자와 함께 생성자 함수로서 호출되면 함수 내부의 new.target은 함수 자신을 가리킨다. new 연산자 없이 일반 함수로서 호출된 내부의 new.target은 undefined이다.
이를 이용하여, 생성자 함수를 new 를 이용하여 생성하지 않고 일반 함수처럼 사용하는 문제를 해결할 수 있다.
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
해당 예제를 분석하면, 분명 맨 처음에는 Circle(5)
로 생성자 함수를 일반 함수로 call 했다. 때문에 Circle() 함수 객체 내부에서는 new.target이 undefined로 정의된다.
이를 이용하여 undefiend가 falsy이므로 if(!new.target)은 true가 되고, 내부적으로 자신을 new로 재귀호출하여 return 한다.
이 경우, new Circle()로 만들어진 객체가 return되며, new Circle에서는 new.target이후의 코드들이 실행되고 암묵적인 반환값으로 this로 바인딩된 객체가 반환된다.
참고로 IE에서는 new.target을 사용할 수 없으므로, 스코프 세이프 생성자 패턴을 사용한다.
function Circle(radius){
if(!(this instanceof Circle)){
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에 바인딩된다. 이 때 this와 circle은 프로토타입에 의해 연겨로되므로 this instanceof circle
시에 true가 된다.
그러나 new 연산자를 사용하지않으면 프로토타입에 의해 연결되지 않으므로 문제가 this instanceof Circle
이 false가 된다. 따라서 이를 이용하여 생성자 함수를 호출하는 것이다.
이에 대한 자세한 내용은 추후에 프로토타입에 대해서 배울 때 더 자세히 알아보자
참고로 대부분의 빌트인 생성자 함수(Object, String, Number, Boolean,Function, Array, Date, RegExp , Promise 등)는 new 연산자와 함께 호출되었는 지를 확인한 후 적절한 값을 반환한다.
예를 들어, Object와 Function 생성자 함수는 new 연산자 없이 호출해도 new 연산자와 함꼐 호출했을 때와 동일하게 동작한다.
반면, String, Number, Boolean 생성자 함수는 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) // [Function: anonymous]
f = Function('x', 'return x**x')
console.log(f) // [Function: anonymous]
const str= String(123)
console.log(str, typeof str) //123 string
const num = Number('123')
console.log(num, typeof num) // 123 number
const bool = Boolean('true')
console.log(bool, typeof bool) // true boolean
이와 같이 String , Number, Boolean은 new 연산자 없이 호출 시에는 값만을 반환하기 때문에 이를 이용해 타입 변환작업을 손쉽게 할 수 있다.