《Understanding ES6》chapter 9 JavaScript Classes

195 阅读11分钟

2019-7-29

第九章 js类

(一)ES5中的仿类结构

自定义类型:创建一个构造器,将原型指派到该构造器上

    function Person(name) {
      this.name = name;
    }
    Person.prototype.sayName = function () {
      console.log(this.name);
    }
    let per = new Person('zc');
    per.sayName(); //zc

    console.log(per instanceof Person); //true
    console.log(per instanceof Object); //true

(二)ES6中的类

1. 类声明

class className {}
class PersonClass {
  // 等价于Person构造器
  constructor(name) {
    this.name = name;
  }
  // 等价于 Person.prototype.sayName
  sayName() {
    console.log(this.name);
  }
}

let person = new PersonClass('zc');
person.sayName(); // zc

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

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

自有属性:实例上的属性,只能在类的构造器或者方法内部进行创建,比如本例中的name,建议在构造器函数内创建所有可能出现的自有属性,有助于代码检查。

注意:类声明仅仅是自定义类型的语法糖。

类声明与自定义类型的区别:

1)与函数定义不同,类声明不会被提升,行为与let相似,在程序执行到声明处之前,类都会位于暂时性死区;

2)类声明中的所有代码会自动运行并锁定在严格模式下;

3)类的所有方法都是不可枚举的,自定义类型必须用Object.defineProperty()才能将方法改变为不可枚举;

4)类的所有方法内部都没有[[Contruct]],不能使用new来调用;

5)调用类的构造器,必须使用new,否则会报错;

6)不能在类的方法内部重写类名,会报错,但可以在外部重写类名

上例中的PersonClass类声明实际上等价于以下未使用类语法的代码:

// 直接等价于PersonClass 
// let声明确保类声明不会被提升,立即执行函数来确保类声明中的所有代码会自动运行
let PersonType2 = (function() {

    "use strict";

  // const确保类名不能在类的方法内被重写
    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";

2. 类表达式

类和函数相似,都有两种形式:声明与表达式。

类表达式被设计用于变量声明,或可作为参数传递给函数。

1)基本类表达式:(匿名类表达式)

let PersonClass = class {

    // 等价于Person构造器
    constructor(name) {
        this.name = name;
    }

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

使用类声明还是类表达式,主要是代码风格问题。相对于函数声明和函数表达式之间的区别,类声明和类表达式都不会被提升,因此对代码运行时的行为影响甚微。

2) 具名类表达式

let PersonClass = class PersonClass2 {

    // 等价于Person构造器
    constructor(name) {
        this.name = name;
    }

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

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

PersonClass标识符只在类定义内部存在,因此只能用在类方法内部,如本例中的sayName()内。

以下PersonClass具名类表达式实际上等价于下面未使用类语法的代码;

// 直接等价于PersonClass 
// let声明确保类声明不会被提升,立即执行函数来确保类声明中的所有代码会自动运行
let PersonClass = (function() {

    "use strict";

  // const确保类名不能在类的方法内被重写
    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;
}());

类声明:用let定义的外部绑定和用const定义的内部绑定同名;

类表达式:可在内部使用const来定义不同的名称。

3. 作为一等公民的类

一等公民:能被当做值来使用,所有可以给变量赋值,作为函数参数,函数返回值。

  // 作为函数参数  
	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);
      }
    }('Nicholas');

  person.sayName();

4. 类与对象字面量的相似点

1)访问器属性

在类上创建访问器属性,所用语法类似于对象字面量。

   class CustomHTMLElement{
      constructor(element){
        this.element = element;
      }
      get html() {
        return this.element.innerHTML;
      }
      set html(value) {
         this.element.innerHTML = value;
      }
    }
		
		// getter,setter定义在原型上
    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) {

          // make sure the function was called with 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 descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype,'html')
      console.log('get' in descriptor);   // true
      console.log('set' in descriptor);   // true
      console.log(descriptor.enumerable); // false
2)可计算的成员名

