팀원들과 함께하는 모던 JS 딥다이브 스터디 7-8차 💕
프로토타입
여태까지 코드 상에서 프로토타입을 사용할 일이 없기도 했고, 따로 공부한 적도 없지만
프로토타입 기반 언어
라 불리는 JS에서 프로토타입을 모른다는 것은 JS를 모르는 것과 다름 없다고 생각한다. 모던 JS 튜토리얼 스터디에서 드디어!! 프로토타입에 대해 다루게 되어서 신중하게 파트를 나눠 공부해보기로 했다.
책을 읽을 때 텀이 길어지면 이전 내용이 기억이 안나고 진도는 나가야 되기 때문에 넘어가는 경우가 빈번해진다는 걸 느꼈다. 내가 정리한 내용 중 A는 B다라고 설명하고 넘어가지 않고 why에 대해 밑줄을 긋고 바로 밑에 why에 대한 답을 정리해보기로 했다.
함수가 생성자 함수로서 호출되면 this는 생성자 함수의 인스턴스를 가리킨다.
화살표 함수에서의 this는 상위 스코프의 this를 의미하므로, getArea 내부의 this는 상위 스코프인 Circle 생성자 함수를 가리키게 된다.
getArea
메서드는 인스턴스가 생성될 때마다 계속 생성된다. 이를 위해 Circle.prototype의 메소드로 추가해주었다.
이때 화살표 함수로 메소드를 할당하게 되면 내부의 this는 window를 가리키게 된다. 화살표 함수는 상위 스코프를 가리키는데 Circle.prototype은 현재 전역에서 정의되어있기 때문이다.
객체, 프로토타입, 생성자 함수는 서로 연결되어 있다.
생성자 함수는 자신의 prototype 프로퍼티를 통해 프로토타입 객체에 접근할 수 있다.
프로토타입은 자신의 constructor 프로퍼티를 통해 생성자 함수에 접근할 수 있다.
객체는 __proto__
접근자 프로퍼티를 통해 프로토타입 객체에 접근할 수 있다.
console.log(Circle.prototype)
console.log(Circle.prototype.constructor) // 자신이 참조하는 생성자함수를 가리킴
console.log(circle1.__proto__)
이 때 __proto__
접근자 프로퍼티는 circle1 객체의 프로퍼티가 아닌 Object.prototype의 프로퍼티로, 상속받아 사용할 수 있다.
Object.prototype을 상속받지 않는 객체란?
Object.create(proto, [descriptors])
= [[Prototype]]이 proto를 참조하는 빈 객체를 만듦.
Object.create(null)
= [[Prototype]]이 null = 프로토타입 체인의 종점
[[Prototype]]이 null이기 때문에 Object.prototype을 상속받지 않는 객체가 생성되어 __proto__
접근자 프로퍼티를 사용할 수 없다.
따라서 정확한 프로토타입 참조를 확인하고 싶을 땐
👉🏻 Object.getPrototypeOf(object)
👉🏻 Object.setPrototypeOf(object, proto)
두 메서드를 사용하는 것이 좋다.
모든 객체는 상속을 위해 [[prototype]]이라는 내부 슬롯을 가진다.
함수 또한 객체이므로 [[prototype]] 내부 슬롯을 가지며,
추가적으로 prototype 프로퍼티도 가진다.
일반 객체는 prototype 프로퍼티를 가지지 않는다.
함수가 생성될 때 함수 자신과 연결된 프로토타입 객체를 동시에 생성하며, 이 둘은 각각 prototype과 constructor라는 프로퍼티로 서로를 참조하게 된다.
당연하게도 prototype 프로퍼티는 이름에서 알 수 있듯이 내부 슬롯이 아니므로 바로 접근이 가능하다.
prototype은 함수 입장에서 자신과 연결된 프로토타입 객체를 가리키고,
[[Prototype]]은 객체의 입장에서 자신의 부모 객체인 프로토타입 객체를 가리킨다.
https://tc39.es/ecma262/#sec-ordinaryobjectcreate
모든 객체는 추상 연산 OrdinaryObjectCreate
에 의해 생성된다.
추상연산 OrdinaryObjectCreate는 생성할 객체의 프로토타입을 인수로 받아 자신이 생성한 객체의 [[Prototype]] 내부 슬롯에 할당 후, 생성한 객체를 반환한다.
OrdinaryObjectCreate(proto, [ , additionalInternalSlotsList ])
추상 연산 OrdinaryObjectCreate는 proto와 선택인자를 받아 런타임에 새로운 객체를 만든다.
문자열은 원시타입이기 때문에 메서드를 사용할 수 없다.
const str = '문자열';
str.split()
하지만 문자열은 split 등 문자열 관련 메서드를 사용할 수 있다.
In order for that to work, a special “object wrapper” that provides the extra functionality is created, and then is destroyed.
위와 같은 일이 동작하기 위해서 추가적인 함수들을 제공하는 특수한object wrapper
가 만들어졌다가 파괴된다고 한다.
...
The string str is a primitive. So in the moment of accessing its property, a special object is created that knows the value of the string, and has useful methods, like toUpperCase().
That method runs and returns a new string (shown by alert).
The special object is destroyed, leaving the primitive str alone.
특수한object wrapper
가 동작할 땐 (기존의 문자열이 아닌) 새 문자열이 return 되고 object wrapper는 문자열만 남긴채로 파괴된다.
So primitives can provide methods, but they still remain lightweight.
따라서 원시타입이 메서드를 가질 수 있으면서도 여전히 가벼운 채로 남아있을 수 있다.
<JAVASCRIPT.INFO에서 발췌>
🧐기존 문자열이 남지않는 이유는 원시타입의 immutable(변경 불가능) 특성으로 메모리 주소를 변경할 수 없기 때문에 똑같은 원시타입이더라도 새 메모리 주소를 확보해 할당해야 한다. 같은 이치로 기존의 문자열이 return 되지 않고 새 문자열이 return 되는 것이다.
생성자 함수에 의해 프로토타입 교체 시, 미래의 생성할 인스턴스의 프로토타입을 교체하는 것이다.
만약 기존에 생성자 함수에 의해 생성한 인스턴스가 있을 경우, prototype으로 프로토타입을 교체하더라도 이미 생성되어진 인스턴스의 프로토타입을 교체하지는 못한다.
프로토타입을 교체한 시점에서, 이후에 만들어질 인스턴스들은 교체된 프로토타입이 출력된 것을 볼 수 있다.
때문에 __proto__
혹은 Object.setPrototypeOf로 현재 인스턴스의 프로토타입에도 교체된 프로토타입으로 반영해줘야 한다.
2번째 줄에선 생성자 함수의 prototype만 바꿔줬기 때문에 이미 생성되어진 인스턴스의 프로토타입은 변경되지 않았으므로, Object.setPrototypeOf로 바꿔주었다.
책에서는 생성자 함수에 의해 프로토타입을 교체하면 인스턴스가 자연스럽게 교체된 프로토타입을 가리키는 것 같이 표현이 됐는데 위에서도 설명했듯이 새로 생성할 인스턴스만 교체된 프로토타입에 영향을 받는 것 같아 보인다.
그렇다면 기존의 인스턴스는 무엇을 가리키는 걸까?
아마 기존의 덮어씌워지기 전 Person.prototype을 가리키는 것 같아 보인다. Person.prototype은 parent 객체에 의해 덮어씌워졌지만(참조값이 바뀌었지만(?)) 기존의 프로토타입이 메모리 상에 아직 존재하고 현재의 인스턴스가 기존의 프로토타입을 가리키고 있으므로 가비지 컬렉터에 의해 삭제되지 않을 것으로 판단된다.
기존의 인스턴스 또한 parent 객체를 프로토타입으로 설정 후 다시 비교해본 결과 정상 작동한다.
__proto__
접근자 프로퍼티를 통해 [[prototype]]에 접근이 가능하다.Object.getPrototypeOf(object)
, Object.setPrototypeOf(object, proto)
를 통해서도 접근이 가능하다.