본문 바로가기

Frontend Dev/JavaScript

자바스크립트의 프로토타입 (prototype)

반응형

자바스크립트와 프로토타입

 자바스크립트는 프로토타입 기반 언어(prototype-based-language)로, 프로토타입은 원형 객체를 의미한다. 모든 객체들이 메소드와 속성들을 상속 받기 위한 템플릿으로써 프로토타입 객체(prototype object)를 가진다는 의미이다. 프로토타입 객체도 또 다시 상위 프로토타입 객체로부터 메소드와 속성을 상속 받을 수도 있고 그 상위 프로토타입 객체도 마찬가지이다. 이를 프로토타입 체인(prototype chain)이라 부르며 다른 객체에 정의된 메소드와 속성을 한 객체에서 사용할 수 있도록 하는 근간이다.

 

 Java, C++ 과 같은 클래스 기반 객체지향 프로그래밍 언어는 객체 생성 이전에 클래스를 정의하고 이를 통해 객체(인스턴스)를 생성한다. 하지만 프로토타입 기반 객체지향 프로그래밍 언어인 JavaScript에서는 클래스 없이(Class-less)도 객체를 생성할 수 있다. (ES6에서 Class 추가)

 

👾 프로토타입(Prototype)이란?

 자바스크립트의 모든 객체는 자신의 부모 역할을 담당하는 객체와 연결되어 있다. 그리고 이것은 마치 객체 지향의 상속 개념과 같이 부모 객체의 프로퍼티 또는 메서드를 상속받아 사용할 수 있게 한다. 이러한 부모 객체를 Prototype(프로토타입) 객체라고 한다.

 JavaScript의 모든 객체는 Prototype 객체를 가지고 있으며, 프로토타입으로부터 프로퍼티와 메서드를 상속받는다.

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

 

const person = {
  name: "Helen",
  age: 20
};

// person에는 hasOwnProperty 메서드가 없지만 아래 구문은 동작한다.
console.log(person.hasOwnProperty("name")); // true
console.dir(person);

console.dir(person)의 출력 결과

 

👾 [[Prototype]]( __proto__ ) 과 prototype 프로퍼티

 자바스크립트의 모든 객체는 자신의 프로토타입 객체를 가리키는 [[Prototype]]이라는 인터널 슬롯(internal slot) 을 가지며 이는 상속을 위해 사용된다. (ECMAScript spec)

 함수도 객체이므로 [[Prototype]] 인터널 슬롯을 가지는데, 함수 객체는 일반 객체와는 달리 prototype 프로퍼티도 소유하게 된다.

 

 ✨ 인터널 슬롯과 인터널 메서드란?

 인터널 슬롯(Internal Slot)과 메서드(Internal Method)는 자바스크립트 명세에서 필요한 동작을 정의하는 데 사용되는 의사 속성 및 메서드이다. 슬롯은 상태(값)를 나타내고 메서드는 알고리즘(동작)을 설명한다. 이들은 엔진에서 사용되는 객체의 속성과 일치할 수도 있고, 그렇지 않을 수도 있지만, 사용자 코드에서는 노출된 일부 공개 API를 통해만 사용할 수 있다.

 ECMAScript 사양에 등장하는 이중 대괄호[[...]]로 감싼 이름들이 internal slot과 internal method 이다.

 

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

const person = new Person("Helen", 20);

console.dir(Person); // prototype 프로퍼티 있음
console.dir(person); // prototype 프로터피 없음

console.log(Person.__proto__ === Function.prototype) // true
console.log(Person.prototype === person.__proto__); // true

console.dir(Person) & console.dir(person) 출력 결과

 

 ✔️ [[Prototype]]( __proto__ )

 함수를 포함한 모든 객체가 가지고 있는 인터널 슬롯으로 객체의 입장에서 자신의 부모 역할을 하는 프로토타입 객체를 가리키며 함수 객체의 경우 Function.prototype를 가리킨다.

console.log(Person.__proto__ === Function.prototype) // true

 

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

 

 ✨ ES5.1 명세에는 __proto__가 아닌 [[Prototype]]이라는 명칭으로 정의되어 있다. __proto__ 프로퍼티는 브라우저들이 [[Prototype]]을 구현한 대상에 지나지 않았다. 또한 명세에는 instance.__proto__와 같은 방식으로 직접 접근하는 것을 허용하지 않고, Object.getPrototypeOf() 등을 통해서만 접근할 수 있도록 정의했었다. 하지만 대부분의 브라우저들이 __proto__에 직접 접근하는 방식을 포기하지 않았고, 결국 ES6에서는 이를 브라우저에서 동작하는 레거시 코드에 대한 호환성 유지 차원에서 정식으로 인정했다. 다만 어디까지나 브라우저에서의 호환성을 고려한 지원일 뿐 권장되는 방식은 아니다.

 

