ES6常用但被忽略的方法(第八弹Class)

3,296 阅读8分钟

写在开头

  • ES6常用但被忽略的方法 系列文章,整理作者认为一些日常开发可能会用到的一些方法、使用技巧和一些应用场景,细节深入请查看相关内容连接,欢迎补充交流。

相关文章

Class 函数

简介

  • class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到。
// 传统写法
function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);

// class 写法
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
  • 定义类的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。方法之间不需要逗号分隔,加了会报错。 类的所有方法都定义在类的prototype属性上面。
class Point {
  constructor() {...}
  toString() {...}
  toValue() {...}
}
// 等同于
Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

typeof Point // "function"
Point === Point.prototype.constructor // true
  • 类的内部所有定义的方法,都是不可枚举的(non-enumerable)。这与 ES5 的行为不一致。
// es6
class Point {
  constructor(x, y) {...}
  toString() {...}
}

Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]

// es5
var Point = function (x, y) {...};
Point.prototype.toString = function() {...};

Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]

constructor 方法

  • constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
  • constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。
class Foo {
  constructor() {
    return Object.create(Object);
  }
}

new Foo() instanceof Foo // false
new Foo() instanceof Object // true

取值函数(getter)和存值函数(setter)

  • ES5 一样,在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。存值函数和取值函数是设置在属性的 Descriptor 对象上的。
class MyClass {
  constructor() {...}
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}

let inst = new MyClass();
inst.prop = 123; // setter: 123
inst.prop; // 'getter'

const descriptor = Object.getOwnPropertyDescriptor(
  MyClass.prototype, "prop"
);

"get" in descriptor  // true
"set" in descriptor  // true

表达式

  1. 属性表达式
    let methodName = 'getArea';
    
    class Square {
      constructor(length) {...}
      [methodName]() {...}
    }
    
  2. Class 表达式
    const MyClass = class Me {
      getClassName() {
        return Me.name;
      }
    };
    
    • Class 表达式,可以写出立即执行的 Class。
    let person = new class {
      constructor(name) {
        this.name = name;
      }
      sayName() {
        console.log(this.name);
      }
    }('detanx');
    person.sayName(); // "detanx"
    

注意点

  1. 严格模式

    • 类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。
  2. 不存在提升

    • 使用在前,定义在后,这样会报错。
    new Foo(); // ReferenceError
    class Foo {}
    
  3. name 属性

    • 由于本质上,ES6的类只是ES5 的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性。
    class Point {}
    Point.name // "Point"
    
    • name属性总是返回紧跟在class关键字后面的类名。
  4. Generator 方法

    • 如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数。
  5. this 的指向

    • 类的方法内部如果含有this,它默认指向类的实例。一旦单独使用该方法,很可能报错。
    class Logger {
      printName(name = 'there') {
        this.print(`Hello ${name}`);
      }
      print(text) {
        console.log(text);
      }
    }
    
    const logger = new Logger();
    const { printName } = logger;
    printName(); // TypeError: Cannot read property 'print' of undefined
    

静态方法

  • 类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo {
  static classMethod() {
    return 'hello';
  }
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
  • 如果静态方法包含this关键字,这个this指的是类,而不是实例。
  • 父类的静态方法,可以被子类继承。
class Foo {
  static classMethod() {
    return 'detanx';
  }
}
class Bar extends Foo {
}
Bar.classMethod() // 'detanx'
  • 静态方法也是可以从super对象上调用的。
class Foo {
  static classMethod() {
    return 'hello';
  }
}
class Bar extends Foo {
  static classMethod() {
    return super.classMethod() + ', detanx';
  }
}
Bar.classMethod() // "hello, detanx"

实例属性的新写法

  • 实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。
class IncreasingCounter {
  _count = 0
  => // 或写在constructor
  // constructor() {
  //  this._count = 0;
  // }
  get value() {
    console.log('Getting the current value!');
    return this._count;
  }
  increment() {
    this._count++;
  }
}

静态属性

  • 静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。
class Foo { }
Foo.prop = 1;
Foo.prop // 1
  • 现在有一个 提案(目前处于stage 3) 提供了类的静态属性,写法是在实例属性的前面,加上static关键字。
// 新写法
class Foo {
  static prop = 1;
}

私有方法和私有属性

  1. 现有的解决方案
    • 私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但 ES6 不提供,只能通过变通方法模拟实现。
    1. 做法是在命名上加以区别。
      • 开发时约定以什么开头的属性或方法为私有属性。例如以 _ 开头或者 $ 开头的为私有。
    2. 将私有方法移出模块
      class Widget {
        foo (baz) {
          bar.call(this, baz);
        }
      }
      function bar(baz) {
        return this.snaf = baz;
      }
      
    3. 利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。ES6常用但被忽略的方法(第三弹Symbol、Set 和 Map )之Symbol的应用
      const bar = Symbol('bar');
      const snaf = Symbol('snaf');
      
      export default class myClass{
        // 公有方法
        foo(baz) {
          this[bar](baz);
        }
        // 私有方法
        [bar](baz) {
          return this[snaf] = baz;
        }
      };
      
      const inst = new myClass();
      Reflect.ownKeys(myClass.prototype)
      // [ 'constructor', 'foo', Symbol(bar) ]
      
  2. 私有属性的提案
  • 目前,有一个 提案Stage 3),为class加了私有属性和方法。方法是在属性名和方法之前,使用 # 表示。私有属性也可以设置 gettersetter 方法。
class Counter {
  #xValue = 0;
  constructor() {
    super();
    // ...
  }
  get #x() { return #xValue; }
  set #x(value) {
    this.#xValue = value;
  }
}
  • 私有属性和私有方法前面,也可以加上static关键字,表示这是一个静态的私有属性或私有方法。

