Simple JavaScript Inheritance
John Resig 关于javascript继承的文章,留着,慢品。
I've been doing a lot of work, lately, with JavaScript inheritance - namely for my work-in-progress JavaScript book - and in doing so have examined a number of different JavaScript classical-inheritance-simulating techniques. Out of all the ones that I've looked at I think my favorites were the implementations employed by base2 and Prototype.
I wanted to go about extracting the soul of these techniques into a simple, re-usable, form that could be easily understood and didn't have any dependencies. Additionally I wanted the result to be simple and highly usable. Here's an example of what you can do with it:
1: var Person = Class.extend({2: init: function(isDancing){3: this.dancing = isDancing;4: },
5: dance: function(){6: return this.dancing;7: }
8: });
9:
10: var Ninja = Person.extend({11: init: function(){12: this._super( false );13: },
14: dance: function(){15: // Call the inherited version of dance()16: return this._super();17: },
18: swingSword: function(){19: return true;20: }
21: });
22:
23: var p = new Person(true);24: p.dance(); // => true25:
26: var n = new Ninja();27: n.dance(); // => false28: n.swingSword(); // => true29:
30: // Should all be true31: p instanceof Person && p instanceof Class &&
32: n instanceof Ninja && n instanceof Person && n instanceof Class
33:
A couple things to note about this implementation:
- Creating a constructor had to be simple (in this case simply providing an init method does the trick).
- In order to create a new 'class' you must extend (sub-class) an existing class.
- All of the 'classes' inherit from a single ancestor: Class. Therefore if you want to create a brand new class it must be a sub-class of Class.
- And the most challenging one: Access to overridden methods had to be provided (with their context properly set). You can see this with the use of
this._super()
, above, calling the originalinit()
anddance()
methods of thePerson
super-class.I'm pleased with the result: It helps to enforce the notion of 'classes' as a structure, maintains simple inheritance, and allows for the super method calling.
Simple Class Creation and Inheritance
And here's the implementation (reasonably sized and commented well) - clocking in at around 25 lines. Feedback is welcome and appreciated.
1: /* Simple JavaScript Inheritance2: * By John Resig http://ejohn.org/3: * MIT Licensed.4: */5: // Inspired by base2 and Prototype6: (function(){7: var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;8:
9: // The base Class implementation (does nothing)10: this.Class = function(){};11: // Create a new Class that inherits from this class12: Class.extend = function(prop) {13: var _super = this.prototype;14: // Instantiate a base class (but only create the instance,15: // don't run the init constructor)16: initializing = true;17: var prototype = new this();18: initializing = false;19: // Copy the properties over onto the new prototype20: for (var name in prop) {21: // Check if we're overwriting an existing function22: prototype[name] = typeof prop[name] == "function" &&23: typeof _super[name] == "function" && fnTest.test(prop[name]) ?24: (function(name, fn){25: return function() {26: var tmp = this._super;27: // Add a new ._super() method that is the same method28: // but on the super-class29: this._super = _super[name];30: // The method only need to be bound temporarily, so we31: // remove it when we're done executing32: var ret = fn.apply(this, arguments);33: this._super = tmp;34: return ret;35: };
36: })(name, prop[name]) :
37: prop[name];
38: }
39: // The dummy class constructor40: function Class() {41: // All construction is actually done in the init method42: if ( !initializing && this.init )43: this.init.apply(this, arguments);44: }
45: // Populate our constructed prototype object46: Class.prototype = prototype;
47: // Enforce the constructor to be what we expect48: Class.constructor = Class;
49:
50: // And make this class extendable51: Class.extend = arguments.callee;
52: return Class;53: };
54: })();
55:
In my opinion the two trickiest parts are the "initializing/don't call init" and "create _super method" portions. I want to cover those briefly so that you will have a good understanding of what's being achieved in this method.
Initialization
In order to simulate inheritance with a function prototype we use the traditional technique of creating an instance of the super-class function and assigning it to the prototype. Without using the above it would look something like this:
1: function Person(){}2: function Ninja(){}3: Ninja.prototype = new Person();4: // Allows for instanceof to work:5: (new Ninja()) instanceof PersonWhat's challenging about this, though, is that all we really want is the benefits of 'instanceof', not the whole cost of instantiating a Person object and running its constructor. To counteract this we have a variable in our code,
initializing
, that is set to true whenever we want to instantiate a class with the sole purpose of using it for a prototype.Thus when it comes time to actually construct the function we make sure that we're not in an initialization mode and run the init method accordingly:
1: if ( !initializing )2: this.init.apply(this, arguments);What's especially important about this is that the init method could be running all sorts of costly startup code (connecting to a server, creating DOM elements, who knows) so circumventing this ends up working quite well.
Super Method
When you're doing inheritance, creating a class that inherits functionality from a super-class, a frequent desire is the ability to access a method that you've overridden. The final result, in this particular implementation, is a new temporary method (
._super
) which is only accessible from within a sub-classes' method, referencing the super-classes' associated method.For example, if you wanted to call a super-classes' constructor you could do that with this technique.
1: var Person = Class.extend({2: init: function(isDancing){3: this.dancing = isDancing;4: }
5: });
6:
7: var Ninja = Person.extend({8: init: function(){9: this._super( false );10: }
11: });
12:
13: var p = new Person(true);14: p.dancing; // => true15:
16: var n = new Ninja();17: n.dancing; // => false18:
Implementing this functionality is a multi-step process. To start, note the object literal that we're using to extend an existing class (such as the one being passed in to
Person.extend
) needs to be merged on to the basenew Person
instance (the construction of which was described previously). During this merge we do a simple check: Is the property that we're attempting merge a function and is what we're replacing also a function? If that's the case then we need to go about creating a way for our super method to work.Note that we create an anonymous closure (which returns a function) that will encapsulate the new super-enhanced method. To start we need to be a good citizen and save a reference to the old
this._super
(disregarding if it actually exists) and restore it after we're done. This will help for the case where a variable with the same name already exists (don't want to accidentally blow it away).Next we create the new
_super
method, which is just a reference to the method that exists on the super-class' prototype. Thankfully we don't have to make any additional changes, or re-scoping, here as the context of the function will be set automatically when it's a property of our object (this
will refer to our instance as opposed to the super-class').Finally we call our original method, it does its work (possibly making use of
_super
as well) after which we restore_super
to its original state and return from the function.Now there's a number of ways in which a similar result, to the above, could be achieved (I've seen implementations that have bound the super method to the method itself, accessible from
arguments.callee
) but I feel that this technique provides the best mix of usability and simplicity.I'll be covering a lot more of the nitty-gritty behind the JavaScript prototype system in my completed work but I just wanted to get this Class implementation out there to get everyone trying it out and playing with it. I think there's a lot to be said for simplistic code (easier to learn, easier to extend, less to download) so I think this implementation is a good place to start and learn the fundamentals of JavaScript class construction and inheritance.