4. 함수와 프로토타입 체이닝 #5 프로토타입 체이닝

2021. 11. 4. 16:09JavaScript/basic

이전글

https://yonghwankim-dev.tistory.com/158

 

4. 함수와 프로토타입 체이닝 #4 함수 호출과 this

이전글 https://yonghwankim-dev.tistory.com/70?category=962746 4. 함수와 프로토타입 체이닝 #3 함수의 다양한 형태 본 글은 인사이드 자바스크립트 도서의 내용을 복습하기 위해 작성된 글입니다. 4.3 함수의..

yonghwankim-dev.tistory.com

 

본 글은 INSIDE JAVASCRIPT 도서의 내용을 복습하기 위해 작성된 글입니다.

 

요약 및 정리

1. 프로토타입의 두 가지 의미

  • func1.prototype : 함수 객체의 입장에서 자신(func1)과 연결된 프로토타입 객체(func1.prototype 객체)를 가리키는 프로퍼티입니다.
  • [[Prototype]] or __proto__ 링크 : [[Prototype]] 링크는 객체의 입장에서 자신의 부모 객체인 프로토타입 객체를 내부의 숨겨진 링크로 가리키는 링크를 의미합니다.

2. 객체 리터럴 방식으로 생성된 객체의 프로토타입 체이닝

  • 프로토타입 체이닝 : 자바스크립트에서 특정 객체의 프로퍼티나 메서드에 접근하려고 할 때, 해당 객체에 접근하려는 프로퍼티 또는 메서드가 없다면 [[Prototype]] 링크를 따라 올라가면서 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티 또는 메서드를 차례대로 검색하는 것을 프로토타입 체이닝이라고 합니다.

3. 생성자 함수로 생성된 객체의 프로토타입 체이닝

  • 객체 생성의 기본 원칙 : 자바스크립트에서 모든 객체는 자신을 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체(부모 객체)로 취급합니다.

4. 프로토타입 체이닝의 종점

  • 프로토타입 체이닝의 종점 : Object.prototype 객체

5. 기본 데이터 타입 확장

  • Object.prototype, String.prototype, Number.prototype 등과 같이 표준 빌트인 프로토타입 객체에도 사용자가 직접 정의한 메서드들을 추가하는 것을 허용합니다.

6. 프로토타입도 자바스크립트 객체다

  • 프로토타입 객체 역시 자바스크립트 객체이므로 일반 객체처럼 동적으로 프로퍼티를 추가/삭제하는 것이 가능합니다.

7. 프로토타입 메서드와 this 바인딩

  • 프로토타입 메서드 내부에서 this를 사용한다면 그 메서드를 호출한 객체에 바인딩됩니다.

8. 디폴트 프로토타입은 다른 객체로 변경이 가능하다

  • 자바스크립트에서는 함수를 생성할 때 해당 함수와 연결되는 디폴트 프로토타입 객체를 다른 일반 객체로 변경하는 것이 가능하다.
  • 주의점 : 생성자 함수의 프로토타입 객체가 변경되면, 변경된 시점 이후에 생성된 객체들은 변경된 프로토타입 객체로 [[Prototype]] 링크를 연결합니다. 변경되기 이전에 생성된 객체들은 기존 프로토타입 객체로의 [[Prototype]] 링크를 그대로 유지합니다.

9. 객체의 프로퍼티 읽기나 메서드를 실행할 때만 프로토타입 체이닝이 동작한다

  • 프로토타입 체이닝 발생 시점 : 객체의 특정 프로퍼티를 읽으려고 할때 해당 객체에 없는 경우 / 객체의 특정 메서드를 호출하려고 할때 해당 객체에 없는 경우
  • 프로토타입 체이닝 미발생 : 객체에 동적 프로퍼티를 추가

 

1. 프로토타입의 두 가지 의미

[[Prototype]] 링크란 무엇인가?

자바스크립트 모든 객체는 자신의 부모인 프로토타입 객체를 가리키는 참조 링크 형태의 숨겨진 프로퍼티가 존재합니다. ECMAScript에서는 이러한 링크를 암묵적 프로토타입 링크(Implicit Prototype Link)라고 부릅니다. 이러한 링크는 모든 객체의 [[Prototype]] 프로퍼티에 저장됩니다.

 

prototype 프로퍼티란 무엇인가?

prototype 프로퍼티는 함수 객체의 입장에서 자신과 링크된 프로토타입 객체를 가리키고 있습니다.

 

위 두 용어를 정리하면 자바스크립트에서 모든 객체는 자신을 생성한 생성자 함수의 prototype 프로퍼티([[Prototype]] 링크가 아닌)가 가리키는 프로토타입 객체를 자신의 부모 객체로 설정하는 [[Prototype]] 링크로 연결합니다.

 

prototype 프로퍼티와 [[Prototype]] 링크구분

