JavaScript의 다중 상속 / 시제품


132

JavaScript에서 발생하는 기본적인 다중 상속이 필요한 시점에 도달했습니다. (나는 이것이 좋은 아이디어인지 아닌지 논의하기 위해 여기에 있지 않으므로, 그 의견을 친절하게 유지하십시오.)

나는 누군가가 어떤 성공을 거두 었는지, 그리고 어떻게 성공했는지 알고 싶습니다.

그것을 정리하기 위해, 실제로 필요한 것은 둘 이상의 프로토 타입 체인 에서 속성을 상속 할 수있는 객체를 가질 수 있어야합니다 (즉, 각 프로토 타입은 고유 한 적절한 체인을 가질 수 있음). 첫 번째 정의를 위해 체인을 검색하십시오).

이것이 이론적으로 가능한 방법을 설명하기 위해, 2 차 체인을 1 차 체인의 끝에 부착하여 달성 할 수 있지만, 이는 이전 프로토 타입의 모든 인스턴스에 영향을 미치며 이는 내가 원하는 것이 아닙니다.

생각?


1
생각 도장 선언 핸들 다중 상속 SRC는 또한,이 정도의 나를 넘어 않습니다 또한 내가 느낌의 Mootools의이하지만 난의 빠른 읽기해야 겠어 도장 등을 제시
TI

TraitsJS ( link 1 , link 2 ) 를 살펴보면 다중 상속 및 믹스 인에 대한 훌륭한 대안입니다 ...
CMS

1
@Pointy는 매우 역동적이지 않기 때문에. 부모 체인이 변경되면 변경 사항을 선택할 수 있기를 바랍니다. 그러나 그것이 가능하지 않다면 이것에 의지해야 할 수도 있습니다.
devios1


1
이것에 대한 흥미로운 읽을 거리 : webreflection.blogspot.co.uk/2009/06/…
Nobita

답변:


49

프록시 객체 를 사용하여 ECMAScript 6에서 다중 상속을 수행 할 수 있습니다. .

이행

function getDesc (obj, prop) {
  var desc = Object.getOwnPropertyDescriptor(obj, prop);
  return desc || (obj=Object.getPrototypeOf(obj) ? getDesc(obj, prop) : void 0);
}
function multiInherit (...protos) {
  return Object.create(new Proxy(Object.create(null), {
    has: (target, prop) => protos.some(obj => prop in obj),
    get (target, prop, receiver) {
      var obj = protos.find(obj => prop in obj);
      return obj ? Reflect.get(obj, prop, receiver) : void 0;
    },
    set (target, prop, value, receiver) {
      var obj = protos.find(obj => prop in obj);
      return Reflect.set(obj || Object.create(null), prop, value, receiver);
    },
    *enumerate (target) { yield* this.ownKeys(target); },
    ownKeys(target) {
      var hash = Object.create(null);
      for(var obj of protos) for(var p in obj) if(!hash[p]) hash[p] = true;
      return Object.getOwnPropertyNames(hash);
    },
    getOwnPropertyDescriptor(target, prop) {
      var obj = protos.find(obj => prop in obj);
      var desc = obj ? getDesc(obj, prop) : void 0;
      if(desc) desc.configurable = true;
      return desc;
    },
    preventExtensions: (target) => false,
    defineProperty: (target, prop, desc) => false,
  }));
}

설명

프록시 개체는 대상 개체와 일부 트랩으로 구성되어 있으며 기본 작업에 대한 사용자 지정 동작을 정의합니다.

다른 객체로부터 상속받은 객체를 만들 때을 사용 Object.create(obj)합니다. 그러나이 경우에는 다중 상속을 원하므로obj 기본 작업을 적절한 객체로 리디렉션하는 프록시를 사용합니다.

나는이 함정을 사용한다 :

  • has트랩 대한 트랩 인 in연산자 . some적어도 하나의 프로토 타입에 속성이 포함되어 있는지 확인 하는 데 사용 합니다.
  • get트랩은 속성 값을 가져 오는 함정이다. 내가 사용 find하는 속성이 포함 된 첫 번째 프로토 타입을 찾아, 난 값을 반환, 또는 적합한 수신기의 게터를 호출합니다. 이에 의해 처리됩니다 Reflect.get. 프로토 타입에 속성이 포함되어 있지 않으면를 반환 undefined합니다.
  • set트랩은 속성 값을 설정하기위한 함정이다. find해당 속성이 포함 된 첫 번째 프로토 타입을 찾는 데 사용 하고 해당 수신기에서 해당 setter를 호출합니다. 세터가 없거나 프로토 타입에 특성이 포함되지 않은 경우 해당 수신자에 값이 정의됩니다. 이것은 다음에 의해 처리됩니다Reflect.set .
  • enumerate트랩 의 함정 for...in루프 . 첫 번째 프로토 타입과 두 번째 프로토 타입 등에서 열거 가능한 속성을 반복합니다. 속성이 반복되면 다시 반복되지 않도록 해시 테이블에 저장합니다.
    경고 :이 트랩은 ES7 초안에서 제거되었으며 브라우저에서 더 이상 사용되지 않습니다.
  • ownKeys트랩 에 대한 함정입니다 Object.getOwnPropertyNames(). ES7부터for...in 루프는 [[GetPrototypeOf]]를 계속 호출하고 각각의 고유 한 속성을 가져옵니다. 따라서 모든 프로토 타입의 속성을 반복하기 위해이 트랩을 사용하여 열거 가능한 모든 상속 된 속성을 자체 속성처럼 보이게합니다.
  • getOwnPropertyDescriptor트랩 에 대한 함정입니다 Object.getOwnPropertyDescriptor(). ownKeys트랩 에서 열거 가능한 모든 속성을 고유 한 속성처럼 보이게 만드는 것만으로는 충분하지 않습니다. for...in루프는 디스크립터가 열거 가능한지 확인하도록합니다. 따라서 find해당 속성이 포함 된 첫 번째 프로토 타입을 찾고 속성 소유자를 찾을 때까지 프로토 타입 체인을 반복하고 설명자를 반환합니다. 프로토 타입에 속성이 포함되어 있지 않으면를 반환 undefined합니다. 디스크립터는 구성 가능하도록 수정되었습니다. 그렇지 않으면 프록시 불 변형을 깰 수 있습니다.
  • preventExtensionsdefineProperty트랩은 프록시 타겟을 수정이 조작을 방지하기 위해 포함된다. 그렇지 않으면 프록시 불변량을 깰 수 있습니다.

