es6类只是javascript中原型模式的语法糖吗?

big*_*olf 30 javascript ecmascript-6

在玩ES6之后,我真的开始喜欢新的语法和功能了,但我确实对类有疑问.

新的ES6类是旧原型模式的语法糖吗?或者幕后还有更多的事情发生在这里?即:

class Thing {
   //... classy stuff
  doStuff(){}
}
Run Code Online (Sandbox Code Playgroud)

vs:

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

Thing.prototype.doStuff = function() {}; // etc
Run Code Online (Sandbox Code Playgroud)

tra*_*r53 20

是的,或许,但是一些语法糖有牙齿.

声明一个类创建一个函数对象,它是类的构造函数,使用constructor在类体内提供的代码,以及与类同名的命名类.

类构造函数有一个普通的原型对象,类实例以正常的JavaScript方式从该对象继承属性.在类主体中定义的实例方法被添加到此原型中.

ES6没有提供在类主体中声明类实例默认属性值(即非方法的值)的方法,以存储在原型上并继承.要初始化实例值,您可以将它们设置为构造函数中的本地,非继承属性,或者以与prototype普通构造函数相同的方式手动将它们添加到类定义之外的类构造函数对象中.(我不是在争论为JavaScript类设置继承属性的优点或其他方面).

在类主体中声明的静态方法将添加为类构造函数的属性.避免使用与继承了标准功能属性和方法竞争静态类方法的名称Function.prototype,例如call,applylength.

较少含糖的是类声明和方法总是以严格模式执行,并且一个很少引起注意的.prototype特性:类构造函数的属性是只读的:你不能将它设置为你为某些特殊创建的其他对象目的.

扩展一个类时会发生一些有趣的事情:

  • prototype扩展类构造函数的object属性自动在prototype要扩展的类的对象上进行原型化.这不是特别新的,可以使用复制效果Object.create.

  • 扩展类构造函数(object)在被扩展的类的构造函数上自动原型化,而不是Function.虽然有可能使用Object.setPrototypeOf或甚至复制普通构造函数的效果childClass.__proto__ = parentClass,但这将是一种非常不寻常的编码实践,并且经常在JavaScript文档中被建议.

还有其他差异,例如类对象没有以使用function关键字声明的命名函数的方式提升.

我相信认为类声明和表达式在ECMA Script的所有未来版本中都将保持不变是很天真的,看看是否以及何时发生将会很有趣.可以说,将"语法糖"与ES6中引入的类(ECMA-262标准版本6)联系起来已经成为一种时尚,但我个人试图避免重复它.


ale*_*lex 13

新的ES6类是旧的原型模式的语法糖吗?

是的,它们(几乎完全)是一种方便的语法,语义几乎完全相同.Traktor53的答案涉及到差异.

资源

以下简短代码示例显示如何classprototype对象上设置a 中的函数.

class Thing {
   someFunc() {}
}

console.log("someFunc" in Thing.prototype); // true
Run Code Online (Sandbox Code Playgroud)

  • @Jai没有愚蠢的问题! (3认同)

Lew*_*wis 12

是.但他们更严格.

您的示例有两个主要差异.

首先,使用类语法,您无法在没有new关键字的情况下初始化实例.

class Thing{}
Thing() //Uncaught TypeError: Class constructor Thing cannot be invoked without 'new'

var Thing = function() {
  if(!(this instanceof Thing)){
     return new Thing();
  }
};
Thing(); //works
Run Code Online (Sandbox Code Playgroud)

第二个是,使用类语法定义的类是块作用域.它类似于使用let关键字定义变量.

class Thing{}
class Thing{} //Uncaught SyntaxError: Identifier 'Thing' has already been declared

{
    class Thing{}
}
console.log(Thing); //Uncaught ReferenceError: Thing is not defined
Run Code Online (Sandbox Code Playgroud)

编辑

正如@zeroflagL在他的评论中提到的那样,类声明也没有被提升.

console.log(Thing) //Uncaught ReferenceError: Thing is not defined
class Thing{}
Run Code Online (Sandbox Code Playgroud)

  • 类声明也没有悬挂. (6认同)

Oli*_*eke 9

不,ES6类不仅是原型模式的语法糖。

尽管在很多地方都可以理解相反的情况,虽然表面上似乎是正确的,但是当您开始深入研究细节时,事情会变得更加复杂。

