WeniVooks

검색

JavaScript 에센셜

생성자

1. 객체지향 프로그래밍이란 무엇일까?

객체지향은 프로그래밍 방법론중에 하나로, 프로그램을 작성할 때 객체들을 만들어 서로 소통하도록하는 방법이라고 할 수 있습니다.

그렇다면 여기서 말하는 객체란 무엇일까요? 우리가 앞서서 배운 자바스크립트 객체가 키,값 쌍으로 이루어진 데이터의 묶음이라면, 객체 지향의 객체는 우리가 표현하고자 하는 구체적인 사물을 추상적으로 표현한것 이라고 볼 수 있습니다.

이 그림은 무엇을 표현한 것일까요?

  • 피카소의 추상화 과정

즉, 추상화란 필요한 최소한의 정보로 대상을 표현하는 것을 의미합니다.

만약 '나'를 추상화 한다면, 즉 객체로 표현한다면 다음과 같이 나타낼 수 있습니다.

const me = {
  name: '라이캣',
  address: '제주도 제주시 인다 1길',
  phoneNum: '010-0000-0000',
  canWalk: function () {
    console.log('라이캣이 걷는다.');
  },
};
const me = {
  name: '라이캣',
  address: '제주도 제주시 인다 1길',
  phoneNum: '010-0000-0000',
  canWalk: function () {
    console.log('라이캣이 걷는다.');
  },
};

객체는 상태와 행동을 가집니다. 상태는 프로퍼티, 행동은 메서드로 표현됩니다. 'me' 객체에 새로운 능력을 부여해보겠습니다.

const me = {
  name: '라이캣',
  address: '제주도 제주시 인다 1길',
  phoneNum: '010-0000-0000',
  canWalk: function () {
    console.log('라이캣이 걷는다.');
  },
  teaching: function (student) {
    student.levelUp();
  },
};
const me = {
  name: '라이캣',
  address: '제주도 제주시 인다 1길',
  phoneNum: '010-0000-0000',
  canWalk: function () {
    console.log('라이캣이 걷는다.');
  },
  teaching: function (student) {
    student.levelUp();
  },
};

teaching 에 사용할 student 객체를 만들어봅니다.

const lion = {
  level: 1,
  levelUp: function () {
    this.level++;
  },
};
const lion = {
  level: 1,
  levelUp: function () {
    this.level++;
  },
};

라이캣의 능력을 발휘해 보겠습니다!

me.teaching(lion);
me.teaching(lion);

이처럼 객체와 객체가 서로 메소드를 통해 상호작용하게 하는것이 바로 객체지향 프로그래밍이라고 할 수 있습니다.

실습
여러분 자신을 추상화 해봅시다. 그리고 상호작용 하고 싶은 대상을 만들어보고 서로 상호 작용 할 수 있는 메소드를 만들어 봅시다.

자 이제 여러분들은 객체를 통한 추상화를 배워봤습니다. 하지만 문제가 있습니다. 우리가 만들어낸 객체는 한번 생성하고 나면 끝입니다. 좀 더 효율적인 방법으로 객체를 만들어 보겠습니다.

1.1. 생성자 (constructor)

생성자란 객체를 만들 때 사용하는 함수로, new 연산자와 함께 사용됩니다. 생성자 함수는 객체를 만들기 위한 틀로, 객체를 만들기 위한 프로퍼티와 메서드를 정의합니다.

우리는 이미 사용해본 생성자가 있습니다.

let myArr = new Array(1, 2, 3);
let myArr = new Array(1, 2, 3);

이처럼 자바스크립트에서 제공하는 Array, Object, String 등의 생성자 함수를 내장 생성자라고 합니다.

1.2. 우리는 왜 생성자를 사용할까요?

생성자의 장점은 생성자를 통해 생성된 객체는 같은 프로퍼티메서드를 공유할 수 있다는 것입니다.

다음과 같이 생성자를 통해 만들어진 객체는 같은 메서드를 공유합니다. myArr와 myArr2는 같은 메서드를 공유하고 있습니다.

