[모던 자바스크립트 Deep Dive] - 16~18장

Lee Jeong Min·2021년 9월 15일
0
post-thumbnail

이 글은 책 모던 자바스크립트 16장 ~ 18장을 읽고 정리한 글입니다.

16장 - 프로퍼티 어트리뷰트

내부 슬롯과 내부 메서드

내부 슬롯과 내부 메서드는 자바스크립트 엔진이 구현 알고리즘을 설명하기 위해 ECMAScript 사양에서 사용하는 의사 프로퍼티와 의사 메서드이다.

내부 슬롯과 내부 메서드는 이중 대괄호([[...]])로 감싼 이름들이 모두 내부 슬롯과 내부 메서드이다.

개발자가 직접 접근할 수 없게 설계되었으며 일부 몇개의 내부 슬롯과 내부 메서드에 간접적으로 접근할 수 있는 수단이 존재한다.

const o = {};

o.[[Prototype]] // Uncaught SyntaxError

o.__proto__ // Object.prototype

모든 객체는 [[Prototype]] 이라는 내부 슬롯을 갖는데 이 슬롯의 경우 __proto__ 를 통해 간접적으로 접근이 가능하다

프로퍼티 어트리뷰트와 프로퍼티 디스크립터 객체

JS엔진은 프로퍼티를 생성할 때, 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의한다.

프로퍼티 어트리뷰트: 내부 상태 값인 내부 슬롯 [[Value]], [[Writable]], [[Enumerable]], [[Configurable]] 이다.

Object.getownpropertyDescriptor 메서드를 사용하여 간접적으로 확인이 가능하다.

const person = {
  name: 'Lee'
};

// 프로퍼티 어트리뷰트의 정보를 제공하는 프로퍼티 디스크립터 객체를 반환한다.
console.log(Object.getOwnPropertyDescriptor(person, 'name'));
// {value: "Lee", writable: true, enumerable: true, configurable: true}

위 메서드를 사용하게 되면 프로퍼티 디스크립터 객체를 반환한다. 이는 프로퍼티 어트리뷰트 정보를 제공한다. 존재하지 않거나 상속받은 프로퍼티에 대한 디스크립터 요구시 undefinedd가 반환된다.

메서드 사용법

// 하나의 프로퍼티에 대한 프로퍼티 디스크립터 객체 반환
Object.getOwnPropertyDescriptor(person, 'name');

// 모든 프로퍼티에 대한 프로퍼티 디스크립터 객체 반환 (ES8에서 도입)
Object.getOwnPropertyDescriptors(person);

데이터 프로퍼티와 접근자 프로퍼티

  • 데이터 프로퍼티: 키와 값으로 구성된 일반적인 프로퍼티

  • 접근자 프로퍼티: 자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할때 호출되는 접근자 함수로 구성된 프로퍼티

데이터 프로퍼티

프로퍼티 어트리뷰트프로퍼티 디스크립터(객체의 프로퍼티)설명
[[Value]]value프로퍼티 키를 통해 프로퍼티 값에 접근시 반환되는 값. 프로퍼티 없으면 프로퍼티 동적 생성하고 생성된 프로퍼티 [[Value]]에 값 저장
[[Writable]]writable프로퍼티 값의 변경 가능 여부 나타냄, false인 경우 값 변경 X -> 읽기 전용
[[Enumerable]]enumerable프로퍼티 열거 가능 여부, false인 경우 for ... in 문이나 Object.keys 메서드로 열거 불가
[[Configurable]]configurable프로퍼티 재정의 가능 여부, false인 경우 삭제, 값의 변경 금지. 단, [[Writable]]이 true인 경우 [[Value]] 변경과 [[Writable]]을 false로 변경하는 것 허용

프로퍼티가 생성될 때 [[Value]]의 값은 프로퍼티 값으로 초기화되며 [[Writable]], [[Enumerable]], [[Configurable]]의 값은 모두 true로 초기화 (프로퍼티 동적 생성도 마찬가지 --> person이라는 객체에 person.age = 25; 로 동적 생성하면 value는 25로, 나머지는 true로 초기화!)

