您的位置:首页 > Web前端 > JavaScript

【读书笔记】【深入理解ES6】#9-JavaScript中的类

2017-11-23 16:59 806 查看
大多数面向对象的编程语言都支持类和类继承的特性,而JavaScript却不支持这些特性,只能通过其他方法定义并关联多个相似的对象。这个状态一直从ECMAScript 1持续到ECMAScript 5。

尽管一部分JavaScript开发强烈坚持JavaScript中不需要类,但由于类似的库层出不穷,最终还是在ECMAScript 6中引入了类的特性。

ECMAScript 6中的类与其他语言中的还是不太一样,其语法的设计实际上借鉴了JavaScript的动态性

ECMAScript 5 中的近类结构

首先创建一个构造函数,然后定义另一个方法并赋值给构造函数的原型。

function PersonType() {
this.name = name;
}

PersonType.prototype.sayName = function() {
console.log(this.name);
};

var person = new PersonType("JiaJia");
person.sayName(); // "JiaJia"

console.log(person instanceof PersonType); // true
console.log(person instanceof Object); // true

类的声明

基本的类声明语法

通过 class 关键字声明类

class PersonClass {
// 等价于 PersonType 构造函数
constructor(name) {
this.name = name;
}

// 等价于 PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
}

let person = new PersonClass("JiaJia");
person.sayName(); // "JiaJia"

console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true

console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"

创建方法同之前创建 PersonType 构造函数类似,在这里直接在类中通过 constructor 方法名来定义构造函数。除 constructor 外没有其它的保留方法名。

私有属性是实例属性,不会出现在原型中。建议在构造函数中定义所有的私有属性,从而只通过一处就可以控制类中的所有私有属性。

通过上面例子最后两行的输出可以发现,其实 class 关键字只是个语法糖,最终生成的类 PersonClass 仍然是一个函数,而方法也是定义在该函数的原型上的。


Note

与函数不同的是,类属性不可被赋予新值,在之前的示例中,PersonClass.prototype 就是这样一个只可读的类属性。


为何使用类语法

首先看一下类声明与函数声明的差异:

函数声明可以被提升,而类声明与 let 类似,不能被提升;真正执行声明语句之前,它们会一直存在于临时死区中。

类声明中的所有代码将自动运行与严格模式下,而且无法强行让代码脱离严格模式执行。

在自定义类型中,需要通过 Object.defineProperty() 方法手工指定某个方法为不可枚举;而在类中,所有方法都是不可枚举的。

每个类都有一个名为 [[Construct]] 的内部方法,通过关键字 new 调用那些不含 [[Construct]] 的方法会导致程序抛出错误。

使用除 new 关键字外的方式调用类的构造函数会导致程序抛出错误。

在类中修改类名会导致程序报错。

使用出了类之外的语法为之前示例中的 PersonClass 声明编写等价代码。

// 等价于PersonClass
let PersonType2 = (function () {
"use strict";

const PersonType2 = function (name) {
// 确保通过关键字new调用该函数
if (typeof new.target === "undefined") {
throw new Error("必须通过new关键字调用构造函数");
}

this.name = name;
}

Object.defineProperty(PersonType2.prototype, "sayName", {
value: function () {
// 确保不会通过new调用该方法
if (typeof new.target !== "undefined") {
throw new Error("不可使用关键字new调用该方法");
}

console.log(this.name);
},
enumerable: false,
writable: true,
configurable: true
});

return PersonType2;
}());

注意,这段代码中有两处 PersonType2 声明:

外部作用域中的 let 声明。

立即执行函数表达式(IIFE)中的const声明。

这也从侧门说明了为什么可以在外部修改类名而内部却不可修改。

从这个示例可以看出,尽管在不使用class关键字的前提下实现类的所有功能,但代码变的极为复杂。

类表达式

类和函数都是两种存在形式:声明形式表达式形式

基本的类表达式语法

let PersonClass = class {
// 等价于 PersonType 构造函数
constructor(name) {
this.name = name;
}

// 等价于 PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
}

let person = new PersonClass("JiaJia");
person.sayName(); // "JiaJia"

console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true

console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"

console.log(PersonClass.name); // "PersonClass"

命名类表达式

let PersonClass = class PersonClass2 {
// 等价于 PersonType 构造函数
constructor(name) {
this.name = name;
}

// 等价于 PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
}

console.log(PersonClass.name); // "PersonClass2"
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass2); // "undefined"

类的名称为 PersonClass2,但是在声明外部并不存在一个名为 PersonClass2 的绑定,标识符 PersonClass2 只存在与类定义中。

