함께 성장하는 프로독학러

생성자 함수와 prototype 객체 본문

Programming/javascript

생성자 함수와 prototype 객체

프로독학러 2018. 4. 14. 18:42

안녕하세요, 프로독학러 입니다.


이번 포스팅에서는 자바스크립트의 생성자 함수와 prototype 객체에 대해서 알아보도록 하겠습니다.


먼저 prototype 객체에 대해서 알아보겠습니다.


일단 이름부터가 무시무시합니다. 저도 처음에 프로토타입 객체니 생성자니 this니 constructor니... 하는 용어들에 압도된 적이 있었는데요, 천천히 알아가 보도록 합시다.


자바스크립트에는 여러가지 데이터 형태가 있습니다.

Boolean, Number, String, Null, undefined, Object, Array, Function 등 여러가지의 데이터 타입이 있습니다.

이 데이터 타입을 크게 기본 타입과 객체로 나누어 볼 수 있습니다.


  • 기본 타입 : Number, String, Boolean, undefined, null
  • 객체 (Object) : Array, Function, RegExp, Date


기본타입을 제외한 객체는 모두 proto(비표준) 속성을 가지고 있습니다.

그리고 객체의 proto 속성은 객체가 만들어지기 위해 사용된 prototype 객체를 내부적으로(보이지 않는 숨은 링크로) 참조하는 역할을 수행합니다. (자신을 만들어낸 객체의 원형 이라는 의미)

그리고 prototype 객체를 참조하는 객체는 해당 prototype 객체가 가지고 있는 메소드를 상속받습니다.

무슨 말이냐고요? 천천히 살펴봅시다.


1
2
3
4
5
var arr = [12345];
 
var slicedArr = arr.slice(03);
console.log(slicedArr);
// (3) [1, 2, 3]
cs


위의 코드를 살펴 봅시다. 첫 번째 줄에서 배열 arr에 [1, 2, 3, 4, 5]를 할당했습니다.

그리고 다음 줄에서 arr 배열에 slice 메소드를 실행한 리턴값을 slicedArr 에 할당했습니다.

그런데, 우리는 arr를 정의할 때 slice 메소드를 정의 한 적이 없습니다.

"지금까지는 그냥 응, 배열의 기본 메소드니깐. 배열에는 원래 저게 있어."

라고 하고 아무렇지 않게 사용하셨을 수 도 있습니다.

(잘못됐다는 얘기는 아닙니다)

그럼 이 slice 라는 우리가 정의한 적 없는 메소드가 어디서 튀어나와 우리가 사용할 수 있는 것일까요?

이를 자바스크립트 내부에서 이해해 보도록 하겠습니다.


우리가 배열 'arr' 를 선언하고 배열을 할당하는 순간 자바스크립트 내부에서는 'arr' 를 만들어야 할 객체로 인식합니다.

(배열이지만 객체로 인식하는 이유는 위에서 언급한 바와 같이 배열은 객체형 데이터이기 때문입니다.)

그리고 객체 'arr' 를 만들기 시작하는데, 이 객체를 만들때 Array.prototype 객체의 constructor를 참조해 만듭니다.

또한, 만들어지는 'arr' 객체는 내부적으로 proto 속성을 가지고 있습니다.

그 proto 속성은 Array.prototype 을 숨은 링크로 참조합니다.

그리고 어떤 객체가 어떤 prototype 객체를 proto 속성으로 참조한다는 것은 해당 prototype 객체가 가지고 있는 메소드를 상속받는 다는 것입니다.

따라서, Array.prototype 이 가지고 있던 slice 메소드를 배열(객체타입) arr 가 상속받아 사용할 수 있는 것입니다.

이를 그림으로 표현하면 다음과 같습니다.



  1. arr 가 선언 되고, arr 에 배열이 할당 됨.
  2. arr 에는 배열이 할당 되었으니 arr 객체를 만들어야 겠구나!
  3. arr 객체는 어떤 객체를 참조해서 만들지?
  4. arr 가 배열이니 Array.prototype 을 참조해서 만들면 되겠구나! (Array.prototype.cunstructor)
  5. arr 객체를 생성.
  6. arr 객체는 Array.prototype 객체의 컨스트럭터를 참조해 만들었으므로 prototype link(__proto__) 는 Array.prototype 객체를 참조. (arr 를 만들어낸 객체의 원형)
  7. arr 객체는 Array.prototype 의 메소드를 상속 받는다.
  8. 따라서 arr 객체는 Array.prototype 의 slice 메소드를 사용할 수 있다. (상속받았기 때문)