접근자 프로퍼티

자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티이다.

프로퍼티 어트리뷰트프로퍼티 디스크립터(객체의 프로퍼티)설명
[[Get]]get접근자 프로퍼티로 값을 읽을 때 호출되는 접근자 함수(getter 함수 호출)
[[Set]]set접근자 프로퍼티로 값을 저장할 때 호출되는 접근자 함수(setter 함수 호출)
[[Enumerable]]enumerable데이터 프로퍼티의 [[Enumerable]]와 설명 같음
[[Configurable]]configurable데이터 프로퍼티의 [[Configurable]] 와 같음

프로토타입과 프로토타입 체인

프로토타입은 어떤 객체의 상위 객체의 역할을 하는 객체이다. 이 프로토타입 객체의 프로퍼티나 메서드를 상속받은 하위 객체는 자신의 프로퍼티 또는 메서드인 것 처럼 자유롭게 사용이 가능하다.

프로토타입 체인은 프로토타입이 단방향 링크드 리스트 형태로 연결되어 있는 상속 구조이다. 객체의 프로퍼티나 메서드에 접근하려고 할 때 해당 객채에 접근하려는 프로퍼티 또는 메서드가 없다면 프로토타입 체인을 따라 차례대로 검색한다.

// 일반 객체의 `__proto__`는 접근자 프로퍼티
Object.getOwnpropertyDescriptor(Object.prototype, '__proto__');
// get, set enumerable, configurable

// 함수 객체의 prototype은 데이터 프로퍼티
Object.getOwnpropertyDescriptor(function() {}, 'prototype');
// value, writable enumerable, configurable

프로퍼티 정의

새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나, 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의하는 것

Object.defineProperty 함수로 정의 및 재정의를 할 수 있으며 인수로 객체의 참조, 데이터 프로퍼티의 키, 프로퍼티 디스크립터 객체를 전달한다.

Object.defineProperty(person, 'name' { value: 'JeongMin', writable: true enumerable: true, configurable: true });

디스크립터의 객체의 프로퍼티를 누락시키는 경우 value, get ,set은 undefined로, writable, configurable, enumerable은 false로 기본값이 설정된다.

여러개의 프로퍼티를 한 번에 정의려면 Object.defineProperties(객체, 디스크립터 객체) 로 사용하면 된다.

예시

Object.defineProperties(person, 
                        { firstName: { 데이터 프로퍼티 }, 
                         lastName: { 데이터 프로퍼티 }, 
                         fullName: { 접근자 프로퍼티 } });

객체 변경 방지

구분메서드(프로퍼티) 추가삭제값 읽기값 쓰기어트리뷰트 재정의
객체 확장 금지Object.preventExtensionsXOOOO
객체 밀봉Object.sealXXOOX
객체 동결Object.freezeXXOXX

객체 확장 금지

확장이 금지된 객체는 프로퍼티 추가가 금지된다. --> preventExtensions(객체) 메서드
확인 메서드 --> isExtensible(객체)

객체 밀봉

읽기와 쓰기만 가능

isSealed(객체) 메서드로 seal되었는 지 확인

객체 동결

읽기만 가능

isFrozen(객체) 메서드로 동결되었는 지 확인.

불변 객체

위의 메서드들은 얕은 변경 방지 --> 중첩 객체까지 동결할 수 없다.

불변 객체 구현을 위해서는 객체를 값으로 갖는 모든 프로퍼티에 대해 재귀적으로 Object.freeze 메서드를 호출해야 한다.

function deepFreeze(target) {
  if(target && typeof target === 'object' && !Object.isFrozen(target)) {
    Object.freeze(target);
    
    // 모든 프로퍼티 순회
    Object.keys(target).forEach(key => deepFreeze(target[key]));
  }
  return target;
}

deepFreeze(객체);

깊은 객체 동결 (재귀함수로 구현)