// Person 생성자 함수
function Person(name){
    this.name = name;
}

// foo 객체 생성
var foo = new Person('foo');

console.dir(Person);
console.dir(foo);

위 예제의 객체(foo), 생성자 함수(Person), 프로토타입 객체(Person.prototype)의 관계를 그림으로 표현하면 아래와 같습니다. 

  • Person() 생성자 함수는 prototype 프로퍼티로 자신과 링크된 프로토타입 객체(Person.prototype)을 가리킵니다.
  • Person() 생성자 함수로 생성된 foo 객체는 Person() 함수의 프로토타입 객체를 [[Prototype]] 링크로 연결합니다.
  • 위 그림을 통해서 Person() 생성자 함수의 prototype 프로퍼티와 foo 객체의 [[Prototype]] 링크는 같은 프로토타입 객체(Person.prototype)를 가리키는 것을 알 수 있습니다.

위 예제를 통하여 자바스크립트에서 객체를 생성하는 건 생성자 함수의 역할이지만, 생성된 객체의 실제 부모 역할을 하는건 생성자 자신(Person)이 아닌 생성자의 prototype 프로퍼티(Person.prototype)가 가리키는 프로토타입 객체라는 것을 알 수 있습니다.

 

2. 객체 리터럴 방식으로 생성된 객체의 프로토타입 체이닝

프로토타입 체이닝이란 무엇인가?

자바스크립트에서 객체는 자기 자신의 프로퍼티뿐만 아니라, 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티 또한 마치 자신의 것처럼 접근이 가능합니다. 이것을 가능케 하는게 바로 프로토타입 체이닝입니다.

 

프로토타입 체이닝의 개념을 더 자세히 설명하면 자바스크립트에서 특정 객체의 프로퍼티나 메서드에 접근하려고 할 때, 해당 객체에 접근하려는 프로퍼티 또는 메서드가 없다면 [[Prototype]] 링크를 따라 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티를 차례대로 검색하는 것을 프로토타입 체이닝이라고 말합니다.

 

객체 리터럴 방식에서의 프로토타입 체이닝

var myObject = {
    name : 'foo',
    sayName : function(){
        console.log("My Name is " + this.name);
    }
}

myObject.sayName(); // Expected Output : My Name is foo
console.log(myObject.hasOwnProperty("name"));   // Expected Output : true
console.log(myObject.hasOwnProperty("nickName"));   // Expected Output : false
myObject.sayNickName(); // Expected Output : Uncaught TypeError : myObject.sayNickName() is not a function

위 예제의 결과를 보면 myObject 객체의 sayName() 메서드 호출은 잘 출력되었지만 myObject.sayNickName() 메서드 호출은 에러가 발생한 것을 알 수 있습니다. 이는 myObject 객체에 sayNickName() 메서드가 없기 때문입니다. 하지만 myObject.hasOwnProperty() 메서드는 myObject 객체에 정의되지 않아도 잘 수행된 것을 볼 수 있습니다. 이는 객체 리터럴로 생성한 객체는 Object()라는 내장 생성자 함수로 생성된 것이기 때문입니다. 

 

Object() 생성자 함수도 함수 객체이므로 prototype이라는 프로퍼티 속성이 존재합니다. 따라서 자바스크립트의 규칙으로 생성한 객체 리터럴 형태의 myObject는 아래 그림처럼 Object() 함수의 prototype 프로퍼티가 가리키는 Object.prototype 객체를 자신의 프로토 타입 객체로 연결([[Prototype]] 링크)한 것입니다.

 

위 예제의 실행결과를 분석하자면 myObject.hasOwnProperty() 메서드를 호출했지만 myObject 객체는 hasOwnProperty() 메서드가 없다. 따라서 myObject 객체의 [[Prototype]] 링크를 타고 올라가면서 myObject 객체의 부모 역할을 하는 Object.prototype 프로토타입 객체 내에 hasOwnProperty() 메서드가 있는지를 검색합니다. hasOwnProperty() 메서드는 자바스크립트 표준 API로 Object.prototype 객체에 포함되어 있습니다. 따라서 메서드가 에러가 나지 않고 정상적으로 코드가 수행됩니다.

 

sayNickName() 메서드가 호출되지 않은 이유는 [[Prototype]] 링크를 따라 부모 객체의 메서드를 검색했음에도 불구하고 Object.prototype 프로토타입 객체까지 해당 메서드가 존재하지 않았기 때문에 에러가 발생한 것입니다.

 

 

 

3. 생성자 함수로 생성된 객체의 프로토타입 체이닝

생성자 함수로 생성한 객체나 객체 리터럴 방식으로 생성한 객체는 약간 다른 프로토타입 체이닝으로 구성됩니다. 하지만 두가지 방식 모두 기본 원칙은 동일합니다.

 

