fffo

프로토타입 본문

Programming/Javascript

프로토타입

gggs 2021. 10. 3. 22:46

프로토타입

  • js는 명령형, 함수형, 프로토타입 기반 객체지향 프로그래밍을 지원하는 멀티 패러다임 프로그래밍 언어임

객체지향 프로그래밍

  • 명령형 프로그래밍 : 프로그램을 명령어나 함수의 목록으로 보는 전통적인 관점
  • 객체지향 프로그래밍 : 여러 개의 독립적인 단위(객체)의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임
  • 실세계의 실체를 인식하는 사고를 프로그래밍에 접목시키고자 함
    • 속성 : 실체는 특징이나 성질을 나타냄
    • 추상화 : 속성을 프로그램에 필요한 부분만 간추려내 표현
    • 객체 : 속성을 통해 여러 개의 값을 하나의 단위로 구성한 복합적 자료구조.
      • 상태(property)와 동작(method)을 하나의 논리적 단위로 묶음

상속과 프로토타입

  • 상속 : 객체의 프로퍼티나 메서드를 다른 객체가 상속 받아 그대로 사용할 수 있는 것. 객체지향 프로그래밍의 핵심
  • 상속은 중복을 제거, 코드를 재사용
function Person(name) {
    this.name = name,
    this.greeting = function() {
        return `Hi I'm ${name}.`;
    }
}

const p1 = new Person("dlwlrma");
const p2 = new Person("BDNS");

console.log(p1.greeting === p2.greeting); // false
function Person(name) {
    this.name = name,
}

Person.prototype.greeting = function() {
    return `Hi I'm ${name}.`;
};

const p1 = new Person("dlwlrma");
const p2 = new Person("BDNS");

console.log(p1.greeting === p2.greeting); // true;
console.log(p1.greeting()); //Hi I'm dlwlrma.
console.log(p2.greeting()); //Hi I'm BDNS.
  • js는 프로토타입을 기반으로 상속을 구현

프로토타입 객체

  • 모든 객체는 [[Prototype]]이라는 내부 슬롯을 가짐
    • 객체의 프로토타입의 참조가 저장됨
    • 내부 슬롯은 직접 접근 불가. [[Prototype]]는 proto를 통해 간접적으로 접근 가능

proto 접근자 프로퍼티

  • 객체의 [[Prototype]]에 접근할 수 있는 접근자 프로퍼티
  • proto는 객체가 직접 소유하는 프로퍼티가 아닌 Object.prototype의 프로퍼티를 상속 받아서 사용하는 것임
const obj = {};
const parent = { x: 1 };
obj.__proto__ = parent; // 프로퍼티 [[value]]을 설정하는 것이 아닌 setter함수 호출!!!!
console.log(obj.x); // 1
  • 위의 코드에서 obj.proto는 프로퍼티 값을 할당하는 것이 아닌 setter함수를 호출하는 것에 유의
  • 접근자 프로퍼티를 사용하는 이유는 단방향의 프로토타입 체인을 만들기 위해 제약을 주기 위함
  • 순환참조가 생기는 것을 체크하고 방지
  • proto를 코드에서 직접 사용하는 것은 권장되지 않음
    • 모든 객체가 proto를 사용할 수 있는 것은 아니기 때문
    • 대신 Object.getPrototypeOf / ObjectsetPrototypeOf 사용을 권장. 같은 동작 수행함
    • const obj = Object.create(null); console.log(obj.__proto__); // undefinded console.log(Object.getPrototypeOf(obj)); // nulll const child = {}; const parent = { x: 1 }; Object.getPrototypeOf(child); // child.__proto__; 와 같은 동작 Object.setPrototypeOf(child, parent); // child.__proto__ = parent; 와 같은 동작 console.log(child.x); // 1
    • 각각 ES5, ES6에 도입된 메서드이고 IE9이상 IE11이상에서 지원함

