[JavaScript] 2-8. JavaScript Object

JavaScript Object


객체

자바스크립트에서 객체는 단순히 ‘이름(key):값(value)’형태의 프로퍼티들을 저장하는 컨테이너로서,
컴퓨터 과학 분야에서 해시라는 자료구조와 상당히 유사합니다.
자바스크립트에서 기본타입은 하나의 값만을 가지는 데 비해, 참조 타입인 객체는 여러 개의 프로퍼티들을 포함할 수 있으며,
이러한 객체의 프로퍼티는 기본 타입의 값을 포함하거나, 다른 객체를 가리킬 수도 있습니다.
이러한 프로퍼티의 성질에 따라 객체의 프로퍼티는 함수로 포함할 수 있으며, 자바스크립트에서는 이러한 프로퍼티를 메서드라고 부릅니다.



객체 리터럴

객체 리터럴을 이용해서 객체를 생성할 수 있습니다.
중괄호 안에 직접 이름-값 쌍을 적어주면 됩니다.

const moong2 = {
  name: "이근환",
  age: 29,
  gender: "male",
  email: "dlrmsghks7@gmail.com"
};

위의 moong2 라는 변수에 할당된 객체에는 4 개의 속성이 저장되었습니다.
위의 속성 이름은 영어로 되어있지만 한글을 사용할 수도 있습니다.
단, 한글을 사용할 경우 ‘‘를 써서 문자열 표기를 적어줘야 합니다.
속성의 이름도 식별자처럼 허용되지 않는 문자를 사용할 수 없지만 사용할 경우 문자열 표기를 해줘야 합니다.
객체 리터럴을 이용해 속성을 지정할 때, 이미 정의된 변수의 이름을 그대로 속성으로 사용할 수도 있습니다.

const moong2 = {
  name: "이근환"
  age: 29
};

위 코드를 아래처럼 쓸 수도 있습니다.

const name = "이근환";

const moong2 = {
  name,
  age: 29
};

객체의 속성과 값이 동일한 경우에는 이렇게 쓰면 됩니다.
단, 브라우저에서 지원이 안될수도 있으니 주의해야합니다.

let o = {
  a: a,
  b: b,
  c: c
};

// 이렇게 단축이 됩니다.
let o = { a, b, c };

또한 대괄호를 사용해서 다른 변수에 저장된 문자열을 그대로 속성의 이름으로 쓰는 것도 가능합니다.

const propName = "age";

const moong2 = {
  [propName]: 29
};

moong2.age;



점 표기법, 대괄호 표기법

속성 접근자(property accessor)를 이용해 이미 생성된 객체의 속성을 지정해줄 수도 있습니다.

const moong2 = {};

moong2.name = "이근환";
moong2.age = 29;
moong2.gender = "male";
moong2.computerLanguage = ["javascript", "java"];

객체 리터럴을 이용해 빈 객체를 생성한 후 점 표기법을 통해 속성을 갱신하였습니다.
그러나, JS 에서는 식별자로 허용되지 않는 문자가 들어간 속성 이름을 사용해야 하는 경우에 반드시 대괄호 표기법을 사용해야 합니다.

moong2.['재밌게 본 영화'] = "lala land";

위와 같은 경우가 아니라면 주로 점 표기법을 많이 사용합니다.



객체 다루기

속성 접근자, delete 연산자, in 연산자를 통해 객체에 대한 정보를 읽고 쓸수 있습니다.

const moong2 = {
  name: "이근환",
  age: 29,
  gender: "male"
};

// 속성 읽기
moong2.name; //  "이근환"
moong2.age; //  29

// 속성 쓰기 - 이전 속성에 있던 값이 갱신됩니다.
moong2.age = "마음만은 25세";

// 새 속성 추가하기
moong2.nickname = "뭉이";
moong2.favoriteGame = "Battle Ground";

// 속성 삭제하기
delete moong2.age;

// 속성이 객체에 있는지 확인하기 - 여부를 확인하여 있으면 true, 없으면 false 반환
"name" in moong2; //  true
"email" in moong2; //  false



메소드(Method)

객체의 속성값으로 함수를 지정할 수도 있습니다.

const moong2 = {
    react = function () {
        return 'smile'
    }
};

moong2.react(); //  'smile'

이처럼 어떤 객체의 속성으로 접근해서 사용하는 함수를 메소드라고 합니다.
객체 리터럴 안에서 특별한 표기법을 사용해 메소드를 정의할 수 있습니다.

const moong2 = {
  react() {
    return "smile";
  }
};

moong2.react(); //  'smile'

