JS구조분해할당,객체 상속(2024-11-08 수업)

짝은별·2024년 11월 10일

JS

목록 보기
8/23

객체 구조분해 할당

key와 value

객체구조분해 할당에서의 key값은 변수명일치해야한다
하지만 객체는 배열의 속성과 다르게 순서가 존재하지 않는다 따라서 순서 변경이 가능하다

const salaries = {
  a: 800,
  b: 500,
  c: 700,
  d: 100,
};

const { a, c, d, b } = salaries;

console.log(a); // 800
console.log(b); // 500
console.log(c); // 700
console.log(d); // 100

위의 예시처럼 순서가 바뀌어도 각각의 변수에 대한 할당은 유지가 되는 모습이다

하지만 변수명을 꼭 바꿔야 할 일이 생긴다면 다음과 같이 작성할 수 있다

const { a:ace , c:chicken , d, b } = salaries;

console.log(ace); // 800
console.log(chicken); // 700
console.log(a); // not defined에러
console.log(c); // not defined에러

이 경우 변수명이 변했으므로 기존의 a와 c만으로는 value를 찾아낼 수 없다
하지만 그렇다고 해서 점표기법과 대괄호 표기법에 대한 작동은 사라진 것은 아니다

console.log(salaries.a); // 800
console.log(salaries["c"]); // 700

또한 배열구조분해 할당에서의 경우처럼 default값 설정도 가능하다

const {a,b,c,d,e} = salaries;
const {a,b,c,d=200,e=1500} = salaries;

위의 두 식의 차이는 default값을 지정해주어 만약 객체 내부에 존재하지 않더라도 할당하는 것이 가능하다는 점이다

위에서 console.log(d)는 100console.log(e)는 undefined를 출력하지만
아래에서는 console.log(d)는 똑같이 100console.log(e)는 1500을 출력한다

이때 d의 값이 바뀌지 않는 이유default 값을 설정한 것이기 때문이다
default값은 객체 내부에 존재한다면 객체의 값으로 덮어 씌워진다
그리고 e는 default값이 설정되었고 객체 내부에 존재하지 않기 때문에 defualt값이 그대로 할당된 것이다

앞에서 언급한 방식(변수명 변경, default value)들을 모두 한 번에 지정할 수 있지만 가독성이 저하된다

const { a: ace = 100, c: chicken, d:dinosaur, b:bee , e:elephant = 300 } = salaries;

참고로 객체 생성과 객체 구조분해 할당은 혼동하지 않도록 주의해야 한다

const obj = {}; // 객체 생성(literal)
const {a,b,c,d} = obj; // 객체 구조분해 할당

이는 {}가 어디에 위치하는지 확인을 하면 구분하기 수월할 수 있다

객체의 구조분해 할당함수 사용에 많은 도움을 준다

function createUserObject(obj) {
  const { name, age, address, phone, job } = obj;

  return {
    name,
    age,
    address,
    phone,
    job,
  };
}

const data = {
  name: '홍길동',
  age: 35,
  address: '중랑구',
  phone: '010-1234-5678',
  job: '강사',
};

const user = createUserObject('amy', '29', 'california', '333-3333');

이때 매개변수 자리에 바로 구조분해할당이 가능하다
그리고 앞서 언급한 변수명 변경default value설정도 역시 가능하다

function createUserObject({ name, age, address, phone, job }) {
	...
}

재밌는 사실이 하나 있다
지금까지 객체점 표기법과 대괄호 표기법이 있다고 언급을 많이 했었다
그리고 JS는 모든 것이 객체로 이루어져 있다고도 얘기했었다
이때 도출할 수 있는 한 가지 사실이 있다
기본적으로 JS에 존재하는 method들객체로 이루어져 있다는 것이다
대표적인 예로 console.log()를 살펴보자

console.log()객체라 생각하면 console이라는 객체 내부log라는 함수가 있다는 것이다
따라서 이를 구조분해할당으로 log만 추출해보겠다

const { log } = console;
log('안녕'); // 안녕

이제는 log만 사용하더라도 console.log()와 같은 역할을 하게 된다
그렇다면 변수명을 변경하는 것은 어떨까?

const { log : l } = console;
l('안녕');

그리고 객체의 구조분해 할당에서도 rest를 사용이 가능하다
하지만 배열과는 다르게 객체는 rest를 객체로 생성한다

const user = {
    age: 40,
    height: 180,
    money: 100,
    kindess : 100,
}

const {age,...rest} = user;

console.log(rest); //{height: 180, money: 100, kindess: 100}

