ES6 类只是 Javascript 中原型模式的语法糖吗?

IT技术 javascript ecmascript-6
2021-01-22 15:03:46

在玩过 ES6 之后,我真的开始喜欢新的语法和可用的特性,但我确实有一个关于类的问题。

新的 ES6 类只是旧原型模式的语法糖吗?还是幕后有更多事情发生?例如:

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

对比:

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

Thing.prototype.doStuff = function() {}; // etc
6个回答

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

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

我对现有的答案不太满意。在做了更多的研究之后,我是这样对 ES6 类的特性进行分类的:

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

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


因此,让我一步一步地(并尽可能地)对下面的类声明进行“脱糖”,以说明我们继续进行的事情:

// 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. 标准 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;
    }
} );

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

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 }

派生类声明/表达式

除了上述之外,派生类声明或派生类表达式还将在构造函数的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
    }
})

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

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. 语法糖,用于改进 ES5 中可用但不切实际或不常见的伪经典继承模式

ES6 类进一步提供了对伪经典继承模式的改进,该模式本可以在 ES5 中实现,但经常被遗漏,因为它们可能有点不切实际。

类声明/表达式

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

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

使用 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
})();
  • NB 1:如果周围的代码已经在严格模式下运行,当然没有必要将所有内容都包装在 IIFE 中。

  • NB 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 }

派生类声明/表达式

除了上述之外,派生类声明或派生类表达式也将使用以下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 
});

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

console.log( Bird.isBird( new Bird("Titi") ) )  // true

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

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

类声明/表达式

在别处发现的 ES6 特性也使其成为类,特别是:

  1. 类声明的行为类似于let声明——它们在提升时不会被初始化,并在声明之前最终进入临时死区(相关问题
  2. 类名的行为就像const类声明中绑定——它不能在类方法中被覆盖,尝试这样做会导致TypeError.
  3. 类构造函数必须使用内部[[Construct]]方法调用,TypeError如果使用内部[[Call]]方法作为普通函数调用它们则会抛出a
  4. 类方法(方法除外constructor()),无论是否静态,其行为都类似于通过简洁方法语法定义的方法,也就是说:
    • 他们可以使用super关键字 through super.propor 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;
})();
  • NB 1:虽然实例方法和静态方法都是用简洁的方法语法定义的,但super引用在静态方法中的行为不会像预期的那样。事实上,内部[[HomeObject]]属性没有被 复制过来Object.assign()[[HomeObject]]在静态方法上正确设置属性需要我们使用对象字面量定义函数构造函数,这是不可能的。

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

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

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

派生类声明/表达式

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

  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;
})();   
  • NB 1 : 由于Object.create()只能用于设置新的非函数对象的原型,因此在 ES5 中只能通过操作当时的非标准__proto__属性来设置构造函数之间的继承

  • NB 2:无法模拟super()使用this上下文的效果,因此我们必须that从构造函数中显式返回不同的对象。

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

console.log( Object.getPrototypeOf( Bird ) )        // [Function: Vertebrate]
console.log( Bird.isVertebrate )                    // [Function: isVertebrate]

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

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

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

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


结论

ES6 类的一些特性只是标准 ES5 伪经典继承模式的语法糖。然而,ES6 类也带有只能在 ES6 中实现的特性和一些甚至不能在 ES6 中模仿的特性(即不使用类语法)。

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


旁注

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

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

更多资源

  • 本章了解ES6类了解ES6由尼古拉斯Zakas是ES6类最佳写了,我所遇到的。
  • Axel Rauschmayer 的 2ality 博客有一篇关于 ES6 类的非常详尽的文章
  • Object Playground有一个很棒的视频,解释了伪经典继承模式(并将其与类语法进行了比较)。
  • 巴贝尔transpiler是探索你自己的东西的好地方。
+1 对此:-“所有类方法(静态或非静态)都是不可枚举的。” 我试图弄清楚为什么 console.log 不会为类的原型返回相同的结果。
2021-03-18 15:03:46
嗯,但除此之外,[[HomeObject]] 因为他们使用原型来实现,所以其余部分不只是语法糖吗?
2021-03-18 15:03:46
@BoLi - 是的,我想你在这个意义上是对的。然而,根据寻找这个问题的人的来源,他们可能对语法糖有不同的含义因此,上述区别...
2021-03-25 15:03:46
@jw013 感谢您指出这一点,这非常重要。我会添加一个注释。这实际上属于第 3 节,不需要任何修改,因为可以通过Reflect.construct(). 例如:function MyArray() {return Reflect.construct(Array, [], new.target)}; Object.setPrototypeOf(MyArray.prototype, Array.prototype); Object.setPrototypeOf(MyArray, Array); const colors = new MyArray(); colors[0] = "red"; console.log(colors.length) // 1; console.log(colors instanceof MyArray) // true; console.log(colors instanceof Array) // true;
2021-03-28 15:03:46
我正在阅读链接的“理解 ES6”一章,发现了一些我在您的答案中没有看到的内容,我认为在第 4 类中值得一提,即class语法允许您从内置对象(例如Array和其.length属性的特殊行为),这不能仅用原型来完成。
2021-04-05 15:03:46

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

