자바스크립트를 배워보자 19일차 - 프로토타입(prototype) 3편

0

Javascript

목록 보기
19/30
post-thumbnail

프로토타입 3편

1. 프로토타입 체인

function Person(name){
    this.name = name
}

Person.prototype.hello = function(){
    console.log(`my name is ${this.name}`)
}

const me = new Person('lee')
console.log(me.hasOwnProperty('name')) // true
console.log(Person.prototype.hasOwnProperty('name')) // false

지난 번의 예제이다. 어떻게 mePerson.prototypehasOwnProperty 프로퍼티를 가지고 있을까?? 이는 프로토타입 체인 덕분이다.

Object.getPrototypeOf()을 이용하면 prototype이 무엇인지 확인할 수 있다. 이를 이용하여 Person.prototypeprototype이 무엇인지 찾아보도록 하자

위 코드의 아래의 코드를 추가해보도록 하자

console.log(Object.getPrototypeOf(me) === Person.prototype) // true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype) // true

위 예제를 그림으로 확인하면 다음과 같다.

즉, me 객체의 상위인 me.prototypePerson.prototype이고 person.prototype의 상위 객체인 Person.prototype.prototypeObject.prototype이라는 것이다.

이와 같은 구조를 이용하여, 자바스크립트는 객체의 프로퍼티(메서드 포함)에 접근하려고 할 때 , 해당 객체에 접근하려는 프로퍼티가 없으면 [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다. 이를 프로토타입 체인이라고 한다. 프로토타입 체인은 자바스크립트가 객체지향 프로그래밍의 상속을 구현하는 알고리즘이다.

console.log(me.hasOwnProperty('name')) // true

어떻게 me 객체가 프로토타입 체인을 통해 hasOwnProperty를 찾을 수 있는 지 확인해보도록 하자

  1. hasOwnProperty()가 me에 있는가?? 없다. 따라서, 프로토타입 체인에 따라 [[Prototype]]에 바인딩된 프로토타입으로 간다.
  2. me.prototype은 Person.prototype이다. 여기서도 hasOwnProperty()가 있는 지 찾는다. 없으므로 프로토타입 체인을 따라간다. 즉, [[Prototype]] 내부 슬롯에 바인딩된 프로토타입으로 간다. 여기서는 Object.prototype이다.
  3. Object.prototype에 가서 hasOwnProperty를 검색한다. 있으므로 자바스크립트 엔진은 이를 me 객체에 바인딩된다.
  • call 메서드

    Object.prototype.hasOwnProperty.call(me, 'name') // true

    call 메서드는 this로 사용할 객체를 전달하면서 함수를 호출한다. 이후에 자세히 배울 예정이므로, 지금은 this로 사용할 me 객체를 전달하면서 Object.prototype.hasOwnProperty() 메서드를 호출한다고 보면 된다.

프로토타입 체인의 최상위에 위치하는 객체는 언제나 Object.prototype이다. 따라서 모든 객체는 Object.prototype을 상속받는다. Object.prototype을 프로토타입 체인의 종점이라고 한다. Object.prototype의 프로토타입, 즉 [[Prototype]] 내부 슬롯의 값은 null이다.

이처럼 자바스크립트 엔진은 프로토타입 체인을 따라 프로퍼티/메서드를 검색한다. 다시 말해 자바스크립트 엔진은 객체 간의 상속 관계로 이루어진 프로토타입의 계층적인 구조에서 객체의 프로퍼티를 검색한다.

따라서 프로토타입은 상속과 프로퍼티 검색을 위한 매커니즘이라고 할 수 있다.

이에 반해 프로퍼티가 아닌 식별자는 스코프 체인에서 검색한다. 다시 말해, 자바스크립트 엔진은 중첩 관꼐로 이루어진 스코프의 계층적인 구조에서 식별자를 검색한다. 따라서 스코프 체인은 식별자 검색을 위한 매커니즘이다.

2. 오버라이딩과 프로퍼티 섀도잉

const Person = (function(){
    function Person(name){
        this.name = name
    }
    Person.prototype.hello = function(){
        console.log(`my name is ${this.name}`)
    }
    return Person;
}())
const me = new Person('lee')
me.hello() // my name is lee
me.hello = function(){
    console.log(`hey my name is ${this.name}`)
}
me.hello() // hey my name is lee

생성자 함수로 객체(인스턴스)를 생성한 다음, 인스턴스에 메서드를 추가했다.

프로토타입이 소유한 프로퍼티(메서드 포함)를 프로토타입 프로퍼티, 인스턴스가 소유한 프로퍼티를 인스턴스 프로퍼티라고 한다.

프로토타입 프로퍼티와 같은 이름의 프로퍼티를 인스턴스에 추가하면 프로토타입 체인을 따라 프로토타입 프로퍼티를 검색하여 프로토타입 프로퍼티를 덮어쓰는 것이 아니라, 인스턴스 프로퍼티로 추가한다.

이때 인스턴스 메서드 hello()는 프로토타입 메서드 hello()를 오버라이딩했고 프로토타입 메서드 hello()는 가려진다. 이처럼 상속 관계에 의해 프로퍼티가 가려지는 현상을 프로퍼티 섀도잉이라고 한다.

삭제하는 경우도 마찬가지이다.

delete me.hello
me.hello() // my name is lee

당연히 프로토타입 메서드가 아닌 인스턴스 메서드 hello가 삭제된다. 다시 한 번 hello메서드를 삭제하여 프로토타입 메서드의 삭제를 시도해보면

delete me.hello
me.hello() // my name is lee

delete me.hello
me.hello() // my name is lee

재밌게도, 인스턴스 객체에서 해당 인스턴스 프로퍼티를 삭제하는 것은 가능하지만, 인스턴스 객체에서 상위 프로퍼티를 삭제하는 것은 불가능하다.

이와 같이 하위 객체를 통해 프로토타입의 프로퍼티를 변경 또는 삭제하는 것은 불가능하다. 다시 말해 하위 객체를 통해 프로토타입에 get 액세스는 허용되나 set 액세스는 허용되지 않는다.

즉, read는 되는데 변경하는 write는 불가능하다.

따라서, 프로토타입 프로퍼티를 변경 또는 삭제하려면 하위 객체를 통해 프로토타입 체인으로 접근하는 것이 아니라, 프로토타입에 직접 접근해야 한다.

3. 프로토타입의 교체

프로토타입은 임의의 다른 객체로 변경할 수 있다. 이것은 부모 객 체인 프로토타입을 동적으로 변경할 수 있다는 것을 의미한다.

이러한 특징을 활용하여 객체 간의 상속 관계를 동적으로 변경할 수 있다. 프로토타입은 생성자 함수 또는 인스턴스에 의해 교체 가능하다.

3.1 생성자 함수에 의한 프로토타입 교체

  • prototype 프로퍼티를 이용한 교체

    ```js
    const Person = (function(){
        function Person(name){
            this.name = name
        }
        Person.prototype = {
            sayHello(){
                console.log(`my name is ${this.name}`)
            }
        }
        return Person;
    }())
    
    const me = new Person('lee')
    me.sayHello()
    ```

    위와 같이 prototype에 직접 객체 리터럴을 넣는 방식이 있다.

  • 인스턴스에 의한 프로토타입의 교체

    ```js
    function Person(name){
    this.name = name
    }
    
    const me = new Person('lee')
    
    const parent = {
        hello(){
            console.log(`hello ${this.name}`)
        }
    }
    
    Object.setPrototypeOf(me, parent)
    
    me.hello() // hello lee
    ```

    정리하면 다음과 같다.

즉, parent 객체를 me의 prototype로 교체해준 것이다. 다만, parent 객체의 constructor가 없으므로, constructor는 Object로 맵핑된다.

function Person(name){
    this.name = name
}

const me = new Person('lee')

const parent = {
    hello(){
        console.log(`hello ${this.name}`)
    }
}

Object.setPrototypeOf(me, parent)

console.log(me.constructor === Person) // false
console.log(me.constructor === Object) // true

이는 contructor 프로퍼티가 없으므로 constructor 프로퍼티와 생성자 함수 간의 연결이 파괴된 것이다.
따라서, 프로토타입 교체를 통해 객체 간의 상속 관계를 동적으로 변경하는 것은 꽤나 번거롭다. 따라서 프로토타입을 직접 교체하는 것은 좋은 방법이 아니다.

뒤에 배울 직접 상속을 이용하고, 또는 ES6에서 도입된 클래스를 사용하면 간편하고 직관적으로 상속관계를 구현할 수 있다.

4. instanceof 연산자

instanceof 연산자는 이항 연산자로서 좌변에 객체를 가리키는 식별자, 우변에 생성자 함수를 가리키는 식별자를 피연산자로 받는다. 만약 우변의 피연산자가 함수가 아닌 경우 TypeError가 발생한다.

객체 instanceof 생성자 함수

우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변의 객체의 프로토타입 체인 상에 존재하면 true로 평가되고, 그렇지 않은 경우에는 false로 평가된다.

function Person(name){
    this.name = name
}

const me = new Person('lee')
console.log(me instanceof Person) // true
console.log(me instanceof Object) // true

Person은 바로 생성자 함수이기 때문에 true이고, Object는 프로토타입 체인 상에 존재하므로 true로 평가된다.

instanceof 연산자가 어떻게 동작하는지 이해하기 위해 프로토타입을 교체해보자

function Person(name){
    this.name = name
}

const me = new Person('lee')
const parent = {}
Object.setPrototypeOf(me, parent)

console.log(Person.prototype === parent) // false
console.log(parent.constructor === Person) // false

console.log(me instanceof Person) // false
console.log(me instanceof Object) // true

Object는 me 프로토타입 체인 상에서 존재한다. 그러나 me의 프로토타입 체인 상에서 Person은 더이상 존재하지않는다. 즉, Person 생성자 함수는 프로토타입 체인 상에 존재하지 않는 것이다. 따라서 교체한 parent 객체를 Person 생성자 함수의 Prototype 프로퍼티에 바인딩하면 true로 바뀌게 할 수는 있다.

그러나 instanceof 에는 한가지 문제가 있는데, 프로토타입의 constructor 프로퍼티가 가리키는 생성자 함수를 찾는 것이 아니라, 생성자 함수의 prototype에 바인딩된 객체가 프로토타입 테인 상에 존재하는 지 확인하는 것이다 이게 무슨 말인가하면 아래의 예제를 보자

function Person(name){
    this.name = name
}

const me = new Person('lee')
const parent = {}
Object.setPrototypeOf(me, parent)

console.log(Person.prototype === parent) // false
console.log(parent.constructor === Person) // false

Person.prototype = parent

console.log(me instanceof Person) // true
console.log(me instanceof Object) // true

console.log(me instanceof Person) // true 가 좀 충격적인데, 분명 me 인스턴스의 prototype을 parent로 바꾸었다. 그런데 왜 Person이 true로 나왔을가??

이는 Person.prototype = parent 때문이다. Person의 prototype을 parent로 선언해주면 Person의 생성자 함수의 prototype은 parent로 바인딩된다.

위에서 instanceof 연산자의 구동 방식이 프로토타입으로 올라가서 생성자 함수를 확인하는 것이 아니라, 생성자 함수를 확인하고 프토토타입으로 나아가는 방식이라고 했다. 즉, me 인스턴스의 prototype은 parent이지만 Person이 생성자 함수이기 때문에, Person 생성자 함수로 가고, Person 생성자 함수에서 프로퍼티인 prototype을 통해 parent로 간다.

이 후에는 parent로 가고, 프로토타입 체인에 따라 parent의 생성자 함수가 누구인지 확인한다. parent의 생성자 함수는 Object 이므로, Object가 true인 것이다.

그림으로 정리하면 다음과 같다.
따라서 instanceof는 생성자 함수의 prototype 프로퍼티와 프로토타입 간의 연결이 파괴되었는 지는 상관하지 않으므로 이들 간의 연결이 파괴되지 않도록 조심해야한다.

0개의 댓글