객체의 복사

참조 복사

객체는 기본적으로 참조 복사를 수행하고 있다
그렇다면 참조 복사란 무엇일까?
일반적으로 복사라 생각을 했을때 프린터기라고 생각을 한다
그래서 a와 b를 같은 것을 복사했다고 하면 내용은 같지만 a가 훼손됐다고 해서 b도 같이 훼손된다고 생각하지 않는다
하지만 참조복사는 다르다 참조복사원본이 훼손되면 복사본도 마찬가지로 훼손이 되는 원리이다
그렇다면 이런 일은 왜 일어나는 것일까?

원시값을 먼저 살펴보자

let a = 100;
let b = a;
a = 10000;

console.log(b); // 100

바로 알 수 있듯이 원시값프린터기와 같은 역할을 한다
따라서 원본이 훼손되어도 복사본은 훼손되지 않는다

이번엔 객체로 살펴보자

const userA = {
  name : 'kevin',
  age : 10,
  playTime : 50,
}

const userB = userA;
userA.age = 20;

console.log(userB.age); // 20

뭔가 잘못됐다고 생각이 든다
분명 userA의 나이만 바꾸기를 원했는데 userB의 나이도 같이 바뀌는 상황이 발생한다
이를 참조 복사라고 부른다

그렇다면 참조복사는 어떤 방식으로 일어나게 되는 것일까?
바로 userB가 userA의 모든 것을 복사했기 때문이다
그렇다는 말은 userA의 내부 데이터에만 접근한 것이 아닌
userA의 데이터가 할당된 메모리 주소까지 접근했다는 얘기이다
따라서 다음을 보면 이해가 될 것이다

그렇다면 이 예를 들어 또 한가지 신기한 사실을 발견할 수 있다

const a = {};
const b = {};

console.log(a===b); // false

이때 a와 b는 서로 다른 객체를 생성했지만 '객체'라는 사실은 자명한 사실이다
하지만 둘을 비교하니 Data도 같고 Object인 사실도 같지만 false를 출력한다
이것은 바로 메모리 주소가 다르기 때문임을 알 수 있다

따라서 이러한 문제점을 해결하고자 객체의 data만을 복사하여 새로운 객체를 생성하는 방법을 고안한 것이다

얕은 복사와 깊은 복사

우선 얕은 복사를 먼저 살펴보자
얕은 복사객체의 data(property)를 분해하고 이를 다시 조립하여 새로운 객체로 탄생시키는 방법이다

// for...in문을 사용하여 해결
const cloneObject = {};

for (const key in messenger) {
  cloneObject[key] = messenger[key];
}

우선 반복문을 실행시켜 해결하는 방법을 먼저 살펴보자
이는 기존의 객체 내부에 모든 key값을 순회하며 해당하는 value들을 추출하여
새로운 객체에 담아내는 방식이다

// Object.assign을 이용하여 해결
const copyObject = Object.assign({}, messenger);

이번엔 method를 활용한 방식을 살펴보자
우선 사용법은 Object.assing()으로 사용하고 가장 처음으로 오는 arguments반환되는 객체를 입력해준다
-> 이 코드에선 빈 객체에 뒤에 나열한 모든 객체들의 data를 병합하여 조립하고 반환시킨다

const spreadObject = { ...messenger };

마지막으로는 spread syntax를 이용한 방법이다
spread syntax모든 property들을 나열하고 이를 새로운 객체({})로 묶어 할당시키는 방법이다

만약 두 개 이상의 객체한 곳에 복사하고 싶다면 다음과 같이 가능하다

let combinedCssMap = { ...cssMapA, ...cssMapB };
let combinedCssMap = Object.assign({}, cssMapA, cssMapB);

이러한 방법이 간단하고 좋아보이지만 치명적인 단점도 역시 존재한다
그것은 바로 깊이가 1depth인것만 가능하다는 것이다
이게 무슨 의미일까

const user = {
    userA : {
        age:20,
    },
    userB : {
        age:30,
    },
    userC : {
        age:40,
    },
}

이러한 객체는 객체 내부에 또 다른 객체가 존재해서 depth가 2가 된다
그리고 이를 얕은 복사의 방식으로 복사하고 userA의 age를 변경해보자

const newUser = Object.assign({},user);

user.userA.age = 50;
console.log(newUser.userA.age); // 50

예상 못한 결과가 나온다
분명 우리는 모든 data를 분해하고 새로운 객체에 할당했는데 왜 이런 결과가 나왔을까?
바로 객체 내부에 존재하는 객체를 분해하지 않고 할당했기 때문이다

