객체 프로토타입, 객체 순회와 관련한 몇 가지 메소드 회고

MaxlChan·2020년 9월 6일
2

객체 프로토타입과 관련하여 정리해두면 좋을 것 같은 연관(내가 생각하기에)
메소드 세가지를 간단히 정리하고자 한다.
🚨 올바르지 않은 내용이 있을 경우 댓글로 남겨주시면 감사드리겠습니다.

Object.create()

function Location(country, latitude, longitude) {
  this.country = country;
  this.latitude = latitude;
  this.longitude = longitude;
}

Location.prototype.callMyLatitude = function() {
  console.log(`My latitude is ${this.latitude}`);
}

const southKorea = new Location("SouthKorea", 33, 124); 

southKorea.callMyLatitude();  // My latitude is 33

예전에 다루었듯이

생성자 함수의 인스턴스 객체가 생성자 함수의 prototype 속성(객체)을 상속받는 방법으로

new 키워드를 통한 인스턴스 생성이 있었다.

그런데 new 키워드를 사용하지 않고, 객체의 프로토타입을 상속받는 방법이 존재하는데
그것이 Object.create()이다.

Object.create() 메서드는 지정된 프로토타입 객체 및 속성(property)을 갖는 새 객체를 만듭니다. 출처 MDN

역시 무슨 말인지 이해가 가지않는다.
구문을 살펴보고 정리해보자.

구문
Object.create(proto[, propertiesObject])
proto - 새로 만든 객체의 프로토타입이어야 할 객체.

정리하자면 해당 메소드는 객체를 반환하는데,
첫번째 인자로 들어온 객체를 프로토타입으로 상속받은 채로 객체를 반환
한다
는 것이다.

그럼 맨 처음 소개한 코드를 Object.create()를 바꾸어보자.

function Location(country, latitude, longitude) {
  const locaction = Object.create(locactionMethods);
  locaction.country = country;
  locaction.latitude = latitude;
  locaction.longitude = longitude;
  
  return locaction;
}

const locactionMethods = {};

locactionMethods.callMyLatitude = function() {
  console.log(`My latitude is ${this.latitude}`);
}

const southKorea = Location("SouthKorea", 33, 124); 

southKorea.callMyLatitude();  // My latitude is 33

Location 함수 자체의 걸려있는 프로토타입 속성은 없지만
내부에서 반환하는 새로운 객체가 Object.create()메소드를 통해 생성되었고,
locactionMethods를 프로토타입으로 가지고 있다.

따라서 함수 실행을 통해 생성된 변수 southKorea는 아래와 같은 형태를 가진다.

Object.create()는 상위클래스의 멤버나 메소드를 하위클래스에게 상속시켜주기 위해 쓰인다.

아래 예시는 MDN의 예시를 약간 변형하였다.

// 상위 생성자 함수
function Animal(name, mainFood) {
  this.name = name;
  this.mainFood = mainFood;
}

// 상위 생성자 함수 프로토타입 메서드
Animal.prototype.eat = function() {
  console.info('Yummy');
};

Animal.prototype.sleep = function() {
  console.info('zzZ');
};

// 하위 생성자 함수
function Rabbit(name, mainFood, speed) {
  Animal.call(this, name, mainFood); // 상위 생성자 함수 호출.
  this.speed = speed;
}

const firstRabbit = new Rabbit("Que", "grass", "fast");
firstRabbit.eat();
//Uncaught TypeError: firstRabbit.eat is not a function at <anonymous>:1:13

// 상위 생성자 함수 프로토타입을 하위 생성자 함수의 프로토타입으로 할당
Rabbit.prototype = Object.create(Animal.prototype);
Rabbit.prototype.constructor = Rabbit;

const secondRabbit = new Rabbit("Que", "grass", "fast");
secondRabbit.eat(); // Yummy
secondRabbit.sleep();  //zzZ 

우선 Animal.call을 호출함으로서
firstRabbit 인스턴스는 상위 생성자 함수의 프로토타입을 상속받지 못한 반면
secondRabbit은 상속을 받아 eat, sleep를 프로토타입 체인을 타고 올라가 호출할 수 있게되었다.

constructor를 다시 재설정해준 이유는 Object.create(Animal.prototype)을 통해
기존의 Rabbit.prototype정보가 아예 덮어진다.
따라서 Rabbit.prototype.constructor를 재할당 해줌으로서 해당 인스턴스의
constructor가 무엇인지 명확하게 하기 위해서이다.

