ProtoType pt.2

심현인·2021년 7월 11일
0
post-custom-banner

프로토타입 체인

메소드 오버라이드

prototype 객체를 참조하는 __proto__를 생략하면 인스턴스의 정의된 프로퍼티나 메소드를 마치 자신의 것 처럼 사용할 수 있다. 근데 만약 동일한 이름의 프로퍼티 혹은 메소드가 있는 상황이라면 어떨까?

var Person = function(name){
	this.name = name;
}
Person.prototype.getName = function(){
	return this.name
}

var hi = new Person('감자');
hi.getName = function(){
	return '나는' + this.name;
}

console.log(hi.getName()) //나는 감자

hi.__proto__.getName이 아닌 hi 객체에 있는 getName 메소드가 호출됐다. 당연한 결과라고 생각할 수있지만 간혹 헷갈릴 수 있따. 여기서 일어난 현상을 메소드 오버라이드 이라고 한다. 메소드 위세 메소드를 덮어씌었다는 의미이다. 원본을 제거하고 다른 대상으로 교체하는 것이 아니라 원본이 그대로 있는 상태에서 다른 대상을 그 위에 얹는 것으로 이해하면 된다.
JS엔진이 getName이라는 메소드를 찾는 방식은 가장 가까운 대상인 자신의 프로퍼티를 검색하고, 없으면 그 다음으로 가까운 대상인 __proto__를 검색하는 순서로 진행된다. 따라서 __proto__에 있는 메소드는 자신에게 있는 메소드보다 검색 순서에서 밀려서 호출되지 않았던 것이다. 앞에서 교체가 아니라 얹는 것이라고 한 이유는 교체라고 하면 원본에는 접근 할 수 없어야하는데 접근 할 수 있는 방법이 있다. 그렇다면 메소드 오버라이딩이 일어나는 상황에서 prototype에 있는 메소드에는 어떻게 접근할까?

console.log(hi.__proto__.getName()) //undefined

여기선 undefined가 호출되었다. 그 이유는 this가 가르키는 대상(hi.__proto__)이 잘못되었기 때문이다.

Person.prototype.name = '심감자'
console.log(hi.__proto__.getName()) //심감자

이 코드에서 보면 원하는 메소드가 호출되고 있다는게 확실해졌다. 다만 this가 prototype을 바라보고 있는데 이걸 인스턴스를 바라보게 바꿔줘야겠다.

console.log(hi.__proto__.getName.call(hi)) //심감자

드디어 성공이다! 즉 일반적으로 메소드가 오버라이드된 경우에는 자신으로부터 가장 가까운 메소드에만 접근할 수 있지만, 그 다음으로 가까운 __proto__의 메소드도 우회적인 방법이긴하지만 접근이 불가능 한 것이 아니다.

프로토타입 체인

설명하기 앞서 이 객체의 구조를 살펴보자

console.dir({a:1})


첫 줄을 통해 Object의 인스턴스임을 알 수 있고, 프로퍼티 a의 값이 1, __proto__내부에는 hasOwnProperty, toString등이 있다.

이번엔 배열의 구조를 보자

console.dir([1,2])


여기서도 첫 줄에 Array의 인스턴스임을 알 수 있고,__proto__내부에 낯익은 pop, push, map등 익숙한 배열 매소드와 constructor가 있다. 근데 사진에서는 짤렸지만 맨 밑에 __proto__가 또 등장한다. 그 이유는 바로 prototype객체가 '객체'이기 때문이다. 기본적으로 모든 객체의 __proto__에는 Object.protype이 연걀된다. prototype객체도 예외가 아니다.
앞에서 __proto__는 생략가능 하다고 했다. 따라서 Object.prototype 내부의 메소드도 자신의 것처럼 실행할 수 있다. 생략 가능한 __proto__를 한 번 더 따라가면 Object.prototype를 참조 할 수 있기 때문이다!

let arr = [1,2]
arr(.__proto__).push(3)
arr(.__proto__)(.__proto__).hasOwnProperty(2) // true

