[JS] 프로토타입

Pavel_Dmr·2022년 6월 21일
0

JavaScript

목록 보기
5/9

🥗 프로토타입이란 (Prototype)

Java,C++과 같은 클래스 기반 객체지향 프로그래밍 언어와 달리 자바스크립트는 프로토타입 기반 객체지향 프로그래밍 언어이다.
따라서 자바스크립트의 동작 원리를 이해하기 위해서는 프로토타입의 개념을 이해해야 한다.

클래스 기반 객체지향 프로그래밍 언어는 객체 생성 이전에 클래스를 정의 하고, 이를 통해 객체(인스턴스)를 생성한다.
하지만 프로토타입 기반 객체지향 프로그래밍 언어는 클래스 없이도 객체를 생성 할 수 있다.

자바스크립트의 모든 객체는 자신의 부모 역할을 담당하는 객체와 연결(Prototype-Chain)되어 있다.
이것은 객체지향의 상속개념과 비슷하게, 부모 객체의 프로퍼티,메소드를 상속받아 사용 할 수 있게한다.

이러한 부모 객체를 Prototype 객체 또는 Prototype이라고 한다.

Prototype 객체는 생성자 함수에 의해 생성된 각각의 객체에 공공 프로퍼티를 제공하기 위해 사용한다.

var student = {
  name: 'Lee',
  score: 90
};

// student에는 hasOwnProperty 메소드가 없지만 아래 구문은 동작한다.
// 왜? student객체의 프로토타입객체은 해당 메소드을 가지고 있으니깐
console.log(student.hasOwnProperty('name')); // true

console.dir(student);

자바스크립트에서 모든 객체는 [[Prototype]]이라는 인터널 슬롯(Internal slot)를 가진다. [[Prototype]]의 값은 null 또는 객체이며 상속을 구현하는데 사용된다. [[Prototype]] 객체의 데이터 프로퍼티는 get 액세스를 위해 상속되어 자식 객체의 프로퍼티처럼 사용할 수 있다. 하지만 set 액세스는 허용되지 않는다.

[[Prototype]]의 값은 Prototype(프로토타입) 객체이며 proto accessor property로 접근할 수 있다. proto 프로퍼티에 접근하면 내부적으로 Object.getPrototypeOf가 호출되어 프로토타입 객체를 반환한다.

student 객체는 proto 프로퍼티로 자신의 부모 객체(프로토타입 객체)인 Object.prototype을 가리키고 있다.

var student = {
  name: 'Lee',
  score: 90
}
console.log(student.__proto__ === Object.prototype); // true

객체를 생성할 때 프로토타입은 결정된다.
결정된 프로토타입 객체는 다른 임의 객체로 변경할 수 있다.
이것은 부모 객체인 프로토타입을 동적을 변경할 수 있다는 것을 의미한다.
이러한 특징을 활용하여 객체의 상속을 비슷하게 구현할 수 있다.


🍡 [[Prototype]]와 Prototype 프로퍼티의 차이

모든 객체는 자신의 프로토타입 객체를 가르키는 [[Prototype]] 인터널 슬롯(Internal slot)을 가지며, 상속을 위해 사용된다.

함수도 객체이므로 [[Prototype]] 인터널 슬롯을 갖는다. 그런데 함수 객체는 일반 객체와 다르게 prototype 프로퍼티도 소유하게 된다.

주의 사항으로 [[Prototype]]와 Prototype 프로퍼티는 엄현히 다르다. 둘다 모두 프로토타입를 가리키지만 관점의 차이가 있다

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

var dog = new Animal('겨울이');

console.dir(Animal); // prototype 프로퍼티가 있다.
console.dir(dog);    // prototype 프로퍼티가 없다.
  1. [[Prototype]]
  • 함수를 포함한 모든 객체가 가지고 있는 인터널 슬롯이다.
  • 객체의 입장에서 자신의 부모 역할을 하는 프로토타입 객체를 가리키며 함수 객체의 경우
    Function.prototype를 가르킨다.

이는 Object.prototype -> Function.prototype -> 함수 생성 절차대로 생성되기 때문. 최상위에 객체 프로토타입이 있고, 그 아래의 함수 프로토타입이 있어서, 함수을 생성하면 해당 함수의 부모 프로토타입 객체는 Function.prototype이 된다.