length, forEach 등은 Array에 정의된 메서드입니다. 따라서 생성자 함수 Array로 만들어진 객체는 Array의 메서드를 공유합니다.

let myArr = new Array(1, 2, 3);
let myArr2 = new Array(4, 5, 6);
 
myArr2.length;
myArr.length;
 
myArr.forEach((item) => {
  console.log(item);
});
 
myArr2.forEach((item) => {
  console.log(item);
});
let myArr = new Array(1, 2, 3);
let myArr2 = new Array(4, 5, 6);
 
myArr2.length;
myArr.length;
 
myArr.forEach((item) => {
  console.log(item);
});
 
myArr2.forEach((item) => {
  console.log(item);
});

생성자의 장점. 조금 이해가 가시나요? 자 그럼 우리만의 생성자를 만들어 볼 수도 있습니다.

1.3. 커스텀 생성자

생성자는 함수이기 때문에 기본적으로 함수가 필요합니다. 생성자 함수는 암묵적으로 대문자로 시작하는 이름을 가지는 것으로 약속되어 있습니다.

function Factory() {}
function Factory() {}

그리고 new 키워드를 통해 객체를 생성합니다.

function Factory() {}
let robot1 = new Factory();
function Factory() {}
let robot1 = new Factory();

Factory 생성자 함수는 따로 return 값을 가지지 않지만 new 키워드와 함께 실행되면 자동적으로 객체를 생성하고 반환합니다. 이렇게 반환되어 만들어진 객체를 인스턴스(instance) 라고 합니다.

따라서 instanceof 연산자를 이용해서 생성자 함수와 객체의 관계를 확인할 수 있습니다.

console.log(robot1 instanceof Factory); // true
 
const obj = {};
console.log(obj instanceof Factory); // false
console.log(robot1 instanceof Factory); // true
 
const obj = {};
console.log(obj instanceof Factory); // false

자 그러면 이제 우리만의 프로퍼티와 메서드를 가진 로봇 객체를 만들어 봅시다.

function NewFactory(name) {
  this.name = name;
  this.sayYourName = function () {
    console.log(`삐리비리. 제 이름은 ${this.name}입니다. 주인님.`);
  };
}
function NewFactory(name) {
  this.name = name;
  this.sayYourName = function () {
    console.log(`삐리비리. 제 이름은 ${this.name}입니다. 주인님.`);
  };
}

원래 함수 안에서의 this는 함수를 호출한 객체를 참조합니다. 하지만 생성자 함수 앞에 new 연산자가 사용되면 함수안의 this는 생성자가 만들어낸 객체 즉, 인스턴스를 참조합니다.

생성자 함수를 통해 객체를 생성할 때는 new 키워드를 사용합니다. robot1은 NewFactory에서 정의된 프로퍼티와 메서드를 가지고 있습니다.

let robot1 = new NewFactory('브랜든');
console.log(robot1.name);
robot1.sayYourName();
let robot1 = new NewFactory('브랜든');
console.log(robot1.name);
robot1.sayYourName();

실습
음식 이름의 배열을 전달하면 배열 안에서 랜덤하게 메뉴를 뽑아내는 로봇 객체의 생성자 함수를 만들어보세요.

이렇듯 우리가 필요에 따라서 배열(Array), 객체(Object)를 사용하는 것 처럼, 우리가 필요할 때 사용할 수 있는 우리만의 객체를 만들어서 사용할 수 있도록 도와주는 것이 바로 생성자입니다.

2. 프로토타입 (prototype)

생성자 함수를 이용하여 손쉽게 객체를 만들 수 있지만, 객체의 메서드는 인스턴스를 만들 때마다 새로운 함수를 생성하게 됩니다. 100개의 객체를 생성할 때마다 100개의 함수를 새롭게 만들게 됩니다. 이러한 자원의 낭비를 해결하기 위해 등장한 것이 바로 프로토타입입니다.

this.sayYourName = function () {
  console.log(`삐리비리. 제 이름은 ${this.name}입니다. 주인님.`);
};
this.sayYourName = function () {
  console.log(`삐리비리. 제 이름은 ${this.name}입니다. 주인님.`);
};

