alexmngn
alexmngn

Reputation: 9627

Javascript inheritance with revealed prototype design pattern

I'm trying to create a simple javascript inheritance but there's something I missed and I need your help.

Basically, I have a User which can have a username. I also have a Player which inherit from User and can have a score, and play.

var Game = (function () {
  User = function () {
    var username;

    return {
        setUsername: function (newUsername) {
          username = newUserName;
        },
        getUsername: function () {
          return username;
       }
     }
  };

  Player = function () {
    var score = 0;

    return {
      getScore: function () {
        return score;
      },
      play: function () {
        score = Math.round(Math.random()*100);
      }
    };
  };

  Player.prototype = new User();

  return {
    player1: new Player(), 
    player2: new Player()
  };
});

var game = new Game();

game.player1.setUsername('alex');
game.player2.setUsername('tony');

game.player1.play();
game.player2.play();

console.log(game.player1.getUsername()+': '+game.player1.getScore());
console.log(game.player2.getUsername()+': '+game.player2.getScore());

The issue I have comes from the fact I don't have access of the method from the User in my Player. I'm not sure how I can get access to them though.

Here is a jsfiddle: https://jsfiddle.net/3xsu8pdy/

Any idea? Thanks.

Thanks.

Upvotes: 0

Views: 190

Answers (3)

user1636522
user1636522

Reputation:

Try this:

User = function () {};

User.prototype.setUsername = function (newUsername) {
  this.username = newUserName;
};

User.prototype.getUsername = function () {
  return this.username;
};

Same for Player :-)

The following code is a variation of this pattern. The extend() function takes care of binding prototypes together. The sup parameter refers to the parent prototype. Though, I'm afraid that it's old fashioned, I'm not sure if it's a good idea to go this way. Anyway, I believe that it's easier to understand than the previous code.

var A = extend(Object, function (sup) {
  this.init = function (name) {
    this.name = name;
  };
  this.whoami = function () {
    return this.name;
  };
});

var B = extend(A, function (sup) {
  this.whoami = function () {
    return 'I\'m ' + sup.whoami.call(this) + '.';
  };
});

var C = extend(B, function (sup) {
  this.init = function (name) {
    sup.init.call(this, '☆ ' + name + ' ☆');
  };
});

var a = new A('an instance of A');
var b = new B('an instance of B');
var c = new C('an instance of C');

log(
  'a.whoami()',
  'b.whoami()',
  'c.whoami()',
  'b instanceof A',
  'b instanceof B',
  'b instanceof C'
);

