프로토타입


프로토타입 공부에 앞서 객체지향 프로그래밍(OOP)이란 무엇인가? 

전통적인 명령형 프로그래밍의 절차지향적 관점에서 벗어나 여러 개의 독립적 단위, 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임을 말한다. OOP는 실세계의 실체를 인식하는 철학적 사고를 프로그래밍에 접목하려는 시도에서 시작한다. 특징이나 성질을 나타내는 속성을 가지고 있고, 이를 통해 실체를 인식하거나 구별 할 수 있다. 이처럼 다양한 속성 중에서 프로그램에 필요한 속성만 간추려 내어 표현하는 것을 추상화라고한다. 

프로퍼티 : 객체의 상태를 나타내는 데이터

메소드 : 상태 데이터를 조작할 수 있는 동작을 하나의 논리적인 단위

 

상속과 프로토타입

상속은 어떤 객체의 프로퍼티 또는 메소드를 다른 객체가 상속 받아 그대로 사용할 수 있는 것을 말한다. 자바스크립트는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거하고 코드를 재사용한다.  

prototype을 이용한 메소드 공유

첫 번째 코드 처럼 같은 메소드가 들어있는 생성자 함수로, 인스턴스를 여러개 찍게 되면 각 인스턴스마다 메소드를 갖게 되어 메모리 낭비하는 꼴이 되지만 두번 째 코드처럼 protoype에 해당 메소드 혹은 프로퍼티를 미리 구현해 두면 생성자 함수가 생성할 모든 인스턴스는 별도의 구현없이 상위 객체인 프로토타입의 자산을 공유하게 되면서 성능을 더 좋게하고 메모리를 절약할 수 있다. 

 

프로토타입 객체

프로토타입 객체란 객체지향 프로그래밍의 근간을 이루는 객체 간 상속을 구현하기 위해 사용된다. 프로토타입은 어떤 객체의 상위 객체의 역할을 하는 객체로서 다른 객체에 공유 프로퍼티를 제공한다. 프로퍼타입을 상속받은 하위 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 사용할 수 있다. 

 

__proto__ 접근자 프로퍼티 

모든 객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입, [[prototype]] 내부슬롯에 간접 접근이 가능하다.

1.  __proto__는 접근자 프로퍼티다.

const obj = {};
const parent = { x: 1 };

// getter 함수인 get __proto__가 호출되어 obj 객체의 프로토타입을 취득
obj.__proto__;
// setter함수인 set __proto__가 호출되어 obj 객체의 프로토타입을 교체
obj.__proto__ = parent;

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

 

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

const person = { name: 'Lee' };

// person 객체는 __proto__ 프로퍼티를 소유하지 않는다.
console.log(person.hasOwnProperty('__proto__')); // false

// __proto__ 프로퍼티는 모든 객체의 프로토타입 객체인 Object.prototype의 접근자 프로퍼티다.
console.log(Object.getOwnPropertyDescriptor(Object.prototype, '__proto__'));
// {get: ƒ, set: ƒ, enumerable: false, configurable: true}

// 모든 객체는 Object.prototype의 접근자 프로퍼티 __proto__를 상속받아 사용할 수 있다.
console.log({}.__proto__ === Object.prototype); // true
console.log(person.__proto__ === Object.prototype); // true

person이라는 객체 자체는 _proto__ 프로퍼티를 소유하지않지만,

console.log 찍어보면 상위 object.prototype을 상속받아 사용하는것을 확인

 

3. __proto__ 접근자 프로퍼티를 통해 프로토타입에 접근하는 이유 

const parent = {};
const child = {};

// child의 프로토타입을 parent로 설정
child.__proto__ = parent;
// parent의 프로토타입을 child로 설정
parent.__proto__ = child; // TypeError: Cyclic __proto__ value

Cyclic은 순환이란뜻인데, __proto__로 두 객체가 서로를 상위 프로토타입으로 설정해주려고하면, 순환 참조하는 프로토타입 체인이 만들어지면서 종점이 생기지 않으므로, 무한루프에 빠진다. 이를 방지하기 위해 타입에러가 에러가 나오는 거고, 그래서 프로토타입은 단방향 링크드 리스트로 구현되어야 한다. 

 

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

