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

问题描述 投票:23回答:6

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

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

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

vs:

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

Thing.prototype.doStuff = function() {}; // etc
javascript ecmascript-6
6个回答
17
投票

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

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

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

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

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

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

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

  • 扩展类构造函数的prototype对象属性自动在要扩展的类的prototype对象上进行原型化。这不是特别新的,可以使用Object.create复制效果。
  • 扩展类构造函数(object)在被扩展的类的构造函数上自动原型化,而不是Function。虽然可以使用Object.setPrototypeOf甚至childClass.__proto__ = parentClass复制对普通构造函数的影响,但这将是一种非常不寻常的编码实践,并且通常在JavaScript文档中被建议不要使用。

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

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


13
投票

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

是的,它们(几乎完全)是一种方便的语法,语义几乎完全相同。 Traktor53's answer进入差异。

Source

以下短代码示例显示了如何在class对象上设置prototype中的函数。

class Thing {
   someFunc() {}
}

console.log("someFunc" in Thing.prototype); // true

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

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

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

{
    class Thing{}
}
console.log(Thing); //Uncaught ReferenceError: Thing is not defined

Edit

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

console.log(Thing) //Uncaught ReferenceError: Thing is not defined
class Thing{}

2
投票

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

虽然相反的情况可以在许多地方阅读,虽然从表面上看似乎是正确的,但当你开始深入挖掘细节时,事情变得更加复杂。

我对现有的答案不太满意。在做了一些更多的研究之后,我就在脑海中对ES6类的特征进行了分类:

  1. 标准ES5伪经典继承模式的语法糖。
  2. 用于改进伪经典继承模式的句法糖可用但在ES5中不切实际。
  3. 用于改进伪经典继承模式的语法糖在ES5中不可用,但可以在没有类语法的情况下在ES6中实现。
  4. 即使在ES6中,没有类语法也无法实现这些功能。

(我试图让这个答案尽可能完整,结果变得很长。那些对好的概述更感兴趣的人应该看看traktor53’s answer。)


因此,让我'逐步'(尽可能)下面的类声明,以说明我们进行的事情:

// 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() {
        return super.walk();
    }
}

1. Syntactic sugar for the standard ES5 pseudoclassical inheritance pattern

从本质上讲,ES6课程确实为标准的ES5伪经典继承模式提供了语法糖。

Class Declarations / Expressions

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

  1. 构造函数的内部[[Construct]]属性是指附加到classe的constructor()方法的代码块。
  2. 类的方法是在构造函数的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;
    }
} );

初始类声明和上面的代码片段都会产生以下结果:

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

除了上述内容之外,派生类声明或派生类表达式还将在构造函数的prototype属性之间建立继承,以便:

  1. 子构造函数的protoype属性继承自父构造函数的prototype属性。

因此,使用ES5语法,初始派生类声明大致相当于以下内容(现在省略constructor()主体和walk()方法):

function Bird() {}

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

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

console.log( Object.getPrototypeOf( Bird.prototype ) )      // Vertebrate {}
console.log( Bird.protoype.constructor === Bird )           // true

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

ES6类进一步改进了已经在ES5中实现的伪经典继承模式,但是经常被忽略,因为它们设置起来有点不切实际。

Class Declarations / Expressions

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

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

因此,使用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', {  // 2. Methods are defined to be non-enumerable
        value: function walk() {
            this.isWalking = true;
            return this;
        },
        writable: true,
        configurable: true
    } );

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

    return Vertebrate
})();
  • 注意1:如果周围的代码已经在严格模式下运行,那么当然不需要将所有内容都包装在IIFE中。
  • 注2:虽然可以在ES5中定义静态属性而没有问题,但这并不常见。这样做的原因可能是,如果不使用当时的非标准__proto__属性,则无法建立静态属性的继承。

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

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

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

Derived Class Declarations / Expressions

在此部分下无需添加任何内容。


3. Syntactic sugar for improvements to the pseudoclassical inheritance pattern not available in ES5

ES6类进一步改进了ES5中不可用的伪经典继承模式,但可以在ES6中实现,而无需使用类语法。

Class Declarations / Expressions

