코드노트

프로토타입 기반의 객체지향 언어, 자바스크립트 프로토타입 이해 본문

Code note/자바스크립트

프로토타입 기반의 객체지향 언어, 자바스크립트 프로토타입 이해

코드노트 2022. 12. 19. 17:48

자바스크립트를 지금까지 공부하면서 겉핥기로 계속 넘어갔던 부분들을 공부하려고 했다.

그 중에서도 정말로 이해안되던 프로토타입...

MDN은 역시나 이해안되는 단어 문장들로 되어있다..

책을 봐도 어렵게 풀어놨다... 약간 논문을 보는 느낌이라고 해야하나? 일부로 어렵게 설명해놓은듯한 느낌..

 

코딩애플을 보고 역시나 10분만에 이해해버렸다..



자바스크립트는 프로토타입 기반의 객체지향 언어이다.

- 원시 타입의 값을 제외함수, 배열, 정규표현식 등 모두 객체이다.



* 인스턴스

- 인스턴스(instance)란 클래스에 의해 생성되어 메모리에 저장된 실체를 말한다.

- 클래스 또는 프로토타입을 사용하여 만들어 낸 결과물



프로토타입 이것만 알자!

 

프로토타입은 객체를 생성하게 되면 만들어진다.

- prototype에는 기본적인 여러가지들이 정의되어 있다. 그걸 가지고 우리는 메서드로 사용하기도 한다.

* 원형? 속성? 그냥 유전자라고 생각하자. 원래 가지고 있는..것!! 객체는 프로토타입을 가지고 있다. 유전자를 가지고 있다.

 



- 생성자 함수 상속은?

function Person(name) {
  this.name = name;
  this.sayName = function () {
    return `나는 ${this.name} 입니당`;
  };
}

const traveler1 = new Person("곽튜브");
const traveler2 = new Person("빠니보틀");

console.log(traveler1.sayName() === traveler2.sayName()); // false

console.log(traveler1.sayName()); // 나는 곽튜브 입니당
console.log(traveler2.sayName()); // 나는 빠니보틀 입니당

=====================

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

Person.prototype.sayName = function (name) {
  return `나는 ${this.name} 입니당`;
};

const traveler1 = new Person("곽튜브");
const traveler2 = new Person("빠니보틀");

console.log(traveler1.sayName() === traveler2.sayName()); // true

console.log(traveler1.sayName()); // 나는 곽튜브 입니당
console.log(traveler2.sayName()); // 나는 빠니보틀 입니당

상속 inheritance

- 다른 객체의 프로퍼티, 메서드를 상속받아 사용하는 것

- 불필요한 중복을 제거하여 기존의 코드를 재사용

 

- sayName() 메서드는 인스턴스를 생성할 때마다 메서드도 새로 생성된다.

- Person.prototype에 메서드를 추가해서 사용하게 되면 인스턴스가 생성될때마다 새로 생성되지 않고 사용할 수 있다.

- 비교를 했을 때에도 true값이 나온는걸 알 수 있다.

- 이처럼 상속은 생성자 함수가 생성한 모든 인스턴스는 상위객체의 prototype의 모든 프로퍼티와 메서드를 상속받는걸 알 수 있다.

 



- 객체는 상태 데이터와 동작을 하나의 논리적인 단위로 묶은 복합적인 자료구조

* 상태 데이터( 객체의 데이터 ), 메서드 ( 상태 데이터를 조작하는 동작)



자바스크립트는 프로토타입 기반의 객체지향 언어다양한 객체 생성방법이 존재 한다.

 객체 리터럴

더보기

- 객체 리터럴은 문자 또는 약속된 기호를 사용하여 값을 생성하는 표기법을 말한다.

- 객체 리터럴은 객체를 생성하는 표기법이다.

- {...} 내에 0개 이상의 프로퍼티를 정의

- 프로퍼티를 동적으로 추가 가능

- 변수에 할당이 이루어지는 시점에 객체를 생성

- 일반 함수와 구분하기 위해 프로퍼티에 함수는 메서드라고 부른다.

 

 

let person = {
  name = "lee",
  seyHello() {
    console.log(`Hello! My name is ${this.name}.`);
  }
};

console.log(typeof person) // object
console.log(person) {name: "lee", sayHello: f}

- 객체 리터럴 이외에 방식은 모두 함수를 활용한 방식

Object 생성자 함수, 생성자 함수

 

자바스크립트 Object 객체 생성자 함수

