[코어 자바스크립트] 프로토타입

변진상·2024년 6월 10일
0

학습 기록

목록 보기
26/31

코어 자바스크립트_프로타입

자바스크립트는 프로토타입 기반 언어이다.

  • 클래스 기반 언어: 상속을 사용
  • 프로토타입 기반 언어: 어떤 객체(prototype)을 원형으로 삼고 이를 복제(참조) → 상속과 비슷한 효과

1. 프로토타입의 개념 이해

6-1-1. constructor, prototype, instance

var instace = new Constructor;
  • 위 코드를 그림으로 표현하면 아래 그림과 같다.

프로토타입과 인스턴스의 관계

> 어떤 생성자 함수를 new 연산자와 함께 호출하면Constructor에서 정의된 내용을 바탕으로 인스턴스 생성인스턴스에 __proto__라는 프로퍼티 자동 부여이 프로퍼티는 Constructor의 prototype 프로퍼티 참조
  • prototype 객체 내부에는 생성될 인스턴스가 사용할 메서드 저장
    → 인스턴스에서는 prototype을 통해 이 메서드에 접근
💬 참고: __proto__와 [[prototype]] 이름의 역사..?
ES5.1 스펙에서는 __proto__이 아닌, `[[prototype]]`이라는 명칭으로 정의
-> `__proto__`은 원래 브라웆저들이 `[[prototype]]`을 구현한 대상에 지나지 않는다.
명세에서는 instance.`__proto__`으로 접근하는 것을 막았다.
오직 Object.getPrototypeOf(instance) / Reflect.getPrototypeOf(instance)를 통해서만 접근할 수 있도록 정의함.
그러나 브라우저들이 `__proto__`을 이용해 접근하는 것을 포기하지 않음.
-> 그래서 ES6에서는 `__proto__`를 인정, 하지만 이는 브라우저에서의 호환성을 위한 이이지 권장되는 방식은 아니다.
var Person = function (name) {
	this._name = name;
}

Person.prototype.getName = function(){
	return this._name;
}

// Person 생성자를 이용해 만든 인스턴스는 getName을 호출할 수 있다.

const jinsang = new Person(‘Jinsang’);
jinsang.__proto__.getName(); //undefined

// 이 코드에서 에러가 아닌 undefined가 나왔다는 것은 해당 함수가 호출할 수 있는 함수라는 것을 의미 그러나 왜 undefined 뜰까?
// => this 규칙에따라 어떤 함수를 메서드로서 호출할 때는 메서드 명 바로 앞에 객체인 jinsang.__proto__가 this가 되는데, 이는 Person.prototype을 가리키고 있기 때문에 Person에 _name이지정되지 않았기 때문이다.

jinsang.getName();// “Jinsang”

// 이렇게하면 정상동작하는데, __proto__는 생략 가능하도록 언어가 설계되어있다.

__proto__생략

  • 정리: new 연산자로 Constructor를호출하면 instance가만들어지는데, 이 instance의 생략 가능한 프로퍼티인 __proto__는 Constructor의 prototype을 참조한다. -> instance는 __proto__를 생략할 수 있는 특징 덕에 자신의 것처럼 메서드나 프로퍼티에 접근할 수 있다.
var Constructor = function (name) {
this.name = name;
}

Constructor.prototype.method1 = function() {};
Constructor.prototype.property1 = ‘Constuctor prototype property’;

var instance = new Constructor(’Instance’);
console.dir(Constructor);
console.dir(instance);

객체 콘솔 출력 결과 크롬 개발자 도구의 콘솔에서 확인한 코드 출력결과

  • 참고: 출력된 요소들 중 짙은 보라색(method1, property1)으로 표시된 요소들의 경우 {enumerable: true} 속성이 부여되어 열거 가능한 요소들입니다. 열거 가능 여부에 따라 색의 차이를 보이는데, for in과 같은 문법으로 접근 가능한 여부를 표현해줍니다.

❗️ 리터럴도 생성자 함수를 이용해 인스턴스를 생성하는 것과 같다.

var arr1 = new Array();
var arr2 = [];

