PersonType是一个构造器函数,sayName()方法被指派到原型上,因此PersonType的所有实例上都共享的此方法。 使用 new 运算符创建了 PersonType 的一个新实例 person ,此对象会被认为是一个通过原型继承了 PersonType 与 Object 的实例。
类声明以class关键字开始,后面是类的名称。 在类内部的方法声明有点类似对象字面量中的方法简写,并且在方法之间并不需要使用逗号。
class PersonClass{ constructor(name){ this.name = name; } sayName(){ console.log(this.name); } } let person = new PersonClass("nic"); person.sayName(); //"nic" console.log(person instanceof PersonClass); // true console.log(person instanceof Object); // true console.log(typeof PersonClass); // "function" console.log(typeof PersonClass.prototype.sayName); // "function" 类声明允许你在其中使用特殊的constructor方法直接定义一个构造器 在构造器内部出现的属性都是实例属性,这种实例属性只能在类的构造器内部进行创建。sayName()则是成为的原型上的方法。其实相对于ES5中的自定义类型声明方式来说,类声明仅仅是一个语法糖。PersonClass声明实际上创建了一个拥有constructor方法及一些行为的函数,因此typeof PersonClass会得到function。此例中的 sayName() 方法最终也成为 PersonClass.prototype 上的一个方法。
类声明与之前的自定义类型之间仍有一些重要的区别:
类声明不会被提升。类声明的行为与let类似,因此在程序的执行到达声明处之前,类会存在于暂时性死区内。类声明中的所有代码会自动运行在严格模式下,并且也无法退出严格模式。类的所有方法都是不可枚举的类的所有方法内部都没有[[Construct]] ,因此使用 new 来调用它们会抛出错误。调用类构造器时不使用new,会抛出错误试图在类的方法内部重写类名,会抛出错误类与函数有相似之处,即它们都有两种形式,声明与表达式。 类表达式被设计用于变量声明,或可作为参数传递给函数。
类表达式不需要在class关键字后使用标识符,除了语法差异,类表达式的功能等价于类声明。
使用类声明还是类表达式,主要是代码风格问题。相对于函数声明与函数表达式之间的区别,类声明与类表达式都不会被提升,因此对代码运行时的行为影响甚微。
上一节是声明了一个匿名的类表达式,但是你也可以为类表达式命名:
let PersonClass = class PersonClass2{ constructor(name) { this.name = name; } sayName() { console.log(this.name); } } console.log(typeof PersonClass); // "function" console.log(typeof PersonClass2); // "undefined"此处的类表达式被命名为PersonClass2,这个标识符只在类定义内部存在,因此只能用在类方法内部。在类的外部,typeof PersonClass2 的结果为 “undefined”,这是因为外部不存在PersonClass2绑定。
能被当做值来使用的就是一级公民。这意味着:
能作为参数传给函数能作为函数返回值能给变量赋值js中的函数就是一级公民,而ES6中的类同样也是一级公民。 可以作为参数传入函数:
function createObject(classDef) { return new classDef(); } let obj = createObject(class { sayHi() { console.log("Hi!"); } }) obj.sayHi();此处createObject()被调用时接受了一个匿名类表达式作为参数,使用new创建了该类的一个实例并将其返回。
类表达式的另一个用途是立即调用类构造器,以创建单例。 为此,必须要用new来配合类表达式,并在表达式后面添加括号:
let person = new class { constructor(name) { this.name = name; } sayName() { console.log(this.name); } }("nic"); person.sayName();此处创建了一个匿名类表达式,并立即执行了它。类表达式后面的圆括号表示要调用前面的函数,并且还允许传入参数。
此模式允许你使用类语法来创建单例。
类还允许你在原型上定义访问器属性。 要创建getter,则要使用get关键字,并要与后方标识符之间留出空格,创建setter同理:
class Element{ constructor(element){ this.element = element; } get html(){ return this.element.innerHTML; } set html(value){ this.element.innerHTML = value; } } var descriptor = Object.getOwnPropertyDescriptor(Element.prototype,"html"); console.log("get" in descriptor); // true console.log("set" in descriptor); // true console.log(descriptor.enumerable); // false此访问器属性被创建在Element.prototype上,并且像其他类属性一样被创建为不可枚举的属性。
与对象字面量类似,类方法与类访问器属性也都能使用需计算的名称,使用方括号包裹:
let methodName = "sayName"; class PersonType = { constructor(name) { this.name = name; } [methodName](){ console.log(this.name); } } let me = new PersonClass("Nicholas"); me.sayName(); // "Nicholas"这里的PersonClass使用了一个变量来命名类定义内的方法。
访问器属性也可以以相同的方式使用需计算名称:
let propertyName = "html"; class CustomHTMLElement { constructor(element) { this.element = element; } get [propertyName]() { return this.element.innerHTML; } set [propertyName](value) { this.element.innerHTML = value; } }类内部允许定义生成器方法:
class MyClass { *createIterator(){ yield 1; yield 2; yield 3; } } let instance = new MyClass(); let iterator = instance.createIterator();此代码创建了一个拥有createIterator()生成器的MyClass类,该方法返回了一个迭代器,它的值在生成器内部用硬编码提供。
可以使用Symbol.iterator来定义类的默认迭代器:
class Collection { constructor(){ this.items = []; } *[Symbol.iterator](){ yield *this.item.values(); } } var collection = new Collection(); collection.items.push(1); collection.items.push(2); collection.items.push(3); for (let x of collection) { console.log(x); } // 输出: // 1 // 2 // 3此处的生成器方法委托了数组的values()迭代器。现在Collection的任何实例都可以在for-of循环内部被直接使用。
以上定义的访问器属性、生成器方法等,都是在原型上的,所有的实例都能访问。 若你想让方法与访问器属性只存在于类自身,那么就要使用静态成员。
在ES5中,一般通过直接在构造函数中添加额外方法来模拟静态成员:
function PersonType(name) { this.name = name; } //静态方法 PersonType.create = function(name){ return new PersonType(name); } // 实例方法 PersonType.prototype.sayName = function() { console.log(this.name); }; var person = PersonType.create("Nicholas");此处PersonType.create() 是一个静态方法,它不依赖于任何实例。
ES6简化了静态成员的创建,只要在方法和访问器属性的名称前添加static标注。
class PersonClass { constructor(name) { this.name = name; } sayName() { console.log(this.name); } //静态成员 static create(name){ return new PersonClass(name); } } let person = PersonClass.create("Nicholas");静态方法的定义只多了一个static关键字。 你可以在类中的任何方法与访问器属性上使用static关键字,唯一限制是不能用于constructor方法的定义。
静态成员不能用实例来访问,始终需要直接用类自身来访问它们。
ES6之前,需要这样实现继承:
function Rectangle(length,width){ this.length = length; this.width = width; } Rectangle.prototype.getArea = function() { return this.length * this.width; }; //子类 function Square(length){ Rectangle.call(this,length,length); } Square.prototype = Object.create(Rectangle.prototype); var square = new Square(3); console.log(square.getArea()); // 9 console.log(square instanceof Square); // true console.log(square instanceof Rectangle); // trueSquare 继承了 Rectangle ,为此它必须使用 Rectangle.prototype 所创建的一个新对象来重写 Square.prototype,并且还要在构造函数内部调用 Rectangle.call() 方法。
ES6类的出现让继承变得更容易:
使用extends关键字来指定当前类所需要继承的类。生成的类的原型会被自动调整可以使用super()方法来访问基类的构造器 class Rectangle { constructor(length, width) { this.length = length; this.width = width; } getArea() { return this.length * this.width; } } class Square extends Rectangle{ constructor(length){ super(length,length); } } var square = new Square(3); console.log(square.getArea()); // 9 console.log(square instanceof Square); // true console.log(square instanceof Rectangle); // true此次 Square 类使用了 extends 关键字继承了 Rectangle. Square 构造器使用了 super() 配合指定参数调用了 Rectangle 的构造器。
继承了其他类的类被称为派生类:
如果派生类指定了构造器,则一定要使用super(),否则会抛出错误。如果没有使用构造器,则super()方法会被自动调用,并会使用创建新实例时提供的所有参数: class Square extends Rectangle { // 没有构造器 } // 等价于: class Square extends Rectangle { constructor(...args) { super(...args); } }使用super()时有几个需要注意的地方:
只能在派生类中使用super()。若尝试在非派生的类(即:没有使用 extends 关键字的类)或函数中使用它,就会抛出错误。在构造器中,必须在访问this之前调用super()。 由于super()负责初始化this,因此试图先访问 this 自然就会造成错误。唯一能避免调用super()的方法就是从构造器中返回一个对象。派生类中的方法总是会屏蔽基类的同名方法。
class Square extends Rectangle{ constructor(length){ super(length, length); } //重写并屏蔽 Rectangle.prototype.getArea() getArea(){ return this.length * this.length; } }此时Square的实例调用getArea()调用的就是Square.prototype上面的方法而不是Rectangle.prototype上面的方法。
当然,你总是可以使用super.getArea()来调用基类中的同名方法:
class Square extends Rectangle { constructor(length) { super(length, length); } // 重写、屏蔽并调用了 Rectangle.prototype.getArea() getArea() { return super.getArea(); } }用这种方式使用 super ,其效果等同于在ES6对象中的 super 引用。this会被自动设置为正确的值。
如果基类包含静态成员,那么这些静态成员在派生类中也是可用的。
class Rectangle { constructor(length, width) { this.length = length; this.width = width; } getArea() { return this.length * this.width; } static create(length, width) { return new Rectangle(length, width); } } class Square extends Rectangle { constructor(length) { super(length, length); } } var rect = Square.create(3,4); console.log(rect instanceof Rectangle); // true console.log(rect.getArea()); // 12 console.log(rect instanceof Square); // false通过继承,派生类也以静态方法的方式调用:Square.create(),并且其行为与Rectangle.create()相同。
你也可以从表达式中派生类,只要这个表达式能够返回一个具有[[Constructor]]属性以及原型的函数,就可以对其使用extends。
function Rectangle(length, width) { this.length = length; this.width = width; } Rectangle.prototype.getArea = function() { return this.length * this.width; }; class Square extends Rectangle { constructor(length){ super(length,length); } } var x = new Square(3); console.log(x.getArea()); // 9 console.log(x instanceof Rectangle); // trueRectangle是ES5风格的构造器,Square则是ES6中的类,但由于Rectangle具有[[Constructor]]以及原型,因此Square可以继承他。
extends后面能接受任意类型的表达式,这带来了巨大可能性,例如动态地决定所要继承的类:
function Rectangle(length, width) { this.length = length; this.width = width; } Rectangle.prototype.getArea = function() { return this.length * this.width; }; function getBase() { return Rectangle; } class Square extends getBase(){ constructor(length) { super(length, length); } } var x = new Square(3); console.log(x.getArea()); // 9 console.log(x instanceof Rectangle); // truegetBase() 函数作为类声明的一部分被直接调用,它返回了 Rectangle.
由于可以动态地决定基类,那也就能创建不同的继承方式:
let SerializableMixin = { serialize() { return JSON.stringify(this); } }; let AreaMixin = { getArea() { return this.length * this.width; } }; function mixin(...mixins) { var base = function() {}; Object.assign(base.prototype, ...mixins); return base; } class Square extends mixin(AreaMixin, SerializableMixin) { constructor(length) { super(); this.length = length; this.width = length; } } var x = new Square(3); console.log(x.getArea()); // 9 console.log(x.serialize()); // "{"length":3,"width":3}"此处使用了mixin而不是传统继承。mixin函数接受代表混入对象的任意数量的参数,他创建一个名为base的函数,并将每个混入对象的属性都赋值到新函数的原型上。此函数随后被返回,于是Square就能够使用extends关键字了。
Square的实例既有来自AreaMixin的的 getArea() 方法,又有来自SerializableMixin 的 serialize() 方法,这是通过原型继承实现的。 mixin() 函数使用了混入对象的所有自有属性,动态地填充了新函数的原型。
任意表达式都能在extends关键字后使用,但并非所有表达式的结果都是一个有效的类。特别地,下列表达式类型会导致错误:
null生成器函数试图使用结果为上述值的表达式来创建一个新的类实例,都会抛出错误,因为不存在 [[Construct]] 可供调用。
在 ES6 中的类,其设计目的之一就是允许从内置对象上进行继承。
在ES5的传统继承中,this的值会先被派生类创建,然后基类构造器(内置类型如Array)才被调用。这意味着一开始this就是派生类的实例,然后才使用了Array进行装饰,因此内置类型的一些内置行为是不能被成功继承到的。
在ES6基于类的继承中,this的值会先被基类创建,然后才被派生类的构造器修改。因此this初始就拥有作为基类的内置对象的所有功能。
class MyArray extends Array{ } var colors = new MyArray(); colors[0] = "red"; console.log(colors.length); // 1 colors.length = 0; console.log(colors[0]); // undefinedMyArray 直接继承了 Array ,因此工作方式与正规数组一致。与数值索引属性的互动更新了 length 属性,而操纵 length 属性也能更新索引属性。
new.target属性允许你检测函数或构造方法是否通过是通过new运算符被调用的,如果是通过new调用的则返回一个指向函数或构造方法的引用,否则返回undefined。
你可以在类构造器中使用new.target,来判断类是如何被调用的。
在简单情况下,new.target就等于本类的构造器函数:
class Rectangle { constructor(length, width) { console.log(new.target === Rectangle); this.length = length; this.width = width; } } // new.target 就是 Rectangle var obj = new Rectangle(3, 4); // 输出 true此代码说明在 new Rectangle(3, 4) 被调用时, new.target 就等于 Rectangle 。
类构造器被调用时不能缺少new,因此new.target属性就始终会在类构造器内部被定义。 不过这个值不总是相同的:
class Rectangle { constructor(length, width) { console.log(new.target === Rectangle); this.length = length; this.width = width; } } class Square extends Rectangle { constructor(length) { super(length, length) } } // new.target 是 Square var obj = new Square(3); // 输出 falseSquare调用了Rectangle的构造器,因此当Rectangle构造器被调用时,new.target等于Square。 这很重要,因为构造器能根据如何被调用而有不同行为,并且这给了更改这种行为的能力。
可以利用new.target来创建一个抽象基类(不能被实例化的类):
class Shape { constructor(){ if(new.target == Shape){ throw new Error("This class cannot be instantiated directly.") } } } class Rectangle extends Shape { constructor(length, width) { super(); this.length = length; this.width = width; } } var x = new Shape(); // 抛出错误 var y = new Rectangle(3, 4); // 没有错误 console.log(y instanceof Shape); // true此例中的 Shape 类构造器会在 new.target 为 Shape 的时候抛出错误,意味着 new Shape() 永远都会抛出错误,即不能被实例化。 然而,你依然可以将 Shape 用作一个基类,正如 Rectangle 所做的那样。 super() 的调用执行了 Shape 构造器,而且 new.target 的值等于 Rectangle ,因此该构造器能够无错误地继续执行。
由于调用类时不能缺少 new ,于是 new.target 属性在类构造器内部就绝不会是 undefined 。