자바스크립트 딥다이브 - 프로토타입

ChoiYongHyeun·2023년 12월 14일
0

내용이 상당히 많고 복잡해서 공부하는데 3일이나 넘게 걸렸다.

자바스크립트는 명령형 , 함수형, 프로토타입 기반, 객체지향 프로그래밍을 지원하는 멀티 패러다임 프로그래밍 이다.

객체 지향 프로그래밍

개체지향 프로그래밍은 프로그램을 명령어 또는 함수의 목록으로 보는 명령형 프로그래밍 의 절차지향형 관점을 벗어나 여러 개의 독립적인 단위인 객체의 집합으로 프로그램을 표현하려고 하는 프로그래밍 패러다임을 의미한다.

실세계의 사물이나 개념들은 특징이나 성질을 나타내는 속성 (attribute)을 가지고 있다.

사람은 여러가지 특징과 성질을 가지고 있지만, 그 중 필요한 속성들만 가지고 객체를 생성해보자

let person = {
  name: 'tom',
  age: 16,
};

이처럼 다양한 속성 중 프로그램에 필요한 속성만 간추려 표현하는 것을 추상화라고 한다.

추상화

추상화는 객체지향 프로그래밍에서 중요한 개념 중 하나다.

  • 중요한 특징만 포착
    현실 세계의 개체나 시스템에서 핵심적인 특징만 포착하고 나머지는 무시함으로서 복잡성을 간소화한다.

  • 세부 사항 숨김
    불필요한 세부사항을 숨기고 필요한 부분만 노출하여 사용자가 해당 개념을 더 쉽게 이해할 수 있게 돕는다.

  • 모델링
    추상화는 모델을 만들어내는 과정이며, 모델은 실제 세계의 시스템을 단순화하고 표현한 것이다.
    이처럼 속성을 통해 여러개의 값을 하나의 단위로 구성한 복합적인 자료구조객체라고 한다.

이 때 객체에는 상태를 나타내는 데이터와, 데이터를 조작할 수 있는 동작을 하나의 논리저인 단위로 묶어 사용한다.

상태를 나타내는 데이터를 property , 동작을 method 라 부른다.

정리

객체지향 프로그래밍은 독립적인 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임이다.
객체는 객체의 상태를 나타내는 프로퍼티와 메소드로 이뤄진 복합적인 자료구조이다.

프로토타입과 상속

상속은 객체지향 프로그래밍의 핵심 개념이다.

어떤 객체의 프로퍼티 또는 메소드를 다른 객체가 상속받아 사용할 수 있는 것을 말한다.

프로토타입

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

를 통해 person 이라는 생성자 함수를 정의하였다.

이렇게 되면 person이란 생성자 함수는 프로퍼티로 name 을 갖고 , 생성자 함수를 호출 할 때 받은 인자를 name 프로퍼티의 프로퍼티 값으로 설정하여 객체를 반환한다.

이것이 우리가 여태까지 배운 생성자 함수로 인스턴스를 생성하는 방법이였다.

한 발자국만 더 나아가 보자

생성자 함수는 생성될 때 사용자가 설정하지 않아도 prototype 이란 객체를 생성해둔다.

일단은 아 생성자 함수를 생성하면 내부 프로퍼티로 prototytpe 이란 객체를 같이 생성해두는구나 ? 만 알고 있어보자

상속

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

let tom = new person('tom');
let jerry = new person('jerry');

이렇게 person 생성자 함수로 tom , jerry 라는 인스턴스를 만들어주었다.

이 때 모든 person 으로 생성된 인스턴스들에게 프로퍼티를 추가하거나 , 메소드를 추가하고 싶다면 어떻게 할까 ?

모두에게 자기 소개를 하는 메소드를 추가 해보자

function person(name) {
  this.name = name;
  this.introduce = function () {
    console.log(`hi i am ${this.name}`);
  };
}

let tom = new person('tom');
let jerry = new person('jerry');

tom.introduce(); // hi i am tom

이렇게 생성자 함수에 메소드를 추가해주면 인스턴스들도 메소드를 사용 할 수 있을 것이다.

하지만 이는 메모리 관리 관점에서 매우 비효율적이다.

introduce 메소드는 어떤 매개변수도 받지 않는 항상 같은 메소드인데 인스턴스가 생성 될 때 마다

메모리에 할당 되기 때문이다.

만약 인스턴스를 1억개를 만든다면 1억개의 똑같은 introduce 메소드가 메모리에 할당된다.

이를 상속 이란 개념을 통해서 해결 할 수 있다.

상속에 대해 깊게 들여다보기

생성자 함수 도 함수이며, 결국 함수 또한 객체이다.

위에서는 여태 배운 내용들을 가지고 설명하려고 하니 반복적으로 생성자 함수 라고 말을 했지만

생성자 함수 를 상위 객체, 인스턴스 를 하위 객체로 바라 볼 수 있다.

이 때 하위 객체들이 상위 객체의 어떤 속성이나 기능을 공유 할 때, 하위 객체들은 상위 객체로부터 상속 받는다고 한다.

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

person.prototype.introduce = function () {
  console.log(`hi i am ${this.name}`);
};

let tom = new person('tom');
let jerry = new person('jerry');

tom.introduce(); // hi i am tom

person 상위 객체에서 인스턴스들이 공유 할 프로퍼티나 메소드를 담은 객체에 동적으로 introduce 를 할당해주었다.

