개발 알다가도 모르겠네요

프로토타입 본문

학습일지/모던 자바스크립트 Deep Dive

프로토타입

이재빵 2022. 8. 14. 22:08
728x90

객체지향 프로그래밍

객체(object)의 집합 으로 프로그램을 표현하는 것.

실세계의 실체를 인식하는 철학적 사고를 프로그래밍에 반영한 것

 

 

추상화

추상화(abstraction) : 객체의 다양한 속성 중에서 프로그램에 필요한 속성만 간추려 내여 표현하는 것

  • 객체에는 크게 2가지 요소로 구성
    • 상태를 나타내는 프로퍼티
    • 상태를 조작하는 행동을 표현하는 메서드
// person 이라는 객체
// "name(이름)", "address(주소)"라는 속성(property)으로 표현하도록 가정
const person = {
  name: "WI",
  address: "Incheon",
};

// circle 이라는 객체
const circle = {
  // 원의 반지름 프로퍼티(상태)
  radius: 5,

  // 원의 지름을 구하는 메서드(행위)
  getDiameter() {
    return 2 * this.radius;
  },

  // 원의 둘레를 구하는 메서드(행위)
  getPerimeter() {
    return 2 * Math.PI * this.radius;
  },

  // 원의 넓이를 구하는 메서드(행위)
  getArea() {
    return Math.PI * this.radius ** 2;
  },
};

 

상속

상속(inheritance) : 어떤 객체의 프로퍼티, 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것

  • 자바스크립트는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거 → 코드를 재사용
// '원' 생성자 함수
function Circle(radius) {
  // 각각의 원 반지름 데이터(상태)는 독립
  this.radius = radius;
}

// 원의 넓이를 구하는 메서드(행위)는 Circle 객체의 프로토타입에 등록하여 공유
Circle.prototype.getArea = function () {
  return Math.PI * this.radius ** 2;
};

// '원' 객체(인스턴스) 생성
const circle1 = new Circle(1);
const circle2 = new Circle(2);

// 각기 다른 circle1 객체와 circle2 객체의 원의 넓이를 구하는 메서드 getArea는 이 둘의 동일한 Prototype 에 등록된 getArea
console.log(circle1.getArea === circle2.getArea); // true

console.log(circle1.getArea()); // 3.141592653589793
console.log(circle2.getArea()); // 12.566370614359172

 

프로토타입

자바스크립트는 프로토타입(prototype) 을 기반으로 상속을 구현

 

자바스크립트의 모든 객체는 자신의 부모 역할을 담당하는 객체와 연결되어 있다.

그리고 이것은 마치 객체 지향의 상속 개념과 같이 부모 객체의 프로퍼티 또는 메소드를 상속받아 사용할 수 있게 한다.

이러한 부모 객체를 Prototype(프로토타입) 객체 또는 줄여서 Prototype(프로토타입)이라 한다.

 

 

위에 예제에서 getArea 메서드  Circle 객체의 생성자 함수에 메서드로 등록한 것이 아닌, Circle 생성자 함수의 prototype 객체에 메서드로 등록.

  • 생성자 함수에 메서드로 등록할 경우 → 각각의 객체에 독립적인 동일한 기능을하는 메서드가 할당  메모리 소모
  • prototype 에 메서드로 등록할 경우 → 동일한 기능을 하는 하나의 메서드를 여러 객체가 공유 , 각각의 객체는 독립된 상태(state)만 관리할 수 있음.

 

모든 객체는 자신의 프로토타입 , 즉 상위 객체 역할을 하는 prototype 의 모든 프로퍼티와 메서드를 상속받음.

 

모든 객체는 [[Prototype]] 이라는 내부 슬롯을 가진다.

  • 이는 프로토타입의 참조값(null인 경우도 있음)
  • [[Prototype]] 에 저장되는 프로토타입은 객체 생성 방식 에 의해 결정
    • 예를 들어, 객체 리터럴( { } ) 로 생성된 객체의 프로토타입 → Object.prototype
    • 생성자 함수에 의해 생성된 객체의 프로토타입 → 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체

모든 객체는 하나의 프로토타입을 갖는다.

  • 모든 프로토타입은 생성자 함수와 연결 되어 있다.
  • 즉, 객체 - 프로토타입 - 생성자 함수는 연결되어 있다.

 

__proto__ 접근자 프로퍼티

모든 객체는 __proto__ 접근자 프로퍼티 를 통해 자신의 프로토타입,
즉 [[Prototype]] 내부 슬롯에 간접적으로 접근 할 수 있음.

 

 

