221211 TIL

오늘 한 일

  • 자바스크립트 완벽가이드 ~180p

    프로퍼티 열거

  • 객체가 상속하는 내장 메서드를 제외하고 대부분의 프로퍼티는 열거 가능하다.

for/in 루프를 사용하여 프로퍼티 전체를 순회한다면,

let o = {x: 1, y: 2, z: 3};
o.propertyIsEnumerable("toString") // => false: 열거 불가하기 때문이다.
for(let p in o) {
	console.log(p); // x, y, z를 출력한다.
}

상속된 프로퍼티가 열거되는 것을 막을 때는

for(let p in o) {
	if (!o.hasOwnProperty(p)) continue; // 상속된 프로퍼티를 건너뛴다.
}

for(let p in o) {
	if (typeof o[p] === "function") continue; //메서드는 건너뛴다.
}

객체의 프로퍼티 이름을 배열에 저장하여 for/of 루프를 사용하는 방법도 있다.

  • 프로퍼티 이름을 배열로 저장할 수 있는 함수
    • Object.keys() : 객체의 열거 가능한 자체 프로퍼티 이름을 배열로 반환
      • 열거 불가 프로퍼티, 상속된 프로퍼티, 이름이 심벌인 프로퍼티 제외
    • Object.getOwnPropertyNames() : Object.keys() + 이름이 문자열인 열거 불가 프로퍼티를 배열로반환
    • Object.getOwnPropertySymboles() : 열거 가능 여부 따지지 않고 이름이 심벌인 자체 프로퍼티를 배열로 반환
    • Reflect.ownKeys() : 열거 가능 여부, 문자열 심볼 따짖지지 않고자체 프로퍼티를 전부 반환

프로퍼티의 열거 순서

Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymboles(), Reflect.ownKeys(), JSON.stringify() 등 관련 메서드는 다음 순서에 따라 프로퍼티 열거한다.

  1. 이름이 음이 아닌 정수인 문자열 프로퍼티가 첫번째로 나열
    1. 작은 수 → 큰 수
    2. 배열 및 배열 비슷한 객체의 프로퍼티도 마찬가지
  2. 배열 인덱스와 비슷한 프로퍼티를 모두 열거한 다음 음수나 부동 소수점 숫자처럼 보이는 프로퍼티를 포함해 이름이 문자열인 프로퍼티 열거
    1. 객체에 추가된 순서대로 열거된다.
    2. 객체 리터럴로 정의된 프로퍼티는 리터럴에 쓰인 순서를 따른다.
  3. 마지막으로 이름이 심벌인 프로퍼티를 추가된 순서대로 열거한다.

객체 확장

자바스크립트에서 객체의 프로퍼티를 다른 객체로 복사하고 싶을 때 다음과 같은 코드를 사용할 수 있다.

let target = {x: 1}, source = {y: 2, z: 3};
for(let key of Object.keys(source)) {
	target[key] = source[key];
}
target // => {x: 1, y: 2, z: 3}

복사는 흔한 일이기 때문에 ES6에서는 이런 기능을 Object.assign()이라는 이름으로 도입했다.

Object.assign()

  • 인자로 두 개 이상의 객체를 받으며 첫 번째 인자는 수정해서 반환할 대상 객체, 두번째 인자는 소스 객체이다.
  • 이름이 심벌인 것을 포함한 열거 가능한 자체 프로퍼티를 대상 객체에 복사한다.
  • 복사할 프로퍼티가 대상 객체에 존재한다면, 대상 객체의 프로퍼티를 복사할 값으로 덮어쓴다.
  • 소스 객체에 기본 값을 정의해두고 사용하는 것이 목적이라면 다음과 같이 사용할 수 있다.
o = Object.assign({}, defaults, o); // o를 전부 default로 덮어 쓴다.
  • 구조 분해 연산자를 사용할 수도 있다. (6.10.4절 참고)
o = { ...default, ...o };
  • 존재하지 않는 프로퍼티만 복사하고 싶다면, Object.assign을 변형하여 사용할 수도 있다.
function merge(target, ...sources) {
    for(let source of sources) {
        for(let key of Object.keys(source)) {
            if(!(key in target)) { // 존재하지 않는 키만 추가
                target[key] = source[key];
            }
        }
    }
    return target;
}

Object.assign({x: 1}, {x: 2, y: 2}, {y: 3, z: 4}); // {x: 2, y: 3, z: 4}
merge({x: 1}, {x: 2, y: 2}, {y: 3, z: 4}); // {x: 1, y: 2, z: 4}

객체 직렬화

객체 직렬화는 객체를 문자열로 변환하는 작업이다. 직렬화 후 문자열을 다시 객체로 되돌릴 수 있다.