将上述示例改成不使用 class 关键字的等价声明:

// 等价于命名类表达式 PersonClass
let PersonClass = (function () {
"use strict";

const PersonClass2 = function (name) {
// 确保通过关键字new调用该函数
if (typeof new.target === "undefined") {
throw new Error("必须通过new关键字调用构造函数");
}

this.name = name;
}

Object.defineProperty(PersonClass2.prototype, "sayName", {
value: function () {
// 确保不会通过new调用该方法
if (typeof new.target !== "undefined") {
throw new Error("不可使用关键字new调用该方法");
}

console.log(this.name);
},
enumerable: false,
writable: true,
configurable: true
});

return PersonClass2;
}());

在JS引擎中,类表达式的实现与类声明稍有不同。

类声明

通过let定义的外部绑定与通过const定义的内部绑定具有相同名称

命名类表达式

通过const定义名称

作为一等公民的类

在程序中,一等公民是指一个可以传入函数,可以从函数返回,并且可以赋值给变量的值。

JS中函数是一等公民,ES6中也将类设计为一等公民,允许通过多种方式使用类的特性。

function createOjbect(classDef) {
return new classDef();
}

let obj = createObject(class {
sayHi() {
console.log("Hi!");
}
});

obj.sayHi();

类表达式还有另外一种使用方式,通过立即调用类构造函数可以创建单例

let person = new class {
constructor(name) {
this.name = name;
}

sayName() {
console.log(this.name);
}
}("JiaJia");

person.sayName(); // "JiaJia"

访问器属性

类支持在原型上定义访问器属性。

class CustomHTMLElement {
constructor(element) {
this.element = element;
}

// getter
get html() {
return this.element.innerHTML;
}

// setter
set html(value) {
this.element.innerHTML = value;
}
}

var descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, "html");
console.log("get" in descriptor); // true
console.log("set" in descriptor); // true
console.log(descriptor.enumerable); // false

下面是上面示例非类形式的等价代码。

let CustomHTMLElement = (function() {
"use strict";

const CustomHTMLElement = function(element) {
// 确保通过关键字new调用该函数
if (typeof new.target === "undefined") {
throw new Error("必须通过关键字new调用构造函数");
}
this.element = element;
}

Object.defineProperty(CustomHTMLElement.prototype, "html", {
enumerable: false,
configurable: false,
get: function() {
return this.element.innerHTML;
},
set: function(value) {
this.element.innerHTML = value;
}
});

return CustomHTMLElement;
}());

比起非类等效实现,类语法可以节省很多代码。

可计算成员名称

用方括号包裹一个表达式即可使用可计算名称。

let methodName = "sayName";

class PersonClass {
constructor(name) {
this.name = name;
}

[methodName]() {
console.log(this.name);
}
}

let me = new PersonClass("JiaJia");
me.sayName(); // "JiaJia"

通过相同的方式可以在访问器属性中应用可计算名称:

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();

可以通过 Symbol.iterator 定义类的默认迭代器。

class Collection {
constructor() {
this.items = [];
}

*[Symbol.iterator]() {
yield *this.items;
}
}

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

静态成员

在ES5及早期版本中,直接将方法添加到构造函数中来模拟静态成员是一种常见的模式。

function PersonType(name) {
this.name = name;
}

// 静态方法
Person.create = function(name) {
return new PersonType(name);
};

// 实例方法
PersonType.prototype.sayName = function() {
console.log(this.name);
};

var person = PersonType.create("JiaJia");

ES6中简化了创建静态成员的过程,在方法或访问器属性前使用正式的静态注释即可。

class PersonClass {
// 等价于PersonType构造函数
constructor(name) {
this.name = name;
}

// 等价于PersonType.prototype.sayName
sayName() {
console.log(this.name);
}

// 等价于PersonType.create
static create(name) {
return new PersonClass(name);
}
}

let person = PersonClass.create("JiaJia");

类中的所有方法和访问器属性都可以用 static 关键字来定义,唯一的限制是不能将 static 用于定义构造函数方法。


Note

不可在实例中访问静态成员,必须要直接在类中访问静态成员。


继承与派生类

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, {
constructor: {
value: Square,
enumerable: true,
writable: true,
configruable: true
}
});

var square = new Square(3);

console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true

Square 继承自 Rectangle,为了这样做,必须用一个创建自 Rectangle.prototype 的新对象重写 Square.prototype 并调用 Rectangle.call() 方法。

类的出现可以让我们更轻松的实现继承功能。

class Rectangle {
constructor(length, width) {
this.length = length;
this.width = width;
}

getArea() {
return this.length * this.width;
}
}