객체 속성 값으로 함수명을 지정하여 사용할 수도 있습니다.

function greet() {
  return "hello";
}

const person = {
  greet: greet
};

person.greet();

위의 코드는 아래와 같습니다.

let greet = function() {
  return "hello";
};

const person = {
  greet: greet
};

person.greet();



this

다른 함수들과 달리 메소드라는 특별한 이름을 사용하는 이유는 메소드가 다른 함수들과 다르게 특별취급되기 때문입니다.
this 를 사용하면 메소드 호출 시에 해당 메소드를 갖고 있는 객체에 접근할 수 있습니다.

const moong2 = {
  name: "이근환",
  age: 29,
  introduce() {
    return `안녕하세요 제 이름은 ${this.name}이에요! 나이는 ${
      this.age
    }살입니다.`;
  },
  recovery() {
    this.age--;
  }
};

moong2.introduce(); // '안녕하세요 제 이름은 이근환이에요! 나이는 29살입니다.'
moong2.recovery(); //  undefined
moong2.introduce(); // '안녕하세요 제 이름은 이근환이에요! 나이는 28살입니다.'

메소드를 사용하면, 데이터와, 그 데이터와 관련된 동작을 객체라는 하나의 단위로 묶어서 다룰 수 있습니다.
이 것이 함수대신 메소드를 사용하는 핵심적인 이유입니다.
다만 function 키워드를 통해 정의된 함수 내부의 this 키워드가 실제로 무엇을 가리킬 것인가는
메소드가 어떻게 정의되는가에 의해 결정되는 것이 아니라 어떻게 사용되는가에 의해 결정됩니다.

function thischeck() {
  return `이 this는 ${this.name}함수를 가리킵니다.`;
}

const func1 = {
  name: "func1",
  thischeck
};

const func2 = {
  name: "func2",
  thischeck
};

func1.thischeck();
func2.thischeck();

이렇게 thischeck 라는 함수가 객체 외부에서 정의되었고, func1 과 func2 에서 재사용되었는데 불구하고 메소드가 잘 동작했습니다.
즉, 같은 함수임에도 어떤 객체의 메소드로 사용되었느냐에 따라 내부의 this 가 가리키는 객체가 달라질 수 있다는 것입니다.
다만, 화살표 함수는 this 키워드를 전혀 다르게 취급하기 때문에 위와 같은 방식으로는 메소드로 사용될 수 없습니다.
또한, function 키워드를 통해 정의된 메소드가 항상 위와 같은 방식으로 this 를 취급하는 것은 아닙니다.
특별한 방법을 통해 아예 this 를 우리가 원하는 객체로 바꿔버릴 수 있습니다.

function favoriteFood() {
  console.log(`${this.name}님이 좋아하시는 음식은 ${this.menu}입니다.`);
}

const info = { name: "이근환", menu: "초밥" };
const favoriteFoodForMoong2 = favoriteFood.bind(info);

favoriteFoodForMoong2();

이렇게 우리가 원하는 값을 가리키게 만들려면 함수 객체의 bind, call, apply 메소드를 사용하면 됩니다.
함수 객체의 bind 메소드를 호출하면, 메소드의 인수로 넘겨준 값이 this 가 되는 새로운 함수를 반환합니다.
call 혹은 apply 메소드를 사용하면, 새로운 함수를 만들지 않고도 임시적으로 this 를 바꿔버릴 수 있습니다.
call 과 apply 는 인수를 넘겨주는 형식에 차이가 있을 뿐, 나머지 기능은 같습니다.

function favoriteFood(menu) {
  console.log(`${this.name}님이 좋아하시는 음식은 ${menu}입니다.`);
}

const info = { name: "이근환" };

favoriteFood.call(info, "초밥");
favoriteFood.apply(info, ["떡볶이"]);



프로토타입

JS 에서는 객체 간에 공유되어야 하는 속성과 메소드를 프로토타입이라는 기능을 이용해서 효율적으로 저장할 수 있습니다.
어떤 객체에 프로토타입을 지정하면, 프로토타입의 속성을 해당 객체에서 재사용할 수 있습니다.
객체의 프로토타입을 지정하는 방법에는 여러 가지가 있는데, 가장 쉬운 방법은 Object.create 함수를 이용하는 것입니다.

const lunchMenuPrototype = {
  suggest: function() {
    return `오늘 점심으로 ${this.menu} 어떠신가요?`;
  }
};

const day1 = Object.create(lunchMenuPrototype);
day1.menu = "짜장면";

const day2 = Object.create(lunchMenuPrototype);
day2.menu = "회덮밥";

day1.suggest();
day2.suggest();

