Javascript의 프로토타입

프로토타입(Protytype) 이란?

자바스크립트의 모든 객체는 프로토타입이라고 불리는 또 다른 객체를 내부적으로 참조할 수 있다. 그리고 객체는 프로토타입의 프로퍼티들은 자신의 프로퍼티로 가져온다.
자바스크립트는 자바, C++, C#에서 제공하는 것과 같은 실제 클래스(true class)를 지원하지 않는 대신 모조 클래스(pseudoclass)를 정의할 수 있다. 결국 Prototype은 Class 개념이 없는 Javascript에서 객체를 확장하고, 객체 지향적인 프로그래밍을 할 수 있게 도와주는 일종의 모조 class 개념이다.

1
모든 Javascript 객체는 prototype을 가지고 있다. 또한 그 prototype은 object이다. 모든 자바스크립트 Object는 그들의 프로토타입으로부터 properties와 methods를 상속받는다.

객체 지향 프로그래밍

객체지향 언어(Object-oriented) 언어는 일반적으로 클래스를 통해 프로퍼티와 메서드를 가지는 객체를 여러개 만든다는 특징이 있다. 그러나, javascript는 클래스 개념이 없으며, 다른 클래스 기반 언어와는 다르다. ECMA script 에서는 객체를 프로퍼티의 순서 없는 컬렉션이며, 각 프로퍼티는 원시값이나 객체, 함수를 포함한다(특별한 순서가 없는 값의 배열) 라고 명시한다. 최종적으로 객체 프로퍼티의 모든 값은 원시값을 가진다.

1
2
3
4
5
var person = {
name: "김철수",
age: 20,
gender: "man"
}
  • person이라는 객체는 name, age, gender 라는 프로퍼티로 이뤄져 있다.
  • 프로퍼티에는 데이터 프로퍼티와 접근자 프로퍼티 두 가지 타입이 있니다.

데이터 프로퍼티

  • 데이터 값에 대한 단 하나의 위치를 포함하여 이 위치에서 값을 읽고 쓸수 있다.
  • 4가지 속성은 아래와 같다

    1. Configurable - 해당 프로퍼티가 delete를 통한 삭제, 속성 변경, 접근자 프로퍼티로 변환할 수 있음을 나타냄 (default: true) => 실험결과 false
    2. Enumerable - for-in 루프에서 해당 프로퍼티를 반환할 수 있음을 나타냄 (default: true)
    3. Writable - 프로퍼티 값을 바꿀 수 있음을 나타냄 (default: true)
    4. Value - 프로퍼티의 실제 데이터를 포함, 프로퍼티의 값을 읽는 위치이며 새로운 값을 쓰는 위치 (default: undefined)
  • 객체에 프로퍼티를 명시적으로 추가할 때 Configurable, Enumerable, Writable의 속성은 true, Value에는 할당된 값이 지정된다.

  • 위의 예제에서 name이라는 프로퍼티의 Value는 ‘김철수’로 저장된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Writable
var person = {};
Object.defineProperty(person, "name", {
writable: false, //해당 값을 바꿀 수 없음을 의미
value: "김철수"
})
console.log(person); //result: 김철수
person.name = "김영희";
console.log(person); //result: 김철수

//Configurable
var person = {};
Object.defineProperty(person, "name", {
configurable: true, //false면 삭제 불가능
value: "김철수"
});
console.log(person); //result: {name: "김철수"}
delete person.name;
console.log(person); //result: {}

접근자 프로퍼티

  • getter와 setter 함수로 구성

    1. Configurable - 해당 프로퍼티가 delete를 통한 삭제, 속성 변경, 접근자 프로퍼티로 변환할 수 있음을 나타냄 (default: true) => 실험결과 false
    2. Enumerable - for-in 루프에서 해당 프로퍼티를 반환할 수 있음을 나타냄 (default: true)
    3. get - 프로퍼티를 읽을 때 호출하는 함수
    4. set - 프로퍼티를 바꿀 때 사용하는 함수
  • getter 함수는 읽으려 할 때 발생하며, setter 함수는 바꾸려는 시도가 있을 경우 실행

  • getter 함수만 지정하는 경우, 해당 프로퍼티는 읽기 전용이 되며 바꾸려는 시도는 무시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var person={
name: "김철수",
age: 0
}
Object.defineProperty(person,"getName",{
get: function(){
return this.name;
},
set: function(value){
if(value.length>0){
this.newName = value;
}
}
});

