본문 바로가기
JavaScript/JavaScript

[JAVASCRIPT.INFO] 객체: 기본

by HJ0216 2023. 10. 20.

이 글은 [모던 JavaScript 튜토리얼]을 공부하며 정리한 글입니다.

 

 

👉 기본 환경

- Language: JavaScript

- IDE: VS code


객체(Object)

- 중괄호 {…} 선언

    - 중괄호 안에 ‘키(key): 값(value)’ 쌍으로 구성된 프로퍼티(property)로 구성

    - 키(이름): 문자형, 값: 모든 자료형

        - 키 값에 예약어 사용 가능

        - 숫자 입력 시,

            - 자동으로 문자로 형변환

            - 자동으로 key가 오름차순 정렬(오름차순 정렬을 원하지 않을 경우에는 문자로 취급되도록 " " 등 추가)

 

* 객체 생성

1
2
3
let user = new Object(); // '객체 생성자' 
let user = {};  // '객체 리터럴' 
 
 

⭐ 객체 리터럴이 주로 사용됨

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let user = {     
  name"John",  
  age: 30,
  "likes birds"true// 마지막 , 생략 가능
  // 띄어쓰기 시, "" 사용
};
 
 
// key값에 띄어쓰기가 없을 경우,
// set
user.isAdmin = true;
 
// get
user.isAdmin;
 
// delete
delete user.age;
 
 
// key값에 띄어쓰기가 있을 경우,
// set
user["likes birds"= true;
 
// get
alert(user["likes birds"]); // true
 
// delete
delete user["likes birds"];
 
 

 

⭐ Const 상수는 수정하지 못하지만, Const 객체의 property는 수정 가능

 

* 점 표기법과 대괄호 표기법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 점 표기법
user.likes birds = true // error
 
let user = {
  name"John",
  age: 30
};
 
let key = "name";
alert( user.key ) // undefined
 
 
// 대괄호 표기법
user["likes birds"= true;
 
let user = {
  name"John",
  age: 30
};
 
let key = prompt("사용자의 어떤 정보를 얻고 싶으신가요?""name");
 
// 변수로 접근
alert( user[key] ); // John
 
 

 

* 계산된 Property

- 객체를 만들 때 객체 리터럴 안의 프로퍼티 키가 대괄호로 둘러싸여 있는 경우

1
2
3
4
5
6
7
8
let fruit = prompt("어떤 과일을 구매하시겠습니까?""apple");
 
let bag = {
  [fruit]: 5// 변수 fruit에 프로퍼티 이름을 동적으로 할당
};
 
alert( bag.apple ); // fruit에 "apple" 할당 시, 5 출력
 
 

 

* 단축 Property

1
2
3
4
5
6
7
8
function makeUser(name, age) {
  return {
    name// name: name 과 같음
    age,  // age: age 와 같음
    // ...
  };
}
 
 

 

* 존재하는 Property인지 확인

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// undefined
let user = {};
alert( user.noSuchProperty === undefined ); // '프로퍼티가 존재하지 않음'
 
let obj = {
  test: undefined
};
alert( obj.test === undefined ); // value: undefined와 존재하지 않는 undefined가 구분되지 않음
 
 
// in operator
let user = { name"John", age: 30 };
alert"age" in user ); // true
alert"blabla" in user ); // false
 
 

 

* for .. in 반복문

1
2
3
4
5
6
7
8
9
10
11
12
let user = {
  name"John",
  age: 30,
  isAdmin: true
};
 
for (let key in user) {
  alert( key );  // name, age, isAdmin
  // 키에 해당하는 값
  alert( user[key] ); // John, 30, true
}
 
 

 

 


2. 참조에 의한 객체 복사

 

* Object.assgin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Object.assign
let user = { name"John" };
 
let permissions1 = { canView: true };
let permissions2 = { canEdit: true };
 
Object.assign(user, permissions1, permissions2);
 
// user = { name: "John", canView: true, canEdit: true }
// permissions1과 permissions2의 프로퍼티를 user로 복사
 
 
// 목표 객체(user)에 동일한 이름을 가진 프로퍼티가 있는 경우
let user = { name"John" };
 
