기본 객체를 확장하는 것이 왜 나쁜 습관입니까?


136

모든 JS 의견 리더는 기본 객체를 확장하는 것은 나쁜 습관이라고 말합니다. 그런데 왜? 성능에 영향을 받습니까? 그들은 누군가가 "잘못된 길"을 두려워하고 열거 가능한 유형을 추가하여 Object실제로 모든 객체의 모든 루프를 파괴합니까?

가지고 TJ Holowaychukshould.js을 예를 들어. 그는 간단한 getter추가Object 하고 모든 것이 잘 작동합니다 ( source ).

Object.defineProperty(Object.prototype, 'should', {
  set: function(){},
  get: function(){
    return new Assertion(Object(this).valueOf());
  },
  configurable: true
});

이것은 실제로 의미가 있습니다. 예를 들어 하나를 확장 할 수 있습니다 Array.

Array.defineProperty(Array.prototype, "remove", {
  set: function(){},
  get: function(){
    return removeArrayElement.bind(this);
  }
});
var arr = [0, 1, 2, 3, 4];
arr.remove(3);

네이티브 형식 확장에 대한 논쟁이 있습니까?


9
나중에 고유 한 의미가 다른 "제거"기능을 포함하도록 기본 개체를 변경하면 어떻게 될까요? 당신은 표준을 통제하지 않습니다.
ta.speot.is는

1
기본 유형이 아닙니다. 모두의 기본 유형입니다.

6
"누군가가"잘못된 길 "을 두려워하고 열거 가능한 유형을 Object에 추가하여 실제로 모든 객체의 모든 루프를 파괴합니까?" 네 이 의견이 형성되던 시절에는 열거 할 수없는 속성을 만들 수 없었습니다. 이제 이런 점에서 상황이 다를 수 있지만 모든 라이브러리가 네이티브 객체를 원하는 방식으로 확장한다고 상상해보십시오. 네임 스페이스 사용을 시작한 이유가 있습니다.
Felix Kling

5
예를 들어 Brendan Eich와 같은 "의견 리더"의 가치는 네이티브 프로토 타입을 확장하는 것이 좋습니다.
Benjamin Gruenbaum

2
Foo는 요즘 글로벌이 아니어야합니다. requirejs, commonjs 등이 포함되어 있습니다.
Jamie Pate

답변:


127

객체를 확장하면 객체의 동작이 변경됩니다.

자신의 코드에서만 사용할 객체의 동작을 변경하는 것이 좋습니다. 그러나 다른 코드에서도 사용되는 동작을 변경하면 다른 코드가 손상 될 위험이 있습니다.

javascript에서 객체 및 배열 클래스에 메소드를 추가 할 때 javascript 작동 방식으로 인해 무언가를 깨뜨릴 위험이 매우 높습니다. 오랜 경험을 통해 이런 종류의 것들이 자바 스크립트에서 모든 종류의 끔찍한 버그를 일으킨다는 것을 알게되었습니다.

사용자 정의 동작이 필요한 경우 기본 클래스를 변경하는 대신 자체 클래스 (아마도 서브 클래스)를 정의하는 것이 훨씬 좋습니다. 그렇게하면 아무것도 깨지 않을 것입니다.

클래스를 서브 클래 싱하지 않고 클래스의 작동 방식을 변경하는 기능은 좋은 프로그래밍 언어의 중요한 기능이지만 거의 사용하지 않고주의해서 사용해야합니다.


2
그래서 같은 .stgringify()것을 추가하는 것이 안전하다고 간주됩니까?
buschtoens가

7
예를 들어, 다른 코드 stringify()가 다른 행동으로 자신의 방법 을 추가하려고하면 어떻게됩니까 ? 일상적인 프로그래밍에서해야 할 일이 아닙니다 ... 원하는 몇 가지 코드를 여기 저기에 저장하는 것이 아닙니다. 입력을 받아들이고 문자열 화하는 자신의 클래스 또는 함수를 정의하는 것이 좋습니다. 대부분의 브라우저는를 정의 JSON.stringify()하며, 존재하는지 확인하고 직접 정의하지 않는 것이 가장 좋습니다.
Abhi Beckert