어떤 데이터의 __proto__프로퍼티 내부에서 다시 __proto__프로퍼티가 연쇄적으로 이어진 것을 프로토타입 체인이라고 하고, 이 체인을 따라가면서 검색하는 것을 프로토타입 체이닝이라고 한다.
프로토타입 체이닝은 앞서 소개한 메소드 오버라이드와 동일한 맥락이다. 어떤 메소드를 호출하면 JS엔진은 데이터 자신의 프로퍼티를 검색해서 원하는 메소드가 있으면 그 메소드를 실행하고, 없으면 __proto__를 검색해서 있으면 그 메소드를 실행하고 없으면 다시 __proto__를 검색해서 실행하는 식으로 진행한다.

let arr = [1,2]
Array.prototype.toString.call(arr); //"1,2"
Object.prototype.toString.call(arr) //[object Array]
arr.toString() // "1,2"

arr.toString = function(){
	return this.join('-')
}

arr.toString() // 1_2

arr은 변수이므로 arr.__proto__는 Array.prototype -> Object.prototype 순으로 참조를 할 것이다. toString이라는 메소드는 Array, Object에도 있따. 이 둘 중 어떤 값이 출력되는 지 확인하기 위해 Array, Object의 각 프로토타입에 있는 toString 메소드를 arr에 적용했을 때 출력값을 확인해봤따. 4번째 줄에서 실행 시킨 것은 Array.prototype.toString을 적용한 결과가 나왔고 6번째 줄에서 toString을 직접 적용한 것을 출력했을 때 4번째 줄에서 한 결과가 같았다. 9번째 줄에서는 Array.prototype.toString이 아니라 arr.toString이 바로 실행될 것이다.

객체 전용 메소드의 예외사항

어떤 생성자 함수이든 prototype은 반드시 객체이기 때문에 Object.prototype이 언제나 프로토타입 체인의 최상단에 존재하게 된다. 따라서 객체에서만 사용할 메소드는 다른 여느 데이터 타입처럼 프로토타입 객체안에 정의할 수가 없다. 객체에서만 사용할 메소드를 Object.prototype 내부에 정의한다면 다른 데이터 타입도 해당 메소드를 사용할 수 있기 때문이다.

Object.prototype.getEntries = function(){
  var res = [];
  for(let prop in this){
    if(this.hasOwnProperty(prop)){
      res.push([prop, this[prop]])
    }
  }
  return res;
}

let data = [
  ['object', {a:1, b:2, c:3}], //[["a",1], ["b",1], ["c",1]]
  ['number', 345], // []
  ['string','abc'], //[["0","a"], ["1","b"], ["2","c"]]
  ['boolean', false], // []
  ['function', function(){}], // []
  ['array', [1,2,3]]// [["0",1], ["1",2], ["2",3]]
]
data.forEach(function(datum){
  console.log(datum[1].getEntries())
})

객체에서만 사용할 getEntries라는 메소드를 만들었다. data에 있는 요소를 forEach문으로 모두 돌려보니, 모든 데이터가 오류 없이 결과를 반환하고 있다. 원래 의도라면 객체가 아닌 다른 데이터타입에는 오류가 나와야할텐데 어떤 데이터 타입이건 무조건 프로토타입 체이닝을 통해 getEntries 메소드에 접근할 수 있으니 그렇게 동작하지 않는 것이다.

이 같은 이유로 객체만을 대상으로 동작하는 객체 전용 메소들은 부득이 Object.prototype이 아닌 Object에 스태틱 메소드로 부여할 수밖에 없었다. 도한 생성자 함수인 Object와 인스턴스인 객체 리터럴 사이에는 this를 통한 연결이 불가능하기 때문에 여느 전용 메소드처럼 '메소드명 앞의 대상이 곧 this'가 되는 방식 대신 this의 사용을 포기하고 대상 인스턴스를 인자로 직접 주입해야하는 방식으로 구현돼있다.
만약 객체 전용 메소드들에 대해서도 다른 데이터 타입과 마찬가지의 규칙을 적용할 수 있다면, 예를들어 Object.freeze(instance)의 경우 instance.freeze()처럼 표현 할 수 있었을 것이다.
다시 말하면 instance.__proto__(생성자 함수의 prototype)에 freeze라는 메소드가 있고, 또한 앞에서 소개한 Object.getPrototypeOf(instance)의 경우에도 instance.getProptype()정도로 충분했을 것이다. 객체 한정 메소드들을 Object.prototype이 아닌 Object에 직접 부여할 수 밖에 없었던 이유를 다시 강조하자며느 Object.prototype이 여타의 참조형 데이터뿐 아니라 기본형 데이터조차 __proto__에 반복 접근함으로써 도달할 수 있는 최상위 존재이기 떄문이다.
반대로 같은 이유에서 Object.prototype에는 어떤 데이터에서도 활용할 수 있는 범용적인 메소드들만 있다. toString, hasOwnProperty, valuOf 등은 모든 변수가 마치 자신의 메소드인 것처럼 호출할 수 있다.