17장 - 생성자 함수에 의한 객체 생성

객체 리터럴 이외에도 다양한 방법으로 생성할 수 있는데, 생성자 함수를 사용하여 객체를 생성하는 방식에 대해서 알아보고, 객체 리터럴 방식과 생성자 함수 방식의 장단점에 대해 알아보자.

Object 생성자 함수

const person = new Object();

생성자 함수란 new 연산자와 함게 호출하여 객체를 생성하는 함수이다.
이때 생성된 객체를 인스턴스라고 한다 (person이 인스턴스)
Object 외에도 String, Number, Boolean 등등의 생성자 함수를 제공한다.

생성자 함수

객체 리터럴에 의한 객체 생성 방식의 문제점

동일한 프로퍼티를 갖는 객체를 여러 개 생성해야하는 경우 매번 같은 프로퍼티를 기술해야함.

--> circle이라는 객체 여러개

생성자 함수에 의한 객체 생성 방식의 장점

객체를 생성하기 위한 템플릿(클래스)처럼 생성자 함수를 사용하여 프로퍼티 구조가 동일한 객체 여러 개를 간편하게 생성할 수 있다.

function Circle(radius) {
  this.radius = radius;
  this.getDiameter = function () {
    return 2 * this.radius;
  };
}

const circle1 = new Circle(5);
const circle2 = new Circle(10);

객체 여러개를 객체 리터럴로 정의할 때보다 간편하게 생성할 수 있다. (함수 하나만 정의해 놓고 new연산자와 같이 생성자 함수로 객체 생성)

생성자 함수는 일반 함수와 동일한 방법으로 생성자 함수를 정의하고 new 연산자와 함께 호출하면 생성자 함수로 동작한다.

new연산자 같이 안쓰면 생성자 함수가 아닌 일반 함수로 동작

this

this는 자기 참조 변수로 this가 가리키는 값 바인딩은 함수 호출 방식에 따라 동적으로 결정된다.

일반함수로서 호출 --> this는 전역 객체를 가리킴
메서드로서 호출 --> this는 메서드를 호출한 객체를 가리킴
생성자 함수로서 호출 --> this는 생성자 함수가 생성할 인스턴스를 가리킴

생성자 함수의 인스턴스 생성 과정

생성자 함수의 역할: 인스턴스를 생성하기 위한 템플릿(클래스)으로서 동작하여 인스턴스 생성, 생성된 인스턴스를 초기화(인스턴스 프로퍼티 추가 및 초기값 할당)

함수 내부에 인스턴스 생성및 반환하는 코드가 없어도 JS엔진은 암묵적인 처리를 통해 인스턴스를 생성하고 반환한다. 그 처리과정은 아래와 같다.

  1. 인스턴스 생성과 this 바인딩 (런타임 이전에 암묵적으로 빈 객체 생성, 이 빈 객체는 인스턴스이고 this에 바인딩 된다.)

바인딩 --> 식별자와 값을 연결하는 과정 (this라는 식별자)

  1. 인스턴스 초기화 (런타임에 코드가 한줄 씩 실행되어 this에 바인딩 되어 있는 인스턴스 초기화)

  2. 인스턴스 반환 (완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다.)

3번의 과정에서 만약 다른 객체를 명시적으로 반환하면 this가 반환되지 못하고 명시한 객체가 반환되고, 원시값 반환 시 암묵적으로 this가 반환된다.
생성자 함수 내부에선 return문을 반드시 생략해야 생성자 함수의 기본 동작을 훼손시키지 않을 수 있다.

내부 메서드 [[Call]]과 [[Construct]]

함수 선언문 또는 함수 표현식으로 정의한 함수는 생성자 함수로서 호출할 수 있다 (new와 같이 호출하여 객체 생성한다는 말)

함수는 객체이므로 일반 객체와 같이 동일하게 동작하지만 유일한 차이점은 일반객체는 호출할 수 없고, 함수는 호출이 가능하다