그 기본원칙은 "자바스크립트에서 모든 객체는 자신을 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체(부모 객체)로 취급합니다."입니다.

 

생성자 함수 방식에서의 프로토타입 체이닝

// Person() 생성자 함수
function Person(name, age, gender){
    this.name = name;
    this.age = age;
    this.gender = gender;
}

// foo 객체 생성
var foo = new Person('foo', 20, 'man');

// 프로토타입 체이닝
console.log(foo.hasOwnProperty('name'));    // Expected Output : true

// Person.prototype 객체 출력
console.dir(Person.prototype);  // Expected Output : Object {constructor : f Person(name,age,gender), [[Prototype]]: Object}

위 출력결과를 보면 foo 객체는 hasOwnProperty() 메서드 호출에 성공한 것을 볼 수 있습니다. 하지만 foo 객체의 [[Prototype]] 링크는 자신을 생성한 Person.prototype 프로토타입 객체와 연결된 것을 알 수 있습니다. 그러나 Person.prototype 프로토타입 객체에서조차 hasOwnProperty() 메서드는 존재하지 않습니다. Person.prototype 프로토타입 객체는 constructor 프로퍼티만을 가지고 있습니다. 그렇지만 hasOwnProperty() 메서드가 호출에 성공한 이유는 Person.prototype 프로토타입 객체 또한 [[Prototype]] 링크가 Object.prototype 프로토타입 객체를 가리키기 때문입니다. 이것은 myObject 객체에서 시작해 [[Prototype]] 링크를 타고 올라가면서 Object.prototype 프로토타입 객체까지 hasOwnProperty() 메서드를 검색한 것을 의미하고 Object.prototype 프로토타입 객체에 hasOwnProperty() 메서드가 존재하기 때문에 호출에 성공한 것을 알 수 있습니다. 위 예제의 관계를 아래 그림과 같이 표현할 수 있습니다.

 

 

4. 프로토타입 체이닝의 종점

자바스크립트에서 Object.prototype 객체는 프로토타입 체이닝의 종점입니다. 위와 같이 객체 리터럴 방식이나 생성자 함수를 이용한 객체 생성 방식이나 결국엔 Object.prototype에서 프로토타입 체이닝이 끝나는 것을 알 수 있습니다. 

 

모든 객체의 프로토타입 체이닝의 종점이 Object.prototype 객체라는 것을 안다면 모든 객체는 Object.prototype 객체의 프로퍼티와 메서드에 접근하고 공유가 가능하다는 것을 알 수 있습니다.

 

 

5. 기본 데이터 타입 확장

앞서 모든 객체가 프로토타입 체이닝으로 Object.prototype 프로토타입 객체 정의된 프로퍼티와 메서드에 접근이 가능한 것을 알 수 있었습니다. 이와 같은 방식으로 자바스크립트의 숫자(Number), 문자열(String), 배열(Array) 등에서 사용되는 표준 메서드들의 경우, 이들의 프로토타입인 Number.prototype, String.prototype, Array.prototype 등에 정의되어 있습니다. 물론 이러한 기본 내장 프로토타입 객체(Number.prototype, String.prototype 등) 또한 Object.prototype을 자신의 프로토타입으로 가지고 있어서 프로토타입 체이닝으로 연결됩니다.

 

자바스크립트는 Object.prototype, String.prototype 등과 같이 표준 빌트인 프로토타입 객체에도 사용자가 직접 정의한 메서드들을 추가하는 것을 허용합니다.

 

String 기본 타입에 메서드 추가

String.prototype.testMethod = function(){
    console.log("This is the String.prototype.testMethod()");
};

var str = "this is test";
str.testMethod();   // Expected Output : This is the String.prototype.testMethod()

console.log(String.prototype);

위 예제의 객체 관계를 표현하면 아래 그림과 같습니다.

 

6. 프로토타입도 자바스크립트 객체다

프로토타입 객체 역시 자바스크립트 객체이므로 일반 객체처럼 동적으로 프로퍼티를 추가/삭제하는 것이 가능합니다.

 

프로토타입 객체의 동적 메서드 생성 예제 코드

// Person() 생성자 함수
function Person(name){
    this.name = name;
}

// foo 객체 생성
var foo = new Person('foo');

// foo.sayHello() // Expected Output : Error, sayName() 메서드가 정의되지 않음

// 프로토타입 객체에 sayHello() 메서드 정의
Person.prototype.sayHello = function(){
    console.log("hello");
}

foo.sayHello();  // Expected Output : hello

위와 같이 Person.prototype.sayHello 문장으로 Person.prototype 프로토타입 객체에 sayHello() 메서드를 동적으로 추가할 수 있습니다.

 

7. 프로토타입 메서드와 this 바인딩