prototype 객체는, 간단히 말하면 어떤 객체를 만들고, 메소드를 상속해 주는 역할을 수행하는 내부적으로 존재하는 객체입니다.


이제 생성자 함수에 대해서 알아보도록 합시다.


생성자 함수에 본격적으로 들어가기 이전에 이해해야 할 것이 있습니다.

그것은 자바스크립트에서 어떤 함수를 정의하면 내부적으로 그 함수의 prototype 객체가 생성된다는 것입니다.

그리고 함수의 정의로 인해 생성된 prototype 객체는 prototype link 와 constructor 를 가지고 있습니다.

이 prototype 의 prototype link 는 자신을 만들어낸 객체의 원형을 참조하는 역할을 하고, constructor 는 정의된 함수가 생성자가 될 수 있는 자격을 부여합니다.

즉, 자바스크립트에서 사용하는 함수는 모두 생성사의 자격을 갖추고 있다는 뜻입니다.


그렇다면 생성자는 무엇일까요?

생성자는 new 라는 키워드와 함께 함수를 실행했을 때, 해당 함수에 정의된 내용으로 객체를 만드는 것을 의미합니다.

(이는 함수의 프로토타입 객체에 constructor 가 있기 때문에 가능한 것입니다)

하지만 이렇게 만들어진 객체는 함수를 정의한 것이 아니기 때문에 그만의  prototype 객체를 가지고 있지는 않습니다.

대신 이렇게 생성된 객체 내부에 prototype link 를 가지고 있고, prototype link 는 자신을 생성한 함수의 프로토타입 객체를 숨은 링크로 참조합니다.


이해를 돕기위해 코드와 함께 살펴보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
function person(name){
    this.name = name
}
person.prototype.getName = function(){
    return console.log(this.name);
}
 
var person1 = new person('Mark');
console.log(person1);
// person {name: "Mark"}
 
person1.getName();
// Mark
cs


위의 코드에서 person이라는 함수를 정의 했습니다. (1)

person 이라는 함수를 정의 하는 순간 자바스크립트 내부적으로는 person의 프로토타입 객체를 생성합니다. (2)

그리고 person 프로토타입 객체에는 constructor 와 prototype link가 있습니다.

constructor 가 존재하기 때문에 person 함수는 생성자 함수로서 객체를 만들어낼 수 있는 자격을 가지고 있습니다.

4번째 줄에서 person.prototype 객체에 getName 이라는 메소드를 직접 정의 했습니다. (3)

getName 메소드는 생성된 객체의 name 속성을 콘솔로 찍는 메소드 입니다.


(생성자 함수에서의 this가 익숙하지 않으신 분들은 아래의 링크를 참조해 주세요)

<자바스크립트의 this>


8번째 줄에서는 person1 이라는 변수를 선언하고, person함수를 생성자로 호출하여 (객체생성) 할당했습니다. 

person1 객체가 만들어 진 것입니다. (4)

(person1 객체는 함수를 정의 한 것이 아니기 때문에 person1 프로토타입 객체는 존재하지 않습니다. person1 객체의 prototype 링크가 person 함수의 prototype 객체를 링크로 참조하고 있을 뿐입니다. / person 함수의 컨스트럭터로 만들어졌기 때문) (5)

12번째 줄에서 person1에 person 함수의 프로토타입 객체의 메서드인 getName을 사용했습니다.

person1 객체는 person 함수의 프로토타입 객체의 constructor 로 만들어 졌으므로 (메소드를 상속) person 함수의 메소드를 사용할 수 있는 것입니다.


그림으로 표현하면 다음과 같습니다.



위에서 살펴본 것과 같이 함수를 선언하면 프로토타입객체가 자동으로 생성됩니다.

그리고 프로토타입 객체에 메서드를 정의할 때는 (프로토타입 객체에 메소드를 정의하는 것은 메소드의 상속을 위함) 

함수명.prototype.메소드명 = 메소드내용

와 같이 정의 합니다. (위의 코드의 4번째 줄과 같이)


만약 어떤 함수의 프로토타입링크를 해당 함수의 프로토타입이 아닌 다른 객체로 변경하려면 어떻게 하면 될까요?