day1.suggest === day2.suggest;

이렇게 프로토타입 기능을 이용해 한 객체에서 다른 객체의 기능을 가져와 사용하는 것을 프로토타입 상속이라고 합니다.
위의 경우를 lunchMenuPrototype은 day1의 프로토타입이다. 혹은 day1 객체는 lunchMenuPrototype 객체를 상속받았다고 표현합니다.
프로토타입 상속은 다른 언어에서는 흔히 찾아볼 수 없는 JS 의 특징적인 기능입니다.
단, 복수개 이상의 부모 프로토타입을 받을수 는 없습니다.
하지만 부모의 부모식으로 프로토타입을 받는 것은 가능합니다. (프로토타입 체인)

[ 만약 프로토타입을 안쓰고 그냥 배열에 객체를 반복 생성해서 사용한다면? ]


프로토타입을 안쓰고 배열에 객체를 반복 생성할 경우 메모리를 객체가 반복 생성되는 만큼 잡아먹게 될 뿐더러
배열,객체,함수 모두 새로생성된 것들은 안의 내용이 같을지라도 다른 것으로 판단됩니다.



프로토타입 읽고 쓰기

어떤 객체의 프로토타입을 읽어오기 위해 Object.getPrototypeOf 함수를 사용할 수 있습니다.
또한 Object.setPrototypeOf 함수를 통해 이미 생성된 객체의 프로토타입을 변경할 수 있습니다.
(Object.setPrototypeOf 함수는 null(null 도 객체이기 때문) 또는 또 다른 객체에 특정 객체의 프로토타입을 설정할 수 있게 해줍니다.)
하지만 객체가 생성된 이후에 프로토타입을 변경하는 작업은 굉장히 느리므로, Object.setPrototypeOf 함수의 사용은 피하는게 좋습니다.

const moong2 = {
  firstName: "lee"
};

const moong2child = Object.create(moong2);

Object.getPrototypeOf(moong2child) === moong2; //  true

const newMoong2 = {
  firstName: "bang"
};

Object.setPrototypeOf(moong2child, newMoong2);
Object.getPrototypeOf(moong2child) === moong2; // false (Object.getPrototypeOf(moong2child)는 {firstName: "bang"}이 되었기 때문입니다.)

객체 리터럴을 통해 생성된 객체의 프로토 타입에는 자동으로 Object.prototype이 지정됩니다.

const empty = {};
Object.getPrototypeOf(empty) === Object.prototype; //  true



프로토타입 체인(prototype chain)

const parent = {
  a: 1
};

const child = {
  b: 2
};

Object.setPrototypeOf(child, parent);
console.log(child); //  {b:2}
console.log(child.a); //  1

console.log(child)의 결과로 {a:1}이 나올줄 알았는데 결과는 {b:2}가 나왔습니다.
child 객체에 a 속성이 없는걸까 하고 child 객체의 a 속성을 출력해보면 1 이라는 결과가 나옵니다.
child.a 와 같이 JS 객체의 속성에 접근하면, JS 엔진은 child.a 의 속성만 확인하는 것이 아니라 프로토타입 객체의 속성까지 확인합니다.
그래서 프로토타입에 해당 이름을 갖는 속성이 있다면 그 속성의 값을 반환합니다.
만약 프로토타입의 객체에도 해당 이름의 속성이 없다면 프로토타입의 객체도 객체이므로 프로토타입의 프로토타입에 해당 이름의 속성이 있는지 확인합니다.
이렇게 계속 이어져 있는 프로토타입의 연쇄를 프로토타입 체인이라고 합니다.
이렇게 연쇄적으로 확인하여 최상위 프로토타입까지 확인하였을 떄 그때도 찾지 못한다면 그제서야 속성값으로 undefined 를 반환합니다.
JS 엔진은 속성 접근자를 통해 어떤 객체의 속성을 확인할 떄 프로토타입 체인을 전부 확인합니다.

const obj1 = {
  a: 1
};

const obj2 = {
  b: 2
};

const obj3 = {
  c: 3
};

// obj1을 obj2에게 상속, obj2를 obj3에게 상속
Object.setPrototypeOf(obj2, obj1);
Object.setPrototypeOf(obj3, obj2);

console.log(obj3.a); //  1 : obj3의 프로토타입(obj2)의 프로토타입(obj1)의 존재하는 속성 a의 값을 출력
console.log(obj3.b); //  2 : obj3의 프로토타입(obj2)의 존재하는 속성 b의 값을 출력
console.log(obj3.c); //  3 : obj3의 속성 c의 값을 출력