→ JSON.stringify(), JSON.parse() 함수를 이용한다.

JSON은 자바스크립트 문법의 부분 집합이다.

  • JSON이 지원하는 것 : 객체, 배열, 문자열, 유한한 숫자, true, false, null
  • 직렬화 시 변환되는 것 :
    • NaN, Infinity, -Infinity ⇒ null
    • Date 객체 ⇒ ISO 형식 날짜 문자열 (ex : Date.toJSON)
  • JSON.parse()에서 함수, RegExp, Error 객체, undefined 값은 직렬화하거나 복원 불가하다.
  • JSON.stringify()는 열거 가능한 자체 프로퍼티만 직렬화 하고 직렬화 할 수 없는 값은 결과에서 생략된다.

객체 메서드

Object.prototype에서 상속되는 프로퍼티는 대부분이 메서드이다 (hasOwnProperty() 같은..)

Object.prototype에 정의된 메서드 중 사용자가 변형하여 사용할 수 있도록 의도된 공용 메서드를 알아보자.

1) toString() 메서드

호출한 객체의 값을 나타내는 문자열을 반환

let s = { x: 1, y: 1 }.toString(); // s == "[object Object]"

⇒ 유용하지 않기 때문에 자신만의 메서드를 지정하여 사용한다.

다음과 같이 정의할 수 있다.

let point = {
	x: 1,
	y: 2,
	toString: function() { return `${this.x}, ${this.y}` };
}

String(point) // => "1, 2" 문자열로 반환할 때 toString() 호출

2) toLocaleString() 메서드

목적은 지역에 맞는 문자열 표현을 반환하는 것이지만 메서드 자체에 지역화 기능이 없으며 toString()을 호출해 값만 반환한다.

Date 객체와 숫자 클래스에는 숫자, 날짜, 시간을 지역의 관습에 맞게 표현하는 toLocaleString()이 있다.

⇒ 메서드 재작성 하고 싶다면 국제화 클래스 키워드를 찾아보자

3) valueof() 메서드

객체를 문자열이 아닌 다른 기본 타입, 보통은 숫자를 변환할 때 사용한다.

내장 클래스 중에서 valueOf() 메서드를 따로 정의하여 사용한 것도 있다.

  • Date.valueof() 는 날짜를 숫자로 변환한다.

      new Date().valueOf() // => 1670753951694
    

4) toJSON() 메서드

Object.prototype에 toJSON() 메서드가 정의되어 있지는 않지만, JSON.stringify()메서드에서 직렬화할 객체에서 toJSON() 메서드를 검색한다.

Date 클래스의 toJSON() 메서드는 직렬화 가능한 문자열 표현을 반환한다.

new Date().toJSON() // => 2022-12-11T10:22:40.533Z

확장된 객체 리터럴 문법

단축 프로퍼티

식별자가 같을 경우 생략이 가능하다.

  • 변경 전

      let x = 1, y = 2;
      let o = {
      	x: x,
      	y: y
      }
    
  • 변경 후

      let x = 1, y = 2;
      let o = {	x, y }
    

계산된 프로퍼티 이름

ES6 이전에는 특정 프로퍼티가 있는 객체를 만들 때, 프로퍼티 이름이 소스 코드에 문자 그대로 입력할 수 있는 컴파일 타입 상수가 아닌 변수에 저장되어 있거나, 함수의 반환 값일 때 객체 리터럴에 사용할 수 없었다.

객체를 만든 후 프로퍼티를 추가하는 과정이 필요했다.

const PROPERTY_NAME = "p1";
function computePropertyName() { return "p" + 2; }

let o = {};
o[PROPERTY_NAME] = 1;
o[computePropertyName()] = 2;

ES6 이후 계산된 프로퍼티(computed property)라는 기능을 사용하면 대괄호를 객체 리터럴 안에 넣을 수 있다.

const PROPERTY_NAME = "p1";
function computePropertyName() { return "p" + 2; }

let o = {
 [PROPERTY_NAME]: 1,
 [computePropertyName()]: 2
};

대괄호 안에 임의의 자바스크립트 표현식이 들어간다. 표현식을 평가한 값을 프로퍼티 이름으로 사용하고 필요하면 문자열로 변환한다.

프로퍼티 이름인 심벌

계산된 프로퍼티 문법을 통해 가능해진 중요한 객체 리터럴 기능이 있다.

ES6 이후 프로퍼티 이름에 문자열이나 심벌을 쓸 수 있게 되었다.

심벌을 변수나 상수에 할당하면 계산된 프로퍼티 문법을 통해 그 심벌을 프로퍼티 이름으로 쓸 수 있다.

