JavaScript之ES6新增类

ES6引入的class关键字具有正式定义类的能力。类class是ES中新的基础语法糖结构。
虽然ES6类表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍然是原型和构造函数的概念。

引入class关键字为了解决使用各种实现继承的代码非常冗长和混乱的问题。

类定义

  • 定义类的主要方式:

    • 类声明 class Person {}
    • 类表达式 let Person = Class {}
  • 与函数表达式类似,类表达式在它们被求值之前不能引用。函数声明可以提升,但类定义不能提升。

  • 函数受函数作用域限制,类受块作用域限制:

  {
    function FunctionDeclaration() {}
    class ClassDeclaration {}
  }

  FunctionDeclaration; // FunctionDeclaration () {}
  ClassDeclaration; // ReferenceError: ClassDeclaration is not defined
  • 类的构成

    • 构造函数constructor
    • 实例方法
    • 获取函数get
    • 设置函数set
    • 静态类方法static
  • 空类照样有效。默认情况下,类定义中的代码都在严格模式下执行。

  // 空类,有效
  class Foo {}

  // 有构造函数的类,有效
  class Foo {
    constructor() {}
  }

  // 有获取函数,有效
  class Foo {
    get mybaz() {}
  }

  // 有静态方法的类,有效
  class Foo {
    static myFoo() {}
  }
  • 类表达式的名称是可选的。在把类表达式赋值给变量之后,可以通过name属性取得类表达式的名称字符串。但不能在类表达作用域外部访问这个标识符。
  let Person = class PersonName {
    identify() {
      console.log(Person.name, PersonName.name);
    }
  }

  let p = new Person();

  p.indentify(); // 'PersonName' 'PersonName'

  Person.name; // PersonName
  PersonName; // ReferenceError: PersonName is not defined

类构造函数

实例、原型和类成员

继承

  • ES6新增特性中最出色的一个就是原生支持了类继承机制。虽然类继承使用的是新语法,但背后依旧是使用的是原型链。
  1. 继承基础
  • ES6类支持单继承。使用extends关键字,就可以继承任何拥有[[Constructor]]和原型的对象。很大程度上,这意味着不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容)
  // 继承类
  class Vehicle {}

  class Bus extends Vehicle {};

  let b = new Bus();
  b instanceof Bus; // true
  b instanceof Vehicle; // true

  function Person () {}

  // 继承普通构造函数
  class E extends Person {}

  let e = new E();
  e instanceof E; // true
  e instanceof Person; // true
  • 派生类都会通过原型链访问到类和原型上定义的方法。this的值会反映调用相应方法的实例或类:
  class Vehicle {
    identifyPrototype(id) {
      console.log(id, this);
    }

    static identifyClass(id) {
      console.log(id, this);
    }
  }

  class Bus extends vehicle {}

  let v = new Vehicle();
  let b = new Bus();

  b.idenetifyPrototype('bus'); // bus, Bus {}
  v.identifyPrototype('vehicle');   // vehicle, Vehicle {}
  Bus.identifyClass('bus');         // bus, class Bus {} 
  Vehicle.identifyClass('vehicle'); // vehicle, class Vehicle {}
  • 【注意】extends关键字也可以在类表达式中使用,因此let Bar = class extends Foo{}是有效的语法。
  1. 构造函数、HomeObject和super()
  • 派生类的方法可以通过super关键字引用它们的原型。
  • 这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。
  • 在类构造函数中使用super可以调用父类构造函数。
  class V {
    constructor() {
      this.hasEngine = true;
    }
  }

  class Bus extends V {
    constructor() {
      // 不要在调用super()之前引用this,否则会抛出ReferenceError
      super(); // 相等于super.constructor()

      console.log(this instanceof V); // true
      console.log(this); // Bus (hasEngine: true)
    }
  }

  new Bus()
  • 在静态方法中可以通过super调用继承的类上定义的静态方法:
  class V {
    static identify() {
      console.log('vehicle');
    }
  }

  class Bus extends V {
    static identify() {
      super.identify();
    }
  }

  Bus.identify(); // vehicle
  • ES6给类构造函数和静态方法添加了内部特性[[HomeObject]],这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的。而且只能在JavaScript引擎内部访问。super始终会定义为[[HomeObject]]的原型

  • 在使用super时要注意以下几个问题:

    • 1.super只能在派生类构造函数和静态方法中使用。
    • 2.不能单独引用super关键字,要么用它调用构造函数,要么用它引用静态方法。
    • 3.调用super()会调用父类构造函数,并将返回的实例赋值给this。
    • 4.super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入。
    • 5.如果没有定义类构造函数,在实例化派生类时会调用super(),而且会传入所有传给派生类的参数。
    • 6.在类构造函数中,不能在调用super之前引用this。
    • 7.如果在派生类中显式定义了构造函数,则要么必须在其中调用super(),要么必须在其中返回一个对象。
      // 1.
      class V {
        constructor() {
          super();
          // SyntaxErrror: 'super' keyword unexpected
        }
      }
    
      // 2.
      class V {}
      class B extends V {
        constructor() {
          console.log(super);
          // SyntaxError: 'super' keyword unepexted here
        }
      }
    
      // 3.
      class V {}
      class B extends V {
        constructor() {
          super();
    
          console.log(this instanceof V);
        }
      }
    
      new Bus(); // true
    
      // 4.
      class V {
        constructor(licensePlate) {
          this.licensePlate = licensePlate;
        }
      }
    
      class B extends V {
        constructor(licensePlate) {
          super(licensePlate);
        }
      }
    
      console.log(new B('1337H4X')); // B {licensePlate: '1337H4X'}
    
      // 5.
      class Vehicle {
        constructor(li) {
          this.li = li;
        }
      }
      class B extends V {}
    
      new B('1337H4X'); // B {li: '1337H4X'}
    
      // 6.
      class V {}
      class B extends V {
        constructor() {
          console.log(this);
        }
      }
      new B(); // Error
    
      // 7.
      class V {}
    
      class Car extends V {}
    
      class Bus extends V {
        constructor() {
          super();
        }
      }
    
      class Van extends V {
        constructor() {
          return {};
        }
      }
    
      new Car(); // Car {}
      new Bus(); // Bus {}
      new Van(); // {}
    
  1. 抽象基类
  • 有可能需要定义一个类,它可供其他类继承,但本身不会被实例化。虽然ES没有专门支持这种类的语法,但通过new.target也能容易实现。
  • new.target保存通过new关键字调用的类或函数。
  • 通过在实例化时,监测new.target是不是抽象基类,可以阻止对抽象基类的实例化:
  // 抽象基类
  class V {
    constructor() {
      console.log(new.target);
      if (new.target === V) {
        throw new Error('V cannot be directly instantiated');
      }
    }
  }

  // 派生类
  class Bus extends V {}

  new Bus(); // class Bus() {}
  new V(); // class V {}
  // Error: V cannot be directly instantiated
  • 通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在调用类构造函数之前就已经存在了,所以可以通过this关键字来检查相应的方法:
  // 抽象基类
  class V {
    constructor() {
      if (new.target === V) {
        throw Error('V cannot be directly instantiated');
      }

      if (!this.foo) {
        throw Error('Inheriting class must define foo()');
      }
      console.log('success!');
    }
  }

  // 派生类
  class Bus extends V {
    foo() {}
  }

  // 派生类
  class Van extends V {}

  new Bus(); // success!
  new Van(); // Error: Inheriting class must define foo()
  1. 继承内置类型
  • ES6类为继承内置引用类型提供了顺畅的机制,开发者可以方便地扩展内置类型。
  class SuperArray extends Array {
    shuffle() {
      // 洗牌算法
      for(let i = this.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [this[i], this[j]] = [this[j], this[i]];
      }
    }
  }

  let a = new SuperArray(1, 2, 3, 4, 5);

  a instanceof Array; // true
  a instanceof SuperArray; // true
  a; // [1, ,2, 3, 4, 5]
  a.shuffle(); // [3, 1, 4, 5, 2]
  • 有些内置类型的方法会返回新实例。默认情况下,返回实例的类型与原始实例的类型是一致的:

  • 如果想覆盖这个默认行为,则可以覆盖Symbol.species访问器,这个访问器决定在创建返回的实例时使用的类:

  class SuperArray extends Array {
    static get [Symbol.species] () {
      return Array;
    }
  }

  let a1 = new SuperArray(1, 2, 3, 4, 5);
  let a2 = a1.filter(x => !!(x % 2));
  // !!操作符等于否定之否定,即返回结果本身

  a1; // [1,2,3,4,5]
  a2; // [1,3,5]
  a1 instanceof SuperArray; // true
  a2 instanceof SuperArray; // false
  // 由于改变了get()方法,使得get()返回Array类型,所以a2不再是SuperArray的实例
  1. 类混入
  • 把不同的行为集中到一个类是一种创建的JS模式。
  • ES6没有显式支持多类继承,但通过现有特性可以轻松地模拟这种行为。

