-
[JS] prototype을 이용한 상속programing/Language 2019. 7. 27. 17:09
안녕하세요, Einere입니다.
(ADblock을 꺼주시면 감사하겠습니다.)
이번 포스트에서는 prototype을 이용한 상속과, 다양한 실험을 해보도록 하겠습니다.
prototype의 종류
(사진을 제공해주신 나모씨에게 감사의 말씀 드립니다..)
[[prototype]] 혹은 __proto__
해당 객체의 프로토타입을 가리키는 포인터입니다.
예를 들어,
Person
생성자 함수로 만든 인스턴스person.__proto__
는Person.prototype
입니다.또한,
Person.__proto__
는Function.prototype
입니다.prototype
함수 객체에만 존재하는 속성입니다.
해당 함수가 생성자 함수로 사용되었을 때 생성되는 인스턴스의 프로토타입에 넣을 대상입니다.
예를 들어,
Person.__proto__
는Function.prototype
이지만Person.prototype
은Person.prototype
입니다. 따라서Person
함수로 만든 인스턴스person
의__proto__
는Person.prototype
이 되는 것입니다.상속관계가 아닌 경우
// a ---> Object.prototype ---> null const A = function(){ this.aa = 1; }; const a = new A(); // b ---> a ---> Object.prototype ---> null const B = function(){ // A.call(this); this.bb = 2; }; // B.prototype = Object.create(A.prototype); // B.prototype.constructor = B; const b = new B(); console.log(a); // A { aa: 1 } 출력 console.log(b); // B { bb: 2 } 출력 console.log(a instanceof A); // true 출력 console.log(b instanceof A); // false 출력 console.log(b instanceof B); // true 출력
생성자함수 A는 속성으로 aa=1을 가지며, B는 속성으로 bb=2를 가지게 합니다.
이렇게 생성한 인스턴스 a, b에 대해 다양한 출력 결과는 위와 같습니다.
a는 A함수를 이용해 생성한 인스턴스이므로 A { aa:1 }이 출력되며, b는 B함수를 이용해 생성한 인스턴스이므로 B { bb:2 }가 출력됩니다.
그리고 a는 A의 인스턴스이며, b는 B의 인스턴스입니다만, b는 A를 상속하지 않으므로 A의 인스턴스가 아닙니다.
A.call(this)
Java와 비슷하게, 객체의 생성자 연결(chain)에 call을 사용할 수 있습니다.
// a ---> Object.prototype ---> null const A = function(){ this.aa = 1; }; const a = new A(); // b ---> a ---> Object.prototype ---> null const B = function(){ A.call(this); this.bb = 2; }; // B.prototype = Object.create(A.prototype); // B.prototype.constructor = B; const b = new B(); console.log(a); // A { aa: 1 } 출력 console.log(b); // B { aa: 1, bb: 2 } 출력 console.log(a instanceof A); // true 출력 console.log(b instanceof A); // false 출력 console.log(b instanceof B); // true 출력 console.log(b.hasOwnProperty('aa')); // true 출력 console.log(b.hasOwnProperty('bb')); // true 출력
B생성자 함수에서 A.call(this);를 추가한 결과, 위와 같은 결과가 나왔습니다.
바로 b인스턴스는 aa: 1이라는 속성과 값을 가지게 되었습니다.
이 결과로 보아, A.call(this)를 호출하면 A의 속성값(즉, 필드)를 상속받는 것을 알 수 있습니다.
하지만, 여전히 b는 A의 instance는 아니라고 하네요.
조금 더 자세히 살펴보자면, A.call()에서 A생성자 함수를 호출하면서, 해당 함수 내부에서 사용되는 this를 B생성자 함수의 this로 바인딩을 했습니다. A.call()로 인해 인자로 넘겨준 this에 aa 프로퍼티를 추가하고 값을 1로 초기화 하게 됩니다. 결국, B생성자로 만들어진 인스턴스의 프로퍼티에는 aa속성이 존재하게 됩니다.
B.prototype
// a ---> Object.prototype ---> null const A = function(){ this.aa = 1; }; const a = new A(); // b ---> a ---> Object.prototype ---> null const B = function(){ // A.call(this); this.bb = 2; }; B.prototype = Object.create(A.prototype); // B.prototype.constructor = B; const b = new B(); console.log(a); // A { aa: 1 } 출력 console.log(b); // A { bb: 2 } 출력 console.log(a instanceof A); // true 출력 console.log(b instanceof A); // true 출력 console.log(b instanceof B); // true 출력 console.log(b.hasOwnProperty('aa')); // false 출력 console.log(b.hasOwnProperty('bb')); // true 출력
이번에는 B.prototype에 A.prototype을 할당한 결과, 위와 같은 결과가 나왔습니다.
놀랍게도 b인스턴스는 B함수로 생성했지만 B객체가 아닌 A객체라고 출력됩니다.
하지만 instanceof 키워드를 사용하면 참으로 판단을 하네요.
특정 인스턴스가 어느 객체의 인스턴스인지 확인할려면 instanceof 키워드를 이용해 확인하는 것이 명확한 것 같습니다.
// a ---> Object.prototype ---> null const A = function(){ this.aa = 1; }; A.prototype.myCall = function() { console.log(`i'm A`); }; const a = new A(); // b ---> a ---> Object.prototype ---> null const B = function(){ // A.call(this); this.bb = 2; }; // B.prototype = Object.create(A.prototype); // B.prototype.constructor = B; /*B.prototype.myCall = function() { console.log(`i'm B`); };*/ const b = new B(); a.myCall(); // i'm A 출력 b.myCall(); // 에러 발생
B.prototype에 A.prototype을 대입하지 않은 경우에, 추가로 A.prototype에만 myCall이란 속성을 정의하고 B.prototype에는 정의하지 않은 경우, 위와 같은 결과가 나옵니다.
// a ---> Object.prototype ---> null const A = function(){ this.aa = 1; }; A.prototype.myCall = function() { console.log(`i'm A`); }; const a = new A(); // b ---> a ---> Object.prototype ---> null const B = function(){ // A.call(this); this.bb = 2; }; B.prototype = Object.create(A.prototype); // B.prototype.constructor = B; /*B.prototype.myCall = function() { console.log(`i'm B`); };*/ const b = new B(); a.myCall(); // i'm A 출력 b.myCall(); // i'm A 출력
궁금해서 한번 디버거로 찍어보니, 위와 같이 나왔습니다.
b는 myCall이라는 속성이 정의되어 있지 않기 때문에 b.__proto__에서 찾아봅니다.
b.__proto__에는 A객체가 들어가 있기 때문에, A에 myCall이라는 속성이 있는지 찾아봅니다.
A.myCall이라는 속성이 정의되어 있지 않기 때문에, a.__proto__에서 찾아봅니다.
A.prototype.myCall은 함수로 정의되어 있으니, 해당 함수를 실행하여 i'm A를 출력합니다.
// a ---> Object.prototype ---> null const A = function(){ this.aa = 1; }; A.prototype.myCall = function() { console.log(`i'm A`); }; const a = new A(); // b ---> a ---> Object.prototype ---> null const B = function(){ // A.call(this); this.bb = 2; }; B.prototype = Object.create(A.prototype); B.prototype.constructor = B; B.prototype.myCall = function() { console.log(`i'm B`); }; const b = new B(); a.myCall(); // i'm A 출력 b.myCall(); // i'm B 출력
B.prototype에 myCall이라는 함수를 정의한 결과는 위와 같습니다.
즉, 부모의 prototype에 정의된 함수와 같은 이름의 함수를 자식의 prototype에 설정해주면, 오버라이딩(overriding)이 되는 것을 알 수 있습니다.
B.prototype.constructor
// a ---> Object.prototype ---> null const A = function(){ this.aa = 1; }; const a = new A(); // b ---> a ---> Object.prototype ---> null const B = function(){ // A.call(this); this.bb = 2; }; // B.prototype = Object.create(A.prototype); B.prototype.constructor = B; const b = new B(); console.log(a); // A { aa: 1 } 출력 console.log(b); // B { bb: 2 } 출력 console.log(a instanceof A); // true 출력 console.log(b instanceof A); // false 출력 console.log(b instanceof B); // true 출력 console.log(b.hasOwnProperty('aa')); // false 출력 console.log(b.hasOwnProperty('bb')); // true 출력
B.prototype.constructor에 B생성자 함수를 할당하였더니 위와 같은 결과가 나왔습니다.
이번엔 b를 출력했을 때, B { bb: 2}라고 정확하게 출력됩니다.
하지만 B.prototype에 A.prototype을 할당하지 않았기 때문에 b는 A의 인스턴스가 아니라고 판단하네요.
명확한 상속
// a ---> Object.prototype ---> null const A = function(){ this.aa = 1; }; const a = new A(); // b ---> a ---> Object.prototype ---> null const B = function(){ A.call(this); this.bb = 2; }; B.prototype = Object.create(A.prototype); B.prototype.constructor = B; const b = new B(); // c ---> b ---> a ---> Object.prototype ---> null const C = function(){ B.call(this); this.cc = 3; }; C.prototype = Object.create(B.prototype); C.prototype.constructor = C; const c = new C(); console.log(a); // A { aa: 1 } console.log(b); // B { aa: 1, bb: 2 } console.log(c); // C { aa: 1, bb: 2, cc: 3 } console.log(c.aa); // 1 console.log(c.bb); // 2 console.log(c.cc); // 3 console.log(a instanceof A); // true console.log(b instanceof A); // true console.log(b instanceof B); // true console.log(c instanceof A); // true console.log(c instanceof B); // true console.log(c instanceof C); // true console.log(c.hasOwnProperty('aa')); // true console.log(c.hasOwnProperty('bb')); // true console.log(c.hasOwnProperty('cc')); // true
이제, B를 상속하는 C생성자 함수를 추가해보았습니다.
물론 B.call(this)로 B의 속성도 가지도록 하며, C.prototype = Object.create(B.prototype)을 이용해 B의 메소드들을 사용할 수 있으며, C.prototype.constructor에 C함수를 대입해서 C생성함수로 생성된 인스턴스임을 명확히 했습니다.
상속은 어떻게 이루어질까
상속은 기본적으로 prototype chaining을 통해 이루어집니다. 위의 코드를 예로 들자면, c의 프로퍼티에는 __proto__가 있으며, 해당 프로퍼티에는 C.prototype의 프로퍼티들이 저장되어 있습니다.
프로토타입, 상속, 객체에 대해 더 자세한 정보를 알고싶으신 분들은, 최하단의 참고 링크의 peiema web을 참고해주세요. (정말 설명이 자세하게 잘 되어 있습니다.)
결론
prototype을 이용한 상속을 하기 위해서는 다음과 같은 세가지 요소를 구현해야 합니다.
- 자식의 생성자 함수 내에서, Parent.call(this)를 호출해야 한다
- 부모의 필드를 상속하는 기능
- Child.prototype에 Object.create(Parent.prototype)을 할당해야 한다
- 부모의 메소드를 상속하는 기능
- child instanceof Parent를 true로 만들어주는 기능
- 오버라이딩을 하고자 한다면, Child.prototype에 같은 이름의 함수를 할당하면 된다
- Child.prototype.constructor에 Child생성자 함수를 할당해야 한다
- Child생성자 함수로 생성한 child인스턴스가 Parent의 인스턴스가 아닌 Child의 인스턴스임을 명시하는 기능
참고
이선 브라운, 『러닝 자바스크립트, ES6로 제대로 입문하는 모던 자바스크립트 웹 개발』, 한빛미디어(2017).
[[JS 프로토타입] 프로토타입을 사용하여 상속하기
https://poiemaweb.com/js-object-oriented-programming
상속과 프로토타입'programing > Language' 카테고리의 다른 글
[Node] 동일 출처 정책과 CORS와 에러 해결법 (0) 2019.07.29 [JS] spread operator을 이용한 객체 복사 (0) 2019.07.27 [JS] state와 mutable, immutable (0) 2019.07.23 [JS] promise와 async await의 차이 (0) 2019.07.23 [Functional] 지연 평가와 L.map, L.filter (0) 2019.07.22 댓글
- 자식의 생성자 함수 내에서, Parent.call(this)를 호출해야 한다