Preface
[class_ES6 Notes 10](/articles/class-es6 笔记 10/) mentioned that class, constructor, static and other keywords simplify class definition, including simpler getter/setter and various simplified function attribute definition methods. If just this, ES6's class seems to be missing something.
Yes, inheritance. Before ES5 there were 6 inheritance schemes, ES5 provided official beget method (Object.create()), indicating support for parasitic combinational inheritance. ES6 hopes to bring some more powerful and convenient changes, not just a tool function with a native label.
So simplifying class definition is just completed incidentally when simplifying object definition methods. What ES6 really wants to say is: now we provide a more convenient inheritance mechanism.
I. How to Inherit Static Attributes?
Thinking carefully, when we discuss JS inheritance, we seem to never have mentioned static attributes, for example:
function Super() {}
Super.staticAttr = 'static';
function Sub() {}
Sub.prototype = new Super();
console.log(Sub.staticAttr); // undefined
Doing this of course cannot inherit static attributes, and other 5 similar schemes also haven't considered the feelings of static attributes. Static attributes have few application scenarios in JS, not being able to inherit won't affect anything, but for inheritance mechanism, not being able to inherit static attributes is somewhat a regret.
If must inherit static attributes, is there a way? Let's try to analyze:
// Define a Function instance Type, the so-called "class"
function Type() {}
// Function instance's prototype is naturally what Function's prototype property points to
Type.__proto__ === Function.prototype
// Looking from the other side, Type's prototype property naturally points to the prototype of this class's instances
Type.prototype === new Type().__proto__
// So, these are two different things
Type.prototype !== Type.__proto__
The last line's left side Type should be called custom class, right side Type should be called Function instance. Because Type.prototype points to something that affects all instances of custom class Type, they can all access this thing; while Type.__proto__ points to something that affects all instances of the class Type belongs to, Type itself is one of them. prototype can only affect the future, while __proto__ will rewrite history:
function SubType() {}
var p = new Type();
SubType.prototype = p;
Type.__proto__ = {a: 1};
console.log(new SubType().a); // undefined
// p.__proto__ = {a: 1};
// console.log(new SubType().a); // 1
Every object has internal attribute __proto__, prototype chain lookup during attribute access is the process of tracing this attribute. The reason for the difference above is that SubType instance's prototype chain and SubType itself's prototype chain's only intersection is Object.prototype, besides that they have nothing to do with each other:
// SubType instance's prototype chain
new SubType().__proto__ === SubType.prototype
new SubType().__proto__.__proto__ === Type.prototype
new SubType().__proto__.__proto__.__proto__ === Object.prototype
new SubType().__proto__.__proto__.__proto__.__proto__ === null
// SubType itself's prototype chain
SubType.__proto__ === Function.prototype
SubType.__proto__.__proto__ === Object.prototype
SubType.__proto__.__proto__.__proto__ === null
newed instance's prototype chain won't have Function.prototype this link at all, unless one of its ancestors' prototype is function type, but this is unlikely, because there's no reason to do this:
// Unless doing this, but how to call this function?
Type.prototype = function() {};
Now everything is clear. So if want to inherit static attributes, we should modify the other prototype chain:
Sub.__proto__ = Super; // Inherit static attributes
Test effect with the initial example:
function Super() {}
Super.staticAttr = 'static';
function Sub() {}
Sub.prototype = new Super(); // Inherit instance attributes and prototype attributes
Sub.__proto__ = Super; // Inherit static attributes
console.log(Sub.staticAttr); // static
This is a simple prototype chain inheritance supporting static attributes, but unfortunately, we cheated, __proto__ this internal attribute is not widely compatible,沮丧地发现白忙活了 (frustratingly found all efforts were in vain), this is also why there's never been talk of inheriting static attributes all along, because we need to directly modify a Function instance's prototype, but this really cannot be done.
ES6 solved this problem, because it wants to provide a complete inheritance mechanism.
II. Prototype Manipulation API
ES6 provides Object.get/setPrototypeOf() to replace internal attribute __proto__, for example:
let obj = {};
Object.setPrototypeOf(obj, Array.prototype);
obj.push(1);
console.log(obj.pop()); // 1
console.log(obj.length); // 0
console.log(obj instanceof Array); // true
Through Object.setPrototypeOf() directly modify a normal object's prototype, making it become an array instance.
Very powerful, but need to note:
Changing the [[Prototype]] of an object is, by the nature of how modern JavaScript engines optimize property accesses, a very slow operation, in every browser and JavaScript engine. The effects on performance of altering inheritance are subtle and far-flung, and are not limited to simply the time spent in obj.proto = ... statement, but may extend to any code that has access to any object whose [[Prototype]] has been altered. If you care about performance you should avoid setting the [[Prototype]] of an object. Instead, create a new object with the desired [[Prototype]] using Object.create().
(Excerpted from Object.setPrototypeOf() - JavaScript | MDN)
Main point is this API has very poor performance, manually tampering prototype chain will cause JS engine unable to optimize property access, the performance impact brought is much greater than tampering obj.__proto__, because all code accessing objects whose prototype has been tampered will be affected. If you care about performance, suggest using Object.create() to create a new prototype, then use this as template to create needed objects.
Poor performance is because tampering __proto__ is rewriting history (travel back to the past, swap key figures, will rewrite all history after that point), while modifying Type.prototype performance isn't poor, because it's paving the future, doesn't affect already existing things, classic example:
function Super() {
this.key = 'value';
}
function Sub() {}
var obj1 = new Sub();
// Paving the future
Sub.prototype = new Super();
var obj2 = new Sub();
console.log(obj1.key); // undefined
console.log(obj2.key); // value
Because modifying prototype doesn't affect already existing things (obj1), performance impact is naturally small. While __prototype__ is obviously different:
function Super() {
this.key = 'value';
}
function Sub() {}
var obj1 = new Sub();
// Rewriting history
Sub.prototype.__proto__ = new Super();
var obj2 = new Sub();
console.log(obj1.key); // value
console.log(obj2.key); // value
Although two examples aren't controlled experiment, latter modifies obj's next-next link, but we successfully rewrote history, while using prototype definitely cannot achieve this (modify Object.prototype? Indeed can, but in this case, what's the point of discussing inheritance?)
So, inheritance comes with a price, inheriting native classes costs even more (native classes have some intertwined things internally, and array objects and normal objects' memory layouts are different, just thinking about it is troublesome)
III. Most Perfect Inheritance
ES5's most perfect inheritance method is parasitic combinational, as follows:
function Super(){
// Only declare basic attributes and reference attributes here
this.val = 1;
this.arr = [1];
}
Super.staticProp = 1; // Static attribute
// Declare functions here
Super.prototype.fun1 = function(){};
Super.prototype.fun2 = function(){};
//Super.prototype.fun3...
function Sub(){
// 1.Inherit instance attributes val, arr
Super.call(this);
// ...
}
// 2.Inherit prototype attributes fun1, fun2
var proto = Object.create(Super.prototype);
proto.constructor = Sub;
Sub.prototype = proto;
Perfectly inherits instance attributes and prototype attributes, avoids prototype reference being shared between subclass instances problem and cuts off the extra copy of instance attributes.
But it also doesn't consider static attribute inheritance problem, we manually add it:
function Super(){
// Only declare basic attributes and reference attributes here
this.val = 1;
this.arr = [1];
}
Super.staticProp = 1; // Static attribute
// Declare functions here
Super.prototype.fun1 = function(){};
Super.prototype.fun2 = function(){};
//Super.prototype.fun3...
function Sub(){
// 1.Inherit instance attributes val, arr
Super.call(this);
// ...
}
// 2.Inherit prototype attributes fun1, fun2
Object.setPrototypeOf(Sub.prototype, Super.prototype);
// 3.Inherit static attributes
Object.setPrototypeOf(Sub, Super);
Here simplified inheriting prototype attributes' 3 sentences into 1 sentence, compare:
/* Previous 3 sentences */
// 1.Wrap Super.prototype into an anonymous object's prototype, return this anonymous object
var proto = Object.create(Super.prototype);
// 2.Correct constructor attribute
proto.constructor = Sub;
// 3.Let subclass instances gain access rights to anonymous object prototype attributes
// new Sub().__proto__ === proto
Sub.prototype = proto;
/* Simplified to 1 sentence */
// Effect equivalent to above 3 sentences (subclass instances gained parent class prototype attribute access rights), implementation method similar to "next-next link"
Object.setPrototypeOf(Sub.prototype, Super.prototype); // Equivalent to Sub.prototype.__proto__ = Super.prototype
Doing this besides simplifying code (3 lines to 1 line), doesn't have much meaning, considering performance, there's even less reason to do this simplification.
But Object.setPrototypeOf(Sub, Super) is unavoidable, this is up to ES6, the only legal means that can truly "rewrite history" in the real sense.
Complete example after adding static attribute inheritance support is as follows:
function Super(){
// Only declare basic attributes and reference attributes here
this.val = 1;
this.arr = [1];
}
Super.staticProp = 1; // Static attribute
// Declare functions here
Super.prototype.fun1 = function(){};
Super.prototype.fun2 = function(){};
//Super.prototype.fun3...
function Sub(){
// 1.Inherit instance attributes val, arr
Super.call(this);
// ...
}
// 2.Inherit prototype attributes fun1, fun2
Object.setPrototypeOf(Sub.prototype, Super.prototype);
// 3.Inherit static attributes
Object.setPrototypeOf(Sub, Super);
// test
var sub = new Sub();
console.log(sub.val); // 1
console.log(sub.arr); // [1]
console.log(Super.staticProp); // 1
console.log(Sub.staticProp); // 1
This is the most perfect inheritance, parasitic combinational inheritance supporting static attributes. But there's a problem, "where should what be declared" is just a moral constraint, this weak constraint is not conducive to workshop production, we need a stronger constraint.
IV. ES6 Inheritance
We've already learned class, static (see [class_ES6 Notes 10](/articles/class-es6 笔记 10/)), also know how to inherit static attributes, so ES6's inheritance should be like this:
class Super {
constructor(sub) {
console.log(sub);
this.greeting = 'hello' + (sub && `, ${sub.name}`);
}
}
class Sub {
constructor() {
// Inherit instance attributes
this.name = 'sam';
return new Super(this);
}
}
// Inherit prototype attributes
Object.setPrototypeOf(Sub.prototype, Super.prototype);
// Inherit static attributes
Object.setPrototypeOf(Sub, Super);
// test
console.log(new Sub().greeting); // hello, sam
Much more concise, looks very perfect, but:
console.log(new Sub() instanceof Super); // true
console.log(new Sub() instanceof Sub); // false
Types are indeed confused, manually modify constructor again? Too troublesome. ES6 also discovered this point, so provided extends keyword:
class Super {
constructor() {
// Instance attributes
this.val = 1;
this.arr = [1];
}
// Static attributes
static get staticProp() {
return this._staticProp || 1;
}
static set staticProp(val) {
this._staticProp = val;
}
// Prototype attributes
fun1() {}
fun2() {}
}
class Sub extends Super {
// ...
}
// test
var obj = new Sub();
console.log(obj.val); // 1
console.log(obj.arr); // [1]
console.log(Super.staticProp); // 1
console.log(Sub.staticProp); // 1
console.log(obj instanceof Super); // true
console.log(obj instanceof Sub); // true
Only now see the true purpose of several new keywords, as follows:
-
Provide strong constraints for "where should what be declared"
-
Support static attribute inheritance
-
Automatically maintain types
class Sub extends Super syntax, Super can be other classes, functions based on prototype inheritance, normal functions, variables containing functions or classes, some attribute on objects, function calls. In addition, if don't want to inherit from Object.prototype can also extends null, this provides enough flexibility, letting us easily use new syntax to extend existing classes and third-party classes.
Basic elements of inheritance mechanism are there, naturally there will be some more advanced demands, for example, how to access parent class attributes? How to pass parameters to parent class constructor?
Access Ancestor Class Attributes
Through super keyword can access ancestor class attributes, call parent class constructor, as follows:
class A {
constructor(name) {
// Instance attributes
this.name = name;
}
// Prototype attributes
fn() {
console.log('fn at A');
}
// Static attributes
static get staticProp() {
return this._staticProp || 1;
}
static set staticProp(val) {
this._staticProp = val;
}
}
class B extends A {
constructor(name) {
super(name.toUpperCase());
super.fn();
}
fn() {
super.fn();
}
}
var b = new B('BextendsfromA'); // fn at A
console.log(b.name); // BEXTENDSFROMA
super will skip attributes defined in subclass, start lookup directly from subclass prototype.
Because internally is still prototype chain lookup, so super can access ancestor class attributes limited to prototype attributes only, cannot access ancestor class's static attributes and instance attributes, for example:
class B extends A {
constructor(name) {
super(name.toUpperCase());
super.fn();
// Start prototype chain lookup from subclass instance's prototype, of course cannot find ancestor class static attributes located on another prototype chain
console.log(super.staticProp); // undefined
// Want to find ancestor class's instance attributes through prototype chain is even more impossible, obviously should find through `this.key` (instance attribute inheritance method is value copy, not holding attribute access rights)
console.log(super.key); // undefined
}
...
}
Note: super looks very similar to this, this is like a built-in variable name, may point to different values in different scopes, but super is different, super is a keyword:
typeof super; // Uncaught SyntaxError: 'super' keyword unexpected here
super instanceof SuperType; // Same as above
Either super.xxx/super['xxx'] to access parent class attributes, or super() to call parent class constructor, other forms are all illegal.
Support Extending Native Types
If CharArray extends Array, then Array.isArray() detection should return true, instanceof detection should return true, and methods like slice should also return CharArray (Chrome47 returns Array, currently 53 can correctly return CharArray), example as follows:
class CharArray extends Array {
constructor(str) {
console.log(typeof str, str);
if (str.length > 1) {
// First create instance [], otherwise this errors
// Uncaught ReferenceError: this is not defined
super();
// Then push
super.push.apply(this, str.split(''));
}
else {
super(str);
}
}
toUpperCase() {
return this.map(function(c) {
return c.toUpperCase();
});
}
}
var ca1 = new CharArray('abcde'); // string abcde
var ca2 = new CharArray('c'); // string c
console.log(ca1); // ["a", "b", "c", "d", "e"]
console.log(ca2); // ["c"]
console.log(ca1.slice(1)); // ["b", "c", "d", "e"] number 4
console.log(ca1.toUpperCase()); // ["A", "B", "C", "D", "E"] number 5
console.log(Array.isArray(ca1)); // true
console.log(ca1 instanceof CharArray); // true
//! Theoretically should return true
console.log(ca1.slice(2) instanceof CharArray); // true number 3
console.log(ca1 instanceof Array); // true
Native class's extended class besides being able to pass all existing type detections like the real thing, all inherited native methods should have same behavior. A key point in above code is console.log(typeof str, str);, we expected first parameter should be string, but actually sometimes this parameter is number (when calling slice()), this shows native slice() might be like this:
Array.prototype.slice = function(start) {
// Temporarily don't consider start being negative
let size = this.length - start;
let res = new Array(size);
for (let i = 0; i < size; i++) {
res[i] = this[start + i];
}
return res;
}
new Array(size) is why sometimes constructor receives number, this shows custom fake goods and real goods internal mechanism is completely consistent.
Subclass Instance Creation Process
In subclass constructor, this needs to call super() to obtain, using this before super() will error ReferenceError (this constraint is similar to in Java class's constructor, super() must be on first line, same principle), for example:
class C {}
class D extends C {
constructor() {
// this.a = 1; // Uncaught ReferenceError: this is not defined
super(); // Call C's default empty constructor
// Can also directly return another object, don't use this
// return {a: 1};
}
}
// test
console.log(new D());
Defining subclass constructor is telling JS "leave the matter of creating subclass instances to us, no need for you to manage anymore".
So in subclass constructor, we can call parent class constructor to create suitable instances, then borrow ancestor attributes to initialize instances, or even more roughly, we can give up this, manually return a completely unrelated other object, as the result of new operation.
Of course, can also not define constructor, indicating we don't care about subclass instance creation process, then will create a blank instance of this class.
Must call super() before accessing this is理所当然 (natural), otherwise this's structure is uncertain (is it Array or normal object?)
new.target
Ancestor class constructor can detect new.target's value to know who is calling this constructor.
Because subclass constructor must execute super() first before can get this, and certain sibling subclasses have essential differences (such as Array and normal object memory layouts are different), so parent class needs to know which kind of object should be returned as caller's this, new.target is used to solve this problem. For example:
class E {
constructor() {
switch(new.target) {
case E:
console.log('call from E');
break;
case F:
console.log('call from F');
break;
case G:
console.log('call from G');
break;
case H:
console.log('call from H');
break;
default: break;
}
}
}
// Build inheritance tree
// E
// / \
// F G
// |
// H
class F extends E {} // Default call parent class constructor
class G extends E {}
class H extends G {}
// test
new E(); // call from E
new F(); // call from F
new G(); // call from G
new H(); // call from H
P.S. new.target is legal in any function, if function is not called through new, new.target will be assigned undefined.
Base class needs to know subclass information, this is hard to understand, feels like abstraction反过来 (in reverse) depends on concrete, so why does base class need to know what subclass to new?
As mentioned above, because
Certain sibling subclasses have essential differences
Since differences are so big, why separate as 2 base classes? Doing this in Java can definitely solve the problem, but JS can't, because JS's inheritance tree has only one root node——Object, so Object needs to know whether we want to new a normal Object or Array.
Simply put, it's because JS hangs everything under Object this root node, that's why the "sibling subclasses have huge differences" problem appeared, then there's new.target solution. While Java's inheritance tree has more than one root node, so this problem doesn't exist.
V. Multiple Inheritance
There's a method called class extends Mixin:
function mix(...mixins) {
class Mix {}
function copy(target, source) {
// Named functions have name attribute
let filterSet = new Set(['constructor', 'prototype', 'name']);
// for (let key of Reflect.ownKeys(source)) {
for (let key in source) {
// if (!filterSet.has(key)) {
if (source.hasOwnProperty(key) && !filterSet.has(key)) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
for (let mixin of mixins) {
// If mixin is "class", "inherit" its static attributes and prototype attributes
// If mixin is normal object, "inherit" its instance attributes
copy(Mix, mixin);
copy(Mix.prototype, mixin.prototype);
}
return Mix;
}
Previous mix/extends scheme was mixing multiple objects into one object, now went one more step, mix multiple mixins into one class, this class's instances can "inherit" template "class"'s static attributes and prototype attributes and template object's instance attributes.
Note: "class" with double quotes is very key, because class here doesn't refer to classes defined with class keyword, but types defined with function keyword, difference is former is non-enumerable (this is also class encapsulation embodiment), copy scheme fails, then nothing can be "inherited".
Test the effect:
let obj = {key: 'value'};
function Type() {}
Type.staticProp = 'static value from Type';
Type.prototype.fn = function() {
return 'proto fn from Type';
};
let M = mix(obj, Type);
console.log(M.key); // value
console.log(M.staticProp); // static value from Type
console.log(new M().fn()); // proto fn from Type
How to say, feels effect is limited, slightly more powerful than previous mix/extends scheme, but effect is relatively limited, especially mixin doesn't support classes defined with class keyword, this scheme is hard to have further development.
So, JS still doesn't have multiple inheritance.
VI. Summary
If you've followed the entire text step by step from ES5, then there's no reason to reject ES6's class, because it solves many problems that have existed all along, such as "where should what be declared" and "more elegant inheritance implementation".
References
- "ES6 in Depth": Free e-book provided by InfoQ Chinese station
No comments yet. Be the first to share your thoughts.