-【注意】Object.assign()方法是为混入对象行为而设计的。只有需要混入类的行为时才有必要自己实现混入表达式。如果只有需要混入多个对象的属性,那么使用Object.assign()就可以了。

  • 在下面的代码片段中,extends关键字后面是一个JavaScript表达式。任何可以解析为一个类或一个构造函数的表达式都是有效的。这个表达式会在求值类定义时被求值。
  class V {}
  function getParentClass() {
    console.log('evaluated expression');
    return V;
  }

  class Bus extends getParentClass() {}
  // 可求值的表达式

  let b = new Bus();
  b instanceof Bus; // true
  b instanceif V; // true
  • 混入模式可以通过在一个表达式中连缀多个混入元素来实现,这个表达式最终会解析为一个可以被继承的类。如果Person类需要组合A、B、C,则需要某种机制实现B继承A,C继承B,而Person在继承C,从而把A、B、C组合到这个超类中。实现这种模式又不同的策略。
  • 一个策略是定义一组“可嵌套”的函数,每个函数分别接收一个超类作为参数,而将混入类定义为这个参数的子类,并返回这个类/这些组合函数可以连缀调用,最终组合成超类表达式:
  class V {}

  let FooMixin = (Superclass) => class extends Superclass {
    foo() {
      console.log('foo');
    }
  };

  let BarMixin = (Superclass) => class extends Superclass {
    bar() {
      console.log('bar');
    }
  };

  let BazMixin = (Superclass) => class extends Superclass {
    baz() {
      console.log('baz');
    }
  };

  class Bus extends FooMixin(BarMixin(BazMixin(V))) {}

  let b = new Bus();
  b.foo(); // foo
  b.bar(); // bar
  b.baz(); // baz
  • 通过写一个辅助函数,可以把嵌套调用展开:
  function mix(BaseClass, ...Mixins) {
    return Mixins.reduce((acc, current) => current(acc), BaseClass);
  }

  class Bus2 extends mix(V, FooMixin, BarMixin, BazMixin) {}

  let b2 = new Bus2();
  b.bar(); // bar
  b.baz(); // baz
  • 【注意】很多JS框架特别是React已经抛弃了混入模式,转向了组合模式(把方法提取到独立的类和辅助对象中,然后把它们组合起来,但不使用继承)。
  • 这反映了那个众所周知的软件设计原则:“组合胜过继承(composition over inheritance)。”这个设计原则被很多人遵循,在代码设计中能提供极大的灵活性。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值