함수 호출 시: 함수 객체의 내부 메서드 [[Call]]이 호출 --> 이것을 가지고 있는 함수 객체는 callable

생성자 함수 호출 시: 내부 메서드 [[Construct]]가 호출 --> 이것을 가지고 있는 함수 객체 constructor, 그렇지 않은 것은 non-constructor

모든 함수 객체는 callable이지만 constructor일 수도 있고 non-constructor일 수도 있다.

constructor와 non-construcor 구분

  • constructor: 함수 선언문, 함수 표현식, 클래스
  • non-constructor: 메서드(ES6 메서드 축약 표현), 화살표 함수

non-constructor인 메서드 및 함수는 new연산자와 같이 사용하였을 시, TypeError발생

객체의 메서드로 정의할 시, 메서드 축약표현이 아닌 함수형식(x: function() {})으로 정의한 것들은 일반 함수로 정의되어 constructor이다.

ECMAScript 사양에서 메서드란 ES6의 메서드 축약 표현 만을 의미한다.

new 연산자

new 연산자와 함께 함수를 호출하게 되면 내부 메서드 [[Call]\이 호출 되는 것이 아니라 [[Construct]]가 호출된다. 단 이 경우 함수는 non-constructor가 아닌 constructor이어야 한다.

생성자 함수는 파스칼 케이스로 명명하여 일반 함수와 구별할 수 있도록 노력하자.

new.target

new 연산자와 함께 생성자 함수로서 호출되면 함수 내부의 new.target은 함수 자신을 가리킨다. new 연산자 없이 일반함수로서 호출된 함수 내부의 new.target은 undefined다.

if (!new.target) { return new Circle(radius); }

ES6에서 도입되어 IE는 지원하지 않아서 아래의 스코프 세이프 생성자 패턴을 사용하기도 함.

스코프 세이프 생성자 패턴

if (!(this instanceof Circle)) { return new Circle(radius); }

18장 - 함수와 일급 객체

일급 객체

일급 객체의 조건

  • 무명의 리터럴로 생성할 수 있다. 즉 런타임에 생성이 가능하다.
  • 변수나 자료구조(객체, 배열 등)에 저장할 수 있다.
  • 함수의 매개변수에 전달할 수 있다.
  • 함수의 반환값으로 사용할 수 있다.

함수는 일급 객체이면서도 호출 가능한 특성을 가지고 있다.

함수 객체의 프로퍼티

함수는 객체이므로 함수또한 프로퍼티를 가질 수 있기 때문에 console.dir, Object.getOwnPropertyDescriptor로 확인해볼 수 있다.

확인 결과 함수 객체의 데이터 프로퍼티에는 arguments, caller, length, name, prototype과 같은 고유의 프로퍼티를 가지고 있음을 알 수 있다.

Object.prototype객체의 프로퍼티는 모든 객체가 상속받아 사용할 수 있기 때문에 __proto__ 접근자 프로퍼티는 모든 객체가 사용할 수 있다.

arguments 프로퍼티

arguments 객체는 함수 호출 시 전달된 인수들의 정보를 담고 있는 순회가능한 유사배열 객체이며 함수 내부에서 지역 변수처럼 사용된다. --> 가변 인자 함수를 구현할 때 유용

Function.arguments 처럼 사용하면 X, 지역 변수처럼 사용하라

arguments 객체는 인수를 프로퍼티 값으로 소유하고, 키는 인수의 순서를 나타낸다.

프로퍼티로 인수 외에도 callee(arugments객체를 생성한 함수), length(인수 개수)를 가지고 있다.

최종적으로는 이 arguments 객체의 번거로움을 해결하기 위해 Rest 파라미터를 도입하였다.

caller 프로퍼티

caller 프로퍼티는 함수 자신을 호출한 함수를 가리킨다.

ECMAScript 사양에 포함되지 않는 비표준 프로퍼티이다.

length 프로퍼티

함수를 정의할 때 선언한 매개변수의 개수를 가리킨다.

length !== arguments 객체의 length

length --> 매개변수 개수

arugments length --> 인자(수)의 개수

name 프로퍼티

함수 이름을 나타내고 익명함수인 경우 빈 문자열을 값으로 갖는다.

__proto__ 접근자 프로퍼티

[[Prototype]] 내부 슬롯이 가리키는 프로토타입 객체에 접근하기 위해 사용하는 접근자 프로퍼티다.

모든 객체는 [[Prototype]] 이라는 내부 슬롯을 갖고 __proto__를 통해 접근할 수 있다.

hasOwnProperty 메서드

인수로 전달받은 프로퍼티 키가 객체 고유의 프로퍼티 키인 경우에만 true이고 상속받은 프로토타입의 프로퍼티 키인 경우 false를 반환한다.

prototype 프로퍼티

함수가 객체를 생성하는 생성자 함수로 호출될 때 생성자 함수가 생성할 인스턴스의 프로포타입 객체를 가리킨다.

생성자 함수로 호출할 수 있는 함수 객체, constructor만이 소유하는 프로퍼티이다.

일반 객체와 non-constructor 함수는 prototype 프로퍼티가 없다.

수업시간 필기

breakChange --> 이전버전과 완전하게 달라진 부분이 있다.

버전 -> 메이저, 마이너, 패치
1.0.0

바벨 --> 배포 전 단계이기 때문에 devDependencies

Dependencies --> 배포단계에서 사용하는 것

Pacakage json --> 똑같은 개발환경을 굳이 다 만들필요 없이 의존성 관리를 해주기

팀장급 한명이 이것을 만들어서 그냥 팀원들은 받아서 npm install을 하면 됨

스코프 == 네임스페이스

메서드를 어디에 집어 넣을 것이냐? --> 빌트인 객체들은 어떻게 메서드를 구성했느냐? 에서 해답을 조금 얻을 수 있음

메서드 --> 인스턴스 메서드, 프로토타입 메서드, 정적 메서드

재할당이 필요한 경우 let 사용시 변수의 스코프 --> 최대한 좁게(라인수 적게)하는것이 재할당 될 기회를 줄여준다.


16장

객체 리터럴로 만든 객체의 프로토타입은 Object.prototype이다.

모든 객체는 내부 슬롯[[Prototype]]을 갖는다. <-- 여기에 접근하기 위해 proto 로 접근할 수 있음

배열 --> 순회: 순서대로 회전하는거

열거: 여러개를 나열하는 거

Object.keys(객체) --> 객체의 키들이 순서대로 안나와도 댐(비표준임), 그러나 최신 자바스크립트 엔진은 순서대로 출력해줌
객체의 프로퍼티 키를 열거할 때에는 순서가 무의미함.(상속받은거는 출력 안함)

In 연산자는 객체의 프로퍼티를 확인하기 위해서 사용하는데 상속받은 o가 사용할 수 있는 프로퍼티까지 다 열거하여 확인한다.f

for..in 문은 객체가 가지고 있는 (상속받은 거 까지) 프로퍼티 개수만큼 for문을 돈다. --> 일반 Object.prototype의 프로퍼티들은 enumerable이 false이기 때문에 콘솔로 for문을 통해 찍어보면 나오진 않음

const o2 = {
    a: 1,
    b: 2,
    c: 3,
    __proto__: {x : 10},
};

이렇게 작성 시 x는 enumerable 이기 때문에 원하지 않는 프로퍼티의 키인 경우 if문을 사용하여 filter하는 작업이 필요함

for(const key in o) {
  if(o.hasOwnProperty(key)) {
    console.log(key);
  }
}

이러한 경우 eslint에는 if문 안이 빨간줄이 쳐지는데 o.hasOwnProperty는 o가 hasOwnProperty를 상속받거나 가지고 있어야하는데 프로퍼티를 조작하는 경우 o객체가 이것을 가지고 있지 않을 수 있기 때문에 Object.hasOwnProperty.call(o, key)) 로 사용하는 것이 좋다고 함.
for문 안에서 const를 쓰는 이유는 재할당이 굳이 필요 없기 때문에(for문이 한번 돌 때마다 변수의 생명주기가 끝남)