arr1.forEach(() => {}); // (O)
arr2.from({length: 11}, () => 0); //TypeError: arr2.from is not a function
  • 그래서 arr1, arr2는모두 Array.prototype 내부의 함수들을 호출 가능하다.
  • 하지만 arr2, arr2는 Array의 메서드를 호출할 수 없다.

배열의 스테틱 메서드, 인스턴스 메서드

6-1-2. constructor 프로퍼티

  • 생성자 함수의 프로퍼티인 prototype 객체 내부에 constructor라는 프로퍼티가 있다(인스턴스의 proto에도 마찬가지).
  • constructor 프로퍼티는 생성자 함수를 가리킨다.
  • 굳이 자신을 참조하는 프로퍼티를 가지고 있는 이유는 인스턴스로부터 그 원형이 무엇이었는지 확인할 수 있는 수단이기 때문이다.
var arr = [1, 2];
Array.prototype.constructor === Array; //true
arr.__proto__.constructor === Array; //true
arr.constructor === Array; //true

var arr2 = new arr.constructor(3, 4);
console.log(arr2); // [3, 4]

Q. 인스턴스의 생성자 정보를 알아내기 위해 constructor에 의존하는 게 위험한 이유
A. constructor는 읽기 전용 속성이 부여된 예외적인 경우(기본형 리터럴 변수 - number, string, boolean)를 제외하면 값을 바꿀 수 있기 때문이다.

var NewConstructor = function(){

}

1.constructor = NewConstructor();
console.log(1.constructor, 1 instanceof Newconstructor) // Number false

const num = new Number();
console.log(num.constructor, num instanceof Newconstructor) // NewConstructor false

다음은 모두 constructor를 가리킵니다.

[Constructor]
[instance].__proto__.constructor
[instance].constructor
Object.getPrototypeOf([instance]).constructor
[Constructor].prototype.constructor

다음은 모두 prototype을 가리킵니다.

[Constructor].prototype
[instance].__proto__
[instance]
Object.getPrototypeOf([instance])

2. 프로토타입 체인

6-2-1. 메서드 오버라이드

  • 메서드 오버라이드란, prototype에 이미 정의된 메서드를 인스턴스에서 다시 정의해 덮어씌우는 것을 의미한다.
  • 자바스크립트 엔진이 인스턴스 내부부터 살펴 일치하는 메서드가 있다면 인스턴스 내부 메서드를 사용한다.
  • 인스턴스 내부에 해당 메서드가 없다면 프로토타입을 타고 올라가며 일치하는 메서드를 찾는다.
var Animal = function(name){
	this.name = name;
}

Animal.prototype.getName = function() {
	return this.name;
}

var otter = new Animal(’otter’);

otter.getName = function(){
	return ‘브라질 ’ + this.name;
}

console.log(otter.getname()); // 브라질 수달

❗️ otter에서 Animal.prototype.getName을 사용하는 방법

console.log(otter.__proto__.getName()); // undefined

// Animal에 name이 정의되어 있지 않다.

Animal.prototype.name = ‘동물‘;
console.log(otter.__proto__.getName()); // 동물

console.log(otter.__proto__.getName.call(otter)); // 수달

6-2-2. 프로토타입 체인

console.dir({ a: 1 });

프로토타입 체인

  • *__proto__ 안에* __proto__가 있다.
  • 이유: prototype이 객체이기 때문이다. 기본적으로 모드 객체의 __proto__에는 Object.prototype이 연결된다.

배열과 객체 프로토타입 체인

  • __proto__는 생략이 가능하다. 그래서 Array가 Object.prototype 내부의 메서드도 자신의 메서드도 자신의 것처럼 호출 가능하다.
var arr = [1, 2];

arr(.__proto__).push(3);
arr(.__proto__)(.__proto__).hasOwnProperty(2); // true;
  • 이처럼 어떤 데이터의 __proto__ 프로퍼티 내부에 다시 __proto__ 프로퍼티가 연쇄적으로 이어진 것을 프로토타입 체인이라고 한다. 그리고 그 체인을 따라 검색하는 것을 프로토타입 체이닝이라고 한다.

프로토타입 체인의 재귀적 구조

- 생성자 함수도 함수이기 때문에 Function 생성자 함수의 prototype과 연결된다. - 생성자 함수 -> Function의 prototype과 연결 - prototype -> Object의 prototype과 연결 - 이런 구조가 재귀적으로 콘솔에 찍히지만, 실제로 메모리 상에서 데이터를 무한대 구조 전체를 들고 있는 것은 아니다. 사용자가 접근시에만 해당 정보를 얻을 수 있다.

