Languages/JavaScript

[HUFS/GnuVil] #10 프로토타입

성중 2022. 10. 13. 13:56

프로토타입

JS는 명령형 / 함수형 / 프로토타입 기반 / 객체 지향 프로그래밍을 지원하는 멀티 패러다임 프로그래밍 언어다. 간혹 객체지향 언어가 아니라는 오해를 받지만 JS는 클래스 기반 객체지향 프로그램 언어보다 더 강력한 프로토타입 기반의 객체지향 프로그래밍 언어이며 원시 타입의 값을 제외한 모든 값들(함수, 배열, 정규 표현식 등)은 객체

 

객체지향 프로그래밍(OOP) = 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임

 

const circle = {
  radius: 5, // 반지름

  // 원의 지름: 2r
  getDiameter() {
    return 2 * this.radius;
  },

  // 원의 둘레: 2πr
  getPerimeter() {
    return 2 * Math.PI * this.radius;
  },

  // 원의 넓이: πrr
  getArea() {
    return Math.PI * this.radius ** 2;
  }
};

console.log(circle);
// {radius: 5, getDiameter: ƒ, getPerimeter: ƒ, getArea: ƒ}

console.log(circle.getDiameter());  // 10
console.log(circle.getPerimeter()); // 31.41592653589793
console.log(circle.getArea());      // 78.53981633974483

현실의 사물이나 개념의 상태(프로퍼티)동작(메서드)추상화해 객체로 표현할 수 있다

 

// 생성자 함수
function Circle(radius) {
  this.radius = radius;
}

// Circle 생성자 함수가 생성한 모든 인스턴스가 getArea 메서드를
// 공유해서 사용할 수 있도록 프로토타입에 추가한다.
// 프로토타입은 Circle 생성자 함수의 prototype 프로퍼티에 바인딩되어 있다.
Circle.prototype.getArea = function () {
  return Math.PI * this.radius ** 2;
};

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

// Circle 생성자 함수가 생성한 모든 인스턴스는 부모 객체의 역할을 하는
// 프로토타입 Circle.prototype으로부터 getArea 메서드를 상속받는다.
// 즉, Circle 생성자 함수가 생성하는 모든 인스턴스는 하나의 getArea 메서드를 공유한다.
console.log(circle1.getArea === circle2.getArea); // true

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

프로토타입을 기반으로 상속을 구현해 불필요한 중복을 제거할 수 있다

 

// obj 객체는 Object 생성자 함수로 생성한 객체가 아니라 객체 리터럴로 생성했다.
const obj = {};

// 하지만 obj 객체의 생성자 함수는 Object 생성자 함수다.
console.log(obj.constructor === Object); // true

생성자 함수가 아닌 리터럴로 객체를 생성해도 constructor로 생성자 함수를 가진다

* Object 생성자 함수로 생성한 객체와 세부 내용은 다름 (new.target, 프로퍼티 처리 등)

 

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

const me = new Person('Lee');

// 결국 Person.prototype과 me.__proto__는 결국 동일한 프로토타입을 가리킨다.
console.log(Person.prototype === me.__proto__);  // true

생성자 함수는 생성되는 시점에 prototype 프로퍼티를 추가로 가지는데 이는 모든 객체가 가지는 __proto__ 접근자 프로퍼티와 결국 동일하지만 사용하는 주체가 다르다

 

프로토타입 사용 목적

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

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

const me = new Person('Lee');

// hasOwnProperty는 Object.prototype의 메서드다.
console.log(me.hasOwnProperty('name')); // true

프로토타입 체인에 의해 상위 프로토타입의 메서드를 상속받아 사용할 수 있다

 

프로토타입 체인

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

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

  // 생성자 함수를 반환
  return Person;
}());

const me = new Person('Lee');

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

// 인스턴스 메서드가 호출된다. 프로토타입 메서드는 인스턴스 메서드에 의해 가려진다.
me.sayHello(); // Hey! My name is Lee

