[JS] 프로토타입 체인, 오버라이딩, 프로퍼티 섀도잉

sehannnnnnn·2021년 9월 2일
0
post-thumbnail

프로토타입 체인

프로토타입은 상속을 위한 존재로써 어떠한 인스턴스로 부터 부모,자식 격의 계층 구조가 존재한다.

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

Person.prototype.sayHello = function() {
	console.log('hi! my name is ${this.name}');
    }

const me = new Person('Lee');

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

예시 속 Person 생성자 함수에 의해 생성된 me 객체는 hasOwnProperty라는 메서드를 호출 할 수 있는데, 이는 me 객체가 Person.prototype 뿐만 아닌 Object.prototype도 상속받았다는 것을 의미한다.

me 라는 객체는 상위 프로토타입은 Person.prototype과 연결되어 있으며 Person.prototype 객체는 더 상위 객체인 Object.prototype과 연결되어 있다.

하위 객체에서 어떠한 메서드를 불러올 때 만약, 해당 객체에 프로퍼티가 없다면 [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다. (예시 속 hasOwnProperty를 불러온 것 처럼)
프로토타입 체인은 자바스크립트가 객체지향 프로그래밍에 상속을 구현하는 메커니즘이다.

예시 상황) me.hasOwnProperty('name');

  1. me 객체 속엔 hasOwnPropery 메서드가 없으므로 [[Prototype]] 내부 슬롯에 바인딩된 걸 따라 Person.prototype로 이동하여 메서드가 있는지 확인한다.
  2. Person.prototype에도 없으니 그 상위인 Object.prototype으로 이동하여 메서드를 검색한다.
  3. Object.prototype 에는 hasOwnProperty가 존재함으로 이를 호출하고 이때, Object.prototype.hasOwnProrperty 메서드의 this에는 me 객체가 바인딩 된다.

Object.prototype은 프로토타입 체인의 종점으로 이에 [[Prototype]] 슬롯은 null이다. 프로토타입 체인에 종점에서도 메서드를 찾을 수 없는 경우 error가 발생하지 않는다. (undefine 반환)

이에 반해 프로퍼티가 아닌 식별자는 스코프 체인에서 검색하는데, 이는 함수의 중첩관계로 이루어진 구조이다. me 식별자가 어디서 선언되었는지를 보고 해당 스코프에서 식별자를 검색한다. 식별자를 찾으면 해당 메서드를 프로토타입 체인을 통해 검색한다.

스코프 체인과 프로토타입 체인은 서로 연관없이 별도 동작이 아닌 서로 협력하여 식별자와 프로퍼티를 검색하는데 사용된다.


오버라이딩, 프로퍼티 섀도잉