console.log(Animal.__proto__ === Function.prototype);
  1. Prototype 프로퍼티
  • 함수 객체만 가지고 있는 프로퍼티이다.
  • 함수 객체가 생성자로 사용될 때,
    해당 함수를 통해 생성될 객체의 부모 역할을 하는 프로토타입 객체를 가리킨다.
console.log(Animal.prototype === dog.__proto__);

변수 dog의 프로토타입은 Animal이 된다. 그렇다고 변수 dog에서 Function이나 Object의 프로퍼티나 메소드에 접근하지 못하는 것은 아니다. 상속 관점에서 차이가 있을 뿐이다.


🥑 생성자 프로퍼티 (Constructor)

프로토타입 객체는 생성자 프로퍼티를 갖습니다. 이 생성자 프로퍼티는 해당 객체의 입장에서 자신을 생성한 객체를 가리킨다.

예를 들어,

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

var dog = new Person('겨울이');

// Animal() 생성자 함수에 의해 생성된 프로토타입 객체를 생성한 객체는 Animal() 생성자 함수이다.
console.log(Animal.prototype.constructor === Animal);

// dog 객체를 생성한 객체는 Person() 생성자 함수이다.
console.log(dog.constructor === Animal);

// Animal() 생성자 함수를 생성한 객체는 Function() 생성자 함수이다.
console.log(Person.constructor === Function);

dog 객체 입장에서 자신을 생성한 생성자 객체는 Animal이다.
dog 객체의 프로토타입 객체는 Animal.prototype이다.

따라서 Animal.prototype 객체 입장에서 자신을 생성한 생성자 객체는 Animal 객체가 된다.


🍄 프로토타입 체인 (Prototype Chain)

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

그러니깐 이 말이 무슨 소리냐면, 특정 객체에서 프로퍼티나 메소드를 사용하려 할때, 자기가 해당 프로퍼티,메소드가 없으면, 자신의 부모 객체에서 그걸 또 찾아보고 못찾으면, 그 부모의 부모 객체에서 또 찾아보고 못찾으면, 그 부모의 부모의 부모 객체에서 찾아보고, 컴퓨터는 절대 포기을 안한다. 최상위 객체까지 요청한 프로퍼티나 메소드를 찾아본다.

이게 특정 객체가 자신이 가진 프로퍼티,메소드가 없어도, 프로토타입에 의해서 그것을 사용 할 수 있는 이유이다.

var student = {
  name: 'Lee',
  score: 90
}

// Object.prototype.hasOwnProperty()
console.log(student.hasOwnProperty('name')); // true

student 객체는 Object.prototype.hasOwnProperty() 메소드가 없다.

그런데, 정상적으로 출력이 된다. 이는 student 객체의 [[Prototype]]이 가리키는 링크를 따라가서, student 객체의 부모 역할을 하는 프로토타입 객체(Object.prototype)의 메소드 hasOwnProperty()를 호출하였기 떄문에 가능한 것이다.


객체 생성으로 형성되는 프로토타입 체인 관계

객체 생성 방법은 3가지가 있다.

  1. 객체 리터럴
  2. 생성자 함수
  3. Object() 생성자 함수

객체 리터럴 방식

객체 리터럴 방식으로 생성된 객체는 내장 함수(Built-in)인 Object()생성자 함수로 객체를 생성하는 것을 단순화 한것이다.

자바스크립트 엔진은 객체 리터럴로 객체를 생성하는 코드를 찾으면 내부적으로 Object()생성자 함수 함수를 사용하여 객체를 생성한다.

그러니깐 우리 눈엔 다른 방식으로 객체를 생성하는 거 같지만, 같은 방식으로 생성 하는 것이다.

Object() 생성자 함수는 함수이다. 따라서 함수 객체인 Object() 생성자 함수는 일반 객체와 달리 Prototype 프로퍼티가 있다.

  • Prototype 프로퍼티는 함수 객체가 생성자로 사용될 때 이 함수를 통해 생성된 객체의 부모 역할을 하는 객체, 즉 프로토타입 객체를 가리킨다.
  • [[Prototype]]은 객체의 입장에서 자신의 부모 역할을 하는 객체, 즉 프로토타입 객체를 가리킨다.