__proto__  접근자 프로퍼티.

  • 원래 내부슬롯과 내부 메서드는 직접적으로 접근 가능하지 않음.
  • 다만, 일부 내부 슬롯과 내부 메서드에 한하여 , 간접적으로 접근할 수 있는 수단을 제공.
    • __proto__  접근자 프로퍼티 이며, [[Prototype]] 내부 슬롯의 값, 즉 프로토타입에 접근할 수 있다.
    • __proto__ 가 접근자 프로퍼티이므로, getter/setter 접근자 함수 를 통해, 프로토타입을 취득하거나 할당할 수 있는 것이다.
const obj = {};
const parent = { x: 1 };

// __proto__ 접근자 프로퍼티의 getter 접근자 함수로 obj 객체의 프로토타입 객체 취득
console.log(obj.__proto__); // [Object: null prototype] {}

// __proto__ 접근자 프로퍼티의 setter 접근자 함수로 obj 객체의 프로토타입에 값 할당
obj.__proto__ = parent;
console.log(obj.__proto__); // { x: 1 }

 

__proto__ 접근자 프로퍼티  상속을 통해 사용됨.

  • __proto__ 접근자 프로퍼티 는 객체가 직접 소유하고 있는 프로퍼티가 아닌, Object.prototype 의 프로퍼티다.
  • 즉, 모든 객체는 상속을 통해 Object.prototype.__proto__ 접근자 프로퍼티 를 사용할 수 있다.

 

__proto__ 접근자 프로퍼티 를 통해 프로토타입에 접근하는 이유

  • 상호 참조 에 의해 프로토타입 체인(참조 루프)이 생성되는 것을 방지하기 위해서.
    • 프로토타입 체인은 단방향 연결 리스트로 구현 되어야 한다.
    • 즉, 프로퍼티 식별자 검색 방향이 한쪽 방향 으로만 흘러야 한다.
const parent = {};
const child = {};

child.__proto__ = parent;
parent.__proto__ = child; // TypeError: Cyclic __proto__ value

 

__proto__ 접근자 프로퍼티를 코드 내에서 직접 사용하는 것은 권장하지 않음.

 

__proto__ 접근자 프로퍼티를 사용하는 대신

  • 프로토타입 취득에 경우 → Object.getPrototypeOf 메서드 
  • 프로토타입 교체에 경우 → Object.setPrototypeOf 메서드 를 사용하는 것을 권장
const obj = {};
const parent = { x: 1 };

// Object.getPropertyOf 메서드로 obj 객체의 프로토타입 취득
console.log(Object.getPrototypeOf(obj)); // [Object: null prototype] {}

// Object.setPrototypeOf 메서드롤 obj 객체의 프로토타입을 교체
Object.setPrototypeOf(obj, parent);

// obj 객체의 __proto__ 접근자 프로퍼티로 obj 객체의 프로토타입 변경사항을 조회
console.log(obj.__proto__); // { x: 1 }

 

 

함수 객체의 prototype 프로퍼티

함수 객체만이 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스(객체)의 프로토타입을 가리킴.

  • 생성자 함수로서 호출할 수 없는 non-constructor  화살표 함수, ES6 메서드 축약 표현으로 정의한 메서드
    prototype 프로퍼티를 소유하지 않고, 프로토타입도 생성하지 않는다.
// 함수 객체는 protoype 프로퍼티를 가진다.
console.log(function () {}.hasOwnProperty("prototype")); // true

// 일반 객체는 prototype 프로퍼티를 가지지 않는다.
console.log({}.hasOwnProperty("prototype")); // false

 

모든 객체가 가지고 있는 __proto__ 접근자 프로퍼티와 함수 객체만이 가지고 있는 prototype 프로퍼티는 
동일한 프로토타입을 가리킴.

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

const me = new Person("WI");

// Person 생성자 함수의 prototype 프로퍼티와 me 객체(인스턴스)의 __proto__ 접근자 프로퍼티가 가리키는 것은 동일한 프로토타입이다.
console.log(me.__proto__ === Person.prototype); // true

 

구분 소유값 사용주체 사용 목적
__proto__ 
접근자 프로퍼티
모든 객체 프로토타입의 참조값 모든 객체 객체가 자신의 프로토타입에 접근 또는 교체하기위해 사용
prototype 프로퍼티 constructor 프로토타입의 참조값 생성자 함수 생성자 함수가 자신이 생성할 객체의 프로토타입을 할당하기 위해 사용

 

프로토타입의 constructor 프로퍼티와 생성자 함수

  • 모든 프로토타입은 constructor 프로퍼티를 갖는다.
  • 이는, 자신을 참조하고 있는 생성자 함수를 가리킨다.
  • 이 연결은 생성자 함수가 생성될 때, 즉 함수 객체가 생성될 때 이뤄진다.