Object.assign(user, { name"Pete" });
 
alert(user.name);
// user = { name: "Pete" }
// 기존 값이 덮어씌워짐
 
 

🚨 Object.assign()은 중첩 객체 복사를 하지 못함

 

+ 중첩 객체 복사 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let user = {
    name"John",
    sizes: {
        height: 182,
        width: 50
    }
};
 
 
// 1. lodash library
let deep = _.cloneDeep(user);
console.log(user.sizes === deep.sizes); // false
 
 
// 2. for .. in
let clone = {};
 
for (let key in user) {
    if(typeof user[key] == 'object'){
        clone[key] = Object.assign({}, user.sizes);
        continue;
    }
    clone[key] = user[key];
}
 
console.log(user.sizes === clone.sizes); // false
 
 

 

 


3. 가비지 컬렉션

- 객체는 도달 가능한 상태일 때 메모리에 남음

 

* 참조 복사

1
2
3
4
5
6
7
8
9
let user = {
    name"John"
};
 
let admin = user;
 
user = null;
console.log(admin.name);
 
 

🚨 user.name에 접근하여 직접 값을 변경하여 admin에도 반영이 되는 것과는 다른 개념

    - name: "John" 대신 null을입력한 것이 X

    - user의 화살표를 제거한 것

 

 


4. 메서드와 this

 

* 객체는 사용자(user), 주문(order) 등과 같이 실제 존재하는 개체(entity)를 표현하고자 할 때 생성

1
2
3
4
5
let user = {
  name"John",
  age: 30
};
 
 

 

* 객체의 프로퍼티에 함수를 할당(= 메서드)해 객체에게 행동할 수 있는 능력을 부여

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 표현식 1
user.sayHi = function() {
  alert("안녕하세요!");
};
 
user.sayHi(); // 안녕하세요!
 
 
// 표현식 2
// 함수 선언
function sayHi() {
  alert("안녕하세요!");
};
 
// 선언된 함수를 메서드로 등록
user.sayHi = sayHi;
 
user.sayHi(); // 안녕하세요!
 
 
// 표현식 3
user = {
  sayHi: function() {
    alert("Hello");
  }
};
 
user.sayHi(); // 안녕하세요!
 
 
// 표현식 4
user = {
  sayHi() { 
    alert("Hello");
  }
};
 
 

 

* this

- 현재 객체

- ‘점 앞의’ 객체가 무엇인가에 따라 ‘자유롭게’ 결정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let user = {
  name"John",
  age: 30,
 
  sayHi() {
    alert( user.name ); // Error: Cannot read property 'name' of null
  }
 
};
 
 
let admin = user;
user = null;
 
admin.sayHi();
// sayHi()가 user의 name을 참조하면서 오류 발생
 
 

🚨 this.name으로 선언 시, admin의 name을 참조하게 되므로 오류가 발생하지 않음

 

* 화살표 함수

- 자신만의 this를 가지지 않음

- 화살표 함수 안에서 this를 사용하면, 외부에서 this 값을 가져옴

1
2
3
4
5
6
7
8
9
10
let user = {
  firstName: "보라",
  sayHi() {
    let arrow = () => alert(this.firstName);
    arrow();
  }
};
 
user.sayHi(); // 보라
 
 

 

* 객체 리터럴에서 'this' 사용하기

1
2
3
4
5
6
7
8
9
10
11
12
// Case1
function makeUser() {
  return {
    name"John",
    ref: this
  };
};


let user = makeUser();
 
alert( user.ref.name ); // Error: Cannot read property 'name' of undefined
 
 

⭐ Undefined Error 발생 이유

- 객체의 메서드로써 호출된 게 아니라 함수로써 호출되었기 때문

    - ref 속성은 함수 내에서 this로 설정되지 않고, 전역 객체를 참조

        - 일반적으로 브라우저 환경에서, 전역 객체는 window 객체
        - window.name은 브라우저 창의 이름을 나타내는 속성으로, 일반적으로 초기에는 빈 문자열로 설정

 

1
2
3
4
5
6
7
8
9
10
11
12
13
function makeUser() {
  return {
    name"John",
    ref() {
      return this;
    }
  };
};
 
