[JavaScript] 9. Constructor

100tick·2022년 12월 21일
0

JavaScript Deep Dive

목록 보기
9/16
post-thumbnail

지금까지는 object를 생성하기 위해 Object Literal{}, 그리고 필요에 따라 내부에 Key: Value 형태로 Property를 작성하는 형태였다.

이번에는 Constructor를 통한 object 생성 방식을 알아보도록 하겠다.

1. Constructor Function

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에 할당을 하는 것으로 보아, thisobject라는 것을 유추할 수 있다.

그런데 여기서 this라는 변수는 존재하지 않는다.

this란 무엇일까.

바로 앞으로 생성될 object를 뜻한다.

이렇게 명시적으로 존재하지 않는 identifierthis에 값을 할당하는 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 Functionundefined를 반환하며 아무런 object도 생성되지 않는다.
게다가 무슨 영문인지 최상위 객체인 windowname, age Property가 추가되었다.

이는 의도치 않게 windowProperth를 덮어쓰거나 메모리 낭비를 발생시키니 주의해야 한다.

이렇게 되는 이유는, this가 가리키는 대상이 함수 호출 방식에 따라 동적으로 결정되기 때문이다.

new로 호출한 함수는 Constructor Function으로 인식되어 this는 해당 Constructor Function에 의해 생성될 object를 가리키게 된다.
new 없이 호출한 일반 함수는 thisGlobal Object 즉, 브라우저에서는 window를 가리킨다.

object litreal 방식으로 object를 생성할 때와 마찬가지로, MethodConstructor 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 내에서의 thisConstructor Function과 마찬가지로 object를 가리킨다.
단, object가 생성된 후에 Method를 실행할 수 있을테니, 생성될 object가 아니라 호출한 object라고 보면 되겠다.

맥락상 의미는 조금 다르지만 결과적으로는 둘 다 자기 자신을 참조하는 셈이 되므로, this라는 특별한 Keywordself-referencing variable, 자기 참조 변수라고도 부른다.

코드를 봐서 알겠지만 Constructor Functionreturn이 존재하지 않아도 new로 호출될 때 암묵적으로 초기화된 object를 반환한다.

이제를 생성할 때, objectProperty들을 미리 Constructor Function 내부에 선언하고, new로 호출하면 일일이 object를 생성할 때마다 모든 Property들을 기재할 필요 없이, 호출 한 번으로


2. Process of Instantiation

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의 방식은 JSobject literal 생성 방식과 비슷한 모습을 하고 있고, Java, C++, C# 등의 메이저 언어들은 Constructor Function 생성 방식과 똑같이 new를 사용한다.

이렇게 object가 갖게 될 Property들을 미리 정의하고 object를 만드는 것을 Instantiation이라고 한다.

지금까지 object라고 부르던 것은 instance라는 용어로 더욱 자주 사용되니 기억해두자.(Go는 특이하게 공식 문서에서 Value라는 용어를 사용하지만, 결국 본질은 같다. instance라고 알아두면 충분할 것이다.)

그럼 이제 Instantiation가 실제로 처리되는 과정을 좀 더 자세히 알아보자.

1. instance 생성, 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;
}

2. Instance Initialization

this에 암묵적으로 {}가 바인딩 된 후, 그 안에 Property들을 인자로 받은 값으로 초기화해주는 과정이다.

function Person(name, age) {
    // this = {}
  	this.name = name; // <--
  	this.age = age;
}

3. return Instance

Constructor Functionreturn 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}

이렇게 쓸 일은 없을테니 별로 중요하지는 않을 것 같다.


3. Internal Methods [[Call]], [[Construct]]

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]]을 갖는 함수 objectcallable이라 하고, [[Construct]]를 갖는 함수 objectconstructor라 한다.
또한 [[Construct]]를 갖지 않는 함수 objectnon-constructor이라고 부른다.

모든 함수는 호출이 가능하므로 callable이다.
function으로 선언된 함수는 new와 함께 호출되어 object를 생성할 수 있으므로 constructor이다.
Method, Arrow Functionnew와 함께 호출될 수 없으므로 non-constructor이다.

정리하면 함수는 2가지 종류로 나뉜다.
1. callable + constructor(function)
2. callable + non-constructor(arrow function, method)

주의할 점은 여기서(ECMAScript Spec) non-constructor로 분류되는 MethodobjectProperty로 들어간 함수를 뜻하는 것이 아니라, 아래와 같이 Method 축약 표현으로 선언된 함수를 뜻한다는 것이다.

const obj = {
	method: () {}, // method 축약 표현
    
    fn: function() {}, // method
};

4. new operator

방금 봤던 내용이지만 한번 더 정리하겠다.
간단하게 아래 코드를 살펴보자.

// 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를 사용한다.


5. new.target

Constructor Functionnew 없이 호출되었을 경우를 검사하고, 내부적으로 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

0개의 댓글