function Person(name) {
  this.name = name;
}

const me = new Person("WI");

// me 객체의 생성자 함수는 Person
// me 객체의 constructor 프로퍼티(정확히는 me 객체의 프로토타입인 Person.prototype 에 constructor 프로퍼티)
console.log(me.constructor === Person); // true

 

프로토타입의 생성 시점

프로토타입은 생성자 함수가 생성되는 시점에 더불어 생성됨.

 

사용자 정의 생성자 함수와 프로토타입 생성 시점

함수 정의가 평가 되어 함수 객체를 생성하는 시점 에 프로토타입도 더불어 생성

  • 프로토타입도 객체이며, 모든 객체는 프로토타입을 가진다.
  • 프로토타입도 자신의 프로토타입을 가진다.
    • 생성된 프로토타입의 프로토타입은 Object.prototype

 

빌트인 생성자 함수와 프로토타입 생성 시점

빌트인 생성자 함수는 빌트인 생성자 함수가 생성되는 시점에 프로토타입이 생성

  • 모든 빌트인 생성자 함수는 전역 객체가 생성되는 시점에 생성
  • 생성된 프로토타입은 빌트인 생성자 함수의 prototype 프로퍼티에 바인딩

 

프로토타입 체인

자바스크립트가 객체지향 프로그래밍의 상속 을 구현하는 메커니즘

 

  1. 자바스크립트는 객체의 프로퍼티(메서드 포함)에 접근하려고 할 때, 해당 객체에 접근하려는 프로퍼티가 있는지 확인한다.
  2. 없다면 [[Prototype]] 내부 슬롯의 참조값 을 따라, 자신의 부모 역할을 하는 프로토타입의 프로퍼티 를 순차적으로 검색한다.
function Person(name) {
  this.name = name;
}

const me = new Person("WI");

console.log(me.hasOwnProperty("name")); // true
// 1. hasOwnProperty 메서드가 me 객체에 프로퍼티에 존재하는지 검색한다.
// 2. 없기 때문에, me 객체의 [[Prototype]] 내부 슬롯의 참조값을 통해, Person.prototype 객체의 프로퍼티를 검색
// 3. Person.prototype 에도 hasOwnProperty 메서드가 없으므로, [[Prototype]] 내부 슬롯의 참조값을 통해 Object.prototype 프로퍼티를 검색
// 4. Object.prototype 에는 hasOwnProperty 메서드가 있으므로, 자바스크립트 엔진은 Object.prototype.hasOwnProperty 메서드를 호출
// (이 때 this 에는 me 객체가 바인딩된다.)

 

  • 프로토타입 체인의 최상위에는 언제나 Object.prototype 이다.
  • 따라서, 모든 객체는 Object.prototype을 상속 받는다.

 

프로토타입 체인 / 스코프 체인

1.프로토타입 체인

  • 자바스크립트 엔진은 프로토타입 체인을 따라 프로퍼티 & 메서드를 검색
  • 객체 간의 상속 관계로 이뤄진 프로토타입의 계층적인 구조에서 객체의 프로퍼티를 검색
  • 따라서, 프로토타입 체인은 상속과 프로퍼티 검색을 위한 메커니즘

2.스코프 체인

  • 자바스크립트 엔진은 함수의 중첩 관계로 이뤄진 스코프의 계층적 구조에서 식별자를 검색
  • 따라서, 스코프 체인은 식별자 검색을 위한 메커니즘

 

오버라이딩 / 프로퍼티 섀도잉

동일한 이름의 프로퍼티에 대해

  • 인스턴스 메서드가 프로토타입의 메서드를 → 오버라이딩(overriding)
  • 프로토타입 메서드는 인스턴스 메서드로 인해 프로퍼티가 가려짐 → 프로퍼티 섀도잉(property shadowing)
오버라이딩(overriding)
	상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의하여 사용하는 방식

오버로딩(overloading)
	함수의 이름은 동일하나, 매개변수의 타입 또는 개수가 다른 메서드를 구현하고, 매개변수에 의해 메서드를 구별하여 호출하는 방식
	자바스크립트에서는 오버로딩을 지원하지 않는다. ( 다만, arguments 객체를 사용하여 구현할 수는 있다. )

 

instaneof 연산자

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

const me = new Person("WI");

// 프로토타입으로 교체할 객체
const parent = {};

Object.setPrototypeOf(me, parent);

// Person 생성자 함수와 parent 객체는 연결되어 있지 않다.
console.log(Person.prototype === parent); // false
console.log(parent.constructor === Person); // false

// parent 객체를 Person 생성자 함수의 prototype 프로퍼티에 바인딩
Person.prototype = parent;