__proto__ 접근자 프로퍼티 대신 프로토타입의 참조를 취득하고싶은 경우는 Object.getPrototypeOf나 ObjectsetPrototypeOf 메소드를 사용할 것을 권장한다. 

 

 

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

 

생성자 함수에 의해 생성된 인스턴스는 프로토타입의 constructor 프로퍼티에 의해 생성자 함수와 연결된다.

이때 constructor 프로퍼티가 가리키는 생성자 함수는 인스턴스를 생성한 생성자 함수다. 

// 객체 리터럴
const obj = {};

// 함수 리터럴
const add = function (a, b) { return a + b; };

// 배열 리터럴
const arr = [1, 2, 3];

// 정규표현식 리터럴
const regexp = /is/ig;

리터럴 표기법으로 생성한 객체도 프로토타입이 존재한다. Object 생성자 함수에 인수를 전달하지 않거나 undefined또는 null을 인수로 전달하면서 호출하면 추상 연산을 호출하여 object.prototype을 갖는 빈객체를 생성하는데 객체 리터럴도 똑같이 적용되면서, constructor 프로퍼티가 잡히는걸 볼 수 있다. 

 

추상 연산 ( Abstract Operation )

ECMAScript 사양에서 내부 동작의 구현 알고리즘을 표현한 것이다. ECMAScript 사양에서 설명을 위해 사용되는 함수와 유사한 의사 코드

 

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

 

1. 객체 리터럴

자바스크립트 엔진은 객체 리터럴을 평가해여 객체 생성시 추상 연산을 호출한다.

이때 OrdinartyObjectCreate에 전달되는 프로토타입은 Object.prototype이다. 

이로써 객체는 자신의 프로토타입으로 Object.prototype을 상속받았기 때문에 자유롭게 사용 가능하다.

 

2. Object 생성자 함수

 Object 생성자 함수는 일단 빈 객체를 생성하고, 마찬가지로 추상 연산 OrdinaryObjectCreate가 호출된다.

빈 객체는 자신의 프로토타입으로 Object.prototype을 상속받았기 때문에 자유롭게 사용 가능하다.

3. 생성자 함수

생성자 함수가 호출되면 마찬가지로 추상 연산 OrdinaryObjectCreate가 호출이되고, 

여기에 전달되는 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩 되어있는 객체 되시겠다.

사용자 정의 생성자 함수와 더불어 생성된 프로토타입 생성자함수.prototype 프로퍼티는 constructor뿐이다.

여기 생성자함수.prototype에 프로퍼티를 추가하여 하위 객체가 상속 받을 수 있도록 할 수 있다. 

4. Object.create 메소드

5. 클래스 (ES6)

 

프로토타입 체인 

