JavaScript < ES5、ES6、ES7、… >ECMAScript 6面向对象

ES6(十四)类

2019-02-02  本文已影响47人  CodeMT

前面的话


  大多数面向对象的编程语言都支持类和类继承的特性,而JS却不支持这些特性,只能通过其他方法定义并关联多个相似的对象,这种状态一直延续到了ES5。由于类似的库层出不穷,最终还是在ECMAScript 6中引入了类的特性。本文将详细介绍ES6中的类

ES5近似结构

ES5中没有类的概念,最相近的思路是创建一个自定义类型:首先创建一个构造函数,然后定义另一个方法并赋值给构造函数的原型

function PersonType(name) {
  this.name = name;
}
PersonType.prototype.sayName = function() {
  console.log(this.name);
};
let person = new PersonType("huochai");
person.sayName(); // 输出 "huochai"
console.log(person instanceof PersonType); // true
console.log(person instanceof Object); // true

类的声明

ES6有一种与其他语言中类似的类特性:类声明。同时,它也是ES6中最简单的类形式

【基本的类声明语法】

要声明一个类,首先编写class关键字,紧跟着的是类的名字,其他部分的语法类似于对象字面量方法的简写形式,但不需要在类的各元素之间使用逗号分隔

class PersonClass {   
  // 等价于 PersonType 构造器 
  constructor(name) {
    this.name = name;
  }
  // 等价于 PersonType.prototype.sayName
  sayName() {
    console.log(this.name);
  }
}
let person = new PersonClass("huochai");
person.sayName(); // 输出 "huochai"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"

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

【为何使用类语法】

尽管类与自定义类型之间有诸多相似之处,但是它们之间仍然有一些差异

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

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

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

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

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

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

// 直接等价于 PersonClass
let PersonType2 = (function() {   
  "use strict";
  const PersonType2 = function(name) {        
    // 确认函数被调用时使用了 new
    if (typeof new.target === "undefined") {            
      throw new Error("Constructor must be called with new.");
    }        
    this.name = name;
  }
  Object.defineProperty(PersonType2.prototype, "sayName", {
    value: function() {            
      // 确认函数被调用时没有使用 new
      if (typeof new.target !== "undefined") {                
        throw new Error("Method cannot be called with new.");
      }
      console.log(this.name);
    },
    enumerable: false,
    writable: true,
    configurable: true 
    });
    return PersonType2;
}());

【常量类名】

类的名称只在类中为常量,所以尽管不能在类的方法中修改类名,但可以在外部修改

class Foo {
  constructor() {
    Foo = "bar"; // 执行时抛出错误 
  }
}// 但在类声明之后没问题Foo = "baz";

类表达式

类和函数都有两种存在形式:声明形式和表达式形式。声明形式的函数和类都由相应的关键字(分别为functionclass)进行定义,随后紧跟一个标识符;表达式形式的函数和类与之类似,只是不需要在关键字后添加标识符

类表达式的设计初衷是为了声明相应变量或传入函数作为参数

【基本的类表达式语法】

下面这段代码等价于之前PersonClass示例的类表达式

let PersonClass = class {
  // 等价于 PersonType 构造器 
  constructor(name) {
    this.name = name;
  }
  // 等价于 PersonType.prototype.sayName 
  sayName() {
    console.log(this.name);
  }
};
let person = new PersonClass("huochai");
person.sayName(); // 输出 "huochai"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass.prototype.sayName); // "function"

【命名类表达式】

类与函数一样,都可以定义为命名表达式。声明时,在关键字class后添加一个标识符即可

let PersonClass = class PersonClass2 {
  // 等价于 PersonType 构造器 
  constructor(name) {
    this.name = name;
  }
  // 等价于 PersonType.prototype.sayName 
  sayName() {
    console.log(this.name);
  }
};
console.log(typeof PersonClass); // "function"
console.log(typeof PersonClass2); // "undefined"
// 直接等价于 PersonClass 具名的类表达式
let PersonClass = (function() {    
  "use strict";
  const PersonClass2 = function(name) {        
    // 确认函数被调用时使用了 new
    if (typeof new.target === "undefined") {            
      throw new Error("Constructor must be called with new.");
    }        
    this.name = name;
  }
  Object.defineProperty(PersonClass2.prototype, "sayName", {
    value: function() {            
      // 确认函数被调用时没有使用 new
      if (typeof new.target !== "undefined") {                
        throw new Error("Method cannot be called with new.");
      }
      console.log(this.name);
    },
    enumerable: false,
    writable: true,
    configurable: true 
  });
  return PersonClass2;
}());

一等公民

在程序中,一等公民是指一个可以传入函数,可以从函数返回,并且可以赋值给变量的值。JS函数是一等公民(也被称作头等函数),这也正是JS中的一个独特之处

