본문 바로가기

자기계발

19장 모던 자바스크립트 DeepDive

자바스크립트는 프로토타입 기반의 객체지향 프로그래밍 언어이며

객체는 상태 데이터와 동작을 하나의 논리적인 단위로 묶은 복합적인 자료구조라고 할 수 있다.
이때 객체의 상태 데이터를 프로퍼티, 동작을 메서드라 부른다.

19.2 상속과 프로토타입

자바스크립트는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거한다.

 

function Circle(radius) {
  this.radius = radius;
  this.getArea = function () {
    return Math.PI * this.radius ** 2;
  };
}

const circle1 = new Circle(1);
const circle2 = new Circle(2);

console.log(circle1.getArea === circle2.getArea);	// false
console.log(circle1.getArea()); // 3.14159...
console.log(circle2.getArea());	// 12.56637...

Circle 함수 내에서 this.getArea를 정의할 경우, getArea 함수가 각 인스턴스마다 중복 생성됩니다. 이로 인해 circle1과 circle2의 getArea 함수는 서로 다른 함수 객체가 되어 메모리 낭비가 발생하고, 성능에 부정적인 영향을 미칠 수 있습니다.

다시 말해, this.getArea가 Circle 생성자 함수 안에 정의되어 있기 때문에, Circle의 인스턴스가 생성될 때마다 getArea 함수가 새로 생성되는 겁니다.. 즉, circle1과 circle2는 각각 독립적인 getArea 함수를 가지게 됩니다.

 

자바스크립트는 프로토타입(prototype)을 기반으로 상속을 구현하기 때문에 아래와 같이 코드를 짜면

동일한 메서드 중복을 피하기 때문에 위의 예시의 문제점을 해결할 수 있습니다.

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

const circle1 = new Circle(1);
const circle2 = new Circle(2);
// Circle 생성자 함수가 생성하는 모든 인스턴스는 하나의 getArea 메서드를 공유한다.
console.log(circle1.getArea === circle2.getArea);	// true
console.log(circle1.getArea()); // 3.14159...
console.log(circle2.getArea());	// 12.56637...

getArea 메서드를 Circle.prototype에 정의함으로써, Circle의 모든 인스턴스는 하나의 getArea 메서드를 공유하게 됩니다. 이는 메모리 사용량을 줄이고, 코드 재사용성을 높이며, 성능을 개선하는 데 중요한 역할을 합니다.

 

Circle 생성자 함수가 생성한 모든 인스턴스는 상위(부모) 객체 역할을 하는 Circle.prototype의 모든 프로퍼티와 메서드를 상속받는다.

위 예제에서 생성된 모든 인스턴스는 radius 프로퍼티만 개별적으로 소유하고 동일한 메서드는 상속을 통해 공유하여 사용하는 것입니다.

 

 

19.3 프로토타입 객체

프로토타입 객체란 객체 간 상속을 구현하기 위해 사용됩니다.
모든 객체는 [[Prototype]] 이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토타입의 참조입니다.

객체가 생성될 때 객체 생성 방식에 따라 프로토타입이 결정되고 [[Prototype]]에 저장이 됩니다.

객체와 프로토타입과 생성자 함수는 다음 그림과 같이 서로 연결되어 있다.

Circle이라는 생성자 함수가 있다고 가정합니다. 프로토타입 객체 Circle.prototype은 Circle로 생성된 객체들이 공유하는 메서드나 속성을 담고 있는 객체입니다. 인스턴스 객체 new Circle(1)로 생성된 객체는 Circle.prototype을 자신의 [[Prototype]]으로 설정합니다. circle1 객체는 __proto__를 통해 Circle.prototype에 접근합니다. Circle.prototype은 공통 메서드 (getArea())를 가지고 있고, 이를 통해 중복 없이 여러 인스턴스가 같은 메서드를 사용할 수 있게 됩니다. Circle 생성자 함수는 prototype 프로퍼티를 통해 이 프로토타입 객체에 접근할 수 있습니다.

 

객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입에 간접적으로 접근할 수 있습니다.

프로토타입에는 constructor 프로퍼티가 있어 해당 프로토타입을 만든 생성자 함수에 접근할 수 있습니다.

생성자 함수는 prototype 프로퍼티를 가지고 있으며, 이를 통해 해당 생성자 함수로 생성된 객체들이 공유하는 프로토타입에 접근할 수 있습니다.

 

 

 

