이 글은 [모던 JavaScript 튜토리얼]을 공부하며 정리한 글입니다.
 
 
👉 기본 환경
- Language: JavaScript
- IDE: VS code
객체(Object)
- 중괄호 {…} 선언
    - 중괄호 안에 ‘키(key): 값(value)’ 쌍으로 구성된 프로퍼티(property)로 구성
    - 키(이름): 문자형, 값: 모든 자료형
        - 키 값에 예약어 사용 가능
        - 숫자 입력 시,
            - 자동으로 문자로 형변환
            - 자동으로 key가 오름차순 정렬(오름차순 정렬을 원하지 않을 경우에는 문자로 취급되도록 " " 등 추가)
 
* 객체 생성
|  | 
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
- 객체를 만들 때 객체 리터럴 안의 프로퍼티 키가 대괄호로 둘러싸여 있는 경우
|  | 
let fruit = prompt("어떤 과일을 구매하시겠습니까?", "apple");   let bag = {   [fruit]: 5, // 변수 fruit에 프로퍼티 이름을 동적으로 할당 };   alert( bag.apple ); // fruit에 "apple" 할당 시, 5 출력     | 
 
 
* 단축 Property
|  | 
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. 가비지 컬렉션
- 객체는 도달 가능한 상태일 때 메모리에 남음
 
* 참조 복사
|  | 
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)를 표현하고자 할 때 생성
|  | 
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 값을 가져옴
|  | 
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() { // 사다리에서 몇 번째 단에 올라와 있는지 보여줌     alert( this.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() {     alert( this.step );     return this;   } }   ladder   .up()   .up()   .down()   .up()   .down()   .showStep(); // 1     | 
 
⭐ 객체 반환을 통해서 메서드 체이닝
 
 
 
5. new 연산자와 생성자 함수
⭐ 유사한 객체를 여러 개 만들 때 유용 
 
*함수 이름의 첫 글자는 대문자로 시작
    - 일반 함수와 동일한 형태이지만, 구분을 위해 첫 글자를 대문자로 작성
*반드시 new 연산자를 붙여 실행
    - new와 함께 호출하면 내부에서 this가 암시적으로 만들어지고, 마지막엔 this가 반환 
|  | 
function User(name) {   this.name = name;   this.isAdmin = false; }   let user = new User("보라");   alert(user.name); // 보라 alert(user.isAdmin); // false   |  | 
 
- 빈 객체를 만들어 this에 할당
- 함수 본문
- this에 새로운 프로퍼티를 추가해 this를 수정
- this를 반환
|  | 
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가 아닌 새로운 객체 반환 }   alert( new BigUser().name ); // 고릴라     function SmallUser() {   this.name = "원숭이";   return 2; // <-- this 반환 }   alert( new 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() {} 형태로 작성해야 함
 
|  | 
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)를 만들고 싶을 때 사용
- 심볼형 값은 다른 자료형으로 암시적 형 변환(자동 형 변환)되지 않음
 
* 심볼 설명
|  | 
// 심볼 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)을 만들고 레지스트리 안에 저장
- 이름이 같은 심볼이 같은 개체를 가리키길 원하는 경우 사용
- 애플리케이션에서 광범위하게 사용해야 하는 심볼이라면 전역 심볼을 사용 
|  | 
// 전역 레지스트리에서 심볼을 검색 let id = Symbol.for("id"); // 심볼이 존재하지 않으면 새로운 심볼 생성   let idAgain = Symbol.for("id"); // 동일한 이름을 이용해 심볼을 다시 검색   alert( id === idAgain ); // true     | 
 
 
* Symbol.for() && Symbol.keyFor()
|  | 
// 이름을 이용해 심볼을 찾음 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 프로퍼티 사용
|  | 
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)는 심볼형 키를 포함한 객체의 모든 키를 반환
* 그러나, 대부분의 라이브러리, 내장 함수 등은 이런 메서드를 사용하지 않음