이후 인스턴스들은 상위 객체의 prototype 객체를 상속 받아 prototype 내에 존재하는 프로퍼티나 메소드 를 사용 할 수 있다.

이렇게 하게되면 인스턴스 들은 공유되는 메소드를 독립적인 메모리에 할당 할 필요 없이 상위 객체 가 지정한 prototytpe 객체가 저장된 메모리를 참조하면 되기 때문에 메모리를 불필요하게 사용 할 필요가 없다.

또한 상위 객체들도 인스턴스에게 상속 하고 싶은 프로퍼티나 메소드가 있으면 모든 인스턴스들에 대해서 설정해줄 필요 없이 prototype 객체에서 추가하거나 삭제, 수정 해주면 된다.

상속과 프로토타입 정리

생성자 함수인 상위 객체 는 선언 될 때 prototype 이란 프로퍼티를 가지고 있으며 prototype 프로퍼티는 인스턴스 들에게 공유할 프로퍼티 , 메소드 를 담고 있는 객체이다.

인스턴스 들은 상위 객체prototype 객체를 상속 받아 상위 객체에서 지정한 프로퍼티나 메소드를 사용 할 수 있다.

프로토타입 체인

위에서 프로토타입을 이용한 상속에 대해 알아보았다.

상속을 통해 인스턴스는 상위 객체에서 공유하는 프로토타입 을 사용 할 수 있다.

좀 더 예시를 통해 알아보자

function Veichle() {}

Veichle.prototype.drive = function () {
  console.log('앞으로가유');
};

function car() {
  this.seat = 4;
}
car.prototype = new Veichle();
car.prototype.wheel = 4;

function autobycycle() {
  this.seat = 2;
}
autobycycle.prototype = new Veichle();
autobycycle.prototype.wheel = 2;

let bmw = new car();
let kawasaki = new autobycycle();

위 코드에서 가장 상위 객체은 Vehicle 이 존재하고

그 밑에 car , autobycycleVehicle을 상속 받은 인스턴스이자 생성자 함수 (객체)이다.

생성자 함수가 다른 생성자 함수를 상속받기 위해 prototype 객체를 다른 생성자 함수 객체로 설정해주었다.

그 밑에 bmw , kawasakicar , autobycycle 로 생성된 인스턴스이다.

해당 모습을 도식화 하면 다음과 같다.

다음처럼 상속된 객체들간의 관계를 프로토타입 체인 이라고 한다.

프로토타입 체인 정의

객체간 상속을 구현하는 메커니즘

프로토타입 체인을 통해 상속 받은 프로퍼티들을 사용하는 예시를 살펴보자

console.log(bmw.wheel); // 4 (autobycycle의 프로퍼티)
bmw.drive(); // 앞으로 가유 (vehicle의 프로퍼티)

console.log(kawasaki.wheel); // 2 (car의 프로퍼티)
kawasaki.drive(); // 앞으로 가유 (vehicle 의 프로퍼티)

bmw , kawasaki 객체 자체에는 wheel 이란 프로퍼티도 , drive() 라는 메소드도 존재하지 않는다.

하지만 프로토타입 체인을 통해 상위 객체들의 프로퍼티들을 사용 할 수 있다.

위 도식화에서 살펴 볼 수 있듯이 프로토타입 체인은 단방향으로 이뤄져 있으며

프로퍼티를 호출하는 인스턴스를 기준으로 상위 객체를 향해 올라가며 프로퍼티나 메소드를 탐색한다.

그럼 이번에는 새로운 인스턴스를 생성해보자

let porsche = new car();
porsche.drive = function () {
  console.log('겁나게 빨리가유');
};

porsche 라는 인스턴스를 생성하고 porsche 에 프로퍼티를 drive 로 새로운 것을 할당해주었다.

그리고 proschedrive 를 실행하면 ?

porsche.drive() // 겁나게 빨리가유

porsche 내부에 존재하는 프로퍼티인 drive 가 먼저 실행된다.

이처럼 프로토타입 체인은 프로퍼티나 메소드를 호출한 위치로부터

자신이 소유한 프로퍼티를 먼저 탐색 => 존재하지 않으면 상위 객체가 소유한 프로퍼티 탐색 => 상위 객체의 프로토타입 => ... 최상위 계층이 소유한 프로퍼티 탐색 => 최상의 계층의 프로토타입 탐색 한다.

프로토타입 체인 정리

프로토타입 체인이란 객체들의 상속 관계를 나타내는 메커니즘으로 단방향 연결리스트 로 구성되어 있다.
인스턴스의 프로퍼티나 메소드를 호출하면 인스턴스 내부를 살펴보고 최상위 객체까지 호출한 프로퍼티나 메소드를 찾을 때 까지 올라가며 참조한다.

프로토타입 객체

위에서 모든 생성자 함수(상위 객체가 될 수 있는)는 선언과 함께 prototype 프로퍼티가 내부적으로 생성된다고 하였다.

그럼 prototype 객체에는 무엇이 들어있을까 ?

function Veichle() {}

내부에 존재하는 prototype 객체 또한 Veichle 로 인해 생성된 하나의 객체이다.

prototype 객체또한 Veichle 로 인해 생성된 인스턴스로 볼 수 있으며