5
나는 someError.stringify()이상을 선호 합니다 errors.stringify(someError). 간단하고 js의 개념에 완벽하게 적합합니다. 특정 ErrorObject에 특별히 바인딩 된 작업을 수행하고 있습니다.
buschtoens

1
남아있는 유일한 (그러나 좋은) 주장은 일부 거대한 매크로 라이브러리가 유형을 인수하기 시작하고 서로 방해 할 수 있다는 것입니다. 아니면 다른 것이 있습니까?
buschtoens

4
문제가 충돌이 발생할 수있는 경우이를 수행 할 때마다 패턴을 사용할 수 있습니까? 정의하기 전에 항상 함수가 정의되어 있지 않은지 확인합니까? 또한 항상 자신의 코드 앞에 타사 라이브러리를 배치하면 이러한 충돌을 항상 감지 할 수 있어야합니다.
Dave Cousineau

32

성능 저하와 같이 측정 가능한 단점은 없습니다. 적어도 아무도 언급하지 않았습니다. 이것이 개인적인 취향과 경험의 문제입니다.

주요 프로 논쟁 : 더보기 좋고 직관적입니다 : 구문 설탕. 유형 / 인스턴스 별 기능이므로 해당 유형 / 인스턴스에 특별히 바인딩되어야합니다.

주요 반대 주장 : 코드가 간섭 할 수 있습니다. lib A가 함수를 추가하면 lib B의 함수를 덮어 쓸 수 있습니다. 코드가 매우 쉽게 깨질 수 있습니다.

둘 다 요점이 있습니다. 유형을 직접 변경하는 두 개의 라이브러리에 의존하는 경우 예상되는 기능이 동일하지 않기 때문에 코드가 깨질 가능성이 큽니다. 나는 그것에 동의합니다. 매크로 라이브러리는 기본 유형을 조작해서는 안됩니다. 그렇지 않으면 개발자로서 당신은 실제로 무대 뒤에서 무슨 일이 일어나고 있는지 알 수 없습니다.

그리고 이것이 jQuery, 밑줄 등과 같은 라이브러리를 싫어하는 이유입니다. 그들은 절대적으로 잘 프로그램되어 있고 매력처럼 작동하지만 그들은 크다 . 10 % 만 사용하고 약 1 %를 이해하십시오.

그렇기 때문에 본인 이 실제로 필요한 것만 필요한 원자 적 접근 방식을 선호 합니다. 이런 식으로, 당신은 항상 무슨 일이 일어나는지 알고 있습니다. 마이크로 라이브러리는 원하는 작업 만 수행하므로 간섭하지 않습니다. 어떤 기능이 추가되는지 최종 사용자에게 알리는 기본 유형 확장은 안전하다고 간주 될 수 있습니다.

TL; DR 의심스러운 경우 기본 유형을 확장하지 마십시오. 최종 사용자가 해당 동작을 알고 원하는 경우 100 % 확신 할 경우에만 기본 유형을 확장하십시오. 에서 는 기존의 인터페이스를 깰 것 같은 경우, 기본 유형의 기존 기능을 조작 할 수 있습니다.

유형을 확장하기로 결정한 경우 Object.defineProperty(obj, prop, desc) ; 할 수 없으면 type의을 사용하십시오 prototype.


ErrorJSON을 통해 보낼 수 있기를 원했기 때문에 원래이 질문을했습니다 . 그래서 나는 그것들을 묶는 방법이 필요했습니다. error.stringify()보다 나은 느낌 errorlib.stringify(error); 두 번째 구조에서 알 수 있듯이, 나는 스스로 작동 errorlib하지 않고 작동 하고 error있습니다.


나는 이것에 대한 추가 의견에 열려 있습니다.
buschtoens