let user = makeUser();
 
alert( user.ref().name ); // John
 
 

- ref 속성은 메서드로 정의되었으며, 함수 내부에서 this는 해당 메서드를 호출한 객체

 

⭐ 1번에서는 메서드로 정의되지 않은 ref이므로 this가 해당 메서드를 호출한 객체를 가르키게 되는게 아니라, 전역 객체를 가리키게 되고

⭐ 2번에서는 메서드로 정의된 ref에서 this는 해당 메서드를 호출한 객체를 가르키게 되므로 user 객체가 가르키는 객체가 됨

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let calculator = {
  sum() {
    return this.a + this.b;
  },
 
  mul() {
    return this.a * this.b;
  },
 
  read() {
    this.a = +prompt('첫 번째 값:'0);
    this.b = +prompt('두 번째 값:'0);
  }
};
 
calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );
 
 

* this를 활용하여 변수를 선언하지 않고도 계산 수행 가능

* prompt에 + 선언

    - 사용자 입력 값을 숫자로 변환

 

⭐ 객체 리터럴 내에서 속성을 선언하고 값을 할당하지 않으면 JavaScript는 자동으로 해당 속성을 undefined로 초기화

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// * 일반
let ladder = {
  step: 0,
  up() {
    this.step++;
  },
  down() {
    this.step--;
  },
  showStep: function() { // 사다리에서 몇 번째 단에 올라와 있는지 보여줌
    alertthis.step );
  }
};
 
ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1
 
 
// * Chaining
let ladder = {
  step: 0,
  up() {
    this.step++;
    return this;
  },
  down() {
    this.step--;
    return this;
  },
  showStep() {
    alertthis.step );
    return this;
  }
}
 
ladder
  .up()
  .up()
  .down()
  .up()
  .down()
  .showStep(); // 1
 
 

⭐ 객체 반환을 통해서 메서드 체이닝

 

 


 

5. new 연산자와 생성자 함수

유사한 객체를 여러 개 만들 때 유용

 

*함수 이름의 첫 글자는 대문자로 시작

    - 일반 함수와 동일한 형태이지만, 구분을 위해 첫 글자를 대문자로 작성

*반드시 new 연산자를 붙여 실행

    - new와 함께 호출하면 내부에서 this가 암시적으로 만들어지고, 마지막엔 this가 반환

1
2
3
4
5
6
7
8
9
10
function User(name) {
  this.name = name;
  this.isAdmin = false;
}
 
let user = new User("보라");
 
alert(user.name); // 보라
alert(user.isAdmin); // false
 
 

- 빈 객체를 만들어 this에 할당

- 함수 본문

- this에 새로운 프로퍼티를 추가해 this를 수정
- this를 반환

1
2
3
4
5
let user = {
  name"보라",
  isAdmin: false
};
 
 

 

* 반환해야 할 것들은 모두 this에 저장되고, this는 자동으로 반환되기 때문에 반환문을 명시적으로 써 줄 필요가 없음

    - return 객체: this 대신 객체 반환

    - return 원시형: return문 무시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function BigUser() {
  this.name = "원숭이";
  return { name"고릴라" };  // <-- this가 아닌 새로운 객체 반환
}
 
alertnew BigUser().name ); // 고릴라
 
 
function SmallUser() {
  this.name = "원숭이";
  return 2// <-- this 반환
}
 
alertnew SmallUser().name ); // 원숭이
 
 

 

⭐ return문이 있는 생성자 함수는 거의 없음

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Calculator(){
    this.read = function(){
        this.a = +prompt('Enter a number.''0');
        this.b = +prompt('Enter a number.''0');
    },
    this.sum = function (){
        return this.a + this.b;
    },
    this.mul = function (){                
        return this.a * this.b;
    }
}
 
let calculator = new Calculator();
calculator.read();
 
alert("Sum=" + calculator.sum());
alert("Mul=" + calculator.mul());
 
 

* 생성자 함수 내부에서 this를 사용 시, 그 this는 새로 생성되는 객체 인스턴스를 가리킴