인스턴스 내부 프로퍼티에는 자신을 생성한 생성자를 가리키는 Constructor 프로퍼티가 존재한다.

우리는 프로토타입 체인은 상속 받은 prototype 들을 타고 올라가 상위 객체를 향해 간다고하였다.

위 도식화에서 bmwcar.prototype 객체를 상속 받았다고 하였다.

function Veichle() {}

Veichle.prototype.drive = function () {
  console.log('앞으로가유');
};

function car() {
  this.seat = 4;
}
car.prototype = new Veichle();
car.prototype.wheel = 4;

function autobycycle() {
  this.seat = 2;
}
autobycycle.prototype = new Veichle();
autobycycle.prototype.wheel = 2;

let bmw = new car();
let kawasaki = new autobycycle();

bmw

[[Prototype]] 은 자신이 상속 받은 prototype 객체를 의미한다.

bmw 인스턴스는 car.prototype 을 상속받았으며 carVeichle.prototype 을 상속 받는다.

prototype 은 자신을 생성한 생성자 함수를 가리키는 프로퍼티인 constructor 를 가지고 있기 때문에 prototype에 존재하는 constructor 프로퍼티를 이용하며 상위 객체로 올라가며 탐색하는 것이 가능하다.

프로토타입 객체 정리

프로토타입 객체는 상위 객체가 될 수 있는 생성자 함수가 선언됨과 동시에 생성되는 객체이다>
프로토타입 객체 는 생성과 동시에 자신을 생성한 생성자 함수를 가리키는 constructor 프로퍼티를 가지고 있다.
프로토타입 객체는 인스턴스에게 상속되는 객체인데, 상속된 프로토타입 객체constructor 프로퍼티를 통해 프로토타입 체인을 따라 상위 객체로 접근하는 것이 가능하다.

__proto__

정의

모든 객체는 __proto__ 를 통해 자신에게 상속된 프로토타입 , 즉 [[Prototype]] 내부 슬롯에 접근 할 수 있다.

bmw.__proto__

bmwcar 로 인해 생성된 인스턴스이지만 상속 받은 prototypecarprototypeVeichle 이다.

그로 인해 Veichle을 가리키고 있는 모습을 볼 수 있다.

__proto__[[Prototype]] 내부 슬롯에 접근하는 메소드라고 하였는데, 이처럼 접근하는 메소드를 접근자 프로퍼티 라고 한다고 하였다.

접근자 프로퍼티 는 호출하는 getter 함수와 설정하는 setter 함수로 이뤄져있다고 하였다.

let obj = {}
let parent = {x : 1}

console.log(obj.__proto__);
obj.__proto__ = parent
console.log(obj.__proto__);

이처럼 __proto__ 를 통해 obj 가 상속 받은 prototype 에 접근 할 수도 있고
__proto__ = 상속할 객체 를 통해서 parent 객체를 프로토타입으로 상속 받는 것도 가능하다.

의문이 들 수 있다.
obj 는 어떤 프로토타입도 상속 안받고 객체 리터럴로 생성했는데 왜 prototype 이 존재하지 ?
이것은 빌트인 생성자 함수인 Object 로 생성되었기 때문이며, Object로 생성되어 Object 의 프로토타입을 상속 받았기 때문에 상속받은 프로토타입이 존재한다.

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

위에서는 생성자 함수를 선언하면 prototype 객체가 자동으로 생성된다고 하였다.

그럼 객체를 생성하면 사용 가능한 __proto__ 도 객체가 생성되면 객체의 프로퍼티로 설정된 내부 메소드일까 ?

반은 맞고 반은 틀리다.

객체를 생성하면 __proto__ 를 사용 가능한 것은 맞다.

하지만 이것은 객체가 생성되면 모든 객체는 Object() 생성자 함수를 이용해 생성 된 것이기 때문에

Object 의 프로토타입을 자동으로 상속 받고, Object.prototype 내부에 존재하는 메소드인 __proto__ 를 사용 할 수 있는 것이다.

let obj = { name: 'lee' };

console.log(obj.hasOwnProperty('__proto__')); // false
console.log(Object.getOwnPropertyDescriptor(Object.prototype, '__proto__'));
/*
{
  get: [Function: get __proto__],
  set: [Function: set __proto__],
  enumerable: false,
  configurable: true
}
*/

hasOwnProperty(프로퍼티명) 는 해당 객체의 프로퍼티가 상속받은 것이 아닌 해당 객체 자신만의 프로퍼티임을 묻는 것인데 이에선 false 가 나오고

Object.prototype 객체가 가지고 있는 __proto__ 란 프로퍼티에 대한 설명을 물어보면 get , set 등 프로퍼티의 어트리뷰트들이 나온다.

이를 통해 __proto__ 프로퍼티는 Object.prototype 을 상속받아 사용 가능한 프로퍼티라는 것을 알 수 있다.

__proto__ 를 사용해 프로토타입에 접근하는 이유

[[Prototype]] 에 접근하기 위해 접근자 프로퍼티를 사용하는 이유는

상호 참조를 방지하기 위함이다.

let child = {};
let parent = {};

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

프로토타입 체인은 단방향 연결리스트 형태여야 하는데 상호 참조하게 되면 무한 로프에 빠질 수 있기 때문에

상호 참조를 방지하기 위해 접근자 프로포티를 사용한다.

웬만하면 __proto__ 사용하지 마세유