데이터, 접근자 프로퍼티의 근본적 다른점 --> [[Value]]가 있느냐 없느냐 --> 프로퍼티 값

접근자 프로퍼티: 자체적으로 값을 갖지않고 다른 데이터 프로퍼티의 값을 읽거나 저장할때 호출되는 접근자 함수로 구성된 프로퍼티(accessor function)

생긴것은 메서드처럼 생겼지만 --> fullName 자체는 프로퍼티(메서드가 아닌)이다. person.fullName으로 접근
Get -> return문이 존재해야한다.(데이터 프로퍼티를 조작하여 값을 얻기 위해 사용)

메서드와 getter setter의 차이 --> 함수호출 vs 표현식의 차이 (표현식이 더 좋은 방법이긴한데 아무거나 써도 무방하다고 봄)

o.__proto__를 왜 메서드가 아닌 표현식으로 만들었는 지? --> 함수가 아닌 그냥 접근하는 것처럼 보이기 위해

readOnly --> freeze 통해, 상수값을 만들기 위해! --> 객체를 상수처럼 쓰고 싶을 때


17장

클래스는 반드시 new를 붙여야한다.

생성자 함수를 통해서 객체를 만들어야할 일이 있다. --> 클래스를 쓰는것을 권장한다.

그러나 프론트엔드에서 이벤트같은 처리를 할 때 클래스를 사용하여 this 사용시 엄청난 문제가 생김

