prototype과 __proto__ (야매 주의)
이번 강의는 잘 이해를 못 했지만 최대한 이해한 부분을 적어봤다.
적으면서 이해가 안되거나 궁금한 부분은 다시 알아봐야겠다. 🥹
프로토타입은 어떤 객체의 속성이나 메서드를 정의하는 템플릿? 모음집? 같은 녀석이라고 이해했다.
아래 코드를 보면 Person이라는 생성자 함수를 만들었고, 이 함수의 프로토타입에 sayHello라는 메서드를 추가했다. 이로써 생성자 함수 Person을 사용해 만든 객체들은 모두 sayHello() 메서드를 공유하게 된다. 그래서 person1, person2들은 Person의 프로토타입을 상속 받아서 sayHello() 메서드를 사용하는 것을 볼 수 있다.
// 생성자 함수 정의
function Person(name) {
this.name = name;
}
// Person 생성자 함수의 prototype에 메서드 추가
Person.prototype.sayHello = function() {
console.log(`안녕하세요, 저는 ${this.name}입니다.`);
};
// Person 생성자 함수를 사용하여 객체 생성
const person1 = new Person('Alice');
const person2 = new Person('Bob');
// 객체의 프로토타입 체인 확인
console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true
// 프로토타입 체인을 통한 메서드 호출
person1.sayHello(); // 안녕하세요, 저는 Alice입니다.
person2.sayHello(); // 안녕하세요, 저는 Bob입니다.
콘솔에 둘을 찍어보면 prototype의 유무를 확인할 수 있다.
부모격 되는 Person은 prototype을 갖고 있지만 Person으로 생성된 객체 person1에는 이게 없다.
person1을 자세히 보면 __proto__에 추가된 sayHello() 메서드와 생성자가 찍혀 있다.
__proto__는 모든 객체에 존재하는 내부 속성으로 객체의 프로토타입을 가리킨다.
객체가 생성될 때 해당 객체의 __proto__는 생성자 함수의 prototype 속성을 가리키게 된다.
이 __proto__를 따라 올라가면 상위 프로토타입을 찾을 수 있게 된다.
캡쳐할 때 모든 객체에 __proto__가 존재한다는데 왜 Person에는 안 보이는지 확인해봤다.
있긴 있다.
저게 뭔지 또 검색해봤다.
Person 생성자 함수의 __proto__ 속성이 Function.prototype을 가리킨다는 뜻이다.
그건 JS 내장 함수인 함수의 프로토타입 객체인데, Function은 모든 함수의 생성자 함수며, 모든 함수 객체가 상속받는 프로토 타입이다. Person 생성자 함수 또한 Function.prototype을 상속 받았다는 표시다. (이 Function.prototype은 또 Object.prototype을 상속 받고...)
결론적으로 prototype으로 뭔가 속성되는 걸 알 수 있다. 까지만 이해하고 넘어간다.
prototype과 __proto__의 메서드 호출 방법
prototype에 연결된 메소드를 호출해보자.
함수이자 객체인 Book에 getPoint라는 메서드를 추가했다. 이 메서드는 호출한 대상과 Book.prototype이 같은지 다른지 불리언 값으로 반환하고, 호출한 대상의 point 값을 반환한다.
Book.prototype.getPoint()를 실행하면 getPoint()에서 this가 Book.prototype을 참조한다.
즉 console.log에는 두 값이 같기 때문에 true가 반환된다. 반면 return this.point에서는 Book.prototype.point이므로 이 값은 없어서 undefined가 반환된다.
call 메서드는 파라미터를 this의 객체로 사용한다.
마지막 줄을 실행하면 this는 Book이 되는데, Book과 Book.prototype을 비교했을 때에는 같지 않기 때문에 false를 반환한다. 또한 Book에는 point 속성이 없기 때문에 undefined가 출력된다.
function Book() {
this.point = 100;
};
Book.prototype.getPoint = function() {
console.log(Object.is(this, Book.prototype));
return this.point
};
console.log(Book.prototype.getPoint());
console.log(Book.prototype.getPoint.call(Book))
올바른 point 값을 반환하기 위해 this.point를 참조하려면, 인스턴스를 생성하고 그 인스턴스의 메서드를 호출해야 된다.
function Book() {
this.point = 100;
};
Book.prototype.getPoint = function() {
return this.point;
};
const obj = new Book();
console.log(obj.getPoint())
obj라는 인스턴스를 생성해서 getPoint에 접근하면 정상적으로 100이 출력된다.
obj는 Book의 프로토타입을 상속 받아서 getPoint라는 메서드를 갖고 있고, point:100도 갖고 있다.
이해가 안돼서 하나하나 콘솔에 찍어보면서 확인했다. 🥹
인스턴스에 함수 추가하기
new 연산자로 인스턴스를 생성하고, 인스턴스의 프로퍼티로 함수를 추가한다.
이 때 다른 인스턴스에는 해당 함수가 포함되어 있지 않으므로 공유할 수 없다.
아래 코드의 obj는 생성자 함수 Book으로 만든 인스턴스다.
생성할 때만 해도 obj는 point: 100을 갖고 있고, getPoint() 메서드를 갖고 있다.
그러나 obj에 setPoint라는 함수를 추가했다. 이 함수는 호출자의 point를 param 값으로 변경한다. obj.setPoint(200)으로 파라미터에 200이 들어왔으니, 이제 obj는 setPoint()를 갖고 있고 point도 200으로 업데이트 된다.
function Book() {
this.point = 100;
};
Book.prototype.getPoint = function() {
return this.point;
};
const obj = new Book();
obj.setPoint = function (param) {
this.point = param;
};
obj.setPoint(200);
console.log(obj.getPoint()) // 200
const newObj = new Book();
console.log(newObj.setPoint) // undefined
하지만 Book으로 만든 또 다른 인스턴스 newObj는 obj처럼 setPoint가 별도로 추가되지 않았기 때문에 undefined가 뜨는 것이다.
이렇게 다르게 생겼다는 것을 확인할 수 있다.
뭔가 다 비슷비슷한 말 같아서 이해가 안될 때 콘솔에 찍어보면 바로 알 수 있다.
__proto__에 메소드 추가하기
__proto__에 함수를 추가하면 prototype에 설정된다. (=prototype에 메소드 추가하는 것과 같음)
prototype에 설정하게 되면 앞으로 만들 인스턴스에도, 현재 만든 인스턴스에도 반영이 된다.
function Book(param) {
this.point = param;
};
Book.prototype.getPoint = function() {
return this.point;
};
const obj = new Book(100);
obj.__proto__.setPoint = function(param) {
this.point = param;
};
obj.__proto__.setPoint처럼 메소드를 직접 추가했다.
Book도 찍어보니 원래는 prototype에 getPoint만 있었는데, setPoint()가 추가됐다!
이런 걸 공유, prototype sharing이라고 한다.
공유를 다시 한 번 더 확인해보자.
function Book(param) {
this.point = param;
};
Book.prototype.getPoint = function () {
return this.point;
};
const obj = new Book(100);
const beforeObj = new Book(100);
obj.__proto__.setPoint = function(param) {
this.point = param;
}
const afterObj = new Book(300);
beforeObj.setPoint(700)
beforeObj의 경우 obj에 setPoint를 추가하기 전에 선언했고, afterObj는 추가한 후에 선언했다.
당연하게도 obj, beforeObj, afterObj 모두 getPoint, setPoint를 갖고 있음이 확인됐다.
결론적으로 __proto__에 함수를 추가하면 이는 곧 prototype에 메소드를 추가하는 것이 되며, 이렇게 되면 공유가 이뤄진다.
(어렵구나!)
Object.setPrototypeOf() : 인스턴스 사용
첫 번째 파라미터로는 오브젝트 또는 인스턴스가 들어가고, 두 번째 파라미터에는 오브젝트의 prototype 또는 null이 들어간다.
어려워 보이니 일단 적용부터 해본다.
let obj1 = {0: 10, length: 1};
Object.setPrototypeOf(obj1, Array.prototype)
이런 게 나왔다.
강사님은 처음 선언한 obj이 인스턴스라고 말씀하셨다.
생성자가 없는데 이게 왜 인스턴스일까? 콘솔에 또 찍어봤다.
prototype이 없다!
오브젝트 리터럴({})로 오브젝트를 만들면 빌트인 오브젝트의 prototype에 연결되어 있는 메서드로 인스턴스를 만들기 때문이다! (허걱)
아무튼 돌아와서... 현재 obj1은 Array-like 오브젝트라는 걸 알아야 한다.
인덱스를 키로 값과 쌍을 짓고 있고 length가 표시되어 있기 때문이다.
Object.setPrototypeOf(obj1, Array.prototype)
위와 같이 obj1의 prototype, __proto__에 Array.prototype을 설정했다.
그리고 console.dir로 확인해보니 Object였던 obj1이 Array의 모습을 띄고 있음을 확인했다.
즉 obj1은 이제 Array의 메서드들을 사용할 수 있게 된 것이다!
(실제로 이렇게 쓰진 않고 설명을 위해 쓰셨다고 한다.)
(++ES5에는 getPrototypeOf()만 있고 set은 없었는데... 이제 생겼다고 한다. 강사님이 왜 set은 없는지 엄청 고민하셨는데 __proto__를 ES6에서 다뤘기 때문에 이제 생긴 것 같다고 추측하셨다. 근데 엄청 고민하셨다는 부분에서 목소리가 풀이 죽으셨다... 정말 자바스크립트를 사랑하시는 개발자시구나...)
Object.setPrototypeOf() : prototype 사용
위의 사용법을 토대로 첫 번째 파라미터의 prototype이 두 번째 파라미터의 prototype으로 설정되는구나! 라고 생각했으나...
function Book() {};
Book.prototype.getBook = function() {};
function Point() {};
Point.prototype.getPoint = function() {};
Object.setPrototypeOf(Point.prototype, Book.prototype);
const obj = new Point(300);
prototype을 확인했는데 getBook이 없다.
해당 prototype을 설정하면 거기에 있는 메서드도 같이 와야 하는데...?
[[Prototype]]을 클릭해보니 여기에 getBook이 위치해 있다.
set이라서 뭔가 prototype에 설정하는 것 같지만 prototype에 __proto__를 만들고 여기에 설정하는 것이다.
prototype에 설정하면 getPoint()가 지워지면서 point에 작성된 메서드들을 사용할 수 없게 된다.
이를 피하기 위해서 __proto__를 만들어서 설정한 것이다!
(구조적으로 계층을 만들어 설정했기 때문에 같은 이름의 메서드가 있어도 대체되지 않는다.
엔진이 식별자를 해결할 때 __proto__순서로 검색하므로 같은 이름의 메서드가 있을 때 앞의 메서드가 호출되기 때문에, getPoint()가 먼저 사용된다고 한다.)
Point로 만든 obj 역시 동일한 것을 확인할 수 있다.
__proto__로 구조를 만들어 확장한다고 보면 된다.
(+상속이 목적이라면 super 처럼 상속 처리 키워드를 제공하는 Class를 사용하는 게 좋다. 이게 뭔지 모른다.)
'👋🏻 JavaScript > 📖 자바스크립트 ES6+' 카테고리의 다른 글
[JS] Array 오브젝트의 배열 엘리먼트 복사와 Generic을 알아보자 (0) | 2023.06.21 |
---|---|
[JS] 템플릿 리터럴(Template Literal)을 알아보자 (0) | 2023.06.21 |
[JS] Object 변환에 대해 알아보자 (0) | 2023.06.20 |
[JS] Object 오브젝트 중 is 메서드와 복사를 알아보자 (0) | 2023.06.20 |
[JS] String 오브젝트 중 다양한 메서드를 알아보자 (0) | 2023.06.20 |