아니 실컷 __proto__ 설명해놓고 사용하지 말라고 하면

모든 객체가 Object 의 프로토타입을 상속받는 것이 아니다.

직접 상속을 통해 Object.prototype 을 상속받지 않는 객체를 생성 할 수도 있다.

let obj = Object.create(null); // 직접 상속을 통해 obj 생성
// obj 는 Object 의 프로토타입을 상속받지 않아 obj는 프로토타입 체인의 ㅈ오점이다.

console.log(obj.__proto__); // undefined
console.log(Object.getPrototypeOf(obj)); // null

그렇기 때문에 어떤 객체가 상속받은 프로토타입을 확인할 때에는 Object.getPrototypeOf 를 사용하는 것이 바람직하다.

프로로타입을 설정 할 때는 Object.setPrototypeOf 를 사용하면 된다.

.prototype vs __proto__

두개 모두 상속 시키거나, 상속 받을 프로로타입 객체를 가리킨다는 공통점이 있지만

다른 부분들이 존재한다.

속성 .prototype __proto__
사용 목적 생성자 함수가 객체 인스턴스에게 상속할 프로퍼티와 메소드를 정의 객체 인스턴스가 자신의 프로토타입 객체를 가리키는 속성
존재하는 위치 생성자 함수 내에서 사용됨 객체 인스턴스에서 사용됨
종속성 생성자 함수에만 해당 모든 객체 인스턴스가 공통적으로 사용
예시
function Example() {}
Example.prototype.method = function() {
  // ...
};
        
const obj = new Example();
console.log(obj.__proto__);
        
# 객체 리터럴로 생성한 객체

이전 챕터에서 객체 리터럴로 생성한 객체도 Object 생성자 함수로 생성한 객체로 취급한다고 했었다.

이는 자바스크립트의 추상 연산 (OrdinaryObjectCreate) 를 호출하여 Object.prototype 을 갖게 하기 때문이다.

let obj1 = new Object({ a: 1 });
let obj2 = { a: 1 };

console.log(obj1.constructor); // [Function: Object]
console.log(obj2.constructor); // [Function: Object]

추상 연산

추상 연산은 언어 사양에서 사용되는 일련의 동작을 나타내는 명세서이다. 이러한 추상 연산은 언어의 여러 부분에서 사용되는 동작이나 변환을 표현하며, 구현자들이 언어를 일관되게 구현하도록 도와준다.

추상 연산 정의
ToPrimitive (input [, preferredType]) 객체를 해당하는 원시 값으로 변환
GetValue (V) 주어진 값을 평가하고 반환
ToBoolean (argument) 주어진 값을 불리언 값으로 변환
ToString (argument) 주어진 값을 문자열로 변환
ToNumber (argument) 주어진 값을 숫자로 변환
IsCallable (argument) 주어진 값이 호출 가능한지 여부 확인
IsArray (argument) 주어진 값이 배열인지 여부 확인
IsObject (argument) 주어진 값이 객체인지 여부 확인
IsUndefined (argument) 주어진 값이 `undefined`인지 여부 확인
IsNaN (argument) 주어진 값이 NaN (숫자가 아님)인지 여부 확인
let obj1 = new Object(123);
console.log(obj1); // [Number: 123]

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

추상 연산의 예시이다.

객체 리터럴로 생성된 객체는 추상 연산을 통해 안의 값을 추상 연산하여 객체 형태로 반환한다.

생성 과정에서 미묘한 차이는 있지만 동일한 특성과 프로토타입을 갖기 때문에 같게 생각해도 크게 무리는 없다.

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

이전 도식화에서 Vehicle 객체로부터 프로토타입 체인이 쭉쭉 내려가는 모습을 보였다.

이 때 사용된 Vehicle 은 프로토타입 체인의 종점처럼 표현했지만 사실 Vehicle 도 객체이기에

다른 생성자 함수로 생성된 객체이다.

그것은 바로 ~~~

전역 객체

전역 객체는 코드가 실행되기 이전 단계에서 자바스크립트 엔진에 의해 생성되는 특수한 객체를 말한다.

전역 환경이란 브라우저 환경에선 window , Nodejs 환경에선 global 을 의미한다.

전역 객체는 표준 빌트인 객체 Object . String , Number , Function , Array ... 등과 Var 키워드로 선언한 전역 변수 및 전역 함수를 프로퍼티로 갖는다.

Math , Reflect , JSON 을 제외하고 표준 빌트인 객체는 모두 생성자 함수이다.

자세한 내용은 다음에 ..

하지만 모든 객체 프로토타입 종점에는 전역 객체가 존재한다는 것을 명심해두자

console.log(global.Object === Object); // true

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

객체는 프로퍼티와 메소드로 이뤄진 복잡한 자료구조이다.

이 때 객체의 프로퍼티 및 메소드는 인스턴스 내부에 존재 할 수도 있고 프로토타입이 소유한 프로퍼티 일 수도 있다.

이 때 인스턴스가 소유한 프로퍼티 및 메소드를 인스턴스 프로퍼티 , 프로토타입이 소유한 프로퍼티 및 메소드를 프로토타입 프로퍼티라고 한다.

function Dog(name) {
  this.name = name;
  this.introduce = function () {
    console.log(`hi i am ${this.name}`);
  };
}

Dog.prototype.bark = function () {
  console.log('왕왕');
};

