|
|
@@ -1,37 +1,79 @@ |
|
|
// Here is a proposal for minimalist JavaScript classes, humbly offered. |
|
|
|
|
|
// There are (at least) two different directions in which classes can be steered. |
|
|
// If we go for a wholly new semantics and implementation, then fancier classical |
|
|
// inheritance can be supported with parallel prototype chains for true inheritance |
|
|
// of properties at both the class and instance level. |
|
|
|
|
|
// If however, we keep current JavaScript prototype semantics, and add a form that |
|
|
// can desugar to ES3, things must necessarily stay simpler. This is the direction |
|
|
// I'm assuming here. |
|
|
|
|
|
// If we want to have static class bodies (no executable code at the top level), |
|
|
// then we would do well to reuse the known and loved JavaScript idiom for |
|
|
// fixed lists of properties -- the object literal. |
|
|
// A response to jashkenas's fine proposal for minimalist JavaScript classes. |
|
|
|
|
|
// Harmony always stipulated classes as sugar, so indeed we are keeping current |
|
|
// JavaScript prototype semantics, and classes would only add a syntactic form |
|
|
// that can desugar to ES5. This is mostly the same assumption that Jeremy |
|
|
// chose, but I've stipulated ES5. |
|
|
|
|
|
// Where I part company is on reusing the object literal. It is not the syntax |
|
|
// most classy programmers expect, coming from other languages. It has annoying |
|
|
// and alien overhead, namely colons and commas. For JS community members who |
|
|
// don't want classes, either proposal is "bad", so let's focus on the class |
|
|
// fans who will use this feature. |
|
|
// |
|
|
// Therefore I propose giving classes their own new kind of body syntax, which |
|
|
// is neither an object literal nor a function body. The main advantage is that |
|
|
// syntax for class elements can be tuned to usability, even as it desugars to |
|
|
// kernel semantics drawn from ES5 extended by both the super and private names |
|
|
// proposals. |
|
|
|
|
|
// Not shown below is the super proposal at |
|
|
// |
|
|
// http://wiki.ecmascript.org/doku.php?id=harmony:object_initialiser_super |
|
|
// |
|
|
// which composes as expected when 'super' is used in methods defined in a |
|
|
// class instead of in an object literal. |
|
|
// |
|
|
// In contrast, you will see hints of the private name objects proposal at |
|
|
// |
|
|
// http://wiki.ecmascript.org/doku.php?id=harmony:private_name_objects |
|
|
// |
|
|
// Also visbile throughout: the method definition shorthand syntax proposed at |
|
|
// |
|
|
// http://wiki.ecmascript.org/doku.php?id=harmony:object_literals#object_literal_property_shorthands |
|
|
// |
|
|
// For programmers who want classes as sugar for constructors and prototype, I |
|
|
// hope this proposal wins. |
|
|
// |
|
|
// To be perfectly honest, I personally would be happy with Allen Wirfs-Brock's |
|
|
// "Exemplars" approach and no classes. But for programmers coming from classy |
|
|
// languages, I believe "minimalist classes" will tend to be too minimal to be |
|
|
// usable out of the box. They'll want more. |
|
|
// |
|
|
// Brendan Eich, 1-Nov-2011 |
|
|
|
|
|
// First, basic usage from a real-world library (Three.js) |
|
|
|
|
|
class Color { |
|
|
|
|
|
constructor: function(hex) { |
|
|
constructor(hex) { |
|
|
... |
|
|
}, |
|
|
|
|
|
r: 1, g: 1, b: 1, |
|
|
} |
|
|
|
|
|
copy: function(color) { |
|
|
// Prototype data properties, introduced by var or const at class body level. |
|
|
// Rationale: object literal syntax requires comma separation and has no way |
|
|
// to define a const that's clearly in Harmony. Classy languages do not need |
|
|
// no steenkin' commas, but we do want const in ES.next. |
|
|
// |
|
|
// An objection even I have made: we should not conflate property definition |
|
|
// syntax with lexical binding declaration syntax. But the cat's out of the |
|
|
// bag with 'var' in global code in ES1-5, and it's hard to beat "const" as |
|
|
// the keyword for initialize-only data properties. |
|
|
// |
|
|
// One fair objection: this syntax may mislead people into thinking methods |
|
|
// lexically close over prototype properties declared this way. A calculated |
|
|
// risk, what can I say? |
|
|
|
|
|
var r = 1, g = 1, b = 1; |
|
|
|
|
|
copy(color) { |
|
|
... |
|
|
}, |
|
|
} |
|
|
|
|
|
setRGB: function(r, g, b) { |
|
|
setRGB(r, g, b) { |
|
|
... |
|
|
}, |
|
|
} |
|
|
|
|
|
setHSV: function(h, s, v) { |
|
|
setHSV(h, s, v) { |
|
|
... |
|
|
} |
|
|
|
|
|
@@ -42,115 +84,148 @@ class Color { |
|
|
|
|
|
class Fox extends Animal { |
|
|
... |
|
|
} |
|
|
|
|
|
// Note that "Animal" here is a class object (constructor function) in its |
|
|
// own right. Fox.prototype is set to an instance of Animal that has been |
|
|
// constructed without calling its constructor function -- this is the |
|
|
// usual two-step setting-up-a-prototype shuffle. |
|
|
|
|
|
|
|
|
// There is no special syntax for setting class-level properties, as they are |
|
|
// relatively rare. Just add them to the class object itself: |
|
|
|
|
|
Fox.CONSTANT = value; |
|
|
// The special syntax for defining class-level properties and methods uses a |
|
|
// prefix keyword. The 'static' keyword is reserved and somewhat hated (or at |
|
|
// least considered a misnomer), but I went with it instead of 'class' to |
|
|
// avoid confusion in overloading the c-word, and to be more future-friendly |
|
|
// in case nested classes are wanted (right now I do not want them, since a |
|
|
// class at class-body level would declare a prototype inner class and not a |
|
|
// static inner class). |
|
|
|
|
|
static const CONSTANT = 42; |
|
|
|
|
|
// Note that the right-hand side of a class definition is just an expression, |
|
|
// an object literal is not required. You can be fully dynamic when creating a |
|
|
// class: |
|
|
static classMethod() { |
|
|
... |
|
|
} |
|
|
} |
|
|
|
|
|
class Student objectContainingStudentProperties |
|
|
console.log(Fox.CONSTANT); |
|
|
|
|
|
// Or even: |
|
|
Fox.classMethod(); |
|
|
|
|
|
class Protester merge(YoungAdult, WorkEthic, Idealism, { |
|
|
student: true |
|
|
}) |
|
|
|
|
|
// The point I'm trying to make being that the own properties of the right hand |
|
|
// side, however they're derived, become the prototypal properties of the resulting |
|
|
// class. |
|
|
// Classes are expression forms as well as declaration forms. As with function |
|
|
// expressions, the name is optional in a class expression. |
|
|
|
|
|
const frozenClass = Object.freeze(class {...}); |
|
|
|
|
|
// Similarly, class definitions are themselves expressions, and anonymous classes |
|
|
// are equally possible: |
|
|
// Named class expressions are supported too. The name is bound only in the |
|
|
// scope of the class body, as for named function expressions. |
|
|
|
|
|
animals.push(class Fox {}); |
|
|
|
|
|
// An anonymous class expression with superclass specification, after Jeremy's |
|
|
// gist, but with an explicit and required body. |
|
|
|
|
|
var subclass = function(parent) { |
|
|
return class extends parent; |
|
|
return class extends parent { |
|
|
... |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
// Naturally, classes can be built up programmatically in this fashion. |
|
|
|
|
|
var generateModelClass = function(columns) { |
|
|
// Unlike Jeremy's proposal, classes cannot be built up programmatically by |
|
|
// abutting an expression to a class head. That's too dynamic, it doesn't work |
|
|
// with the 'super' proposal. So any synthesis of a class body must use eval |
|
|
// or equivalent. At this time, I do not propose adding a Class constructor |
|
|
// analogous to Function, but I believe it could be added without issue. |
|
|
|
|
|
var definition = {}; |
|
|
|
|
|
columns.forEach(function(col) { |
|
|
definition['get' + col] = function() { |
|
|
return this[col]; |
|
|
}; |
|
|
definition['set' + col] = function(value) { |
|
|
return this[col] = value; |
|
|
}; |
|
|
}); |
|
|
|
|
|
return class definition; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
// Finally, the Monster class from the current nutshell proposal |
|
|
// As in Jeremy's gist, here's the Monster class from the harmony: proposal |
|
|
// (http://wiki.ecmascript.org/doku.php?id=harmony:classes#the_proposal_in_a_nutshell) |
|
|
// ... sans unnecessary restrictions: |
|
|
// ... sans unnecessary 'public' keyword prefixing! |
|
|
|
|
|
class Monster { |
|
|
|
|
|
constructor: function(name, health) { |
|
|
constructor(name, health) { |
|
|
this.name = name; |
|
|
this.health = health; |
|
|
}, |
|
|
} |
|
|
|
|
|
attack: function(target) { |
|
|
attack(target) { |
|
|
log("The monster attacks " + target); |
|
|
}, |
|
|
} |
|
|
|
|
|
isAlive: function() { |
|
|
isAlive() { |
|
|
return this.health > 0; |
|
|
}, |
|
|
} |
|
|
|
|
|
setHealth: function(value) { |
|
|
setHealth(value) { |
|
|
if (value < 0) { |
|
|
throw new Error("Health must be non-negative."); |
|
|
} |
|
|
this.health = value; |
|
|
}, |
|
|
} |
|
|
|
|
|
numAttacks: 0, |
|
|
var numAttacks = 0; |
|
|
|
|
|
attackMessage: "The monster hits you!" |
|
|
const attackMessage = "The monster hits you!"; |
|
|
|
|
|
} |
|
|
|
|
|
// Now there's one more problem to address, which could be deferred, but which |
|
|
// falls under "batteries included" to some (not all) of the cohorts of classy |
|
|
// programmers using JS or coming to it fresh in the future. And that is the |
|
|
// cost of this[kPrivateName] given const kPrivateName = Name.create(...) where |
|
|
// module Name from "@name" has been declared. |
|
|
// |
|
|
// Instead I propose we support private kPrivateName, ...; as a special form |
|
|
// only in class bodies (for now) that both creates a private name object and |
|
|
// binds it to a lexical const binding that may be accessed on the right of @ |
|
|
// in methods defined in a class. |
|
|
// |
|
|
// For class-private instance variables, obj@foo is supported as well (with no |
|
|
// LineTerminator to the left of @ in this case, or it's short for this[foo]). |
|
|
// See the new sameName method for an example of infix and prefix @ in action. |
|
|
// |
|
|
// There is no const instance variable declaration. Non-writable instance vars |
|
|
// (properties to most people) are quite rare. Use ES5's Object.defineProperty |
|
|
// if you must. This avoids ugly fights about read before constant instance var |
|
|
// initialization too. |
|
|
|
|
|
class Monster { |
|
|
|
|
|
private name, health; |
|
|
|
|
|
// I think that's about the run of it. Note what is left out: public / private / |
|
|
// static / frozen / const properties and their ilk. Personally, I'm of the view |
|
|
// that all of these modifiers are deeply undesirable in a language as dynamic |
|
|
// as JavaScript and won't be much used, if added ... but I also think that |
|
|
// getters and setters should be deprecated and removed. |
|
|
constructor(name, health) { |
|
|
@name = name; |
|
|
@health = health; |
|
|
} |
|
|
|
|
|
sameName(other) { |
|
|
return @name === other@name; |
|
|
} |
|
|
|
|
|
// If public / private / static / frozen / const must be a part of class syntax |
|
|
// in JS.next, then they must be valid prefixes for object literals as well -- |
|
|
// and can easily be used to define classes with those properties under this |
|
|
// proposal. |
|
|
attack(target) { |
|
|
log("The monster attacks " + target); |
|
|
} |
|
|
|
|
|
// There are no new semantics here, and these classes can easily be transpiled |
|
|
// into ES3 if needed -- just simpler declaration of constructors with prototypal |
|
|
// properties and correctly configured prototype chains. |
|
|
isAlive() { |
|
|
return @health > 0; |
|
|
} |
|
|
|
|
|
// tl;dr |
|
|
// Classes are a new expression with the form ([] means optional): |
|
|
// class [name] [extends parent] [expression] |
|
|
setHealth(value) { |
|
|
if (value < 0) { |
|
|
throw new Error("Health must be non-negative."); |
|
|
} |
|
|
@health = value; |
|
|
} |
|
|
|
|
|
var numAttacks = 0; |
|
|
|
|
|
const attackMessage = "The monster hits you!"; |
|
|
|
|
|
} |
|
|
|
|
|
// As in the harmony:classes proposal, 'const class' freezes all of |
|
|
// 1. the class binding, if named declaration rather than expression, |
|
|
// 2. the class prototype induced by the const class, |
|
|
// 3. the constructor method of the const class. |
|
|
// |
|
|
// 'const class' also seals instances constructed by the constructor, as if the |
|
|
// last line of the constructor method were Object.seal(this), called using the |
|
|
// original values of Object and Object.seal. |
|
|
|
|
|
const class Frosty { |
|
|
} |