function extend (tail, head) {
  function Class () {
    this.init.apply(this, arguments);
  }
  head.prototype = tail.prototype;
  Class.prototype = new head(tail.prototype);
  Class.prototype.constructor = Class;
  return Class;
}
<table><thead><tr><th>code</th><th>result</th></tr></thead><tbody></tbody></table><script>function log(){var el=document.getElementsByTagName('tbody')[0];Array.prototype.slice.call(arguments).forEach(function(x){el.innerHTML+='<tr><td>'+x+'</td><td>'+eval(x)+'</td></tr>';});}</script><style>table,td,th{text-align:left;border:1px solid #333;border-collapse:collapse;padding:.5em;}</style>

Upvotes: 0

T.J. Crowder
T.J. Crowder

Reputation: 1075119

First, note that you're using new with User and Person, but your code throws away the object new creates by returning a different object from those functions. So new User() and User() do exactly the same thing.

And that's largely the reason you don't have access to User features in Person, because the prototype of the object returned isn't User.prototype, it's Object.prototype.

If you don't want new...

...you want to create your "person" objects so they're backed by User directly (or via Object.create(User())):

var Game = (function() {
    var User = function() { // <== Note the `var`
        var username;

        return {
            setUsername: function(newUsername) {
                username = newUserName;
            },
            getUsername: function() {
                return username;
            }
        }
    };

    var Player = function() {    // <== Note the `var`
        var score = 0;

        // Create the player, either using a User directly:
        var player = User();
        // ...or by using a User as a prototype:
        var player = Object.create(User());

        player.getScore = function() {
            return score;
        };
        player.play = function() {
            score = Math.round(Math.random() * 100);
        };

        return player;
    };

    return {
        player1: Player(),  // No `new`
        player2: Player()
    };
});

var game = new Game();

game.player1.setUsername('alex');
game.player2.setUsername('tony');

game.player1.play();
game.player2.play();

console.log(game.player1.getUsername() + ': ' + game.player1.getScore());
console.log(game.player2.getUsername() + ': ' + game.player2.getScore());

That keeps the username and score properties private, as in your original code.

If you want to use new

...then you probably want the fairly standard pattern I describe in this answer, which looks like this applied to your code:

var Game = (function() {
    var User = function() {
    };
    User.prototype.setUsername = function(newUsername) {
        this.username = newUserName;
    };
    User.prototype.getUsername = function() {
        return this.username;
    };

    var Player = function() {
        this.score = 0;
    };
    Player.prototype = Object.create(User.prototype);
    Player.prototype.constructor = Player;
    Player.prototype.getScore = function() {
        return this.score;
    };
    Player.prototype.play = function() {
        this.score = Math.round(Math.random() * 100);
    };

    return {
        player1: new Player(),
        player2: new Player()
    };
});

var game = new Game();

game.player1.setUsername('alex');
game.player2.setUsername('tony');

game.player1.play();
game.player2.play();

console.log(game.player1.getUsername() + ': ' + game.player1.getScore());
console.log(game.player2.getUsername() + ': ' + game.player2.getScore());

Or in ES2015:

var Game = (function() {

    class User {
        setUsername(newUsername) {
            this.username = newUserName;
        }
        getUsername() {
            return this.username;
        }
    }

    class Player extends User {
        constructor() {
            this.score = 0;
        }
        getScore() {
            return this.score;
        }
        play() {
            this.score = Math.round(Math.random() * 100);
        }
    }

    return {
        player1: new Player(),
        player2: new Player()
    };
});

var game = new Game();

game.player1.setUsername('alex');
game.player2.setUsername('tony');

game.player1.play();
game.player2.play();

console.log(game.player1.getUsername() + ': ' + game.player1.getScore());
console.log(game.player2.getUsername() + ': ' + game.player2.getScore());

Note that in both of those second two examples, username and score are no longer private. That said, even private variables in languages with built-in privacy like Java are trivially used outside of the private scope, via the reflection features of those languages.

In ES2015, we can use a WeakMap to get privacy as good as your original code's is:

var Game = (function() {
    var UserNames = new WeakMap();

    class User {
        setUsername(newUsername) {
            UserNames.set(this, newUsername);
        }
        getUsername() {
            return UserNames.get(this);
        }
    }

    var PlayerScores = new WeakMap();
    class Player extends User {
        constructor() {
            PlayerScores.set(this, 0);
        }
        getScore() {
            return PlayerScores.get(this);
        }
        play() {
            PlayerScores.set(this, Math.round(Math.random() * 100));
        }
    }

    return {
        player1: new Player(),
        player2: new Player()
    };
});

var game = new Game();

game.player1.setUsername('alex');
game.player2.setUsername('tony');

game.player1.play();
game.player2.play();

console.log(game.player1.getUsername() + ': ' + game.player1.getScore());
console.log(game.player2.getUsername() + ': ' + game.player2.getScore());

That doesn't cause a memory leak, because when a User or Person object is no longer referenced by anything other than the WeakMap, the WeakMap lets go of it and it can be garbage collected.

Upvotes: 2

Vikk
Vikk

Reputation: 647

I see where you are trying to go with the revealing module design pattern but If you are going for inheritance, you should be going with constructors. Something like:

var module = (function () {
    function User () {
        this.username = 'default';
    }
    User.prototype.setUsername = function (username) {
        this.username = username;
    };
    User.prototype.getUsername = function () {
        return this.username;
    };

    function Player () {
        User.call(this);
        this.score = 0;
    }
    Player.prototype = Object.create(User.prototype);
    Player.prototype.getScore = function () {
        return this.score;
    };
    Player.prototype.play = function () {
        this.score = Math.round(Math.random()*100);
    };

    return {
        player1 = new Player(),
        player2 = new Player()
    };
}());

So we are still using the revealing module pattern but we also get prototypal inheritance and all of the optimizations that come with it. This is what I would recommend.

Upvotes: 0

Related Questions