리액트도 class 대신 function형으로 만드는 것을 권장한다!

JS에선 생성자 함수나 클래스로 찍어냈을 때 인스턴스라고 한다.

생성자 함수의 문제점 --> this가 동적으로 결정됨, 메서드의 내용이 반복될 경우, 인스턴스 개수만큼 똑같은 코드를 생성함

const foo = function() {
    console.log(this);
}

foo(); // 일반함수 -> this는 전역을 가리킴


Const inst = new foo(); // 생성자 함수 -> this는 생성자 함수가 생성할 인스턴스 (foo {})

Const o = { foo };
o.foo(); // 메서드로서 호출 -> this는 메서드를 호출한 객체(마침표 앞의 객체)

This = {} 이렇게 명시적으로 this는 할당이 불가능하다.

인스턴스 생성후 this와 바인딩

데이터의 경우 인스턴스 자신이 가지고 있을 필요가 있지만 함수의 경우 내용이 같으므로 인스턴스 내부에 있는 것 보다 프로토타입에 가지고 있는 것이 더 낫다

new.target 전체가 키워드라고 보면 됨


18장

JS -> 함수형 프로그래밍 언어 (이유? 함수가 일급 객체이기 때문에)

일급 객체 -> 객체랑 똑같다.

무명의 리터럴 --> 변수에 담아야한다 --> 변수에 할당할 수 있는 값이다.
런타임에 생성이 가능하다.

폐지 --> 이런 문법을 쓰면 안된다는 뜻

Arguments --> 인수의 정보, 개수

객체가 함수 객체보다 큰 범위이기 때문에 함수 객체는 객체의 특성을 모두 갖고 있다고 볼 수 있다.

person이라는 생성자 함수가 있다고 하였을 때, 함수가 만들어지면서 그 함수 이름과 같은 prototype이 만들어짐. person은 prototype 프로퍼티로 person.prototype을 가리키고 person.prototype은 constructor로 person을 가리키고 있음.

함수 객체에만 prototype이 있다.

함수 객체로 만들어진 일반 객체는 [[Prototype]] 을 가지고 있는데 이는 person.prototype과 연결됨

person.prototype --> 또한 일반 객체라 [[prototype]]을 가지고 있고 이는 Object.prototype을 가리킴

인스턴스에 만들면 메모리 공간이 계속 차지되기 때문에 prototype과 상속을 이용하여 이 문제를 해결해줌

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글