사용할 수없는 트랩이 더 있습니다.

  • getPrototypeOf트랩을 추가 할 수 있지만, 여러 프로토 타입을 반환 할 적절한 방법이 없습니다. 이것은 instanceof작동하지 않습니다. 따라서 대상의 프로토 타입을 얻습니다. 처음에는 null입니다.
  • setPrototypeOf트랩을 추가하고 프로토 타입을 대체 할 것이다, 객체의 배열을 받아 들일 수 있습니다. 이것은 독자를위한 연습으로 남습니다. 여기서는 대상의 프로토 타입을 수정하도록했습니다. 트랩이 대상을 사용하지 않기 때문에별로 유용하지 않습니다.
  • deleteProperty트랩은 자신의 속성을 삭제하기위한 함정이다. 프록시는 상속을 나타내므로별로 의미가 없습니다. 어쨌든 속성이 없어야하는 대상에서 삭제를 시도했습니다.
  • isExtensible트랩은 확장 성을 얻기위한 함정이다. 불변으로 인해 대상과 동일한 확장 성을 반환한다는 점을 고려할 때별로 유용하지 않습니다. 따라서 작업을 대상으로 리디렉션하면 확장 가능합니다.
  • applyconstruct트랩은 전화 또는 인스턴스에 대한 함정입니다. 대상이 함수 또는 생성자 인 경우에만 유용합니다.

// Creating objects
var o1, o2, o3,
    obj = multiInherit(o1={a:1}, o2={b:2}, o3={a:3, b:3});

// Checking property existences
'a' in obj; // true   (inherited from o1)
'b' in obj; // true   (inherited from o2)
'c' in obj; // false  (not found)

// Setting properties
obj.c = 3;

// Reading properties
obj.a; // 1           (inherited from o1)
obj.b; // 2           (inherited from o2)
obj.c; // 3           (own property)
obj.d; // undefined   (not found)

// The inheritance is "live"
obj.a; // 1           (inherited from o1)
delete o1.a;
obj.a; // 3           (inherited from o3)

// Property enumeration
for(var p in obj) p; // "c", "b", "a"

1
일반 규모의 응용 프로그램에서도 관련이있을만한 성능 문제가 있습니까?
Tomáš Zato-복원 모니카

1
@ TomášZato 일반 객체의 데이터 속성보다 속도가 느릴 것이지만 접근 자 속성보다 훨씬 나쁘지는 않을 것이라고 생각합니다.
Oriol

TIL :multiInherit(o1={a:1}, o2={b:2}, o3={a:3, b:3})
bloodyKnuckles

4
무슨 일이 일어나고 있는지 더 잘 알기 위해 "다중 상속"을 "다중 위임"으로 바꾸는 것을 고려할 것입니다. 구현의 핵심 개념은 프록시가 실제로 메시지 를 위임 (또는 전달) 할 올바른 객체를 선택한다는 것 입니다. 솔루션의 장점은 대상 프로토 타입을 동적으로 확장 할 수 있다는 것입니다. 다른 답변은 연결 (ala Object.assign)을 사용하거나 상당히 다른 그래프를 얻는 것입니다. 결국 모두 객체 사이에 하나의 프로토 타입 체인을 얻습니다. 프록시 솔루션은 런타임 분기를 제공합니다.
sminutoli

성능에 대해 여러 객체에서 상속되는 객체를 생성하거나 여러 객체에서 상속하는 객체 등을 생성하면 지수가됩니다. 예, 느려질 것입니다. 그러나 정상적인 경우에는 그렇게 나쁘지 않다고 생각합니다.
Oriol

16

업데이트 (2019) : 원래 게시물이 오래되었습니다. 이 기사 (현재 인터넷 아카이브 링크, 도메인이 사라 졌으므로 )와 관련 GitHub 라이브러리 는 훌륭한 현대적인 접근 방식입니다.

원본 게시물 : 다중 상속 [편집, 유형의 적절한 상속이 아니라 속성의 상속; Javascript의 mixins]는 일반 객체가 아닌 생성 된 프로토 타입을 사용하는 경우 매우 간단합니다. 상속 할 두 개의 상위 클래스는 다음과 같습니다.

function FoodPrototype() {
    this.eat = function () {
        console.log("Eating", this.name);
    };
}
function Food(name) {
    this.name = name;
}
Food.prototype = new FoodPrototype();


function PlantPrototype() {
    this.grow = function () {
        console.log("Growing", this.name);
    };
}
function Plant(name) {
    this.name = name;
}
Plant.prototype = new PlantPrototype();

각 경우에 동일한 "이름"멤버를 사용 했으므로 부모가 "이름"을 처리하는 방법에 대해 동의하지 않으면 문제가 될 수 있습니다. 그러나이 경우 호환 가능합니다 (중복, 실제로).

이제 우리는 둘 다로부터 상속받은 클래스가 필요합니다. 상속은 프로토 타입과 객체 생성자에 대해 생성자 함수 (새 키워드를 사용하지 않고)를 호출 하여 수행됩니다 . 먼저 프로토 타입은 상위 프로토 타입에서 상속되어야합니다.

