[JS] 프로토타입

merci·2023년 8월 3일
0

JavaScript

목록 보기
11/15

자바스크립트는 프로토타입 기반 객체지향 프로그래밍 언어라고 합니다.
프로토타입을 알아보기에 앞서 OOP를 가볍게 살펴보겠습니다.


OOP와 JavaScript

객체지향 프로그래밍은 Java 및 C++를 비롯한 많은 프로그래밍 언어의 기본이 되는 프로그래밍 패러다임으로
주요 개념은 클래스와 인스턴스, 상속과, 캡슐화입니다.

객체는 다른 코드에서 사용할 수 있게 인터페이스를 제공해주고 내부 데이터는 캡슐화하여 비공개 상태를 유지합니다.
ES6 (ECMAScript 2015)에서 도입된 클래스를 이용한다면 아래처럼 만들 수가 있죠.

class Professor {
    constructor(name = null, teaches = null) {
        this.name = name;
        this.teaches = teaches;
    }

    grade(paper) {
    }

    introduceSelf() {
    }
}

const prof1 = new Professor("Dr. Smith", "Physics");  // 두 매개변수 모두 제공
const prof2 = new Professor("Dr. Lee");               // 하나의 매개변수만 제공
const prof3 = new Professor();                        // 매개변수 없음

상속을 한다면 아래처럼 만들 수가 있습니다.

class Student extends Professor  {
    #year;

    constructor(name, year) {
        super(name); // Professor 클래스의 생성자를 호출
        this.#year = year;
    }

    introduceSelf() {
    }

    canStudyArchery() {
        return this.#year > 1;
    }
}

