[Javascript] 프로토타입 총정리 ① | 프로토타입의 정의, 특징 및 생성법

Re_Go·2023년 12월 17일
0

Javascript

목록 보기
19/44
post-thumbnail

1. 객체지향 프로그래밍의 역사

자바스크립트는 클래스 기반의 객체지향 프로그래밍 언어(Java)보다 효율적이며, 더 강력한 객체지향 프로그래밍 능력을 지니고 있는 프로토타입 기반의 객체지향 프로그래밍 입니다.

이는 원시 타입을 제외한 나머지 값들이 객체인 것을 보면 더욱 명확히 알 수 있는데요. 이를 좀더 알기 위해서는 객체지향 프로그래밍이 등장한 배경에 대해 알아볼 필요가 있습니다.

객체지향 프로그래밍은 프로그램을 데이터 접근과 함수를 따로 보는 전통적인 명령형 프로그래밍의 특성인 C언어와 같은 절차지향적 프로그래밍의 관점에서 탈피하여 이들을 하나로 묶은 객체들 여러 개의 독립적 단위, 즉 객체의 집합으로 프로그램을 표현하려는 패러다임입니다.

이러한 자바와 같은 객체지향 프로그래밍의 관점은 실체에 대한 속성을 구별하는 관점이 핵심이며, 하나의 대상이 가지는 여러 개의 속성 중 필요한 속성망 간추려 표현해내는 것을 추상화(absctaction)라고 합니다.