이러한 문제를 해결하기 위해 깊은 복사가 필요하다

깊은 복사란? 얕은 복사depth가 1인 객체에만 적용이 가능하지만 깊은 복사depth 깊이에 영향을 받지 않는 복사를 말한다

그렇다면 이는 어떤 방식으로 사용할까?
우선 재귀library를 사용하는 방식이 있다

function cloneDeep(object) {
  return Object.fromEntries(
    Object.entries(object).map(([key, value]) => {
      let type = typeof value;
      if (value && type === 'object') {
        value = cloneDeep(value);
      }
      return [key, value];
    })
  );
}

재귀에 대한 설명은 넘어가도록 하겠다
굳이 재귀를 사용해야 하는 이유는 무엇일까?
바로 반복되는 작업이 존재하기 때문이다

  1. 객체인지 확인
  2. 내부에 key값을 순회
  3. key에 해당하는 value가 객체인지 확인
  4. 객체가 아니라면 전달
  5. 객체라면 재귀 호출

과 같은 원리로 모든 값을 추출할 수 있다

참고로 Object.fromEntries()배열의 key값과 value가 있다면 그것을 다시 객체로 변환하는 작업이다

그렇다면 이번엔 library(lodash)사용에 대한 간단한 얘기만 해보겠다
library를 사용하면 위의 함수보다 단순하게 추출할 수 있을 것이다
하지만 굳이 직접 구현하는 이유는 무엇일까?
library특정한 함수만을 가져오기 힘들다
따라서 깊은 복사를 위한 method만 사용하고 싶어도 사용하지 않는 모든 method들을 가져와야 한다는 의미이다
그렇기 때문에 성능적인 측면에서 성능 저하를 유발할 수 있다

가비지 컬렉터

도달 가능성(reachability)

JS도달가능성의 개념을 사용해 메모리를 관리한다

도달 가능성이란? 어떻게든 접근하거나 사용할 수 있음을 의미한다
도달 가능한(reachable) 값은 메모리에서 삭제되지 않는다

또한 태생적으로 도달 가능하여 명백한 이유 없이는 사라지지 않는 것들이 존재한다

  • 현재 함수의 지역 변수와 매개변수
  • 중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수
  • 전역 변수 등등
    이러한 값들을 루트(root)라고 부른다

루트와 연관된 것들 즉, 루트가 참조하거나 루트에서 참조할 수 있는 값들도 역시 도달가능한 값이 된다

JS는 모든 엔진 내에서 가비지 컬렉션끊임없이 작동한다
이때 모든 객체를 모니터링하고 도달가능하지 않다고 판단되면 그 객체는 삭제한다

위의 그림을 예시로 들어보자
지금은 현재 모든 객체들이 root를 참조하는 형식이 됐다
하지만 이때 2번과 5번의 화살표를 삭제하면
obj2는 더 이상 root를 참조하지 않게 된다
따라서 obj2는 삭제된다

더욱 극단적인 예를 들어보자
이번엔 1번 화살표만 삭제해보자
그러면 obj1이 루트를 참조하지 않게 되고
obj2와 obj3도 역시 루트를 참조하지 못한다
따라서 root를 제외한 모든 객체가 삭제된다

도달할 수 없는 섬

위와 같은 예제를 도달할 수 없는 섬이라고 지칭한다
또한 mark-and-sweep이라는 가비지 컬렉션 알고리즘에 의해 도달할 수 없는 섬을 찾는 것도 가능하다
mark-and-sweep의 가장 중요한 점은 방문한 곳은 mark를 하고 mark가 되어있다면 다시 방문하지 않는다는 점이다
mark-and-sweep의 순서를 살펴보자

  • 가비지 컬렉터는 루트 정보를 수집하고 이를 mark한다
  • 루트가 참조하고 있는 모든 객체를 방문하고 이를 mark한다
  • mark된 모든 객체에 방문하고 그 객체가 참조하고 있는 모든 객체를 방문해 mark한다
  • 위 과정에서 mark된 객체는 방문하지 않는다
  • 루트에서 도달 가능한 모든 객체를 방문할 때 까지 반복한다
  • 마지막으로 mark가 되지 않는 객체들은 삭제한다

프로토타입 상속

[[Prototype]]

JS의 객체는 [[Prototype]]이라는 숨김 프로퍼티 값을 갖는다
이는 값이 null이거나 다른 객체에 대한 참조가 가능하다
만약 다른 객체를 참조했다면 참조 대상을 '프로토타입'이라고 부른다

