FEDev Story

프로토타입 오염을 막기 위해 hasOwnProperty를 사용해라 본문

Javascript/★★★

프로토타입 오염을 막기 위해 hasOwnProperty를 사용해라

지구별72 2016. 10. 13. 23:32

프로토타입 오염

모든 객체는 그 prototype 객체의 프로퍼티들을 상속하고 for...in 반복문은 객체의 상속된 프로퍼티 또한 자신이 '소유한' 프로퍼티로 열거한다. 다음 코드와 같이 요소를 객체 자신의 프로퍼티로 저장하는 사용자 정의 생성자와 Array 타입을 사용하여 객체를 생성하게 되면 프로토타입을 오염시킬 수 있다.
function NaiveDict() { }
NaiveDict.prototype.count = function() {
   var i = 0;
   for (var name in this) { // counts every property
      i++;
   }
   return i;
};
NaiveDict.prototype.toString = function() {
   return "[object NaiveDict]";
};
var dict = new NaiveDict();
dict.alice = 34;
dict.bob = 24;
dict.chris = 62;
dict.count(); // 5
위 코드는 count가 객체의 프로퍼티를 열거할 때, dict 인스턴스에 추가된 프로퍼티(alice, bob, chris)뿐만 아니라 NaiveDict.prototype에 추가된 프로퍼티(count, toString)들의 수까지 세개된다.
Array.prototype.first = function() {
   return this[0];
};
Array.prototype.last = function() {
   return this[this.length – 1];
};

var dict = new Array();
dict.alice = 34;
dict.bob = 24;
dict.chris = 62;
dict.bob; // 24

var names = [];
for (var name in dict) {
names.push(name);
}

names; // ["alice", "bob", "chris", "first", "last"]

위 코드는 편의를 위해서 Array.prototype에 추가된 메서드로 인해서 객체의 항목을 열거할 때 프로토타입 객체의 프로퍼티가 예상치 않게 나타나게 된다.

하지만 new Array()를 단순히 빈 객체 리터럴로 교체하면 프로토타입 오염에 훨씬 덜 민간해 진다.

var dict = {};
dict.alice = 34;
dict.bob = 24;
dict.chris = 62;
var names = [];
for (var name in dict) {
   names.push(name);
}
names; // ["alice", "bob", "chris"]

가벼운 객체를 만들기 위해 객체 리터럴을 사용해야 하며 for...in 반복문을 오염시킬 수 있기 때문에, 절대 Object.prototype에 프로퍼티를 추가하지 말아야 한다.

프로토타입 오염을 막기 위한 null 프로토타입

ES5 이전에는 프로토타입이 비어있는 새로운 객체를 만드는 표준적인 방법이 없다.

function C() 
C.prototype = null;   

하지만 이 생성자로 인스턴스를 만들면 여전히 Object의 인스턴스를 갖게 된다.

var o = new C();
Object.getPrototypeOf(o) === null;  // false
Object.getPrototypeOf(o) === Object.prototype;   // true

하지만 Object.create 함수는 단순히 null 프로토타입 인자를 전달하면, 다음과 같이 진짜로 비어있는 객체를 만들 수 있다.

var x = Object.create(null);
Object.getPrototypeOf(x) === null;   // true
이런 객체에는 프로토타입 오염이 어떠한 영향도 미칠 수 없을 것이다.

Object.create를 지원하지 않는 오래된 자바스크립트 환경에서는 객체 리터벌을 사용하여 새로운 객체의 프로토타입 연결을 null로 초기화할 수 있다.

var x = { __proto__: null };
x instanceof Object;   // false (비표준)

이 문법은 동등하지만, Object.create를 사용하는 것이 더 신뢰할 만 하다.

프로토타입 오염을 막기 위한 hasOwnProperty

빈 객체 리터럴조차도 Object.prototype의 수많은 프로퍼티들을 상속한다.

var dic = {};
"alice" in dict;   //false
"toString" in dict;    //true
"valueOf" in dict;    //true

hasOwnProperty 메서드를 사용하면 프로토타입 오염을 피할 수 있게 도와준다.

dic.hasOwnProperty("alice");   // false
dic.hasOwnProperty("toString");   // false
dic.hasOwnProperty("valueOf");   // false

dic.hasOwnProperty("alice") ? dict.alice : undefined;
dic.hasOwnProperty(x) ? dic[x] : undefined;

