[REAL Deep Dive into JS] 19. 프로토타입

young_pallete·2022년 9월 18일
0

REAL JavaScript Deep Dive

목록 보기
19/46

🚦본론

자바스크립트를 처음 마주할 때 어려울 수 있는 파트에요. (저도 처음엔 어려웠어요!)
하지만, 우리가 객체를 사용하는 이유를 깊게 생각해보면, 프로토타입이라는 것이 어떻게, 왜 필요한 것인지 이해할 수 있어요. 😉

🙆🏻 너무 어렵다고 생각하지 말고, 천천히 한 번 살펴 나가볼까요?

객체와 프로토타입

프로그래밍은 현실의 문제를 해결하기 위한 수단 중 하나에요.
그리고 우리는 답을 구하기 위해, 현실의 어떤 것을 특정 데이터로 정의해야 하죠.
이것이 추상화에요. 어떤 것에 대한 특성을 간추려서 표현하는 거죠!

그리고 객체는, 이러한 추상화의 대상이며, 추상화된 동작과 속성을 토대로 우리는 문제를 해결하는 겁니다.

그런데 말이죠. 만약에 어떤 특성이 같은 객체들을 무수히 많이 만들어야 한다면 어떨까요?
17장에서, 우리는 반복적인 객체 리터럴 할당을 효율적으로 해결하기 위해 생성자 함수를 사용하는 것을 확인했어요!

한 번 예시를 가져와 볼게요.

function Person(name, age) {
	this.name = name;
  	this.age = age;
    this.introduce = function() {
    	return `hi! my name is ${this.name}`
    }
}

const jaeyoung = new Person('jaeyoung', 29)
console.log(jaeyoung.introduce()); // hi! my name is jaeyoung

const sunyoung = new Person('sunyoung', 30)
console.log(jaeyoung.introduce()); // hi! my name is sunyoung

위의 코드는 사실 동작에서는 큰 문제가 되지 않아요.
다만 이것이 최적화된 코드냐고 하면, 아쉬운 부분이 존재합니다.

객체를 생성자 함수로 생성한다는 것은 효율적이지만, 사실 introduce라는 메서드는 로직이 이미 정해져 있기 때문에, 굳이 계속해서 만들 필요가 없어요.

그런데, 메서드는 프로퍼티이며, 프로퍼티는 함수라는 값을 할당해줘야 하죠.
따라서 중복되는 함수가 모든 객체를 생성할 때마다 추가되는 거에요.

지금은 문제 없지만, 만약 10억 개를 생성한다면? 점점 성능 차이가 눈에 들어오겠죠?!

이런 이유에서, 프로토타입을 토대로, 상속을 구현할 수 있답니다.

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

Person.prototype.introduce = function() {
  return `hi! my name is ${this.name}`
}

const jaeyoung = new Person('jaeyoung', 29)
const sunyoung = new Person('sunyoung', 30)
console.log(jaeyoung.introduce === sunyoung.introduce) // true, 같은 주소 값을 공유하게 됨. (상속)

❓아니, 이제 메모리 최적화를 위해 필요한 것은 알겠어요.
그런데 프로토타입이 도대체 뭐죠?

아마 많은 분들께서, 이에 대한 질문을 찾느라 이 글까지 오게 되셨을 거 같아요!
어렵게 설명하지 않고, 쉽게 설명해볼게요. 천천히 따라가보자구요! 😉

프로토타입 객체란

책의 정의는 다음과 같이 기술되어 있어요!

객체지향 프로그래밍의 근간을 이루는 객체간 상속을 구현하기 위해 사용하며, 상위 객체의 역할을 하는 객체로서 다른 객체 공유 프로퍼티를 제공하는 객체.

어유... 어떻게 보면 참 난감할 정도로 개념이 어렵게 정의되어 있어요.
우리, 이렇게 너무 원문적으로 이해하지 말고, 좀 더 풀어서 이해해보자구요.

한 번 다음을 이렇게 바꿔서 읽어보실까요?

  • 상위 객체의 역할을 하는 객체 - 상위 호환스러운 특성들을 담은 객체
  • 다른 객체 공유 프로퍼티 - 공통된 DNA

객체지향 프로그래밍의 근간을 이루는 객체간 상속을 구현하기 위해 사용하며, 상위 호환스러운 특성을 담은 공통된 DNA를 제공하는 객체.