function FoodPlantPrototype() {
    FoodPrototype.call(this);
    PlantPrototype.call(this);
    // plus a function of its own
    this.harvest = function () {
        console.log("harvest at", this.maturity);
    };
}

그리고 생성자는 부모 생성자로부터 상속 받아야합니다 :

function FoodPlant(name, maturity) {
    Food.call(this, name);
    Plant.call(this, name);
    // plus a property of its own
    this.maturity = maturity;
}

FoodPlant.prototype = new FoodPlantPrototype();

이제 다양한 인스턴스를 재배, 섭취 및 수확 할 수 있습니다.

var fp1 = new FoodPlant('Radish', 28);
var fp2 = new FoodPlant('Corn', 90);

fp1.grow();
fp2.grow();
fp1.harvest();
fp1.eat();
fp2.harvest();
fp2.eat();

내장 프로토 타입으로이 작업을 수행 할 수 있습니까? (배열, 문자열, 숫자)
Tomáš Zato-Reinstate Monica

내장 프로토 타입에 호출 할 수있는 생성자가 있다고 생각하지 않습니다.
Roy J

글쎄, 할 수는 Array.call(...)있지만 통과하는 것에 영향을 미치지 않는 것 같습니다 this.
Tomáš Zato-복원 Monica Monica

@ TomášZato 당신은 할 수Array.prototype.constructor.call()
Roy J

1
@AbhishekGupta 알려 주셔서 감사합니다. 링크를 보관 된 웹 페이지에 대한 링크로 교체했습니다.
Roy J

7

이것은 Object.create실제 프로토 타입 체인을 만드는 데 사용 됩니다.

function makeChain(chains) {
  var c = Object.prototype;

  while(chains.length) {
    c = Object.create(c);
    $.extend(c, chains.pop()); // some function that does mixin
  }

  return c;
}

예를 들면 다음과 같습니다.

var obj = makeChain([{a:1}, {a: 2, b: 3}, {c: 4}]);

돌아올 것이다 :

a: 1
  a: 2
  b: 3
    c: 4
      <Object.prototype stuff>

그래서 obj.a === 1, obj.b === 3


간단한 가상의 질문 : Number와 Array 프로토 타입을 혼합하여 Vector 클래스를 만들고 싶었습니다 (재미있게). 이것은 배열 인덱스와 수학 연산자를 모두 제공합니다. 그러나 작동합니까?
Tomáš Zato-복원 모니카

@ TomášZato, 서브 클래 싱 배열을 살펴보고 있다면 이 기사 를 확인하는 것이 좋습니다. 두통을 덜 수 있습니다. 행운을 빕니다!
user3276552

5

John Resig의 클래스 구조 구현이 마음에 듭니다. http://ejohn.org/blog/simple-javascript-inheritance/

이것은 단순히 다음과 같이 확장 될 수 있습니다 :

Class.extend = function(prop /*, prop, prop, prop */) {
    for( var i=1, l=arguments.length; i<l; i++ ){
        prop = $.extend( prop, arguments[i] );
    }

    // same code
}

상속 할 여러 객체를 전달할 수 있습니다. instanceOf여기서 능력 을 잃을 것이지만 다중 상속을 원한다면 그것은 주어집니다.


위의 다소 복잡한 예는 https://github.com/cwolves/Fetch/blob/master/support/plugins/klass/klass.js에 있습니다.

해당 파일에 일부 불완전한 코드가 있지만 살펴 보려는 경우 다중 상속을 허용합니다.


체인 상속을 원한다면 (다중 상속은 아니지만 대부분의 사람들에게 동일합니다) 클래스와 같이 수행 할 수 있습니다.

var newClass = Class.extend( cls1 ).extend( cls2 ).extend( cls3 )

원래 프로토 타입 체인을 유지하지만 무의미한 코드가 많이 실행됩니다.


7
병합 된 얕은 복제본을 만듭니다. "상속 된"객체에 새 속성을 추가해도 실제 프로토 타입 상속 에서처럼 파생 된 객체에 새 속성이 나타나지 않습니다.
다니엘 Earwicker

@DanielEarwicker-맞습니다.하지만 한 클래스에서 "다중 상속"을 원한다면 두 가지 클래스에서 파생됩니다. 실제로 다른 대안은 없습니다. 단순히 클래스를 연결하는 것이 대부분의 경우에 동일하다는 것을 반영하도록 수정 된 답변.
Mark Kahn

GitHUb이 사라진 것 같습니다. github.com/cwolves/Fetch/blob/master/support/plugins/klass/를 가지고 계십니까 ?… 공유하고 싶으면 신경 쓰지 않겠습니까?
JasonDavis

4

다중 상속의 JavaScript 프레임 워크 구현과 혼동하지 마십시오.

당신이해야 할 모든 사용이다 Object.create는 () 의 변화에 확인 후, 지정된 프로토 타입 객체 및 속성에 새로운 객체를 매번 생성 Object.prototype.constructor에게 당신이 인스턴스화 계획이라면 방법의 각 단계를B 합니다. 미래.

상속 인스턴스 속성에 thisAthisB우리가 사용 Function.prototype.call ()을 각 개체 함수의 끝에서. 프로토 타입 상속에만 관심이있는 경우 선택 사항입니다.

다음 코드를 어딘가에서 실행하고 관찰하십시오 objC.

function A() {
  this.thisA = 4; // objC will contain this property
}

A.prototype.a = 2; // objC will contain this property

B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;

function B() {
  this.thisB = 55; // objC will contain this property

  A.call(this);
}

B.prototype.b = 3; // objC will contain this property

C.prototype = Object.create(B.prototype);
C.prototype.constructor = C;

function C() {
  this.thisC = 123; // objC will contain this property

  B.call(this);
}

C.prototype.c = 2; // objC will contain this property