let bbomi = new Dog('bbomi');

다음 같은 코드가 있을 때 뽀미는 Dog 의 인스턴스 프로퍼티와 프로토타입 프로퍼티를 상속 받은 인스턴스이다.

이 때 뽀미는 매우 예의가 바른 강아지라서 자신을 소개 할 때 격식을 차린다고 해보자

bbomi.introduce = function () {
  console.log(`hello gentle man and ladies , i am ${this.name}`);
};

bbomi.introduce(); // hello gentle man and ladies , i am bbomi

상속 받은 introduce 프로퍼티가 존재하였으나 뽀미의 인스턴스의 프로퍼티로 동일한 프로퍼티를 수정하였다.

이 때 상속 시킨 부모 객체의 introduce 프로퍼티는 변하지 않고, 뽀미 인스턴스의 프로퍼티만 수정된다.

이처럼 상속 받은 프로퍼티 (메소드 포함) 을 수정하는 행위를 오버라이딩 이라고 한다.

뽀미가 introduce 프로퍼티를 수정하였기 때문에 Dogintroduce 는 뽀미에게는 사용되지 않는다.

프로토타입 체인에 의해 뽀미가 introduce 라는 프로퍼티를 가지고 있기 때문에 부모 객체까지 탐색하지 않고 뽀미의 프로퍼티가 호출되었다.

이처럼 상속 시킨 프로퍼티가 가려지는 행위를 프로퍼티 쉐도잉 이라고 한다.

동일한 매개변수를 가지고 프로퍼티를 덮는 행위를 오버 라이딩이라고 하고

기존 프로퍼티와 다른 매개변수를 받아 덮는 행위는 오버로딩이라고 한다.

bbomi.introduce = function (friendName) {
  console.log(`hello Mr /Ms ${friendName} , i am ${this.name}`);
};

bbomi.introduce('yongdol'); // hello Mr /Ms yongdol , i am bbomi

상위 객체와 다르게 이번엔 friendName 이란 매개 변수를 받아 프로퍼티를 덮었다.

이는 위에서 말한 오버로딩에 해당한다.

상속 관계에서의 프로퍼티 삭제

뽀미 예시를 가지고 계속 진행해보자

function Dog(name) {
  this.name = name;
  this.introduce = function () {
    console.log(`hi i am ${this.name}`);
  };
}

Dog.prototype.bark = function () {
  console.log('왕왕');
};

let bbomi = new Dog('bbomi');

bbomi.introduce = function (friendName) {
  console.log(`hello Mr /Ms ${friendName} , i am ${this.name}`);
};

bbomi.bark = function () {
  console.log('왈왈왈크르르르르르릉');
};

이번에는 인스턴스 프로퍼티와 프로토타입 프로퍼티 모두 오버라이딩을 했다.

모두 삭제해보자

프로퍼티를 삭제하는 삭제선언문은 delete 라고 하였다.

delete bbomi.bark; // 프로토타입 프로퍼티 삭제
delete bbomi.introduce; // 인스턴스 프로퍼티 삭제

console.log(bbomi.bark()); // 왕왕
console.log(bbomi.introduce()); // TypeError: badduk.introduce is not a function

오버라이딩 한 프로토타입 프로퍼티를 삭제하면 프로토타입 체인을 따라 상위 객체의 프로퍼티인 bark 가 실행된다.

하지만 오버라이딩한 인스턴스 프로퍼티를 삭제하면 상위 객체의 프로토타입을 참조하고, 상위 객체의 프로토타입안에는 introduce 가 존재하지 않아 참조하지 못한다.

프로토타입 프로퍼티를 완전히 제거하기 위해서는 상위 객체의 프로토타입에 접근하여 프로퍼티를 완전히 제거하거나

delete Dog.prototype.introduce

인스턴스의 프로퍼티를 undefined 로 변경하는 방법이 있다.

bbomi.bark = undefined

프로토타입의 교체

프로토타입은 객체임으로 다른 객체로 변경하는 것이 가능하다.

이러한 특징을 활용하여 객체 간의 상속 관계를 동적으로 변경 할 수 있다.

Dog 이란 생성자 함수를 굳이 즉시 실행 함수로 모듈화 하여 사용했는지 모르겠지만 서적에서 이렇게 했으니 따라가도록 하겠다.

다음처럼 생성자 함수인 Dog 의 프로토타입을 {bark : ...} 로 감싸진 객체로 변경했다.

그렇게 생성된 bbomi 인스턴스를 살펴보면 상속받은 프로토타입이 Dog 가 아닌 Object 임을 알 수 있다.

하지만 Dog 생성자 함수로 인해 생성되었기 때문에 Dog 로 생성된 객체임은 Dog {name : bbomi} 을 통해 알 수 있다.

이는 생성자 함수인 Dog 의 프로토타입이 {bark : ..} 로 변경되면서 constructorDog 를 가리키고 있는 것이 아닌

{bark : ... } 자체 객체가 상속받은 프로토타입이 constructor : Object 를 가리키고 있기 때문이다.

{bark : ... } 또한 빌트인 생성자 함수인 Obejct 로 생성된 객체이기 때문에 상속 받은 프로토타입이 존재한다.

이처럼 상속 관계를 유지하기 위해서는 상속 받는 prototype 객체에 상속 관계를 유지할 constructor 프로퍼티가 필요하다.