이는

함수명.prototype = 참조대상

으로 하면, 해당 함수의 prototype link 가 해당 함수의 프로토타입이 아닌 지정한 참조대상으로 바뀌게 됩니다.


함수의 프로토타입 링크를 바꾸는 것을 통해 어떤 작업을 할 수 있을까요?

바로 "상속" 의 변경입니다.

위에서 객체가 prototype link 를 통해 어떤 프로토타입 객체를 참조하면 해당 프로토타입 객체의 메소드를 상속한다고 했습니다.

프로토타입의 링크를 상속받고자하는 함수의 프로토타입 객체로 변경하면 메소드를 따로 정의 할 필요없이 해당 프로토타입의 메소드를 사용할 수 있는 것입니다.


아래부터 코드를 통해 상속을 변경하는 방법에 대해서 천천히 살펴보겠습니다.


첫 번째 방법은 부모함수를 이용하여 객체를 생성하고, 자식에 해당하는 함수의 prototype link 를 부모함수를 통해 생성한 객체에 참조하는 방법입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(name) {  
    this.name = name || "홍길동"
}
 
Person.prototype.getName = function(){  
    return this.name;
};
 
function Korean(name){}  
Korean.prototype = new Person();
 
var kor1 = new Korean();  
console.log(kor1.getName());  // 혁준
 
var kor2 = new Korean("류현진");  
console.log(kor2.getName());  // 혁준
cs


코드의 첫 번째 줄에서 함수 Person에 대해서 정의 했습니다. 함수의 내용으로는 이 함수를 통해 생성될 객체의 name 속성을 부여하는 것이 들어가 있습니다. (Person 함수를 생성자로 호출할 때 인자가 들어오면 그 인자를 name 속성의 값으로 할당하고, 인자가 들어오지 않으면 '홍길동'이 할당되도록 정의되어 있습니다.) (1)

*함수 Person이 정의됨과 동시에 자바스크립트 내부에는 Person 함수의 프로토타입 객체가 생성됩니다. (2)


코드의 다섯 번째 줄에서 자동으로 생성된 Person 함수의 프로토타입 객체에 getName 이라는 메소드를 정의하고 있습니다. (3)

메소드의 내용은 해당 객체의 name 속성을 리턴하는 것입니다.


아홉번째 줄에서 Korean 라는 함수를 정의 하고 파라메터를 name 이라고 주었습니다. (4)

Korean 함수 역시 함수가 정의 됐으므로 자동으로 자바스크립트 내부에 Korean 함수의 프로토타입 객체를 생성합니다. (5)

그리고 korean 함수의 prototype링크는 기본적으로 자신의 프로토타입 객체에 참조되므로 korean 함수 프로토타입 링크에 참조되어 있습니다. (6)


코드의 10번째 줄에서 Korean 함수의 prototype link 를 Person 함수를 생성자로 호출한 객체(7)로 변경하고 있습니다. (8)

(Korean 함수의 prototype link = Korean 함수의 프로토타입 객체 -> Person 함수 생성자 호출로 생성된 객체(익명의-변수에 할당하지 않았으므로)) (8)


코드의 12번째 줄에서 kor1 변수에 Korean 함수를 생성자로 호출해 객체를 만들어 할당했습니다. (9)

호출시 인자는 들어오지 않았습니다. 

kor1 객체의 prototype 은 10번째 줄에서 만들어진 객체를 참조합니다. (8)

10번째 줄에서 만들어진 객체(7)는 Person 함수의 프로토타입 객체 constructor로 만들어졌기 때문에 prototype link 가 Person 함수의 프로토타입 객체를 숨은 링크로 참조합니다. (10)

(Person 함수의 프로토타입 객체의 getName 메소드를 사용할 수 있습니다)

그리고 kor1 객체의 프로토타입링크(8)가 이 객체(7)이기 때문에 kor1 역시 getName 메소드를 사용할 수 있습니다.

따라서 13번째 줄의 결과는 혁준입니다.


15번째 줄의 kor2는 kor1 과 비슷해 보이지만 생성자로 Korean 함수를 호출할 때 인자를 주었습니다.(11)

Korean 함수에 인자값 name이 들어갔으므로 16번째 줄의 콘솔로그의 결과가 "류현진"이 찍혀야 할 것같지만 kor1과 마찬가지로 혁준이 찍힙니다.