new.target 属性

  • new是从构造函数生成实例对象的命令。ES6new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。Class 内部调用new.target,返回当前 Class
class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    this.length = length;
    this.width = width;
  }
}
var obj = new Rectangle(3, 4); // 输出 true
  • 子类继承父类时,new.target会返回子类。 利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。
class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error('本类不能实例化');
    }
  }
}
class Rectangle extends Shape {
  constructor(length, width) {
    super();
  }
}
var x = new Shape();  // 报错
var y = new Rectangle(3, 4);  // 正确

继承

注意点

  1. 子类继承父类需要先调用super方法。
    class Point { /* ... */ }
    class ColorPoint extends Point {
      constructor() {
      }
    }
    let cp = new ColorPoint(); // ReferenceError
    
    • ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。
  2. 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。
    class Point {
      constructor(x) {
        this.x = x;
      }
    }
    class ColorPoint extends Point {
      constructor(x, color) {
        this.color = color; // ReferenceError
        super(x);
        this.color = color; // 正确
      }
    }
    

Object.getPrototypeOf()

  • Object.getPrototypeOf方法可以用来从子类上获取父类。可以使用这个方法判断,一个类是否继承了另一个类。
Object.getPrototypeOf(ColorPoint) === Point // true

super 关键字

  • super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
  1. super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。否则 JavaScript 引擎会报错。
    class A {
      constructor() {
        console.log(new.target.name);
      }
    }
    class B extends A {
      constructor() {
        super();
      }
    }
    new A() // A
    new B() // B
    
    • super虽然代表了父类A的构造函数,但是返回的是子类B的实例,super()相当于A.prototype.constructor.call(this)super()内部的this指向的是B
    • 作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。
    class A {}
    class B extends A {
      m() {
        super(); // 报错
      }
    }
    
  2. super作为对象时
    • 普通方法中,指向父类的原型对象;静态方法(带static前缀的方法)中,指向父类。
    class A {
      p() {
        return 2;
      }
    }
    class B extends A {
      constructor() {
        super();
        console.log(super.p()); // 2
      }
    }
    
    let b = new B();
    
    • super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。如果属性定义在父类的原型对象上,super就可以取到。
    class A {
      constructor() {
        this.p = 2;
      }
    }
    class B extends A {
      get m() {
        return super.p;
      }
    }
    let b = new B();
    b.m // undefined
    
    // 定义到原型上
    class A {}
    A.prototype.x = 2;
    
    class B extends A {
      constructor() {
        super();
        console.log(super.x) // 2
      }
    }
    let b = new B();
    
    • 在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。
    class A {
      constructor() {
        this.x = 1;
      }
      static print() {
        console.log(this.x);
      }
    }
    
    class B extends A {
      constructor() {
        super();
        this.x = 2;
      }
      static m() {
        super.print();
      }
    }
    
    B.x = 3;
    B.m() // 3
    
    • 使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。
    class A {}
    
    class B extends A {
      constructor() {
        super();
        console.log(super); // 报错
      }
    }
    
    • 由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字。
    var obj = {
      toString() {
        return "MyObject: " + super.toString();
      }
    };
    
    obj.toString(); // MyObject: [object Object]
    

类的 prototype 属性和__proto__属性

  • 大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。存在两条继承链。
    1. 子类的__proto__属性,表示构造函数的继承,总是指向父类。
    2. 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
    class A {}
    class B extends A {}
    
    B.__proto__ === A // true
    B.prototype.__proto__ === A.prototype // true
    
  • 类的继承模式实现
    class A {}
    class B {}
    
    // B 的实例继承 A 的实例
    Object.setPrototypeOf(B.prototype, A.prototype);
    // B 继承 A 的静态属性
    Object.setPrototypeOf(B, A);
    
    const b = new B();
    
    • Object.setPrototypeOf方法的实现。
    Object.setPrototypeOf = function (obj, proto) {
      obj.__proto__ = proto;
      return obj;
    }
    
  • 类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。子类的原型的原型,是父类的原型。
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');

p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true

原生构造函数的继承

  • 原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有:Boolean()Number()String()Array()Date()Function()RegExp()Error()Object()...
  • ES6 可以自定义原生数据结构(比如ArrayString等)的子类,这是 ES5 无法做到的。例如实现一个自己的带其他功能的数组类。
class VersionedArray extends Array {
  constructor() {
    super();
    this.history = [[]];
  }
  commit() {
    this.history.push(this.slice());
  }
  revert() {
    this.splice(0, this.length, ...this.history[this.history.length - 1]);
  }
}

var x = new VersionedArray();

x.push(1);
x.push(2);
x // [1, 2]
x.history // [[]]

x.commit();
x.history // [[], [1, 2]]

x.push(3);
x // [1, 2, 3]
x.history // [[], [1, 2]]

x.revert();
x // [1, 2]
class NewObj extends Object{
  constructor(){
    super(...arguments);
  }
}
var o = new NewObj({attr: true});
o.attr === true  // false
  • 上面代码中,NewObj继承了Object,但是无法通过super方法向父类Object传参。这是因为 ES6 改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6 规定Object构造函数会忽略参数。

Mixin 模式的实现

  • Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。使用的时候,只要继承这个类即可。
function mix(...mixins) {
  class Mix {
    constructor() {
      for (let mixin of mixins) {
        copyProperties(this, new mixin()); // 拷贝实例属性
      }
    }
  }
  for (let mixin of mixins) {
    copyProperties(Mix, mixin); // 拷贝静态属性
    copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
  }
  return Mix;
}

function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if ( key !== 'constructor'
      && key !== 'prototype'
      && key !== 'name'
    ) {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}

// 使用
class DistributedEdit extends mix(Loggable, Serializable) {
  // ...
}