이해가 되시나요? 아직 이해가 안 되시는 분들을 위해 좀 더 풀어 설명해볼게요.
우리, 어떻게 사냥이라는 것을 배웠을까요?
이는 다양한 이유가 있겠지만, 사람이라는 생물 자체에는 사냥이라는 본능이 존재하는 거죠.

생물은 선천적으로 어떤 특성을 물려 받아요. 그리고 이러한 문제를 우리는 프로그래밍에서 객체로 추상화하죠.

프로토타입은 여기서 사냥이라는 본능에 해당됩니다. 그저 처음부터 원래 이런 속성과 동작을 가진 친구야!라고 정의해준 거에요.

그럼 하나하나 일일이 다시 또 정의할 필요가 없겠죠? 그저 사냥이란 건, 나라는 객체가 조상으로부터 물려받은, DNA와 같은 본능이니까요.

🌈 따라서 프로그래밍이 현실을 추상화하여 접근하고 해결해나간다면, 프로토타입 객체는 마치 공통된 특성을 한데 모아준 상위 개념의 객체(소위 종특)이라고 생각하면 되겠어요!

이런 관점에서 다시 예제를 살펴 보죠.

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

Person.prototype.introduce = function() {
  return `hi! my name is ${this.name}`
}

const jaeyoung = new Person('jaeyoung', 29)
const sunyoung = new Person('sunyoung', 30)
console.log(jaeyoung.introduce === sunyoung.introduce) // true, 같은 주소 값을 공유하게 됨. (상속)

결국, jaeyoungsunyoungPerson을 통해 name 프로퍼티와 age라는 프로퍼티를 가졌지만, Person이 가진 프로토타입 객체로부터 마치 DNA처럼 물려받아 introduce라는 메서드를 상속받을 수 있게 된 겁니다!


__proto__

인스턴스가 자신의 프로토타입인 [[Prototype]]에 간접 접근하기 위해 사용하는 접근자 프로퍼티에요.
이를 통해 자신의 프로토타입을 볼 수 있답니다.

이때 주의해야 할 것은, __proto__는 엄밀히 말하자면 Object.prototype의 프로퍼티라는 점! 즉, Object라는 객체에서 대대로 내려오는 프로토타입인 거에요 😉

이를 사용하는 이유는 상호참조를 막기 위해서라고 하는데요.

const a = {};
const b = {};

a.__proto__ = b;
b.__proto__ = a; // Uncaught TypeError: Cyclic __proto__ value

기본적으로 프로토타입도 스코프 체인처럼 체이닝 방식을 위해, 단방향 링크드 리스트 자료구조를 채택해요.

따라서 순환참조가 일어나서는 안되기 때문에, __proto__라는 것을 만들어서, 간접적으로 접근하는 것이랍니다 😉 이러한 __proto__라는 것은 ES6부터 표준이 되었어요!

⚠️ 다만 직접 상속을 할 때에는 Object.prototype을 상속받지 않는 객체를 생성할 수 있어서, 위 프로퍼티를 사용하는 것은 권장하지 않는답니다! 대신, Object.getPrototypeOf를 사용하기를 추천해요.

const obj = Object.create(null); // root of prototype chain
console.log(obj.__proto__) // undefined; 이미 종점이므로, 상위 개념의 객체가 존재하지 않는다.

console.log(Object.getPrototypeOf(obj)); // null;
Object.setPrototypeOf(obj, { a: 1 }) // 프로토타입을 직접 설정할 수도 있답니다!

함수의 프로토타입

함수는 결국 생성자 함수를 통해 객체를 생성할 수 있는데요! 이러한 조건은 constructor여야 합니다.

contructor가 있는 함수는, prototype이라는 것을 프로퍼티로 가질 수 있고, 그 친구들은 생성할 인스턴스의 프로토타입이 되는 거에요! 💪🏻

즉, 함수의 프로토타입은 곧 만들어낼 객체들의 DNA를 정의한 거겠죠?

constructorprototype

모든 프로토타입은 constructor을 갖고 있어요.
이 친구는 생성자라는 의미 그대로 자신을 참조하는 생성자 함수를 가리켜요.
따라서 곧 constructor은 함수 정의 때 생성되는 값을 가리킨다고 할 수 있겠네요!