참고로 MDN에서는 위와 같은 이유로,
객체의 constructor 속성에 의존하는 게 항상 안전하지는 않음을 보인다. 라고 말한다.

Object.prototype.hasOwnProperty()

모든 객체는 hasOwnProperty 를 상속하는 Object의 자식이다.
이 메소드는 객체가 특정 프로퍼티를 자기만의 직접적인 프로퍼티로서 소유하고 있는지를
판단하는데 사용된다.

in 연산과는 다르게, 이 메소드는 객체의 프로토타입 체인을 확인하지는 않는다. -MDN

앞서 프로토타입과 관련된 이슈이다.
만약 위의 예시에서
secondRabbit의 속성들(name, speed, mainFood)만을 순회하고 싶은 이유로 for...in을 실행시켜보자.

for (let prop in secondRabbit) { 
  console.log(prop);
}
// name
// mainFood
// speed
// constructor
// eat
// sleep

위 처럼 아무런 필터링 없이 순회를 하게 되면 해당 객체가 상속받고 있는 속성들까지 전부 순회하게 되어 예기치 못한 오류가 발생할 여지가 발생하게 된다.

이를 방지하기 위해서 hasOwnProperty()를 활용하면 메소드의 뜻 그대로 본인이 가지고 있는 고유 속성에 대해서만 필터링을 해주게 된다.

for (let prop in secondRabbit) {
  if (secondRabbit.hasOwnProperty(prop)) {
    console.log(prop);
  }
}
// name
// mainFood
// speed

이걸 대체 왜쓰나 싶어서 그전에는,
if (prop in obj), if (obj[prop]) 이런식으로 필터링을 했었는데,
부트캠프 알고리즘 챌린지 때 필요성을 뼈저리게 느끼게 되었다..

엄밀히 말하면 hasOwnProperty() 를 사용하지 않았다고 해서 에러가 발생하지는 않지만,
객체와 객체 프로토타입 체인의 내용을 보장할 수 없다면, hasOwnProperty() 확인을 추가하는 편이 안전하다.

Object.defineProperty()

Object.defineProperty()를 통해 객체의 세부속성(writable, configurable, enumerable 상세하게 조절할 수 있는데, 그 중 enumerablefalse로 지정하면 위와 같은 예기치 못한 순회를 방지할 수 있다.

할당을 통해 속성을 추가하는 일반적인 방법을 사용하면 속성 열거enumeration(for...in 반복문이나 Object.keys 메서드)를 통해 노출되어 값을 변경하거나 delete 연산자로 삭제할 수 있습니다.
defineProperty를 사용하면 이런 부분을 상세하게 조절할 수 있습니다. - MDN

Object.prototype.add = function () { console.log("add!"); }
// 실제로 객체 프로토타입에 직접 메소드를 추가하는 것은 신중하게 해야한다.

// 중간에 코드 천줄이 있다고 가정 . . . 

const person = {name: "chan", age: 18};

for (let prop in person) {
  console.log(person[prop]); // 차후에 어떤 동료 사용자가 객체를 순회하려고 함.
}

// chan
// 18
// ƒ () { console.log("add!") } 의도치 않은 함수 발견!

위 코드를 Object.defineProperty()를 통해 Object.prototype.add가 순회되지 못하도록 변경해보았다.

Object.defineProperty(Object.prototype, 'add', {
  enumerable: false,
  value: function () { console.log("add!"); }
});

const person = {name: "chan", age: 18};

for (let prop in person) {
  console.log(person[prop]); // 차후에 어떤 동료 사용자가 객체를 순회하려고 함.
}

// chan
// 18

위와 같이 애초에 해당 메소드가 프로토타입 체인을 타고 올라가 순회되지 못하게
enumerable속성의 값을 false 바꿔주면 된다.

해당 메소드의 자세한 내용을 모두 다루기에는 너무 길어 질 수 있기 때문에,
객체 순회 및 프로토타입과 관련된 부분만 간단히 다루어보았다.
상세한 메소드의 내용들은 모두 MDN에 있다. MDN은 교과서이다...

참고

profile
한가지를 알아도 제대로 알자

2개의 댓글

comment-user-thumbnail
2020년 9월 8일

캬..

답글 달기
comment-user-thumbnail
2020년 9월 8일

저도 알고챌린지때 in 썼다가 hasOwnProperty로 고쳤네요..하핫

답글 달기