4
jQuery와 밑줄이 기본 객체를 확장한다는 것을 의미합니까? 그들은하지 않습니다. 따라서 그러한 이유로 피하지 않으면 실수 한 것입니다.
JLRishe

1
다음은 lib A로 네이티브 객체를 확장하면 lib B와 충돌 할 수 있다는 주장에서 누락 된 것으로 생각되는 질문입니다. 왜 자연적으로 너무 유사하거나 자연적으로 광범위 하여이 충돌이 발생할 수있는 두 개의 라이브러리가 있습니까? 즉, lodash 또는 밑줄을 모두 선택하지 마십시오. 자바 스크립트의 현재 libraray 환경은 지나치게 포화되어 있으며 개발자 (일반적으로)가 너무 부주의 해져서 Lib Lord를 달래기위한 모범 사례를 피하게됩니다.
micahblu

2
@micahblu-라이브러리는 자체 내부 프로그래밍의 편의를 위해 표준 객체를 수정하기로 결정할 수 있습니다. 따라서 동일한 기능을 가진 두 개의 라이브러리를 사용하지 않기 때문에 이러한 충돌을 피할 수 없습니다.
jfriend00

1
또한 (es2015 기준) 기존 클래스를 확장하는 새 클래스를 만들어 자신의 코드에서 사용할 수 있습니다. 다른 서브 클래스와 충돌하지 않고 메소드를 MyError extends Error가질 수 있다면 stringify. 자신의 코드에서 생성되지 않은 오류를 여전히 처리해야하므로 다른 코드보다 유용하지 않을 수 있습니다 Error.
ShadSterling

16

제 생각에는 나쁜 습관입니다. 주된 이유는 통합입니다. 인용 해야하는 should.js 문서 :

OMG IT는 개체를 확장합니까 ???!?! @ 예, 그렇습니다. 단일 getter를 사용하면 코드가 중단되지 않습니다.

글쎄, 저자는 어떻게 알 수 있습니까? 내 조롱 프레임 워크가 동일한 경우 어떻게합니까? 내 약속 lib가 동일하게 수행하면 어떻게됩니까?

당신이 당신의 자신의 프로젝트에서하고 있다면 괜찮습니다. 그러나 라이브러리의 경우 나쁜 디자인입니다. Underscore.js는 올바른 방법으로 수행 된 작업의 예입니다.

var arr = [];
_(arr).flatten()
// or: _.flatten(arr)
// NOT: arr.flatten()

2
TJ의 답변은 그러한 약속이나 조롱 프레임 워크를 사용하지 않을 것이라고 확신합니다. x
Jim Schubert

_(arr).flatten()예제는 실제로 기본 객체를 확장하지 않도록 설득했습니다. 그렇게하는 개인적인 이유는 순전히 구문 적이었습니다. 그러나 이것은 내 미적 느낌을 만족시킵니다 foo(native).coolStuff(). 감사합니다!
egst

16

사례별로 살펴보면 일부 구현이 허용 될 수 있습니다.

String.prototype.slice = function slice( me ){
  return me;
}; // Definite risk.

이미 작성된 메소드를 겹쳐 쓰면 해결하는 것보다 더 많은 문제가 발생하므로이 방법을 피하기 위해 많은 프로그래밍 언어에서 일반적으로 언급됩니다. 개발자는 기능이 변경되었음을 어떻게 알 수 있습니까?

String.prototype.capitalize = function capitalize(){
  return this.charAt(0).toUpperCase() + this.slice(1);
}; // A little less risk.

이 경우 알려진 핵심 JS 메소드를 덮어 쓰지 않지만 String을 확장합니다. 이 게시물의 한 주장은 새로운 개발자 가이 방법이 핵심 JS의 일부인지 또는 문서를 찾을 수 있는지 아는 방법을 언급했습니다. 핵심 JS String 객체가 capitalize 라는 메소드를 가져 오면 어떻게 될까요? 될까요?