* 생성자 함수에서 메서드를 정의할 때 this.read = function() {}와 같이 작성하는 이유

- 생성된 각 객체 인스턴스가 자신만의 'read' 메서드를 가지게 됨

- 즉, 'read' 메서드는 해당 객체에 속한 프로퍼티가 되어 해당 객체의 컨텍스트(this)에서 접근하고 호출할 수 있습니다.

- 반대로, 만약 this 없이 그냥 function read() {}와 같이 작성한다면, 'read' 함수는 생성자 함수의 로컬 함수가 되어 외부에서 접근할 수 없음


* 따라서, 각각의 Calculator 인스턴스가 자신만의 'read', 'sum', 'mul' 메서드를 가지고 이들 메서드가 해당 인스턴스에 속한 데이터('a', 'b')에 접근할 수 있도록 하기 위해, 메서드 정의에 this.methodName = function() {} 형태로 작성해야 함

 

1
2
3
4
5
6
7
8
function Accumulator(startNum){
    this.number = startNum;
    this.read = function(){
        this.number += +prompt('Enter a number');
    }
    this.value = this.number;
}
 
 

* value가 startNum으로 고정된 이유

    - this.value = number는 객체 생성 시, 1번만 실행되므로 이미 설정된 value의 값은 변경되지 않음

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Accumulator(startNum) {
    this.number = startNum;
    this.read = function () {
        this.number += +prompt('Enter a number');
    };
    this.value = function () {
        return this.number;
    };
}
 
let accumulator = new Accumulator(1); // 최초값: 1
 
accumulator.read(); // 사용자가 입력한 값을 더해줌
accumulator.read(); // 사용자가 입력한 값을 더해줌
 
alert(accumulator.value()); // 최초값과 사용자가 입력한 모든 값을 더해 출력함
 
 

* value를 메서드로 만들어 현재까지 누적된 값을 반환하게 할 수 있도록 함

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Accumulator(startNum) {
    this.value = startNum;
    this.read = function () {
        this.value += +prompt('Enter a number');
    };
}
 
let accumulator = new Accumulator(1); // 최초값: 1
 
accumulator.read(); // 사용자가 입력한 값을 더해줌
accumulator.read(); // 사용자가 입력한 값을 더해줌
 
alert(accumulator.value); // 최초값과 사용자가 입력한 모든 값을 더해 출력함
 
 

* value 변수를 직접 변경

 

 


6. 옵셔널 체이닝 '?.'

 

* Optional Chaining

- ?. ?.의 왼쪽 평가 대상이 undefined null이면 평가를 멈추고 undefined를 반환

⭐ ?.는 존재하지 않아도 괜찮은 대상에만 사용

    - 논리상 user는 반드시 있어야 하는데 address는 필수값이 아닐 경우, user.address?.street를 사용하는 것이 바람직

    - 꼭 있어야 하는 값인데 없는 경우에 ?.을 사용하면 프로그래밍 에러를 쉽게 찾을 수 없으므로 유의

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// key: 값
let user = null;
alert( user?.address ); // undefined
 
 
// key: 함수
let user1 = {
  admin() {
    alert("관리자 계정입니다.");
  }
}
 
let user2 = {};
 
user1.admin?.(); // 관리자 계정입니다.
user2.admin?.(); // 평가가 멈춤
 
 

 

🚨 ?.은 읽기나 삭제하기에는 사용할 수 있지만 쓰기에는 사용할 수 없음

1
2
3
4
5
6
7
8
9
10
11
12
// 검색
let user = {}; // 주소 정보가 없는 사용자
alert( user?.address?.street ); // undefined,
 
 
// 삭제
delete user?.name// user가 존재하면 user.name을 삭제
 
 
// 삽입
user?.name = "Violet"// undefined = "Violet"으로 SyntaxError 발생
 
 

 

 


7. 심볼형

* 유일한 식별자(unique identifier)를 만들고 싶을 때 사용

- 심볼형 값은 다른 자료형으로 암시적 형 변환(자동 형 변환)되지 않음

 

* 심볼 설명

1
2
3
// 심볼 id에는 "id"라는 설명 추가
let id = Symbol("id");
 
 

 