6-2-3. 객체 전용 메서드의 예외사항

  • 어떤 생성자 함수든 prototype은 반드시 객체이기 때문에 Object.prototype이 언젠 프로토타입 체인의 최상단에 존재한다. -> 그래서 어떤 데이터에서도 사용 가능한 메서드들만 있다(toString, hasownProperty, valueOf, isPrototypeOf 등)
  • 그래서 객체 전용 메서드들은 부득이 Object.prototype이 아닌 Object에 스태틱 메서드로 부여할 수 밖에 없었다.
  • 결과적으로 생성자 함수인 Object와 객체의 리터럴({}) 사이에 this를 통한 연결이 불가해 다른 자료형의 전용 메서드처럼 '메서드명 앞의 대상이 곧 this'가 되는 방식(instance.freeze()) 대신 대상 인스턴스를 직접 인자로 주입(Object.freeze(instance))해야하는 방식으로 구현되어 있다.

💬 참고: __proto__가 빈 객체 만들기

'프로토타입 체인상 가장 마지막에는 Object.prototype이 있다.'고 했는데, Object.create(null)은 __proto__가 없는 객체를 생성한다. -> 이를 통해 기본 기능에 제약이 있지만, 객체의 무게가 가벼워져 성능상 이점이 있다.

var __proto = Object.create(null);

__proto.getValue = function(key){
   return this[key];
}

var obj = Object.create(__proto);

obj.a = 1;
console.log(obj.getValue("a"));
console.log(obj)

Object.create()

The Object.create() static method creates a new object, using an existing object as the prototype of the newly created object.
기존에 존재하는 객체를 새로 생성할 객체의 프로토타입으로 사용함.

__proto__가 빈 객체 생성 결과 출력

6-2-4. 다중 프로토타입 체인

  • 기본 내장 데이터 타입은 프로토타입 체인이 1, 2단계로 끝나는 경우가 있다. 다만, 사용자가 새롭게 만드는 경우 제한이 없다.
var Grade = function(){
	var args = Array.prototype.slice.call(arguments);

	for(var i = 0; i < args.length; i++){
		this[i] = args[i];
	}
	this.length = args.length;
}

var g = new Grade(100, 80);
  • 위 코드는 배열의 메서드를 사용할 수 없는 유사배열 객체이다.
  • 유사 배열 객체가 배열의 메서드를 사용할 수 있게 하는 방법은 아래 코드, 그림과 같이 인스턴스의 g.__proto__ 즉, Grade.prototype이 배열의 인스턴스를 바라보게 하면 된다.
Grade.prototype = [];

유사 배열 객체 __proto__의 배열 인스턴스 참조

g.pop() // [100];
g.push(90) // [100, 90]

3. 정리

  • 생성자 함수를 new 연산자와 호출하면 Constructor에 정의된 내용을 바탕으로 인스턴스를 생성한다.
  • 인스턴스 내에 __proto__라는, Constructor의 prototype을 참조하는 프로퍼티가 자동으로 부여된다.
  • __proto__는 언어 설계상 생략 가능한 속성이라, 인스턴스가 Constructor.prototype을 마치 자신의 메서드 인 것처럼 사용 가능하다.
    ex. arr1.push(100)
  • Constructor.prototype은 constructor라는 프로퍼티가 있는데, 이는 인스턴스가 자신의 생성자 함수가 무엇인지를 알 수 있도록 생성자 함수 자신을 가리킨다.
  • __proto__를 계속 찾아가면 최종적으로 Object.prototype에 당도한다.
    • 찾아가는 과정을 프로토타입 체이닝이라 한다.
    • 가까운 대상부터 먼 대상까지 탐색하다 원하는 값을 찾으면 중단한다.
  • Object.prototype은 모든 데이터 타입에서 사용할 수 있는 범용적 메서드만 존재한다.
    • 객체 전용 메서드는 다른 데이터 타입과 다르게 Object 생성자 함수에 스태틱하게 담겨있다.
profile
자신을 개발하는 개발자!

0개의 댓글