Object 생성자 함수 - new 연산자와 Object 생성자 함수를 호출하게 되면 빈 객체를 생성하여 반환 - 빈 객체를 생성한 후 프로퍼티, 메서드를 추가해 객체를 완성 - 생성자 함수에 의해 생성된 객체를

codeno-te.tistory.com

Object.create 메서드

- 명시적으로 프로토타입을 지정하여 새로운 객체를 생성 / 권장하지 않음

클래스

 



객체와 프로토타입, 생성자함수 는 서로 연결되어 있는걸 알 수 있다.

- __proto__ 접근자 프로퍼티를 통해서 프로토타입 내부슬롯에 간접적으로 접근을 할 수 있다.

- 그러나 __proto__ 접근자 프로퍼티를 사용하는건 권장하지 않는다.

 

* 프로토타입은 생성자 함수가 생성되는 시점에 같이 생성된다.

* 생성자 함수를 사용할건데 문제가 있다면 prototype을 확인해보자(non-constructor는 프로토타입이 생성되지 않는다.)

 

 



 

- 프로토타입의 참조를 할 경우에는 Object.getPrototypeOf

- 프로토타입을 교체를 할 경우에는 Object.setPrototypeOf

const obj = {};
const parent = { x:1 };
Object.getPrototypeOf(obj) // obj.__proto__
Object.setPrototypeOf(obj, parent) // obj.__proto__ == parent;

console.log(obj.x); // 1

 

 



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

무슨말일까?

 

간단하게 말하면 함수 객체는 prototype 프로퍼티를 가지고 있다. 일반 객체는 프로퍼티를 소유하지 않는다!

* prototype 프로퍼티는 생성자 함수가 생성하는 객체, 바로 인스턴스의 prototype을 가르키고 있다.

 

자바스크립트 Object 객체 생성자 함수

Object 생성자 함수 - new 연산자와 Object 생성자 함수를 호출하게 되면 빈 객체를 생성하여 반환 - 빈 객체를 생성한 후 프로퍼티, 메서드를 추가해 객체를 완성 - 생성자 함수에 의해 생성된 객체를

codeno-te.tistory.com

- 객체 생성자 함수에서 정리한거처럼 non-constructor인 화살표함수, 축약 표현 메서드prototype 프로퍼티를 소유하지 않고, 생성하지도 않는다.

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

- 모든 프로토타입은 constructor 프로퍼티를 갖는다!

- new 연산자로 객체가 생성이 될 때 객체는 constructor 프로퍼티가 없지만 생성자함수는 constructor를 가지고 있다!

   그렇기 때문에 객체는 생성자함수의 프로퍼티를 상속받아서 사용할 수 있다.

 

 

 



let obj = new Object();
console.log(obj); // {}

class Foo extends Object {}
console.log(new Foo()); // Foo {}

obj = new Object(123);
console.log(obj); // [Number: 123]

obj = new Object("123");
console.log(obj); // [String: '123']


function foo() {}
console.log(foo.constructor === Function); // true

- 객체 리터럴이 평가될 때 추상 연산 OrdinaryObjectCreate를 호출하여 빈 객체를 생성 후 프로퍼티를 추가하도록 정의 되어 있다.

- 따라서 객체 리터럴에 의해 생성된 객체는 Object 생성자 함수가 생성한 객체가 아니다!

 

- 하단에 foo()함수는 선언문으로 생성했지만 constructor 프로퍼티를 확인하면 Function인것을 볼 수 있다.

* 프로토타입과 생성자 함수는 단독으로 존재할 수 없으며 언제나 쌍으로 존재한다.

- 리터럴 표기법으로 생성된 객체, 함수, 배열, 정규표현식은 생성자 함수로 생각해도 크게 다르지 않다.

 

 


prototype의 생성 시점

console.log(Person.prototype); // Person {}

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

- 생성된 prototype은 constructor 프로퍼티만 가지고 있는 객체이다.

- Object, String, Number, Function, Array, RegExp, Date, Promise 등과 같은 빌트인 생성자 함수도 일반 함수와 마찬가지로 빌트인 생성자 함수가 되는 시점에 prototype이 생성된다.

- prototype이 생성된 후 OrdindryObjectCreate에 전달되는 인수에 의해서 결정

- 이 인수는 객체가 생성되는 시점에 객체 생성방식에 의해 결정

 

- 객체 리터럴 방식은 객체 리터럴 내부에 프로퍼티를 추가