생성자 함수에 메서드로 작성한 sayYourName을 프로토타입으로 표현하면 다음과 같습니다. 선언한 생성자 함수의 prototype 프로퍼티에 메서드를 추가합니다.

function NewFactory2(name) {
  this.name = name;
}
 
NewFactory2.prototype.sayYourName = function () {
  console.log(`삐리비리. 제 이름은 ${this.name}입니다. 주인님.`);
};
function NewFactory2(name) {
  this.name = name;
}
 
NewFactory2.prototype.sayYourName = function () {
  console.log(`삐리비리. 제 이름은 ${this.name}입니다. 주인님.`);
};
2.1. 프로토타입이란 무엇인가?

prototype은 특정 객체에 대한 참조입니다. 즉 어떠한 공간을 가리키고 있습니다.

생성자 함수가 인스턴스를 생성하게 되면 그 안에는 숨겨진 프로퍼티인 [[Prototype]] 이 존재하게 됩니다. 코드상에서는 __proto__로 표현됩니다. __proto__ 프로퍼티는 자신을 만든 생성자 함수의 prototype을 참조하는 역할을 합니다. 즉, new 키워드를 통해 객체를 생성하면 생성자 함수의 prototype과 인스턴스의 __proto__ 가 연결됩니다.

function Test() {}
 
const obj = new Test();
 
obj.__proto__ === Test.prototype;
function Test() {}
 
const obj = new Test();
 
obj.__proto__ === Test.prototype;

잠시만요 … prototype__proto__ 는 다른건가요???
네 맞습니다. prototype은 오직 function 안에 존재하는 참조값입니다. __proto__ 는 객체 안에 존재하는 숨겨진 프로퍼티로, 인스턴스는 이 __proto__를 통해 생성자 함수의 prototype에 접근하여 필요한 여러가지 값과 메서드를 사용할 수 있습니다.

function Test() {}
 
const obj = new Test();
 
console.log(obj.prototype); // undefined
console.log(obj.__proto__ === Test.prototype); // true
function Test() {}
 
const obj = new Test();
 
console.log(obj.prototype); // undefined
console.log(obj.__proto__ === Test.prototype); // true

이렇듯 프로토타입은 모든 인스턴스가 하나의 메서드를 공유 하도록 만들어 자원을 더 효율적으로 사용하도록 도와줍니다. 생성된 객체끼리 메서드를 비교하여 같은 곳을 참조하는지 살펴보는것도 이해하는데 도움이 됩니다.

실습

  1. 음식 로봇 객체의 메서드를 프로토타입으로 분리해보세요. 그리고 여러 개의 객체를 생성하여 메서드가 서로 동일한 주소를 참조하는지 확인해보세요.

  2. 객체지향 개념 파트에서 만들었던 ‘나’ 와 ‘대상’ 객체를 생성자를 통해서 만들어 볼 수 있도록 코드를 수정해봅시다.

2.2. 객체의 상속
2.2.1. 프로토타입 체이닝

CSS에서 익숙하게 다루었던 상속이라는 개념은 자바스크립트에서도 존재합니다. 자바스크립트의 상속은 기본적으로 prototype을 통해 일어납니다.

const obj = { name: 'test', }; console.log(obj.hasOwnProperty('name')); const arr = [1, 2, 3]; console.log(arr.hasOwnProperty('name'));

배열 arr의 __proto__Array.prototype을 참조하고, Array.prototype의 __proto__는 Object.prototype을 참조하고 있기 때문에 Array.prototype에 존재하지 않는 Object.prototype의 프로퍼티와 메서드를 사용할 수 있습니다. 이처럼 자기 자신에게 존재하지 않는 프로퍼티나 메서드를 프로토타입 체인을 통해 추적하는 과정을 프로토타입 체이닝이라고 합니다. 이 프로토타입 체이닝을 이용해서 배열 arr는 Object.prototype의 프로퍼티와 메서드, Array.prototype의 프로퍼티와 메서드를 모두 사용할 수 있습니다.

자바스크립트의 타입들은 기본적으로 Object 타입을 상속받고 있습니다.