ES6延续了这个传统,将类也设计为一等公民,允许通过多种方式使用类的特性。例如,可以将类作为参数传入函数中

function createObject(classDef) {
  return new classDef();
}
let obj = createObject(class {
  sayHi() {
    console.log("Hi!");
  }
});
obj.sayHi(); // "Hi!"
let person = new class {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(this.name);
  }
}("huochai");
person.sayName(); // "huochai"

访问器属性

尽管应该在类构造函数中创建自己的属性,但是类也支持访问器属性。创建getter时,需要在关键字get后紧跟一个空格和相应的标识符;创建setter时,只需把关键字get替换为set即可

class CustomHTMLElement {
  constructor(element) {        
    this.element = element;
  }
  get html() {
    return this.element.innerHTML;
  }
  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("Constructor must be called with new.");
  }        
  this.element = element;
  }
  Object.defineProperty(CustomHTMLElement.prototype, "html", {
    enumerable: false,
    configurable: true,
    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("huochai");
me.sayName(); // "huochai"
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();
class Collection {
  constructor() {        
    this.items = [];
  }*[Symbol.iterator]() {
    yield *this.items.values();
  }
}
var collection = new Collection();
collection.items.push(1);
collection.items.push(2);
collection.items.push(3);
for (let x of collection) {
  // 1
  // 2
  // 3 
  console.log(x);
}

静态成员

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

[注意]不可在实例中访问静态成员,必须要直接在类中访问静态成员

继承与派生类

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,
    configurable: true 
  }
});
var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
class Rectangle {
  constructor(length, width) {        
    this.length = length;this.width = width;
  }
  getArea() {
    return this.length * this.width;
  }
}
class Square extends Rectangle {
  constructor(length) {        
    // 与 Rectangle.call(this, length, 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
class Square extends Rectangle {    
  // 没有构造器
}
// 等价于:
class Square extends Rectangle {
  constructor(...args) {
    super(...args);
  }
}

注意事项

使用super()时有以下几个关键点

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

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

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

【类方法遮蔽】

派生类中的方法总会覆盖基类中的同名方法。比如给square添加getArea()方法来重新定义这个方法的功能

class Square extends Rectangle {
  constructor(length) {
    super(length, length);
  }    
  // 重写并屏蔽 Rectangle.prototype.getArea() 
  getArea() {
    return this.length * this.length;
  }
}
class Square extends Rectangle {
  constructor(length) {
    super(length, length);
  }    
  // 重写、屏蔽并调用了 Rectangle.prototype.getArea() 
  getArea() {
    return super.getArea();
  }
}

【静态成员继承】

如果基类有静态成员,那么这些静态成员在派生类中也可用。JS中的继承与其他语言中的继承一样,只是在这里继承还是一个新概念

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) {
    // 与 Rectangle.call(this, length, 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

【派生自表达式的类】

ES6最强大的一面或许是从表达式导出类的功能了。只要表达式可以被解析为一个函数并且具有[[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
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
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}"

[注意]extends后可以使用任意表达式,但不是所有表达式最终都能生成合法的类。如果使用null或生成器函数会导致错误发生,类在这些情况下没有[[Consturct]]属性,尝试为其创建新的实例会导致程序无法调用[[Construct]]而报错

【内建对象的继承】

JS数组诞生以来,一直都希望通过继承的方式创建属于自己的特殊数组。在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.create(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"
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属性】

内建对象继承的一个实用之处是,原本在内建对象中返回实例自身的方法将自动返回派生类的实例。所以,如果有一个继承自Array的派生类MyArray,那么像slice()这样的方法也会返回一个MyArray的实例

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
Array
ArrayBuffer
Map
Promise
RegExp
Set
Typed arrays
// 几个内置类型使用 species 的方式类似于此
class MyClass {
  static get [Symbol.species]() {        
    return this;
  }
  constructor(value) {        
    this.value = value;
  }
  clone() {
    return new this.constructor[Symbol.species](this.value);
  }
}
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("bar"),
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
class MyArray extends Array {
  static get [Symbol.species]() {        
    return Array;
  }
}
let items = new MyArray(1, 2, 3, 4),
subitems = items.slice(1, 3);
console.log(items instanceof MyArray); // true
console.log(subitems instanceof Array); // true
console.log(subitems instanceof MyArray); // false

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

new.target及它的值根据函数被调用的方式而改变。在类的构造函数中也可以通过new.target来确定类是如何被调用的。简单情况下,new.target等于类的构造函数

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    this.length = length;this.width = width;
  }
}
// new.target 就是 Rectanglevar 
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.target 就是 Squarevar 
obj = new Square(3); // 输出 false
// 静态的基类
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

[注意]因为类必须通过new关键字才能调用,所以在类的构造函数中,new.target属性永远不会是undefined

上一篇 下一篇

猜你喜欢

热点阅读