var objC = new C();
  • B 에서 프로토 타입을 상속 A
  • C 에서 프로토 타입을 상속 B
  • objC 의 예입니다 C

위 단계에 대한 좋은 설명입니다.

JavaScript의 OOP : 알아야 할 사항


그래도 모든 속성을 새 객체에 복사하지 않습니까? 따라서 A와 B라는 두 개의 프로토 타입이 있고 C에서 모두 프로토 타입을 재생성하는 경우 A의 속성을 변경해도 C의 해당 속성에는 영향을 미치지 않으며 그 반대도 마찬가지입니다. 메모리에 저장된 A와 B의 모든 속성의 사본으로 끝납니다. A와 B의 모든 속성을 C로 하드 코딩 한 것과 같은 성능입니다. 가독성이 좋으며 속성 조회가 부모 개체로 이동할 필요는 없지만 복제와 같은 상속은 아닙니다. A의 속성을 변경해도 C의 복제 된 속성은 변경되지 않습니다.
Frank

2

나는 자바 스크립트 OOP에 대한 전문가는 아니지만, 올바르게 이해하면 (의사 코드)와 같은 것을 원합니다.

Earth.shape = 'round';
Animal.shape = 'random';

Cat inherit from (Earth, Animal);

Cat.shape = 'random' or 'round' depending on inheritance order;

이 경우 다음과 같은 것을 시도합니다.

var Earth = function(){};
Earth.prototype.shape = 'round';

var Animal = function(){};
Animal.prototype.shape = 'random';
Animal.prototype.head = true;

var Cat = function(){};

MultiInherit(Cat, Earth, Animal);

console.log(new Cat().shape); // yields "round", since I reversed the inheritance order
console.log(new Cat().head); // true

function MultiInherit() {
    var c = [].shift.call(arguments),
        len = arguments.length
    while(len--) {
        $.extend(c.prototype, new arguments[len]());
    }
}

1
이것이 단지 첫 번째 프로토 타입을 선택하고 나머지는 무시하지 않습니까? 설정 c.prototype을 여러 번하는 것은 여러 프로토 타입을 생성하지 않습니다. 당신이 가지고 예를 들어 Animal.isAlive = true, Cat.isAlive아직 정의되지 않은 것입니다.
devios1

예, 저는 프로토 타입을 혼합하고 수정했습니다 ... (jQuery의 확장을 사용했지만 그림을 볼 수 있습니다)
David Hellsing

2

라이브러리는 거의 없지만 JavaScript로 다중 상속을 구현할 수 있습니다.

내가 아는 유일한 예인 Ring.js를 가리킬 수 있습니다.


2

나는 오늘이 일을 많이하고 있었고 ES6에서 직접 달성하려고했습니다. 내가 한 방식은 Browserify, Babel을 사용하고 Wallaby로 테스트 한 결과 작동하는 것 같습니다. 내 목표는 현재 어레이를 확장하고 ES6, ES7을 포함하고 오디오 데이터를 처리하기 위해 프로토 타입에 필요한 몇 가지 추가 사용자 정의 기능을 추가하는 것입니다.

왈라비는 내 테스트 중 4 개를 통과했습니다. example.js 파일을 콘솔에 붙여 넣을 수 있으며 'includes'속성이 클래스의 프로토 타입에 있음을 알 수 있습니다. 나는 아직도 이것을 내일 더 시험하고 싶다.

내 방법은 다음과 같습니다. (잠자기 후 모듈로 리팩토링하고 다시 패키지 할 것입니다!)

var includes = require('./polyfills/includes');
var keys =  Object.getOwnPropertyNames(includes.prototype);
keys.shift();

class ArrayIncludesPollyfills extends Array {}

function inherit (...keys) {
  keys.map(function(key){
      ArrayIncludesPollyfills.prototype[key]= includes.prototype[key];
  });
}

inherit(keys);

module.exports = ArrayIncludesPollyfills

Github Repo : https://github.com/danieldram/array-includes-polyfill


2

나는 그것이 엄청나게 간단하다고 생각합니다. 여기서 문제는 하위 클래스가instanceof 첫 번째 클래스 입니다.

https://jsfiddle.net/1033xzyt/19/

function Foo() {
  this.bar = 'bar';
  return this;
}
Foo.prototype.test = function(){return 1;}

function Bar() {
  this.bro = 'bro';
  return this;
}
Bar.prototype.test2 = function(){return 2;}

function Cool() {
  Foo.call(this);
  Bar.call(this);

  return this;
}

var combine = Object.create(Foo.prototype);
$.extend(combine, Object.create(Bar.prototype));

Cool.prototype = Object.create(combine);
Cool.prototype.constructor = Cool;

var cool = new Cool();

console.log(cool.test()); // 1
console.log(cool.test2()); //2
console.log(cool.bro) //bro
console.log(cool.bar) //bar
console.log(cool instanceof Foo); //true
console.log(cool instanceof Bar); //false

1

다중 상속을 지원하는 IS 코드를 아래에서 확인하십시오. PROTOTYPAL 상속 을 사용하여 완료

function A(name) {
    this.name = name;
}
A.prototype.setName = function (name) {

    this.name = name;
}

function B(age) {
    this.age = age;
}
B.prototype.setAge = function (age) {
    this.age = age;
}

function AB(name, age) {
    A.prototype.setName.call(this, name);
    B.prototype.setAge.call(this, age);
}

AB.prototype = Object.assign({}, Object.create(A.prototype), Object.create(B.prototype));

AB.prototype.toString = function () {
    return `Name: ${this.name} has age: ${this.age}`
}

const a = new A("shivang");
const b = new B(32);
console.log(a.name);
console.log(b.age);
const ab = new AB("indu", 27);
console.log(ab.toString());

1