함수 객체의 prototype 프로퍼티

  • prototype 프로퍼티 : 생성자로 호출할 수 있는 함수 객체만 소유함. 생성자 함수가 생성할 인스턴스의 프로토타입을 참조
  • 일반 함수도 생성자로 호출할 수 있다면 prototype을 가지지만 의미 없음
  • 생성자함수의 prototype 프로퍼티와 인스턴스의 proto 접근자 프로퍼티로 접근한 객체는 같은 프로토타입 객체를 가리킴
function Person(name) {
  this.name = name 
};
const p1 = new Person('dlwlrma');
console.log(Person.prototype === p1.__proto__); // true

프로토타입의 constructor 프로퍼티와 생성자 함수

  • 모든 프로토타입은 constructor 프로퍼티를 가짐
  • 생성자 함수로 생성된 인스턴스 객체는 constructor 프로퍼티를 가지지 않지만 프로토타입을 상속 받아서 사용 가능

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

  • 엄밀히 말하면 리터럴로 생성된 객체와 생성자 함수로 생성된 객체는 다름
  • 리터럴로 생성한 객체도 상속을 위해 프로토타입이 필요하므로 생성자 함수가 존재해야함
  • 리터럴로 생성된 객체는 실제로 객체의 생성자 함수는 아니지만 가상적인 생성자 함수를 가짐
function temp() {}
console.log(temp.constructor === Function); // true
// 실제 temp함수 객체는 Fucntion 생성자함수에 의해 생성된 것이 아니지만 그렇게 생각해도 무방

프로토타입의 생성 시점

  • 프로토타입은 생성자 함수가 생성되는 시점에 함께 생성됨

사용자 정의 생성자 함수의 프로토타입 생성 시점

  • 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입 생성
  • 함수 선언문은 런타임 이전에 js엔진에 의해 먼저 실행되므로 프로토타입도 런타임 이전에 생성됨

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

  • 모든 빌트인 생성자 함수는 전역 객체가 생성되는 시점에 생성되고 이 때 프로토타입도 생성되고 바인딩됨
  • 전역 객체 : 런타임 이전에 js엔진에 의해 생성되는 특수한 객체
    • 브라우저는 window, node.js는 global
    • 프로퍼티로 표준 빌트인 객체와 환경에 따른 호스트 객체, var키워드로 선언한 전역 변수 및 전역 함수를 가짐
    • Math, Reflect, JSON을 제외한 모든 표준 빌트인 객체는 생성자 함수임

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

  • 다양한 객체 생성 방법이 있지만 공통적으로 추상 연산 OrdinaryObjectCreate에 의해 생성됨
  • OrdinaryObjectCreate는 자신이 생성할 객체의 프로토타입을 인수로 전달 받음, 즉, 프로토타입은 OrdinaryObjectCreate에 전달되는 인수에 의해 결정됨
  • 이 인수는 객체가 생성되는 시점에 객체 생성 방식에 의해 결정됨
  • 객체 리터럴 : Object.prototype 전달. 객체 리터럴 내부에 프로퍼티 추가
  • Object 생성자 함수 : Object.prototype 전달. 빈 객체 생성 후 프로퍼티 추가
  • 생성자 함수 : 생성자 함수의 prototype에 바인딩되어있는 객체 전달

