在玩ES6之后,我真的开始喜欢新的语法和功能了,但我确实对类有疑问。
新的ES6类是旧原型模式的语法糖吗?或者幕后还有更多的事情发生在这里?即:
class Thing {
//... classy stuff
doStuff(){}
}
vs:
var Thing = function() {
// ... setup stuff
};
Thing.prototype.doStuff = function() {}; // etc
是的,或许,但是一些语法糖有牙齿。
声明一个类创建一个函数对象,它是类的构造函数,使用在类体中为constructor
提供的代码,以及与类同名的命名类。
类构造函数有一个普通的原型对象,类实例以正常的JavaScript方式从该对象继承属性。在类主体中定义的实例方法被添加到此原型中。
ES6没有提供一种方法来声明类主体中要存储在原型上并继承的类实例默认属性值(即非方法的值)。要初始化实例值,您可以将它们设置为构造函数中的本地,非继承属性,或者以与普通构造函数相同的方式手动将它们添加到类定义之外的类构造函数的prototype
对象中。 (我不是在争论为JavaScript类设置继承属性的优点或其他方面)。
在类主体中声明的静态方法将添加为类构造函数的属性。避免使用与Function.prototype
继承的标准函数属性和方法竞争的静态类方法名称,如call
,apply
或length
。
较少含糖的是类声明和方法总是以严格模式执行,并且一个很少引起注意的特性:类构造函数的.prototype
属性是只读的:你不能将它设置为你为某些创建的其他对象特殊目的。
扩展一个类时会发生一些有趣的事情:
prototype
对象属性自动在要扩展的类的prototype
对象上进行原型化。这不是特别新的,可以使用Object.create
复制效果。Function
。虽然可以使用Object.setPrototypeOf
甚至childClass.__proto__ = parentClass
复制对普通构造函数的影响,但这将是一种非常不寻常的编码实践,并且通常在JavaScript文档中被建议不要使用。还有其他差异,例如类对象没有以使用function
关键字声明的命名函数的方式提升。
我相信认为类声明和表达式在ECMA Script的所有未来版本中都将保持不变是一种天真,并且看看是否以及何时发生这将是有趣的。可以说,将“语法糖”与ES6中引入的类(ECMA-262标准版本6)联系起来已经成为一种时尚,但我个人试图避免重复它。
新的ES6类是旧的原型模式的语法糖吗?
是的,它们(几乎完全)是一种方便的语法,语义几乎完全相同。 Traktor53's answer进入差异。
以下短代码示例显示了如何在class
对象上设置prototype
中的函数。
class Thing {
someFunc() {}
}
console.log("someFunc" in Thing.prototype); // true
是。但他们更严格。
您的示例有两个主要差异。
首先,使用类语法,如果没有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课程不仅仅是原型模式的语法糖。
虽然相反的情况可以在许多地方阅读,虽然从表面上看似乎是正确的,但当你开始深入挖掘细节时,事情变得更加复杂。
我对现有的答案不太满意。在做了一些更多的研究之后,我就在脑海中对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();
}
}
从本质上讲,ES6课程确实为标准的ES5伪经典继承模式提供了语法糖。
在后台,类声明或类表达式将创建一个与该类同名的构造函数,以便:
[[Construct]]
属性是指附加到classe的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
属性之间建立继承,以便:
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
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', { // 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
})();
__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 }
在此部分下无需添加任何内容。
ES6类进一步改进了ES5中不可用的伪经典继承模式,但可以在ES6中实现,而无需使用类语法。
在其他地方发现的ES6特征也使其成为类,特别是:
let
声明 - 它们在提升时不会初始化,并在声明之前在Temporal Dead Zone中结束。 (相关question)const
绑定 - 它不能在类方法中覆盖,尝试这样做会导致TypeError
。[[Construct]]
方法调用类构造函数,如果使用内部TypeError
方法将[[Call]]
称为普通函数,则抛出constructor()
。super
方法除外),静态与否,行为类似于通过简洁方法语法定义的方法,也就是说:
他们可以通过super.prop
或super[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
[[HomeObject]]
引用在静态方法中的行为不会如预期的那样。事实上,内部Object.assign()
财产不会被[[HomeObject]]
复制。在静态方法上正确设置new
属性需要我们使用对象文字来定义函数构造函数,这是不可能的。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()
除上述内容外,以下内容还适用于派生类声明或派生类表达式:
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()
__proto__
只能用于设置新的非函数对象的原型,因此只能在ES5中通过操作当时的非标准super()
属性来实现构造函数之间的继承。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
ES6类进一步提供了以下在没有实际使用类语法的情况下根本无法实现的功能:
prototype
属性是不可写的。
没有办法为普通的构造函数实现这一点,因为普通函数的[[HomeObject]]
属性是不可配置和可写的。ES6类的一些特性只是标准ES5伪经典继承模式的语法糖。然而,ES6类还具有只能在ES6中实现的功能以及甚至在ES6中甚至无法模仿的一些其他功能(即,不使用类语法)。
综上所述,我认为可以说ES6类比ES5伪经典继承模式更简洁,更方便,更安全。结果也不那么灵活(例如参见super()
)。
值得指出的是,在上述分类中没有找到位置的类的一些特殊性:
this
只是派生类构造函数中的有效语法。super()
之前尝试在派生类构造函数中访问ReferenceError
会导致super()
。eval
。arguments
和constructor()
不是有效的类标识符(虽然它们是非严格模式下的有效函数标识符)。constructor( ...args ) { super( ...args ); }
方法(如果没有提供)(对应于Understanding ES6 Classes)。__proto__
转换器是您自己探索事物的好地方。它们完全是语法糖。 ES6中关于原型继承的新内容是重新定义对象的__proto__
属性。 Are the es6 classes really semantic sugar?现在是合法的,这就是JS如何使用数组子类化。
是的,差不多。
使用es6可以扩展Function类和Array类,在es5中你不能有相同的行为:extends Function不能生成可调用对象,而扩展Array不会继承es5中的.length auto属性
对于其余的原型逻辑和类在JavaScript中是相同的
qazxswpoi