이는 Korean 함수의 인자 name이 Person 함수의 name과 연결돼 있지 않기 때문입니다.

10번째 줄의 코드는 Korean 함수의 프로토타입 링크만을 변경할 뿐 (8) Person 함수로 인자 Korean 함수의 인자를 전달하지 못합니다.


이를 그림으로 나타내면 다음과 같습니다.



두 번째 방법은 부모함수의 this 에 자식객체를 바인딩 하는 방법입니다.


위의 첫 번째 방법에서는 Korean 함수의 인자를 Person 함수로 전달하지 못하는 단점이 있었습니다.

이를 함수의 간접실행을 통해 해결하는 방법에 대해 살펴봅시다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(name) {  
    this.name = name || "홍길동";
}
 
Person.prototype.getName = function(){  
    return this.name;
};
 
function Korean(name){  
    Person.apply(this, arguments);
}
 
var kor1 = new Korean("류현진");  
console.log(kor1.name);  // 류현진
 
console.log(kor1.getName());
// Uncaught TypeError: kor1.getName is not a function
cs


Person 함수를 정의하는 부분과 Person 함수의 프로토타입 객체에 getName 메소드를 정의하는 코드까지는 첫 번째 방법과 같으므로 설명을 생략하겠습니다.


코드의 9번째 줄부터 보겠습니다.

코드의 9번째 줄에서 Korean 함수를 정의 할 때, 함수의 내용으로 Person 함수를 apply를 통해 간접 실행하고 있습니다.

간접실행(call, apply 메소드)는 첫 번째 인자에 실행 문맥을 지정해 주는 것입니다.

*apply 메소드의 두 번째 인자는 유사배열만 들어올 수 있습니다. (*arguments 객체는 유사배열)

Person 함수를 실행 할 때의 this를 Korean 함수로 만들어질 객체(생성자 Korean 에서의 this)로 바인딩 하는 것입니다.


Korean 함수를 실행 할 때 Person 함수를 this 로 바인딩해 간접 실행하기 때문에 Korean 함수는 Person 함수와 똑같이 작동합니다.

(Korean 이 실행되면 Korean 안에서 Person이 실행됨)

따라서 생성된 kor1 객체에서 name 속성을 사용할 수 있습니다. (Person의 this 가 Korean의 this와 같아짐)


하지만 Korean 함수와 Person.prototype 객체간의 연결고리가 없기 때문에 getName 메소드는 사용할 수 없습니다.


세 번째 방법은 생성자를 빌려쓰고 프로토타입을 지정해 주는 방법입니다.


세 번째 방법은 첫 번쨰 방법과 두 번째 방법을 합친 것으로 볼 수 있습니다.

코드를 통해 살펴 보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(name) {  
    this.name = name || "홍길동"
}
 
Person.prototype.getName = function(){  
    return this.name;
};
 
function Korean(name){  
    Person.apply(this, arguments);
}
Korean.prototype = new Person();
 
var kor1 = new Korean("류현진");  
console.log(kor1.getName());  // 류현진
cs


9번째 줄에서 두 번째 방식과 마찬가지로 Person 함수를 간접실행하여 this 를 바인딩했습니다. (name 속성 사용가능)

그리고 12번째 줄에서 첫 번째 방식처럼 Korean 함수의 프로토타입 링크를 Person 생성자를 통해 생성된 객체에 참조시켰습니다.


즉, name 속성은 this 바인딩을 통해 Korean 생성자자 Person 함수내용을 동작시켜 사용가능하고, getName 메소드는 Korean 함수의 프로토타입 링크를 new Person() 객체에 참조함으로 사용가능 한 것입니다.


이름 그림으로 표현하면 다음과 같습니다.



그러나 이 방법에도 단점은 있습니다. 

Person 함수가 Korean 함수내에서 호출되어 kor1 객체에 name 속성이 존재하고, 프로토타입 링크를 위해 만든 new Person 객체에도 name 속성이 존재하게 됩니다.

그림에서 kor1 에 name 속성이 (류현진) 존재하고 new Person() 객체에도 name 속성(홍길동)이 존재하는것을 알 수 있습니다.


네 번쨰 방법은 프로토타입을 공유하는 방법입니다.


이 방법에서는 부모생성자를 한번도 생성하지 않고 프로토타입 객체를 공유합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(name) {  
    this.name = name || "홍길동";
}
 