⬇︎ 더 자세한 내용은 아래 MDN 참고

 

Object.prototype.__proto__ - JavaScript | MDN

주의: 객체의 [[Prototype]]을 변경하는 것은 최신 JavaScript 엔진이 속성 접근을 최적화하는 방식의 특성상 모든 브라우저 및 JavaScript 엔진에서 매우 느린 작업입니다. 상속 구조를 변경하는 것이 성

developer.mozilla.org

 

 ✔️ prototype 프로퍼티

 함수 객체만 가지고 있는 프로퍼티로 함수 객체가 생성자로 사용될 때 이 함수를 통해 생성될 객체의 부모 역할을 하는 객체(프로토타입 객체)를 가리킨다.

console.log(Person.prototype === person.__proto__); // true

 

 ✔️ 프로토타입 조회 (__proto__ & prototype)

/* 객체에서의 프로토타입 접근 */

// 배열 메서드를 확인할 수 있음
Array.prototype
[].__proto__
/* 클래스에서의 프로토타입 접근 */

// 클래스는 prototype으로 접근한다.
Class.prototype 

// 인스턴스는 __proto__로 접근한다.
instance.__proto__ 
instance.__proto__.__proto__

 


👾 프로토타입 생성

 프로토타입을 생성하는 가장 기본적인 방법은 객체 생성자 함수를 작성하는 것이다. 생성자 함수를 작성하고 new 연산자를 사용해 객체를 생성하면, 같은 프로토타입을 가지는 객체들을 생성할 수 있다.

 

function Cat(name, age, color) {	
  this.name = name;				        
  this.age = age;			
  this.color = color;				     
}

const myCat = new Cat("두부", 1, "white&brown");

console.log(myCat.__proto__ === Cat.prototype); // true
console.log(myCat.constructor === Cat); // true

 

  ✔️ constructor 프로퍼티

 프로토타입 객체는 constructor 프로퍼티를 가지며, 이 constructor 프로퍼티는 객체의 입장에서 자신을 생성한 객체를 가리킨다.

class Human {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  work() {
    console.log(`${this.name} 일하는 중`);
  }
}

let Emily = new Human('에밀리', 27);

console.log( Human.prototype.constructor === Human ) 
// true (Human 클래스의 생성자 함수는 Human)

console.log( Emily.constructor === Human ) 
// true (Human 클래스는 Emily 객체를 생성한 생성자 함수)

console.log( Human.constructor === Function ) 
// true (Human 클래스를 생성한 객체는 Function() 생성자 함수)

console.log( Human.prototype === Emily.__proto__ )  
// true (Human 클래스의 프로토타입은 Human 클래스의 인스턴스인 Emily의 __proto__)

console.log( Human.prototype.work ) 
// [Function: work] 클래스 내부에서 정의한 메서드는 Human.prototype에 저장된다.

console.log( Human.prototype.work === Emily.work ) 
// true (Human 클래스의 work 메서드는 프로토타입에 있으며, Human 클래스의 인스턴스인 Emily에서 Emily.work로 사용할 수 있다.)

console.log( Object.getOwnPropertyNames(Human.prototype) ) 
// [ 'constructor', 'work' ] 현재 프로토타입에는 두 개의 메서드가 있다.

 


👾 프로토타입 체인(prototype chain)

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

 

 ✨ 요약 모든 객체는 다른 객체를 가리키는 내부 링크인 프로토타입을 가지고 있고, 이를 통해 직접 객체를 연결하는 것을 프로토타입 체인이라고 한다.

 

// obj의 프로토타입은 Object.prototype
let obj = new Object();
cosnole.log( obj.__proto__ === Object.prototype ) // true

let arr = new Array();  // arr의 프로토타입은 Array.prototype
let date = new Date();  // date의 프로토타입은 Date.prototype

 → 객체 리터럴 방식으로 객체를 생성한 경우, 그 객체의 프로토타입 객체는 Object.prototype이다.

 → new 연산자를 사용해 생성한 객체는 생성자의 프로토타입을 자신의 프로토타입으로 상속받는다.

 