프로토타입 체인

  • 프로토차입 체인 : 객체의 프로퍼티에 접근하려할 때 객체에 해당 프로퍼티가 없다면 [[Prototype]] 내부 슬롯의 참조를 타고 올라가면서 프로토타입의 프로퍼티를 검색함
  • 프로토타입의 최상위는 항상 Object.prototype임. 프로토타입 체인의 종점
  • 최상위에도 프로퍼티가 없다면 에러 발생이 아닌 undefined를 반환함에 유의
  • 프로퍼티가 아닌 식별자는 스코프 체인에서 검색함
  • 스코프 체인과 프로토타입 체인은 별도로 동작하는 것이 아니라 서로 협력하여 식별자와 프로퍼티를 검색하는 데 사용됨

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

  • 오버라이딩 : 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의해 사용하는 방식
  • 오버로딩 : 함수의 이름은 동일하지만 매개변수의 타입이나 개수가 다른 메서드를 구현해 매개변수에 의해 메서드를 구별하여 호출하는 방식. js는 오버로딩을 지원하지 않지만 arguments 객체를 이용해 구현할 수는 있음
  • 프로퍼티 섀도잉 : 상속관계에 의해 프로퍼티가 가려지는 현상
  • 인스턴스에 메서드를 추가시 해당 객체의 프로퍼티가 추가될 뿐, 실제로 프로토타입을 덮어쓰진 않음
  • 거꾸로 메서드 삭제시 해당 객체에서의 삭제가 일어날 뿐, 프로토타입의 메서드는 삭제 불가능
const Person = (function () {
    function Person(name) {
        this.name = name;
    }
    Person.prototype.greeting = function () {
        console.log(`Hi I'm ${this.name}`);
    };
    return Person;
}());

const p1 = new Person('dlwlrma');
p1.greeting(); // Hi I'm dlwlrma
p1.greeting = function () {
    console.log(`Hello. I'm ${this.name}`);
}
p1.greeting(); // Hello I'm dlwlrma

delete p1.greeting;
p1.greeting(); // Hi I'm dlwlrma

delete p1.greeting;
p1.greeting(); // Hi I'm dlwlrma

프로토타입의 교체

  • prototype에 새로운 객체를 넣음으로써 프로토타입 교체 가능
  • constructor를 추가하지 않으면 생성자 함수와의 연결이 끊김
  • 생성자 함수를 통한 교체와 객체 인스턴스를 통한 교체가 있음
const Person = (function () {
    function Person(name) {
        this.name = name;
    }
    Person.prototype = {
        constructor: Person,
    greeting () {
      console.log(`Hi I'm ${this.name}`);
    }
    };
    return Person;
}());

const p1 = new Person('dlwlrma');
p1.greeting(); // Hi I'm dlwlrma

const parent = {
  constructor: Person,
  greeting () {
    console.log(`Hello I'm ${this.name}`);
  }
}
Object.setPrototypeOf(p1,parent);
p1.greeting(); // Hello I'm dlwlrma
  • 객체 간 상속 관계를 동적으로 변경하는 것은 번거롭기 때문에 프로토타입은 이렇게 직접 교체하지 않는 것을 권장
  • 상속 관계 변경은 직접 상속이나 클래스를 사용 권장

instanceof 연산자

  • instanceof : 이항 연산자로, 좌변에 객체를 가리키는 식별자, 우변에 생성자 함수를 가리키는 식별자를 피연산자로 받음
  • 우변의 피연산자가 함수가 아니면 TypeError 발생
  • 우변의 prototype에 바인딩된 객체가 좌변 객체의 프로토타입 체인 상에 존재하면 true, 그렇지 않으면 false 반환
  • 프로토타입의 constructor 프로퍼티가 가리키는 생성자 함수를 찾는 것이 아님에 유의
function Person(name) {
    this.name = name;
}

const me = new Person('wlrma');
console.log(me instanceof Person); // true

const parent = {};
Object.setPrototypeOf(me, parent);
console.log(me instanceof Person); // false

Person.prototype = parent;
console.log(me instanceof Person); // true

직접 상속

Object.create에 의한 직접 상속

  • Object.create : 명시적으로 프로토타입을 지정하여 새로운 객체를 만드는 메서드
  • 다른 객체 생성방식과 마찬가지로 추상 연산 OrdinaryObjectCreate를 호출함
  • 매개변수로 생성할 객체의 프로토타입으로 지정할 객체 전달.
    • 옵션으로 생성할 객체의 프로퍼티 키와 프로퍼티 디스크립터 객체로 이루어진 객체 전달(Object.defineProperties 메서드의 둘째 인수와 형식 동일)
let obj = Object.create(null); // 프로토타입 체인의 종점에 위치하는 객체 생성
// console.log(obj.toString()) ; // -> TypeError

