Using object literal for prototype classes

2019-07-22 14:44发布

问题:

I'm writing some JavaScript classes (old school and NOT using ES2015/ES6 and I don't want to use Babel or other transpilers) and I have one that inherits from the other, overriding one of the parent methods.

So I have my initial App.Hello class:

var App = {};
App.Hello = function(args) {
    this.name = args.name;
}
App.Hello.prototype = {
    constructor: App.Hello,
    sayHello: function() {
        console.log('Hello, ' + this.name);
    },
    sayGoodbye: function() {
        console.log('Goodbye, ', this.name);
    }
}

An then my App.Yo class which inherits from it:

// inherits from App.Hello
App.Yo = function(args) {
    App.Hello.call(this, args);
}
App.Yo.prototype = Object.create(App.Hello.prototype);
App.Yo.prototype = { // want to combine this with above!
    constructor: App.Yo,
    sayHello: function() {
        console.log('Yo, ', this.name);
    }
}

However because I'm using Object literal structure I overwrite the prototype of App.Yo when I pass it the constructor and sayHello methods after setting the Object.create. So I don't inherit the sayGoodby method from App.Hello

1. How can I get around this but using literal structure?

I know I could just do:

App.Yo.prototype = Object.create(App.Hello.prototype);
App.Yo.prototype.constructor = App.Yo;
App.Yo.prototype.sayHello = function sayHello() {
    console.log('Yo, ', this.name);
}

But I want to keep the literal structure as my classes are going to have a lot of different methods in them. So want to keep it nice and tidy.

2. Is it possible to nest the complete class as a literal? So have the constructors as well nested as part of a literal?

e.g.

App.Hello = function(args) {
    this.name = args.name;
}

and

App.Yo = function(args) {
    App.Hello.call(this, args);
}

回答1:

  1. How can I get around this but using literal structure?

Use Object.assign, which was added in ES2015 but which can be polyfilled so you don't have to transpile:

App.Yo.prototype = Object.assign(Object.create(App.Hello.prototype), {
    constructor: App.Yo,
    sayHello: function() {
        console.log('Yo, ', this.name);
    }
});

Or if you don't want to polyfill, just use your own helper, like the standard extend function (jQuery has one called $.extend, as do many other utility libraries):

function extend(target) {
    var i, source;
    for (i = 1; i < arguments.length; ++i) {
        source = arguments[i];
        Object.keys(source).forEach(function(name) {
            target[name] = source[name];
        });
    }
    return target;
}

App.Yo.prototype = extend(Object.create(App.Hello.prototype), {
    constructor: App.Yo,
    sayHello: function() {
        console.log('Yo, ', this.name);
    }
});
  1. Is it possible to nest the complete class as a literal?

Yes, by going further with helper functions. For instance:

function derive(base, props) {
    var cls = function() {
        return base.apply(this, arguments);
    };
    cls.prototype = Object.create(base.prototype);
    Object.assign(cls.prototype, props); // Or use your `extend` here
    return cls;
}

App.Yo = derive(App.Hello, {
    constructor: App.Yo,
    sayHello: function() {
        console.log('Yo, ', this.name);
    }
});

Of course, there are a lot of features missing from that, like controlling which arguments you use in Yo vs. passing on to Hello.

If you want to explore this further, you can look at my Lineage library, which makes creating classes in ES5 and earlier fairly simple and declarative. Personally, I consider it obsolete because of ES2015 and transpiling, but you've said you don't want to use a transpiler...