JavaScript Inheritance
DouglasCrockford
www.crockford.com
And you think you're so clever and classless and free
--John Lennon
JavaScript一种没有类的,面向对象的语言,它使用原型继承来代替类继承。这个可能对受过传统的面向对象语言(如C++和Java)训练的程序员来说有点迷惑。JavaScript的原型继承比类继承有更强大的表现力,现在就让我们来看看。
Java
|
JavaScript
|
强类型
|
弱类型
|
静态
|
动态
|
基于类
|
基于原型
|
类
|
函数
|
构造器
|
函数
|
方法
|
函数
|
But first, why do we care so much about inheritance? There are two main reasons. The first is type favorable. We hope that the language system can automatically perform similar type reference conversioncast. Small type safety can be obtained from a type system that requires programs to explicitly convert object references. This is the most critical point of a strongly typed language, but it has nothing to do with a weakly typed language like JavaScript, where class references do not require casting.
The second reason is for code reuse. In a program, you often find that many objects implement the same methods. Classes make it possible to create objects with a single definition set. It is also common for objects to contain objects that other objects contain, but the difference is only the addition or modification of a small number of methods. Class inheritance is very useful for this, but prototypal inheritance is even more useful.
To demonstrate this, we're going to introduce a little "sweet spot" that allows us to write code like a regular class language. We then show some useful patterns not found in class languages. Finally, we'll explain these "desserts."
Class inheritance
First, we create a Parenizor class, which has get and set methods for member value, and a toString method that wraps value in parentheses.
function Parenizor(value) {
this.setValue (value);
}
Parenizor.method('setValue', function (value) {
this.value = value;
return this;
});
Parenizor. method('getValue', function () {
return this.value;
});
Parenizor.method('toString', function () {
return '(' this.getValue( ) ')';
});
This syntax may not be useful, but it is easy to see the form of the class. The method method takes a method name and a function and puts them into the class as public methods.
Now we can write
myParenizor = new Parenizor( 0);
myString = myParenizor.toString();
As expected, myString is "(0)".
Now we are going to create another class that inherits from Parenizor, it is basically the same except that the toString method will produce "-0-" if the value is zero or empty.
function ZParenizor(value) {
this.setValue (value);
}
ZParenizor.inherits(Parenizor);
ZParenizor.method("e;toString"e;, function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-";
});
The inherits method is similar to Java's extends. The uber method is similar to Java's super. It makes a method call a method of the parent class (the name was changed to avoid conflicts with reserved words).
We can write it like this
myZParenizor = new ZParenizor( 0);
myString = myZParenizor.toString();
This time, myString is "-0-".
JavaScript does not have a class, but we can achieve this purpose programmatically.
Multiple inheritance
By operating the prototype object of a function, we can implement multiple inheritance. Mixed multiple inheritance is difficult to implement and may run the risk of name collisions. We can implement mixed multiple inheritance in JavaScript, but for this example we will use a more standardized form called Swiss inheritance.
Suppose there is a NumberValue class that has a setValue method to check whether the value is within a specified range A number within and throws an exception when appropriate. We just need its setValue and setRange methods for our ZParenizor. We certainly don't want its toString method. In this way, we write:
ZParenizor.swiss(NumberValue, 'setValue', 'setRange');
This will only add the required methods.
Parasitic inheritance
This is another way to write the ZParenizor class. It does not inherit from Parenizor, but writes a constructor that calls the Parenizor constructor, modifies the result, and finally returns the result. This constructor adds privileged methods rather than public methods.
function ZParenizor2(value) {
var self = new Parenizor(value);
self.toString = function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-"
};
return self;
}
Class inheritance is a "is..." relationship, while parasitic inheritance is a relationship about "the original It was...and now it is..." relationship. Constructors play a large role in the construction of objects. Note that uber (instead of the super keyword) still works for privileged methods.
Class extension
The dynamic nature of JavaScript allows us to add or replace methods to an existing class. We can call methods at any time. We can extend a class at any time. Inheritance doesn't work this way. So we call this situation "class extension" to avoid confusion with Java's extends-also called extensions, but not the same thing.
Object extension
In static object-oriented languages, if you want one object to be different from another object, you must create a new class. But in JavaScript, you can add methods to individual objects without creating a new class. This has huge power because you can write as few classes as possible, and classes can be written more simply. Think of JavaScript objects like hash tables. You can add new values at any time. If the value is a function, it becomes a method.
So in the above example, I don't need the ZParenizor class at all. I just need to make a simple modification to my instance.
myParenizor = new Parenizor(0);
myParenizor .toString = function () {
if (this.getValue()) {
return this.uber('toString');
}
return "-0-";
} ;
myString = myParenizor.toString();
We added a toString method to the myParenizor instance without using any inheritance. We can evolve individual instances because the language is typeless.
Small desserts
To make the above example work, I wrote four "dessert" methods. First, the method method can add an instance method to a class.
Function.prototype.method = function (name, func) {
this.prototype[name] = func;
return this;
};
This will add a public method to Function.prototype, so that it can be extended by the class All functions can use it. It takes a name and a function as parameters.
It returns this. When I write a method that doesn't return a value, I usually have it return this. This can form a chained statement.
The following is the inherits method, which will indicate that one class inherits from another class. It must be defined after both classes have been defined, but must be called before method inheritance.
Function.method('inherits', function (parent) {
var d = 0, p = (this.prototype = new parent());
this.method('uber', function uber(name) {
var f, r, t = d , v = parent.prototype;
if (t) {
while (t) {
v = v.constructor.prototype;
t -= 1;
}
f = v[name];
} else {
f = p[name];
if (f == this[name]) {
f = v[name];
}
}
d = 1;
r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
d -= 1;
return r;
});
return this;
});
Next, let’s extend the Function class. We add an instance of the parent class and make it the new prototype. We also have to modify the constructor field and we add the uber method.
The uber method will look for a method in its own prototype. This is a case of parasitic inheritance or class extension. If we are inheriting from a class, then we need to find the function in the parent's prototype. The return statement calls the function's apply method to call the function, while explicitly setting this and passing parameters. The parameters (if any) are obtained from the arguments array. Unfortunately, the arguments array is not a real array, so we have to use apply again to call the slice method in the array.
Finally, the swiss method
Function.method(' swiss', function (parent) {
for (var i = 1; i < arguments.length; i = 1) {
var name = arguments[i];
this.prototype[name] = parent.prototype[name];
}
return this;
});
The swiss method loops through each parameter. For each name, it copies the members from the parent's prototype into the prototype of the new class.
Summary
JavaScript can be used like a class language, but it also has a very unique level of expression. We have looked at class inheritance, Swiss inheritance, parasitic inheritance, class extension and object extension. This series of code reuse patterns can all come from this JavaScript language, which has always been considered to be small and simple.
Class objects are "hard". The only way to add members to a "hard" object is to create a new class. In JavaScript, objects are "soft". To add members to a "soft" object, simply assign values.
Because classes in JavaScript are so flexible, you may also think of more complex class inheritance. But deep inheritance is not suitable. Shallow inheritance is more efficient and easier to express.