프로토타입 체인은 눈에 명확하게 보이지 않지만, 객체의 속성에 접근할 때마다 탐색됩니다.
따라서 프로토타입 체인의 깊이가 너무 깊으면 속성의 읽기 속도에 영향을 미치므로 주의해야 합니다.
어떤 객체가 다른 객체의 프로토타입으로 존재하는지 확인하기 위해서 Object.prototype.isPrototypeOf 메소드를 사용할 수 있습니다.

obj1.isPrototypeOf(obj3); //  true
obj2.isPrototypeOf(obj3); //  true



프로토타입 체인의 끝

속성에 접근할 때 더이상 프로토타입이 없을 떄까지 프로토타입 체인을 확인한다고 했는데 프로토타입이 더 이상 없다는 것이 무슨말일까요?
앞에서 말씀드린 것처럼 JS 에서는 객체의 프로토타입으로 객체 또는 null 이외의 값을 지정할 수 없습니다.
지정하려고 하면 에러가 발생하거나 무시됩니다.

Object.create(22); // Uncaught TypeError : Object prototype may only be an Object or null

그리고 Object.prototype 의 프로토타입을 확인해보면 null 이 나옵니다.

Object.getPrototypeOf(Object.prototype); //  null

결국, 프로토타입 체인을 따라가다 보면 언젠가는 null 을 만난다는 결론에 도달하게 됩니다.
프로토타입을 명시적으로 null 로 지정하지 않아도, 언젠가는 Object.prototype, 즉 프로토타입이 null 인 객체를 만나게 됩니다.
이 때에 프로토타입 체인을 확인하는 과정이 끝나는 것입니다.



프로토타입 체인 - 속성가리기 (property shadowing)

만약 프토토타입 체인에서 같은 이름의 속성이 여러 번 등장한다면 어떻게 될까요?

const parent = {
  a: 1
};

const child = {
  a: 3
};

Object.setPrototypeOf(child, parent);

child.a; //  3

parent 에 같은 이름의 속성이 있음에도 불구하고, child.a 에 접근하면 프로토타입 체인에서 가장 먼저 만나는 값이 불려와집니다.
이렇게 프로토타입 체인의 상위에 있는 속성이 하위 속성에 의해 가려지는 현상을 속성 가리기라고 합니다.

그렇다면 child 를 통해 parent 의 속성을 변경하거나 삭제할수 있을까요?( = 프로토타입의 객체 속성을 변경하거나 삭제할 수 있을까요?)

const parent = {
  do: "something"
};

const child = Object.create(parent);

delete child.do; // child.do 삭제
parent.do; //  'something'

child.do = "rest";
parent.do; //  'something'
child.do; //  'rest'

이처럼 어떤 객체의 속성을 변경하거나 속성을 삭제하는 작업은 그 객체의 프로토타입에 아무런 영향을 미치지 않습니다.

!! [[prototype]]을 끊으려면? !!
Object.setPrototypeOf(prototype 을 끊을 대상, null)이라고 하면 됩니다.



생성자 (constructor)

지금까지는 객체를 생성하기 위해 객채 리터럴({}) 또는 Object.create 함수를 사용했습니다.
하지만 이 것말고도 한 가지 방법이 더 있는데, 바로 new 키워드를 이용하는 것입니다.

const obj = new Object();

위 문장은 new 키워드가 붙었다는 것 말고는 함수 호출 문법과 비슷하게 생겼습니다.

typeof Object; //  function

사실 Object 는 함수입니다. 이렇게 객체를 만들 때, new 키워드와 함께 사용되는 함수를 가지고 생성자 혹은 생성자 함수라고 합니다.



생성자 정의하기

JS 에서는 Object 뿐만 아니라, 내장된 많은 생성자들이 있고, 심지어 직접 생성자를 만들 수도 있습니다.

// 생성자 정의
function Lunch(menu) {
  this.menu = menu;
}

// 생성자를 통한 객체 생성
const todaylunch = new Lunch("회덮밥");

function 구문을 통해 lunch 라는 생성자를 정의하고, 생성자 안에서 this 라는 키워드를 사용해서,
새로 만들어질 객체의 속성을 지정하였습니다. new 키워드를 이용해서 객체를 생성하는 순간에 생성자 안에
있는 코드가 실행되어 객체의 속성이 지정되는 것입니다.
생성자의 이름으로는 식별자로 사용할 수 있는 것이면 뭐든지 사용할 수 있지만,
변수와는 다르게 대문자로 시작하게끔 짓는 것이 널리 사용되는 관례입니다.

인스턴스