따라서 Person이라는 생성자 함수가 있다고 치자구요.그렇다면 Person의 프로토타입 프로퍼티가 존재하게 되는데요! 이를 통해서 특이한 것을 확인할 수 있어요.

const a = {};
a.constructor // f Object() { [native code] }

응? Object객체가 생성자로 나오는군요!
실제로 객체리터럴은 자바스크립트 엔진 내부의 추상연산 OrdinaryObjectCreate에 의해 암묵적으로 Object.prototype객체를 프로토타입으로 갖게 된다고 합니다 😉

이를 통해 여러 값들의 생성자 함수들도 찾을 수 있어요.

const a = '123';
console.log(a.constructor) // f String() { [native code] }
const b = 123;
console.log(b.constructor) // f Number() { [native code] }
const c = () => {};
console.log(c.constructor) // f Function() { [native code] }
const d = true;
console.log(d.constructor) // f Boolean() { [native code] }

결국 모든 값들을 할당할 때는 위처럼, 어떤 특정한 추상연산을 거치게 되고, 결과적으로 자신의 내부 슬롯인[[Prototype]]에 이를 할당 받아 각각의 생성자 함수의 프로토타입을 갖게 되는 거에요!

그리고 이 종점에는, Object가 있다는 것! 참고해주세요 🥰

프로토타입 생성 시점

이건 생각해보면 간단합니다. 생성자 함수라는 것을 통해서, 프로토타입을 갖게 된다고 했죠?
따라서 이를 바꿔서 말하면, 생성자 함수가 생성되는 순간에 프로토타입이 생성됩니다!


프로토타입 체인

쉽게 말하자면, 이러한 프로토타입도 마치 체이닝 방식으로 동작해요.
이는 스코프 체인의 방식과 유사하다고 보면 돼요. 현재 프로토타입 객체에서 탐색이 불가능하면, 찾을 때까지 상위 프로토타입 객체로 계속해서 탐색해요. 결국에는 종점까지 접근해나가는 방식입니다!

이 종점은 Object.prototype이 있어요 😉 (결국, 자바스크립트의 모든 것은 객체니까요!)

💡 결국 이전에 살펴 보았던 스코프 체인을 이해했다면 쉽죠? 역시 모든 지식들은 이어져 있어요! 🥰

오버라이딩과 섀도잉

이것도 굳이 어렵게 설명할 이유가 없을 거 같아요.
스코프에서 지역변수와 전역변수를 생각하면 쉬운데요!
지역 스코프에서 해당 지역 스코프에 등록된 변수는 전역변수보다 우선해서 적용됩니다.
이를 shadow variable라고 표현도 해요! (ESLint에서는 이 규칙을 적용하기 위해 no-shadow로 컨트롤 가능합니다!)

오버라이딩은 이처럼 상위 프로토타입 객체의 메서드를 현재 프로토타입 객체에 같은 프로퍼티명으로 덧대어 정의해서 우선적으로 적용되게끔 하는 거에요!

반대로 섀도잉은 이렇게 오버라이딩으로 인해 프로토타입 객체의 프로퍼티가 적용되지 않는 현상을 말하는 거구요 😉


프로토타입의 교체

🚨 음... 웬만하면 최대한 하지 마세요! 오히려 추적을 어렵게 합니다!

웬만하면 지양할 방식인 것 같습니다. 실제로 자바스크립트는 프로토타입을 동적으로 변경할 수 있어요.
하지만 저는 사용하기를 권장하지는 않아요. 그 이유는, 잘못하면 필수적인 프로토타입 프로퍼티가 생략되어 원치 않은 동작이 발생할 수 있기 때문입니다.

무엇보다 인스턴스의 프로토타입 프로퍼티인 constructor와 생성자 함수의 연결이 파괴되기 때문에, 기존 프로토타입 체인의 흐름을 알기 어렵게 해요.

💡 따라서 제 생각에는 정말 필요한 경우가 아니라면, 이 방식은 지양하는 게 좋을 거 같아요!

const Person = (function() {
	function Person(name) {
    	this.name = name;
    }
  
    Person.prototype = {
    	constructor: Person,
        sayHello() {
          console.log('Hello!');
        }
    }
  
  	return Person;
}());

const me = new Person('Hwang');

const nextPrototype = {
  sayHello() {
  	console.log('Hi!');
  }
};

Object.setPrototypeOf(me, nextPrototype);

me.sayHello(); // Hi!

정적 프로퍼티/메서드