// ① 특정 대상(사람)의 인적사항(속성)을 추상화한 예시) 즉 객체는 속성을 통해 여러 개의 값을 하나의 단위로 구성하는 복합적인 자료구조를 뜻하며, 객체지향 프로그래밍은 이러한 독립적인 객체의 집합으로 프로그램을 표현하고자 하는 프로그래밍 패러다임인 것입니다.
//
const rego = { //rego 라는 사람은
    name : "Parkjongmin", // 이름이 다음과 같고
    age : 30, // 나이는 다음과 같으며
    address : "Seoul" // 주소도 다음과 같은 인적사항(속성)을 가지고 있다.
  	sayHello : function(){
    console.log(`안녕하세요? 저는 ${this.name}입니다!)}, // 인사를 할 경우 다음과 같이 인사를 한다는 특징(기능)을 가지고 있습니다.
};

console.log(rego); // rego라는 객체(대상)의 이름과 나이, 주소라는 속성과 그 값을 출력합니다.

이처럼 객체지향 프로그래밍은 객체의 상태를 나타내는 데이터와 그 데이터를 조작할 수 있는 동작을 하나의 논리적인 단위로 묶어 생각합니다. 즉 객체는 상태 데이터(프로퍼티)와 동작(메서드)을 하나의 논리적인 단위(객체)로 묶은 복합적인 자료구조인 것이죠.

2. 상속의 정의

이러한 객체지향 프로그래밍의 핵심은 단연 '상속' 이라고 해도 과언이 아닐 것인데, 이는 어떠한 객체의 프로퍼티 혹은 메서드를 다른 객체가 상속 받아 그대로 사용하는 것을 뜻합니다. 이는 기존의 데이터를 상속 받아 불필요한 중복 데이터의 남용을 막는데 보다 효과적인 방법이기도 하죠.

즉 생성자 함수에 의해 생성된 각각의 인스턴스들은 생성자 함수의 메서드들 또한 각각 가지고 있을텐데, 이러한 메서드들을 굳이 각각의 메서드들을 가지고 있을 필요 없이 자신의 상위 객체(부모이자 생성자 함수의 prototype)로부터 메서드를 상속 받아 공유하듯 사용하면 굳이 공통되는 작업을 개별 인스턴스가 각각 가지고 있을 필요가 없어지므로 불필요한 코드 및 메모리 누수를 최소화 할 수 있는데, 이것이 바로 상속이 가지는 강점이라고 할 수 있습니다.

// ① 프로토타입을 사용해 특정 함수를 상속 받는 전형적인 예시 코드) 여기서 Circle.prototype의 의미는 각 인스턴스들의 부모 객체로 이해하면 쉽습니다.
function Circle(radius){
    this.radius = radius;
} // 각 인스턴스마다 고유한 원의 반지름 값을 가지고 있되

Circle.prototype.getArea = function(){ // Circle의 프로토타입 메서드로 원의 둘레를 반환하는 코드 구현하면
    return Math.PI * this.radius ** 2; // 
}

const circle1 = new Circle(1);
const circle2 = new Circle(2); // 각각 1과 2를 반지름으로 갖고 있는 circle1와 2 인스턴스를 생성시

console.log(circle1.getArea === circle2.getArea); // 원 1과 2는 같은 함수 내에서 동일한 메서드를 상속을 받고 있으므로 true값이 출력되고

console.log(circle1.hasOwnProperty('getArea')); // 비록 이 두 인스턴스들은 getArea 메서드를 직접 가지고 있지는 않으나
console.log(circle2.hasOwnProperty('getArea'));

console.log(circle1.getArea()); // Circle의 프로토타입 메서드인 getArea를 공유하여 사용이 가능합니다.
console.log(circle2.getArea());

3. 프로토타입?

앞서 살펴본 바와 같이 각 인스턴스들은 생성자 함수(부모 객체)의 프로토타입으로부터 특정 변수나 메서드를 상속받을수 있었음을 확인할 수 있었습니다.

프로토타입이란 자식 객체들이 공유하게 될 부모 객체의 별도의 공간(객체)라고 보면 되는데요. 객체마다 자신의 constructor(누구로부터 생성 되었는지에 대한 정보를 가지고 있는 프로퍼티) 프로퍼티를 확인 함으로서 해당 프로토타입이 무엇인지를 유추해낼 수 있지만, 보통 객체의 프로퍼티 정보를 알아내기 위해서는

그리고 이러한 프로토타입은 인스턴스들 뿐만이 아니라 모든 객체들이 가지고 있는데, 이들이 상속 받고 있는 프로토타입 객체가 무엇인지에 대한 정보를 담고 있는 프로퍼티 저장소(내부 슬롯에) 이 직접 접근하는 것은 어려우나, Object의 프로토타입 프로퍼티인 proto 접근자 프로퍼티를 사용해 간접적으로나마 접근이 가능합니다.

하지만 이러한 접근자 프로퍼티(proto)는 코드 내에서 직접 사용하는 것은 권장되지 않으며, 만약 프로토타입의 참조를 취득하고 싶은 경우, ES6부터 추가된 Object.getPrototypeOf 메서드를 사용하고, 프로토타입을 교체하고 싶을 경우 Object.setPrototypeOf 메서드의 사용이 권장됩니다.

// ① 접근자 프로퍼티(__proto__)를 활용하여 객체의 프로토타입을 얻어오고, 다시 설정하는 코드 예시.

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

obj.__proto__; // obj의 프로토타입을 getter 함수를 통해 확인
obj.__proto__ = parent; // obj의 프로토타입을 setter 함수를 통해 parent 프로토타입 객체로 지정

console.log(obj.x); // obj는 부모 객체인 parent의 x변수를 상속 받게 됨.

// ② Obj의 프로토타입 접근 메서드를 활용한 프로토타입 설정 코드 예시)

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

Object.getPrototypeOf(obj); // Object 빌트인 객체의 getPrototypeOf 메서드를 이용해 obj의 프로토타입을 확인.
Object.setPrototypeOf(obj, parent); // 다시 Object의 setPrototypeOf 메서드를 이용해 obj의 프로토타입을 parent로 설정하면 parent에 선언되어있는 변수와 메서드 등을 상속받게 됩니다. 

console.log(obj.x); // obj는 parent의 변수를 성공적으로 상속.

★ 어떠한 객체의 프로토타입 지정은 프로토타입 객체를 지정하지 않고 일반 객체로도 지정이 가능합니다. 다시 말해 프로토타입 객체 뿐만 아니라 일반 객체 또한 위의 코드 예시처럼 어떠한 객체의 프로토타입이 될 수 있다는 뜻입니다.

4. 프로토타입 객체의 생성 시점

이러한 프로토타입(프로토타입 객체)의 생성 시점은 생성자 함수가 생성되는 시점에 같이 생성됩니다. 이는 생성자 함수와 프로토타입이 쌍으로 존재하기 때문인데요.

앞서 설명한 constructor를 가지지 않고 있는 메서드 축약 표현에 의한 함수나 화살표 함수를 제외하고 나머지 함수 종류를 통해 new 키워드와 같이 생성자 함수가 생성될 경우(런타임 이전에 정의 될 때) 이때 프로토 타입 객체도 같이 생성되게 됩니다.

이때 생성된 프로토타입은 객체의 형태로 생성되며, 초기에는 constructor 프로퍼티만을 가지고 있게 됩니다. 그리고 이렇게 생성된 프로토타입 객체에 상속 변수나 상속 함수를 만들게 되면 동일한 생성자 함수에 의해 생성된 인스턴스들은 생성자 함수의 프로토타입 객체가 가지고 있는 상속 변수나 함수를 참조하여 공통적으로 사용할 수 있게 되는 것입니다.

// ① constructor 함수의 프로토타입 생성 시점) 호이스팅에 의해 함수는 런타임 이전에 선언되고, 이때 함수의 프로토 타입 객체 또한 생성되게 되므로 프로토 타입 또한 호이스팅의 영향에 따라 런타임 이전에 접근이 가능합니다.

console.log(Person.prototype); // 런타임 이전에 프로토타입 객체가 생성되므로 접근 가능

function Person(name){
    console.log(name);
}

console.log(Person.prototype);


// ② non-constructor 함수의 프로토타입 생성 시점) non-constructor의 객체는 프로토타입 객체를 생성하지 않기 때문에 해당 함수를 통해 prototype의 정보를 확인하려 할 때 undefined를 반환 받습니다.

const Person = name =>{ // 화살표 함수로 생성한 객체는 non-constructor 이기 때문에 프로토타입 객체 또한 생성되지 않습니다.
    this.name = name
    return console.log(`저는 ${this.name} 입니다.`)
}

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

5. 객체의 생성 방식과 이에 따른 프로토타입 객체의 결정

객체를 생성하는 방법은 다양하게 존재합니다. 객체 리터럴로 직접 생성할 수도 있고, 빌트인 생성자 함수 중 하나인 Object와 new 연산자를 함꼐 사용해 객체를 활용하거나, 일반 생성자 함수를 사용하거나, Object의 메서드중 하나인 create로 객체를 생성하거나, ES6에 도입된 클래스를 통해 객체를 생성하는 방법등이 대표적으로 거론됩니다.

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

객체 리터럴로 객체를 생성할 경우 해당 객체는 Object 빌트인 생성자 함수에 의해 생성된 인스턴스(객체)로 취급되기 때문에 당연히 Object의 프로토타입 객체를 상속받아 그 객체가 가지고 있는 프로퍼티와 메서드들을 사용할 수 있게 됩니다.


const obj = {x : 1} // 타입은 Object이므로 Object.prototype을 상속받게 됩니다.
console.log(Object.getPrototypeOf(obj))

② Object 생성자 함수에 의해 생성한 객체와 프로토타입

Object 빌트인 함수로 객체를 생성할 경우 마찬가지로 자바스크립트 엔진은 추상 연산을 호출하면서 해당 객체의 프로토타입으로 Object.prototype을 전달합니다. 즉 객체 리터럴로 생성한 객체와 동일한 프로토타입을 갖게 된다는 것이죠.

const obj  = new Object();
obj.x = 1;
console.log(Object.getPrototypeOf(obj))

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

생성자 함수에 의해 인스턴스가 만들어질떄 자바스크립트 엔진은 추상 연산에 해당하는 생성자 함수가 정의될 때 같이 만들어진 프로토타입 객체를 보내게 됩니다. 그리고 이렇게 생성된 인스턴스는 해당 생성자 함수의 프로토타입 객체의 프로퍼티와 메서드를 공유하게 됩니다. 이때 프로토타입 객체에는 constructor 프로퍼티만을 가지게 됩니다.


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

Person.prototype.sayHello = function (){
    console.log(`안녕하세요. 저는 ${this.name} 입니다!`);
}

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

me.sayHello(); // 객체 각각의 값은 별개이되 프로토타입 메서드는 공유하므로 정상적으로 사용 가능.
kim.sayHello();

④ Object.create 메서드에 의해 생성된 객체의 프로토타입

Object.create에 의해 특정 객체의 인스턴스를 생성 후 obj 변수에 할당합니다. 이때 선택된 변수가 객체일 경우 Object.prototype이 선택되는데, 만일 배열이라고 한다면 Array.prototype 을 상속받게 될것입니다. 즉 Object.create에 의해 생성 된 인스턴스는 전달 된 객체의 상태에 따라 프로토타입 또한 바뀌게 됨을 의미합니다.

정리하자면 첫번째 매개변수는 상속 받을 프로토타입이나 객체를, 두번째 값은 생성될 해당 인스턴스의 프로퍼티를 정의하여 직접적으로 프로토타입을 상속 받을 수 있기에 편리한 기능이라고 할 수 있습니다.

단 모든 객체들은 프로토타입 체인의 종점인 Object.prototype이기에 이 프로토타입에 포함되어있는 여러 메서드들을 사용할 수 있으나, 직접적으로 프로토타입을 null로 설정할 경우, 해당 메서드들을 사용할 수 없기에 Object.create를 통해 상속 받을 프로토타입을 직접 설정해주는 방법에 주의가 필요합니다. 이렇게 프로토타입이 null로 선언된 인스턴스의 프로퍼티를 확인할 경우에는, Object.prototype.hasOwnProperty.call(obj, 'obj의 특정 키') 이런 식으로 찾을 수 있습니다.

const obj = { x: 1 }; // obj 상수를 객체 데이터로 생성 후
const numberIs = Object.create(obj); //  Object.create 메서드로 obj를 프로토타입으로 하는 numberIs 인스턴스 객체를 생성합니다. 이 경우 obj의 x값을 상속 받는데, 주의할 점은 obj를 직접 상속 받는 또 다른 인스턴스가 있을 경우 값을 공유하기 때문에 주의를 해야할 필요가 있습니다.
const numberIs2 = Object.create(Object.prototype, { // 또한 첫번째 매개변수는 지정할 프로토 타입, 두번째 매개변수는 값과 그에 대한 데이터프로퍼티를 설정할 수 있습니다.
  x : {value : 1, writable : true, enumerable : true, configurable: true}
});

⑤ class에 의해 생성된 객체의 프로토타입

class에 의해 생성된 인스턴스는 class.prototype를 상속하게 됩니다.
단 클래스에 프로토타입 메서드를 직접 선언하는 방법은 ES6 클래스 문법에서 지원되지 않기 때문에 프로토타입 상속을 이용한 메서드 공유를 의도하게 된다면 class보다는 일반 생성자 함수를 사용하는 방법을 권장합니다.

// Person 클래스 정의
class Person {
    // 생성자: 이름과 나이를 초기화
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    // 메서드: 인사말 출력
    greet() {
        console.log(`안녕하세요, 제 이름은 ${this.name}이고, 나이는 ${this.age}살입니다.`);
    }

    // 메서드: 나이 한 살 더하기
    haveBirthday() {
        this.age += 1;
        console.log(`생일 축하합니다! 이제 ${this.age}살이 되었습니다.`);
    }
}
profile
인생은 본인의 삶을 곱씹어보는 R과 타인의 삶을 배워 나아가는 L의 연속이다.

0개의 댓글