- Object 생성자 함수 방식은 빈 객체를 생성 한 후 프로퍼티를 추가



프로토타입 체인

- 자바스크립트는 객체의 프로퍼티에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면 [[ prototype ]] 내부 슬롯의 참조를 따라서 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다. 이를 프로토타입 체인 이라고 한다.

 

- 프로토타입 체인의 최상위 위치하는 객체는 언제나 Object.prototype

- 모든 객체는 Object.prototype을 상속 받는다.

- 만약 프로토타입 체인의 종점인 Object.prototype에서도 프로퍼티를 검색할 수 없는 경우 undfined를 반환. 에러발생 X

 

* 프로토타입 체인은 상속과 프로퍼티 검색

* 스코프 체인은 식별자 검색

 

* 스코프 체인과 프로토타입 체인은 서로 협력하여 식별자와 프로퍼티를 검색하는데 사용



프로토타입 오버라이딩, 프로퍼티 쉐도잉

- 프로토타입이 소유한 프로퍼티를 인스턴스 메서드로 추가하게 되면 프로토타입 프로퍼티를 검색하여 덮어쓰는것이 아닌 인스턴스 프로퍼티로 추가

- 인스턴스는 프로토타입 메서드를 오버라이딩 한다.

* 오버라이딩 (overriding) : 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의 하여 사용

- 오버라이딩이 되었기 때문에 프로토타입 프로퍼티는 쉐도잉이 된다.

* 쉐도잉  (shadowing): 상속 관계에 의해서 프로퍼티가 가려지는 현상

const Person = (function () {
  function Person(name) {
    this.name = name;
  }
  Person.prototype.sayHello = function () { // 프로토타입 메서드
    console.log(`Hi! prototype ${this.name}`);
  };

  return Person;
})();

const me = new Person("곽튜브");

me.sayHello = function () { // 인스턴스 메서드
  console.log(`Hi! instance ${this.name}`);
};

me.sayHello(); // Hi! instance 곽튜브

delete me.sayHello; // 인스턴스 메서드 삭제
me.sayHello(); // Hi! prototype 곽튜브

delete Person.prototype.sayHello; // 프로토타입 메서드 삭제
me.sayHello(); // TypeError: me.sayHello is not a function

- 만약 인스턴스 메서드를 삭제하게 되면? 프로토타입의 메서드가 호출되게 된다.

- 그럼 메서드를 완전 삭제하려면? 프로토타입에 직접 접근해야한다.



프로토타입 교체

- 프로토타입은 다른 객체로 변경이 가능하다.

- 이것은 부모 객체인 프로토타입을 동적으로 변경할 수 있다!

- 프로토타입은 생성자 함수 또는 인스턴스에 의해 교체할 수 있다.

 

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

  Person.prototype = {
    sayHello() {
      console.log(`Hi! prototype ${this.name}`);
    },
  };

  return Person;
})();

const me = new Person("곽튜브");
console.log(me.constructor); // [Function: Object]

- prototype에 객체 리터럴을 할당하게 되면 constructor가 Person이 아닌 Object인것을 알 수 있다.

- 프로토타입을 교체하게 되면 constructor 프로퍼티와 생성자 함수 간의 연결이 파괴 된다.

 

- 여기서 constructor 프로퍼티를 추가하여 프로퍼티와 생성자 함수 간의 연결을 설정할 수 있다.

 

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

  Person.prototype = {
    constructor: Person,
    sayHello() {
      console.log(`Hi! prototype ${this.name}`);
    },
  };

  return Person;
})();

const me = new Person("곽튜브");
console.log(me.constructor); // [Function: Person]

- constructor에 생성자 함수를 연결해주게 되면 constructor 프로퍼티가 생성자함수를 가리키는걸 볼 수 있다.

- 이 외에도 인스턴스의 __proto__접근자함수로도 교체가 가능하다. 그러나 집적 상속이 더 편리하니깐 넘어가자!

* __proto__ 접근자 함수는 setPrototypeOf(인스턴스, 생성자함수)로 교체 가능



instanceof 연산자

- 이항 연산자로 좌변에는 객체를 가리키는 식별자, 우변에는 생성자 함수를 가리키는 식별자

* 우변에 피연산자가 함수가 아닌 경우에는 TypeError 발생

객체 instanceof 생성자함수
// 객체의 프로토타입 체인상에 생성자함수에 바인딩 된 객체가 존재하는지 확인

// ex 인스턴스 intanceof 생성자함수