자바스크립트는 객체의 프로퍼티에 접근하려고 할 때 해당 객체에 접근하려는 프로퍼티가 없다면  [[prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색하는 것을 프로토타입 체인이라고 한다. 자바스크립트가 객체지향 프로그래밍의 상속을 구현하는 메커니즘이다.

 

프로토타입 체인의 최상위에 위치하는 객체는 언제나 Object.prototype 일것이며 모든 객체는 이를 상속 받는다. Object.prototype을 프로토타입 체인의 종점 ( end of prototype chain ) 이라고 한다. 

 

자바스크립트에서는 스코프, 프로토타입 체인이 서로 협력하며 검색하며 사용되는데,  간단히 말하자면

- 프로토타입 체인은 상속과 프로퍼티 검색을 위한 메커니즘

- 스코프 체인은 식별자 검색을 위한 메커니즘 

 

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

프로퍼티 섀도잉 - 상속 관계에 의해 프로퍼티가 가려지는 현상을 프로퍼티 새도잉이라 한다.

오버리이딩 - 상위 클래스가 가지고 있는

오버로딩 - 함수의 이름은 동일하지만 매개변수의 타입, 개수가 다른 메소드를 구현하고 매개변수에 의해 메소드를 구별하여 호출하는 방식이다.

 

 

프로토타입 교체

프로토타입은 임의의 다른 객체로 변경할 수 있다. 이것은 부모 객체인 프로토타입을 동적으로 변경할 수 있다는 것을 의미하고 이러한 특징을 활용하여 객체 간 상속 관계를 동적으로 변경할 수 있습니다. 

 

생성자 함수에 의한 프로토타입의 교체 

생성자 함수의 prototype 프로퍼티를 통해 프로토타입을 교체하는 건데, 객체 리터럴에서 constructor 프로퍼티가 없기 때문에 constructor 프로퍼티는 암묵적으로 js 엔진이 추가한다. 하지만 이렇게 교체하면 constructor 프로퍼티와 생성자 함수 간 연결이 파괴되기 때문에 프로토타입으로 교체한 객체 리터럴에 constructor 프로퍼티를 추가하여 연결을 되 살린다.

 

인스턴스에 의한 프로토타입의 교체 

이미 생성된 객체의 프로토타입을 교체하는 것인데, 프로토타입으로 교체한 객체 역시 constructor 프로퍼티가 없기 때문에 생성자 함수간 연결이 파괴된다. 프로토타입으로 교체한 객체 리터럴에 constructor 프로퍼티를 추가하고 생성자 함수의 prototype 프로퍼티를 재설정하여 파괴된 생성자 함수와 프로토타입간 연결을 되 살릴 수 있다. 

 

프로토타입 교체를 통한 상속 관계를 동적으로 변경하는 것은 꽤나 번거롭다. 직접 상속을 사용한 방법이 더 편하고 안전하다고 한다. 

 

instanceof 연산자

이항 연산자로 좌변에 객체를 가리키는 식별자, 우변에 생성자 함수를 가리키는 식별자를 피연산자로 받는다. 
우변이 피연산자가 아닌 경우 Type 에러를 발생한다. 

 

Machine 이라는 생성자 함수를 만들어 a라는 인스턴스를 만들었고,

b라는 빈객체 인스턴스를 만들었습니다. 두개의 인스턴스에 instanceof 를 적용시켜보면 당연히 a는 Machine의 생성자 함수랑 Object는 같은 프로토타입 체인 선상에 있으므로, true가 나오며, b는 프로토타입 체인 선상에 있는 Object와만 true가 나올 것이다

 

 

 

 

 

 

직접 상속

명시적으로 프로토타입을 지정하여 새로운 객체를 생성한다. 

 

 

obj 라는 인스턴스를 Object.create라는 메소드에 매개변수로 null을 넣어 프로토타입을 안가지게 만들었고 instanceof로 확인해보면, 역시나 false가 나옵니다. 하지만 다시 obj에 Object.create 메소드로 Object.prototype을 주입해주면 다시 연결이 된것을 확인 할 수 있습니다. 이 메소드의 장점은 new 연산자가 필요없고, 프로토타입 지정을 하면서 인스턴스를 만들 수 있으며, 객체 리터럴로 인해 생성된 개체에도 주입이 가능합니다. 

 

 

 

 

 

 

ES6 부터 생긴 객체 리터럴 내부에서 __proto__ 접근자 프로퍼티를 사용해 직접 상속을 구현하는 방법도 있다.

 

 

 

 

 

 

 

정적 프로퍼티 / 메서드 

정적 프로퍼티 / 메서드는 생성자 함수로 인스턴스를 생성하지 않아도 참조/ 호출할 수 있는 프로퍼티/ 메소드를 말한다. 

 

 

 

위쪽은 동적 메소드로써 x라는 함수를 넣어서 출력하려는데, 저렇게 바로 사용을 못하고 인스턴스를 만들어줘야지,  prototype에서 타고와서 사용이 가능하지만, 밑에 보면 Func이라는 함수에 직접 x라는 정적 메소드를 바로 넣어줌으로써 인스턴스 생성도 할 필요가 없게만드는 것임을 볼 수 있다. 

 

 

 

 

 

 

 

프로퍼티 존재 확인

in 연산자, Reflect.has() 메소드 

객체 내 특정 프로퍼티가 존재하는지 여부 확인

 

Object.Prototype.hasOwnProperty 메소드

위 연산자와 메소드와 같지만 특징은 상속받은 프로퍼티에 대해서는 false를 반환합니다. 

 

 

 

프로퍼티 열거 

for in 문

프로토타입 체인 상에 존재하는 프로퍼티 중 [[Enumerable]] 값이 true인 프로퍼티를 순회하며 열거

 

Object.keys/values/entries 메소드 

for in문은 상속받은 프로퍼티도 열거하기 때문에, 이 메소드를 사용하는 것을 권장한다고 한다.