그런데 모든 것들을, 굳이 프로토타입으로 전달할 필요는 없겠죠?
심지어 인스턴스에 계속해서 같은 내용들이 들어간다는 것은 비효율적이기도 하고요.

이런 경우에는, 정적 프로퍼티/메서드를 활용할 수 있어요.
Object.keys와 같은 것들이 대표적인 정적 프로퍼티 메서드에요! 🥰

이를 통해 추가한다면, 프로토타입 체인에서 존재하지는 않지만, 해당 생성자 함수로만 호출 가능한 프로퍼티/메서드를 만들어낼 수 있어요!

const Person = (function() {
	function Person(name) {
    	this.name = name;
    }
  
    Person.prototype = {
    	constructor: Person,
        sayHello() {
          console.log('Hello!');
        }
    }
  
  	return Person;
}());

Person.nickname = 'JengYoung';
Person.sayNickname = function() {
	console.log(`Hi, ${this.nickname}`); 
}

const sister = new Person('sunyoung');
sister.nickname // undefined;

instanceof

이제부터는 사실상 계속해서 같은 주제에 대한 응용입니다!

A instanceof B의 방식으로 사용할 수 있어요. 이때의 값은 Boolean으로 나타납니다.

이는 쉽게 말하자면, A가 B의 인스턴스인지를 확인하는 건데요. 이 기준은 B의 프로토타입 객체가, A의 프로토타입 체인 상에 존재하는지로 판단합니다!

// Object는 모든 생성자 함수의 프로토타입 체인의 종점에 위치하므로 true
Array instanceof Object // true;
String instanceof Object // true;

직접 상속

이 친구는 추상 연산을 내부적으로 호출해서 프로토타입을 지정한 객체를 생성해주어요!
대표적으로 Object.create가 있어요.

이 메서드는 [[프로토타입으로 지정할 객체]], [[프로퍼티 키, 프로퍼티 디스크립터 객체로 이루어진 객체]]를 매개변수로 받는데요, 2번째 인수는 optional합니다!

const test = { a: 1 };
const testObj = Object.create(test);

console.log(testObj) // {}
console.log(testObj.a) // 1

프로퍼티 존재 확인

크게 어려운 게 아니니, 간단한 설명만 하고 넘어가죠!

  • A in B: in을 통해 A가 B의 프로퍼티인지 확인할 수 있어요!
  • Object.prototype.hasOwnProperty(keyName): hasOwnPeroperty라는 프로토타입 메서드를 활용하여 확인도 가능하답니다!

프로퍼티 열거

이는 for ... in ...을 통해 열거가 가능하죠!
주의할 건, 상속받은 프로퍼티 모두를 열거합니다. 다만 대부분의 내장 메서드가 나오지 않는 이유는, 프로퍼티 어트리뷰트인 enumerablefalse로 되어있기 때문이에요!

const person = {
	name: 'jaeyoung' 
}

person.__proto__.age = 29; // person의 프로토타입 객체에 접근하여, 프로퍼티 정의

console.log(person) // { name: 'jaeyoung' }

for (let myAttribute in person) {
	console.log(myAttribute) // name age
}

따라서 이는 프로토타입 프로퍼티를 열거하기 싫은 경우 원치 않는 결과가 발생해요.
따라서 이럴 때에는

  • Object.keys: 현재 객체의 고유 프로퍼티 키를 담은 배열
  • Object.values: 현재 객체의 고유 프로퍼티 값을 담은 배열
    +Object.entries: 현재 객체의 [고유 프로퍼티 키, 프로퍼티 값]을 값으로 가지는 2차원 배열

와 같은 Object의 정적 메서드를 사용하기를 권장해요!

🌈 마치며

후... 생각보다 양이 엄~청 많았네요.
아무래도 프로토타입을 제대로 이해해야, 우리가 평소에 쓰는 객체의 프로퍼티와 메서드들이 어떨 때 접근할 수 있고, 없는지 알 수 있기 때문이에요.

결국 자바스크립트라는 것은 객체로 이루어져 있고, 이를 가능케하는 것은 프로토타입이라는 강력한 상속방식이 있다는 것! 꼭 기억하면 좋을 것 같아요.

그럼, 다들 즐거운 공부하시길 바라요. 이상!

profile
People are scared of falling to the bottom but born from there. What they've lost is nth. 😉

0개의 댓글