不,ES6 类不仅仅是原型模式的语法糖。
虽然在很多地方都可以看到相反的情况,而且表面上似乎是正确的,但当您开始深入研究细节时,事情就会变得更加复杂。
我对现有的答案不太满意。在做了一些研究之后,这是我在脑海中对 ES6 类的特性进行分类的方式:
- 标准 ES5 伪经典继承模式的语法糖。
- 用于改进伪经典继承模式的语法糖在 ES5 中可用但不切实际或不常见。
- 用于改进伪经典继承模式的语法糖在 ES5 中不可用,但可以在没有类语法的情况下在 ES6 中实现。
- 没有
class 语法就无法实现的功能,即使在 ES6 中也是如此。
(我试图使这个答案尽可能完整,结果它变得相当长。那些对良好概述更感兴趣的人应该看看traktor53’s answer。)
所以让我'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;
}
}
1。标准 ES5 伪经典继承模式的语法糖
在其核心,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;
}
} );
初始类声明和上面的代码 sn-p 都会产生以下结果:
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
}
})
初始派生类声明和上面的代码 sn-p 都会产生以下结果:
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 中实现的伪经典继承模式,但由于设置起来可能有点不切实际,因此经常被遗漏。
类声明/表达式
类声明或类表达式将通过以下方式进一步设置:
- 类声明或类表达式中的所有代码都在严格模式下运行。
- 类的静态方法在构造函数本身上定义。
- 所有类方法(静态或非静态)都是不可枚举的。
- 构造函数的原型属性是不可写的。
使用 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
})();
现在初始类声明和上面的代码 sn-p 也将产生以下结果:
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]() 调用的近似值,它不依赖于对父类的直接引用。如果不使用 class 语法,则无法完全模仿静态方法中的 super[method]() 调用,并在第 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
});
现在初始派生类声明和上面的代码 sn-p 也将产生以下结果:
console.log( Bird.isBird( new Bird("Titi") ) ) // true
3。用于改进 ES5 中不可用的伪经典继承模式的语法糖
ES6 类进一步提供了对 ES5 中不可用的伪经典继承模式的改进,但可以在 ES6 中实现而无需使用类语法。
类声明/表达式
在其他地方发现的 ES6 特性也将其纳入类,特别是:
- 类声明的行为类似于
let 声明 - 它们在提升时未初始化,并在声明之前位于 Temporal Dead Zone。 (相关question)
- 类名的行为类似于类声明中的
const 绑定 - 它不能在类方法中被覆盖,尝试这样做将导致 TypeError。
- 必须使用内部
[[Construct]] 方法调用类构造函数,如果使用内部[[Call]] 方法将它们作为普通函数调用,则会抛出TypeError。
- 类方法(
constructor() 方法除外),无论是否静态,其行为都类似于通过简洁方法语法定义的方法,也就是说:
- 他们可以通过
super.prop 或super[method] 使用super 关键字(这是因为他们被分配了一个内部[[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;
})();
注意 1:尽管实例和静态方法都使用简洁的方法语法定义,但 super 引用在静态方法中的行为不会像预期的那样。实际上,Object.assign() 不会复制内部 [[HomeObject]] 属性。在静态方法上正确设置 [[HomeObject]] 属性需要我们使用对象字面量定义函数构造函数,这是不可能的。
NB 2:为了防止在没有 new 关键字的情况下调用构造函数,可以通过使用 instanceof 运算符在 ES5 中实现类似的保护措施。但这些并未涵盖所有情况(请参阅answer)。
现在初始类声明和上面的代码 sn-p 也将产生以下结果:
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()相当于使用当前new.target值调用父构造函数的内部[[Construct]]方法并将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;
})();
现在初始派生类声明和上面的代码 sn-p 也将产生以下结果:
console.log( Object.getPrototypeOf( Bird ) ) // [Function: Vertebrate]
console.log( Bird.isVertebrate ) // [Function: isVertebrate]
4。没有class 语法就无法实现的功能
ES6 类进一步提供了以下如果不实际使用class 语法就无法实现的功能:
- 静态类方法的内部
[[HomeObject]] 属性指向类构造函数。
- 对于普通的构造函数没有办法实现这一点,因为它需要通过对象字面量定义一个函数(另请参见上面的第 3 节)。这对于使用
super 关键字的派生类的静态方法(如我们的Bird.isBird() 方法)尤其成问题。
如果事先知道父类,可以部分work around这个问题。
结论
ES6 类的一些特性只是标准 ES5 伪经典继承模式的语法糖。然而,ES6 类也具有只能在 ES6 中实现的特性以及一些在 ES6 中甚至无法模仿的特性(即不使用类语法)。
看了以上,我觉得可以说 ES6 类比 ES5 伪经典继承模式更简洁、更方便、更安全。结果,它们也不太灵活(例如,请参阅this question)。
附注
值得指出一些在上述分类中没有找到位置的类的更多特性:
-
super() 仅在派生类构造函数中是有效的语法,并且只能被调用一次。
- 在调用
super() 之前尝试在派生类构造函数中访问this 会导致ReferenceError。
-
如果没有显式返回任何对象,则必须在派生类构造函数中调用
super()。
-
eval 和 arguments 不是有效的类标识符(在非严格模式下它们是有效的函数标识符)。
- 如果没有提供,派生类会设置默认的
constructor() 方法(对应于constructor( ...args ) { super( ...args ); })。
- 不能使用类声明或类表达式在类上定义数据属性(尽管您可以在类声明后手动添加数据属性)。
更多资源