생성자를 통해 생성된 객체를 그 생성자의 인스턴스라고 합니다.
instanceof 연산자를 사용하면 객체가 특정 생성자의 인스턴스가 맞는지를 확인할 수 있습니다.

todaylunch instanceof Lunch; //  true

객체 리터럴을 통해 생성된 객체는 Object 의 인스턴스입니다.

const empty = {};
empty instanceof Object; //  true



셍성자와 프로토타입

생성자에 관련하여 알아야 할 것이 더 있는데 바로 생성자의 prototype 이라는 속성입니다.
생성자를 통해 만들어낸 객체의 프로토타입에는 생성자의 prototype 속성에 저장되어 있는 객체가 자동으로 지정됩니다.

Object.getPrototypeOf(todaylunch) === Lunch.prototype; //  true

Lunch.prototype 에 객체를 정한 적이 없고 심지어 Lunch 는 함수인데도 불구하고 속성을 갖고 있는데 이유는
앞에서도 공부했듯이 먼저 JS 에서는 함수도 특별한 형태의 객체이기 때문입니다.
그리고, JS 에서는 function 구문을 통해 함수를 정의할 때 함수의 prototype 속성에 객체가
자동으로 생성되어 저장됩니다.
prototype 은 constructor 가 가지는 속성이며, 함수객체만 이 속성을 가지고 있습니다. (여기서 말하는 프로토타입은 proto와 다른 prototype property 를 의미하는 것입니다.)

function Lunch() {
  // blah blah code~~
}

typeof Lunch.prototype; // 'object'



생성자 - constructor

생성자의 prototype 속성에 자동 생성되는 객체에는 constructor 라는 특별한 속성이 들어있습니다.
이 속성에는 생성자 자신이 저장됩니다.

function Lunch() {
  // blah blah code~~`
}

Lunch.prototype.constructor === Lunch; //  true

이를 통해, 어떤 객체가 어떤 생성자로부터 생성되었는지를 constructor 속성을 통해 알아낼 수 있습니다.

function Lunch() {
  // blah blah code~~`
}

const todayLunch = new Lunch();
todayLunch.constructor === Lunch; //  true
// 점심메뉴를 알려주는 객체를 생성하는 함수
function Lunch(menu) {
  this.menu = menu; // 개별적으로 저장되어야 하는 것은 생성자를 통해 저장하고
}

Lunch.prototype.select = function() {
  return `오늘 점심 메뉴로 ${this.menu} 어떠신가요?`; // 공유되어야하는 동작은 프로토타입 속성에 저장합니다.
};

const todayLunch = new Lunch("회덮밥");

todayLunch.select(); // '오늘 점심 메뉴로 회덮밥 어떠신가요?'



정적 메소드 (static method)

생성자의 속성에 직접 지정된 메소드를 가지고 정적 메소드라고 합니다.
Number.isNaN, Object.getPropertyOf 등의 함수들은 모두 정적 메소드입니다.
정적 메소드는 특정 인스턴스에 대한 작업이 아니라, 해당 생성자와 관련된 일반적인 작업을 정의하고 싶을 때 사용됩니다.

function Age(age) {
  this.age = age;
}

const moong2 = new Age(29);
const min_ji = new Age(24);

// 생성자의 속성에 함수를 직접 할당합니다.
Age.compare = function(person1, person2) {
  if (person1.age > person2.age) {
    return `첫번째 사람의 나이가 ${person1.age - person2.age}살 더 많습니다.`;
  } else if (person1.age < person2.age) {
    return `두번째 사람의 나이가 ${person2.age - person1.age}살 더 많습니다.`;
  } else {
    return `두 사람의 나이가 같습니다.`;
  }
};

Age.compare(moong2, min_ji);

2. Today I Found Out

객체에 대해서 너무 쉽게만 알고 있었는데 자세히 공부를 하니 몰랐던 부분이 있어서
흥미롭게 살펴볼 수 있었습니다. 프로토타입에 대해서는 자바스크립트 관련 서적을 볼 때
굉장히 어렵게 느껴져서 잘 이해하지 못한채 넘어갔는데 오늘 승하쌤의
hellojavascriptworld를 통해서 설명을 보고 스스로 예시를 들어보니
이해를 할 수 있었습니다. 수업시간에 강의를 통해서 이제 이해했던 것을
확인하고 복습을 하면 충분히 제 것으로 만들 수 있을 것 같습니다.



3. refer

https://helloworldjavascript.net/pages/180-object.html?q=

http://insanehong.kr/post/javascript-object/

https://developer.mozilla.org/ko/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript

https://wayhome25.github.io/javascript/2017/02/19/js-oop-2/