console.log(Array.prototype.__proto__ === Object.prototype);
console.log(Number.prototype.__proto__ === Object.prototype);
console.log(String.prototype.__proto__ === Object.prototype);
console.log(Math.__proto__ === Object.prototype);
console.log(Array.prototype.__proto__ === Object.prototype);
console.log(Number.prototype.__proto__ === Object.prototype);
console.log(String.prototype.__proto__ === Object.prototype);
console.log(Math.__proto__ === Object.prototype);
2.2.2. 프로토타입을 이용한 상속

이 프로토타입 체이닝을 이용하여 객체의 상속을 구현할 수 있습니다. 두 개의 생성자 함수를 사용하여 상속을 구현해보겠습니다. 먼저 부모의 역할을 할 생성자 함수를 만들고, 프로토타입 프로퍼티로 메서드를 추가합니다.

function Parent() {
  this.name = '라이캣';
}
Parent.prototype.rename = function (name) {
  this.name = name;
};
Parent.prototype.sayName = function () {
  console.log(this.name);
};
function Parent() {
  this.name = '라이캣';
}
Parent.prototype.rename = function (name) {
  this.name = name;
};
Parent.prototype.sayName = function () {
  console.log(this.name);
};

자식 역할의 생성자 함수를 만듭니다. 이 때, call 함수를 이용하여 Parent 내부의 this가 Child 인스턴스의 this를 가리키게 합니다. Parent 함수 내에서 정의된 프로퍼티가 Child를 통해 생성된 인스턴스에 추가됩니다. 이를 통해 프로퍼티를 상속받을 수 있습니다.

function Child() {
  Parent.call(this);
}
function Child() {
  Parent.call(this);
}

프로토타입 체이닝을 생성하여 부모의 프로토타입 메서드를 상속받아 사용할 수 있습니다. Object.create 함수를 사용하여 부모의 프로토타입을 자식의 프로토타입으로 지정합니다. Child.prototype은 Parent.prototype을 참조하게 되고, Parent.prototype의 프로퍼티와 메서드를 사용할 수 있게 됩니다.

Child.prototype = Object.create(Parent.prototype); // 지정된 프로토타입 객체를 갖는 새 객체를 만듭니다.
 
Child.prototype.canWalk = function () {
  console.log('now i can walk!!');
};
Child.prototype = Object.create(Parent.prototype); // 지정된 프로토타입 객체를 갖는 새 객체를 만듭니다.
 
Child.prototype.canWalk = function () {
  console.log('now i can walk!!');
};

위의 두 과정을 통해 Child 객체는 Parent 객체의 모든 것을 상속받게 됩니다. 이처럼 프로토타입을 이용해 상속을 구현할 수 있고, 메서드를 공유하여 메모리 공간을 절약할 수 있습니다.

이러한 프로토타입 기반의 상속 방식에는 몇 가지 단점이 있습니다.

  1. 구문의 분산
    객체 안에 존재하던 메서드가 prototype 키워드와 함께 객체의 외부로 빠져나와 가독성이 떨어집니다. 중괄호 안에 프로퍼티와 메서드가 모여있던 객체의 모습과 거리가 멀어졌습니다. 생성자 함수, prototype 객체, prototype 메서드가 각각 분산되어 있어 객체의 전체 구조를 파악하기 어려울 수 있습니다.

  2. 보편적인 객체지향 코드
    자바스크립트의 객체지향 코드는 다른 언어와 차이가 있습니다. prototype 키워드는 자바스크립트에서만 사용되는 특별한 키워드입니다. 이는 자바스크립트가 무엇인지 잘 모르는 다른 언어의 프로그래머들에게 혼란을 줄 수 있습니다. 또한 prototype은 보통 함수에서는 의미가 없으며, 오직 생성자 함수에서만 의미를 가집니다.

이러한 단점을 보완하기 위해서 ES6에는 class 문법이 도입되었습니다. 다음 장에서는 class 문법을 통해 객체지향 프로그래밍을 어떻게 할 수 있는지 알아보겠습니다.

11장 객체지향 프로그래밍11.2 클래스