Person.prototype.getName = function(){  
    return this.name;
};
 
function Korean(name){  
    this.name = name;
}    
Korean.prototype = Person.prototype;
 
var kor1 = new Korean("류현진");  
console.log(kor1.getName());  // 류현진 
cs


위의 코드에서 9 번째 줄에서 Korean 함수를 정의 할 때 Korean 생성자로 만들어질 객체에 name 속성을 따로 부여했습니다.

그리고 12 번째 줄에서 Korean 함수의 prototype 링크를 Person 함수의 프로토타입 객체로 지정했습니다.


따라서 kor1 객체는 Person 함수의 프로토타입 객체의 메서드인 getName 메서드를 사용할 수 있습니다.


세 번째 방법과 달리 중간에 Person 함수로 생성한 객체가 존재하지 않습니다.


첫 번째 부터 네 번째 까지의 방식을 classical 방식이라고 하는데, 이는 JAVA에서의 객체를 생성하는 방법과 유사하기 때문입니다.


다섯 번째 방식은 Object.create 메소드를 활용한 prototypal 방식입니다.


Object.create 메소드는 첫 번째 인자로 들어오는 객체를 새로 만들 객체의 프로토타입 객체로 지정합니다.

두 번째 인자는 선택사항인데, 새롭게 생성될 객체에만 부여할 속정을 정의하는 것입니다.

리턴값은 새로운 객체입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var person = {  
    type : "사람",
    getType : function(){
        return this.type;
    },
    getName : function(){
        return this.name;
    }
};
 
var jin = Object.create(person, {'job':{value:"야구선수"}});  
jin.name = "류현진";
 
console.log(jin.getType());  // 사람  
console.log(jin.getName());  // 류현진
console.log(jin.job) // 야구선수
console.log(person.job) // undefined
cs


코드의 첫 번째 줄에서 객체 person을 정의 했습니다. person에는 type 속성이 있고, getType, getName 메소드가 있습니다.

11 번째 줄에서 Object.create 를 이용해 객체를 생성합니다. 생성하는 객체의 프로토타입 객체를 person 객체로 지정했습니다. 

따라서 jin 객체는 person 객체를 상속받습니다. 

Object.create 메소드를 실행 할 때, 두 번째 인자로 jin만의 속성을 부여했습니다. (person에서는 접근 불가)

두 번째 인자는 객체로 들어와야하며, 객체안의 key는 새롭게 생성될 객체의 key 가 되고, 값은 다른 객체 안의 value 값으로 지정합니다.


12번째 줄에서는 jin 객체에 name 속성을 직접 정의 했습니다.


따라서 객체 jin은 person 객체의 모든 속성을 상속받으면서, 자신만의 job, name 속성도 가지게 되었습니다.


자바스크립트에서는 객체의 속성상속을 할 때, classical 방식 보다 prototypal 방식이 선호됩니다. (이해하기도 쉽다)


여기까지 프로토타입 객체와 생성자 함수, 그리고 생성자함수와 프로토타입을 이용한 객체속성의 상속에 대해서 알아보았습니다.


정리하자면, 

prototype 객체는 constuctor 를 통해 만든 객체를 만들거나(컨스트럭터를 통해 만들어진 객체는 프로토타입 객체를 숨은 링크로 참조) 어떤 객체가 자신을 프로토타입 링크로서 참조했을 때 자신의 속성들을 상속해 주는 역할을하는 객체입니다. (임의 변경한 경우도 포함됨)

생성자 함수는 함수가 정의될 때 자동으로 생성되는 함수의 프로토타입 객체의 constructor 를 이용하여 객체를 만드는 역할을 수행하는 것 입니다.

속성의 상속은 new 연산자를 통해 만든 객체를 사용하여 상속하는 classical 방식과 Object.create 메소드를 이용해 프로토타입 객체를 직접 지정해 주는 prototypal 방식이 있습니다. (prototypal 방식이 선호됨)


객체지향적 언어인 자바스크립트의 큰 산 중 하나인 prototype 에 대해서 이해하는데 이 글이 조금이나마 도움이 됐으면 좋겠습니다.

감사합니다.


** 참고자료

http://www.nextree.co.kr/p7323/


*다녀가셨다는 표시는 공감으로 부탁드릴게요! (로그인 하지 않으셔도 공감은 가능합니다 ㅎㅎ)

Comments