DeepDive에서 예시 코드를 보면
function Person(name){
this.name = name;
}
Person.prototype.sayHello = function () {
console.log(`Hi! My name is ${this.name}.`);
}
와 같이 Person 생성자 함수 내부에서 prototype의 메서드를 정의하지 않고 외부에서 정하고 있다. 나는 이러한 방식으로 코드를 작성하는 이유가 납득이 잘 가지 않았고 내부에서 작성하는 형태를 시도했고, 그러한 방식으로도 문제없이 작동하였다. ( 프로토타입 교체의 경우 예외 )
하지만 생성자 함수에서 prototype 메서드를 정의하게되면, 해당 생성자 함수를 호출할때마다, 즉, 새로운 인스턴스를 생성할때마다 해당 메서드의 함수 정의가 계속해서 일어나고, 이는 효율적이라고 볼수는 없다. 이러한 이유로, prototype 메서드의 정의는 생성자 함수 내부가 아닌, 외부에서 진행하는것이 적합하다.
위와 동일한 생각으로, 나는 프로토타입의 교체또한 생성자 함수 내부에서 진행하는것을 시도했고, 다음과 같이 해당 생성자 함수의 인스턴스는 교체된 프로토타입을 제대로 추적하지 못했다.
// DeepDive p291 ex19.40 변형된 코드
function Person(name) {
this.name = name;
Person.prototype = {
sayHello() {
console.log('hi');
}
}
}
const person = new Person('alang');
person.sayHello() // Error! person.sayHello is not a function
// 왜 에러가 날까?
해당 이유를 알기위해 필요한 사실이 있다.
생성자 함수로서 호출할 수 있는 함수가 평가되는 시점에, 함수 객체가 생성되고, 이때 해당 함수의 프로토타입도 더불어 생성된다.
console.log(Person.prototype); // {constructor: f}
function Person(name) {
this.name = name;
}
생성자 함수가 new 키워드가 호출될때( 즉, 인스턴스가 생길때 ), 해당 생성자 함수 내부 코드가 평가되고, 이때 빈 인스턴스 객체가 생겨나고, 해당 생성자 함수의 프로토타입이 바인딩 된다.
객체가 담겨있는 식별자에 다른 객체를 재할당 할경우, 해당 식별자는 기존의 객체 참조값에 대한 참조가 끊어진다.
let a = {};
let b = a;
a = {};
a === b // false
위 3가지 사실을 생각하고 다시 코드를 바라보게 되면 이유가 확실해진다.
Person 생성자 함수가 new 키워드와 함께 호출되게되면, 생성자 함수 평가단계에서 빈 인스턴스 객체가 생성되고, 이때 이미 함수객체가 생성된 시점( 함수의 선언시점, 위의 경우 전역코드의 평가단계 )에 만들어졌던 프로토타입(이 가르키고 있는 객체의 참조값)이 바인딩되고, 이후에 Person.prototype의 객체 교체가 이루어지게 되면서, Person.prototype이 기존의 참조하고있던 객체와의 연결이 끊어지고, sayHello
메소드를 가진 객체와 연결된다. 그렇다고 해서, 생성자 함수 평가단계에서 생겨난 인스턴스 객체와 기존의 프로토타입 객체의 바인딩이 변경되는것이 아니다! 그렇기 때문에 결국, 해당 인스턴스는 해당 메소드를 가지지 않은, 기존 프로토타입과 바인딩된 채이고, 결국 해당 메소드를 찾을수 없게 된다.
아래의 상황또한 이와 유사하다고 볼수 있겠다
function Person(name) {
this.name = name;
}
const person1 = new Person('person1');
const person2 = new Person('person2');
Person.prototype = {
sayHello(){
console.log(`Hi! My name is ${this.name}`);
}
}
const person3 = new Person('person3');
const person4 = new Person('person4');
person1.sayHello // undefined
person2.sayHello // undefined
person3.sayHello // function { console.log(...)}
person4.sayHello // function { console.log(...)}
이러한 것들을 종합적으로 생각했을때, 프로토타입 교체에 대한 감상평은... 교체 하지말자!
메소드 덮어쓰기(overiding)라는 이름만 들었을땐, 마치 꼭 실제로 해당 메서드가 재할당되는것과 같이 생각되어진다. 하지만 그렇지 않다.
생성자 함수와 인스턴스의 관계를 예시로, 프로토타입에 존재하는 동일한 이름의 식별자를 인스턴스의 메소드로 선언하는것은 말그대로 해당 인스턴스 객체의 메소드로 등록되는것이다. 즉, 프로토타입에 등록된 메소드가 할당되는것이아니다. 해당 메소드(프로퍼티)를 검색할때, 프로토타입 체인상에서 먼저 발견되어지는것뿐. 덮어쓰기라는 용어는 단순 보여지는 현상을 일컫는다는점 명심!
for in 문에서 몰랐던 사실, for in 문을 사용할 경우, 대상 객체의 프로퍼티뿐만 아니라 상속받은 프로토타입의 프로퍼티까지 열거( 프로퍼티 어트리뷰트[[Enumerable]]가 true인 프로퍼티 한정 )한다는 사실. 그리고 아마, 대부분의 상황에서는 프로토타입의 프로퍼티까지 열거하는 상황을 원치 않을것이다. 그렇기 때문에, 자신의 프로퍼티만을 열거하기 위해서 Object.prototype.hasOwnProperty
메소드를 통해 꼭 validate를 진행하자!
모던 자바스크립트 DeepDive 19장 프로토타입