만약 어떤 객체에서 property를 읽으려 할 때 해당 property가 없다면 자동으로 프로토타입에서 찾는다
이러한 동작 방식을 프로토타입 상속이라 부른다

이를 활용해 프로토타입 값을 설정하는 것도 가능하다

proto

전에 한 번 봤던 기억이 있다 '던더 프로토'라고 부르며, 이는 프로토타입 값을 변경하는 것이 가능하다

이것은 [[Prototype]]의 전용 getter, setter이다

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal;
console.log(rabbit.eats); // true

이때 rabbit의 prototype은 animal이 된다

rabbit에는 분명 eats라는 key를 설정하지 않았지만 앞서 언급한 프로토타입 상속에 의해 rabbit.eats는 true를 반환한다

프로토타입 체인

프로토타입 상속을 이용해 새로운 객체를 손 쉽게 만드는 응용방법이 있다

const animal = {
  legs: 4,
  tail: true,

  //getter
  get eat() {
    return this.stomach;
  },
  //setter
  set eat(food) {
    this.stomach = [];
    this.stomach.push(food);
  },
};

const tiger = {
  pattern: '호랑이 무늬',
  hunt(target) {
    this.prey = target;
    this.eat = this.prey;
    return `${target}에게 조용히 접근한다`;
  },
  __proto__: animal,
};

const white_tiger = {
  name: '백돌이',
  color: 'white',
  __proto__: tiger,
};

이때 동물의 특성animal호랑이의 특성tiger에 저장하여 white_tiger라는 새로운 객체를 만든 것이다

이는 상속의 상속을 준 것이다

지금은 animaltiger의 prototype, tigerwhite_tiger의 prototype이다
이 상황에서 나올 수 있는 질문은 white_tigeranimal의 특성을 가질 수 없는 것이 아닌가?
라는 생각을 할 수도 있다
하지만 프로토타입 체인에 의해 tiger가 상속받은 값을 그대로 white_tiger에게 넘겨주는 일이 발생한다

프로토타입 체이닝두 가지 제약이 있다

  • 순환 참조가 불가능하다
  • 던더 프로토의 값은 객체 or null만 가능하다 이 외의 다른 값들은 무시된다

또한 당연하게도 하나의 객체하나의 프로토타입만 존재한다

this

위의 코드를 유심히 보면 중간중간에 this가 계속해서 사용되는 것을 알 수 있다
그러면 여기서 this는 어떻게 작동하는 것일까?

단순하다 프로토타입을 무시하고 호출한 객체를 반환한다

getter,setter

또한 get eat() {}set eat(){}라고 사용된 부분이 있다
이는 gettersetter를 지정해준 것이다
하지만 이름이 중복되었으니 오류가 나야하는 것이 아닌가?
그렇지 않다 gettersetter이름이 중복되도 상관 없다

그렇다면 사용법은 어떠할까?
함수처럼 생겼지만 실제 사용법은 객체처럼 사용한다

animal.eat = '먹이';
aniaml.eat;

예전에 배웠던 style을 지정하는 법에 대해 다시 살펴보면
$0.style.background = 'red'와 같은 경우 객체처럼 사용하고 있다
역시 마찬가지로 getter와 setter로 작동하고 있는 것이다

함수의 property 프로토타입

function Animal() {
  this.legs = 4;
  this.tail = true;

  this.getEat = function () {
    return this.stomach ?? [];
  };
  this.setEat = function (food) {
    this.stomach = [];
    this.stomach.push(food);
  };
}

function Tiger(name) {
  Animal.call(this);
  this.name = name;
  this.pattern = '호랑이무늬';
  this.hunt = function (target) {
    this.prey = target;
    return `${target}에게 조용히 접근한다`;
  };
}

const Stiger = new Tiger('금순이');

함수는 기본적으로 prototype 프로퍼티를 갖는다
이때 prototype propertyconstructor를 가리키는데 constructor함수 자신을 가리킨다

만약 함수의 prototype을 변경하고 싶으면 위의 코드에서 보이는 call을 사용하는 것이다
call함수를 대신 실행시켜주는 기능을 담당하고 있다
단, 이때 this는 바꿔치기된다
따라서 Animal.call(this)를 살펴보면 thisAnimal을 불러와야 하지만 Tiger에 객체가 생성되는 모습을 확인할 수 있다

만약 생성자 함수로 만들었다면 .prototype을 이용해 변경도 가능하다

profile
FE(철 아님) 개발자 꿈꾸다

0개의 댓글