클래스를 다중 상속으로 정의 할 수있는 기능이 있습니다. 다음과 같은 코드를 허용합니다. 전반적으로 자바 스크립트에서 네이티브 클래스 기술과 완전히 다른 점을 알 수 있습니다 (예 class: 키워드 는 보이지 않습니다 ).

let human = new Running({ name: 'human', numLegs: 2 });
human.run();

let airplane = new Flying({ name: 'airplane', numWings: 2 });
airplane.fly();

let dragon = new RunningFlying({ name: 'dragon', numLegs: 4, numWings: 6 });
dragon.takeFlight();

다음과 같이 출력을 생성하십시오.

human runs with 2 legs.
airplane flies away with 2 wings!
dragon runs with 4 legs.
dragon flies away with 6 wings!

클래스 정의는 다음과 같습니다.

let Named = makeClass('Named', {}, () => ({
  init: function({ name }) {
    this.name = name;
  }
}));

let Running = makeClass('Running', { Named }, protos => ({
  init: function({ name, numLegs }) {
    protos.Named.init.call(this, { name });
    this.numLegs = numLegs;
  },
  run: function() {
    console.log(`${this.name} runs with ${this.numLegs} legs.`);
  }
}));

let Flying = makeClass('Flying', { Named }, protos => ({
  init: function({ name, numWings }) {
    protos.Named.init.call(this, { name });
    this.numWings = numWings;
  },
  fly: function( ){
    console.log(`${this.name} flies away with ${this.numWings} wings!`);
  }
}));

let RunningFlying = makeClass('RunningFlying', { Running, Flying }, protos => ({
  init: function({ name, numLegs, numWings }) {
    protos.Running.init.call(this, { name, numLegs });
    protos.Flying.init.call(this, { name, numWings });
  },
  takeFlight: function() {
    this.run();
    this.fly();
  }
}));

makeClass함수를 사용하는 각 클래스 정의 Object는 부모 클래스에 매핑 된 부모 클래스 이름 중 하나 를 허용 함을 알 수 있습니다. 또한 Object정의중인 클래스에 대한 포함 속성을 반환하는 함수도 허용합니다 . 이 기능에는 매개 변수가 있습니다protos , 여기에는 부모 클래스에 의해 정의 된 모든 속성에 액세스하기에 충분한 정보가 포함됩니다.

마지막으로 필요한 부분은 makeClass기능 자체인데, 이는 상당한 작업입니다. 여기 나머지 코드와 함께 있습니다. 나는 makeClass매우 심하게 언급 했다.

let makeClass = (name, parents={}, propertiesFn=()=>({})) => {
  
  // The constructor just curries to a Function named "init"
  let Class = function(...args) { this.init(...args); };
  
  // This allows instances to be named properly in the terminal
  Object.defineProperty(Class, 'name', { value: name });
  
  // Tracking parents of `Class` allows for inheritance queries later
  Class.parents = parents;
  
  // Initialize prototype
  Class.prototype = Object.create(null);
  
  // Collect all parent-class prototypes. `Object.getOwnPropertyNames`
  // will get us the best results. Finally, we'll be able to reference
  // a property like "usefulMethod" of Class "ParentClass3" with:
  // `parProtos.ParentClass3.usefulMethod`
  let parProtos = {};
  for (let parName in parents) {
    let proto = parents[parName].prototype;
    parProtos[parName] = {};
    for (let k of Object.getOwnPropertyNames(proto)) {
      parProtos[parName][k] = proto[k];
    }
  }
  
  // Resolve `properties` as the result of calling `propertiesFn`. Pass
  // `parProtos`, so a child-class can access parent-class methods, and
  // pass `Class` so methods of the child-class have a reference to it
  let properties = propertiesFn(parProtos, Class);
  properties.constructor = Class; // Ensure "constructor" prop exists
  
  // If two parent-classes define a property under the same name, we
  // have a "collision". In cases of collisions, the child-class *must*
  // define a method (and within that method it can decide how to call
  // the parent-class methods of the same name). For every named
  // property of every parent-class, we'll track a `Set` containing all
  // the methods that fall under that name. Any `Set` of size greater
  // than one indicates a collision.
  let propsByName = {}; // Will map property names to `Set`s
  for (let parName in parProtos) {
    
    for (let propName in parProtos[parName]) {
      
      // Now track the property `parProtos[parName][propName]` under the
      // label of `propName`
      if (!propsByName.hasOwnProperty(propName))
        propsByName[propName] = new Set();
      propsByName[propName].add(parProtos[parName][propName]);
      
    }
    
  }
  
  // For all methods defined by the child-class, create or replace the
  // entry in `propsByName` with a Set containing a single item; the
  // child-class' property at that property name (this also guarantees
  // there is no collision at this property name). Note property names
  // prefixed with "$" will be considered class properties (and the "$"
  // will be removed).
  for (let propName in properties) {
    if (propName[0] === '$') {
      
      // The "$" indicates a class property; attach to `Class`:
      Class[propName.slice(1)] = properties[propName];
      
    } else {
      
      // No "$" indicates an instance property; attach to `propsByName`:
      propsByName[propName] = new Set([ properties[propName] ]);
      
    }
  }
  
  // Ensure that "init" is defined by a parent-class or by the child:
  if (!propsByName.hasOwnProperty('init'))
    throw Error(`Class "${name}" is missing an "init" method`);
  
  // For each property name in `propsByName`, ensure that there is no
  // collision at that property name, and if there isn't, attach it to
  // the prototype! `Object.defineProperty` can ensure that prototype
  // properties won't appear during iteration with `in` keyword:
  for (let propName in propsByName) {
    let propsAtName = propsByName[propName];
    if (propsAtName.size > 1)
      throw new Error(`Class "${name}" has conflict at "${propName}"`);
    
    Object.defineProperty(Class.prototype, propName, {
      enumerable: false,
      writable: true,
      value: propsAtName.values().next().value // Get 1st item in Set
    });
  }
  
  return Class;
};