hasOwnProperty는 Object.prototype에서 상속된다. 하지만 dict 객체에 'hasOwnProperty'라는 이름의 항목을 저장한다면, 프로토타입의 메서드에는 더 이상 접근할 수 없다.

가장 안전한 방법은 hasOwnProperty를 dict의 메서드로써 호출하는 대신에 call 메서드를 사용하는 것이다.

var hasOwn = Object.prototype.hasOwnProperty;
혹은
var hasOwn = {}.hasOwnProperty;
hasOwn.call(dict, "alice");

이 방법은 수신자 객체의 hasOwnProperty 메서드가 오버라이딩되었는지와 상관없이 잘 작동한다.

다음과 같이 이 패턴을 Dict 생성자에 추상화시킬 수 있다.

function Dict (elements) {
   this.elements = elements || {};
}
Dict.prototype.has = function(key){
   //자신이 소유한 프로퍼티만
   return {}.hasOwnProperty.call(this.elements, key);
};
Dict.prototype.get = function(key){
   // 자신이 소유한 프로퍼티만
   return this.has(key) ? this.elements[key] : undefined;
};
Dict.prototype.set = function(key, val){
   this.elements[key] = val;
};
Dict.prototype.remove = function(key){
   delete this.elements[key];
};
var dict = new Dict({
   alice : 34,
   bob : 24,
   chris : 62
});
dict.has("alice");   //true
dict.get("bob");    //24
dcit.has("valueOf");    // false

몇몇 실행환경에서 특수한 프로퍼티 __proto__는 스스로의 오염 문제를 일으킬 수 있다. 어떤 실행환경에서 __proto__ 프로퍼티는 단순히 Object.prototype에서 상속되므로, 빈 객체는 (다행히도) 진짜로 비어 있다.

var empty = Object.create(null);
"__proto__" in empty;
// false (몇몇 실행환경에서)
var hasOwn = {}.hasOwnProperty;
hasOwn.call(empty, "__proto__");
// false (몇몇 실행환경에서)

다른 실행환경에서는 in 연산자만 true를 보이기도 한다.

var empty = Object.create(null);
"__proto__" in empty;  // true (몇몇 실행환경에서)
var hasOwn = {}.hasOwnProperty;
hasOwn.call(empty, "__proto__");   // false (몇몇 실행환경에서)

또 다른 실행환경에서는 __proto__라는 인스턴스 프로퍼티가 있으면 영원히 모든 객체를 오염시키기도 한다.

var empty = Object.create(null);
"__proto__" in empty;  // true (몇몇 실행환경에서)
var hasOwn = {}.hasOwnProperty;
hasOwn.call(empty, "__proto__");   // true (몇몇 실행환경에서)

즉 실행환경에 따라 다음 코드는 서로 다른 결과를 보일 수 있다.

var dict = new Dict();
dict.has("__proto__");   // ?

최대의 이식성과 안전을 위해, 모든 Dict 메서드에 "__proto__" 키를 위한 특별한 로직을 추가한다. 더 복잡하지만 안전한 구현을 보장한다.

function Dict (elements) {
   this.elements = elements || {};
   this.hasSpecialProto = false;    // "__proto__"키를 가지는가?
   this.specialProto = undefined;   // "__proto__" 엘리먼트
}
Dict.prototype.has = function(key){
   if (key === "__proto__") {
      return this.hasSpecialProto;
   }
   //자신이 소유한 프로퍼티만
   return {}.hasOwnProperty.call(this.elements, key);
};
Dict.prototype.get = function(key){
   if (key === "__proto__") {
      return specialProto;
   }
   // 자신이 소유한 프로퍼티만
   return this.has(key) ? this.elements[key] : undefined;
};
Dict.prototype.set = function(key, val){
   if (key === "__proto__"){
      this.hasSpecialProto = true;
      this.specialProto = val;
   } else {
      this.elements[key] = val;
   }
};
Dict.prototype.remove = function(key){
   if (key === "__proto__"){
      this.hasSpecialProto = false;
      this.specialProto = undefined;
   } else {
      delete this.elements[key];
   }
};

이 구현은 실행환경에서 __proto__를 어떤 식으로 처리하더라도 동작을 보장한다. __proto__라는 이름을 가지는 프로퍼티를 처리하지 않도록 피하기 때문이다.

var dict = new Dict();
dict.has("__proto__");   // false


Comments