obj = Object.create(Object.prototype); // obj = {}; 와 동일

obj = Object.create(Object.prototype, {
  x: { value: 1, writable: true, enumerable: true, configurable: true }
}); // obj = { x: 1}; 와 동일

const myProto = { x: 10};
obj = Object.create(myProto);
console.log(obj.x); // 10
  • 장정 :
    • new 연산자가 없이 객체 생성 가능
    • 프로토타입 지정하면서 객체 생성 가능
    • 객체 리터럴에 의해 생성된 객체도 상속 가능
  • ESLint에서는 Object.prototype의 빌트인 메서드를 객체가 직접 호출하는 것을 권장하지 않음 → Object.create를 통해 프로토타입 체인의 종점에 위치하는 객체 생성 가능하기 때문
    • call을 통해 간접적으로 호출 권장
    • const obj = Object.create(null); obj.a = 1; console.log(Object.prototype.hasOwnProperty.call(obj,'a')); // true

객체 리터럴 내부에서 proto에 의한 직접 상속

  • ES6에서 추가된 Object.create 메서드보다 간편한 방법
const myProto = { x: 10};
const obj = {
  y: 20,
  __proto__: myProto
};

console.log(obj.x, obj.y); // 10 20

정적 프로퍼티/메서드

  • 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메서드
  • 인스턴스는 정적 프로퍼티/메서드를 사용할 수 없음
  • MDN과 같은 문서에서 메서드를 설명할 때 프로토타입 메서드와 정적 메서드를 구분해서 표기함
  • 프로토타입 메서드를 표기할 때 prototype을 #으로 표기하는 경우도 있음

프로퍼티 존재 확인

in 연산자

const person = {
    name: 'wlrma'
}
console.log('name' in person); // true
console.log('toString' in person); // true
  • 객체가 상속 받은 모든 프로토타입의 프로퍼티를 확인함
  • ES6에 도입된 Reflect.has 메서드도 동일하게 동작
const person = {
    name: 'wlrma'
}
console.log(Reflect.has(person, 'name')); // true
console.log(Reflect.has(person, 'toString')); // true

Objec.prototype.hasOwnProperty 메서드

  • 인수로 전달받은 프로퍼티 키가 객체 고유의 프로퍼티인 경우만 true 반환
const person = {
    name: 'wlrma'
}
console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('toString')); // false

프로퍼티 열거

for ... in 문

  • 객체의 프로토타입 체인 상에 존재하는 모든 프로퍼티 중 프로퍼티 어트리뷰트 [[Enumerable]]값이 true인 프로퍼티를 순회하며 열거
  • 열거할 때 순서를 보장하지 않음(대부분의 모던 브라우저는 순서를 보장하고 숫자(사실은 문자열)인 프로퍼티 키에 대해서는 정렬을 실시함)
const obj = {
    2: 2,
  3: 3,
  1: 1,
  b: 'b',
  a: 'a'
}
for (const key in obj) {
  if (!obj.hasOwnProperty(key)) continue;
  console.log(`${key}: ${obj[key]}`);
}

/*
1: 1
2: 2
3: 3
b: b
a: a
 */

Object.keys/values/entries 메서드

  • 객체 자신의 열거 가능한 고유 프로퍼티를 열거
  • Object.values/entries는 ES8에 도입됨
const person = {
  name: 'wlrma',
  age: 29,
  __proto__: { sing: 'night letter'}
}

for (const key in person) {
  console.log(key);
}
/*
name
age
sing
*/

console.log(Object.keys(person)); // [ 'name', 'age' ]

'Programming > Javascript' 카테고리의 다른 글

빌트인 객체  (0) 2021.10.05
strict mode  (0) 2021.10.04
함수의 일급 객체  (0) 2021.10.02
생성자 함수에 의한 객체 생성  (0) 2021.10.01
프로퍼티 어트리뷰트  (0) 2021.09.30
Comments