let Named = makeClass('Named', {}, () => ({
  init: function({ name }) {
    this.name = name;
  }
}));

let Running = makeClass('Running', { Named }, protos => ({
  init: function({ name, numLegs }) {
    protos.Named.init.call(this, { name });
    this.numLegs = numLegs;
  },
  run: function() {
    console.log(`${this.name} runs with ${this.numLegs} legs.`);
  }
}));

let Flying = makeClass('Flying', { Named }, protos => ({
  init: function({ name, numWings }) {
    protos.Named.init.call(this, { name });
    this.numWings = numWings;
  },
  fly: function( ){
    console.log(`${this.name} flies away with ${this.numWings} wings!`);
  }
}));

let RunningFlying = makeClass('RunningFlying', { Running, Flying }, protos => ({
  init: function({ name, numLegs, numWings }) {
    protos.Running.init.call(this, { name, numLegs });
    protos.Flying.init.call(this, { name, numWings });
  },
  takeFlight: function() {
    this.run();
    this.fly();
  }
}));

let human = new Running({ name: 'human', numLegs: 2 });
human.run();

let airplane = new Flying({ name: 'airplane', numWings: 2 });
airplane.fly();

let dragon = new RunningFlying({ name: 'dragon', numLegs: 4, numWings: 6 });
dragon.takeFlight();

makeClass함수는 클래스 속성도 지원합니다. 이들은 속성 이름 앞에 $기호 를 붙여 정의됩니다 (결과의 최종 속성 이름은 $제거됩니다). 이를 염두에두고, Dragon드래곤의 "타입"을 모델링 하는 특수 클래스를 작성할 수 있습니다 . 여기서 사용 가능한 드래곤 유형의 목록은 인스턴스가 아닌 클래스 자체에 저장됩니다.

let Dragon = makeClass('Dragon', { RunningFlying }, protos => ({

  $types: {
    wyvern: 'wyvern',
    drake: 'drake',
    hydra: 'hydra'
  },

  init: function({ name, numLegs, numWings, type }) {
    protos.RunningFlying.init.call(this, { name, numLegs, numWings });
    this.type = type;
  },
  description: function() {
    return `A ${this.type}-type dragon with ${this.numLegs} legs and ${this.numWings} wings`;
  }
}));

let dragon1 = new Dragon({ name: 'dragon1', numLegs: 2, numWings: 4, type: Dragon.types.drake });
let dragon2 = new Dragon({ name: 'dragon2', numLegs: 4, numWings: 2, type: Dragon.types.hydra });

다중 상속의 도전

의 코드를 따라 누구나 makeClass가깝게는 자동으로 발생 오히려 상당한 바람직하지 않은 현상을주의 할 것이다 때 위의 코드를 실행 : A는 인스턴스 RunningFlying받는 두 통화가 발생합니다 Named생성자!

상속 그래프가 다음과 같기 때문입니다.

 (^^ More Specialized ^^)

      RunningFlying
         /     \
        /       \
    Running   Flying
         \     /
          \   /
          Named

  (vv More Abstract vv)

서브 클래스의 상속 그래프에서 동일한 부모 클래스에 대한 경로여러 개인 경우 생성자 여러 번, 서브 클래스의 인스턴스화 그 상위 클래스를 호출한다. '

이것과 싸우는 것은 사소한 것이 아닙니다. 클래스 이름이 단순화 된 예제를 살펴 보겠습니다. 우리는 클래스 고려할 것 A, 가장 추상적 인 부모 클래스, 클래스 BC에서 모두 상속 A, 수업을 BC하는 상속에서 BC(따라서 개념적으로 "이중 상속"에서 A)

let A = makeClass('A', {}, () => ({
  init: function() {
    console.log('Construct A');
  }
}));
let B = makeClass('B', { A }, protos => ({
  init: function() {
    protos.A.init.call(this);
    console.log('Construct B');
  }
}));
let C = makeClass('C', { A }, protos => ({
  init: function() {
    protos.A.init.call(this);
    console.log('Construct C');
  }
}));
let BC = makeClass('BC', { B, C }, protos => ({
  init: function() {
    // Overall "Construct A" is logged twice:
    protos.B.init.call(this); // -> console.log('Construct A'); console.log('Construct B');
    protos.C.init.call(this); // -> console.log('Construct A'); console.log('Construct C');
    console.log('Construct BC');
  }
}));

BC이중 호출 을 방지 A.prototype.init하려면 상속 된 생성자를 직접 호출하는 스타일을 포기해야 할 수 있습니다. 중복 호출이 발생하는지 확인하고 발생하기 전에 단락을 발생시키기 위해서는 어느 정도의 간접적 인 수준이 필요합니다.

함께 : 우리는 작동 특성에 공급되는 매개 변수를 변경 고려할 수 protos, Object상속 된 속성을 설명하는 원시 데이터를 포함, 우리는 또한 부모 방법도 호출하는 방식으로 인스턴스 메소드를 호출하는 유틸리티 기능을 포함 할 수 있지만 중복 통화가 감지 방지했다. 우리가 어디에 매개 변수를 설정하는지 살펴 보겠습니다.propertiesFn Function .