声明一个类创建一个函数对象,它是类的构造函数,使用constructor类体内提供的代码,并为命名类创建一个与类相同的名称。

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

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

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

不那么甜的是类声明和方法总是在严格模式下执行,还有一个很少受到关注的.prototype特性:类构造函数属性是只读的:你不能将它设置为你为某些特殊创建的其他对象目的。

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

  • prototype扩展类构造函数对象属性自动在prototype被扩展类对象上建立原型这并不是特别新,可以使用 复制效果Object.create

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

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

我相信认为类声明和表达式将在 ECMA 脚本的所有未来版本中保持不变的想法可能是幼稚的,看看是否以及何时发生发展将会很有趣。可以说,将“语法糖”与 ES6(ECMA-262 标准版本 6)中引入的类联系起来已经成为一种时尚,但我个人尽量避免重复它。

这是一个迟到的答案,在此重复问题关闭后发布
2021-03-15 15:03:46

是的。但他们更严格。

您的示例有两个主要区别。

首先,使用 class 语法,您不能在没有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

编辑

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

console.log(Thing) //Uncaught ReferenceError: Thing is not defined
class Thing{}
类声明也不会被提升。
2021-03-21 15:03:46
另请参阅“使用原型和静态方法进行装箱”:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...
2021-04-05 15:03:46

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

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

来源

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

class Thing {
   someFunc() {}
}

console.log("someFunc" in Thing.prototype); // true
您的消息来源还提到了与经典方法的细微差别。可惜你没有。
2021-03-12 15:03:46
这个someFunc()方法在prototype类中也是如此,因为 OP 正在尝试使用 es5 方式。
2021-03-14 15:03:46
@Jai 没有愚蠢的问题!
2021-03-18 15:03:46
啊!我问的这么愚蠢的问题。
2021-03-19 15:03:46
@Jai 是的,运行答案中的代码示例,您可以看到它相同。
2021-04-03 15:03:46

ES6 类只是 Javascript 中原型模式的语法糖吗?

当被问到这个问题时,答案是几乎(但是,你知道,是一种很好的糖)。

现在,答案是:

有些事情你可以class在 ES2015 中做,而你在 ES5 和更早的版本中做不到,但它们都是你可以在 ES2015 中以一种或另一种方式做的事情,即使没有class,因为classES2015 中添加了新的非特性,比如Reflect.constructnew.target有些事情真的很尴尬,但还是有可能的。可以说,除了语法糖之外,唯一可以考虑的是[[HomeObject]]函数上的新插槽。您可以通过其他方式获得相同的效果,但您实际上无法使用该新插槽。

但是,随着类字段私有方法和访问器以及静态私有方法即将到来(我在 2020 年 12 月下旬写这篇文章的第三阶段,可能会在新的几个月内完成),有些事情几乎肯定会发生超出了语法糖的水平。

例如,私有字段是对在class语法之外不可用的对象的基本补充私有字段存储在无法以任何其他方式访问的新内部对象槽中(如[[HomeObject]]is)。

这并不意味着您不能以不同的方式做类似的事情,只是您实际上不能使用新的私有插槽。例如,考虑使用私有字段的此类:

class Person {
    #name;
    constructor(name) {
        this.#name = name;
    }
    getName() {
        return this.#name;
    }
}
const person = new Person("Joe");
console.log(person.name); // undefined
// console.log(person.#name); // Would be a SyntaxError
console.log(person.getName()); // "Joe"

可以通过几种不同的方式在没有私有字段的情况下编写,例如通过getName在构造函数中定义(因此它关闭name参数)而不是继承它,或使用WeakMap

const Person = (() => {
    const names = new WeakMap();
    return class Person {
        constructor(name) {
            names.set(this, name);
        }
        getName() {
            return names.get(this);
        }
    };
})();
const person = new Person("Joe");
console.log(person.name); // undefined
console.log(person.getName()); // "Joe"

但是虽然这有效,但它没有利用对象的新能力来包含实际的私有字段。