프로토타입 메서드 내부에서 this를 사용한다면 this는 어디에 바인딩 되는가?

  • this는 그 메서드를 호출한 객체에 바인딩됩니다.

프로토타입 메서드와 this 바인딩

// Person() 생성자 함수
function Person(name){
    this.name = name;
}

// getName() 프로토타입 메서드
Person.prototype.getName = function(){
    return this.name;
}

// foo 객체 생성
var foo = new Person('foo');

console.log(foo.getName()); // Expected Output : foo

// Person.prototype 객체에 name 프로퍼티 동적 추가
Person.prototype.name = "person";

console.log(Person.prototype.getName()); // Expected Output : person

위 예제의 결과를 보면 foo.getName() 메서드 호출시 Person.prototype 프로토타입 객체의 getName() 메서드를 호출하지만 호출한 객체가 foo이기 때문에 this는 foo 객체를 가리킵니다. 그러나 Person.prototype.getName() 메서드 호출시에는 호출한 객체가 Person.prototype 프로토타입 객체이므로 this는 Person.prototype 객체를 가리키고 이 객체가 가지고 있는 name 프로퍼티 값(person)을 반환합니다. 위 예제의 과정을 그림으로 표현하면 아래와 같습니다.

 

 

8. 디폴트 프로토타입은 다른 객체로 변경이 가능하다

자바스크립트에서는 함수를 생성할 때 해당 함수와 연결되는 디폴트 프로토타입 객체를 다른 일반 객체로 변경하는 것이 가능하다. 단, 주의할 점은 프로토타입 객체가 변경되면, 변경된 시점 이후에 생성된 객체들은 변경된 프로토타입 객체로 [[Prototype]] 링크를 연결한다는 점을 기억해야 합니다. 이에 반해 생성자 함수의 프로토타입이 변경되기 이전에 생성된 객체들은 기존 프로토타입 객체로의 [[Prototype]] 링크를 그대로 유지합니다.

 

프로토타입 객체 변경 예제

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

console.log(Person.prototype.constructor);  // Expected Output : f Person(name){...}

// foo 객체 생성
var foo = new Person('foo');
console.log(foo.country);                   // Expected Output : undefined

// 디폴트 프로토타입 객체 변경
Person.prototype = {
    country : 'korea'
};

console.log(Person.prototype.constructor);  // Expected Output : f Object(){...}

// bar 객체 변경
var bar = new Person('bar');
console.log(foo.country);                   // Expected Output : undefined
console.log(bar.country);                   // Expected Output : korea
console.log(foo.constructor);               // Expected Output : f Person(name){...}
console.log(bar.constructor);               // Expected Output : f Object(){...}

위 예제의 결과를 보면 Person.prototype 프로토타입의 객체를 변경한 이후의 foo 객체와 bar 객체의 country, constructor 프로퍼티를 참조하는 결과를 보면 foo 객체의 country 프로퍼티는 undefined를 반환하고 constructor는 Person() 생성자 함수를 가리키는 것을 볼 수 있습니다. 반면 Person.prototype 프로토타입 객체를 변경한 이후 생성된 객체인 bar 객체는 country 프로퍼티를 참조할 수 있고 constructor 프로퍼티는 객체 리터럴 방식의 생성자 함수의 프로토타입 객체인 Object.prototype 객체를 가리키는 것을 볼 수 있습니다. 위의 예제의 객체 관계를 그림으로 표현하면 아래와 같습니다.

 

 

9. 객체의 프로퍼티 읽기나 메서드를 실행할 때만 프로토타입 체이닝이 동작한다

객체의 특정 프로퍼티를 읽으려고 할 때, 프로퍼티가 해당 객체에 없는 경우 프로토타입 체이닝이 발생합니다. 반대로 객체에 있는 특정 프로퍼티에 값을 쓰려고 한다면 이때는 프로토타입 체이닝이 일어나지 않습니다. 자바스크립트는 객체에 없는 값을 쓰려고 할 경우 동적으로 객체에 프로퍼티를 추가하기 때문입니다.

 

프로토타입 체이닝과 동적 프로퍼티 생성 예제

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

Person.prototype.country = "Korea";

var foo = new Person('foo');
var bar = new Person('bar');

console.log(foo.country);   // Expected Output : Korea
console.log(bar.country);   // Expected Output : Korea

foo.country = "USA";

console.log(foo.country);   // Expected Output : USA    
console.log(bar.country);   // Expected Output : Korea

위 예제 결과를 보시면 foo.country = "USA" 명령어로 인하여 foo 객체에 country 프로퍼티를 동적으로 생성하고 값은 "USA"로 저장하게 됩니다. 위 객체 관계를 그림으로 표현하면 아래와 같습니다.

 

 

References

source code : https://github.com/yonghwankim-dev/javascript_study
INSIDE JAVASCRIPT 한빛미디어, 송형주, 고현준 지음