在其他地方发现的ES6特征也使其成为类,特别是:

  1. 类声明的行为类似于let声明 - 它们在提升时不会初始化,并在声明之前在Temporal Dead Zone中结束。 (相关question
  2. 类名称的行为类似于类声明中的const绑定 - 它不能在类方法中覆盖,尝试这样做会导致TypeError
  3. 必须使用内部[[Construct]]方法调用类构造函数,如果使用内部TypeError方法将[[Call]]称为普通函数,则抛出constructor()
  4. 类方法(super方法除外),静态与否,行为类似于通过简洁方法语法定义的方法,也就是说: 他们可以通过super.propsuper[expr]使用[[HomeObject]]关键字(这是因为他们被赋予了内部prototype属性)。 它们不能用作构造函数 - 它们缺乏[[Construct]]属性和内部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; })(); 属性。

使用ES6语法,初始类声明因此更精确(但仍然只是部分)等效于以下内容:

super
  • 注1:虽然实例和静态方法都是用简洁的方法语法定义的,但[[HomeObject]]引用在静态方法中的行为不会如预期的那样。事实上,内部Object.assign()财产不会被[[HomeObject]]复制。在静态方法上正确设置new属性需要我们使用对象文字来定义函数构造函数,这是不可能的。
  • 注意2:为了防止在没有instanceof关键字的情况下调用构造函数,可以通过使用answer运算符在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 )。

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

super()

Derived Class Declarations / Expressions

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

  • 子构造函数继承自父构造函数(即派生类继承静态成员)。
  • 在派生类构造函数中调用new.target相当于使用当前this值调用父构造函数并将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 with the current new.target value and binding the 'this' context to the returned value that.hasWings = true; return that; } Bird.prototype = { constructor: Bird, walk() { // Methods are defined using the concise method syntax return super.walk(); }, }; Object.defineProperty( Vertebrate.prototype, 'constructor', {enumerable: false} ); Object.defineProperty( Vertebrate.prototype, 'walk', {enumerable: false} ); Object.setPrototypeOf( Bird, Vertebrate ); // 1. Inheritance is established between the constructors directly Object.setPrototypeOf( Bird.prototype, Vertebrate.prototype ); return Bird; })(); 上下文绑定到返回的对象。

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

Object.create()
  • 注意1:由于__proto__只能用于设置新的非函数对象的原型,因此只能在ES5中通过操作当时的非标准super()属性来实现构造函数之间的继承。
  • 注2:使用this上下文无法模仿console.log( Object.getPrototypeOf( Bird ) ) // [Function: Vertebrate] console.log( Bird.isVertebrate ) // [Function: isVertebrate] console.log( new Bird("Titi")) // Bird { name: 'Titi', hasVertebrae: true, isWalking: false, hasWings: true } console.log( new Bird( "Titi" ).walk().isWalking ) // true 的效果,因此我们必须从构造函数中显式返回一个对象。

现在,最初的派生类声明和上面的代码片段也将产生以下结果:

prototype

4. Features impossible to implement without the class syntax

ES6类进一步提供了以下在没有实际使用类语法的情况下根本无法实现的功能:

  1. 由类声明或类表达式产生的构造函数的prototype属性是不可写的。 没有办法为普通的构造函数实现这一点,因为普通函数的[[HomeObject]]属性是不可配置和可写的。
  2. 静态类方法的内部this question属性指向类构造函数。 没有办法为普通的构造函数实现它,因为它需要通过对象文字定义一个函数(参见上面的第3节)。

Conclusion

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

综上所述,我认为可以说ES6类比ES5伪经典继承模式更简洁,更方便,更安全。结果也不那么灵活(例如参见super())。


Side Notes

值得指出的是,在上述分类中没有找到位置的类的一些特殊性:

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

Further Resources

  • Nicholas Zakas在理解ES6中的章节post是我遇到过的最好的ES6课程。
  • Axel Rauschmayer的2ality博客在ES6课程上有一个非常彻底的Object Playground
  • Babel有一个很棒的视频解释伪古典继承模式(并将其与类语法进行比较)。
  • __proto__转换器是您自己探索事物的好地方。

1
投票

它们完全是语法糖。 ES6中关于原型继承的新内容是重新定义对象的__proto__属性。 Are the es6 classes really semantic sugar?现在是合法的,这就是JS如何使用数组子类化。


0
投票

是的,差不多。

使用es6可以扩展Function类和Array类,在es5中你不能有相同的行为:extends Function不能生成可调用对象,而扩展Array不会继承es5中的.length auto属性

对于其余的原型逻辑和类在JavaScript中是相同的

qazxswpoi

© www.soinside.com 2019 - 2024. All rights reserved.