- 생성자 함수의 prototype에 바인딩 된 객체가 좌변의 객체의 프로토타입 체인 상에 존재 여부 확인 연산자 (true, false)

- 생성자 함수의 prototype에 바인딩 된 객체가 프로토타입 체인 상에 존재하는지 확인

 

* constructor 프로퍼티가 가리키는 생성자 함수를 찾는것이 아니다. 헷갈리지 말자!



정적 프로퍼티 / 메서드

- 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메서드

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

// 정적 프로퍼티
Person.정적프로퍼티 = "정적 프로퍼티";

// 정적 메서드
Person.정적메서드 = function () {
  console.log("정적 메서드");
};

const kwak = new Person("곽튜브");

Person.정적메서드(); // 정적메서드

kwak.정적메서드(); // TypeError: kwak.정적메서드 is not a function

- 생성자함수 Person으로는 호출 가능

- kwak 인스턴스로 호출 시 TypeError가 난다. 이유는?

   > 정적프로퍼티, 정적메서드는 프로토타입 체인 상에 존재하지 않는다.

   * 즉 생성자 함수에 추가한 프로퍼티/메서드는 인스턴스로 참조, 호출할 수 없다.

 

- 만약 인스턴스/프로토타입 메서드 내에서 this를 사용하지 않는다면 그 메서드는 정적 메서드로 변경할 수 있다.

   > 메서드 내에서 인스턴스를 참조할 필요가 없다면 정적 메서드로 변경하여야 동작

   * 인스턴스가 호출한 인스턴스/프로토타입 메서드 내에서 this는 인스턴스를 가르킨다.

   > 프로토타입 메서드를 호출하려면 인스턴스를 생성해야하지만 정적 메서드는 인스턴스를 생성하지 않아도 호출 가능

 

- 정적 메서드는 인스턴스를 생성하지 않고 바로 호출이 가능하다.

 

 

정적 메서드 표기법 : Object.메서드

프로토타입 메서드 표기법 : Object.prototype.메서드 / prototype대신 #으로 표기하기도 한다.



프로퍼티 존재 확인 방법

key in Object

- 객체 내에 있는 프로퍼티가 존재하는 확인한다.

const traveler = {
  first: "곽튜브",
  second: "빠니보틀",
};

console.log("first" in traveler); // true
console.log("second" in traveler); // true
console.log("third" in traveler); // false

 

console.log("toString" in traveler);  // true

- 프로토타입 체인 상에 존재하는 메서드(toString) 또한 확인 가능하다.

 

console.log(Reflect.has(traveler, "first")); // true

- in 연산자 대신 Reflect.has() 메서드를 통해서 확인도 가능하다( ES6 도입 )

 

console.log(traveler.hasOwnProperty("first")); // true
console.log(traveler.hasOwnProperty("toString")); // false

- hasOwnProperty메서드 또한 사용이 가능하지만 객체의 고유의 프로퍼티 키인 경우에만 true를 반환한다.

- 만약 상속받은 프로토타입의 프로퍼티 키라면 false를 반환한다.



프로퍼티 열거

for...in 문

for (변수 in 객체) {...}

- 객체의 모든 프로퍼티를 순회

 

const traveler = {
  first: "곽튜브",
  second: "빠니보틀",
};

for (const key in traveler) {
  console.log(`${key}, ${traveler[key]}`);
}

// first, 곽튜브
// second, 빠니보틀

- for문과 비슷하지만  for...in은 객체의 프로퍼티 개수 만큼 순회한다. key는 index이다.

* 객체의 프로토타입 체인에 존재하는 모든 프로토타입의 프로퍼티 중에서 프로퍼티 어트리뷰트 [[ Enumerable ]]의 값이 true인 프로퍼티를 순회하며 열거

* 상속받은 프로토타입의 프로퍼티까지 열거

* 프로퍼티를 열거할 때 순서를 보장하지 않는다고 하니 주의하자!

 

- 배열에서는 for...in문 대신 for문 또는 for...of 또는 forEach를 권장한다고 한다!

 

- 그 외에도 성능면에서 for...in은 그렇게 좋지 않다.!

 

Object.keys() // 객체 자신의 키를 배열에 담아 반환
Object.values() // 객체 자신의 값을 배열에 담아 반환
Object.entries() // 객체 자신의 키와 값을 배열에 담아 반환

상속받은 프로토타입의 프로퍼티까지 열거하기 때문에 for...in문은 자신의 프로퍼티인지 확인하는 추가 처리가 필요!