const extension = Symbol("my extension symbol");
let o = {
	[extension]: { /* 여기에 확장 데이터를 저장한다. */ }
};
o[extension].x = 0; // o의 다른 프로퍼티와 충돌하지 않는다.
  • 심벌은 프로퍼티 이름 이외에 다른 용도로 사용할 수 없다.
  • Symbol()에 전달되는 문자열은 심벌을 문자열로 변환할 때 사용한다.
  • 자바스크립트 객체가 사용할 수 있는 안전한 확장 메커니즘을 정의하는 것이다.
  • 제어할 수 없는 서드 파티 코드에서 가져온 객체에 프로퍼티를 추가하고 싶지만, 추가한 프로퍼티가 이미 존재하는 프로퍼티와 충돌하는지 확신할 수 없을 때 프로퍼티 이름에 심벌을 사용하면 안전하다.
  • 추가한 심벌을 확인하려면 Object.getOwnPropertySymbols()를 사용할 수 있다.
    • 메서드를 사용해 심벌을 가져올 수 있기 때문에 보안 목적을 위해 심벌을 사용하지는? 않는다.

분해 연산자

  • ES2018 이후에 객체 리터럴 안에서 분해 연산자 를 사용해 기존 객체의 프로퍼티를 새 객체에 복사할 수 있게 되었다.
  • 점 세개를 다른 목적으로 사용하는 경우도 있지만 객체 리터럴에 사용할 경우 객체에 분산하는 기능으로 사용할 수 있다.
  • 분해되는 객체와 프로퍼티를 받는 객체 둘 다 같은 이름의 프로퍼티를 갖는다면 해당 프로퍼티의 값은 마지막에 오는 값이 된다.
let o = { x: 1 };
let p = { x: 0, ...o };
p.x // => 1: 객체 o의 값이 초깃값을 덮어 씀
let q = { ...o, x: 2 };
q.x // => 2: 기존 o 값을 덮어씀
  • 상속된 프로퍼티에는 적용되지 않는다.
let o = Object.create({x: 1}); // 프로퍼티 x를 상속하는 객체 o
let p = { ...o };
p.x // => undefined

🐸  분해 연산자를 사용할 경우 자바스크립트 인터프리터가 많은 일을 담당하게 된다. 객체에 프로퍼티 n개가 있다면 다른 객체로 분해하는 작업은 O(n)이다. …를 루프나 재귀함수에 넣어 데이터를 큰 객체 하나에 누산한다면 n이 커질수록 비효율적인 O(p²) 알고리즘을 쓰게 된다.

단축 메서드

객체 프로퍼티로 정의된 함수를 메서드라고 부른다.

ES6 전에는 객체 리터럴 안에 함수 정의 표현식을 써서 메서드를 정의했다.

let square = {
	area: function() { return this.side * this.side },
	side: 10
}
square.area() // => 100

ES6 이후 객체 리터럴 문법에서 function 키워드, 콜론을 생략할 수 있게 되었다.

let square = {
	area() { return this.side * this.side; }
	side: 10
}
square.area() // => 100

문자열 리터럴이나 계산된 프로퍼티 이름, 심벌 역시 사용할 수 있다.

const METHOD_NAME = "m";
const symbol = Symbol();
let weirdMethods = {
	"method With Spaces"(x) { return x + 1; },
	[METHOD_NAME](x) { return x + 2; },
	[symbol](x) { return x + 3; }
};

메서드 이름에 심벌을 사용하는 것은 의아해하지 않아도 된다. 객체를 for/of 루프에서 사용할 수 있도록 이터러블로 만들려면 반드시 심벌 이름 Symbol.iterator를 사용할 메서드를 정의해야 한다.

프로퍼티 게터와 세터

자바스크립트는 접근자 메서드 getter, setter를 갖는 접근자 프로퍼티(accessor property)를 지원한다.

  • 프로그램이 접근자 프로퍼티의 값을 검색하면 자바스크립트는 인자 없이 게터 메서드를 호출한다.
    • 게터 메서드의 반환 값이 프로퍼티 접근 표현식의 값이다.
  • 프로그램이 접근자 프로퍼티의 값을 설정하면 자바스크립트는 세터 메서드를 호출하고 할당 표현식의 오른쪽 값을 인자로 전달한다.
    • 세터 메서드의 반환 값은 무시한다.
  • 게터/세터가 모두 있다면 읽기와 쓰기가 모두 가능한 프로퍼티, 게터만 있다면 읽기 전용 프로퍼티, 세터 메서드만 있다면 쓰기 전용 프로퍼티이다. (데이터 프로퍼티에서는 쓰기 전용이 불가능)
  • 게터와 세터 접근자 프로퍼티는 ES5에서 도입한 객체 리터럴 문법의 확장이다.