객체

김수정·2020년 4월 9일
0

객체란

프로퍼티에 다양한 종류의 값을 저장할 수 있는 점을 활용하여 데이터 컬렉션이나 복잡한 개체를 표현할 때 사용합니다.

객체 생성

let user = new Object(); // '객체 생성자' 문법
let user = {
  name: "John",  // 키: "name",  값: "John"
  age: 30,       // 키: "age", 값: 30
  "likes birds": true,  // 복수의 단어는 따옴표로 묶어야 합니다.
};  // '객체 리터럴' 문법

CRUD data

// read data
alert( user.name ); // John
alert( user["likes birds"] );

// create & update data
user.isAdmin = true;

// delete data
delete user.age;

대괄호 표기법
객체의 키를 지정하는 방법은 크게 2가지 입니다. '.'과 '[]'이죠. 대괄호는 점보다 더 유연하게 동작이 가능한데요.
대괄호는 키가 띄어 쓰기가 되어있어도 키를 인지할 수 있고, 대괄호 안에 있는 표현식의 평가를 런타임에서 하기 때문에 키 값을 변수로 넣을 수도 있습니다.
가능한 경우

let user = {
  name: "John",
  age: 30
};
let key = prompt("사용자의 어떤 정보를 얻고 싶으신가요?", "name");
alert( user[key] ); // John (프롬프트 창에 "name"을 입력한 경우)

불가능한 경우

let user = {
  name: "John",
  age: 30
};
let key = "name";
alert( user.key ) // undefined

또한, 아예 객체의 키를 대괄호로 지정하면 처음부터 변수를 받을 수도 있습니다. 이를 계산된 프로퍼티 라고 부릅니다.

let fruit = 'apple';
let bag = {
  [fruit + 'Computers']: 5 // bag.appleComputers = 5
};

property

프로퍼티 이름(키)는 반드시 문자열이거나 심볼형이어야 합니다. 아닐 경우 문자열로 자동 형변환 됩니다.

  • 계산된 프로퍼티 : 위에서 설명함
  • 단축 프로퍼티(shorthand property) : 키와 값이 변수의 이름과 동일할 경우 한 번만 적습니다.
function makeUser(name, age) {
  return {
    name, // name: name 과 같음
    age   // age: age 와 같음
    // ...
  };
}

property flag

객체 프로퍼티는 특별한 속성(flag)이 있습니다.

  • writable : 값을 수정할 수 있는지 여부
  • enumerable : 반복이 가능한지 여부
  • configurable : 프로퍼티 삭제, 플래그 수정 여부

플래그 여부 조회
Object.getOwnPropertyDescriptor(obj, propertyName)
일반적으로 만든 객체의 프로퍼티는 플래그가 전부 true로 되어 있습니다.

let user = {
  name: "John"
};

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
/*
{value: "John", writable: true, enumerable: true, configurable: true}
configurable: true
enumerable: true
value: "John"
writable: true
__proto__: Object
*/

프로퍼티 만들기
Object.defineProperty(obj, propertyName, descriptor)
defineProperty를 이용하여 프로퍼티를 만들때 descriptor에 flag값을 명시하지 않으면 플래그 값이 기본으로 다 false입니다.

let user = {};

Object.defineProperty(user, "name", {
  value: "John"
});

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
  "value": "John",
  "writable": false,
  "enumerable": false,
  "configurable": false
}
 */

여러 개 한 번에 만들 때는 아래 메서드로 합니다.
Object.defineProperties(obj, descriptors)

descriptors = {
prop1: descriptor1,
prop2: descriptor2,
//...
}

Object.defineProperties(user, {
  name: { value: "John", writable: false },
  surname: { value: "Smith", writable: false },
  // ...
});

프로퍼티 설명자 전체 가져오기
Object.getOwnPropertyDescriptors(obj)
Object.defineProperties와 함께 사용하면 객체 복사 시 플래그도 함께 복사합니다.
getOwnPropertyDescriptors는 심볼형 프로퍼티를 포함한 프로퍼티 설명자 전체를 반환합니다.

let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));

객체의 수정을 막아주는 메서드

프로퍼티 하나가 대상이 아닌 프로퍼티 전체, 객체의 수정을 조절해봅시다.

Object.preventExtensions(obj) 객체에 새로운 프로퍼티를 추가할 수 없습니다.
Object.seal(obj) configurable: false와 동일
Object.freeze(obj) configurable: false, writable: false와 동일. nested Object는 여전히 풀림.
Object.defineProperty(obj) == Object.freeze
Object.isExtensible(obj) 프로퍼티 추가 가능 여부
Object.isSealed(obj) 프로퍼티 추가, 삭제 가능여부
Object.isFrozen(obj) 프로퍼티 추가, 삭제, 변경 가능여부

접근자 프로퍼티

위에는 전부 데이터 프로퍼티이고, 접근자 프로퍼티가 따로 존재합니다.
접근자 프로퍼티는 본질은 함수지만 프로퍼티처럼 보입니다.