//1. 바꾸려고 시도할 경우, setter 함수가 실행된다.
person.getName = "김영희";
console.log(person.newName) //result: '김영희'

//2. 읽을 때는, getter 함수가 실행된다. ( 위에서 값을 넣어줬음에도 get함수가 있어서 변경이 안됨 )
console.log(person.getName); //result: 김철수( 만일 getter 함수가 없을 경우는 undefined가 나옴)

객체 생성하기

  • 객체는 복합 타입(composite datatype) 여러 값들의 결합체로, 각자의 이름을 통해 원하는 값 저정/읽기가 가능
  • 객체 리터럴 - 객체를 생성하는 가장 쉬운 방법이다.
1
2
3
4
5
6
var person = {
name: "김철수",
age: 20,
"gender": "man",
"is Korean?": true
}
  • 객체 리터럴로 생성자로 생성하게 될시, 같은 인터페이스를 가진 객체를 여러개 만들 때는 중복된 코드를 많이 써야 한다는 단점이 존재 => `팩터리 패턴

팩터리 패턴

  • 팩터리 패턴이란 특정 객체를 생성하는 과정을 추성화 한 것으로 소프트웨어 공학에서는 잘 알려진 디자인 패턴으로 Spring 과 같은 프레임워크에서 많이 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createObject(name, age, gender){
var obj = new Object();
obj.name = name;
obj.age = age;
obj.gender = gender;
obj.getName = function(){
console.log(this.name)
};
return obj;
}
var person1 = createObject("김철수", 20, "남자");
var person1 = createObject("김영희", 19, "여자");

console.log(person1); //result: {name: "김철수", age: 20, gender: "남자", getName: ƒ}
console.log(person2); //result: {name: "김영희", age: 19, gender: "여자", getName: ƒ}
  • 단점으로는 생성한 객체가 어떤 타입인지 알 수 없다는 문제가 있다.

생성자 패턴

  • 생성자 패턴이란 특정한 타입의 객체를 만드는 데 사용한다.
    • 명시적으로 객체를 생성하지 않음.
    • 프로퍼티와 메서드는 this 객체에 직접 할당
    • return 문이 없음.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person(name, age, gender){
//2. 생성자의 this 값을 할당
//3. 생성자 내부 코드를 실행하며, 객체에 프로퍼티를 추가
this.name = name;
this.age = age;
this.gender = gender;
this.getName = function(){
console.log(this.name);
return this.name;
}
}
//1. 객체를 생성
var person1 = new Person("김철수", 20, "남자");
var person2 = new Person("김영희", 19, "여자");
//4. 새 객체를 반환

console.log(person1); //result: {name: "김철수", age: 20, gender: "남자", getName: ƒ}
console.log(person2); //result: {name: "김영희", age: 19, gender: "여자", getName: ƒ}

하지만 이러한 생성자 패턴 역시 인스턴스마다 메서드를 생성하게 되어 성능적인 부분에서 좋지 않다. 예를 들어 아래의 예제를 보면 조금 더 이해하기가 쉽다.

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name, age, gender){
this.name = name;
this.age = age;
this.gender = gender;
this.getName = function(){
//Ecma script에서 함수는 객체이므로 함수를 정의할 때 마다 새로운 객체 인스턴스가 생성된다.
console.log(this.name);
}
}
var person1 = new Person("김철수", 20, "남자");
var person2 = new Person("김영희", 19, "여자");
console.log( person1.getName == person2.getName() ); //result: false

프로토 타입

모든 함수는 prototype 프로퍼티를 가지며, 이 프로퍼티는 해당 참조 타입의 인스턴스 가져야 할 프로퍼티와 메서드를 담고 있는 객체이다. prototype의 property와 method는 객체 인스턴스 전체에서 공유되어 prototype의 property를 상속받은 객체들의 상당한 메모리를 줄일 수 있다. 또한 prototype에 새로운 property가 새로 추가되면 이미 생성되었던 객체일지라도 추가된 property를 그대로 상속받는다. 이 점은 상황에 따라 장점일 수도, 단점일 수 도 있다.

1
2
3
4
5
6
7
8
9
function Person(){};
Person.prototype.name = "김철수";
Person.prototype.age = 20;
Person.prototype.getThis = function(){
console.log(this.name);
}
var person1 = new Person();
var person2 = new Person();
console.log(person1.getThis == person2.getThis); //result: true

위의 생성자 패턴과는 다르게 ‘getThis’ 라는 함수가 같다고 나온다. 이는 프로퍼티와 메서드를 모든 인스턴스에서 공유한다는 것을 의미한다.

p.267

1
var a = new Date();

예를 들어서, 위와 같이 a 객체를 Date() 라는 생성자 함수를 이용하여 만들 경우, new 연산자는 객체a 의 프로토 타입을 설정하게 되는데, 객체 a 의 프로토 타입은 자신을 만들어낸 생성자 함수 Date() 의 프로토타입 프로퍼티값을 자신의 프로토타입으로 설정한다.
(모든 함수에는 prototype이라는 프로퍼티가 있는데, 이 것은 함수가 정의될 때부터 자동적으로 생성되고 초기화된다.)

1
2
3
4
5
6
7
8
9
10
11
function func01(){}
func01.prototype.temp = true;

function func02(){}
func02.prototype = new func01();

function func03(){}
func03.prototype = new func02();

var test = new func03;
console.log(test.temp); //true

프로퍼티의 상속에 대해 조금 더 살펴보면, 위의 소스를 기준으로 “test.temp” 를 console로 찍어보면 “true” 값이 나온다. 그 이유를 분석해보면,

  1. func01의 프로토타입에 temp라는 프로퍼티의 값을 true로 설정하였다.( 1-2번째줄 )
  2. func02의 프로토타입은 생성자 함수 func01로 만들어 func01의 프로퍼티를 그대로 상속한다. (4-5번째줄)
  3. func03 역시 프로토타입을 생성자 함수 func02로 만들어 func02의 프로퍼티를 그대로 상속한다. (7-8번째줄)
  4. 변수 test는 생성자 함수 func03으로 만들어 func03의 프로퍼티를 그대로 상속한다.(10-11번째줄)

결국 test는 생성자 func03의 프로퍼티를, func03은 func02의 프로퍼티를, func02는 func01의 프로퍼티를 상속받는다. 그래서 console에 test.temp를 찍으면 true 값이 나오게 된다.

프로토 타입 사용법

1
2
3
4
5
6
7
8
9
function func(){
this.test1 = "test1";
this.test2 = "test2";
}

var test1_1 = new func();

console.log( test1_1.test1); //test1
console.log( test1_1.test2); //test2

위의 소스를 보면 func() 생성자를 사용하여 만드는 객체 test1_1에는 test1과 test2라는 프로퍼티가 있다. 이 말은 func() 생성자를 사용하는 모든 객체에는 2개의 프로퍼티가 존재한다는 것이다.

현재 이커머스회사에서 frontend 개발자로 업무를 진행하고 있는 Martin 입니다. 글을 읽으시고 궁금한 점은 댓글 혹은 메일(hoons0131@gmail.com)로 연락해주시면 빠른 회신 드리도록 하겠습니다. 이 외에도 네트워킹에 대해서는 언제나 환영입니다.:Martin(https://github.com/martinYounghoonKim
NodeJS의 API 프레임워크 사용해보기
Javascript Array 타입 Methods