Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
809 views
in Technique[技术] by (71.8m points)

ecmascript 6 - Are ES6 classes just syntactic sugar for the prototypal pattern in Javascript?

After playing with ES6, I've really started to like the new syntax and features available, but I do have a question about classes.

Are the new ES6 classes just syntactic sugar for the old prototypal pattern? Or is there more going on here behind the scenes? For example:

class Thing {
   //... classy stuff
  doStuff(){}
}

vs:

var Thing = function() {
  // ... setup stuff
};

Thing.prototype.doStuff = function() {}; // etc
Question&Answers:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

No, ES6 classes are not just syntactic sugar for the prototypal pattern.

While the contrary can be read in many places and while it seems to be true on the surface, things get more complex when you start digging into the details.

I wasn't quite satisfied with the existing answers. After doing some more research, this is how I classified the features of ES6 classes in my mind:

  1. Syntactic sugar for the standard ES5 pseudoclassical inheritance pattern.
  2. Syntactic sugar for improvements to the pseudoclassical inheritance pattern available but impractical or uncommon in ES5.
  3. Syntactic sugar for improvements to the pseudoclassical inheritance pattern not available in ES5, but which can be implemented in ES6 without the class syntax.
  4. Features impossible to implement without the class syntax, even in ES6.

(I have tried to make this answer as complete as possible and it became quite long as a result. Those more interested in a good overview should look at traktor53’s answer.)


So let me 'desugar' step by step (and as far as possible) the class declarations below to illustrate things as we go along:

// Class Declaration:
class Vertebrate {
    constructor( name ) {
        this.name = name;
        this.hasVertebrae = true;
        this.isWalking = false;
    }

    walk() {
        this.isWalking = true;
        return this;
    }

    static isVertebrate( animal ) {
        return animal.hasVertebrae;
    }
}

// Derived Class Declaration:
class Bird extends Vertebrate {
    constructor( name ) {
        super( name )
        this.hasWings = true;
    }

    walk() {
        console.log( "Advancing on 2 legs..." );
        return super.walk();
    }

    static isBird( animal ) {
        return super.isVertebrate( animal ) && animal.hasWings;
    }
}

1. Syntactic sugar for the standard ES5 pseudoclassical inheritance pattern

At their core, ES6 classes indeed provide syntactic sugar for the standard ES5 pseudoclassical inheritance pattern.

Class Declarations / Expressions

In the background a class declaration or a class expression will create a constructor function with the same name as the class such that:

  1. The internal [[Construct]] property of the constructor refers to the code block attached to the class' constructor() method.
  2. The classe' methods are defined on the constructor’s prototype property (we are not including static methods for now).

Using ES5 syntax, the initial class declaration is thus roughly equivalent to the following (leaving out static methods):

function Vertebrate( name ) {           // 1. A constructor function containing the code of the class's constructor method is defined
    this.name = name;
    this.hasVertebrae = true;
    this.isWalking = false;
}

Object.assign( Vertebrate.prototype, {  // 2. Class methods are defined on the constructor's prototype property
    walk: function() {
        this.isWalking = true;
        return this;
    }
} );

The initial class declaration and the above code snippet will both yield the following:

console.log( typeof Vertebrate )                                    // function
console.log( typeof Vertebrate.prototype )                          // object

console.log( Object.getOwnPropertyNames( Vertebrate.prototype ) )   // [ 'constructor', 'walk' ]
console.log( Vertebrate.prototype.constructor === Vertebrate )      // true
console.log( Vertebrate.prototype.walk )                            // [Function: walk]

console.log( new Vertebrate( 'Bob' ) )                              // Vertebrate { name: 'Bob', hasVertebrae: true, isWalking: false }

Derived Class Declarations / Expressions

In addition to to the above, derived class declarations or derived class expressions will also set up an inheritance between the constructors' prototype properties and make use of the super syntax such that:

  1. The prototype property of the child constructor inherits from the prototype property of the parent constructor.
  2. The super() call amounts to calling the parent constructor with this bound to the current context.
    • This is only a rough approximation of the functionality provided by super(), which would also set the implicit new.target parameter and trigger the internal [[Construct]] method (instead of the [[Call]] method). The super() call will get fully 'desugared' in section 3.
  3. The super[method]() calls amount to calling the method on the parent's prototype object with this bound to the current context (we are not including static methods for now).
    • This is only an approximation of super[method]() calls which don't rely on a direct reference to a parent class. super[method]() calls will get fully replicated in section 3.

Using ES5 syntax, the initial derived class declaration is thus roughly equivalent to the following (leaving out static methods):

function Bird( name ) {
    Vertebrate.call( this,  name )                          // 2. The super() call is approximated by directly calling the parent constructor
    this.hasWings = true;
}