getter
값을 읽어올 떄 사용. 프로퍼티처럼 보이지만 메서드이기 때문에 obj.property로 읽어올 수 없습니다.

let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

alert(user.fullName); // John Smith
user.fullName = "Test"; // Error (프로퍼티에 getter 메서드만 있어서 에러가 발생합니다.)

setter
값을 수정할 때 사용. getter와 setter를 구현하면 객체에 가상의 프로퍼티가 생깁니다. 읽고 쓰는 게 가능하지만 실제로 존재하진 않습니다.

let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  },

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  }
};

// 주어진 값을 사용해 set fullName이 실행됩니다.
user.fullName = "Alice Cooper";

alert(user.name); // Alice
alert(user.surname); // Cooper

접근자 프로퍼티 설명자
접근자 프로퍼티의 설명자는 get, set, enumerable, configurable이 들어갑니다.
프로퍼티가 접근자와 데이터 프로퍼티 두 군데에 동시에 속할 수는 없습니다.

let user = {
  name: "John",
  surname: "Smith"
};

Object.defineProperty(user, 'fullName', {
  get() {
    return `${this.name} ${this.surname}`;
  },

  set(value) {
    [this.name, this.surname] = value.split(" ");
  }
});

alert(user.fullName); // John Smith

for(let key in user) alert(key); // name, surname

활용
private 변수화 하거나 호환성을 위해 기존 값을 바꿀 때 사용합니다.

메소드

값이 아닌 어떤 행동을 객체에 저장하고자 할 때 사용됩니다.
형태적으로는 객체 프로퍼티에 할당된 함수를 가리킵니다.

let user = {
  name: "John",
  age: 30
};

user.sayHi = function() {
  alert("안녕하세요!");
};

user.sayHi(); // 안녕하세요!

concised method
메소드를 단축하여 쓸 수 있습니다. 아래 두 obj는 같습니다.
(1) 형태변경
(2) 생성자 함수로 사용될 수 없습니다.
console.dir(obj.getName)을 해보면 이전 메소드들과 달리 prototype 속성이 없습니다.
원래 메소드가 생성자 함수로 사용되면 안되므로 올바른 에러처리와 함께 메소드가 더 가벼워진 효과가 있습니다.
(3) this.__proto__.메소드명() === super.메소드명(). 상위 클래스를 지칭할 말이 생긴 것입니다.

// (1)
var obj = {
  name: 'foo',
  getName: function () { return this.name }
}
const obj = {
  name: 'foo',
  getName () { return this.name }
}

// (2)
const Person = {
  greeting () { return 'hello' }
}
const p = new Person.greeting() // error

// (3)
const Person = {
  greeting () { return 'hello' }
}
const friend = {
  greeting () {
    return 'hi, ' + super.greeting()
    // == return 'hi, ' + this.__proto__greeting()
  }
}
Object.setPrototypeOf(friend, Person)
friend.greeting()

this

메서드 내부에서 this를 사용하면 this는 해당 객체를 가리킵니다. 이러면 객체 내부에서 객체의 프로퍼티나 메소드 등을 가져와 사용할 수 있습니다.

let user = {
  name: "John",
  age: 30,

  sayHi() {
    // 'this'는 '현재 객체'를 나타냅니다.
    alert(this.name);
  }

};

user.sayHi(); // John

this.name 대신 user.name을 써도 동일하게 동작하지만, 외부의 side effect에 의해 user의 값이 변해버리면 엉뚱한 값을 가져오는 결과를 가져올 수 있습니다.

객체의 종류

배열객체

키 값에 인덱스가 들어있는 객체. 배열 메소드를 사용할 수 있고, 이터러블하여 반복이 가능합니다.

iterable object

이터러블 객체는 배열을 일반화한 객체로 반복이 가능하다는 뜻이 됩니다.
원래 객체는 반복이 안되는 단점이 있었는데, Symbol.iterator가 구현되면 for..of 반복문을 적용할 수 있습니다.

유사배열

인덱스와 length 프로퍼티가 있어 배열처럼 보이는 객체. 배열 메서드를 사용할 수 없고 이터러블 객체일 수는 있습니다.

Map, Set, WeakMap, WeakSet

참조형 데이터.

전역객체

자바스크립트가 동작하는 호스팅 환경 전역에 접근 가능한 객체입니다. 요즘 표준으로 globalThis라는 이름을 쓰도록 하지만 전통적으로 브라우저에서는 window, node에서는 global로 불립니다. 전역객체에는 주로 내장 객체와 브라우저 환경 전용 변수들이 담겨 있습니다. 우리가 작업하는 코드에서는 새로운 전역객체를 최대한 만들지 않는 것이 좋습니다. 지역변수와 겹치는 문제를 해결하기 위해 사용할 때는 명시적으로 전역객체명.프로퍼티네임으로 사용합니다.

객체 활용방법들

객체에 해당 키가 존재하는 지 확인하기