类方法和类的访问器属性都能使用可计算的名称,语法与对象字面量相同,用[]来包裹表达式。

    let methodName = "sayName";
    class PersonClass {
      constructor(name) {
        this.name = name;
      }
      // 类方法使用可计算的名称
      [methodName]() {
        console.log(this.name);
      }
    }  
    let me = new PersonClass('zc');
    me.sayName();   // zc
  <div>hello, August!</div>
  <script>
    let propertyName = 'html';
    class CustomHTMLElement {
      constructor(element) {
        this.element = element;
      }
      // 访问器使用可计算名称
      get [propertyName]() {
        return this.element.innerHTML;
      }
      set [propertyName](value) {
        this.element.innerHTML = value;
      }
    }

    let div = document.getElementsByTagName('div')[0];
    let cus = new CustomHTMLElement(div);
    console.log(cus.html);  // hello, August!
  </script>
3)生成器方法

跟对象字面量相似,在类的方法名称前附上*,就能将对类的方法变为一个生成器。

   class MyClass {
      *createIterator() {
        yield 1;
        yield 2;
        yield 3;
      }
    }  

    let instance = new MyClass();
    let iterator = instance.createIterator();
    console.log(iterator.next());   // {value: 1, done: false}
  // 为表示集合的自定义类定义默认迭代器
	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.items){
      console.log(x);
    }

任意管理集合的类都包含一个默认迭代器,因为一些集合专用的操作都要求目标集合具有迭代器。

5. 静态成员

如果想让方法与访问器属性在对象实例上出现,应当把他们添加到类的原型上,而

如果将方法与访问器属性绑定到类自身,就需要使用静态成员。

静态成员不能用实例来访问,必须直接用类自身来访问它们。

ES5直接在构造器上添加方法来模拟静态成员:

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

// static method
PersonType.create = function(name) {
    return new PersonType(name);
};

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

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

ES6的类简化了静态成员的创建,只要在方法和访问器属性的名称前添加static 标注:

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('zc');
person.sayName(); // zc

可以在类的任何方法和访问器属性上使用static关键字,但不能用于constructor方法的定义。

(三) 类继承

1. ES5的继承

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: false,
      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

2. ES6让继承变得更轻松

extents — 指定当前类所需要继承的函数

super — 访问基类的构造器

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);
  }
}
  • 派生类:即子类,派生类如果指定了构造器,则必须使用super(),否则会报错。若没有指定构造器,super()方法会被自动调用,且会使用创建新实例时提供的所有参数。
class Square extends Rectangle {
    // 没有构造器
}

// 等价于:

class Square extends Rectangle {
    constructor(...args) {
        super(...args);
    }
}

使用super需注意:

1)只能在派生类中使用super()

2)在构造器中,必须在访问this之前调用super(),由于super()负责初始化this,因此试图先访问this会造成错误;

