2022-05-02
함수 생성자는 여러 프로그래밍 언어에서는 '클래스'와 동의어다.
어떤 경우 사람들은 참조 타입(Reference Types), 클래스, 데이터 타입, 아니면 간단하게 생성자라고 부른다.
클래스에 아직 익숙하지 않다면, 어떤 속성과 행동을 정의하도록 해주고, 그 속성과 행동을 통해 여러 객체가 생성될 수 있도록 해주는 생성자라고 이해하면 된다.
클래스는 객체로서 집을 짓기 위한 청사진이라는 비유를 많이 한다. 여러 집이 단 하나의 청사진으로부터 시작하는 것처럼, 여러 객체도 단 하나의 클래스로부터 생성될 수 있다.
function Person(name, position) {
this.name = name,
this.position = position
}
함수 이름은 의무는 아니지만 대문자로 시작하는 것이 naming convention으로 자리잡았다. 또 보통 함수는 괄호와 함께 호출하고 생성자 함수는 new
연산자를 사용한다.
const moonchess = new Person('Chess Moon', programmer)
const jinyoung = new Perseon('Jinyoung Park', 'student')
여기서 우리는 Person
이라는 객체를 두 개 생성하고 있다.
new
키워드와 함께 생성자 함수가 호출될 때, this
는 생성되고 있는 바로 그 객체를 참조한다.
메소드는 생성자 함수 안에서 속성에 함수를 할당하면서 정의된다.
function Person(name) {
this.name = name;
this.hi = function () {console.log(`hi, My name is ${this.name}`)}
}
const moonchess = new Person('Chess Moon');
moonchess.hi(); // Hi, My name is Chess Moon
여기서 hi
속성은 할당된 함수이다. Person
객체가 호출될 때 this
키워드는 새로 생성된 Person
객체에 응답할 것이다.
이런 방식으로 메소드가 정의된다 해도 이런 접근법은 약간 귀찮다.
Person
인스턴스가 생성될 때마다 그 객체의 hi
프로퍼티에 새로운 함수가 정의되고, 할당된다.
만일 우리가 5개의 Person 객체를 생성한다면, 각자 모두가 같은 일을 하는 자신만의 hi 메소드를 가지게 되는 것이다.
더 효율적인 방법은 hi 속성을 단 한 번만 정의하는 것이다. 그리고 각 Person 객체는 같은 함수를 참조하면 된다.
이를 위해서는 함수의 prototype
을 써볼 수 있다.
function Person(name) {
this.name = name;
}
Person.prototype.hi = function () {
console.log('hi my name is' + this.name)
}
const moonchess = new Person('Chess Moon')
const jinyoung = new Person('Jinyoung Park')
moonchess.hi();
jinyoung.hi();
자바스크립트의 모든 함수는 'prototype
'이라고 하는 속성이 있는데, 이는 거의 빈 객체를 포함한다 말할 수 있다.
Person
인스턴스가 생성될 때마다 이 객체는 Person.prototype
에 선언된 어떤 속성이든, 메소드든 상속 받게 된다.
위 예시를 아래처럼 적을 수도 있다.
function Person(name) {
this.name = name;
}
Person.prototype = {
constructor: Person,
hi() {
console.log('hi my name is' + this.name)
}
}
여러 번 person.prototype
에 새로운 메서드를 추가하는 것보다 그냥 Person.prototype
객체를 재정의하는 게 낫다.
앞서 prototype이 거의 빈 객체라고 언급했는데, 정확하게 말하자면, prototype
은 constructor
라는 속성을 가지고 있고, 이는 생성자 함수를 가리킨다.
우리가 완전히 새로운 객체로 prototype
을 덮어씌운다면, 이constructor
속성도 reset 해야 한다.
prototype
위의 그 어떤 것이든 모든 해당 생성자 인스턴스 객체 사이에 공유되기 때문에, 우리는 prototyp
e에 선언된 메소드랄지 생성자 객체 위에 저장된 속성만을 보곤 한다.
메소드는 공유된 행동양식으로 각 객체는 자신만의 유일한 메소드가 없다.
하지만, 각 객체는 자신만의 속성을 가져야 할 때도 있는 법이다.
prototype
이 아니라 객체 자체에 정의된 속성은 "own properties
"라고 불린다.
ES6 덕분에 우리는 위 생성자 함수를 class 문법으로 쓸 수 있게 되었다.
class Person {
constructor(name) {
this.name = name;
}
hi() {
console.log('..')
}
}
여기서 hi()
는 Person.prototype
에 저장된다.
만일 우리가 객체와 생성자의 prototype에 같은 이름의 속성을 선언하면 어떤 일이 벌어질까?
function Person(name) {
this.name = name;
this.walk = function () {
console.log('moon walking')
}
}
Person.prototype.walk = function () {
console.log('normal walking')
};
const mj = new Person('Michael Jackson');
mj.walk();
이 예제에서는, walk
이라는 메소드가 Person 인스턴스와 Person.prototype에 각각 선언되었다. 콘솔에는 뭐라고 찍힐까?
JavaScript는 우선 객체 자신의 "own property"부터 찾으려 한다. 만일 own property가 존재한다면, 해당 속성이 사용된다. 그렇지 않으면, 해당 객체를 만든 함수의 prototype
을 찾아보려 할 것이다.
따라서 위 예제에서는 mj
객체 자체에서 walk
가 발견되고, 콘솔에는 'moon walking'이 찍힐 것이다.
만일 Person
객체가 아래처럼 생겼다면, 객체 자체에 walk 메서드가 발견되지 않으니, 자바스크립트는 walk
메서드가 발견되는 Person.prototype
을 찾게 되고, 'normal walking'이 콘솔에 찍힐 것이다.
function Person(name) {
this.name = name;
}
만일 클래스 기반의 언어를 공부한 사람이라면, 상속이 어떻게 작동하는지 궁금할 것이다. Animal이라는 생성자가 있다고 해보자.
function Animal() {}
Animal.prototype.eat = function () {
console.log('eating')
};
이번에는 Cat 생성자가 있다고 해보자.
function Cat() {
Cat.prototype.meow = function () {
console.log('meowing');
}
};
Cat은 동물의 한 종류이니, Animal로부터 확장된 Cat을 갖고자 한다. 상속을 하기 위한 한 가지 방법은 아래와 같다.
function Animal() {}
Animal.prototype.eat = function () {
console.log('eating')
}
function Cat(){}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
Cat.prototype.meow = function () {
console.log('meowing');
}
예전에 클래스 기반의 언어를 사용한 적이 있다면, 분명 이 대목에서 이게 대체 뭔가 하고 생각할 것이다. 굉장히 투박해보이지 않나. 감사하게도 ES6/ES2015의 클래스가 이 과정을 훨씬 깔끔하게 만들어주었다.
class Animal {
eat() {
console.log('eating')
}
}
class Cat extend Animal () {
meow() {
console.log('meowing')
}
}
자바스크립트는 Stirng
, Number
, Boolean
, Array
, Function
, Object
, RegExp
, Date
를 포함해서 생성자처럼 사용 가능한 여러 빌트인 함수를 가지고 있다.
const str = new String('some string');
// OR
const str = 'some string'; // literal syntax
const age = new Number(25);
// OR
const age = 25; // literal syntax
const person = new Object();
// OR
const person = {}; // literal syntax
const x = new Boolean(false);
const x = false; // literal syntax
const add = new Function('a', 'b', 'return a+b');
const add = function (a, b) {
return a + b;
}; // literal syntax
네이티브 생성자를 통해 numbers, strings, objects, booleans, functions를 생성한다고 해도 언제나 리터럴 신택스가 더 간단하고 직관적이다.
네이티브 생성자 함수도 확장될 수 있다. 커스텀 String 메서드를 만든 후에, 미래에 브라우저가 그 메서드를 약간 다르게 implement했기 때문에 코드가 예상한 대로 작동하지 않으니 그다지 추천되는 방법은 아니다. 그럼에도 불구하고, prototype를 이해하기 위해서 배워볼 만한 가치는 있다.
String.prototype.dasherize = function () {
return this.replace(/\s/g, '-');
}
'hello world'.dasherize(); // hello-world