I. Is JS Turning into Java!?
ES6 enables keywords like class, constructor, static, extends, super etc. to support class definitions, feels like it's about to become Java, finally don't need to worry about prototype?
Not like that. Enabling this set of keywords is merely to reduce "syntax noise", reduce workload when defining class structures, JS's prototype-based object system cannot be easily changed to class-based, this is JS language designers' choice, impossible to uproot the existing object system and stitch Java's system in.
P.S. "Syntax noise" is a term borrowed from Old Zhao, using Python and Java to implement the same functionality, the diff result is "syntax noise", such as function, prototype etc. in JS that are uncomfortable to type
Moreover, prototype-based object system is relatively flexible, for example JS can dynamically modify class inheritance tree at runtime, therefore we can pre-define empty class inheritance tree, add class members only when needed, for example:
// Empty inheritance tree
var SuperType = function() {};
var SubType = function() {};
SubType.prototype = new SuperType();
// Dynamic enhancement
void function() {
//...
SuperType.prototype.cl = console.log.bind(console);
new SubType().cl('invoked by subType'); // invoked by subType
}();
This feeling is quite magical, Java and JS painting together, Java uses fine carving knife on canvas meticulously completed Along the River During the Qingming Festival, framed and hung on wall, amazing everyone around. JS picks up marker pen draws two wavy horizontal lines, says "I'm done, this is Along the River During the Qingming Festival", audience feels fooled, but reluctantly frames JS's work, hangs it next to Java's masterpiece. Java stands still waiting for judgment, at this time JS starts working, picks up pen seriously completes identical work on frame glass surface, audience feels incredible. Suddenly a voice comes from crowd, "the third house from left should be pointed roof, you all painted it wrong", Java's face can't hold it, hurriedly spreads out another rice paper, wants to repaint one to fix that eyesore error. JS points to upper left corner saying "is it here? I've already fixed it"
JS is imperative language, dynamic language and functional language combination, for the entire system, object system needs this flexibility brought by prototype implementation, no need to envy Java's meticulously carved class-based object system, completely doesn't fit. ES specification designers won't and have no reason to copy Java,至此,JavaScript and Java still have nothing to do with each other, like 20 years ago
P.S. JS and the author were both born in 1995, as for Netscape with JavaScript and Java Applet's connections, please search yourself
II. New Features Brought by class
"class" before ES6 might be like this:
function Circle(radius) {
if (typeof radius !== 'number') {
throw new TypeError('radius: a number expected');
}
// Instance properties
this.radius = radius;
// Static properties
Circle.count++;
}
Circle.count = 0;
// Prototype properties
Circle.prototype.getArea = function() {
return Math.PI * this.radius * this.radius;
};
Each instance obtained from new Circle has two properties, instance property radius and prototype property getArea
Occasionally can see more rigorous "class" implemented with ES5 features:
// More rigorous, defining getter/setter
function CircleEx(radius) {
this.radius = radius;
CircleEx.count++;
}
// Define static properties
Object.defineProperty(CircleEx, 'count', {
get: function() {
// this points to CircleEx
// console.log(this);
// CircleEx.count++ first get returns 0
// then set adds _count property to CircleEx and assigns value 1
return !this._count ? 0 : this._count;
},
set: function(val) {
this._count = val;
}
});
CircleEx.prototype.getArea = function() {
return Math.PI * this.radius * this.radius;
};
// Define prototype properties
// Actually defining getter/setter for instance properties
Object.defineProperty(CircleEx.prototype, 'radius', {
get: function() {
//!!! this points to CircleEx instance
// not its prototype object
// console.log(this);
// In constructor this.radius = radius;
// Look up radius property, triggers get, returns this._radius
// Assigns to radius
return this._radius;
},
set: function(val) {
if (typeof val !== 'number') {
throw new TypeError('radius: a number expected');
}
this._radius = val;
}
});
Each instance obtained from new CircleEx has two properties, instance property _radius and prototype property getArea, as for radius, it's accessor (getter/setter) defined on prototype object, non-enumerable
Previously said "occasionally" can see such "class" definitions, because it's too troublesome, everyone is too lazy to use. ES6 class part's goal is to change current situation, provide more convenient class member definition methods
Simplified Object Definition Method
Can directly define getter/setter in object literals, simplified function type property definition methods, including general functions, generators and dynamic function names, for example:
// getter/setter
var obj = {
// getter
//!!! getter has no parameters, cannot pass parameters
get attr() {
console.log('getter');
return this._attr || 0;
},
// setter
//!!! setter accepts at least one parameter
set attr(val) {
console.log('setter');
this._attr = val;
},
// Pre-computed properties (function properties added with [] syntax, dynamic function names)
[(function() {return 'print';})()](arg) {
console.log(arg);
},
// General method
fn(arg) {
console.log(`arg = ${arg}`);
},
// Generator
*gen(i) {
while(true) {
yield i++;
}
}
}
Lengthy function keyword completely disappeared from type definitions, even more concise than arrow functions. Felt the joy when first learning JS again—adding a pair of parentheses to variable is function call, how so simple and crude?
Rewrite previous CircleEx class with ES6 enhanced object literals, will be like this:
// Rewrite CircleEx
CircleEx.prototype = {
getArea() {
return Math.PI * this.radius * this.radius;
},
get radius() {
return this._radius;
},
set radius(val) {
if (typeof val !== 'number') {
throw new TypeError('radius: a number expected');
}
this._radius = val;
}
};
Note, different from before, at this time accessor radius is enumerable, and appears as a prototype property. But similarly, accessor itself won't be exposed, all operations targeting radius are operations on accessor's return value, not accessor itself, therefore:
typeof new CircleEx(1).radius === 'number' // true
Enabled class Definition
P.S. Why say "enabled" instead of "introduced", because all ES6 class related keywords were originally reserved words, just now endowed with clear meanings
In class definition, constructor represents constructor function, static keyword used to distinguish general functions and special functions (same meaning as Java, C++)
constructor is optional, defaults to providing empty constructor (constructor() {}), constructor must be literal form, cannot be dynamic function name, otherwise will get general method named constructor, not constructor function
Rewrite previous Circle class with class syntax, as follows:
// Rewrite CircleEx
class MyCircle {
// Constructor
constructor(radius) {
this.radius = radius;
MyCircle.count++;
}
// Instance property getter/setter
get radius() {
return this._radius;
}
set radius(val) {
if (typeof val !== 'number') {
throw new TypeError('radius: a number expected');
}
this._radius = val;
}
// Prototype properties
getArea() {
return Math.PI * this.radius * this.radius;
}
// Static properties
static get count() {
return this._count || 0;
}
static set count(val) {
this._count = val;
}
}
Regardless of actual effects, but at least looks much clearer
Type Protection
Built-in type protection, types defined with class syntax must be called through new operator, for example:
// Call as general function, will error
MyCircle();
// Uncaught TypeError: Class constructors cannot be invoked without 'new'
This built-in protection can avoid some problems, but indeed also limits flexibility, for example some library provided APIs contain functions that can be called as general functions, also can be used as constructors, cannot be implemented with class syntax
Additionally, class names referenced in class definitions won't be changed by external forces, for example:
class T {
static get val() {
return 'val';
}
get() {
return T.val;
}
}
// test
var t = new T();
console.log(t.get()); // val
// External force damage
T = null;
console.log(t.get()); // val
//! Error Uncaught TypeError: T is not a constructor
// new T();
Before ES6 there was no such protection, rewrite with function, after遭受 external force damage will definitely error
Supports Class Expressions (Anonymous Classes)
// Anonymous class
var circle = new class {
constructor(radius) {
this.radius = radius;
}
}(3);
console.log(circle.radius); // 3
Anonymous classes seem useless, because generally speaking, classes are object templates, through classes can achieve mass production of objects, Java uses anonymous classes to create temporary objects that don't need mass production, while JS has N methods to create objects, using anonymous classes might be the stupidest way
Methods Defined in Class are Configurable Non-enumerable
For example MyClass.prototype (defined through class syntax) all properties are non-enumerable, methods defined in class are also non-enumerable
Doing this seems to be deliberately concealing the fact that object system is based on prototype, for example for...in cannot enumerate previous MyCircle.prototype, but can discover some traces through Object.getOwnPropertyNames(), for example:
console.log(Object.getOwnPropertyNames(MyCircle.prototype));
// log print:
// ["constructor", "radius", "getArea"]
These properties all truly exist on prototype object, but default non-enumerable conceals this fact, may not hope we while using class syntax, manually operate prototype object, breaking existing rules
May always feel this kind of concealing prototype approach is inappropriate, but can't say where wrong, okay, consider classic problem of prototype inheritance: prototype reference properties will be shared among instances, for example:
function Type() {}
Type.prototype.issue = [1, 2];
// test
var t1 = new Type();
var t2 = new Type();
t1.issue.push(3);
console.log(t2.issue); // [1, 2, 3]
Does class syntax have this problem? First we must use class syntax to define an array type property on Type's prototype,沮丧地发现 fundamentally cannot do it, unless directly accessing prototype, so no need to worry this kind of "concealing" will trigger hidden problems, ES6 designers considered much more than us
III. Summary
Clean type definition syntax, keep it simple
ES is gradually removing syntax noise:
Arrow functions + `class` removed `function`
Default parameters + rest parameters removed `arguments`
`class` + `extends` + `super` removed `prototype`
From functional language perspective, these changes are excellent, what mathematicians pursue is ultimate concise beauty
Reference Materials
-
"ES6 in Depth": Free e-book provided by InfoQ Chinese station
-
"JavaScript Language Essence and Programming Practice"
No comments yet. Be the first to share your thoughts.