지금까지는 object
를 생성하기 위해 Object Literal인 {}
, 그리고 필요에 따라 내부에 Key: Value 형태로 Property를 작성하는 형태였다.
이번에는 Constructor를 통한 object
생성 방식을 알아보도록 하겠다.
const person = new Object();
끝이다.
이게 기존 방식이랑 뭐가 다를까.
const person = new Object();
const person2 = {};
person; // {}
person2; // {}
똑같다.
이렇게만 보면 하등 쓸모 없어 보이겠지만, Constructor를 통한 object
생성은 원하는 객체를 대량으로 찍어낼 때 빛을 발한다.
function Person(name, age) {
this.name = name;
this.age = age;
}
const person1 = new Person("a", 10);
const person2 = new Person("b", 20);
위 코드에서 this.name
에 인자로 받아온 name
을 저장했다.
.
연산자를 통해 Property에 할당을 하는 것으로 보아, this
는 object
라는 것을 유추할 수 있다.
그런데 여기서 this
라는 변수는 존재하지 않는다.
this
란 무엇일까.
바로 앞으로 생성될 object
를 뜻한다.
이렇게 명시적으로 존재하지 않는 identifier인 this
에 값을 할당하는 Person
과 같은 함수를 Constructor Function이라고 부른다.
이 Constructor Function이 실행될 때마다 name
, age
라는 Property를 가진 새로운 object
가 생성되는 것이다.
주의할 점은 함수를 Constructor Function으로서 호출할 때는 반드시 new
를 붙여줘야 한다는 것이다.
function Person(name, age) {
this.name = name;
this.age = age;
}
const person1 = new Person("a", 1);
const person2 = Person("a", 1);
person1; // Person {name: 'a', age: 1}
person2; // undefined
window.name; // 'a'
window.age; // 1
new
와 함께 호출된 Constructor Function은 새로운 object
를 반환한다.
this
에 할당한 name
, age
Property가 저장된 것도 확인할 수 있다.
그러나 new
없이 호출된 Constructor Function은 undefined
를 반환하며 아무런 object
도 생성되지 않는다.
게다가 무슨 영문인지 최상위 객체인 window
에 name
, age
Property가 추가되었다.
이는 의도치 않게 window
의 Properth를 덮어쓰거나 메모리 낭비를 발생시키니 주의해야 한다.
이렇게 되는 이유는, this
가 가리키는 대상이 함수 호출 방식에 따라 동적으로 결정되기 때문이다.
new
로 호출한 함수는 Constructor Function으로 인식되어 this
는 해당 Constructor Function에 의해 생성될 object
를 가리키게 된다.
new
없이 호출한 일반 함수는 this
가 Global Object 즉, 브라우저에서는 window
를 가리킨다.
object litreal 방식으로 object
를 생성할 때와 마찬가지로, Method도 Constructor Function 내부에 생성할 수 있다.
아래 코드처럼 만들면 된다.
function Person(name, age) {
this.name = name;
this.age = age;
this.getName = function() {
return this.name;
}
this.getAge = function() {
return this.age;
}
}
const person = new Person("a", 1);
person.getName(); // "a"
person.getAge(); // 1
매우 간단하다.
참고로 Method 내에서의 this
도 Constructor Function과 마찬가지로 object
를 가리킨다.
단, object
가 생성된 후에 Method를 실행할 수 있을테니, 생성될 object
가 아니라 호출한 object
라고 보면 되겠다.
맥락상 의미는 조금 다르지만 결과적으로는 둘 다 자기 자신을 참조하는 셈이 되므로, this
라는 특별한 Keyword는 self-referencing variable, 자기 참조 변수라고도 부른다.
코드를 봐서 알겠지만 Constructor Function에 return
이 존재하지 않아도 new
로 호출될 때 암묵적으로 초기화된 object
를 반환한다.
이제를 생성할 때, object
의 Property들을 미리 Constructor Function 내부에 선언하고, new
로 호출하면 일일이 object
를 생성할 때마다 모든 Property들을 기재할 필요 없이, 호출 한 번으로
Constructor Function을 통해 object
를 생성할 수 있다는 것을 깨달았다.
// go
type Person struct {
Name string
Age int
}
person1 := Person{
Name: "a",
Age: 1,
}
// dart
class Person {
String name;
int age;
Person(this.name, this.age); // constructor
}
var person1 = new Person("a", 1);
다른 프로그래밍 언어들도 이와 비슷하게 class
, struct
등에 미리 Property들을 선언하여 새로운 object
를 만든다.
Go의 방식은 JS의 object literal 생성 방식과 비슷한 모습을 하고 있고, Java, C++, C# 등의 메이저 언어들은 Constructor Function 생성 방식과 똑같이 new
를 사용한다.
이렇게 object
가 갖게 될 Property들을 미리 정의하고 object
를 만드는 것을 Instantiation이라고 한다.
지금까지 object
라고 부르던 것은 instance
라는 용어로 더욱 자주 사용되니 기억해두자.(Go는 특이하게 공식 문서에서 Value라는 용어를 사용하지만, 결국 본질은 같다. instance라고 알아두면 충분할 것이다.)
그럼 이제 Instantiation가 실제로 처리되는 과정을 좀 더 자세히 알아보자.
this
바인딩우선 암묵적으로 빈 객체, {}
가 생성된다.
그리고 this
는 바로 이 빈 객체를 가리키게 된다.
this
가 {}
를 가리키게 만드는 것을 바인딩이라고 한다.
용어가 조금 낯설지만, 우리가 앞서 살펴봤듯, 변수가 선언되면 운영체제로부터 사용 가능한 메모리 공간을 할당받고, 변수명은 할당 받은 그 메모리 공간의 주소를 나타내는 별칭이라는 것을 지금까지 반복적으로 주입했다.
결국 this
는 새로 만들어진 {}
를 가리키는 메모리 주소의 별칭이며, 이 빈 객체가 바로 완성될 instance가 될 것이다.
이 과정이 런타임 이전에 실행된다고 하는데, 이런 식으로 처리가 되는 것이 아닐까 생각한다.(정확하지 않음)
// 원래 코드
function Person(name, age) {
const this = {};
this.name = name;
this.age = age;
}
// 실제 동작과 좀 다르겠지만 런타임 이전에 이런 느낌으로 변경되지 않을까 생각
function Person(name, age) {
const this = {};
this.name = name;
this.age = age;
return this;
}
this
에 암묵적으로 {}
가 바인딩 된 후, 그 안에 Property들을 인자로 받은 값으로 초기화해주는 과정이다.
function Person(name, age) {
// this = {}
this.name = name; // <--
this.age = age;
}
Constructor Function에 return
statement는 존재하지 않지만, new
와 함께 호출할 경우 return
이 없어도 instance를 생성 및 반환 한다고 하였다.
이렇게 생성된 instance를 반환하는 마지막 과정이다.
그 후는 반환된 instance를 새로운 변수에 할당하던가 해서 자유롭게 사용하면 된다.
let person = new Person("a", 10);
주의할 점은, Constructor Function으로 사용할 함수 내부에서 다른 object
를 명시적으로 return
하는 경우, 명시된 object
가 반환된다.
function Person(name, age) {
this.name = name;
this.age = age;
return {};
}
let person = new Person("a", 1);
person; // {}
JS가 워낙 예측할 수 없는 언어이기 때문에 이것저것 실험한 결과 알아낸 사실이 있는데, object
가 아닌 number
, boolean
, string
등의 Primitive Type인 값을 반환하면 명시된 값이라도 무시된다는 것을 발견했다.(이후 책을 보니 써있는 내용이었다)
function Person(name, age) {
this.name = name;
this.age = age;
return 999; // 999, "a", true 등은 명시적으로 반환해도 모두 무시됨
}
let person = new Person("a", 1);
person; // Person {name: 'a', age: 1}
이렇게 쓸 일은 없을테니 별로 중요하지는 않을 것 같다.
Function Declaration, Function Expression으로 선언된 함수는 모두 new
를 붙여 Constructor Function으로서 호출하여 객체를 생성할 수 있다.
그런데 사실 함수도 object
이기 때문에 일반적인 object
와 똑같이 동작할 수 있다.
함수도 Property, Method를 가질 수 있다는 뜻이다.
function fn() {}
fn.a = 1;
fn.method = function() { console.log("method"); };
fn.a; // 1
fn.method(); // "method"
한가지 차이점은 일반 객체는 호출할 수 없지만, 함수는 호출할 수 있다는 것이다.
그 이유는 함수 object
만 내부적으로 [[Call]]
, [[Construct]]
등의 내부 Method를 가지고 있기 때문이다.
그래서 함수를 호출할 때 내부적으로는 [[Call]]
가 호출되고, new
와 함께 호출될 때, [[Construct]]
가 호출된다.
[[Call]]
을 갖는 함수 object
를 callable이라 하고, [[Construct]]
를 갖는 함수 object
를 constructor라 한다.
또한 [[Construct]]
를 갖지 않는 함수 object
를 non-constructor이라고 부른다.
모든 함수는 호출이 가능하므로 callable이다.
function
으로 선언된 함수는 new
와 함께 호출되어 object
를 생성할 수 있으므로 constructor이다.
Method, Arrow Function은 new
와 함께 호출될 수 없으므로 non-constructor이다.
정리하면 함수는 2가지 종류로 나뉜다.
1. callable + constructor(function)
2. callable + non-constructor(arrow function, method)
주의할 점은 여기서(ECMAScript Spec) non-constructor로 분류되는 Method는 object
의 Property로 들어간 함수를 뜻하는 것이 아니라, 아래와 같이 Method 축약 표현으로 선언된 함수를 뜻한다는 것이다.
const obj = {
method: () {}, // method 축약 표현
fn: function() {}, // method
};
방금 봤던 내용이지만 한번 더 정리하겠다.
간단하게 아래 코드를 살펴보자.
// arrow fn
const arrowFn = () => {};
new arrowFn(); // Uncaught TypeError: arrowFn is not a constructor
// method
const obj = {
method() {},
};
new obj.method(); // Uncaught TypeError: obj.method is not a constructor
위 2가지 경우가 아니라면 new
로 호출이 가능하다.
new
와 함께 함수를 호출할 경우, 내부적으로 [[Construct]]
가 호출되어 object
가 생성될 것이고, 그냥 호출하면 [[Call]]
이 호출되어 함수가 실행될 것이다.
Constructor Function은 일반 함수와 쉽게 구분하기 위해 일반적으로 PascalCase를 사용하며, 일반 함수는 camelCase를 사용한다.
Constructor Function이 new
없이 호출되었을 경우를 검사하고, 내부적으로 new
를 붙여서 재호출 해주는 로직을 구성하여 실수를 방지할 수 있다.
function Person(name, age) {
if (!new.target) {
return new Person(name, age);
}
}
this
와 유사하게 Constructor Function인 함수 내부에서 암묵적으로 값이 할당되어 사용할 수 있다.
new
와 함께 호출되면 함수 자신을 가리키고, 일반 함수로 호출되면 undefined
다.
undefined
면 일반 함수에서 호출했다는 뜻이니 확인 후 재호출 한다는 의미인 것이다.
Object
, String
, Number
, Boolean
, Function
, Array
, Date
, RegExp
, Promise
등의 대부분의 Built-in Constructor Function들은 위와 같은 로직을 통해 new
없이 호출되면 내부적으로 재호출 하도록 짜여있기 때문에 new
없이도 잘 동작하는 것이다.
const obj = Object();
const num = Number();
obj; // {}
num; // 0