const myPet = (function () {
  function Dog(name) {
    this.name = name;
  }

  Dog.prototype = {
    constructor: Dog,
    bark: function () {
      console.log('왕왕!');
    },
  };

  return Dog;
})();

let bbomi = new myPet('bbomi');

console.log(bbomi); // Dog { name: 'badduk' }
console.log(bbomi.constructor); // [Function: Dog]

이처럼 상속 줄 prototype 객체를 변경하여 상속 관계를 제거하거나 변경하는 것이 가능하다.

물론 상속 받은 입장에서 __proto__ 를 이용해 상위 받은 prototype 을 변경 하는 방법이나
Object.setPropertyOf('객체명' , '프로토타입 객체') 를 통해 수정하는 것도 가능하다.

하지만 프로토타입 교체를 통해 객체 간의 상속 관계를 동적으로 변경하는 것은 꽤나 번거롭다.

따라서 프로토타입은 직접 교체하지 않는 편이 좋고 , 상속 관계를 인위적으로 설정 하려면 곧 살펴볼 직접 상속 이 더 편리하고 안전하다.

또는 ES6 에서 도입된 class 문법을 이용하는 편이 좋다.

instanceof 연산자

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

우변의 피연산자가 함수가 아닌 경우 TypeError 가 발생한다.

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

let tom = new Person('tom');

console.log(tom instanceof Person); // true
console.log(tom instanceof Object); // true
console.log(tom instanceof String); // false
console.log(Person instanceof Object); // true

프로토타입 체인을 따라 상위 계층의 객체까지 모두 포함한다.

생성자 함수도 객체이기 때문에 instanceof 를 통해 확인 할 수 있다.

instanceof 는 생성자 함수를 가리키는 constructor 를 따라 확인하는걸까 ? 아니면 prototype 으로 확인을 하는걸까 ?

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

let tom = new Person('tom');
const parent = { x: 1 };

console.log('tom 의 prototype 을 parent 로 설정 후');
Object.setPrototypeOf(tom, parent);
console.log(`tom.__proto__ : ${tom.__proto__}`);
console.log(`tom instanceof Person : ${tom instanceof Person}`);
console.log(`tom instanceof Object : ${tom instanceof Object}`);

Person.prototype = parent;
console.log('----------------------------------------');
console.log('Person 의 prototype = parent 설정 후');
console.log(`tom instanceof Person : ${tom instanceof Person}`);

다음처럼 설정했을 때 어떤 결과가 나올 것 같은지 생각해보자

tom 의 prototype 을 parent 로 설정 후

tomPerson 으로 생성된 인스턴스이지만 상속 받은 prototypeparent 객체이다.
tom이 상속받은 prototypePersonprototype 이 아니며
parent 객체는 Object 의 프로토타입을 상속 받았기 때문에

tom 의 prototype 을 parent 로 설정 후
tom.__proto__ : [object Object]
tom instanceof Person : false
tom instanceof Object : true

결과는 다음과 같다.

Person 의 prototype = parent 설정 후

Personprototypeparent 객체로 설정 된 경우
tom 이 상속 받은 prototypeparent 객체 , Personprototypeparent 객체이므로

Person 의 prototype = parent 설정 후
tom instanceof Person : true

결과는 다음과 같다.

결국 instanceOf 는 프로토타입 체인을 따라 해당 인스턴스가 상속 받은 프로토타입이 상위 객체의 프로로타입인지를 확인하는 것이다.

재귀 함수로 구현해보자

function IsInstanceOf(instance, constructor) {
  let prototype = Object.getPrototypeOf(instance);

  if (prototype === null) return false; //프로토타입이 존재하지 않으면 최상위 계층이기 때문에 false 반환

  return (
    prototype === constructor.prototype || IsInstanceOf(prototype, constructor)
  );
}

재귀함수는 항상 볼 때마다 헷갈린다.

매개변수로는 인스턴스와 생성자 함수를 받는다. (instance , constructor)

재귀함수의 중단 조건은 prototype === null 일 경우이구나

그 말은 최상위 객체까지 탐색하였을 때의 프로토타입은 null 이기 때문에 그럴 경우 false 를 반환하란 거구나

그 다음 반환문을 보면 단축 연산 을 이용하는 것을 볼 수 있다.

||or 을 이용하는 연산으로 만약 prototype === constructor.prototype 이면 true 를 반환하고 멈춘다.

만약 좌항이 false 일 경우 우항에 존재하는 재귀 함수가 발동하여 prototypeinstance 로 받아 확인한다.

console.log(IsInstanceOf(tom, Person)); // true
console.log(IsInstanceOf(tom, Object)); // true

결국 어떤 인스턴스의 프로토타입 체인에 어떤 생성자 함수가 존재하는지를 확인 하는 것은 해당 생성자 함수의 프로토타입과 인스턴스 가 상속받은 프로토타입들이 동일한지를 확인 하는 것이다.

이번엔 constructor 를 제거한 프로토타입을 상속받은 인스턴스 를 생성하여 확인해보자

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

  Person.prototype = {
    x: 1,
  };
  return Person;
})();

const tom = new MakePerson('tom');
console.log(tom.constructor); // Object
console.log(tom.constructor === MakePerson); // false
console.log(tom instanceof MakePerson); // true