我对现有答案不太满意。经过更多研究之后,这就是我脑海中对ES6类的功能进行分类的方式:

  1. 标准ES5伪古典继承模式的语法糖。
  2. 可以使用语法糖来改进伪古典继承模式,但在ES5中不切实际或不常见。
  3. 用于改进伪古典继承模式的语法糖在ES5中不可用,但是可以在没有类语法的ES6中实现。
  4. 没有class语法就无法实现的功能,即使在ES6中也是如此。

(我试图使这个答案尽可能完整,结果变得相当长。那些对概述有兴趣的人应该看看traktor53的答案。)


因此,让我一步一步(尽可能地)“ desugar”进行下面的类声明,以说明我们所经历的事情:

// 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

1.用于标准ES5伪古典继承模式的语法糖

实际上,ES6类确实为标准ES5伪古典继承模式提供了语法糖。

类声明/表达式

在后台,类声明或类表达式将创建一个与该类同名的构造函数,例如:

  1. [[Construct]]构造函数的内部属性引用附加到类的constructor()方法上的代码块。
  2. classe方法是在构造函数的prototype属性上定义的(目前我们不包括静态方法)。

因此,使用ES5语法,初始类声明大致等同于以下内容(省略了静态方法):

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;
    }
} );
Run Code Online (Sandbox Code Playgroud)

初始类声明和上面的代码片段都将产生以下内容:

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 }
Run Code Online (Sandbox Code Playgroud)

派生类声明/表达式

除上述内容外,派生类声明或派生类表达式还将在构造函数的prototype属性之间建立继承,并使用以下super语法:

  1. prototype孩子的构造函数的性质从继承prototype父构造函数的性质。
  2. super()调用等于调用this绑定到当前上下文的父构造函数。
    • 这只是所提供功能的粗略近似super(),它还会设置隐式new.target参数并触发内部[[Construct]]方法(而不是[[Call]]方法)。该super()电话将在第3节中完全“废止”
  3. super[method]()呼叫量调用父的方法prototype与对象this绑定到当前上下文(我们不包括现在的静态方法)。
    • 这仅是super[method]()不依赖直接引用父类的调用的近似值。super[method]()调用将在第3节中完全复制

因此,使用ES5语法,初始派生类声明大致等同于以下内容(省略了静态方法):

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
    }
})
Run Code Online (Sandbox Code Playgroud)

初始派生类声明和上面的代码片段都将产生以下内容:

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
Run Code Online (Sandbox Code Playgroud)

2.可以使用语法糖来改进伪古典继承模式,但在ES5中不切实际或不常见

ES6类进一步改进了可能已经在ES5中实现的伪经典继承模式,但是由于建立起来有些不切实际,因此经常被忽略。

类声明/表达式

类声明或类表达式将通过以下方式进一步设置:

  1. 类声明或类表达式中的所有代码均以严格模式运行。
  2. 类的静态方法是在构造函数本身上定义的。
  3. 所有类方法(静态或非静态)都是不可枚举的。
  4. 构造函数的prototype属性是不可写的。

因此,使用ES5语法,初始类声明更精确(但仍仅部分)等同于以下内容:

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
})();
Run Code Online (Sandbox Code Playgroud)
  • 注意1:如果周围的代码已经在严格模式下运行,则当然无需将所有内容包装在IIFE中。

  • 注意2:尽管可以在ES5中毫无问题地定义静态属性,但这并不是很常见。其原因可能是,如果不使用当时的非标准属性,则无法建立静态属性的继承__proto__

现在,初始的类声明和上面的代码片段都将产生以下内容:

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 }
Run Code Online (Sandbox Code Playgroud)

派生类声明/表达式

除上述内容外,派生类声明或派生类表达式还将使用以下super语法:

  1. super[method]()静态方法内部的调用等同于在this绑定到当前上下文的父级构造函数上调用该方法。
    • 这仅是super[method]()不依赖直接引用父类的调用的近似值。super[method]()如果不使用class语法,则不能完全模仿静态方法中的调用,这在第4节中列出。

因此,使用ES5语法,初始派生类声明更精确(但仍仅部分)等同于以下内容:

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: false 
});
Run Code Online (Sandbox Code Playgroud)

现在,初始派生类声明和上面的代码片段都将产生以下内容:

console.log( Bird.isBird( new Bird("Titi") ) )  // true
Run Code Online (Sandbox Code Playgroud)

3.用于改进ES5中不可用的伪经典继承模式的语法糖

ES6类进一步提供了对伪经典继承模式的改进,这些改进在ES5中不可用,但是可以在ES6中实现,而不必使用类语法。

类声明/表达式