다른 라이브러리와 충돌 할 수있는 이름을 추가하는 대신 모든 개발자가 이해할 수있는 회사 / 앱 특정 수정자를 사용하면 어떻게됩니까?

String.prototype.weCapitalize = function weCapitalize(){
  return this.charAt(0).toUpperCase() + this.slice(1);
}; // marginal risk.

var myString = "hello to you.";
myString.weCapitalize();
// => Hello to you.

다른 객체를 계속 확장하면 모든 개발자가 (이 경우) 우리 와 야생에서 만나게됩니다. 회사 / 앱 특정 확장 프로그램임을 알립니다.

이름 충돌을 제거하지는 않지만 가능성을 줄입니다. 핵심 JS 객체를 확장하는 것이 본인 및 / 또는 팀을위한 것이라고 판단되면 아마도 이것이 당신을위한 것입니다.


7

내장 프로토 타입을 확장하는 것은 실제로 나쁜 생각입니다.그러나 ES2015에는 원하는 동작을 얻는 데 사용할 수있는 새로운 기술이 도입되었습니다.

활용 WeakMap 내장의 프로토 타입과 연관 유형들

다음 구현은 프로토 타입 NumberArray프로토 타입을 전혀 건드리지 않고 확장합니다 .

// new types

const AddMonoid = {
  empty: () => 0,
  concat: (x, y) => x + y,
};

const ArrayMonoid = {
  empty: () => [],
  concat: (acc, x) => acc.concat(x),
};

const ArrayFold = {
  reduce: xs => xs.reduce(
   type(xs[0]).monoid.concat,
   type(xs[0]).monoid.empty()
)};


// the WeakMap that associates types to prototpyes

types = new WeakMap();

types.set(Number.prototype, {
  monoid: AddMonoid
});

types.set(Array.prototype, {
  monoid: ArrayMonoid,
  fold: ArrayFold
});


// auxiliary helpers to apply functions of the extended prototypes

const genericType = map => o => map.get(o.constructor.prototype);
const type = genericType(types);


// mock data

xs = [1,2,3,4,5];
ys = [[1],[2],[3],[4],[5]];


// and run

console.log("reducing an Array of Numbers:", ArrayFold.reduce(xs) );
console.log("reducing an Array of Arrays:", ArrayFold.reduce(ys) );
console.log("built-ins are unmodified:", Array.prototype.empty);

보시다시피이 기술로 원시 프로토 타입까지 확장 할 수 있습니다. 지도 구조를 사용하고Object ID를 유형을 내장 프로토 타입과 연결합니다.

내 예제는 빈 인수를 만드는 방법과이 자체와 요소를 배열 자체의 요소에서 연결하는 방법에 대한 정보를 추출 할 수 있기 때문에 단일 인수 reduce로만 필요한 함수를 활성화 Array합니다.

Map약한 참조는 가비지 수집되지 않는 내장 프로토 타입을 나타낼 때 의미가 없으므로 일반 유형 을 사용할 수 있습니다 . 그러나 a WeakMap는 반복 할 수 없으며 올바른 키가 없으면 검사 할 수 없습니다. 이것은 모든 유형의 반사를 피하고 싶기 때문에 원하는 기능입니다.


WeakMap의 멋진 사용법입니다. xs[0]빈 배열에 대해서는 조심하십시오 . type(undefined).monoid ...
감사합니다.

@naomik 알고 있습니다-최신 질문 2 번째 코드 조각을보십시오.

이 @ftor에 대한 표준은 어떻게 지원됩니까?
onassar

5
-1; 이것의 요점은 무엇입니까? 메서드에 대한 별도의 전역 사전 매핑 유형을 만든 다음 해당 사전에서 유형별로 메서드를 명시 적으로 조회합니다. 결과적으로 상속에 대한 지원 (이 방법으로 "추가 된"메서드는 Object.prototype에서 호출 될 수 없음)이 손실 Array되고 프로토 타입을 실제로 확장 한 경우보다 구문이 훨씬 길어집니다. 정적 메소드를 사용하여 유틸리티 클래스를 작성하는 것이 거의 항상 간단합니다. 이 접근법이 갖는 유일한 이점은 다형성에 대한 제한된 지원이다.
Mark Amery