Bird.prototype = Object.create( Vertebrate.prototype, {     // 1. Inheritance is established between the constructors' prototype properties
    constructor: {
        value: Bird,
        writable: true,
        configurable: true
    }
} );

Object.assign( Bird.prototype, {                            
    walk: function() {
        console.log( "Advancing on 2 legs..." );
        return Vertebrate.prototype.walk.call( this );        // 3. The super[method]() call is approximated by directly calling the method on the parent's prototype object
    }
})

The initial derived class declaration and the above code snippet will both yield the following:

console.log( Object.getPrototypeOf( Bird.prototype ) )      // Vertebrate {}
console.log( new Bird("Titi") )                             // Bird { name: 'Titi', hasVertebrae: true, isWalking: false, hasWings: true }
console.log( new Bird( "Titi" ).walk().isWalking )          // true

2. Syntactic sugar for improvements to the pseudoclassical inheritance pattern available but impractical or uncommon in ES5

ES6 classes further provide improvements to the pseudoclassical inheritance pattern that could already have been implemented in ES5, but were often left out as they could be a bit impractical to set up.

Class Declarations / Expressions

A class declaration or a class expression will further set things up in the following way:

  1. All code inside the class declaration or class expression runs in strict mode.
  2. The class’s static methods are defined on the constructor itself.
  3. All class methods (static or not) are non-enumerable.
  4. The constructor’s prototype property is non-writable.

Using ES5 syntax, the initial class declaration is thus more precisely (but still only partially) equivalent to the following:

var Vertebrate = (function() {                              // 1. Code is wrapped in an IIFE that runs in strict mode
    'use strict';

    function Vertebrate( name ) {
        this.name = name;
        this.hasVertebrae = true;
        this.isWalking = false;
    }

    Object.defineProperty( Vertebrate.prototype, 'walk', {  // 3. Methods are defined to be non-enumerable
        value: function walk() {
            this.isWalking = true;
            return this;
        },
        writable: true,
        configurable: true
    } );

    Object.defineProperty( Vertebrate, 'isVertebrate', {    // 2. Static methods are defined on the constructor itself
        value: function isVertebrate( animal ) {            // 3. Methods are defined to be non-enumerable
            return animal.hasVertebrae;
        },
        writable: true,
        configurable: true
    } );

    Object.defineProperty( Vertebrate, "prototype", {       // 4. The constructor's prototype property is defined to be non-writable:
        writable: false 
    });

    return Vertebrate
})();
  • NB 1: If the surrounding code is already running in strict mode, there is of course no need to wrap everything in an IIFE.

  • NB 2: Although it was possible to define static properties without problem in ES5, this was not very common. The reason for this may be that establishing inheritance of static properties was not possible without the use of the then non-standard __proto__ property.

Now the initial class declaration and the above code snippet will also both yield the following:

console.log( Object.getOwnPropertyDescriptor( Vertebrate.prototype, 'walk' ) )      
// { value: [Function: walk],
//   writable: true,
//   enumerable: false,
//   configurable: true }

console.log( Object.getOwnPropertyDescriptor( Vertebrate, 'isVertebrate' ) )    
// { value: [Function: isVertebrate],
//   writable: true,
//   enumerable: false,
//   configurable: true }

console.log( Object.getOwnPropertyDescriptor( Vertebrate, 'prototype' ) )
// { value: Vertebrate {},
//   writable: false,
//   enumerable: false,
//   configurable: false }

Derived Class Declarations / Expressions

In addition to to the above, derived class declarations or derived class expressions will also make use of the super syntax such that:

  1. The super[method]() calls inside static methods amount to calling the method on the parent's constructor with this bound to the current context.
    • This is only an approximation of super[method]() calls which don't rely on a direct reference to a parent class. super[method]() calls in static methods cannot fully be mimicked without the use of the class syntax and are listed in section 4.

Using ES5 syntax, the initial derived class declaration is thus more precisely (but still only partially) equivalent to the following:

function Bird( name ) {
    Vertebrate.call( this,  name )
    this.hasWings = true;
}

Bird.prototype = Object.create( Vertebrate.prototype, {
    constructor: {
        value: Bird,
        writable: true,
        configurable: true
    }
} );

Object.defineProperty( Bird.prototype, 'walk', {
    value: function walk( animal ) {
        return Vertebrate.prototype.walk.call( this );
    },
    writable: true,
    configurable: true
} );

Object.defineProperty( Bird, 'isBird', {
    value: function isBird( animal ) {
        return Vertebrate.isVertebrate.call( this, animal ) && animal.hasWings;    // 1. The super[method]() call is approximated by directly calling the method on the parent's constructor
    },
    writable: true,
    configurable: true
} );

Object.defineProperty( Bird, "prototype", {
    writable: fa

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...