// Person.prototype 이 me 객체의 프로토타입 체인 상에 존재함
console.log(me instanceof Person); // true
// Object.prototype 이 me 객체의 프로토타입 체인 상에 존재함
console.log(me instanceof Object); // true

 

 

정적 프로퍼티/메서드

생성자 함수로 인스턴스를 생성하지 않아도 참조/호출 가능한 프로퍼티/메서드

  • 생성자 함수도 객체.
  • 생성자 함수도 프로퍼티나 메서드를 소유할 수 있음.
  • 생성자 함수가 소유한 프로퍼티나 메서드를 정적 프로퍼티/메서드 라고 함.
    • 생성자 함수가 소유하고 있는 정적 프로퍼티/메서드는 인스턴스에서 직접 참조/호출할 수 없음.
// 생성자 함수
function Person(name) {
  this.name = name;
}

// 프로토타입 메서드
Person.prototype.sayHello = function () {
  console.log(`HI, My name is ${this.name}`);
};

// 정적 프로퍼티
Person.staticProp = "인간 생성자 함수의 정적 프로퍼티 !";

// 정적 메서드
Person.staticMethod = function () {
  console.log("인간 생성자 함수의 정적 메서드 호출 !");
};

const me = new Person("WI");

Person.staticMethod(); // 인간 생성자 함수의 정적 메서드 호출 !
me.staticMethod(); // TypeError: me.staticMethod is not a function



프로퍼티 존재 확인

in 연산자

key in object;
const person = {
  name: "WI",
  age: 100,
};

console.log("name" in person); // true
console.log("age" in person); // true
console.log("address" in person); // false << 🔍 address 프로퍼티는 person 객체에 정의되어 있지 않으므로 false

// toString 메서드는 person 객체의 프로퍼티에는 존재하지 않는다.
// 하지만, person 객체의 프로토타입인 Object.prototype 에 toString 메서드가 존재하기 때문에 true
// 이처럼, in 연산자는 조사할 객체의 상속받은 프로토타입의 프로퍼티까지 조사를 한다.
console.log("toString" in person); // true 🔍

 

 

프로퍼티 열거

for - in 문

for (변수선언문 in 객체) { ... }

 

for - in 문은 순서를 보장하지 않는다.

const person = {
  name: "WI",
  age: 100,

  __proto__: {
    address: "Incheon",
  },
};

// in 연산자로 person 객체에 toString 프로퍼티(메서드)가 존재하는지 확인 -> 존재(true)
console.log("toString" in person); // true

// 그럼에도 불구하고, toString 메서드는 for - in 문에서 key 에 할당되지 않았다.
// 이는 toString 의 프로퍼티 어트리뷰트에서 [[Enumerable]] 값이 false 로 설정되어 있기 때문이다.
// 추가적으로, __proto__ 로 person 객체의 프로토타입에 프로퍼티로 address 프로퍼티를 추가했다. 이 또한 for - in문에 key로 할당
for (const key in person) {
  console.log(`${key} : ${person[key]}`);
}
/*
name : WI
age : 100
address : Incheon
*/

 

 

오로지 해당 객체의 프로퍼티 키들로만 for - in 문을 순회하고 싶을 때는 Object.prototype.hasOwnProperty 메서드를 호출하며 검사한다.

const person = {
  name: "WI",
  age: 100,

  __proto__: {
    address: "Incheon",
  },
};

for (const key in person) {
  // person 객체의 고유 프로퍼티일 경우에만 정보를 출력
  if (person.hasOwnProperty(key)) {
    console.log(`${key} : ${person[key]}`);
  }
}
/*
name : WI
age : 100
*/

 

객체 고유 프로퍼티만으로 구성된 열거하고 싶을 경우

  • Object.keys() : 객체 자신의 열거 가능한 프로퍼티 키를 배열로 반환 ( ES6+ )
  • Object.values() : 객체 자신의 열거 가능한 프로퍼티 값을 배열로 반환 ( ES8 )
  • Object.entries() : 객체 자신의 열거 가능한 프로퍼티 키,값을 배열로 반환 ( ES8 )
const person = {
  name: "WI",
  age: 100,

  __proto__: {
    address: "Incheon",
  },
};

console.log(Object.keys(person)); // [ 'name', 'age' ]
console.log(Object.values(person)); // [ 'WI', 100 ]
console.log(Object.entries(person)); // [ [ 'name', 'WI' ], [ 'age', 100 ] ]

 

'학습일지 > 모던 자바스크립트 Deep Dive' 카테고리의 다른 글

this  (0) 2022.08.23
빌트인 객체  (0) 2022.08.22
일급 객체  (0) 2022.08.09
생성자 함수  (0) 2022.08.09
let & const  (0) 2022.08.06