JavaScript로 객체 다루기 포스팅입니다. 처음 JSON과 객체를 제대로 이해하지 않고 개발을 하다 보니 객체로 데이터를 편리하게 사용할 수 있는 방법들을 모르는 경우가 있었습니다. 생각난 김에 한 번 찬찬히 살펴보고 정리했습니다.
1. Object (객체) 개요
JavaScript는 간단한 객체 기반 패러다임 위에 만들어졌습니다. 객체는 속성(attribute)의 컬렉션(collection)이고, 속성은 이름(name. 키(key))과 값(value) 사이의 연결 관계입니다. 속성의 값이 함수(function)인 경우에는 메서드(method)라고 부릅니다.
객체는 현실에서 인식 가능한 사물로 이해할 수 있습니다. 객체는 속성과 타입을 가진 독립적인 개체(entity)입니다. 컵을 생각해 보면, 컵은 색, 디자인, 무게, 소재 등의 속성을 가진 객체라고 할 수 있습니다.
1.1 객체와 속성
객체는 자신과 연관된 속성을 가집니다. 객체의 속성은 객체에 붙은 변수라고 할 수 있습니다. 객체의 속성은 일반적인 JavaScript 변수와 동일한데, 다만 객체에 붙어있다는 점만 다릅니다. 속성에 접근할 땐 간단한 마침표 표기법을 사용합니다. 객체의 이름(변수)과 속성의 이름 모두 대소문자를 구별합니다.
objectName.propertyName;
객체는 예약어 new와 Object클래스의 생성자인 Object()로 생성할 수 있습니다. 속성은 객체와 마침표 표기법을 사용하여 값을 대입하여 추가할 수 있습니다.
const myCar = new Object(); // 기본 객체 생성
myCar.make = "BMW"; // make 속성 추가
myCar.model = "i8"; // model 속성 추가
myCar.year = 2014; // year 속성 추가
다른 방법으로 literal표기법(initializer 표기법)을 사용해 생성할 수 있습니다. 중괄호( { } ) 안에 쉼표( , )로 구분한 속성의 이름과 값의 목록으로 표시합니다.
const myCar = {
make: "bmw", // 속성(이름:값)의 목록은 쉼표로 구분
model: "i8",
year: 2015, // 마지막 속성 다음에도 쉼표(,)를 표기
};
객체에 할당되지 않은 속성은 undefined입니다. (null이 아닙니다)
myCar.color; // undefined
객체 속성 이름은 JavaScript 문자열 혹은 문자열로 변환 가능한 것이면 모두 가능하며, 빈 문자열도 가능합니다. 그러나 유효한 JavaScript 식별자가 아닌 이름(공백이나 붙임표, 숫자로 시작하는 이름)을 가진 속성은 대괄호 표기법으로만 접근할 수 있습니다. 대괄호 표기법은 속성 이름이 동적으로 정해지는 경우, 즉 런타임 시점까지 알 수 없는 경우 유용합니다.
대괄호 표기법 안의 키는 심벌이 아닌 경우 문자열로 변환됩니다.
// 네 개의 변수를 쉼표로 구분해서
// 한 번에 생성하고 할당
const myObj = new Object(),
str = "myString",
rand = Math.random(),
obj = new Object();
myObj.type = "마침표 구문";
myObj["date created"] = "공백을 포함한 문자열";
myObj[str] = "문자열 값";
myObj[rand] = "무작위 수";
myObj[obj] = "객체";
myObj[""] = "빈 문자열까지";
console.log(myObj);
// 객체를 조회할 경우 속성의 조회 순서는
// 속성을 추가한 순서와 달질 수 있습니다.
속성 접근은 변수에 저장된 문자열 값으로도 가능합니다.
let propertyName = "make";
myCar[propertyName] = "BMW";
1.2 객체 속성 추가하기
객체에 속성을 추가하는 방법은 객체 접근 방법과 동일합니다. 다만 새로운(추가하려는) 속성 명으로 접근하면 됩니다.
const obj = {
prop_1: "prop_1",
};
console.log(obj);
console.log(obj.prop_1, obj.prop_2);
obj.prop_2 = "prop_2"; // . 연산자
console.log(obj.prop_1, obj.prop_2);
obj["prop_3"] = "prop_3"; // 대괄호 표기법
console.log(obj);
만약 이전에 정의된 객체 타입, 즉 기본 객체에 속성을 추가하고 싶다면 prototype 속성을 사용하면 됩니다.
function Person(name, age) {
this.name = name;
this.age = age;
}
const emma = new Person("Emma", 24);
console.log(emma, emma.sex);
// 기반 객체에 sex 속성을 추가하고 기본값을 설정.
Person.prototype.sex = "M";
console.log(emma, emma.sex);
// emma 객체에 sex 속성을 추가.
emma.sex = "F";
console.log(emma, emma.sex);
1.3 객체 속성 삭제하기
상속한 속성이 아닌 경우 delete 연산자로 속성을 삭제할 수 있습니다.
// 빈 객체를 생성
const obj = new Object();
// 객체에 속성 a, b를 추가
obj.a = 5;
obj.b = 10;
console.log(obj, "a" in obj);
// 객체의 속성 a를 삭제
delete obj.a;
console.log(obj, "a" in obj);
1.4 객체 속성 인덱싱
객체의 속성에 접근하려면 속성 이름이나 인덱스(숫자)로 접근할 수 있습니다. 처음에 속성을 이름으로 정의했으면 그 속성은 항상 이름으로만 참조해야 하고, 처음에 인덱스로 정의했으면 항상 인덱스로만 참조해야 합니다. 속성명이 숫자(인덱스)인 경우에는 대괄호 표기법만 사용가능합니다.
const obj = {};
obj.name = "obj";
obj["first"] = "first";
obj[1] = "value_1";
obj[2] = 2;
console.log(obj);
console.log(obj.name);
console.log(obj.first, obj["first"]);
console.log(obj[1], obj[2]);
// console.log(obj.1, obj.2); // SyntaxError
다만 HTML 요소를 나타내는, document.forms와 같은 유사 배열 객체에는 예외로, 문서상의 순서를 나타내는 인덱스로 접근할 수도 있고 (정의된 경우) 이름으로 접근할 수도 있습니다.
<!-- HTML -->
<form name="myForm">
...
</form>
// JavaScript에서 아래의 방식으로 조회할 수 있다.
document.forms[1]
document.forms['myForm']
document.forms.myForm
1.5 객체 속성 나열하기
객체 속성을 나열하거나 순회하는 방법에는 3가지 내장된 방법이 있습니다.
- for..in 반복문
- 객체와 객체 프로토타입 체인에 존재하는 모든 열거 가능한 속성을 순회합니다. - object.keys(obj)
- 객체 자신만의(프로토타입 체인을 제외한) 열거 가능한 속성 이름("키")을 배열로 반환합니다. - Object.getOwnPropertyNames(obj)
- obj 객체 자신만의 모든(열거 불가능하더라도) 속성 이름("키")을 배열로 반환합니다.
대괄호 표기법은 객체의 열거 가능한 속성을 순회할 때도 사용할 수 있습니다.
function showProps(obj, objName) {
let result = "";
for (let i in obj) {
// obj.hasOwnProperty()
// - 객체의 프로토타입 체인에 존재하지 않는 속성을 제외
if (obj.hasOwnProperty(i)) {
result += `${objName}.${i} = ${obj[i]}\n`;
//대괄호 표기법으로 속성 값 조회
}
}
return result;
}
const myCar = {
make: "BMW",
model: "i8",
year: 2014,
}
console.log(showProps(myCar, "myCar"));
2. Object (객체) 생성하기
JavaScript에는 미리 정의된 객체, 내장 객체가 여럿 존재합니다. 여기에 더해 문제 해결을 위한 객체도 생성할 수 있습니다. 객체 생성은 객체 초기자(Object Initializer, 리터럴(Literal) 표기법)를 사용할 수도 있고, 생성자 함수(constructor)를 정의한 후에 new 연산자와 함께 호출해서 객체 인스턴스를 생성할 수도 있습니다.
2.1 Object Initializer (객체 초기자)
객체 초기자(Object Initializer)의 사용은 종종 "리터럴 표기에 의한 객체 생성"이라고 불립니다. 이를 사요한 객체 생성 구문은 다음과 같습니다.
const obj = {
prop_1: value_1, // 속성의 값은 식별자일 수도 있고
2: value_2, // 숫자일 수도 있고
// ...,
"prop n": value_n, // (공백을 포함한)문자열일 수도 있음.
};
obj는 새로운 객체를 할당하는 변수 이름이고, 콜론( : ) 앞에 위치한 속성 이름은 식별자(이름, 숫자, 스트링 리터럴), 각각의 value_i는 속성 이름에 할당할 표현식입니다.
객체 초기자는 표현식이며 자신이 속한 선언문이 실행될 때마다 새로운 객체를 생성합니다. 같은 초기자가 생성한 객체끼리라도 구별 가능하며 서로 비교했을 때 false를 반환합니다. 초기자가 생성하는 객체는 마치 new Object() 호출을 한 것과 같이 생성됩니다. 즉 객체 초기자 표현식의 결과 객체들은 Object의 인스턴스입니다.
let x, y;
const cond = true;
if (cond) {
x = { greeting: "안녕하세요" };
y = { greeting: "안녕하세요" };
}
console.log(x);
console.log(y);
console.log(x === y);
x, y 객체 모두 Prototype이 Object임을 확인할 수 있고, 두 객체가 같은지 비교하면 결과는 false를 반환한다.
2.2 Constructor (생성자 함수)
다음의 두 단계를 통해 객체를 생성할 수 있습니다.
- 생성자 함수를 작성해 객체 타입을 정의합니다.
객체 타입 이름의 첫 글자는 대문자로 시작하는 관례가 있습니다. - new 연산자를 사용해 객체 인스턴스를 생성합니다.
객체 타입을 정의하려면 타입 이름, 속성, 메서드를 지정하는 함수를 생성합니다. 함수에서 전달한 값을 객체 속성으로 할당하기 위해 this를 사용합니다.
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
const john = new Person("John", 20, "M");
const emma = new Person("Emma", 24, "F");
console.log(john);
console.log(emma);
console.log(john.age, emma.age); // . 연산자로 객체의 속성에 접근 가능.
2.3 Object.create 메서드
Object.create() 메서드로 객체를 생성할 수도 있습니다. 이 메서드는 생성자 함수 정의 없이도 생성할 객체의 프로토타입을 지정할 수 있습니다.
2.3.1 리터럴을 사용한 객체
const Pet = {
type: "펫", // 속성 기본 값
getType: function () {
return this.type;
}
}
const pet = Object.create(Pet);
console.log(pet.getType()); // "펫"
const dog = Object.create(Pet);
dog.type = "개";
console.log(dog, dog.getType()); // "개"
console.log(Pet.name, dog.constructor.name);
// 리터럴은 name 속성이 없음
// dog 객체의 생성자는 Object()
리터럴 방식으로 생성한 객체는 생성자가 없는 객체입니다. 따라서 기본 Object 객체의 생성자인 Object를 name으로 반환합니다.
2.3.2 생성자 함수를 사용한 객체
function Animal(type = "척추동물") {
this.type = type;
this.getType = function () {
return this.type;
}
}
// 생성자 함수로 객체 생성
const animal1 = new Animal();
console.log("animal1: ", animal1);
// 생성자 함수로 생성한 객체를 기반으로 Object.create() 메서드를 사용
const animal2 = Object.create(animal1);
console.log("animal2: ", animal2);
const tiger = Object.create(animal1);
// 처음 생성한 객체는 자신의 속성이 없음
// 기반 객체의 값을 반환하게 되므로 기본값인 '척추동물'을 반환
console.log(tiger, tiger.getType()); // {} '척추동물'
tiger.type = "호랑이"; // 객체에 속성을 추가
// 추가한 객체의 속성값을 반환하게 되므로 '호랑이'를 반환
console.log(tiger, tiger.getType()); // {type: '호랑이'} '호랑이'
// 생성자 함수 기반이므로,
// 함수의 이름, 객체의 생성자 이름으로 Animal을 반환
console.log(Animal.name, tiger.constructor.name);
생성자 함수를 사용한 객체는 조금 다르게 동작합니다. 생성자 함수로 만든 객체를 기반으로 Object.create() 메서드로 객체를 생성하면 객체는 생성자를 가지게 됩니다. 따라서 생성자 함수의 이름은 Animal, 생성한 객체의 생성자 이름도 Animal이 됩니다.
3. object (객체)와 method (메서드)
3.1 method 정의
method(메서드)는 객체와 연관된 함수, 다른 말로 객체 속성 중 함수인 것을 말합니다. 메서드는 다른 함수와 똑같이 정의하지만, 객체 속성에 할당한다는 점이 다릅니다.
objectName.methodName = functionName;
const myObj = {
myMethod: function(params) {
// some codes...
}
// 속성명 생략 가능
myOtherMethod(params) {
// some codes...
}
}
// 호출 방법
object.methodName(params);
3.1.1 리터럴 방식
객체를 정의할 때 속성에 함수를 사용할 수 있습니다. 함수명을 속성명으로 사용할 수 있으므로 속성명 없이 작성할 수도 있습니다.
const myCar = {
make: "BMW",
model: "i8",
displayCar: function () { // 속성명과 익명 함수로 작성
console.log(`${this.make} : ${this.model}`);
}
};
const yourCar = {
make: "Audi",
model: "R8",
displayCar() { // 함수명을 속성명으로 사용함.
console.log(`${this.make} : ${this.model}`);
}
};
console.log(myCar, yourCar);
console.log(myCar.displayCar === yourCar.displayCar);
별도로 함수를 정의하고 속성에 함수를 할당하는 방법도 있습니다.
function displayCar() {
console.log(`${this.make} : ${this.model}`);
}
const myCar = {
make: "BMW",
model: "i8",
displayCar: displayCar, // 속성명에 함수명을 할당
}
const yourCar = {
make: "Audi",
model: "R8",
displayCar, // 위 처럼 같은 경우에는 하나만 적을 수 있음.
};
console.log(myCar, yourCar);
console.log(myCar.displayCar === yourCar.displayCar);
3.1.2 생성자 방식
객체 생성자 함수에도 메서드 정의를 포함할 수 있습니다.
function Car(make, model) { // 생성자 함수
this.make = make;
this.model = model;
this.displayCar = function () { // 메서드 정의
console.log(`${this.make} : ${this.model}`);
};
}
const myCar = new Car("BMW", "i8"); // 객체 생성
const yourCar = new Car("Audi", "R8"); // 객체 생성
console.log(myCar, yourCar);
myCar.displayCar(); // displayCar 메서드 호출
yourCar.displayCar(); // displayCar 메서드 호출
리터럴 방식과 마찬가지로, 별도의 함수를 정의하고 함수명을 생성자 함수의 변수에 할당하여 사용할 수도 있습니다.
function displayCar() { // 함수 정의
console.log(`${this.make} : ${this.model}`);
}
function Car(make, model) { // 생성자 함수
this.make = make;
this.model = model;
this.displayCar = displayCar; // 함수(함수명)를 할당하여 메서드 추가
}
const myCar = new Car("BMW", "i8"); // 객체 생성
const yourCar = new Car("Audi", "R8"); // 객체 생성
console.log(myCar, yourCar);
myCar.displayCar(); // displayCar 메서드 호출
yourCar.displayCar(); // displayCar 메서드 호출
3.2 Getter (접근자), Setter (설정자)
getter(접근자)는 특정 속성의 값을 반환하는 메서드입니다. setter(설정자)는 특정 속성의 값을 설정하는 메서드입니다. 객체 초기자(리터럴 표기법)를 사용해 정의하거나, 나중에 필요한 시점에 아무 객체에나 접근자/설정자를 추가 메서드로 추가할 수 있습니다.
3.2.1 리터럴 방식
getter는 메서드 이름 앞에 get, setter는 메서드 이름 앞에 set을 붙이기만 하면 됩니다. 제약 사항은 모든 getter 메서드는 아무 매개변수도 받지 않고, 모든 setter 메서드는 하나의 매개변수(설정할 값)만 받는다는 제약이 있습니다.
const person1 = {
firstname: "John",
lastname: "Doe",
get fullname() { // getter
return `${this.firstname} ${this.lastname}`;
},
set fullname(name) { // setter
// 입력받은 이름 문자열을 공백을 기준으로 배열로 변환
const arr = name.split(" ");
console.log(arr);
this.firstname = arr[0]; // 배열의 첫 번째 값
this.lastname = arr[1]; // 배열의 두 번째 값
},
}
// getter, setter는 속성 처럼 접근하여 사용
console.log(person1);
console.log(person1.fullname); // getter로 값을 조회
person1.fullname = "Jane Foster"; // setter로 값을 설정
console.log(person1.fullname);
객체를 먼저 생성하고 나중에 getter, setter를 추가하려면 Object.defineProperties() 메서드를 사용합니다. 이 메서드의 첫 번째 매개변수는 getter, setter를 추가할 객체고, 두 번째 매개변수는 속성 이름이 추가할 getter/setter 이름, 속성 값은 getter/setter를 정의하는 객체입니다.
// 객체 정의
const person1 = {
firstname: "John",
lastname: "Doe",
}
console.log(person1);
// getter, setter 추가
Object.defineProperties(
person1, // getter, setter를 추가할 객체
{ // getter, setter 속성과 함수를 정의한 객체
fullname: { // getter
get: function () {
return `${this.firstname} ${this.lastname}`;
}
},
// 리터럴 처럼 같은 속성명을 지정할 수 없음
// 객체에서 속성명이 같은 경우 뒤의 속성값 하나만 남게됨.
nameis: { // setter
set: function (name) {
const arr = name.split(" ");
console.log(arr);
this.firstname = arr[0];
this.lastname = arr[1];
}
},
},
);
console.log(person1);
console.log(person1.fullname);
person1.fullname = "Jane Foster";
console.log(person1.fullname);
주목할 점은 getter, setter를 추후에 추가할 경우에는 같은 속성명을 사용할 수 없습니다. 이는 객체를 넘길 때 객체 안에 같은 속성명이 두 개 이상 정의되어 있으면 가장 마지막의 속성값으로 결정되고 나머지 속성값은 무시됩니다.
4. object (객체) 비교
JavaScript의 객체는 참조(reference) 타입입니다. 두 개의 객체는 서로 같은 속성을 갖더라도 다른 객체로 인식하여 false를 반환하고, 오직 객체를 참조하는 경우만 true를 반환합니다.
// 두 개의 변수, 두 개의 같은 속성을 가진 서로 다른 객체
const fruit = { name: "사과" };
const fruitbear = { name: "사과" };
console.log(fruit);
console.log(fruitbear);
console.log(fruit == fruitbear); // false
console.log(fruit === fruitbear); // false
위 경위 두 객체의 속성명과 값은 같지만 각각 메모리를 차지하고 있는 것을 확인할 수 있습니다. 따라서 두 객체를 비교하면 서로 다른 객체로 인식합니다.
// 두 개의 변수, 하나의 객체
const fruit = { name: "바나나" };
const fruitbear = fruit; // fruit 객체 참조를 fruitbear에 할당
// fruit과 fruitbear가 같은 객체를 가리킴
console.log(fruit);
console.log(fruitbear);
console.log(fruit == fruitbear); // true
console.log(fruit === fruitbear); // true
fruit.name = "오렌지";
console.log(fruitbear); // 출력: { name: "바나나" }가 아니라 { name: "오렌지" }
객체를 참조할 경우 메모리에는 1개의 객체만 존재한다. 따라서 객체와 객체를 참조하는 변수를 비교할 때도 같은 것으로 인식하여 true를 반환한다.
참고문헌
https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Working_with_objects
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/name
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Object_initializer
댓글