在玩过 ES6 之后,我真的开始喜欢新的语法和可用的特性,但我确实有一个关于类的问题。
新的 ES6 类只是旧原型模式的语法糖吗?还是幕后有更多事情发生?例如:
class Thing {
//... classy stuff
doStuff(){}
}
对比:
var Thing = function() {
// ... setup stuff
};
Thing.prototype.doStuff = function() {}; // etc
在玩过 ES6 之后,我真的开始喜欢新的语法和可用的特性,但我确实有一个关于类的问题。
新的 ES6 类只是旧原型模式的语法糖吗?还是幕后有更多事情发生?例如:
class Thing {
//... classy stuff
doStuff(){}
}
对比:
var Thing = function() {
// ... setup stuff
};
Thing.prototype.doStuff = function() {}; // etc
不,ES6 类不仅仅是原型模式的语法糖。
虽然在很多地方都可以看到相反的情况,虽然表面上似乎是正确的,但当您开始深入研究细节时,事情会变得更加复杂。
我对现有的答案不太满意。在做了更多的研究之后,我是这样对 ES6 类的特性进行分类的:
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;
}
}
在其核心,ES6 类确实为标准 ES5 伪经典继承模式提供了语法糖。
在后台,类声明或类表达式将创建一个与类同名的构造函数,使得:
[[Construct]]
构造函数的内部属性指的是附加到类constructor()
方法的代码块。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
语法:
prototype
孩子的构造函数的性质从继承prototype
父构造函数的性质。super()
调用相当于调用this
绑定到当前上下文的父构造函数。
super()
,它还将设置隐式new.target
参数并触发内部[[Construct]]
方法(而不是[[Call]]
方法)。该super()
电话将在第 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
ES6 类进一步提供了对伪经典继承模式的改进,该模式本可以在 ES5 中实现,但经常被遗漏,因为它们可能有点不切实际。
类声明或类表达式将通过以下方式进一步设置:
使用 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
语法:
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
ES6 类进一步对 ES5 中没有的伪经典继承模式进行了改进,但可以在 ES6 中实现,而无需使用类语法。
在别处发现的 ES6 特性也使其成为类,特别是:
let
声明——它们在提升时不会被初始化,并在声明之前最终进入临时死区。(相关问题)const
类声明中的绑定——它不能在类方法中被覆盖,尝试这样做会导致TypeError
.[[Construct]]
方法调用,TypeError
如果使用内部[[Call]]
方法作为普通函数调用它们,则会抛出a 。constructor()
),无论是否静态,其行为都类似于通过简洁方法语法定义的方法,也就是说:
super
关键字 through super.prop
or 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
除上述内容外,以下内容也适用于派生类声明或派生类表达式:
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]
class
语法就无法实现的功能ES6 类进一步提供了以下在不实际使用class
语法的情况下根本无法实现的功能:
[[HomeObject]]
静态类方法的内部属性指向类构造函数。
super
像我们的Bird.isBird()
方法这样的关键字的派生类的静态方法,这尤其成问题。如果事先知道父类,则可以部分解决此问题。
ES6 类的一些特性只是标准 ES5 伪经典继承模式的语法糖。然而,ES6 类也带有只能在 ES6 中实现的特性和一些甚至不能在 ES6 中模仿的特性(即不使用类语法)。
综上所述,我认为可以说 ES6 的类比 ES5 的伪经典继承模式更简洁、更方便、使用更安全。因此,它们的灵活性也较差(例如,请参阅此问题)。
值得指出的是,在上述分类中没有找到位置的类的一些特殊性:
super()
只是派生类构造函数中的有效语法,并且只能调用一次。this
在super()
调用之前尝试在派生类构造函数中访问会导致ReferenceError
.super()
如果没有显式返回对象,则必须在派生类构造函数中调用它。eval
并且arguments
不是有效的类标识符(虽然它们在非严格模式下是有效的函数标识符)。constructor()
如果没有提供(对应于constructor( ...args ) { super( ...args ); }
),派生类将设置默认方法。是的,也许,但一些语法糖有齿。
声明一个类创建一个函数对象,它是类的构造函数,使用constructor
类体内提供的代码,并为命名类创建一个与类相同的名称。
类构造函数有一个普通的原型对象,类实例以普通的 JavaScript 方式从中继承属性。在类体中定义的实例方法被添加到这个原型中。
ES6 没有提供在类体内声明类实例默认属性值(即不是方法的值)的方法,这些值存储在原型上并被继承。要初始化实例值,您可以将它们设置为构造函数中的本地、非继承属性,或者以与prototype
普通构造函数相同的方式将它们手动添加到类定义之外的类构造函数的对象中。(我不是在争论为 JavaScript 类设置继承属性的优点或其他方面)。
在类体中声明的静态方法被添加为类构造函数的属性。避免使用与标准函数属性和从、或Function.prototype
等继承的方法竞争的静态类方法名称。call
apply
length
不那么甜的是类声明和方法总是在严格模式下执行,还有一个很少受到关注的.prototype
特性:类构造函数的属性是只读的:你不能将它设置为你为某些特殊创建的其他对象目的。
当你扩展一个类时会发生一些有趣的事情:
prototype
扩展类构造函数的对象属性自动在prototype
被扩展类的对象上建立原型。这并不是特别新,可以使用 复制效果Object.create
。
扩展类构造函数(对象)在被扩展类的构造函数上自动原型化,而不是Function
. 虽然可以使用Object.setPrototypeOf
或 even来复制普通构造函数的效果childClass.__proto__ = parentClass
,但这将是一种非常不寻常的编码实践,并且在 JavaScript 文档中通常不建议这样做。
还有其他区别,例如类对象没有以使用function
关键字声明的命名函数的方式提升。
我相信认为类声明和表达式将在 ECMA 脚本的所有未来版本中保持不变的想法可能是幼稚的,看看是否以及何时发生发展将会很有趣。可以说,将“语法糖”与 ES6(ECMA-262 标准版本 6)中引入的类联系起来已经成为一种时尚,但我个人尽量避免重复它。
是的。但他们更严格。
您的示例有两个主要区别。
首先,使用 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{}
新的 ES6 类只是旧原型模式的语法糖吗?
是的,它们(几乎完全)是一种方便的语法,语义几乎相同。Traktor53 的回答涉及差异。
下面的简短代码示例显示了如何class
在prototype
对象上设置a中的函数。
class Thing {
someFunc() {}
}
console.log("someFunc" in Thing.prototype); // true
ES6 类只是 Javascript 中原型模式的语法糖吗?
当被问到这个问题时,答案是几乎(但是,你知道,是一种很好的糖)。
现在,答案是:不。
有些事情你可以class
在 ES2015 中做,而你在 ES5 和更早的版本中做不到,但它们都是你可以在 ES2015 中以一种或另一种方式做的事情,即使没有class
,因为class
ES2015 中添加了新的非特性,比如Reflect.construct
和new.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"
但是虽然这有效,但它没有利用对象的新能力来包含实际的私有字段。