var person = {
  name: 'Lee',
  gender: 'male',
  sayHello: function(){
    console.log('Hi! my name is ' + this.name);
  }
};

console.dir(person);

console.log(person.__proto__ === Object.prototype);   // ① true
console.log(Object.prototype.constructor === Object); // ② true
console.log(Object.__proto__ === Function.prototype); // ③ true
console.log(Function.prototype.__proto__ === Object.prototype); // ④ true

결론적으로 객체 리터럴을 사용해 객체를 생성하면, 해당 객체의 프로토타입 객체는 Object.Prototype이다.


생성자 함수 방식

생성자 함수로 객체를 생성하려면 우선 생성자 함수를 정의하여야 한다.

함수를 정의하는 방식은 3가지가 있다.

  • 함수선언식 (Function Declaration)
  • 함수표현식 (Function Expression)
  • Function() 생성자 함수

함수표현식으로 함수를 정의할 때 함수 리터럴방식을 사용한다. ( 가장 기본적 )

var square = function(number) {
  return number * number;
};

함수선언식의 경우, 자바스크립트 엔진이 내부적으로 기명 함수표현식으로 알아서 변환한다.

var square = function abc(number) {
  return number * number;
};

결론적으로 함수선언식, 함수표현식 모두 함수 리터럴 방식을 사용한다.
함수 리터럴 방식은 Function() 생성자 함수로 함수를 생성하는 것을 단순화 시킨 것이다.

3가지 함수 정의 방식은 결국 Function() 생성자 함수를 통해 함수 객체를 생성한다. 따라서 어떠한 방식으로 함수 객체를 생성하여도, 모든 함수 객체의 Prototype 객체는 Function.prototype이다. 생성자 함수도 함수 객체이므로, 생성자 함수의 Prototype 객체는 Function.prototype이다.

결국 모든 객체는 부모 객체인 Object.Prototype 객체에서 프로토타입 체인이 끝난다.
이때 Object.Prototype 객체를 프로토타입 체인의 종점(End of Prototpye Chain)이라 한다.

🥛 프로토타입 객체의 확장

프로토타입 객체도 객체이므로 일반 객체와 같이 프로퍼티를 추가/삭제 할 수 있다.
그리고 이렇게 추가/삭제된 프로퍼티는 즉시 프로토타입 체인에 반영된다.

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

var foo = new Person('Lee');

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

foo.sayHello();

생성자 함수 Person()은 프로토타입 객체 Person.prototype와 prototype 프로퍼티에 의해 바인딩되어 있다.
Person.prototype 객체는 일반 객체와 같이 프로퍼티를 추가/삭제가 가능하다. 위에서 Person.prototype 객체에 메소드 sayHello()를 추가했다.
이떄 sayHello() 메소드는 프로토타입 체인에 반영된다.

따라서 생성자 함수 Person에 의해 생성된 모든 객체는 프로토타입 체인에 의해 Person.prototype의 sayHello() 메소드를 사용할 수 있다.

🍟 원시타입(Primitive data type)의 확장

자바스크립트에서 원시타입를 제외한 모든 것은 객체이다.
그런데 원시 타입인 문자열이 객체와 유사하게 동작한다.

원시 타입 문자열과 String() 생성자 함수로 생성한 문자열 객체의 타입은 분명이 다르다.
원시 타입은 객체가 아니므로 프로퍼티나 메소드를 가질 수 없다.

그런데, 원시 타입으로 프로퍼티나 메소드를 호출하면, 일시적으로 원시 타입과 연관된 객체로 변환되어 프로토타입 객체를 사용 할 수 있게 된다.

원시 타입은 객체가 아니므로 프로퍼티나 메소드를 직접 추가 할 수 없다.
하지만 String 객체의 프로토타입 객체 String.prototype에 메소드를 추가하면 원시 타입도 메소드를 사용할 수 있다.

var str = 'test';

String.prototype.myMethod = function () {
  return 'myMethod';
};

console.log(str.myMethod());      // myMethod
// String.prototype 자체에 메소드을 추가했기때문에,
// 모든 String 원시타입,객체에서 myMethod을 사용할 수 있다.
console.log('string'.myMethod()); // myMethod
console.dir(String.prototype);