19.3.1 __proto__ 접근자 프로퍼티

크롬 브라우저의 콘솔에서 출력한 결과다.

__proto__는 접근자 프로퍼티다.

 

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

 

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

 

프로토타입 체인은 단방향 링크드 리스트로 구현되어야 한다.

 

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

프로토타입의 참조를 취득하고 싶을 경우 Object.getPrototypeOf 메서드를 사용하고, 프로토타입을 교체하고 싶을 경우 Object.setPrototypeOf 메서드를 사용할 것을 권장한다.

 

 

19.3.2 함수 객체의 prototype 프로퍼티

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

const me = new Person('Lee');

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

Person 생성자 함수: Person.prototype을 통해 프로토타입 객체를 가리킵니다.

me 객체: me.__proto__를 통해 Person.prototype을 참조합니다.

me 객체는 Person 생성자 함수로 생성되었기 때문에, me의 프로토타입은 Person.prototype입니다.

따라서 Person.prototype === me.__proto__는 같은 객체를 참조하고 있으므로 true가 됩니다.

 

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

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

const me = new Person('Lee');

console.log(me.constructor === Person); // true

 

 

me 객체에는 constructor 프로퍼티가 직접 정의되어 있지 않지만, me의 프로토타입인 Person.prototype이 constructor 프로퍼티를 가지고 있습니다.

Person.prototype의 constructor는 Person 생성자 함수를 가리킵니다.

따라서 me.constructor는 Person과 동일하며, 이로 인해 true가 출력됩니다.

 

 

19.4 리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입

자바스크립트에서 객체를 만드는 방법 중 하나는 객체 리터럴을 사용하는 것입니다. 객체 리터럴은 {}를 사용해 간단하게 객체를 만들 수 있는 방법입니다.

// const obj = { key: 'value' };

const obj = {}; // 내부적으로 Object.prototype을 상속받는 객체가 생성됨
console.log(obj.constructor === Object); // true

객체 리터럴과 new Object() 같은 생성자 함수 호출의 차이점은 생성 과정의 세부사항뿐입니다. 둘 다 Object.prototype을 상속받으며, 프로토타입과 생성자 함수가 연결된다는 점에서 본질적으로 동일합니다.

 

19.5 프로토타입의 생성 시점

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

 

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

생성자 함수로서 호출할 수 있는 함수, 즉 constructor는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성된다. non-constructor는 프로토타입이 생성되지 않는다.

console.log(Person.prototype);

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

 

위 예제에서 볼 수 있듯이 생성자 함수는 어떤 코드보다 먼저 평가되어 함수 객체가 되고, 그 때 프로토타입도 더불어 생성된다.

 

 

non-constructor 인 함수들 

 

메서드 축약 표현

const obj = {
  greet() {
    console.log('Hello');
  }
};

// 생성자로 사용 불가
// new obj.greet(); // TypeError: obj.greet is not a constructor

 

클래스 매서드

class MyClass {
  myMethod() {
    console.log('This is a class method');
  }
}

// 생성자로 사용 불가
// new MyClass().myMethod(); // TypeError: myMethod is not a constructor

 

Generator 함수

function* generatorFunction() {
  yield 1;
}

// 생성자로 사용 불가
// new generatorFunction(); // TypeError: generatorFunction is not a constructor

 

Async 함수

async function asyncFunction() {
  return 'Hello';
}

// 생성자로 사용 불가
// new asyncFunction(); // TypeError: asyncFunction is not a constructor

 

화살표 함수

const Person = name => {
	this.name = name;
};

console.log(Person.prototype); //undefined

 

 

19.6 객체 생성 방식과 프로토타입의 결정

객체는 다음과 깉이 다양한 생성 방법이 있다.

  • 객체 리터럴
  • Object 생성자 함수
  • 생성자 함수
  • Object.create 메서드
  • 클래스(ES6)

이들의 공통점은 추상 연산 OrdinaryObjectCreate에 의해 생성된다는 것이다.

추상 연산은 빈 객체를 생성한 후, 객체에 추가할 프로퍼티 목록이 인수로 전달된 경우 프로퍼티를 객체에 추가한다.

그리고 인수로 전달받은 프로토타입을 자신이 생성한 객체의 [[Prototype]] 내부 슬롯에 할당한 다음