객체의 없는 프로퍼티를 접근하려하면 에러가 나지 않고 undefined를 반환하는 점을 활용합니다.
1번 방식은 실제 값이 undefined일 경우, 의도한대로 동작하지 않으므로 유의하셔야 합니다.
3번 방식은 유효하게 사용되지만 상속받은 키 값은 인식하지 못합니다.

/* (1) */
let user = {};
alert( user.noSuchProperty === undefined ); // true

/* (2) */
let user = { name: "John", age: 30 };

alert( "age" in user ); // user.age가 존재하므로 true가 출력됩니다.
alert( "blabla" in user ); // user.blabla는 존재하지 않기 때문에 false가 출력됩니다.

/* (3) */
let user = { name: "John", age: 30 };
let inheritance = Object.create(user);
alert( user.hasOwnProperty('name') ) // true
alert( inheritance.hasOwnProperty('name') ) // false

객체 순회하기

객체를 반복문에 돌리는 등의 열거를 하게되면 객체의 키가 일련의 순서로 정렬됩니다.
양의 정수형 숫자 오름차순 정렬 + 문자형 키 작성한 순서대로 + 심볼 작성한 순서대로.
기본적으로 심볼은 열거대상에서 제외됩니다. 심볼까지 열거대상에 넣으려면 Reflect.ownKeys(obj).

키정렬은 es6+에서 생긴 것이기 때문에 순서를 지키는 메소드와 아닌 메소드가 구분됩니다.
(1) 순서보장 X

  • for in
  • Object.keys()
  • JSON.stringify()

(2) 순서보장 O

  • Object.getOwnPropertyNames()
  • Reflect.ownKeys()
  • Object.assign()

for...in문

let user = {
  name: "John",
  age: 30,
  isAdmin: true
};

for (let key in user) {
  // 키
  alert( key );  // name, age, isAdmin
  // 키에 해당하는 값
  alert( user[key] ); // John, 30, true
}

순회 메서드

Object.keys(obj) 키가 담긴 배열을 반환
Object.values(obj) 값이 담긴 배열을 반환
Object.entries(obj) [key, value]쌍이 담긴 배열을 반환 <-> Object.fromEntries(arr) 배열을 객체로 변환

이들은 Map, Set, Array에 있는 keys(), values(), entries()와 크게 2가지가 다릅니다.
객체는 진짜 배열을 반환하고, 나머지는 iterable 객체를 반환합니다.

빈 객체인지 확인하기

const isEmpty = (obj) => {
  for(let key in obj) {
    return false;
  }
  return true;
}

객체 복제

객체는 원래 참조에 의해 할당되고 복사됩니다. 그래서 객체와 똑같으면서 독립적인 객체를 만들고 싶다면 다른 방법을 써야합니다.

얕은 복사 반복문

let user = {
  name: "John",
  age: 30
};

let clone = {}; // 새로운 빈 객체

// 빈 객체에 user 프로퍼티 전부를 복사해 넣습니다.
for (let key in user) {
  clone[key] = user[key];
}

// 이제 clone은 완전히 독립적인 복제본이 되었습니다.
clone.name = "Pete"; // clone의 데이터를 변경합니다.

alert( user.name ); // 기존 객체에는 여전히 John이 있습니다.

얕은 복사 Object.assign

Object.assign은 열거할 수 있는 출처 객체의 속성만 대상 객체로 복사합니다.

구문
Object.assign(target, ...sources) // return target object
target 뒤에 오는 obj들의 모든 property를 복사합니다. property name이 겹치면 뒤에 것이 우선합니다.

let user = {
  name: "John",
  age: 30
};

let clone = Object.assign({}, user);

얕은 복사 spread operator

function cloneObj(obj) {
	return { ...obj }
}

깊은 복사 Json 메소드

스트링으로 바꿨다가 다시 오브젝트로 변환하기 때문에 이전 객체와의 연결 고리가 끊어지지만 JSON 메소드가 성능면에서 매우 느립니다.

function cloneObject(obj) {
  return JSON.parse(JSON.stringify(obj));
}

깊은 복사 재귀용법

function cloneObject(obj) {
  if (obj === null || typeof(obj) !== 'object')
  return obj;
  
  let clone = {};
  for(const i in obj) {
    if(typeof(obj[i])=="object" && obj[i] != null) {
    // 혹은 if(obj.hasOwnProperty(i))
      clone[i] = cloneObject(obj[i]);
    } else {
      clone[i] = obj[i];
    }
  }
  return clone;
}

객체의 형변환

객체는 숫자형이나 문자형으로만 형변환이 일어납니다.

숫자형변환

객체끼리 뺄셈을 하거나 수하관련 함수를 적용할 때. ex) Date 객체

문자형변환

대체로 객체를 출력하려고 할 때.

변수의 유효범위

this.멤버변수명은 블록 스코프를 가지지 않고, 자신의 this 계산법에 의해서만 값이 바뀝니다.

profile
정리하는 개발자

0개의 댓글