tom.constructor === Object인 이유

  1. tomprototype{x:1} 이라는 객체이다.
  2. tomconstructor 를 부르면 tom의 인스턴스 프로퍼티를 확인한다.
  3. 없기 때문에 tom 이 상속 받은 프로토타입을 확인한다.
  4. {x:1} 객체 내 인스턴스 프로퍼티를 확인한다.
  5. 없기 때문에 {x:1} 이 상속받은 프로토타입을 확인한다.
  6. {constructor : Object , ... } 내에 constructor 가 존재하기 때문에 Object를 반환
    결국 프로토타입 체인을 통해 존재하는 프로퍼티 ({x:1}Object 에게서 상속받은 프로토타입을 참조)를 반환한 것이다.

instanceof 연산자 정리

어떤 인스턴스가 어떤 생성자 함수의 프로토타입을 상속받았는지를 확인하는 instanceof 연산자는
우항이 소유하고 있는 프로토타입이 좌항이 상속 받은 프로트타입과 동일한지 좌항의 프로토타입 체인을 따라 재귀적으로 확인한다.

직접 상속

우리는 현재 생성자 함수를 이용해 객체를 생성하면, 생성자 함수에 존재하는 prototytpe 객체들 또한 상속되며 , 이런 상속 관계 (프로토타입 체인) 을 통해서 상위 객체의 프로퍼티나 메소드에 접근 할 수 있다고 하였다.

이번에는 생성자 함수를 이용하지 않고 프로퍼티를 직접 객체 형태로 만들어 상속 시키는 직접 상속에 대해 알아보자

let proto = {
  introduce() {
    console.log(`hello~ i am ${this.name}`);
  },
}; // 상속 시킬 프로토타입 객체 생성 

let tom = Object.create(proto, {
  name: {
    value: 'tom',
    writable: true,
    enumerable: true,
    configurable: true,
  },
}); 

console.log(tom);
tom.introduce(); // hello ~ i am tom

Object.create(상속 시킬 프토토타입 객체 , 프로퍼티 디스크립터 형태의 객체) 를 통해 객체에게 프로토타입을 직접 상속 시킬 수 있다.

직접 상속을 하면 편리한 점은 다음과 같다.

  1. new 연산자 없이도 객체를 생성 할 수 있다.
  2. 프로토타입을 지정하면서 객체를 생성 할 수 있다.
  3. 객체 리터럴에 의해 생성된 객체도 상속 받을 수 있다.

Object.create() 를 사용하면 생성자 함수 사용 없이도 객체 리터럴을 이용해 프로토타입 체인에 속하는 객체를 생성 할 수 있다.

이 때 Object.create() 에 사용된 프로토타입 객체의 프로토타입 체인 또한 같이 연결된다.

프로토타입 체인을 동적으로 조절할 수 있기 때문에

빌트인 생성자 함수인 Object 의 프로토타입을 상속받지 않는 객체를 생성하는 것도 가능하다.

let tom = Object.create(null, {
  name: {
    value: 'tom',
    writable: true,
    enumerable: true,
    configurable: true,
  },
});

let jerry = { name: 1 };

function logPrototypeChain(instance) {
  while (instance) {
    console.log(instance);
    instance = Object.getPrototypeOf(instance);
  }
}

logPrototypeChain(tom);
logPrototypeChain(jerry);

그럼으로 객체는 프로토타입 체인에서 Object 의 프로토타입을 상속 받지 않았을 수도 있기 때문에

최상위 빌트인 객체의 프로토타입을 사용 할 때 바로 불러내어 사용 하는 것은 추천되지 않는다.

/*
jerry : 객체 리터럴로 생성하여 Object 의 프로토타입을 상속받음
tom : Object.create(null) 로 생성하여 어떤 프로토타입도 상속받지 않음
*/
console.log(jerry.hasOwnProperty('name')); // true
console.log(tom.hasOwnProperty('name')); // TypeError: tom.hasOwnProperty is not a function

따라서

console.log(Object.hasOwnProperty.call(jerry, 'name')); // name
console.log(Object.hasOwnProperty.call(tom, 'name')); // name 

다음처럼 call 을 이용해 최상위 빌트인 객체의 프로토타입을 호출하는 것을 권장한다고 한다.

객체 리터럴에서 __proto__를 이용한 직접 상속

let proto = { age: 16 };
let obj = {
  name: 'lee',
  __proto__: proto,
};

console.log(Object.getPrototypeOf(obj)); // { age: 16 }

객체 리터럴로 객체를 생성 할 때 __proto__ 를 직접 프로퍼티로 불러 상속 시키는 방법도 있으나

웬만하면 __proto__ 를 이용해 프로토타입에 접근하지 말라고 했기 때문에 추천하는 방식은 아니라고 한다.

정적 메소드

//  생성자 함수 생성 및 인스턴스 프로퍼티 설정
function Person(name) {
  this.name = name;
  this.age = 16;
  this.introduce = function () {
    console.log(`hi i am ${this.name}`);
  };
}

// 프로토타입 메소드 추가

Person.prototype.sing = function sing() {
  console.log('lalala~~');
};

let tom = new Person('tom');
tom.introduce(); // hi i am tom
tom.sing(); // lalala~~

생성자 함수를 이용한 상속에서

생성자 함수를 선언하고 생성자 함수를 통해 생성한 인스턴스는 생성자 함수의 인스턴스 프로퍼티프로토타입 프로퍼티 를 상속받는다고 하였다.