let makeClass = (name, parents, propertiesFn) => {

  /* ... a bunch of makeClass logic ... */

  // Allows referencing inherited functions; e.g. `parProtos.ParentClass3.usefulMethod`
  let parProtos = {};
  /* ... collect all parent methods in `parProtos` ... */

  // Utility functions for calling inherited methods:
  let util = {};
  util.invokeNoDuplicates = (instance, fnName, args, dups=new Set()) => {

    // Invoke every parent method of name `fnName` first...
    for (let parName of parProtos) {
      if (parProtos[parName].hasOwnProperty(fnName)) {
        // Our parent named `parName` defines the function named `fnName`
        let fn = parProtos[parName][fnName];

        // Check if this function has already been encountered.
        // This solves our duplicate-invocation problem!!
        if (dups.has(fn)) continue;
        dups.add(fn);

        // This is the first time this Function has been encountered.
        // Call it on `instance`, with the desired args. Make sure we
        // include `dups`, so that if the parent method invokes further
        // inherited methods we don't lose track of what functions have
        // have already been called.
        fn.call(instance, ...args, dups);
      }
    }

  };

  // Now we can call `propertiesFn` with an additional `util` param:
  // Resolve `properties` as the result of calling `propertiesFn`:
  let properties = propertiesFn(parProtos, util, Class);

  /* ... a bunch more makeClass logic ... */

};

위의 변경의 목적은 호출 할 때 makeClass추가 인수를 제공하는 것 입니다. 또한 어떤 클래스에 정의 된 모든 함수는 다른 모든 함수 뒤에 매개 변수를받을 수 있습니다.propertiesFnmakeClassdup A는, Set이미 상속 된 메소드를 호출의 결과로 호출 된 모든 기능을 보관 유지를 :

let A = makeClass('A', {}, () => ({
  init: function() {
    console.log('Construct A');
  }
}));
let B = makeClass('B', { A }, (protos, util) => ({
  init: function(dups) {
    util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
    console.log('Construct B');
  }
}));
let C = makeClass('C', { A }, (protos, util) => ({
  init: function(dups) {
    util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
    console.log('Construct C');
  }
}));
let BC = makeClass('BC', { B, C }, (protos, util) => ({
  init: function(dups) {
    util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
    console.log('Construct BC');
  }
}));

이 새로운 스타일은 실제로 "Construct A"인스턴스가BC 가 초기화 . 그러나 세 가지 단점이 있으며 그중 세 번째 단점은 매우 중요합니다 .

  1. 이 코드는 읽기 어렵고 유지 관리가 쉬워졌습니다. 많은 복잡성이 숨겨져 있습니다.util.invokeNoDuplicates 함수 ,이 스타일이 다중 호출을 피하는 방법이 직관적이지 않고 두통을 유발하는 방법에 대해 생각합니다. 또한 성가신 dups매개 변수 가 있습니다.이 매개 변수 는 클래스의 모든 단일 함수에서 실제로 정의해야합니다 . 아야.
  2. 이 코드는 느리다-다중 상속으로 바람직한 결과를 얻기 위해서는 약간 더 간접적 인 계산과 계산이 필요하다. 불행히도 이것은 아마도 다중 호출 문제에 대한 해결책 .
  3. 가장 중요한 것은 상속에 의존하는 함수의 구조가 매우 엄격 해졌다는 것 입니다. 하위 클래스 NiftyClass가 함수를 재정의하고 중복 호출없이 함수 를 실행하는 niftyFunction데 사용 util.invokeNoDuplicates(this, 'niftyFunction', ...)하면 NiftyClass.prototype.niftyFunction해당 함수 niftyFunction를 정의하는 모든 상위 클래스의 이름 이 지정된 함수를 호출하고 해당 클래스의 모든 반환 값을 무시하고의 특수 논리를 수행합니다 NiftyClass.prototype.niftyFunction. 이것이 유일하게 가능한 구조 입니다. 만약 NiftyClass상속 CoolClassGoodClass , 그리고이 두 부모 클래스가 제공하는 niftyFunction자신의 정의를, NiftyClass.prototype.niftyFunction(다중 호출을 위험없이) 할 수 할 수 없을 것 :
    • A. 특수 논리를 실행NiftyClass 먼저 한 다음 상위 클래스
    • B. 전문 논리를 실행NiftyClass 모든 특수한 상위 논리가 완료된 이외의 시점에서
    • 씨. 부모의 특수 로직의 반환 값에 따라 조건부로 행동
    • D. 특정 부모의 전문화를 niftyFunction완전히 피하십시오

물론, 우리는 아래에 특수 기능을 정의하여 위의 각 글자 문제를 해결할 수 있습니다 util .

  • ㅏ. 정의util.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
  • B. 정의util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...) (어디 parentName전문 로직 즉시 아이 클래스 '전문 논리에 의해 따라야 할 상위의 이름입니다)
  • C. 정의 util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)(이 경우testFn 부모라는 특수 논리의 결과를 수신 하고 단락 발생 여부를 나타내는 값을 parentName반환 true/false함)
  • D.는 정의 util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(이 경우 blackList가 될 것 인Array 누구의 전문 논리를 모두 건너 뛰어야 부모 이름을)

이 솔루션은 모두 사용할 수 있지만 이것은 완전한 신체 상해입니다 ! 상속 된 함수 호출이 취할 수있는 모든 고유 한 구조에 대해에 정의 된 특수 메소드가 필요합니다 util. 참 재앙입니다.

이를 염두에두면 좋은 다중 상속을 구현하는 데 어려움을 겪을 수 있습니다. 의 전체 구현makeClass이 답변에 제공된 다중 호출 문제 또는 다중 상속과 관련하여 발생하는 다른 많은 문제조차 고려하지 않습니다.

이 답변은 점점 길어지고 있습니다. makeClass포함 된 구현이 완벽하지는 않지만 여전히 유용하기를 바랍니다 . 또한이 주제에 관심이있는 사람은 더 읽을 때 더 많은 정보를 얻을 수 있기를 바랍니다.


0

IeUnit 패키지를 살펴보십시오 .

IeUnit에서 구현 된 개념 동화는 찾고있는 것을 매우 역동적으로 제공하는 것으로 보입니다.


0

다음은 생성자 함수를 사용 하는 프로토 타입 체인 의 예입니다 .

function Lifeform () {             // 1st Constructor function
    this.isLifeform = true;
}

function Animal () {               // 2nd Constructor function
    this.isAnimal = true;
}
Animal.prototype = new Lifeform(); // Animal is a lifeform

function Mammal () {               // 3rd Constructor function
    this.isMammal = true;
}
Mammal.prototype = new Animal();   // Mammal is an animal

function Cat (species) {           // 4th Constructor function
    this.isCat = true;
    this.species = species
}
Cat.prototype = new Mammal();     // Cat is a mammal

이 개념은 Yehuda Katz의 JavaScript 에 대한 "클래스" 정의를 사용합니다 .

... JavaScript "클래스"는 생성자 및 연결된 프로토 타입 객체의 역할을하는 Function 객체 일뿐입니다. ( 출처 : Guru Katz )

Object.create 접근 방식 과 달리 클래스가 이런 방식으로 빌드되고 "클래스"의 인스턴스를 만들려고 할 때 각 "클래스"가 무엇을 상속하는지 알 필요가 없습니다. 우리는 단지 사용 new합니다.

// Make an instance object of the Cat "Class"
var tiger = new Cat("tiger");

console.log(tiger.isCat, tiger.isMammal, tiger.isAnimal, tiger.isLifeform);
// Outputs: true true true true

우선 순위가 의미가 있어야합니다. 먼저 인스턴스 객체를 본 다음 프로토 타입, 다음 프로토 타입 등을 살펴 봅니다.

// Let's say we have another instance, a special alien cat
var alienCat = new Cat("alien");
// We can define a property for the instance object and that will take 
// precendence over the value in the Mammal class (down the chain)
alienCat.isMammal = false;
// OR maybe all cats are mutated to be non-mammals
Cat.prototype.isMammal = false;
console.log(alienCat);

클래스에 구축 된 모든 객체에 영향을주는 프로토 타입을 수정할 수도 있습니다.

// All cats are mutated to be non-mammals
Cat.prototype.isMammal = false;
console.log(tiger, alienCat);

나는 원래이 답변 으로 이것의 일부를 썼습니다 .


2
OP는 여러 프로토 타입 체인을 요청합니다 (예 : 및 child에서 상속 ). 귀하의 예는 하나의 체인에 대해서만 이야기합니다. parent1parent2
poshest

0

장면의 후발자는 SimpleDeclare 입니다. 그러나 다중 상속을 처리 할 때 여전히 원래 생성자의 사본이 생성됩니다. 그것은 자바 스크립트의 필요성입니다 ...

머서.


ES6 프록시까지 Javascript에는 필수입니다.
Jonathon

프록시는 흥미 롭습니다! 프록시가 표준의 일부가 된 후에 프록시를 사용하여 메소드를 복사 할 필요가 없도록 SimpleDeclare를 변경하는 것을 확실히 살펴볼 것입니다. SimpleDeclare의 코드는 정말 읽고 이해하기 쉽습니다 ...
Merc

0

ds.oop 사용 합니다 . prototype.js 및 기타와 유사합니다. 다중 상속을 매우 쉽고 미니멀하게 만듭니다. (2 또는 3kb 만) 인터페이스 및 의존성 주입과 같은 다른 깔끔한 기능도 지원합니다

/*** multiple inheritance example ***********************************/

var Runner = ds.class({
    run: function() { console.log('I am running...'); }
});

var Walker = ds.class({
    walk: function() { console.log('I am walking...'); }
});

var Person = ds.class({
    inherits: [Runner, Walker],
    eat: function() { console.log('I am eating...'); }
});

var person = new Person();

person.run();
person.walk();
person.eat();

0

이 방법은 JavaScript에서 다중 상속을 구현합니다.

    class Car {
        constructor(brand) {
            this.carname = brand;
        }
        show() {
            return 'I have a ' + this.carname;
        }
    }

    class Asset {
        constructor(price) {
            this.price = price;
        }
        show() {
            return 'its estimated price is ' + this.price;
        }
    }

    class Model_i1 {        // extends Car and Asset (just a comment for ourselves)
        //
        constructor(brand, price, usefulness) {
            specialize_with(this, new Car(brand));
            specialize_with(this, new Asset(price));
            this.usefulness = usefulness;
        }
        show() {
            return Car.prototype.show.call(this) + ", " + Asset.prototype.show.call(this) + ", Model_i1";
        }
    }

    mycar = new Model_i1("Ford Mustang", "$100K", 16);
    document.getElementById("demo").innerHTML = mycar.show();

specialize_with () 유틸리티 함수의 코드는 다음과 같습니다.

function specialize_with(o, S) { for (var prop in S) { o[prop] = S[prop]; } }

이것은 실제 코드입니다. html 파일로 복사하여 붙여 넣을 수 있습니다. 작동합니다.

이것이 JavaScript로 MI를 구현하려는 노력입니다. 코드가 많지 않고 노하우가 많습니다.

https://github.com/latitov/OOP_MI_Ct_oPlus_in_JS 에 대한 전체 기사를 자유롭게 참조하십시오.


0

방금 다른 클래스의 속성에 필요한 클래스를 할당하고 원하는 클래스를 자동으로 가리 키도록 프록시를 추가했습니다.

class A {
    constructor()
    {
        this.test = "a test";
    }

    method()
    {
        console.log("in the method");
    }
}

class B {
    constructor()
    {
        this.extends = [new A()];

        return new Proxy(this, {
            get: function(obj, prop) {

                if(prop in obj)
                    return obj[prop];

                let response = obj.extends.find(function (extended) {
                if(prop in extended)
                    return extended[prop];
            });

            return response ? response[prop] : Reflect.get(...arguments);
            },

        })
    }
}

let b = new B();
b.test ;// "a test";
b.method(); // in the method
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.