기억할 만한 내용 정리(두서없음)
1~20장

객체 생성 방법
- 객체 리터럴.
- Object 생성자 함수.
- 생성자 함수.
- Object.create 메서드
- 클래스(ES6)
primitive type 은 pass by value 이지만 object는 항상 pass by reference
원시타입은 불변이다.
한번 생성된 원시 값은 read only값으로서, 변경할 수 없다.
원시 값을 할당한 변수에 새로운 원시 값을 재할당하면 메모리 공간에 저장되어 있는 재할당 이전의 원시 값을 변경하는 것이 아니라, 새로운 메모리 공간을 확보하고 재할당한 원시 값을 저장한 후, 변수는 새롭게 재할당한 원시 값을 가리킨다. 이때 변수가 참조하던 메모리 공간의 주소가 바뀐다.
-> 변수가 나타내는 메모리의 주소는 할당할 때마다 계속 바뀐다.
그렇기 때문에 원시 값은 read only값으로써 변경할 수 없다는 것.
불변성을 갖는 원시 값을 할당한 변수는 재할당 이외에 변수 값을 변경할 수 있는 방법이 없다. 만약 재할당 이외에 원시 값인 변수를 변경할 수 있다면 예기치 않게 변수 값이 변경될 수 있다는 것을 의미한다. 이는 값의 변경, 즉 상태 변경을 추적하기 어렵게 만든다.
const(상수): 재할당이 금지된 변수
const 키워드를 사용해 선언한 변수에 할당한 원시 값(상수)는 변경할 수 없다.
하지만 const 키워드를 사용해 선언한 변수에 할당한 객체는 변경할 수 있다.
const o = {x: {y: 1}};
const c1 = {...o};
// 얕은 복사.
console.log(c1 === o) // false
console.log(c1.x === o.x) // true
// 한 단계만 복사가 된다
// 깊은 복사를 하려면 loadsh cloneDeep() 함수를 사용해야 한다.
인스턴스
인스턴스란 클래스에 의해 생성되어 메모리에 저장된 실체를 말한다.
객체지향 프로그래밍에서 객체는 클래스와 인스턴스를 포함한 개념이다.
클래스는 인스턴스를 생성하기 위한 템플릿의 역할을 한다.
인스턴스는 개체가 메모리에 저장되어 실제로 존재하는 것에 초점을 맞춘 용어이다.
렉시컬 스코프
var x = 1;
function foo() {
var x = 10;
bar();
}
function bar() {
console.log(x);
}
foo();
bar();
출력값은 어떻게 될까?
위 예제의 실행 결과는 bar 함수의 상위 스코프가 무엇인지에 따라 결정된다.
- 함수를 어디서 호출_했는지에 따라 함수의 상위 스코프를 결정한다.
- 함수를 어디서 정의_했는지에 따라 함수의 상위 스코프를 결정한다.
첫번째 방식을 동적 스코프라고 한다.
함수를 정의하는 시점에는 함수가 어디서 호출될지 알 수 없다. 따라서 함수가 호출되는 시점에 동적으로 상위 스코프를 결정해야 하기 때문에 동적 스코프라고 한다.
두 번째 방식을 렉시컬 스코프 또는 정적 스코프라고 한다.
동적 스코프 방식처럼 상위 스코프가 동적으로 변하지 않고 함수 정의가 평가되는 시점에 상위 스코프가 정적으로 결정되기 때문에 정적 스코프라고 부른다.
자바스크립트를 비롯한 대부분의 프로그래밍 언어는 렉시컬 스코프를 따른다.
자바스크립트는 렉시컬 스코프를 따르므로
함수를 어디서 호출했는지가 아니라 함수를 어디서 정의했는지에 따라 상위 스코프를 결정한다.
함수가 호출된 위치는 상위 스코프 결정에 어떠한 영향도 주지 않는다.
함수의 상위 스코프는 언제나 자신이 정의된 스코프다.
따라서 위 예제는 1을 두번 출력한다.
이처럼 함수의 상위 스코프는 함수 정의가 실행될 때 정적으로 결정된다. 함수 정의가 싱행되어 생성된 함수 객체는 이렇게 결정된 상위 스코프를 기억한다.
함수가 호출될 때마다 함수의 상위 스코프를 참조할 필요가 있기 때문이다.
변수는 자신이 등록된 스코프가 소멸(메모리 해제) 될 때까지 유효하다.
할당된 메모리 공간은 더 이상 그 누구도 참조하지 않을 때 가비지 콜렉터에 의해 해제되어 가용 메모리 풀에 반환된다.
즉, 누군가가 메모리 공간을 참조하고 있으면 해제되지 않고 확보된 상태로 남아있게 된다.
이는 스코프도 마찬가지다. 누군가 스코프를 참조하고 있으면 스코프는 소멸하지 않고 생존하게 된다. -> 클로저
호이스팅은 변수 선언이 스코프의 선두로 끌어 올려진 것처럼 동작하는 자바스크립트 고유의 특징을 말한다.
호이스팅은 스코프를 단위로 동작한다.
let도 비슷하게 동작하는데,
console.log(x);
let x = 10;
에서 ReferenceError: Cannot access 'x' before initialization 라는 에러 메시지를 보여준다.
console.log(x);
는 ReferenceError: x is not defined 라는 에러 메시지를 보여준다.
var과 let은 이 점이 다르다. var과 let은 마찬가지로 호이스팅되지만,
let으로 선언한 변수는 undefined으로 접근 가능하지 않고 명시적인 에러를 throw한다.
var키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 된다.
이는 전역 변수의 생명 주기가 전역 객체의 생명 주기와 일치하는다는 것을 말한다.
전역 객체
전역 객체는 코드가 실행되기 이전 단계에자바스크립트 엔진에 의해 어떤 객체보다도 먼저 생성되는 특수한 객체다. 전역 객체는 클라이언트 사이드 환경에서는 window, 서버사이드 환경에서는 global 객체를 의미한다. 환경에 따라 전역 객체를 가리키는 다양한 식별자 (window, self, this, frames, global)가 존자했으나 ES11에서 globalThis로 통일되었다.
전역 객체는 표준 빌트인 객체(Object, String, Number, Function, Array ...) 와 환경에 따른 호스트 객체 (클라이언트 WEB API 또는 Node.js의 호스트API), 그리고 var 키워드로 선언한 전역 변수와 전역 함수를 프로퍼티로 갖는다.
var
- 중복 선언 허용
- 함수 레벨 스코프
- 변수 호이스팅
- 선언,초기화 -> 할당
- var로 선언한 전역 변수와 전역 함수... 는 브라우저 환경에서 window의 프로퍼티가 된다. (let 은 다름)
let
- 선언(렉시컬 스코프에 알려줌) -> 초기화, 할당
- 블록 레벨 스코프
const
- 블록 레벨 스코프
- 재할당을 금지할 뿐 '불변'을 의미하진 않는다.
let foo = 1;
{
console.log(foo);
let foo = 3;
}
// ReferenceError: Cannot access 'foo' before initialization
프로퍼티 어트리뷰트
내부 슬롯과 내부 메서드
내부 슬롯과 내부 메서드는 자바스크립트 엔진의 구현 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 pseudo property와 pseudo method다.
ECMAScript 사양에 등장하는 이중 대괄호로 감싼 이름들이 내부 슬롯과 내부 메서드다.
내부 슬롯과 내부 메서드는 자바스크립트 엔진의 내부 로직이므로 원칙적으로 자바스크립트는 내부 슬롯과 내무 메서드에 직접적으로 접근하거나 호출할 수 있는 방법을 제공하지 않는다. 단, 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 전급할 수 있는 수단을 제공하기는 한다. ex) __proto __ ,[[prototype]]
property attribute, property descriptor
자바스크립트 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의한다.
프로퍼티의 상태란 프로퍼티의 값, 값의 갱신 여부, 열거 가능 여부, 재정의 가능 여부를 말한다.
- value
- writable
- enumerable
- configurable
프로퍼티 어트리뷰트는 자바스크립트 엔진이 관리하는 내부 상태 값,(meta-property) 인 내부 슬롯 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]] 이다.
따라서 프로퍼티 어트리뷰트에 직접 접근할 수는 없지만.
Object.getOwnPropertyDescriptor 메서드를 사용하여 간접적으로 확인할 수는 있다.
Object.getOwnPropertyDescriptor메서드의 첫 번째 매개변수에는 객체의 참조를 전달하고, 두번째 매개변수에는 프로퍼티 키를 문자열로 전달한다.
이때 메서드는 PropertyDescriptor 객체를 반환한다.
만약 존재하지 않는 프로퍼티나 상속받은 프로퍼티에 대한 프로퍼티 디스크립터를 요구하면 undefined가 반환된다.
ES8 에서 도입된 Object.getOwnPropertyDescriptors메서드는 모든 프로퍼티의 프로퍼티 어트리뷰트 정보를 제공하는 프로퍼티 디스크립터 객체들을 반환한다.
Data Property, Accessor Property
데이터 프로퍼티
- 키와 값으로 구성된 일반적인 프로퍼티. 지금까지 살펴본 모든 프로퍼티는 데이터 프로퍼티다.
접근자 프로퍼티
- 자체적으로는 값을 갖고 있지 않고, 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수로 구성된 프로퍼티다.
데이터 프로퍼티
| 프로퍼티 어트리뷰트 | 프로퍼티 디스크립터 객체의 프로퍼티 | 설명 |
|---|---|---|
| [[Value]] | value | 프로퍼티 키를 통해 프로퍼티 값에 접근하면 반환되는 값. 프로퍼티 키를 통해 프로퍼티 값을 변경하면 [[Value]] 에 값을 재할당한다. 이때 프로퍼티가 없으면 프로퍼티를 동적 생성하고 생성된 프로퍼티의 [[Value]]에 값을 저장한다. |
| [[Writable]] | writable | 프로퍼티 값의 변경 가능 여부를 나타내며 불리언 값을 갖는다. [[Writable]]의 값이 false인 경우 해당 프로퍼티의 [[Value]]의 값을 변경할 수 없는 읽기 전용 프로퍼티가 된다. |
| [[Enumerable]] | enumerable | 프로퍼티의 열거 가능 여부를 나타내며 불리언 값을 갖는다. [[Enumerable]]의 값이 false 인 경운 해당 프로퍼티는 for ... in 문이나 Object.keys 메서드 등으로 열거할 수 없다. |
| [[Configurable]] | configurable | 프로퍼티의 재정의 가능 여부를 나타내며 불리언 값을 갖는다. [[Configurable]]의 값이 false 인 경우 해당 프로퍼티의 삭제, 프로퍼티 어트리뷰트 값의 변경이 금지된다. 단 [[Writable]]이 true인 경우 [[value]]의 변경와 [[Writable]]을 false로 변경하는 것은 허용된다. |
Object.getOwnPropertyDescriptor({name : 'Lee'}, 'name')
// { value: 'Lee', writable: true, enumerable: true, configurable: true }
// 프로퍼티 어트리뷰트 [[Value]] 의 값이 'Lee' 인 것을 나타낸다.
// 그리고 writable, enumerable, configurable 이 true인 것은
// 프로퍼티 어트리뷰트 [[Writeable]], [[Enumerable]], [[Configurable]] 가 모두 true인 것을 나타낸다.
프로퍼티가 생성될 때, value는 값으로 초기화되며, writable, enumerable, configurable은 true로 초기화된다. 이것은 프로퍼티를 동적 추가해도 마찬가지이다.
접근자 프로퍼티
접근자 프로퍼티는 자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티다.
접근자 프로퍼티는 다음과 같은 프로퍼티 어트리뷰트를 갖는다.
| 프로퍼티 어트리뷰트 | 프로퍼티 디스크립터 객체의 프로퍼티 | 설명 |
|---|---|---|
| [[Get]] | get | 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수다. 즉, 접근자 프로퍼티 키로 프로퍼티 값에 접근하면 프로퍼티 어트리뷰트 [[Get]]의 값, 즉 getter 함수가 호출되고 그 결과가 프로퍼티 값으로 반환된다. |
| [[Set]] | set | 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 저장할 때 호출되는 접근자 함수다. 즉, 접근자 프로퍼티 키로 프로퍼티 값을 저장하면 프로퍼티 어트리뷰트 [[Set]]의 값, 즉 setter 함수가 호출되고 그 결과가 프로퍼티 값으로 저장된다. |
| [[Enumerable]] | enumerable | 데이터 프로퍼티의 [[Enumerable]]과 같다. |
| [[Configurable]] | configurable | 데이터 프로퍼티의 [[Configurable]]과 같다. |
const person = {
firstName: 'HyeongJun',
lastName: 'Noh',
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(name) {
[this.firstName, this.lastName] = name.split(' ');
},
};
console.log(person.firstName + ' ' + person.lastName);
person.fullName = 'first last';
console.log(person);
console.log(person.fullName);
let descriptor = Object.getOwnPropertyDescriptor(person, 'firstName');
console.log(descriptor);
descriptor = Object.getOwnPropertyDescriptor(person, 'fullName');
console.log(descriptor);
- 일반 객체의 __proto__는 접근자 프로퍼티이다.
- 함수의 prototype은 데이터 프로퍼티이다.
console.log(Object.getOwnPropertyDescriptor(Object.prototype, '__proto__'));
console.log(Object.getOwnPropertyDescriptor(function () {}, 'prototype'));
// OUTPUT
{
get: [Function: get __proto__],
set: [Function: set __proto__],
enumerable: false,
configurable: true
}
{ value: {}, writable: true, enumerable: false, configurable: false }
프로퍼티 재정의 Object.defineProperty 또는 Object.defineProperties 사용가능
(객체, 프로퍼티 이름, 프로퍼티 디스크립터)
enumberable의 값이 false인 경우 열거되지 않는다.console.log(Object.keys(obj)); <- 이 경우 열거되지 않음
writable이 false인 경우 해당 프로퍼티의 value값을 바꿀 수 없다.obj.name = 'new name';
값을 변경하며 에러는 발생하지 않으며, 무시된다.
configurable의 값이 false인 경우 해당 프로퍼티를 삭제할 수 없다.delete obj.name; 마찬가지로 에러는 발생하지 않으며 무시된다.
또한 프로퍼티를 재정의 할 수 없다.Object.defineProperty 불가능
일반 함수에서 호출하는 this는 전역 객체를 가리킨다.
new 연산자와 함께 생성자 함수를 호출하면 자바스크립트 엔진은 다음과 같은 과정을 거쳐
암묵적으로 인스턴스를 생성하고 인스턴스를 초기화한 후 암묵적으로 인스턴스를 반환한다.
- 인스턴스 생성과 this 바인딩
암묵적으로 빈 객체가 생성된다. 이 빈 객체가 바로 생성자 함수가 생성한 인스턴스다. 그리고 암묵적으로 생성된 빈 객체, 즉 인스턴스는 this에 바인딩된다.
생성자 함수 내부의 this 가 생성자 함수가 생성할 인스턴스를 가리키는 이유가 바로 이것이다.
이 처리는 함수 몸체의 코드가 한 줄씩 실행되는 런타임 이전에 실행된다. - 인스턴스 초기화
생성자 함수에 기술되어 있느 코드가 실행되며 this에 바인딩되어 있는 인스턴스를 초기화한다.
즉 this에 바인딩되어 있는 인스턴스에 프로퍼티나 메서드를 추가하고 생성자 함수가 인수로 전달받은 초기값을 인스턴스 프로퍼티에 할당하여 초기화하거나 고정값을 할당한다. - 인스턴스 반환
생성자 함수 내부의 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다.
this가 아닌 다른 객체를 명시적으로 반환하면 this가 반환되지 못하고 return 문에 명시한 객체가 반환된다.
-> 하지만 명시적으로 원시 값을 반환하면 원시 값반환은 무시되고 암묵적으로 this가 반환된다.
생성자 함수 내부에서 명시적으로 this가 아닌 다른 값을 반환하는 것은 생성자 함수의 기본 동작을 훼손한다.
따라서 생성자 함수 내부에서 return 문을 반드시 생략해야 한다.
[[Call]], [[Construct]]
함수 선언문 또는 함수 표현식으로 정의한 함수는 일반적인 함수로서 호출할 수 있는 것은 물론 생성자 함수로서 호출할 수 있다.
생성자 함수로서 호출한다는 것은 new 연산자와 함께 호출하여 객체를 생성하는 것을 의미한다.
함수는 객체이므로 일반 객체외 동일하게 동작할 수 있다.
함수 객체는 일반 객체가 가지고 있는 내부 슬롯과 내부 메서드를 모두 가지고 있기 때문이다.
함수는 객체이므로 프로퍼티를 소유할 수 있고, 메서드를 소유할 수 있다.
함수는 객체이지만 일반 객체와는 다르다. 일반 객체는 호출할 수 없지만 함수는 호출할 수 있다.
따라서 함수 객체는 일반 객체가 가지고 있는 내부 슬롯 내부 메서드는 물론, 함수로서 동작하기 위해 함수 객체만을 위한
[[Environment]], [[FormalParameters]]등의 내부 슬롯과 [[Call]], [[Construct]]같은 내부 메서드를 추가로 가지고 있다.
함수가 일반 함수로서 호출되면 함수 객체의 내부 메서드 [[Call]]이 호출되고, new 연산자와 함께 생성자 함수로 호출되면 내부 메서드 [[Construct]]가 호출된다. 내부 메서드 [[Call]] 을 갖는 함수 객체를 callable이라고 하며, 내부 메서드 [[Construct]]를 가지고 있는 함수를 constructor, 가지고 있지 않다면 non-constructor라고 부른다.
함수는 무조건 callable이지만, constuctor일 수도 있고, non-constructor일 수도 있다.
함수 선언문과 함수 표현식으로 정의된 함수만이 constructor이고,
화살표 함수나 메서드 축약 표현으로 정의된 함수는 non-constructor다.
// 메서드 축약 표현
const A = {
a() {}
};
// 함수 표현식
const B = {
a: function() {}
};
new 연산자를 사용하면 [[Call]]이 호출되는 것이 아닌 [[Construct]]가 호출된다.
생성자 함수를 new 없이 사용하는 경우를 막기 위해 new.target을 사용할 수 있다.
function A() {
if(!new.target) {
return new A();
}
this.a = 10;
}
String, Number, Boolean 등은 new 연산자 여부에 따라 반환값이 다르다.
new 없이 사용하면 형 변환처럼 동작하고, new 연산자를 사용하면 객체를 반환한다.
함수와 일급 객체
자바스크립트에서 함수는 일급 객체이다.
일급 객체란
- 무명의 리터럴로 생성할 수 있다. 즉, 런타임에 생성이 가능하다.
- 변수나 자료구조(객체, 배열 등)에 저장할 수 있다.
- 함수의 매개변수에 전달할 수 있다.
- 함수의 반환값으로 사용할 수 있다.
일급 객체로서 함수가 가지는 가장 큰 특징은 일반 객체와 같이 함수의 매개변수에 전달할 수 있으며, 함수의 반환값으로도 사용할 수도 있다는 것이다.
이는 함수형 프로그래밍을 가능케 하는 자바스크립트의 장점 중 하나다.
함수는 arguments, caller, length, name, prototype 프로퍼티를 가진다.
__proto__는 함수 객체의 프로퍼티가 아니라 Object.prototype객체의 프로퍼티를 상속받았다.
function A(a, b) {
console.log(arguments);
console.log(Object.getOwnPropertyDescriptors(arguments));
return a + b;
}
console.log(Object.getOwnPropertyDescriptors(A));
// OUTPUT
{
length: { value: 2, writable: false, enumerable: false, configurable: true },
name: {
value: 'A',
writable: false,
enumerable: false,
configurable: true
},
arguments: {
value: null,
writable: false,
enumerable: false,
configurable: false
},
caller: {
value: null,
writable: false,
enumerable: false,
configurable: false
},
prototype: { value: {}, writable: true, enumerable: false, configurable: false }
}
arguments 프로퍼티
함수 객체의 arguments 프로퍼티 값은 arguments 객체다.
arguments 객체는 함수 호출 시 전달된 인수들의 정보를 담고있는 순회 가능한 유사 배열 객체이며,
함수 내부에서 지역 변수처럼 사용된다. 함수 외부에서는 참조할 수 없다.
함수를 정의할 때 선언한 매개변수는 함수 몸체 내부에서 변수와 동일하게 취급된다.
함수가 호출되면 함수 몸체 내에서 암묵적으로 매개변수가 선언되고 undefined로 초기화된 이후 인수가 할당된다.
선언된 매개변수의 개수보다 인수를 적게 전달했을 경우 인수가 전달되지 않은 매개변수는 undefined로 초기화된 상태를 유지한다.
매개변수의 개수보다 인수를 더 많이 전달한 경우 초과된 인수는 무시된다.
그렇다고 초과된 인수가 그냥 버려지는 것은 아니다. 모든 인수는 암묵적으로 arguments객체의 프로퍼티로 보관된다.
function A(a, b) {
console.log(arguments);
console.log(Object.getOwnPropertyDescriptors(arguments));
return a + b;
}
A(1, 2, 3, 4, 5);
[Arguments] { '0': 1, '1': 2, '2': 3, '3': 4, '4': 5 }
{
'0': { value: 1, writable: true, enumerable: true, configurable: true },
'1': { value: 2, writable: true, enumerable: true, configurable: true },
'2': { value: 3, writable: true, enumerable: true, configurable: true },
'3': { value: 4, writable: true, enumerable: true, configurable: true },
'4': { value: 5, writable: true, enumerable: true, configurable: true },
length: { value: 5, writable: true, enumerable: false, configurable: true },
callee: {
value: [Function: A],
writable: true,
enumerable: false,
configurable: true
},
[Symbol(Symbol.iterator)]: {
value: [Function: values],
writable: true,
enumerable: false,
configurable: true
}
}
arguments 객체는 인수를 프로퍼티 값으로 소유하며, 프로퍼티 키는 인수의 순서를 나타낸다.
callee 프로퍼티는 호출되어 arguments 객체를 생성한 함수, 즉 함수 자신을 가리키고
length 프로퍼티는 인수의 개수를 가리킨다.
Symbol프로퍼티는 arguments 객체를 순회 가능한 자료구조인 iterable로 만들기 위한 프로퍼티다.
-> 나중에 다시 배울꺼
arguments 객체는 매개변수 개수를 확정할 수 없는 가변 인자 함수를 구현할 때 유용하다.
function sum() {
let ret = 0;
for(let i = 0; i < arguments.length; i++) {
res += arguments[i];
}
return ret;
}
argments 객체는 배열 형태로 인자 정보를 담고 있지만 실제 배열이 아닌 유사 배열 객체다. 유사 배열 객체란 length 프로퍼티를 가진 객체로 for 문으로 순회할 수 있는 객체를 말한다.
-> 유사 배열 객체와 이터러블
유사 배열 객체는 배열이 아니므로 배열 메서드를 사용할 경우 에러가 발생한다.
따라서 배열 메서드를 사용하려면 Function.prototype.call, Function.prototype.apply를 사용해 간접 호출해야 하는 번거로움이 있다.
caller 프로퍼티
자신을 호출한 함수가 무엇인지 알려준다.
function foo(bar) {
bar();
}
function bar() {
console.log(bar.caller);
}
foo(bar);
bar();
// OUTPUT
[Function: foo]
[Function (anonymous)]
length 프로퍼티
함수 객체의 length 프로퍼티는 함수를 정의할 때 선언한 매개변수의 개수를 가리킨다.
arguments 객체의 length 프로퍼티와 함수 객체의 length 프로퍼티으 기밧은 다를 수 있으므로 주의해야 한다.
arguments 객체의 length 프로퍼티는 인자(argument)의 개수를 가리키고, 함수 객체의 length 프로퍼티는 매개변수(parameter)의 개수를 가리킨다.
name 프로퍼티
함수 객체의 name프로퍼티는 함수 이름을 나타낸다.
__proto__ 접근자 프로퍼티
모든 객체는 [[Prototype]]이라는 내부 슬롯을 갖는다.
[[Prototype]] 내부 슬롯은 객체지향 프로그래밍의 상속을 구현하는 프로토타입 객체를 가리킨다.
__proto__ 프로퍼티는 [[Prototype]] 내부 슬롯이 가리키는 프로토타입 객체에 접근하기 위해 사용하는 접근자 프로퍼티다.
내부 슬롯에는 직접 접근할 수 없고 간접적인 접근 방법을 제공하는 경우에 한하여 접근할 수 있다.
[[Prototype]] 내부 슬롯에도 직접 접근할 수 없으며 __proto__ 접근자 프로퍼티를 통해 간접적으로 프로토타입 객체에 접근할 수 있다.
prototype 프로퍼티
prototype 프로퍼티는 생성자 함수로 호출할 수 있는 객체, 즉 constructor만이 소유하는 프로퍼티다.
일반 객체와 생성자 함수로 호출할 수 없는 non-constructor에는 prototype 프로퍼티가 없다.
(function () {}).hasOwnProperty('prototype'); => true
(() => {}).hasOwnProperty('prototype'); => false
prototype 프로퍼티는 함수가 객체를 생성하는 생성자 함수로,
new로 호출될 때 생성자 함수가 생성할 인스턴스의 프로토타입 객체를 가리킨다.
prototype
객체 -> 상태 데이터와 동작을 하나의 논리적인 단위로 묶은 복합적인 자료구조
객체지향 프로그래밍은 객체의 상태를 나타내는 데이터와 상태 데이터를 조작할 수 있는 동작을 하나의 논리적인 단위로 묶어 생각한다.
상태 데이터 -> 프로퍼티
동작 -> 메서드
객체는 고유의 기능을 갖는 독립적인 부품으로 볼 수 있지만,
자신의 고유한 기능을 수행하면서 다른 객체와 관계성을 가질 수 있다.
다른 객체와 메시지를 주고받거나 데이터를 처리할 수도 있다.
또는 다른 객체의 상태 데이터나 동작을 상속받아 사용하기도 한다.
상속과 prototype
자바스크립트는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거한다.
상속이 없는 객체
function Circle(radius) {
this.radius = radius;
this.getArea = function () {
return Math.PI * radius ** 2;
};
}
const circle1 = new Circle(1);
const circle2 = new Circle(2);
// Cricle 생성자 함수는 인스턴스를 생성할 대마다 동일한 동작을 하는
// getArea 메서드를 중복 생성하고 모든 인스턴스가 중복 소유한다.
// getArea 메서드는 하나만 생성하여 모든 인스턴스가 공유해서 사용하는 게
// 바람직하다.
console.log(circle1.getArea === circle2.getArea);
// OUTPUT => false
prototype 기반 상속
이처럼 동일한 생성자 함수에 의해 생성된 모든 인스턴스가 동일한 메서드를 중복 소유하는 것은 메모리를 불필요하게 낭비한다.
또한 인스턴스를 생성할 때마다 메서드를 생성하므로 퍼포먼스에도 악영향을 준다.
만약 10개의 인스턴스를 생성하면 내용이 동일한 메서드도 10개 생성된다.
프로토타입을 기반으로 상속을 구현하여 이러한 비효율을 줄일 수 있다.
function Circle(radius) {
this.radius = radius;
}
// Circle 생성자 함수가 생성한 모든 인스턴스가
// getArea메서드를 공유해서 사용할 수 있도록 프로토타입에 추가한다.
// 프로토타입은 Cricle 생성자 함수의 prototype 프로퍼티에 바인딩된다.
Circle.prototype.getArea = function () {
return Math.PI * this.radius ** 2;
};
const circle1 = new Circle(1);
const circle2 = new Circle(2);
// Circle 생성자 함수가 생성한 모든 인스턴스는 부모 객체의 역할을 하는
// 프로토타입 Circle.prototype으로부터 getArea메서드를 상속받는다.
// 즉, Cricle 생성자 함수가 생성하는 모든 인스턴스는 하나의 getArea
// 메서드를 공유한다.
console.log(circle1.getArea === circle2.getArea);
// OUTPUT => true
프로토타입 객체
프로토타입 객체(줄여서 프로토타입)란 객체간 상속을 구현하기 위해 사용된다.
프로토타입은 어떤 객체의 상위(부모) 객체의 역할을 하는 객체로서 다른 객체에 공유 프로퍼티(메서드 포함)를 제공한다.
프로토타입을 상속받은 하위(자식) 객체는 상위 객체의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 사용할 수 있다.
모든 객체는 [[Prototype]] 이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토타입의 참조(null 인 경우도 있다)다.
[[Prototype]]에 저장되는 프로토타입은 객체 생성 방식에 의해 결정된다. 즉, 객체가 생성될 때 객체 생성 방식에 따라 프로토타입이 결정되고 [[Prototype]]에 저장된다.
예를 들어, 객체 리터럴에 의해 생성된 객체의 프로토타입은 Object.prototype이고, 생성자 함수에 의해 생성된 객체의 프로토타입은 생성자함수의 prototype 프로퍼티에 바인딩되어있는 객체다.
모든 객체는 하나의 프로토타입을 갖는다.
모든 객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입,
즉 [[Prototype]] 내부 슬롯에 간접적으로 접근할 수 있다.
접근자 프로퍼티는 자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수다.
const obj = {};
const parent = { x: 1 };
console.log(obj.x);
// OUTPUT => undefined
obj.__proto__; // get __proto__ 호출
obj.__proto__ = parent; // set __proto__ 호출
console.log(obj.x);
// OUTPUT => 1
__proto__ 접근자 프로퍼티는 상속을 통해 사용된다.
__proto__ 접근자 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아니라
Object.prototype의 프로퍼티다. 모든 객체는 상속을 통해 Object.prototype.__proto__ 접근자 프로퍼티를 사용할 수 있다.
const person = { name: 'Lee' };
// person 객체는 __proto__ 프로퍼티를 소유하지 않는다.
console.log(person.hasOwnProperty('__proto__'));
// OUTPUT => false
// __proto__ 프로퍼티는 모든 객체의 프로토타입 객체인
// Object.prototype의 접근자 프로퍼티다.
console.log(Object.getOwnPropertyDescriptor(Object.prototype, '__proto__'));
// OUTPUT =>
// {
// get: [Function: get __proto__],
// set: [Function: set __proto__],
// enumerable: false,
// configurable: true
// }
// 모든 객체는 Object.prototype의 접근자 프로퍼티 __proto__
// 를 상속받아 사용할 수 있다.
console.log({}.__proto__ === Object.prototype);
// OUTPUT => true
모든 객체는 프로토타입 계층 구조인 프로토타입 체인에 묶여있다.
자바스크립트 엔지는 객체의 프로퍼티(메서드 포함)에 접근하려고 할 떄 해당 객체에 접근하려는 프로퍼티가 없다면 __proto__ 접근자 프로퍼티가 가리키는 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색한다.
프로토타입 체인의 종점, 즉 프로토타입 체인의 최상위 객체는 Object.prototype이며, 이 객체의 프로퍼티와 메서드는 모든 객체에 상속된다.
__proto__ 접근자 프로퍼티를 통해 프로토타입에 접근하는 이유
const parent = {};
const child = {};
child.__proto__ = parent;
parent.__proto__ = child;
프로토타입 체인은 단방향 링크드 리스트로 구현되어야 한다.
하지만 위와같이 순환참조하는 프로토타입 체인이 만들어지게 되면 무한 루프에 빠진다.
따라서 아무런 체크 없이 무조건적으로 프로토타입을 교체할 수 없도록,
__proto__ 접근자 프로퍼티를 통해 프로토타입에 접근하고 교체하도록 구현되어있다.
함수 객체의 prototype 프로퍼티
생성자 함수 객체만이 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다.
기본적으로 생성자 함수의 prototype은 constructor 프로퍼티를 가지는데 contsturctor 프로퍼티는 생성자 함수를 가리킨다.
이 연결은 생성자 함수가 생성될 때, 즉 함수 객체가 생성될 때 이뤄진다.
생성자 함수의 prototype은은 생성자 함수가 생성되는 시점에 더불어 생성된다.
non-constructor는 prototype이 생성되지 않는다.
function A() {
this.name = 'HELLO';
}
const B = () => {};
console.log(A.prototype);
console.log(B.prototype);
console.log(Object.getOwnPropertyDescriptors(A.prototype));
//A.prototype.__proto__ === Object.prototype -> true
// OUTPUT =>
{}
undefined
{
constructor: {
value: [Function: A],
writable: true,
enumerable: false,
configurable: true
}
}
이처럼 빌트인 생성자 함수가 아닌 사용자 정의 생성자 함수는 자신이 평가되어 함수 객체로 생성되는 시점에 프로토타입도 더불어 생성되며, 생성된 프로토타입의 프로토타입은 언제나 Object.prototype이다.
어떤 객체에서 프로퍼티(메서드 포함)에 접근하려고 하면 해당 객체의 프로퍼티를 확인하고, 없다면 해당 객체의 프로토타입을 찾고, 프로토타입의 프로토타입을,, 순차적으로 검색한다. 이를 프로토타입 체인이라 한다.
프로토타입이 아닌 식별자는 스코프 체인에서 검색한다.
자바스크립트 엔진은 함수의 중첩 관계로 이루어진 스코프의 계층적 구조에서 식별자를 검색한다.
따라서 스코프 체인은 식별자 검색을 위한 메커니즘이라고 할 수 있다.
프로토타입이 소유한 프로퍼티(메서드 포함)를 프로토타입 프로퍼티,
인스턴스가 소유한 프로퍼티를 인스턴스 프로퍼티라고 부른다.
프로토타입 프로퍼티와 같은 이름의 프로퍼티를 인스턴스에 추가하면,
프로토타입 체인을 따라 프로토타입 프로퍼티를 검색하여 프로토타입 프로퍼티를 덮어쓰는 것이 아니라 인스턴스 프로퍼티로 추가한다.
이때 인스턴스 메서드 sayHello는 프로토타입 메서드 sayHello를 오버라이딩했고
프로토타입 메서드 sayHello는 가려진다
이처럼 상속 관계에 의해 프로토타입의 프로퍼티가 가려지는 현상을 프로퍼티 섀도잉이라 한다.
프로토타입의 교체
const Person = (function () {
function Person(name) {
this.name = name;
}
Person.prototype = {
sayHello() {
console.log(`Hi! My name is ${this.name}`);
},
};
return Person;
})();
const me = new Person('Lee');
console.log(me.constructor);
// Object, not Person
// Person.prototype이 리터럴 객체가 되었고 따라서 me.constructor는
// Person.prototype.prototype의 constructor인 Object가 된다.
instanceof
객체 instanceof 생성자 함수
우변의 생성자 함수의 prototype에 바인딩된 객체가 좌변의 객체의 프로토타입 체인 상에 존재하면 true로 평가되고, 그렇지 않은 경우에는 false로 평가된다.
function Person(name) {
this.name = name;
}
const me = new Person('name');
const parent = {};
Object.setPrototypeOf(me, parent);
console.log(Person.prototype === parent); // false
console.log(parent.prototype === Person); // false
Person.prototype = parent;
console.log(me instanceof Person); // true
console.log(me instanceof Object); // true
이처럼 instanceof 연산자는 프로토타입의 constructor 프로퍼티가 가리키는 생성자 함수를 찾는 것이 아니라,
생성자 함수의 prototype에 바인딩된 객체가 프로토타입 체인 상에 존재하는 지 확인한다.
in 연산자
in 연산자는 객체 내에 특정 프로퍼티가 존재하는지 여부를 확인한다.
/**
* key: 프로퍼티 키를 나타내는 문자열
* object: 객체로 평가되는 표현식
*/
key in object // true or false
// Reflect.has(object, key) 도 같은 결과를 반환함
in 연산자는 확인 대상 객체의 프로퍼티뿐만 아니라 확인 대상 객체가 상속받은 모든 프로토타입의 프로퍼티를 확인하므로 주의가 필요하다.
Object#hasOwnProperty는 객체 고유의 프로퍼티인 경우에만 true
프로퍼티 for ... in
for ... in 문은 열거가 가능한 상속받은 프로퍼티와 객체 고유의 프로퍼티를 열거한다.
const person = {
name: 'asdf',
age: 13,
};
for (const key in person) {
console.log(key, person[key]);
}
// name asdf
// age 13
toString 과 같은 Object.property의 프로퍼티는 열거되지 않는다.
이는 toString 메서드가 열거할 수 없도록 정의되어 있는 프로퍼티이기 때문이다.
Object.prototype.toString 프로퍼티의 프로퍼티 어트리뷰트 [[Enumerable]]은 false이다.
for ... in문은 객체의 프로토타입 체인 상에 존재하는 모든 프로토타입의 프로퍼티중에서 프로퍼티 어트리뷰트 [[Enumerable]]이 true인 프로퍼티를 순회하며 열거한다.
for ... in문은 프로퍼티 키가 심벌인 프로퍼티는 열거하지 않는다.
상속받은 프로퍼티는 제외하고 객체 자신의 프로퍼티만 열거하려면
Object#hasOwnProperty 메서드를 사용하여 객체 자신의 프로퍼티인지 확인해야 한다.
for... in 문은 프로퍼티를 열거할 때 순서를 보장하지 않으므로 주의 필요하다.
객체 자신의 고유 프로퍼티 열거
for (const key in object) {
if(!object.hasOwnProperty(key)) continue;
console.log(key, ': ', object[key]);
}
Object.prototype.hasOwnProperty를 사용하는 방법이 있다.
하지만 Object.keys/values/entries 메서드를 사용하는 것을 권장한다.
각각 객체 자신의 열거 가능한 프로퍼티 key, value, entry를 배열로 반환한다.
const A = {
a: 1,
b: 2,
__proto__: { c: 3 },
};
console.log(Object.keys(A));
// ['a', 'b']
console.log(Object.values(A));
// [ 1, 2 ]
모던 자바스크립트 Deep Dive
자바스크립트를 둘러싼 기본 개념을 정확하고 구체적으로 설명하고, 자바스크립트 코드의 동작 원리를 집요하게 파헤친다. 작성한 코드가 컴퓨터 내부에서 어떻게 동작할 것인지 예측하고, 명
www.aladin.co.kr