Search code examples
javascriptstatedelegation

A Mixin that doesn't share state


I'm just starting to grasp how JavaScript works w/ Behavioral Delegation/Composition/Mixins etc, and I think its pretty neat. However when I create a new object using this pattern. I just noticed that both of them share the same 'items' prop. value. We all know that JS has really no Class at all. I'm just wondering, how can I implement the correct way of this design pattern w/o using any class, also addressing that shared state between different 'Players'. Thanks.

var Player = {
 init: function(name, level) {
  this.name = name
  this.level = level
 },
 getLevel: function() {
  return this.level
 }
}

var Inventory = {
 items: ['Wooden Sword', 'Small Potion', ...],
 addItem: function(item) {
  this.items.push(item)
 },
 removeItem: function(item) {
  var i = this.items.indexOf(item)
  if (i === -1) return
  this.items.splice(i, 1)
 }
}

// ..

var mario = Object.assign(Object.create(Player), Inventory)
mario.init('Mario', 1)
mario.addItem('Nice hat')
mario.items // ['Wooden Sword', 'Small Potion', 'Nice hat']

var luigi = Object.assign(Object.create(Player), Inventory)
luigi.init('Luigi', 1)
luigi.items // ['Wooden Sword', 'Small Potion', 'Nice hat'] ?
// Should be ['Wooden Sword', 'Small Potion']

Solution

  • Object.assign does not assign a deep copied structure from source to target object. Thus mario.addItem('Nice hat') already changes Inventory.items that remains a shared reference of mario and luigi. A poor men's approach like mario.items = Array.from(Inventory.items) as well as luigi.items = Array.from(Inventory.items) right after creating each Player instance does already help. Writing a createPlayer factory that does encapsulate object creation, Inventory composition and "cloning"/copying items would be even better ...

    var Player = {
     init: function (name, level) {
      this.name = name
      this.level = level
     },
     getLevel: function () {
      return this.level
     }
    }
    
    var Inventory = {
     items: ['Wooden Sword', 'Small Potion'],
     addItem: function (item) {
      this.items.push(item)
     },
     removeItem: function (item) {
      var i = this.items.indexOf(item);
      if (i === -1) {
       return this.items.splice(i, 1);
      }
     }
    }
    
    function createPlayer(name, level) {
      var player = Object.assign(Object.create(Player), Inventory);
      player.items = Array.from(Inventory.items);
      player.init(name, level);
      return player;
    }
    
    var mario = createPlayer('Mario', 1);
    mario.addItem('Nice hat');
    
    console.log('mario.items : ', mario.items); // ['Wooden Sword', 'Small Potion', 'Nice hat']
    
    var luigi = createPlayer('Luigi', 1);
    console.log('luigi.items : ', luigi.items); // ['Wooden Sword', 'Small Potion']
    .as-console-wrapper { max-height: 100%!important; top: 0; }