✨  Object.prototype 객체는 어떠한 프로토타입도 가지지 않으며, 아무런 프로퍼티도 상속받지 않는다.

자바스크립트에 내장된 모든 생성자나 사용자 정의 생성자는 바로 Object.prototype 객체를 프로토타입으로 가진다. 즉, Object.prototype 객체는 프로토타입 체인에서도 가장 상위에 존재하는 프로토타입이다. 따라서 자바스크립트의 모든 객체는 Object.prototype 객체를 프로토타입으로 상속받는다.

 

 ✔️ DOM과 프로토타입

let div = document.createElement('div');

div.__proto__ // HTMLDivElement
div.__proto__.__proto__ // HTMLElement
div.__proto__.__proto__.__proto__ // Element
div.__proto__.__proto__.__proto__.__proto__ // Node
div.__proto__.__proto__.__proto__.__proto__.__proto__ // EventTarget

div.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__ // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
div.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__ // null

 → EventTarget의 부모로는 모든 클래스의 조상인 Object가 존재한다.

 


👾 프로토타입 객체의 확장

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

 

 ✔️ 프로토타입에 프로퍼티 및 메서드 추가

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

const myCat = new Cat("두부");

Cat.prototype.age = 1; // 프로퍼티 추가
Cat.prototype.sayHi = function() { return "야옹~" } // 메서드 추가

console.log(myCat.age) // 1
console.log(myCat.sayHi()) // 야옹~

 → 직접 생성한 프로토타입은 새로운 프로퍼티나 메서드를 추가할 수 있다.

 

/* Array 객체에 메서드 추가 */

Array.prototype.sayHi = function() {
  console.log("Hello, World")
}

const newArr = [];

newArr.codessayHitates() // Hello, World
[].sayHi() // Hello, World

 → 자바스크립트 표준 객체의 프로토타입도 임의로 수정할 수 있지만 권장되는 사항은 절대 아니다.

 

 ✔️ 프로토타입에 프로퍼티 삭제

function Cat(name, age, color) {	
  this.name = name;		
  this.age = age;
  this.color = color;		     
}

const myCat = new Cat("두부", 1, "white&brown");
delete myCat.age;

console.log(myCat) // Cat { name: '두부', color: 'white&brown' }

 • delete 키워드를 사용하여 프로퍼티를 삭제하면 프로퍼티의 값뿐만 아니라 프로퍼티 그 자체도 삭제가 된다.

 

 

 ✔️ 프로토타입 객체의 변경

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

 

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

const myCat = new Cat("두부");

// 프로토타입 객체 변경
Cat.prototype = { age: 1 };

const yourCat = new Cat("나비");

console.log(myCat.age); // undefined
console.log(yourCat.age); // 1

console.log(myCat.constructor); // [Function: Cat]
console.log(yourCat.constructor); // [Function: Object]

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

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

 → 프로토타입 객체 변경 후 Cat() 생성자 함수의 Prototype 프로퍼티가 가리키는 프로토타입 객체를 일반 객체로 변경하면서 Cat.prototype.constructor 프로퍼티도 삭제되었다. 프로토타입 체인에 의해 yourCat.constructorObject.prototype.constructor가 된다.

 

 ✨ 프로토타입 체인의 동작 조건

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

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

 

function Cat(name, age, color) {	
  this.name = name;		
  this.age = age;
  this.color = color;		     
}

const myCat = new Cat("두부", 1, "white&brown");
const yourCat = new Cat("나비", 4, "yellow");

console.log(myCat.age); // 1
console.log(yourCat.age); // 4

// 💬 1. myCat 객체에 age 프로퍼티가 있으면 해당 프로퍼티에 값 할당
myCat.age = 2;
// 💬 2. myCat에 eyeColor 프로퍼티가 없으면 프로퍼티 동적 추가
myCat.eyeColor = "brown";

console.log(myCat.age); // 2
console.log(yourCat.age); // 4

console.log(myCat.eyeColor); // brown
console.log(yourCat.eyeColor); // undefined

 

 


자료출처: poiemaweb

✏️ 공부하며 정리한 내용입니다. 잘못된 정보나 더 공유할 내용이 있으면 댓글로 알려주세요!

읽어주셔서 감사합니다 😊

반응형