객체 다루기
객체 다루기 (Working with objects)
JavaScript는 객체 기반 패러다임(object-based paradigm)으로 설계되었다. **객체(object)**는 속성들의 집합이고, **속성(property)**은 이름(또는 키)과 값 사이의 연관이다. 속성의 값은 함수일 수 있으며, 이 경우 그 속성을 **메서드(method)**라고 한다.
많은 다른 프로그래밍 언어에서처럼 JavaScript의 객체도 실생활의 객체에 비유할 수 있다. JavaScript에서 객체는 속성과 타입을 가진 독립적인 개체다. 예를 들어 컵과 비교해 보자. 컵은 속성을 가진 객체다. 컵에는 색, 디자인, 무게, 만들어지는 재료 등이 있다. 같은 방식으로 JavaScript 객체도 그 특성을 정의하는 속성을 가질 수 있다.
브라우저에 미리 정의된 객체 외에도 자신만의 객체를 정의할 수 있다. 이 장은 객체·속성·메서드를 사용하는 방법과 자신만의 객체를 만드는 방법을 설명한다.
본문
새 객체 만들기 (Creating new objects)
**객체 초기화자(object initializer)**를 사용해 객체를 만들 수 있다. 또는 먼저 **생성자 함수(constructor function)**를 만든 다음 new 연산자로 그 함수를 호출해 객체를 인스턴스화할 수도 있다.
객체 초기화자 사용하기 (Using object initializers)
객체 초기화자는 **객체 리터럴(object literal)**이라고도 한다. "객체 초기화자"는 C++에서 사용하는 용어와 일치한다.
객체 초기화자를 사용한 객체의 문법은 다음과 같다.
const obj = {
property1: value1, // 속성 이름은 식별자일 수 있음
2: value2, // 또는 숫자
"property n": value3, // 또는 문자열
};
콜론 앞의 각 속성 이름은 식별자, 숫자 리터럴, 또는 문자열 리터럴이며, 각 valueN은 속성 이름에 할당되는 값인 표현식이다. 속성 이름은 표현식일 수도 있으며, 계산된 키(computed key)는 대괄호로 감싸야 한다. 객체 초기화자 참조는 문법을 더 자세히 설명한다.
이 예에서 새로 생성된 객체는 변수 obj에 할당된다 — 이것은 선택 사항이다. 이 객체를 다른 곳에서 참조할 필요가 없으면 변수에 할당할 필요가 없다. (문장이 기대되는 곳에 객체 리터럴이 나타나면, 리터럴이 블록 문장과 혼동되지 않도록 객체 리터럴을 괄호로 감싸야 할 수도 있다.)
객체 초기화자는 표현식이며, 각 객체 초기화자는 그것이 나타나는 문장이 실행될 때마다 새 객체가 생성되게 한다. 동일한 객체 초기화자들도 서로 같지 않은 별개의 객체를 만든다.
다음 문장은 표현식 cond가 참일 때에만 객체를 만들고 변수 x에 할당한다.
let x;
if (cond) {
x = { greeting: "hi there" };
}
다음 예는 세 속성을 가진 myHonda를 만든다. engine 속성도 자신의 속성을 가진 객체라는 점에 주목하라.
const myHonda = {
color: "red",
wheels: 4,
engine: { cylinders: 4, size: 2.2 },
};
초기화자로 만들어진 객체를 **플레인 객체(plain objects)**라고 한다. 그것들이 Object의 인스턴스이지만 그 밖의 다른 객체 타입은 아니기 때문이다. 일부 객체 타입은 특별한 초기화자 문법을 가진다 — 예를 들어 배열 초기화자와 정규식 리터럴.
생성자 함수 사용하기 (Using a constructor function)
대안으로 다음 두 단계로 객체를 만들 수 있다.
- 생성자 함수를 작성해 객체 타입을 정의한다. (타당한 이유로) 첫 글자를 대문자로 쓰는 강한 관례가 있다.
new로 객체의 인스턴스를 만든다.
객체 타입을 정의하려면 그 이름·속성·메서드를 지정하는 함수를 만든다. 예를 들어 자동차용 객체 타입을 만들고 싶다고 하자. 이 타입의 객체를 Car라고 부르고, make, model, year 속성을 갖게 하고 싶다. 그러려면 다음 함수를 작성한다.
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
함수에 전달된 값을 기반으로 객체의 속성에 값을 할당하기 위해 this를 사용하는 것에 주목하라.
이제 다음과 같이 myCar라는 객체를 만들 수 있다.
const myCar = new Car("Eagle", "Talon TSi", 1993);
이 문장은 myCar를 만들고 그 속성에 지정된 값을 할당한다. 그러면 myCar.make의 값은 문자열 "Eagle", myCar.model은 "Talon TSi", myCar.year는 정수 1993 등이 된다. 인수와 매개변수의 순서는 같아야 한다.
new를 호출해 원하는 만큼 많은 Car 객체를 만들 수 있다. 예:
const randCar = new Car("Nissan", "300ZX", 1992);
const kenCar = new Car("Mazda", "Miata", 1990);
객체는 그 자체가 또 다른 객체인 속성을 가질 수 있다. 예를 들어 다음과 같이 Person이라는 객체를 정의한다고 하자.
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
그리고 두 개의 새 Person 객체를 인스턴스화한다.
const rand = new Person("Rand McKinnon", 33, "M");
const ken = new Person("Ken Jones", 39, "M");
그러면 Person 객체를 받는 owner 속성을 포함하도록 Car의 정의를 다시 쓸 수 있다.
function Car(make, model, year, owner) {
this.make = make;
this.model = model;
this.year = year;
this.owner = owner;
}
새 객체를 인스턴스화하려면 다음을 사용한다.
const car1 = new Car("Eagle", "Talon TSi", 1993, rand);
const car2 = new Car("Nissan", "300ZX", 1992, ken);
새 객체를 만들 때 리터럴 문자열이나 정수 값을 전달하는 대신, 위 문장은 객체 rand와 ken을 owner의 인수로 전달한다는 점에 주목하라. 그러면 car2의 소유자 이름을 알아내고 싶다면 다음 속성에 접근할 수 있다.
car2.owner.name;
이전에 정의된 객체에 언제나 속성을 추가할 수 있다. 예를 들어 문장:
car1.color = "black";
은 car1에 color 속성을 추가하고 그 값 "black"을 할당한다. 그러나 이것은 다른 객체에는 영향을 주지 않는다. 같은 타입의 모든 객체에 새 속성을 추가하려면 Car 객체 타입의 정의에 속성을 추가해야 한다.
생성자 함수를 정의할 때 함수 문법 대신 class 문법을 사용할 수도 있다. 자세한 내용은 class 가이드를 보라.
Object.create() 메서드 사용하기 (Using the Object.create() method)
객체는 Object.create() 메서드로도 만들 수 있다. 이 메서드는 만들고 싶은 객체의 프로토타입 객체를, 생성자 함수를 정의하지 않고 선택할 수 있게 해주므로 매우 유용할 수 있다.
// Animal 속성과 메서드 캡슐화
const animalProto = {
type: "Invertebrates", // 속성의 기본값
displayType() {
// 동물의 타입을 표시할 메서드
console.log(this.type);
},
};
// `animal`이라는 새 동물 타입 만들기
const animal = Object.create(animalProto);
animal.displayType(); // 기록: Invertebrates
// fish라는 새 동물 타입 만들기
const fish = Object.create(animalProto);
fish.type = "Fishes";
fish.displayType(); // 기록: Fishes
객체와 속성 (Objects and properties)
JavaScript 객체는 관련된 속성을 가진다. 객체 속성은 기본적으로 변수와 같지만, 범위가 아니라 객체와 연관된다는 점이 다르다. 객체의 속성은 객체의 특성을 정의한다.
예를 들어 이 예는 make, model, year라는 이름의 속성과 그 값이 각각 "Ford", "Mustang", 1969로 설정된 myCar라는 객체를 만든다.
const myCar = {
make: "Ford",
model: "Mustang",
year: 1969,
};
JavaScript 변수처럼 속성 이름은 대소문자를 구분한다. 속성 이름은 문자열 또는 Symbol만 될 수 있다 — 모든 키는 Symbol이 아니면 문자열로 변환된다. 배열 인덱스는 사실 정수를 포함하는 문자열 키를 가진 속성이다.
속성 접근하기 (Accessing properties)
속성 이름으로 객체의 속성에 접근할 수 있다. 속성 접근자는 **점 표기법(dot notation)**과 대괄호 표기법(bracket notation) 두 가지 문법이 있다. 예를 들어 myCar 객체의 속성에 다음과 같이 접근할 수 있다.
// 점 표기법
myCar.make = "Ford";
myCar.model = "Mustang";
myCar.year = 1969;
// 대괄호 표기법
myCar["make"] = "Ford";
myCar["model"] = "Mustang";
myCar["year"] = 1969;
객체 속성 이름은 빈 문자열을 포함한 어떤 JavaScript 문자열이나 symbol이든 될 수 있다. 그러나 유효한 JavaScript 식별자가 아닌 이름의 속성에 점 표기법으로 접근할 수는 없다. 예를 들어 공백이나 하이픈이 있고, 숫자로 시작하거나, 변수 안에 담긴 속성 이름은 대괄호 표기법으로만 접근할 수 있다. 이 표기법은 속성 이름을 동적으로 결정해야 할 때, 즉 런타임까지 결정할 수 없을 때도 매우 유용하다. 예는 다음과 같다.
const myObj = {};
const str = "myString";
const rand = Math.random();
const anotherObj = {};
// myObj에 추가 속성 만들기
myObj.type = "Dot syntax for a key named type";
myObj["date created"] = "This key has a space";
myObj[str] = "This key is in variable str";
myObj[rand] = "A random number is the key here";
myObj[anotherObj] = "This key is object anotherObj";
myObj[""] = "This key is an empty string";
console.log(myObj);
// {
// type: 'Dot syntax for a key named type',
// 'date created': 'This key has a space',
// myString: 'This key is in variable str',
// '0.6398914448618778': 'A random number is the key here',
// '[object Object]': 'This key is object anotherObj',
// '': 'This key is an empty string'
// }
console.log(myObj.myString); // 'This key is in variable str'
위 코드에서 키 anotherObj는 문자열도 symbol도 아닌 객체다. 이것이 myObj에 추가될 때 JavaScript는 anotherObj의 toString() 메서드를 호출하고 결과 문자열을 새 키로 사용한다.
변수에 저장된 문자열 값으로 속성에 접근할 수도 있다. 변수는 대괄호 표기법으로 전달되어야 한다. 위 예에서 변수 str은 "myString"을 담고 있었고, 속성 이름이 된 것은 "myString"이다. 따라서 myObj.str은 undefined를 반환한다.
str = "myString";
myObj[str] = "This key is in variable str";
console.log(myObj.str); // undefined
console.log(myObj[str]); // 'This key is in variable str'
console.log(myObj.myString); // 'This key is in variable str'
이를 통해 런타임에 결정된 어떤 속성에든 접근할 수 있다.
let propertyName = "make";
myCar[propertyName] = "Ford";
// 변수의 내용을 바꿔 다른 속성에 접근
propertyName = "model";
myCar[propertyName] = "Mustang";
console.log(myCar); // { make: 'Ford', model: 'Mustang' }
그러나 이름이 외부 입력으로 주어진 속성에 접근할 때 대괄호를 사용하는 것에 주의하라. 이는 코드를 **객체 주입 공격(object injection attacks)**에 취약하게 만들 수 있다.
객체의 존재하지 않는 속성은 값 undefined를 가진다(null이 아니라).
myCar.nonexistentProperty; // undefined
속성 열거하기 (Enumerating properties)
객체 속성을 나열·순회하는 세 가지 기본 방법이 있다.
for...in루프. 이 메서드는 객체의 모든 열거 가능한 문자열 속성과 그것의 프로토타입 체인까지 순회한다.Object.keys(). 이 메서드는 객체myObj안의 열거 가능한 자체(own) 문자열 속성 이름("키")만 담은 배열을 반환하며, 프로토타입 체인의 것은 반환하지 않는다.Object.getOwnPropertyNames(). 이 메서드는 열거 가능 여부와 관계없이 객체myObj안의 모든 자체 문자열 속성 이름을 담은 배열을 반환한다.
for...in과 함께 대괄호 표기법을 사용해 객체의 모든 열거 가능한 속성을 순회할 수 있다. 어떻게 동작하는지 설명하기 위해, 다음 함수는 객체와 객체의 이름을 인수로 전달하면 객체의 속성을 표시한다.
function showProps(obj, objName) {
let result = "";
for (const i in obj) {
// Object.hasOwn()은 객체의 프로토타입 체인에서 속성을
// 제외하고 "own properties"만 표시하는 데 사용
if (Object.hasOwn(obj, i)) {
result += `${objName}.${i} = ${obj[i]}\n`;
}
}
console.log(result);
}
"own property(자체 속성)"라는 용어는 객체의 속성을 의미하되 프로토타입 체인의 것은 제외한다. 따라서 함수 호출 showProps(myCar, 'myCar')는 다음을 출력한다.
myCar.make = Ford
myCar.model = Mustang
myCar.year = 1969
위는 다음과 동등하다.
function showProps(obj, objName) {
let result = "";
Object.keys(obj).forEach((i) => {
result += `${objName}.${i} = ${obj[i]}\n`;
});
console.log(result);
}
비열거 가능한 것을 포함한 모든 상속 속성을 나열하는 기본 방법은 없다. 그러나 다음 함수로 달성할 수 있다.
function listAllProperties(myObj) {
let objectToInspect = myObj;
let result = [];
while (objectToInspect !== null) {
result = result.concat(Object.getOwnPropertyNames(objectToInspect));
objectToInspect = Object.getPrototypeOf(objectToInspect);
}
return result;
}
자세한 내용은 Enumerability and ownership of properties를 보라.
속성 삭제하기 (Deleting properties)
delete 연산자로 상속되지 않은 속성을 제거할 수 있다. 다음 코드는 속성을 제거하는 방법을 보여준다.
// 두 속성 a와 b를 가진 새 객체 myObj 생성.
const myObj = { a: 5, b: 12 };
// a 속성을 제거해 myObj에 b 속성만 남긴다.
delete myObj.a;
console.log("a" in myObj); // false
상속 (Inheritance)
JavaScript의 모든 객체는 적어도 하나의 다른 객체로부터 상속받는다. 상속받는 대상 객체를 **프로토타입(prototype)**이라고 하며, 상속된 속성은 생성자의 프로토타입 객체에서 찾을 수 있다. 자세한 내용은 Inheritance and the prototype chain을 보라.
한 타입의 모든 객체에 대한 속성 정의 (Defining properties for all objects of one type)
prototype 속성을 사용해 특정 생성자로 만든 모든 객체에 속성을 추가할 수 있다. 이것은 객체의 단일 인스턴스가 아니라 지정된 타입의 모든 객체가 공유하는 속성을 정의한다. 다음 코드는 Car 타입의 모든 객체에 color 속성을 추가하고, 인스턴스 car1에서 그 속성 값을 읽는다.
Car.prototype.color = "red";
console.log(car1.color); // "red"
메서드 정의하기 (Defining methods)
메서드는 객체와 연관된 함수, 또는 다르게 말하면 함수인 객체의 속성이다. 메서드는 일반 함수가 정의되는 것처럼 정의되지만, 객체의 속성으로 할당되어야 한다는 점이 다르다. 자세한 내용은 메서드 정의를 보라. 한 예:
objectName.methodName = functionName;
const myObj = {
myMethod: function (params) {
// 무언가 수행
},
// 이것도 동작한다!
myOtherMethod(params) {
// 다른 무언가 수행
},
};
여기서 objectName은 기존 객체, methodName은 메서드에 할당하는 이름, functionName은 함수의 이름이다.
그러면 다음과 같이 객체의 문맥에서 메서드를 호출할 수 있다.
objectName.methodName(params);
메서드는 보통 생성자의 프로토타입 객체에 정의되어 같은 타입의 모든 객체가 같은 메서드를 공유하게 한다. 예를 들어 이전에 정의된 Car 객체들의 속성을 형식화해 표시하는 함수를 정의할 수 있다.
Car.prototype.displayCar = function () {
const result = `A Beautiful ${this.year} ${this.make} ${this.model}`;
console.log(result);
};
메서드가 속한 객체를 가리키기 위해 this를 사용하는 것에 주목하라. 그러면 각 객체에 대해 displayCar 메서드를 다음과 같이 호출할 수 있다.
car1.displayCar();
car2.displayCar();
객체 참조에 this 사용하기 (Using this for object references)
JavaScript에는 메서드 안에서 현재 객체를 가리키는 데 사용할 수 있는 특별한 키워드 this가 있다. 예를 들어 manager와 intern 두 객체가 있다고 하자. 각 객체는 자신만의 name, age, job을 가진다. 함수 sayHi()에서 this.name의 사용에 주목하라. 두 객체에 추가되면 같은 함수가 그 함수가 붙은 각 객체의 이름으로 메시지를 출력한다.
const manager = {
name: "Karina",
age: 27,
job: "Software Engineer",
};
const intern = {
name: "Tyrone",
age: 21,
job: "Software Engineer Intern",
};
function sayHi() {
console.log(`Hello, my name is ${this.name}`);
}
// 두 객체에 sayHi 함수 추가
manager.sayHi = sayHi;
intern.sayHi = sayHi;
manager.sayHi(); // Hello, my name is Karina
intern.sayHi(); // Hello, my name is Tyrone
this는 함수 호출의 "숨은 매개변수"로, 호출된 함수 앞에 객체를 지정함으로써 전달된다. 예를 들어 manager.sayHi()에서 manager가 함수 sayHi() 앞에 오므로 this는 manager 객체다. 다른 객체에서 같은 함수에 접근하면 this도 바뀐다. Function.prototype.call()이나 Reflect.apply() 같은 다른 메서드로 함수를 호출하면 this의 값을 인수로 명시적으로 전달할 수 있다.
getter와 setter 정의하기 (Defining getters and setters)
getter는 특정 속성의 값을 얻는, 속성과 연관된 함수다. setter는 특정 속성의 값을 설정하는, 속성과 연관된 함수다. 함께 사용하면 속성의 값을 간접적으로 나타낼 수 있다.
getter와 setter는
- 객체 초기화자 안에서 정의되거나,
- 나중에 기존 객체에 추가될 수 있다.
객체 초기화자 안에서 getter와 setter는 일반 메서드처럼 정의되지만 키워드 get 또는 set이 앞에 붙는다. getter 메서드는 매개변수를 기대해서는 안 되고, setter 메서드는 정확히 하나의 매개변수(설정할 새 값)를 기대한다. 예:
const myObj = {
a: 7,
get b() {
return this.a + 1;
},
set c(x) {
this.a = x / 2;
},
};
console.log(myObj.a); // 7
console.log(myObj.b); // 8, get b() 메서드에서 반환
myObj.c = 50; // set c(x) 메서드를 호출
console.log(myObj.a); // 25
myObj 객체의 속성은:
myObj.a— 숫자myObj.b—myObj.a에 1을 더한 값을 반환하는 gettermyObj.c—myObj.a의 값을myObj.c가 설정되는 값의 절반으로 설정하는 setter
getter와 setter는 생성 후 언제든지 Object.defineProperties() 메서드로 객체에 추가할 수도 있다. 이 메서드의 첫 매개변수는 getter나 setter를 정의하려는 객체다. 두 번째 매개변수는 속성 이름이 getter 또는 setter 이름이고, 속성 값이 getter 또는 setter 함수를 정의하는 객체인 객체다. 다음은 이전 예에서 사용한 것과 같은 getter와 setter를 정의하는 예시다.
const myObj = { a: 0 };
Object.defineProperties(myObj, {
b: {
get() {
return this.a + 1;
},
},
c: {
set(x) {
this.a = x / 2;
},
},
});
myObj.c = 10; // 10 / 2 (5)를 'a' 속성에 할당하는 setter를 실행
console.log(myObj.b); // a + 1 또는 6을 생성하는 getter를 실행
두 형태 중 어느 것을 선택할지는 프로그래밍 스타일과 당면한 작업에 달려 있다. 원본 객체의 정의를 바꿀 수 있다면 원본 초기화자를 통해 getter와 setter를 정의하는 것이 좋다. 이 형태는 더 간결하고 자연스럽다. 그러나 나중에 getter와 setter를 추가해야 한다면 — 아마 그 특정 객체를 직접 작성하지 않았기 때문에 — 두 번째 형태가 유일한 가능한 형태다. 두 번째 형태는 JavaScript의 동적 성격을 더 잘 나타내지만, 코드를 읽고 이해하기 어렵게 만들 수 있다.
객체 비교하기 (Comparing objects)
JavaScript에서 객체는 **참조 타입(reference type)**이다. 두 개의 서로 다른 객체는 같은 속성을 가져도 결코 같지 않다. 같은 객체 참조를 그 자신과 비교할 때만 true가 된다.
// 두 변수, 같은 속성을 가진 두 개의 서로 다른 객체
const fruit = { name: "apple" };
const anotherFruit = { name: "apple" };
fruit == anotherFruit; // false 반환
fruit === anotherFruit; // false 반환
// 두 변수, 하나의 객체
const fruit = { name: "apple" };
const anotherFruit = fruit; // fruit 객체 참조를 anotherFruit에 할당
// 여기서 fruit와 anotherFruit는 같은 객체를 가리킨다
fruit == anotherFruit; // true 반환
fruit === anotherFruit; // true 반환
fruit.name = "grape";
console.log(anotherFruit); // { name: "grape" }; { name: "apple" }이 아니라
비교 연산자에 대한 자세한 정보는 equality operators를 보라.
더 알아보기
- 상속과 프로토타입 체인(Inheritance and the prototype chain) 가이드
- 클래스(Classes) 가이드
- Object 참조
- 객체 초기화자(Object initializer)
- 키워드: 객체, 속성, 메서드, 생성자, new, 프로토타입, this, getter, setter, Object.create, Object.keys, delete