/*
'프로토타입 체인상 가장 마지막에는 언제나 Object.prototype이 있다'고 했지만 예외적으로
Object.create를 이용하면 Object.prototype에 접근 할 수 없는 경우가 있다. 
Object.create(null)은 __proto__가 없는 객체를 생성한다..!
*/

var _proto = Object.create(null);
_proto.getValue = function(key){ return this[key] }
var obj = Object.create(_proto)
obj.a = 1
console.log(obj.getValue('a')) //a
console.dir(obj)

//_proto에는 __proto__가 없는 객체를 할당했다.
// 다시 obj는 앞서 만든 _proto를 __proto__로 하는 객체를 할당했다.
// 이제 obj를 출력하면, __proto__에는 오직 getValue메소드만 존재하고, 
// __proto__및 constructor 프로퍼티등은 보이지 않는다.
// 이 방식으로 만든 객체는 일반적인 데이터에서 반드시 존재하는 내장 메소드 및 프로퍼티가 제거됨으로써
// 기본 기능에 제약이 생긴 대신, 객체 자체 무게가 가벼워짐으로써 성능상 이점을 가진다.

다중 프로토타입 체인

JS의 기본 내장 데이터 타입들은 모두 프로토타입 체인이 1단계이거나 2단계에서 끝나는 경우가 있었지만, 유저가 새롭게 만드는 경우에는 그 이상도 얼마든지 가능하다. 대각선의 __proto__를 연결해나가기면 하면 무한대로 체인관계를 이어나갈 수 있는데, 이 방법으로 다른 언어의 클래스와 비슷하게 동작하는 구조를 만들 수 있다.

대각선의 __proto__를 연결하는 방법은 __proto__가 가리키는 대상, 즉 생성자 함수의 prototype이 연결하고자 하는 상위 생성자 함수의 인스턴스를 바라보게끔 해주면 된다!

let Grade = function(){
let args = Array.prototype.slice.call(arguments)
for(let i = 0 ; i < args.length; i++){
    this[i] = args[i]
  }
  this.length = args.length
}

let g = new Grade(100, 100)

변수 g는 Grade의 인스턴스를 바라본다. Grade의 인스턴스는 여러 개의 인자를 받아 각각 순서대로 인덱싱해서 저장하고 length 프로퍼티가 존재하는 등으로 배열의 형태를 지니지만, 배열의 메소드를 사용할 수 없는 유사배열객체다. 유사배열객체에 배열 메소드를 적용하려면 call/apply를 사용하면 되지만, 이번에는 기왕 생성자 함수를 직접 만든 김에 인스턴스에서 배열 메소드를 직접 쓸 수 있게끔 하겠다. 그러기 위해서는 g.__proto__ 즉, Grade.prototype이 배열의 인스턴스를 바라보게 하면 된다.

Grade.prototype = []

이렇게 하면 인스턴스인 g에서 직접 배열의 메소드를 이용할 수 있다!

console.log(g) //Grade(2)[100, 100]
g.pop()
console.log(g) //Grade(1)[100]
g.push(90)
console.log(g) //Grade(2)[100, 90]

g 인스턴스의 입장에서는 프로토타입 체인에 따라 g객체 자신이 지니는 맴버, Grade의 prototype에 있는 멤버, Array.prototype에 있는 멤버, 끝으로 Object.prototype에 있는 멤버까지 접근할 수 있게됐다.

profile
가로
post-custom-banner

0개의 댓글