4
@MarkAmery Hey pal, 당신은 그것을 얻지 못합니다. 상속을 잃지 않고 제거하십시오. 상속은 80 년대이므로 잊어 버려야합니다. 이 핵의 핵심은 단순히 오버로드 된 함수 인 타입 클래스를 모방하는 것입니다. 그래도 합법적 인 비판이있다 : 자바 스크립트에서 타입 클래스를 모방하는 것이 유용한가? 아닙니다. 대신 기본적으로 언어를 지원하는 유형이 지정된 언어를 사용하십시오. 나는 이것이 당신이 의미 한 것이라고 확신합니다.

7

하지 말아야 할 또 하나의 이유네이티브 객체를 확장 :

우리는 prototype.js 를 사용하는 Magento를 사용합니다 하고 네이티브 객체에 많은 것들을 확장하는 . 이것은 새로운 기능을 도입하기로 결정하고 큰 문제가 시작될 때까지 잘 작동합니다.

웹 컴포넌트 중 하나에 웹 컴포넌트를 도입 했으므로 webcomponents-lite.js는 IE의 전체 (네이티브) 이벤트 객체를 대체하기로 결정합니다 (이유는 무엇입니까?). 이것은 물론 prototype.js 를 깨뜨려 마 젠토를 깨뜨린 다. (문제를 찾을 때까지 많은 시간을 투자하여 추적 할 수 있습니다)

문제가 마음에 드시면 계속하십시오!


4

적어도 응용 프로그램 내 에서이 작업을 수행하지 않는 세 가지 이유를 알 수 있습니다.

  1. 잘못하면 확장 가능한 유형의 모든 객체에 열거 가능한 속성이 실수로 추가됩니다. Object.defineProperty기본적으로 열거 할 수없는 속성을 만드는을 사용하여 쉽게 해결할 수 있습니다.
  2. 사용중인 라이브러리와 충돌 할 수 있습니다. 부지런히 피할 수 있습니다. 프로토 타입에 무언가를 추가하기 전에 사용하는 라이브러리가 정의한 방법을 확인하고 업그레이드시 릴리스 정보를 확인한 다음 응용 프로그램을 테스트하십시오.
  3. 이후 버전의 기본 JavaScript 환경과 충돌이 발생할 수 있습니다.

포인트 3은 틀림없이 가장 중요한 것입니다. 당신은 때문에 프로토 타입 확장, 사용하는 라이브러리와 충돌을 일으키지 않는 것을 테스트를 통해 확인 할 수 있습니다 당신은 당신이 사용하는 도서관 것을 결정한다. 코드가 브라우저에서 실행된다고 가정하면 기본 객체의 경우도 마찬가지입니다. Array.prototype.swizzle(foo, bar)오늘 정의 하고 내일 Google Array.prototype.swizzle(bar, foo)이 Chrome에 추가 하면 혼란스러워하는 동료들과 함께 .swizzle행동이 왜 MDN에 문서화 된 내용과 일치하지 않는지 궁금해하는 경향이 있습니다.

mootools가 자신이 소유하지 않은 프로토 타입을 처리하는 방법에 대한 이야기웹을 중단하지 않기 위해 ES6 메소드의 이름을 변경하도록 강요했습니다 .

네이티브 객체에 추가 된 메소드 (예 : Array.prototype.myappSwizzle대신에 정의 Array.prototype.swizzle)에 응용 프로그램 특정 접두어를 사용하면 피할 수 있지만, 추악합니다. 프로토 타입을 보강하는 대신 독립형 유틸리티 기능을 사용하여 해결할 수 있습니다.


2

성능도 이유입니다. 때로는 키를 반복해야 할 수도 있습니다. 이를 수행하는 몇 가지 방법이 있습니다