앞서 살펴본 바와 같이 모든 객체는 프로토타입 체인에 의해 Object.prototype 객체의 메소드를 사용할 수 있었다.
Object.prototype 객체는 프로토타입 체인의 종점으로 모든 객체가 사용할 수 있는 메소드를 갖는다.

이후 알아볼 Built-in Object(내장 객체)의 Global Objects(Standard Built-in Objects)인 String,Number,Array 객체 등이 가지고 있는 표준 메소드는 프로토타입 객체인 String.prototype, Number.prototype, Array.prototype등에 정의 되어 있다.

이들 프로토타입 객체 또한 Object.prototype를 프로토타입 체인에 의해 자신의 프로토타입 객체로 연결한다.

자바스크립트는 표준 내장 객체의 프로토타입 객체에 개발자가 정의한 메소드의 추가를 허용한다.

var str = 'test';

String.prototype.myMethod = function() {
  return 'myMethod';
}

console.log(str.myMethod());
console.dir(String.prototype);

console.log(str.__proto__ === String.prototype);                 // ① true
console.log(String.prototype.__proto__  === Object.prototype);   // ② true
console.log(String.prototype.constructor === String);            // ③ true
console.log(String.__proto__ === Function.prototype);            // ④ true
console.log(Function.prototype.__proto__  === Object.prototype); // ⑤ true


🥞 프로토타입 객체의 변경

객체를 생성할 때 프로토타입은 결정된다. 결정된 프로토타입 객체는 다른 임의의 객체로 변경할 수 있다. 이것은 부모 객체인 프로토타입을 동적으로 변경할 수 있다는 것을 의미한다.
이러한 특징을 활용하여 객체의 상속을 구현할 수 있다.

이때 주의할 것은 프로토타입 객체를 변경하면,

프로토타입 객체 변경 시점 이전에 생성된 객체, 기존 프로토타입 객체를 [[Prototype]]에 바인딩한다.

프로토타입 객체 변경 시점 이후에 생성된 객체, 변경된 프로토타입 객체를 [[Prototype]]에 바인딩한다.

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

var foo = new Person('Lee');

// 프로토타입 객체의 변경
Person.prototype = { gender: 'male' };

var bar = new Person('Kim');

console.log(foo.gender); // undefined
console.log(bar.gender); // 'male'

console.log(foo.constructor); // ① Person(name)
console.log(bar.constructor); // ② Object()

① constructor 프로퍼티는 Person() 생성자 함수를 가리킨다.

② 프로토타입 객체 변경 후, Person() 생성자 함수의 Prototype 프로퍼티가 가리키는 프로토타입 객체를 일반 객체로 변경하면서 Person.prototype.constructor 프로퍼티도 삭제되었다. 따라서 프로토타입 체인에 의해 bar.constructor의 값은 프로토타입 체이닝에 의해 Object.prototype.constructor 즉 Object() 생성자 함수가 된다.

🍢 프로토타입 체인 동작 조건

객체의 프로퍼티를 참조하는 경우, 해당 객체에 프로퍼티가 없는 경우, 프로토타입 체인이 동작한다.

겍체의 프로퍼티에 값을 할당하는 경우, 프로토타입 체인이 동작하지 않는다.
이는 객체에 해당 프로퍼티가 있는 경우, 값을 재할당하고 해당 프로퍼티가 없는 경우는 해당 객체에 프로퍼티를 동적으로 추가하기 때문이다.

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

Person.prototype.gender = 'male'; // ①

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

console.log(foo.gender); // ① 'male'
console.log(bar.gender); // ① 'male'

// 1. foo 객체에 gender 프로퍼티가 없으면 프로퍼티 동적 추가
// 2. foo 객체에 gender 프로퍼티가 있으면 해당 프로퍼티에 값 할당
foo.gender = 'female';   // ②

console.log(foo.gender); // ② 'female'
console.log(bar.gender); // ① 'male'

foo 객체에 gender 프로퍼티에 값을 할당하면, foo 객체에 프로퍼티를 동적으로 추가한다.

profile
노는게 좋아

0개의 댓글