ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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.prototypePerson.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
    상속과 프로토타입

    댓글

Designed by black7375.