const student = new Student('Weber', 1);
console.log(student.#year); // 오류: SyntaxError: Private field '#year' must be declared in an enclosing class

하지만 자바스크립트는 고전적인 객체 지향 프로그래과는 조금 다릅니다.
클래스 기반의 OOP에서는 객체는 항상 클래스의 인스턴스로 존재하게 됩니다.
하지만 자바스크립트에서는 클래스를 이용하지 않고도 객체리터럴로 객체를 바로 생성할 수 있죠.

let notoBook = {
  cpu: 'i7',
  ram: 16,
  ssd: 'samsung',
}

두번째 차이는 프로토타입 체인을 이용하는것입니다.
프로토타입 체인은 상속보다는 위임에 가까운데 위임을 통해 각 객체의 동작을 __proto__로 연결된 다른 객체로 전달해 수행합니다.

자바스크립트의 클래스도 내부적으로는 프로토타입 체인으로 구성되어 있다고 합니다.
그럼 이제 프로토타입이 뭐고 프로토타입 체인이 뭔지 알아보겠습니다.




프로토타입

프로토타입이란 단어의 뜻을 알아보면 원래의 형태 또는 전형적인 예라고 합니다.
자바스크립트의 모든 객체는 자신의 부모역할을 하는 객체와 연결되어 있습니다.
연결된 객체를 프로토타입 객체 또는 프로토타입이라고 합니다.
이러한 프로토타입 객체는 생성자 함수에 의해 생성된 각각의 객체에 공유 프로퍼티(속성, 메소드)를 제공합니다.

코드 예시를 보며 설명을 드리겠습니다.

function Person() {
  this.eyes = 2;
  this.nose = 1;
}

var kim  = new Person();
var park = new Person();

console.log(kim.eyes);  // => 2
console.log(kim.nose);  // => 1
console.log(park.eyes); // => 2
console.log(park.nose); // => 1

Person 객체를 생성할 때마다 메모리에는 새로운 Person객체가 생성되고 객체마다 가지고 있는 속성데이터도 중복해서 만들어지게 됩니다.

이를 프로토타입을 이용한다면 중복되는 메모리를 줄일 수가 있죠.

function Person() {}

Person.prototype.eyes = 2;
Person.prototype.nose = 1;

var kim  = new Person();
var park = new Person():

console.log(kim.eyes); // => 2

이제 모든 Person 객체는 중복되는 메모리를 줄이고 프로토타입으로부터 속성 데이터를 가져오게 됩니다.
이처럼 자바스크립트는 프로토타입을 이용해서 상속과 같은 효과를 내고 있습니다.


[[Prototype]] , prototype

ECMAScript spec에서는 자바스크립트의 모든 객체는 [[Prototype]]이라는 인터널 슬롯(internal slot)을 가집니다.
브라우저 콘솔에 생성자 함수를 만들어 보면 아래처럼 객체는 [[prototype]] 인터널 슬롯을 가지고 있고 값이 Object인 것을 확인할 수 있죠.
여기서 [[prototype]] 는 인터널 슬롯이고 Object 라는 값이 prototype이 됩니다.
그리고 JavaScript는 다중상속이 불가하므로 객체는 오직 하나의 프로토타입만 가질 수 있습니다.

생성자 함수가 아닌 객체 리터럴로 생성한 객체도 Object라는 프로토타입을 가집니다.

var student = {
  name: 'Lee',
  score: 90
}
console.log(student.__proto__ === Object.prototype); // true

하지만 생성자 함수로 생성하지 않고 객체 리터럴로 생성한 객체는 prototype프로퍼티를 가지고 있지 않습니다.

Object.prototype을 가지지만 Object의 프로토타입을 수정하기엔 너무 위험하죠.

생성자 함수로 만든 객체는 생성자 함수의 프로토타입을 가집니다.

var person1 = new Person();
console.log(person1.__proto__ === Person.prototype); // true

생성자 함수로 만든 객체들은 프로토타입의 프로퍼티를 공유하므로 아래처럼 프로토타입에 함수를 추가하면 모든 인스턴스가 사용할 수 있습니다.

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

Person.prototype.sayHello = function() {
   console.log(`Hello, my name is ${this.name}`);
};

const john = new Person("John");
john.sayHello();  // "Hello, my name is John"

함수의 prototype 프로퍼티를 수정하게 되면 모든 인스턴스에 반영되는데 그 이유를 알아보겠습니다..


constructor

브라우저에서 확인해보면 모든 함수는 [[prototype]] 슬롯에 프로퍼티를 가지고 있고 이 객체는 constructor 함수를 가지고 있습니다.


해당 생성자 함수로 생성된 인스턴스들은 모두 동일한 객체타입이므로 하나의 protytype 프로퍼티를 공유하게 됩니다.
따라서 new Person()으로 객체를 생성하면 내부의 constructor가 호출되어 Person() 객체를 반환하게 됩니다.

여기서 constructor는 생성자 함수가 반환하는 객체가 없으면 this(자기 자신, 빈 객체)를 반환하게 되는데 생성된 객체의 속성은 this키워드로 연결시켜야 합니다.

constructor함수가 반환하는 객체를 확인해보면 자기 자신입니다.

함수가 아니라면 new 키워드를 사용할 수 없습니다.


new 키워드로 호출되는 함수는 실행되는 코드블록이 있어야 하는데 객체 리터럴은 없기 때문에 호출되지 않습니다.
또한 반환하는 객체로 자기 자신의 빈객체를 만든 뒤, prototype프로퍼티를 연결해야 하는데 객체 리터럴로 만든 객체는 prototype이 없기 때문에 이 단계를 수행할 수 없습니다.

따라서 객체 리터럴의 객체는 prototype 프로퍼티를 사용할 수 없습니다.

생성자 함수로 만든 객체도 prototype 프로퍼티를 이용하지 않으면 해당 객체에만 속성이 추가됩니다.

function Dog(color, name, age) {
    this.color = color,
    this.name = name,
    this.age = age
}
var myDog = new Dog("흰색", "마루", 1);
var myDog1 = new Dog("검은색", "장군", 3);

myDog.family = "시베리안 허스키"; // family 속성을 추가
myDog.breed = function() {  // breed 메소드를 추가
    return this.color + " " + this.family;
}


프로토타입을 이용해서 기존의 내장함수를 수정하는 예시를 들어보겠습니다.

Date.prototype.format = function (formatString) {
    const pad = (number, length = 2) => {
        let str = String(number);
        while (str.length < length) {
            str = '0' + str;
        }
        return str;
    };

    return formatString.replace('yyyy', this.getFullYear())
        .replace('mm', pad(this.getMonth() + 1)) // month: 0 = 1월
        .replace('dd', pad(this.getDate()));
};

const date = new Date(2023, 3, 1); 
console.log(date.format('yyyy.mm.dd')); // 2023.04.01

또한 직접 만든 객체의 프로퍼티 삭제하는 방법은 delete를 사용하는 것입니다.

delete myDog.age; // age 프로퍼티를 삭제함.
console.log(myDog.age); // undefined 



이러한 프로토타입에는 중요한 두가지 개념이 있습니다.

어떤 객체가 생성될 때, 이 객체는 자신의 부모 또는 프로토타입에 대한 내부 참조를 가집니다.
이 내부 참조는 대게 [[Prototype]] 또는 __proto__ (일부 환경에서)에 표시됩니다.
객체의 프로퍼티나 메서드에 접근하려고 할 때 해당 객체에 프로퍼티나 메서드가 없다면, 자바스크립트는 객체의 [[Prototype]] 링크를 통해 해당 프로퍼티나 메서드를 찾습니다.

이 링크는 프로토타입 체인을 통해 계속되며, 체인의 끝에 도달할 때까지 (일반적으로 Object.prototype에 도달할 때까지) 프로퍼티나 메서드를 찾기 위해 계속 탐색합니다.

Prototype Object

함수가 생성될 때마다 새로운 객체가 생성되는데, 이것을 프로토타입 객체라고 합니다.
이 프로토타입 객체는 해당 함수를 생성자로 사용하여 생성된 모든 인스턴스에 공통적인 프로퍼티와 메서드를 제공합니다.
이 객체는 함수의 prototype 프로퍼티를 통해 접근할 수 있습니다.
예를 들면, Dog 함수에 bark라는 메서드를 모든 인스턴스에 추가하려면 Dog.prototype.bark를 정의하면 됩니다.



__proto__

__proto__는 JavaScript의 객체 내에서 해당 객체의 프로토타입을 가리키는 내부 속성입니다.
프로토타입에도 찾는 속성이 없다면 prototype.__proto__ (프로토타입 링크)를 통해 프로토타입의 프로토타입으로 올라갑니다.
이렇게 연결된 프로토타입들의 체인을 프로토타입 체인이라고 합니다.

또한 create() 메소드를 이용하여 인스턴스의 프로토타입을 새롭게 연결할 수 있습니다.

function Person() {}

let person1 = new Person();
let person2 = Object.create(person1);

따라서 아래의 코드는 person1을 반환합니다.

person2.__proto__; // Person{}

이제 person2Person의 프로토타입에 접근할 수 있습니다.


프로토타입 체인

객체의 특정한 속성이나 메소드에 접근할때 해당 객체에 존재하지 않다면 자동으로 객체의 프로토타입에서 찾게되는데 이를 프로토타입 체인이라고 합니다.

생성자 함수로 생성된 모든 객체는 prototype 프로퍼티를 가지고 있고 체인을 타고 올라가다 보면 최상위는 Object객체가 있습니다.
따라서 모든 객체는 Object.prototype를 상속받게 됩니다.

const myObject = {}; 

console.log(myObject.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

이렇게 프로토타입 체인을 타고 올라가면 Obejct의 메소드를 사용하게 됩니다.

var student = {
  name: 'Lee',
  score: 90
}

// Object.prototype.hasOwnProperty()
console.log(student.hasOwnProperty('name')); // true

프로토타입 체인의 또 다른 예로는

function Person(first, last, age, gender, interests) {
    this.name = {
        first,
        last,
    };
    this.age = age;
    this.gender = gender;
    this.interests = interests;
}

var person0 = new Person("Bob", "Smith", 32, "male", ["music", "skiing"]);

생성된 인스턴스의 valueOf 메소드를 호출하면 인스턴스에 없는 메소드이므로
프로토타입 체인을 통해 Object의 메소드인 valueOf()를 호출하게 됩니다.

이렇게 프로토타입은 PersonvalueOf를 가지고 있는지 체크 후 또 없다면 체인을 계속 타고 올라가 ObjectvalueOf를 가지고 있는지 체크한 후 호출합니다.

이전 버전의 브라우저는 person1.__proto__.__proto__ 과 같은 방법으로 프로토타입체인을 구현했었지만
ECMAScript 2015부터는 Object.getPrototypeOf(obj) 함수를 통해 객체의 프로토타입 객체에 바로 접근할 수 있게 되었습니다.

프로토타입 체인을 이용한 또다른 예로 아래의 변수가 String객체의 메소드를 사용할 수 있습니다.
split(), indexOf(), replace()

var myString = "This is my string.";
var strArr = myString.split(' ')


동적 상속

JavaScript는 프로토타입을 기반으로 한 동적 상속을 제공합니다.

동적 상속을 사용하면 객체의 구조와 동작을 런타임(runtime)에 변경할 수 있습니다.
이것은 JavaScript와 같은 동적 언어에서 가능하며, 이는 프로그램의 유연성을 향상시키지만, 코드의 복잡성을 증가시킬 수 있습니다.

JavaScript에서는 Object.setPrototypeOf() 속성을 사용하여 객체의 프로토타입을 변경할 수 있습니다.
그러나 이는 성능 문제를 야기하거나 예기치 않은 동작을 초래할 수 있으므로 권장되지 않습니다.
대신에, 객체를 생성할 때 프로토타입을 설정하거나 클래스와 생성자 함수를 사용하여 상속 구조를 정의하는 것이 일반적입니다.

let animal = {
    speak() {
        console.log(`${this.name} makes a noise.`);
    }
};

let dog = {
    name: 'Rex',
    bark() {
        console.log(`${this.name} barks.`);
    }
};

// 동적 상속을 사용해  `dog`객체가 'animal' 객체를 상속받게 함
Object.setPrototypeOf(dog, animal);

dog.speak(); // "Rex makes a noise."
dog.bark(); // "Rex barks."

Object.setPrototypeOf() 함수는 dog객체에 animal객체를 프로토타입으로 설정합니다.
이로 인해 dog객체는 animal객체의 메소드를 상속받을 수 있습니다.
이렇게 런타임중에 상속을 변경할 수 있습니다.




정리

  • JavaScript의 모든 객체와 함수는 프로토타입 객체를 가진다.
  • 생성자 함수로 생성된 객체는 생성자 함수를 프로토타입으로 가진다.
  • 객체 리터럴로 만든 객체는 prototype 프로퍼티가 없다.
  • 모든 인스턴스에 추가하고 싶은 설정은 prototype 프로퍼티에 추가한다.
  • 내장 함수의 메소드는 프로토타입 체인으로 사용할 수 있게 된다.

출처

https://developer.mozilla.org/ko/docs/Learn/JavaScript/Objects/Object-oriented_programming
https://medium.com/@bluesh55/javascript-prototype-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-f8e67c286b67
https://poiemaweb.com/js-prototype

profile
작은것부터

0개의 댓글