프로토타입 체인에 같은 이름의 메서드가 있다면 오버라이딩(overriding)되어 더 이상 타고 올라가지 않는데, 이를 프로퍼티 섀도잉이라 한다

 

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

const me = new Person('Lee');

// Person.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
console.log(me instanceof Person); // true

// Object.prototype이 me 객체의 프로토타입 체인 상에 존재하므로 true로 평가된다.
console.log(me instanceof Object); // true

instanceof 연산자로 인스턴스가 해당 객체의 프로토타입 체인 상에 존재하는지 확인할 수 있다

 

const myProto = { x: 10 };
// 임의의 객체를 직접 상속받는다.
// obj → myProto → Object.prototype → null
obj = Object.create(myProto);
console.log(obj.x); // 10
console.log(Object.getPrototypeOf(obj) === myProto); // true

Object.create 메서드로 명시적으로 프로토타입을 지정해 객체를 생성할 수 있다

* new 연산자 없이 프로토타입을 지정하면서 객체를 생성 + 리터럴 객체도 상속 가능한 장점이 있지만 체인의 종점에 위치하는 객체를 생성할 수도 있어 ESLint에서는 권장되지 않음

 

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

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

// 정적 프로퍼티
Person.staticProp = 'static prop';

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

const me = new Person('Lee');

// 생성자 함수에 추가한 정적 프로퍼티/메서드는 생성자 함수로 참조/호출한다.
Person.staticMethod(); // staticMethod

// 정적 프로퍼티/메서드는 생성자 함수가 생성한 인스턴스로 참조/호출할 수 없다.
// 인스턴스로 참조/호출할 수 있는 프로퍼티/메서드는 프로토타입 체인 상에 존재해야 한다.
me.staticMethod(); // TypeError: me.staticMethod is not a function

 생성자 함수 객체가 소유한 프로퍼티/메서드를 정적 프로퍼티/메서드라고 하며, 이는 생성한 인스턴스로 참조/호출할 수 없다

 

const person = {
  name: 'Lee',
  address: 'Seoul'
};

// person 객체에 name 프로퍼티가 존재한다.
console.log('name' in person);    // true
// person 객체에 address 프로퍼티가 존재한다.
console.log('address' in person); // true
// person 객체에 age 프로퍼티가 존재하지 않는다.
console.log('age' in person);     // false
console.log('toString' in person); // true (프로토타입 체인을 기준으로 찾는다)
console.log(Reflect.has(person, 'name'));     // true
console.log(Reflect.has(person, 'toString')); // true

in 연산자 / Reflect.has 메서드는 객체의 프로토타입 체인에 특정 프로퍼티가 존재하는지 여부를 확인한다

 

console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('age'));  // false
console.log(person.hasOwnProperty('toString')); // false

Object.prototype.hasOwnProperty 메서드는 프로토타입 체인 상이 아닌 해당 객체에 특정 프로퍼티 존재 여부를 확인한다

 

const person = {
  name: 'Lee',
  address: 'Seoul',
  __proto__: { age: 20 }
};

for (const key in person) {
  console.log(key + ': ' + person[key]);
}
// name: Lee
// address: Seoul
// age: 20

for…in 문은 객체의 프로토타입 체인 상의 프로퍼티 중에서 [[Enumerable]] 값이 true인 프로퍼티를 순회하며 열거한다

* 일부 브라우저는 열거할 때 순서 보장 X

 

const person = {
  name: 'Lee',
  address: 'Seoul',
  __proto__: { age: 20 }
};

console.log(Object.keys(person)); // ["name", "address"]
console.log(Object.values(person)); // ["Lee", "Seoul"]

console.log(Object.entries(person)); // [["name", "Lee"], ["address", "Seoul"]]

Object.entries(person).forEach(([key, value]) => console.log(key, value));
/*
name Lee
address Seoul

배열로 다룰 수 있는 Object.keys/values/entries 메서드를 사용하도록 하자

 

 내용은 위키북스의 '모던 자바스크립트 Deep Dive' 바탕으로 작성되었습니다.