for (let key in object) { ... }
for (let key in object) { if (object.hasOwnProperty(key) { ... } }
for (let key of Object.keys(object)) { ... }

나는 보통 for of Object.keys() 옳은 일을하고 상대적으로 간결하므로 검사를 추가 할 필요가 없으므로 사용합니다.

그러나 훨씬 느립니다 .

성능 대 성능 결과

이유 Object.keys가 느리다고 추측하는 것은 분명 Object.keys()하며 할당을해야합니다. 실제로 AFAIK는 이후 모든 키의 사본을 할당해야합니다.

  const before = Object.keys(object);
  object.newProp = true;
  const after = Object.keys(object);

  before.join('') !== after.join('')

JS 엔진이 어떤 종류의 불변 키 구조를 사용할 수 있기 때문에 불변 키 Object.keys(object)를 반복 object.newProp하고 완전히 새로운 불변 ​​키 객체 를 생성하는 참조 를 반환 하지만 최대 15 배까지 명확하게 느립니다.

점검조차도 hasOwnProperty최대 2 배 느려집니다.

이것의 핵심은 성능에 민감한 코드가 있고 키를 반복 해야하는 경우 for in호출하지 않고도 사용할 수 있다는 것입니다.hasOwnProperty 입니다. 수정하지 않은 경우에만이 작업을 수행 할 수 있습니다Object.prototype

Object.defineProperty추가 한 항목을 열거 할 수없는 경우 프로토 타입을 수정하는 데 사용하는 경우 위의 경우 JavaScript의 동작에 영향을 미치지 않습니다. 불행히도 Chrome 83 이상에서는 성능에 영향을 미칩니다.

여기에 이미지 설명을 입력하십시오

perf 문제를 강제로 나타 내기 위해 열거 할 수없는 3000 개의 속성을 추가했습니다. 30 개의 속성만으로도 테스트가 너무 가까워서 성능에 영향이 있는지 확인할 수 없었습니다.

https://jsperf.com/does-adding-non-enumerable-properties-affect-perf

Firefox 77과 Safari 13.1은 Augmented 클래스와 Unaugmented 클래스 사이에 성능 차이가 없었으며 v8 이이 영역에서 수정되어 성능 문제를 무시할 수 있습니다.

그러나 의 이야기가Array.prototype.smoosh 있습니다. 짧은 버전은 인기있는 라이브러리 인 Mootools Array.prototype.flatten입니다. 표준위원회가 네이티브를 추가하려고 할 때 Array.prototype.flatten많은 사이트를 중단하지 않고 는 네이티브를 추가 할 수 없었습니다. 브레이크에 대해 알게 된 개발자는 es5 방법의 이름을 smoosh농담으로 제안 했지만 사람들은 그것이 농담이라는 것을 이해하지 못해서 놀랐 습니다. 그들은 정착했다flat 대신에flatten

이야기의 교훈은 기본 객체를 확장해서는 안된다는 것입니다. 당신이 당신의 물건 파괴와 같은 문제를 겪을 수 있고 특정 라이브러리가 MooTools만큼 인기가 없다면 브라우저 공급 업체는 당신이 일으킨 문제를 해결하지 못할 것입니다. 도서관이 그 인기를 얻는다면 다른 사람들이 당신이 일으킨 문제를 해결하도록 강요하는 것입니다. 따라서 기본 객체를 확장하지 마십시오


잠깐, hasOwnProperty속성이 객체에 있는지 프로토 타입에 있는지 알려줍니다. 그러나 for in루프는 열거 가능한 속성 만 가져 옵니다 . 방금 시도했습니다 : 열거 할 수없는 속성을 Array 프로토 타입에 추가하면 루프에 표시되지 않습니다. 가장 간단한 루프가 작동하도록 프로토 타입을 수정할 필요가 없습니다 .
ygoe

그러나 그것은 다른 이유로 여전히 나쁜 습관입니다;)
gman
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.