在其他地方找到的ES6特性也将其归类,尤其是:

  1. 类声明的行为类似于let声明-吊起时不会初始化,并在声明之前在临时死区中结束。(相关问题
  2. 类名的行为类似于const类声明中的绑定-不能在类方法中覆盖它,尝试这样做将导致TypeError
  3. 类构造函数必须使用内部[[Construct]]方法调用,如果使用内部方法将TypeError它们作为普通函数调用,则会抛出a [[Call]]
  4. constructor()静态方法和非静态方法的类方法(除方法外)的行为类似于通过简洁方法语法定义的方法,也就是说:
    • 他们可以super通过super.prop或使用关键字super[method](这是因为他们被分配了内部[[HomeObject]]属性)。
    • 它们不能用作构造函数-它们缺少prototype属性和内部[[Construct]]属性。

因此,使用ES6语法,初始类声明甚至更精确(但仍仅部分)等同于以下内容:

let Vertebrate = (function() {                      // 1. The constructor is defined with a let declaration, it is thus not initialized when hoisted and ends up in the TDZ
    'use strict';

    const Vertebrate = function( name ) {           // 2. Inside the IIFE, the constructor is defined with a const declaration, thus preventing an overwrite of the class name
        if( typeof new.target === 'undefined' ) {   // 3. A TypeError is thrown if the constructor is invoked as an ordinary function without new.target being set
            throw new TypeError( `Class constructor ${Vertebrate.name} cannot be invoked without 'new'` );
        }

        this.name = name;
        this.hasVertebrae = true;
        this.isWalking = false;
    }

    Object.assign( Vertebrate, {
        isVertebrate( animal ) {                    // 4. Methods are defined using the concise method syntax
            return animal.hasVertebrae;
        },
    } );
    Object.defineProperty( Vertebrate, 'isVertebrate', {enumerable: false} );

    Vertebrate.prototype = {
        constructor: Vertebrate,
        walk() {                                    // 4. Methods are defined using the concise method syntax
            this.isWalking = true;
            return this;
        },
    };
    Object.defineProperty( Vertebrate.prototype, 'constructor', {enumerable: false} );
    Object.defineProperty( Vertebrate.prototype, 'walk', {enumerable: false} );

    return Vertebrate;
})();
Run Code Online (Sandbox Code Playgroud)
  • 注意1:尽管实例方法和静态方法都是用简洁的方法语法定义的,但是super引用的行为不会像静态方法中所预期的那样。实际上,内部[[HomeObject]]属性不会被复制Object.assign()[[HomeObject]]在静态方法上正确设置属性将需要我们使用对象文字来定义函数构造函数,这是不可能的。

  • 注意2:为防止构造函数在不使用new关键字的情况下调用,可以通过使用instanceof运算符在ES5中实现类似的保护措施。那些并没有涵盖所有情况(请参阅此答案)。

现在,初始的类声明和上面的代码片段都将产生以下内容:

Vertebrate( "Bob" );                                                    // TypeError: Class constructor Vertebrate cannot be invoked without 'new'
console.log( Vertebrate.prototype.walk.hasOwnProperty( 'prototype' ) )  // false
new Vertebrate.prototype.walk()                                         // TypeError: Vertebrate.prototype.walk is not a constructor
console.log( Vertebrate.isVertebrate.hasOwnProperty( 'prototype' ) )    // false
new Vertebrate.isVertebrate()                                           // TypeError: Vertebrate.isVertebrate is not a constructor
Run Code Online (Sandbox Code Playgroud)

派生类声明/表达式

除上述内容外,以下内容也适用于派生类声明或派生类表达式:

  1. 子构造函数从父构造函数继承(即派生类继承静态成员)。
  2. 调用super()派生类的构造函数等于[[Construct]]用当前new.target值调用父构造函数的内部方法,并将this上下文绑定到返回的对象。

因此,使用ES6语法,初始派生类声明更精确(但仍仅部分)等同于以下内容:

let Bird = (function() {
    'use strict';

    const Bird = function( name ) {
        if( typeof new.target === 'undefined' ) {
            throw new TypeError( `Class constructor ${Bird.name} cannot be invoked without 'new'` );
        }

        const that = Reflect.construct( Vertebrate, [name], new.target );   // 2. super() calls amount to calling the parent constructor's [[Construct]] method with the current new.target value and binding the 'this' context to the returned value (see NB 2 below)
        that.hasWings = true;
        return that;
    }

    Bird.prototype = {
        constructor: Bird,
        walk() {   
            console.log( "Advancing on 2 legs..." );
            return super.walk();                                            // super[method]() calls can now be made using the concise method syntax (see 4. in Class Declarations / Expressions above)
        },
    };
    Object.defineProperty( Bird.prototype, 'constructor', {enumerable: false} );
    Object.defineProperty( Bird.prototype, 'walk', {enumerable: false} );

    Object.assign( Bird, {
        isBird: function( animal ) {
            return Vertebrate.isVertebrate( animal ) && animal.hasWings;    // super[method]() calls can still not be made in static methods (see NB 1 in Class Declarations / Expressions above)
        }
    })
    Object.defineProperty( Bird, 'isBird', {enumerable: false} );

    Object.setPrototypeOf( Bird, Vertebrate );                              // 1. Inheritance is established between the constructors directly
    Object.setPrototypeOf( Bird.prototype, Vertebrate.prototype );

    return Bird;
})();   
Run Code Online (Sandbox Code Playgroud)
  • 注意1:由于Object.create()只能用于设置新的非功能对象的原型,因此只能通过处理当时的非标准__proto__属性在ES5中实现在构造函数自身之间建立继承。

  • 注意2:不可能模仿super()使用this上下文的效果,因此我们必须that从构造函数中显式返回另一个对象。

现在,初始派生类声明和上面的代码片段都将产生以下内容:

console.log( Object.getPrototypeOf( Bird ) )        // [Function: Vertebrate]
console.log( Bird.isVertebrate )                    // [Function: isVertebrate]
Run Code Online (Sandbox Code Playgroud)

4.没有class语法就无法实现的功能

ES6类还提供以下功能,这些功能如果不实际使用class语法就根本无法实现:

  1. [[HomeObject]]静态类方法的内部属性指向类构造函数。
    • 对于普通的构造函数,无法实现此功能,因为它将需要通过对象常量来定义函数(另请参见上面的第3节)。对于super像我们的Bird.isBird()方法那样使用关键字的派生类的静态方法,这尤其成问题。

如果事先知道父类,则可以部分解决此问题。


结论

ES6类的某些功能只是标准ES5伪古典继承模式的语法糖。但是,ES6类还具有只能在ES6中实现的功能以及一些甚至不能在ES6中模仿的其他功能(即,不使用类语法)。

综上所述,我认为可以说ES6类比ES5伪经典继承模式更简洁,更方便和更安全地使用。因此,它们的灵活性也较差(例如,请参见此问题)。


旁注

值得指出的是在上述分类中未找到位置的其他一些类的特点:

  1. super() 仅在派生类构造函数中有效的语法,并且只能被调用一次。
  2. 之前尝试this在派生类构造函数中进行访问的super()结果为ReferenceError
  3. super() 如果没有显式返回任何对象,则必须在派生类构造函数中调用它。
  4. eval并且arguments不是有效的类标识符(尽管它们在非严格模式下是有效的函数标识符)。
  5. constructor()如果未提供任何派生类(对应constructor( ...args ) { super( ...args ); }),则会设置默认方法。
  6. 无法使用类声明或类表达式在类上定义数据属性(尽管您可以在声明后手动在类上添加数据属性)。

更多资源

  • 本章了解ES6类了解ES6由尼古拉斯Zakas是ES6类最佳写了,我所遇到的。
  • 由阿克塞尔Rauschmayer先生的2ality博客有一个非常彻底的岗位上ES6类。
  • Object Playground上有一段很棒的视频,解释了伪古典继承模式(并将其与类语法进行比较)。
  • 巴贝尔 transpiler是探索你自己的东西的好地方。

  • @jw013 感谢您指出这一点,这非常重要。我会添加一个注释。这实际上属于第 3 节,不需要任何修改,因为可以通过“Reflect.construct()”捕获异常行为。例如:`function MyArray() {return Reflect.construct(Array, [], new.target)}; Object.setPrototypeOf(MyArray.prototype, Array.prototype); Object.setPrototypeOf(MyArray, Array); const 颜色 = new MyArray(); 颜色[0] = "红色"; console.log(colors.length) // 1; console.log(colors instanceof MyArray) // true; console.log(colors instanceof Array) // true;` (3认同)
  • +1对此:-“所有类方法(静态或非静态)都是不可枚举的。” 我试图弄清楚为什么 console.log 不会为类的原型返回相同的结果。 (2认同)
  • @OliverSieweke 哦,这很聪明。很遗憾我只能投票一次,我想这绝对是对这个问题最准确、最全面的答案。 (2认同)