3)如果在类的构造器中不调用``super()`,唯一避免出错的办法是在构造器中返回一个对象。

3. 屏蔽基类的方法

派生类中的方法总是会屏蔽基类的同名方法。

class Square extends Rectangle {
    constructor(length) {
        super(length, length);
    }

    // 重写并屏蔽 Rectangle.prototype.getArea()
    getArea() {
        return this.length * this.length;
    }
}

4. 继承静态成员

class Rectangle {
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }
    static create(length, width) {
        return new Rectangle(length, width);
    }
}

class Square extends Rectangle {
    constructor(length) {
        // same as Rectangle.call(this, length, length)
        super(length, length);
    }
}

// 与Rectangle.create(3,4)一致
var rect = Square.create(3, 4);

console.log(rect instanceof Rectangle);     // true
console.log(rect instanceof Square);        // false

5. 从表达式中派生类

只要一个表达式能够返回一个具有[[Construct]]属性以及原型的函数,就可以对其使用extends。

let Rectangle = function(length, width) {
  this.length = length;
  this.width = width;
}
Rectangle.prototype.getArea = function () {
  return this.length * this.width;
}

// Rectangle具有[[Construct]]属性以及原型
class Square extends Rectangle {
  constructor(length) {
      // same as Rectangle.call(this, length, length)
      super(length, length);
  }
}
var square = new Square(2);
console.log(square.getArea()); // 4

因为extends后面能接受任意类型的表达式,所有还可以动态地决定所要继承的类:

let Rectangle = function(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) {
      // same as Rectangle.call(this, length, length)
      super(length, length);
  }
}
var square = new Square(2);
console.log(square.getArea());
console.log(square instanceof Rectangle);

还可以构建混入:

let SerializableMixin = {
  serialize() {
    return JSON.stringify(this);
  }
};
let AreaMixin = {
  getArea() {
    return this.length * this.width;
  }
};
function mixin(...mixins) {
  let base = function () {}
  // 若多个混入对象拥有相同的属性,只有最后添加的属性会保留
  Object.assign(base.prototype, ...mixins);
  return base;
}

function getBase() {
  return Rectangle;
}
class Square extends mixin(SerializableMixin, AreaMixin) {
  constructor(length) {
      super();
      this.length = length;
      this.width = length;
  }
}
var square = new Square(2);
console.log(square.getArea()); // 4
console.log(square.serialize());  // {"length":2,"width":2}

注意:null和生成器函数不存在[[Construct]],如果用在extends后会报错。

6. 继承内置对象

在ES5尝试继承数组,会出现length属性与数值属性的行为和内置数组不一致。

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的值先被派生类(如:MyArray)创建,随后基类构造器(如:Array.apply()方法;)才被调用。这意味着this一开始就是MyArray的实例,之后才使用Array的附加属性对其进行装饰。

ES6: this的值先被基类(如:Array)创建,随后才被派生类的构造器(如:MyArray)修改。这意味着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

7. Symbol.species属性

继承内置对象会带来一个有趣的特性,任意能返回内置对象实例的方法,在派生类上都会自动返回派生类的实例,比如,继承了Array的派生类MyArray,诸如slice()之类的方法都会返回MyArray的实例。

class MyArray extends Array{

}
var items = new MyArray(1,2,3,4,5);
// slice()方法是从Array上继承的,原本应该返回的是Array的实例,但这里却返回了MyArray的实例
var sliceItems = items.slice(1,3);

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

Symbol.species:定义了一个可以返回function的静态访问器属性,当不是使用类本身的构造器来创建实例时,这个function会被当做构造器来创造实例。

以下内置类型都定义了Symbol.species

  • Array
  • ArrayBuffer
  • Map
  • Promise
  • RegExp
  • Set
  • 类型化数组

以上每个类型都拥有默认的Symbol.species属性,其返回值为this,意味着该属性总是会返回自身的构造函数。

// 几个内置类型使用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 {
  // 重写了Symbol.species,让其返回MyClass
  static get [Symbol.species]() {
    return MyClass;
  }
}

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

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

使用Symbol.species,任意派生类在调用应当返回实例的方法时,都可以判断出需要返回什么类型的值。

class MyArray extends Array{
  // 重写了Symbol.species,让其返回Array
  static get [Symbol.species]() {
    return Array;
  }
}
var items = new MyArray(1,2,3,4,5);
var sliceItems = items.slice(1,3);

console.log(items instanceof MyArray);   // true
console.log(sliceItems instanceof MyArray);   // false
console.log(sliceItems instanceof Array);   // true

8. 在类的构造器中使用new.target

在简单情况下,new.target就等于本类的构造器函数;

在有继承的情况下,基类的new.target有可能会等于派生类的构造器函数

class Rectangle{
  constructor() {
    console.log(new.target);
  }
}
var obj = new Rectangle();   // 输出Rectangle
class Square extends Rectangle{

}
// new.target是Square
var squ = new Square();   // 输出Square

创建抽象基类(不能被实例化的类,但仍能被其他类所继承):

class Shape{
  constructor(){
    // Shape不能被实例化
    if(new.target === Shape){
      throw new Error('this class cannot be instantiate directly');
    }
  }
}

class Rectangle extends Shape{
  constructor(lenght, width){
    super();
    this.lenght = lenght;
    this.width = width;
  }
}

var x = new Shape();   // 抛出错误
var y = new Rectangle(3, 4);
console.log(y instanceof Shape);  // true