하지만 생성자 함수 자체에서 본인의 프로퍼티들을 가지고 동작만 하고 싶고 인스턴스에겐 상속시키지 않는 프로퍼티들도 분명 존재한다.

이를 정적 메소드 or 정적 프로퍼티라고 한다.

//  생성자 함수 생성 및 인스턴스 프로퍼티 설정
function Person(name) {
  this.name = name;
  this.age = 16;
  this.introduce = function () {
    console.log(`hi i am ${this.name}`);
  };
}

// 프로토타입 메소드 추가

Person.prototype.sing = function sing() {
  console.log('lalala~~');
};

// 정적 메소드 추가
Person.twerk = function twerk() {
  console.log('twerk twerk');
};

let tom = new Person('tom');
tom.introduce(); // hi i am tom
tom.sing(); // lalala~~
Person.twerk(); // twerk twerk
tom.twerk(); // TypeError: tom.twerk is not a function

정적 메소드 , 정적 프로퍼티는 생성자 함수 선언 이후 동적으로 추가된 프로퍼티나 메소드 를 의미한다.

이는 생성자 함수에서만 호출이 가능하며 인스턴스에서는 호출이 불가능하다.

let tom = { name: 'tom' };
let jerry = { name: 'jerry' };

console.log(Object.getOwnPropertyDescriptors(jerry));
/*
{
  name: {
    value: 'jerry',
    writable: true,
    enumerable: true,
    configurable: true
  }
}
(/

console.log(tom.getOwnPropertyDescriptors(jerry)); // TypeError: tom.getOwnPropertyDescriptors is not a function

마치 tomObject 과 프로토타입 체인연결되어 있으나 Object.getOwnPropertyDescriptors 는 사용 불가능 하고 Object 생성자 함수로만 사용 할 수 있듯이 말이다.

프로퍼티 존재 확인

in 연산자

in 연산자는 이항 연산자로 좌항의 프로퍼티 키가 우항의 객체에 존재하는지를 묻는다.

let tom = {
  name: 'tom',
  age: 16,
};

console.log('name' in tom); // true 
console.log('age' in tom); // true
console.log('toString' in tom); // true (상속받은 프로퍼티들 또한 묻는다)
console.log('address' in tom); // false	

in 연산자는 상속받은 프로퍼티들에 대한 내용들 조차도 평가한다는 것을 기억하자.

Object.prototype.hasOwnProperty 연산자

Object.prototype.hasOwnPropertyObjectprototype 객체에 존재하는 프로퍼티로

인스턴스가 상속받지 않고 생성한 프로퍼티들이 존재하는지를 묻는다.

let tom = {
  name: 'tom',
  age: 16,
};

console.log(tom.hasOwnProperty('name')); // true
console.log(Object.prototype.hasOwnProperty.call(tom, 'name')); // true

다음처럼 Object.prototype 을 상속 받았음을 가정하고 불러도 되고

아니면 Object 객체와 call 을 이용한 간접 호출을 이용하여 부를 수도 있다.

프로퍼티 열거

for in

for 문은 반복문이라고 하였다.

let tom = {
  name: 'tom',
  age: 16,
};

for (let key in tom) {
  console.log(key);
} // name age

그런데 in 문은 상속받은 프로퍼티 키들도 불러온다고 하지 않았나 ?

맞아용

그런데 상속받은 프로퍼티 키들이 나타나지 않는 이유는 상속받은 프로퍼티 키들이 호출은 가능해도 나열하는 것이 불가능하기 때문이다.

즉 프로퍼티 어트리뷰트 중 enumerablefalse 로 되어있기 때문이다.

for in 문은 프로퍼티 어트리뷰트중 enumerable : true 인 프로퍼티들에 대해서 모두 나열한다.

그럼 만약 상속된 프로퍼티들 중 enumerable : true 인 프로퍼티들도 있을 텐데 상속 안받고 본인만의 프로퍼티를 나열하고 싶다면 ?

function person(name) {
  this.name = name;
}
person.prototype.introduce = function introduce() {
  console.log(`hi i am ${this.name}`);
};

let tom = new person('tom');

for (let key in tom) {
  console.log(key); // name introduce
}

상속받은 프로퍼티인 introduce 까지 같이 로그 된다.

for (let key in tom) {
  if (tom.hasOwnProperty(key)) { // 본인만의 프로퍼티인지 확인 후 로그
    console.log(key); // name
  }
}

본인만의 프로퍼티인지 확인하는 조건문을 넣어주면 된다.

Object. keys/values/entries

function person(name) {
  this.name = name;
  this.address = 'korea';
}
person.prototype.introduce = function introduce() {
  console.log(`hi i am ${this.name}`);
};

let tom = new person('tom');

console.log(Object.keys(tom)); // [ 'name', 'address' ] 
console.log(Object.values(tom)); // [ 'tom', 'korea' ]
console.log(Object.entries(tom)); // [ [ 'name', 'tom' ], [ 'address', 'korea' ] ]

Object 의 정적 메소드를 이용해 객체의 프로퍼티, 프로퍼티 값 , 프로퍼티 키와 값 쌍을 배열 형태로 반환 할 수 있다.

이 때 설명한 key , values , entries 는 상속받지 않고 본인만의 고유한 프로퍼티들만을 반환하기에 불필요한 조건문을 넣을 필요가 없다.

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글