const Person = (function () {
    //생성자 함수
    function Person(name) {
    	this.name = name;
    }
    
    //프로토타입 메서드
    Person.prototype.sayHello = function () {
   	console.log('hi! my name is ${this.name}`);
     };
     
     //생성자 함수 반환
     return Person
}());

//인스턴스 생성
const me = new Person('Lee');

//인스턴스 프로퍼티(메서드) 오버라이딩(재정의)
me.sayHello = function() {
     console.log('Hey! my name is ${this.name}');
};

me.sayHello(); // Hey! my name is Lee

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

프로토타입 프로퍼티가 가진 프로퍼티를 인스턴스에 추가하면 프로토타입 체인을 따라 해당 메서드를 검색할 때, 프로토타입의 프로퍼티까지 접근하지 못하고 인스턴스 프로퍼티를 찾아 반환하게 된다.

예시 속 인스턴스 메서드 sayHello는 프로토타입 메서드의 sayHello 메서드를 오버라이딩 했다 하며, 이러한 상속관계에 의해 프로퍼티가 가려지는 현상을 프로퍼티 섀도잉이라 한다.

프로퍼티를 삭제하는 경우

sayHello 메서드를 삭제해보자.

delete me.sayHello;

me.sayHello(); //hi! my name is Lee

프로토타입 메서드가 아닌 인스턴스 메서드가 삭제되면서 오버라이딩을 통해 가려졌던 Person.prototype속 sayHello 메서드가 호출된다.

다시 한번 sayHello메서드를 삭제해보면

delete me.sayHello;

me.sayHello(); //hi! my name is Lee

인스턴스 메서드를 삭제하는 방법으로 프로토타입의 프로퍼티(메서드를) 삭제하는 것은 불가능하다. 다시 말해 하위 객체를 통해 프로토타입에 get은 혀용되나 set 엑세스는 허용되지 않는다. 이 경우 프로토타입에 직접 접근해야한다.

Person.prototype.sayHello = function() {
   console.log('Hey! My name is ${this.name}.');
};

me.sayHello(); // Hey! My name is Lee.

delete Person.prototype.sayHello;
me.sayHello(); //TypeError: me.sayHello is not a function

프로토타입의 교체

프로토타입은 임의의 다른 객체로 변경 할 수 있다. 자신의 부모 객체를 동적으로 변경할 수 있으며, 객체 간의 상속관계를 변경할 수 있다. 생성자함수, 인스턴스에 의해 교체된다.

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

const Person = (function () {
    //생성자 함수
    function Person(name) {
    	this.name = name;
    }
    
    //Person 의 프로토타입을 따로 정의하고 지정
    Person.prototype = {
    	sayHello() {
        	console.log('hi! my name is ${this.name}');
        }
    };
    
    return Person;
}());

const me = new Person('Lee');

위 예시에서는 Person.prototype에 sayHello() 라는 메서드를 가진 객체 리터럴을 할당하였다. 기존에 생성자함수에 의해 자연스럽게 할당되는 Person.prototype 객체를 임의로 교체한 것이다.

프로토타입으로 교체한 객체리터럴에는 constructor 프로퍼티가 없다, 왜냐하면 생성자함수가 있어야하는데 리터럴로 선언되어 기존의 생성자함수 Person을 잃어버렸기 때문에.

따라서 me 객체의 constructor를 통해 생성자함수를 검색하면 프로토타입 체인에 의해 Person이 아닌 Object가 나온다.

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

이는 일반적인 constructor 프로퍼티와 생성자 함수간의 연결을 파괴시킨다. 이를 되살리려면 constructor 프로퍼티를 코드내의 추가하여 constructor 프로퍼티를 되살려야한다.

const Person = (function () {
    //생성자 함수
    function Person(name) {
    	this.name = name;
    }
    
    //Person 의 프로토타입을 따로 정의하고 지정
    Person.prototype = {
    	constructor : Person, //여기 주목!
    	sayHello() {
        	console.log('hi! my name is ${this.name}');
        }
    };
    
    return Person;
}());

const me = new Person('Lee');

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

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

이는 인스턴스에 __proto__ (또는 setPrototypeOf)접근자 프로퍼티를 통해 프로토타입을 교체하는 방법이다.

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

const me = new Person('Lee');

const parent = {
	sayHello() {
       console.log('Hi! my name is ${this.name}');
    }
 };

//me 객체의 프로토타입을 parent 객체로 교체한다.
Object.setPrototypeOf(me, parent);

me.sayHello(); //Hi! my name is Lee

이 방법 또한 인스턴스의 생성자 함수에 따라 지정된 프로토타입이 아니기 때문에 constructor 프로퍼티가 비어있다. 따라서 parent라는 프로토타입이 될 객체 내부에 constructor 프로퍼티와 생성자 함수간의 연결을 설정해주어야 정상적인 프로토타입 체인이 성립된다.

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

const me = new Person('Lee');

const parent = {
	constructor: Person, //여기 주목!!
	sayHello() {
       console.log('Hi! my name is ${this.name}');
    }
 };

//me 객체의 프로토타입을 parent 객체로 교체한다.
Object.setPrototypeOf(me, parent);

me.sayHello(); //Hi! my name is Lee

이처럼 프로토타입 교체를 통해 객체간의 상속관계를 동적으로 변경하는 것은 꽤나 번거롭다. 따라서 프로토타입을 직접 교체하지 않는 것이 좋다. 상속관계를 인위적으로 설정하려면 다른 방법을 이용하는 것이 더 편리하고 안전하다!

profile
FE 개발자 준비생 블로그 🪐

0개의 댓글