생성한 객체를 반환한다. 즉, 프로토타입은 추상 연산에 전달되는 인수에 의해 결정된다.


19.6.1 객체 리터럴에 의해 생성된 객체의 프로토타입

const obj = { x: 1 };

 

이 코드에서 { x: 1 }은 객체 리터럴을 사용하여 객체를 생성하는 방법입니다.

객체 리터럴로 생성된 객체는 내부적으로 Object 생성자 함수와 Object.prototype과 연결됩니다.

즉 obj는 Object.prototype을 프로토타입으로 가지며, 이 프로토타입을 통해 constructor 프로퍼티와 hasOwnProperty 같은 메서드를 상속받습니다. 이 때문에 obj는 자신이 직접 정의하지 않은 메서드들도 사용할 수 있게 됩니다.

 

19.6.1 객체 리터럴에 의해 생성된 객체의 프로토타입

const obj = new Object();
obj.x = 1;

 

 

new Object()를 사용하여 객체를 생성하는 방식입니다.

Object 생성자 함수를 호출하면 내부적으로 객체 리터럴과 마찬가지로 추상 연산이 호출되어 Object.prototype이 프로토타입으로 설정됩니다.

즉 obj는 객체 리터럴 방식으로 생성된 객체와 동일하게 Object.prototype을 상속받습니다. 따라서, 이 두 방식의 차이점은 객체를 생성할 때 프로퍼티를 추가하는 방식뿐입니다.

 

19.6.3 생성자 함수에 의해 생성된 객체의 프로토타입

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

const me = new Person('Lee');

 

 

여기서 Person은 사용자 정의 생성자 함수입니다.

new 연산자를 사용해 Person 생성자 함수를 호출하면, me라는 인스턴스 객체가 생성됩니다.

이때 me 객체의 프로토타입은 Person.prototype입니다.

즉 me는 Person.prototype을 상속받으며, Person.prototype은 기본적으로 constructor 프로퍼티만을 가지고 있습니다. 그러나 필요에 따라 추가 메서드를 정의할 수 있으며, 이런 변경은 me와 같은 모든 인스턴스에 반영됩니다.

 

 

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

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

const me = new Person('Lee');
const you = new Person('Kim');

me.sayHello(); // Hi! My name is Lee
you.sayHello(); // Hi! My name is Kim

프로토타입은 객체이므로 일반 객체와 같이 프로토타입에도 프로퍼티를 추가/삭제할 수 있고,

이런 수정사항은 프로토타입 체인에 즉각 반영된다.

Person 생성자 함수를 통해 생성된 모든 객체는 프로토타입에 추가된 sayHello 메서드를 상속받아 사용할 수 있다.

19.7 프로토타입 체인

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

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

const me = new Person('Lee');

// 프로토타입 확인
console.log(Object.getPrototypeOf(me) === Person.prototype); // true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // true

 

me 객체의 프로토타입은 Person.prototype이고, Person.prototype의 프로토타입은 Object.prototype이다.
프로토타입의 프로토타입은 언제나 Object.prototype이다.

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

이를 포로토타입 체인이라 한다.

프로토타입 체인은 자바스크립트가 객체지향 프로그래밍의 상속을 구현하는 메커니즘이다.

 

me.hasOwnProperty('name')과 같이 메서드를 호출하면 자바스크립트 엔진은 다음과 같은 과정을 거쳐 메서드를 검색한다. 물론 프로퍼티를 참조하는 경우도 마찬가지다.

  1. hasOwnProperty 메서드를 호출한 me 객체에서 hasOwnProperty 메서드를 검색한다. me 객체에는 hasOwnProperty 메서드가 없으므로 [[Prototype]] 내부 슬롯에 바인딩되어 있는 프로토타입으로 이동하여 hasOwnProperty 메서드를 검색한다.
  2. Person.prototype에도 hasOwnProperty 메서드가 없으므로 프로토타입 체인을 따라 [[Prototype]] 내부 슬롯에 바인딩되어 있는 프로토타입으로 이동하여 hasOwnProperty 메서드를 검색한다.
  3. Object.prototype에는 hasOwnProperty 메서드가 존재한다. 자바스크립트 엔진은 Object.hasOwnProperty 메서드를 호출하고, 이때 Object.hasOwnProperty 메서드의 this에는 me 객체가 바인딩된다.

 

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