* 숨김 프로퍼티

    - 외부 코드에서 접근이 불가능하고 값도 덮어쓸 수 없는 프로퍼티

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 서드파티 코드에서 가져온 객체
let user = { name"John" };
 
user.id = "스크립트 id 값";
// 문자열 "id"를 사용해 식별자 생성
user.id = "제3 스크립트 id 값"
// 덮어쓰기되어 id 식별자가 무의미해짐
 
 
let id1 = Symbol("id");
// 심볼을 사용하면 서드파티 코드가 모르게 user에 식별자를 부여할 수 있음
user[id1] = "스크립트 id 값";
// 심볼은 유일성이 보장되므로 우리가 만든 식별자와 제3의 스크립트에서 만든 식별자의 이름이 동일하더라도 충돌하지 않음
let id2 = Symbol("id");
user[id2] = "제3 스크립트 id 값";
 
alert(id1 == id2); // false
 
 

 

*  심볼 배제와 포함

- for...in 반복문

- Object.keys()

- Object.assign()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let id = Symbol("id");
let user = {
  name"John",
  age: 30,
  [id]: 123
};
 
 
// for...in 반복문
for (let key in user) alert(key);
// name과 age만 출력되고, 심볼은 출력되지 않음
alert"직접 접근한 값: " + user[id] );
// 심볼로 직접 접근하면 작동
 
 
// Object.keys(user)
alert(Object.keys(user));
// name과 age만 출력되고, 심볼은 출력되지 않음
 
 
// Object.assign(user)
let clone = Object.assign({}, user);
 
alert( clone[id] ); // 123
// 키가 심볼인 프로퍼티를 배제하지 않고 객체 내 모든 프로퍼티를 복사
 
 

 

* 전역 심볼

- 전역 심볼 레지스트리 안에 있는 심볼은 전역 심볼

- 전역 심볼 레지스트리 안에 심볼을 만들고 해당 심볼에 접근하면, 이름이 같은 경우 항상 동일한 심볼을 반환

    - 조건에 맞는 심볼이 레지스트리 안에 없으면 새로운 심볼 Symbol(key)을 만들고 레지스트리 안에 저장

- 이름이 같은 심볼이 같은 개체를 가리키길 원하는 경우 사용

- 애플리케이션에서 광범위하게 사용해야 하는 심볼이라면 전역 심볼을 사용

1
2
3
4
5
6
7
8
9
// 전역 레지스트리에서 심볼을 검색
let id = Symbol.for("id");
// 심볼이 존재하지 않으면 새로운 심볼 생성
 
let idAgain = Symbol.for("id");
// 동일한 이름을 이용해 심볼을 다시 검색
 
alert( id === idAgain ); // true
 
 

 

* Symbol.for() && Symbol.keyFor()

1
2
3
4
5
6
7
8
// 이름을 이용해 심볼을 찾음
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");
 
// 심볼을 이용해 이름을 얻음
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id
 
 

🚨 검색 범위가 전역 심볼 레지스트리이기 때문에 전역 심볼이 아닌 심볼에는 사용할 수 없음

전역 심볼이 아닌 인자가 넘어오면 Symbol.keyFor undefined를 반환

 

전역 심볼이 아닌 모든 심볼은 description 프로퍼티 사용

일반 심볼에서 이름을 얻고 싶으면 description 프로퍼티 사용

1
2
3
4
5
6
7
8
9
let globalSymbol = Symbol.for("gloName");
let localSymbol = Symbol("locName");
 
alert(Symbol.keyFor(globalSymbol)); // gloName, 전역 심볼
alert(Symbol.keyFor(localSymbol)); // undefined, 전역 심볼이 아님
 
alert(localSymbol.description); // locName
alert(globalSymbol.description); // gloName
 
 

 

cf. 심볼 조회 메서드

내장 메서드 Object.getOwnPropertySymbols(obj)를 사용하면 모든 심볼을 볼 수 있고, 

메서드 Reflect.ownKeys(obj)는 심볼형 키를 포함한 객체의 모든 키를 반환

* 그러나, 대부분의 라이브러리, 내장 함수 등은 이런 메서드를 사용하지 않음