class Square extends Rectangle {
// 等价于Rectangle.call(this, length, length)
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() 并传入所有参数。


关于 super() 的小贴士

只可在派生类的构造函数中使用 super(),如果尝试在非派生类(不是用 extends 声明的类)或函数中使用则会导致程序抛出错误。

在构造函数中访问 this 之前一定要调用 super(),它负责初始化this,如果在调用 super() 之前尝试访问 this 会导致程序出错。

如果不想调用 super(),则唯一的方法是让类的构造函数返回一个对象。


类方法遮蔽

派生类中的方法总会覆盖基类中的同名方法。

如果想调用基类中的同名方法,需使用 super.method() 的方式调用。

静态成员继承

如果基类有静态成员,那么这些静态成员在派生类中也可用。

派生自表达式的类

ECMAScript 6最强大的一面或许是从表达式导出类的功能了。只要表达式可以被解析为一个函数并且具有 [[Construct]] 属性和原型,那么就可以用 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); // true

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); // true

此示例实现的功能同之前的示例等价。

extends 后面跟的是方法调用 getBase(),Square 类继承自该方法的返回值。

通过这种方式,可以动态的决定类的继承。

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}

内建对象的继承

通过继承的方式创建属于自己的特殊数组。在ES5及早期版本中,这几乎是不可能的。

var colors = [];
colors[0] = "red";
console.log(colors.length); // 1

colors.length = 0;
console.log(colors[0]); // undefined

// 尝试通过ES5语法继承数组
function MyArray() {
Array.apply(this, arguments);
}

MyArray.prototype = Object.assign(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});

var colors = new MyArray();
colors[0] = "red";
console.log(colors.length); // 0

colors.length = 0;
console.log(colors[0]); // "red"

通过最后的输出可以看出,自定义的数组类型与预想的结果不符。

ES6类语法的一个目标就是支持内建对象继承,因而ES6中的类继承模型与ES5及早期版本中的稍有不同。

在ES5的传统继承方式中,先由派生类型创建 this 的值,然后调用基类型的构造函数。这也意味着,this的值开始指向的是 MyArray 的实例,但是随后会被来自 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]); // undefined

Symbol.species 属性

内建对象的一个实用之处是,原本在内建对象中返回实例自身的方法将自动返回派生类的实例。

class MyArray extends Array {

}

let items = new MyArray(1, 2, 3, 4),
subitems = items.slice(1, 3);

console.log(items instanceof MyArray); // true
console.log(subitems instanceof MyArray); // true

在JS引擎背后是通过 Symbol.species 属性实现该功能的。

Symbol.species 被用于定义返回函数的静态访问器属性。被返回的函数是一个构造函数,每当要在实例的方法中(不是在构造函数中)创建类的实例时必须使用这个构造函数。

以下这些内建类型均已定义 Symbol.species 属性(该属性返回值为 this,这也意味着该属性总会返回构造函数):

Array

ArrayBuffer

Map

Promise

RegExp

Set

Typed arrays

几个内建类型像这样使用 Symbol.species:

class MyClass {
static get [Symbol.species]() {
return this;
}

constructor(value) {
this.value = value;
}

clone() {
return new this.constructor[Symbol.species](this.value);
}
}

class MyDerivedClass1 extends MyClass {

}

class MyDerivedClass2 extends MyClass {
static get [Symbol.species]() {
return MyClass;
}
}

let instance1 = new MyDerivedClass1("foo"),
clone1 = instance1.clone(),
instance2 = new MyDerivedClass2("foo"),
clone2 = instance2.clone();

console.log(clone1 instanceof MyClass); // true
console.log(clone1 instanceof MyDerivedClass1); // true
console.log(clone2 instanceof MyClass); // true
console.log(clone2 instanceof MyDerivedClass2); // false

上例中 MyDerivedClass2 重写了 Symbol.species 属性,使其返回的不再是派生类的构造函数,而是基类的构造函数。所以最终 clone() 的结果不再是派生类型。

在类的构造函数中使用 new.target

在类的构造函数中,也可以通过 new.target 来确定类是如何被调用的。

在简单情况下,new.target 等于类的构造函数。

class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}

// new.tareget的值是Rectangle
var obj = new Rectangle(3, 4); // true

再看看另外一种情况:

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.tareget的值是Square
var obj = new Square(3); // false

上例中,通过派生类调用时, new.target 指向的是派生类的构造函数。

利用这个特性,可以创建一个抽象基类(不能被直接实例化的类)

// 抽象基类
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error("这个类不能被直接实例化。");
}
}
}

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: