What is the best way to use mixins in js?

你离开我真会死。 提交于 2019-12-07 15:24:44

问题


Recently, I came across two articles on mixins. That got me confused between which one is better than other.

First one from mdn

var calculatorMixin = Base => class extends Base {
  calc() { }
};
var randomizerMixin = Base => class extends Base {
  randomize() { }
};

class Foo { }
class Bar extends calculatorMixin(randomizerMixin(Foo)) { }

Second one from https://javascript.info/mixins

let sayMixin = {
  say(phrase) {
    alert(phrase);
  }
};

let sayHiMixin = {
  __proto__: sayMixin, // (or we could use Object.create to set the prototype here)

  sayHi() {
    // call parent method
    super.say(`Hello ${this.name}`);
  },
  sayBye() {
    super.say(`Bye ${this.name}`);
  }
};

class User {
  constructor(name) {
    this.name = name;
  }
}

// copy the methods
Object.assign(User.prototype, sayHiMixin);

// now User can say hi
new User("Dude").sayHi(); // Hello Dude!

Object created in these scenarios has different composition/structure too.

  • Now I am confused like which one is better that other.

  • What advantages does one provide than the other.

  • And hence which one should I prefer to use.


回答1:


I fully agree with Jared Smith. Like with any tool or toolset one needs to be aware of whether to use it at all. If for any reason one chooses the concept of mixins one should really know what it is capable of and what it does miss, especially if applied to the programming paradigms/concepts of JavaScript.

And there I'm opinionated, thus the following thoughts and technical approaches will be provided from my very own point of view. Other solutions are much more widespread and some of them I sometimes use too.

Let's take the first source provided by the OP. If one, for instance, rewrites the above given example to something like ...

const calculatorMixin = Base => class extends Base {
  calc() { }
};
const randomizerMixin = Base => class extends Base {
  randomize() { }
};

class Baz { }
const calcAndRandomizeMixin = calculatorMixin(randomizerMixin(Baz));

class Biz extends calcAndRandomizeMixin { }

const biz = new Biz;

console.log('(biz instanceof Biz) ? ', (biz instanceof Biz));
console.log('(biz instanceof Baz) ? ', (biz instanceof Baz));
console.log('(biz instanceof calcAndRandomizeMixin) ? ', (biz instanceof calcAndRandomizeMixin));
.as-console-wrapper { max-height: 100%!important; top: 0; }

... then my biggest concern comes with the approach itself for it is entirely based on classes and their's extension. The "mixins" are classes, created instantly by class-factories. They always extend another class. Thus it is pure inheritance.

And even though one was writing such a mixin-class as a container of behavior that "can do" things, and a type later "has a" certain behavior, this approach technically does not at all acknowledge the concept behind mixins, because by it's very nature it is based on a child-parent or an "is a" relationship.

The second approach, with making use of objects and Object.assign, at first glance looks like a modern variant of the many ancient mixin approaches that back then all used a combination of behavior linked to objects and a self written extends method ... like ... extends(targetObject, mixinSourceObject).

Unique to this approach is how it supports/solves "composite-mixins" ... mixins that are created from other mixins. Linking behavior by super delegation and assigning another object-based mixin to a mixin's __proto__ property is, in my opinion, viable and elegant.

I personally would spent more time playing with / doing research on this secondly provided approach.

And there is yet another approach ... function-based mixins. The code of the second, object-based mixin-example rewritten into it's function-based pendant does look like this ...

const sayMixin = (function () {   // basic function-based mixin.

  // shared code.                 //
  function say(phrase) {          // a single implementation
    console.log(phrase);          // of `say` behavior ...
  }                               //

  // return function based mixin. //
  return function sayMixin () {   // ... that might get applied
    this.say = say;               // many times but always as
  };                              // reference / shared code.

}());

const sayHiMixin = (function () { // function-based *composite-mixin*.

  // shared code.

  // object that helps with behavior forwarding.
  const sayProxy = {};

  // apply behavior of `sayMixin`.
  sayMixin.call(sayProxy);

  // a single implementation of `sayHi` behavior.
  function sayHi() {
    sayProxy.say(`Hello ${this.name}!`);  // forwarding.
  }
  // a single implementation of `sayBye` behavior.
  function sayBye() {
    sayProxy.say(`Bye ${this.name}!`);    // forwarding.
  }

  // return function based composite mixin.
  return function sayHiMixin () {
    this.sayHi = sayHi;   // - always shares one and the ...
    this.sayBye = sayBye; //   ... same implementation(s).
  };

}());


class User {
  constructor(name) {

    // public property.
    this.name = name;
  }
}
// apply the composite `sayHiMixin`.
sayHiMixin.call(User.prototype);

// now a `User` can say hi and bye
const dude = new User('Dude');

dude.sayHi();   // Hello Dude!
dude.sayBye();  // Bye Dude!

console.log('dude.name : ', dude.name); // Dude
.as-console-wrapper { max-height: 100%!important; top: 0; }

There are some good arguments for even nowadays choosing this approach. Firstly, and this it has in common with both of the other ones mentioned by the OP ... no need for an additional library. Secondly, it does run in every given ES3 environment, unlike the others. At third, a function based approach implements mixins as "applicable types", thus one gets delegation and encapsulation for free.

The power of both is going to be demonstrated now. Still working with the 2nd's example code and the introduced function based mixin approach one very easily could create another user that hides it's initial name from public but does expose it via it's say behaviors. Of course the following code is just for understanding the concepts. One hardly would realize in practice kind of a "hybrid composite mixin" like that ...

const sayMixin = (function () {   // basic function-based mixin.

  // shared code.                 //
  function say(phrase) {          // a single implementation
    console.log(phrase);          // of `say` behavior ...
  }                               //

  // return function based mixin. //
  return function sayMixin () {   // ... that might get applied
    this.say = say;               // many times but always as
  };                              // reference / shared code.

}());

const sayHiMixin = (function () { // function-based *composite-mixin*.

  // shared code.

  // object that helps with behavior forwarding.
  const sayProxy = {};

  // apply behavior of `sayMixin`.
  sayMixin.call(sayProxy);

  // a single implementation of `sayHi` behavior.
  function sayHi() {
    sayProxy.say(`Hello ${this.name}!`);  // forwarding.
  }
  // a single implementation of `sayBye` behavior.
  function sayBye() {
    sayProxy.say(`Bye ${this.name}!`);    // forwarding.
  }

  // // return function based composite mixin.
  // return function sayHiMixin () {
  //   this.sayHi = sayHi;   // - always shares one and the ...
  //   this.sayBye = sayBye; //   ... same implementation(s).
  // };

  // return function based hybrid composite mixin.
  return function sayHiMixin (properties) {
    if (properties && (typeof properties === 'object')) {

      console.log('sayHiMixin :: payload bound to behavior');

      this.sayHi = sayHi.bind(properties);    // - creates each a ...
      this.sayBye = sayBye.bind(properties);  //   ... new reference.
    } else {
      console.log('sayHiMixin :: direct behavior reference');

      this.sayHi = sayHi;   // - always shares one and the ...
      this.sayBye = sayBye; //   ... same implementation(s).
    }
  };

}());


class User {
  constructor(name) {

    // public property.
    this.name = name;
  }
}
// apply the composite `sayHiMixin`.
sayHiMixin.call(User.prototype);

// now a `User` can say hi and bye
const dude = new User('Dude');

dude.sayHi();   // Hello Dude!
dude.sayBye();  // Bye Dude!

console.log('dude.name : ', dude.name); // Dude


class AnotherUser {
  constructor(name) {

    // local property + public accessor methods.
    sayHiMixin.call(this, { name: name });
  }
}

// now a `User` can say hi and bye
const john = new AnotherUser('John');

john.sayHi();   // Hello John!
john.sayBye();  // Bye John!

console.log('john.name : ', john.name); // undefined
.as-console-wrapper { max-height: 100%!important; top: 0; }

Opinionated summary on function based mixins

Just with what functions already since ES3 do offer, encapsulation via closures, explicit delegation of functionality and applying different contexts via call/apply, one already can start with mixin based composition. Combining this techniques enables more powerful concepts like conflict resolution that could/would be based on the already demonstrated forwarding via a proxy reference and some function composition. Injection of and passing around additional state is possible too. Thus one could even implement concepts that reach beyond mixins like Traits, Stateful Traits, and Talents with the latter being the composition concept that really fits the language paradigms of JavaScript.

Rule of thumb of whether to use a mixin

Go for it only in case the code-reuse can be described by an adjective like observable and/or if, within the entire system, unsimilar classes and/or heterogeneous types are in need of the same additional behavior.



来源:https://stackoverflow.com/questions/50614981/what-is-the-best-way-to-use-mixins-in-js

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!