• ES6功能扩展类


    ES5中的“类”

    function Person(name) {
      this.name = name
    }
    
    Person.prototype.sayName = function() {
      console.log(this.name)
    }
    
    var p = new Person('wmui')
    p.sayName() // wmui
    console.log(p instanceof Person) // true
    console.log(p instanceof Object) // true
    

    ES5中没有类的概念,要想实现和类相似的功能,通常是创建一个构造函数,然后把类方法赋值给构造函数的原型。许多模拟类的JS库都是基于这个模式进行开发。

    类的声明

    在ES6中可以用class关键字声明一个类,关键字后面紧跟着类的名字,其他部分语法类似对象字面量,但是各元素之间不需要逗号分隔

    class Person {
      constructor(name) {
        this.name = name
      }
      sayName() {
        console.log(this.name)
      }
    }
    
    let p = new Person('wmui')
    
    p.sayName()
    console.log(p instanceof Person) // true
    console.log(p instanceof Object) // true
    

    通过这种方式定义类和前面的直接使用构造函数定义类的过程相似,只不过这里是使用特殊的constructor方法来定义构造函数

    两者差异

    类声明与自定义类型虽然很相似,但是还是有差异的

    1. 类声明不会出现函数声明提升,不能被提升到执行语句前
    2. 类声明中的所有语句自动运行在严格模式下,且无法强行脱离严格模式执行
    3. 类中所有方法都是不可枚举到的,而自定义类型中,需要通过Object.defineProperty()方法手工指定某个方法为不可枚举
    4. 每个类都有一个名为[[Construct]]的内部方法,通过关键字new调用那些不含[[Construct]]的方法会导致程序抛出错误
    5. 使用new关键字以外的其他方式调用类的构造函数,会导致程序报错
    6. 在类中修改类名会导致程序报错

    了解了两种方式的差异,现在可以不用类语法编写和类等价的代码

    let Person = (function(){
      'use strict';
      const Person = function(name) {
        // 确认函数是通过new关键字被调用
        if(typeof new.target === 'undefined') {
          throw new Error("Constructor must be called with new.");
        }
        this.name = name;
      }
      Object.defineProperty(Person.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 Person;
    }())
    
    let p = new Person('wmui');
    p.sayName() // wmui
    

    尽管可以在不使用new语法的前提下实现类的所有功能,但如此一来代码变得极为复杂

    常量类名

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

    class Person {
      constructor() {
        Person = 'People' // 执行时报错
      }
    }
    
    Person = 'People' // 不报错
    

    内部的Foo就像是通过const声明的,修改它的值会导致程序抛出错误;而外部的Foo就像是通过let声明的,可以随时修改这个绑定值

    类表达式

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

    声明形式的函数和类都由相应的关键字(函数是function,类是class)进行定义,后面紧跟着一个标识符;表达式形式的函数和类与之类似,只是不需要在关键字后添加标识符。

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

    表达式语法

    let Person = class {
      constructor(name) {
        this.name = name
      }
      sayName() {
        console.log(this.name)
      }
    }
    
    let p = new Person('wmui')
    
    p.sayName()
    console.log(p instanceof Person) // true
    console.log(p instanceof Object) // true
    

    这个示例等价于前面的声明形式Person类,仅在代码编写方式略有差异。

    命名表达式

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

    let Person = class Person2{
      constructor(name) {
        this.name = name
      }
      sayName() {
        console.log(this.name)
      }
    }
    
    console.log(typeof Person) // function
    console.log(typeof Person2) // undefined
    

    标识符Person2只存在于类的定义中,可以在类内部的方法中使用。而在类的外部,由于不存在Person2标识符,所以typeof Person2的结果是 undefined

    类表达式还可以通过立即调用构造函数创建单例。用new调用类表达式,然后通过小括号调用这个表达式

    let p = new class {
      constructor(name) {
        this.name = name
      }
      sayName() {
        console.log(this.name)
      }
    }('wmui')
    
    p.sayName() // wmui
    
    

    一等公民

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

    把类作为参数传入函数

    function createObj(classDef) {
      return new classDef()
    }
    
    class Person {
      sayName() {
        console.log('wmui')
      }
    }
    
    let obj = createObj(Person)
    obj.sayName() // wmui
    

    访问器属性

    类也支持访问器属性,创建getter时,需要在关键字get后紧跟一个空格和相应的标识符;创建setter时,只需把关键字get替换为set

    class Person {
      constructor(name) {
        this.name = name
      }
      get myName() {
        return this.name
      }
      set myName(value) {
        this.name = value
      }
    }
    
    let descriptor = Object.getOwnPropertyDescriptor(Person.prototype, 'myName')
    console.log('get' in descriptor) // true
    console.log('set' in descriptor) // true
    

    可计算成员名称

    类方法和访问器属性都支持使用可计算名称。就像在对象字面量中一样,用方括号包裹一个表达式即可使用可计算名称

    let methodName = 'sayName'
    class Person{
      constructor(name) {
        this.name = name
      }
      [methodName]() {
        console.log(this.name)
      }
    }
    
    let p = new Person('wmui')
    p.sayName() // wmui
    

    通过相同的方式可以在访问器属性中应用可计算名称,并且可以像往常一样通过.myName访问该属性

    let propertyName = 'myName'
    class Person {
      constructor(name) {
        this.name = name
      }
      get [propertyName]() {
        return this.name
      }
      set [propertyName](value) {
        this.name = value
      }
    }
    

    生成器方法

    在对象字面量中,可以通过在方法名前附加一个星号(*)的方式来定义生成器,在类中也可以

    class MyClass {
      *createIterator() {
        yield 1;
        yield 2;
        yield 3;
      }
    }
    let instance = new MyClass();
    let iterator = instance.createIterator();
    

    静态成员

    在ES5中,通常直接将方法添加到构造函数中来模拟静态成员

    function Person(name) {
      this.name = name
    }
    
    // 静态方法
    Person.testMethod = function() {
      return 'hello'
    }
    
    // 静态属性
    Person.testProperty = 'hi'
    
    // 实例方法
    Person.prototype.sayName = function() {
      console.log(this.name)
    }
    
    

    在ES6中,可以通过添加静态注释来表示静态成员

    class Person {
      constructor(name) {
        this.name = name
      }
      
      sayName() {
        console.log(this.name)
      }
    
      // 静态方法
      static testMethod() {
        return 'hello'
      }
      
      // 模拟静态属性
      static get testProperty() {
        return 'hi'
      }
    }
    
    console.log(Person.testMethod()) // hello
    console.log(Person.testProperty) // hi
    

    注意: ES6规定,Class内部只有静态方法,没有静态属性。所以这里是通过getter模拟的静态属性

    注意: 静态成员是对象自身的属性和方法,在实例身上无法使用

    继承与派生类

    在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: 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
    

    Square继承自Rectangle,核心是创建一个基于Rectangle.prototype的新对象重写Square.prototype,并且在Square中调用Rectangle.call()方法改变this指针。

    使用ES6实现相同的功能,代码会精简很多

    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)
      }
    }
    
    let square = new Square(3)
    console.log(square.getArea()) // 9
    console.log(square instanceof Square) // true
    console.log(square instanceof Rectangle) // true
    

    通过extends关键字指定要继承的类,子类的原型会自动调整,然后super()方法可以调用基类的构造函数

    继承自其它类的类称为派生类,如果在派生类中指定了构造函数则必须调用super()方法,否则会报错。如果不使用构造函数,创建派生类的实例时会自动调用super()方法并传入所有参数。

    class Rectangle {
      // 没有构造函数
    }
    // 等价于 
    class Square extends Rectangle {
      constructor(...args) {
        super(...args)
      }
    }
    

    注意事项

    1. 只能在派生类中使用super(),在非派生类(不是extends声明的类)或函数中使用会报错

    2. 在构造函数中访问this前一定要调用super(),它负者初始化this。如果在super()前访问this会报错

    3. 如果不想在派生类的构造函数中调用super(),可以让构造函数返回一个对象

    类方法遮蔽

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

    class Square extends Rectangle {
      constructor(length) {
        super(length, length)
      }
    
      getArea() {
        return this.length * this.length;
      }
    }
    

    派生类Square中的getArea()会覆盖Rectangle中的同名方法。如果Square的实例想调用Rectangle的getArea()方法,可以通过调用super.getArea()方法间接实现

    class Square extends Rectangle {
      constructor(length) {
        super(length, length)
      }
    
      getArea() {
        return super.getArea()
      }
    }
    

    静态成员继承

    如果基类有静态成员,派生类也可以继承这些静态成员

    class Rectangle {
      constructor(length, width) {
        this.length = length,
        this.width = width
      }
      getArea() {
        return this.length * this.width
      }
      static create(length, width) {
        // 该方法返回一个Rectangle实例
        return new Rectangle(length, width)
      }
    }
    
    class Square extends Rectangle {
      constructor(length) {
        super(length, length)
      }
    }
    
    let rect = Square.create(3,4)
    console.log(rect.getArea()) // 12
    console.log(rect instanceof Rectangle) // true
    console.log(rect instanceof Square) // false
    

    Square继承了Rectangle的create()静态方法

    派生自表达式的类

    派生类不一定非要是继承自class的基类,它也可以继承自某个表达式。只要表达式可以被解析为函数并且具有[[Construct]]属性和原型

    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)
      }
    }
    
    let square = new Square(3)
    console.log(square.getArea()) // 9
    console.log(square instanceof Rectangle) // true
    

    Rectangle是一个ES5风格的构造函数,Square是一个类,由于Rectangle具有[[Construct]]属性和原型,因此Square类可以直接继承它

    内建对象的继承

    在ES5中要想实现内键对象的继承几乎不可能,比如想通过继承的方式创建基于Array的特殊数组。

    function MyArray(length) {
      Array.apply(this, arguments)
    }
    MyArray.prototype = Object.create(Array.prototype, {
      constructor: {
        value: MyArray,
        enumerable: false,
        writable: true,
        configurable: true
      }
    })
    
    var colors = new MyArray()
    colors[0] = 'red'
    console.log(colors.length) // 0
    

    colors.length的结果不是期望的1而是0,这是因为通过传统JS继承形式实现的数组继承没有从Array.apply()或原型赋值中继承数组相关功能

    在ES6中可以轻松实现内建对象的继承

    class MyArray extends Array {}
    
    let colors = new MyArray()
    colors[0] = 'red'
    console.log(colors.length) // 1
    

    new.target

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

    class Rectangle {
      constructor(length, width) {
        console.log(new.target === Rectangle);
        this.length = length;
        this.width = width;
      }
    }
    
    let rect = new Rectangle(3, 4); 
    // true
    
    class Rectangle {
      constructor(length, width) {
        console.log(new.target === Square);
        this.length = length;
        this.width = width;
      }
    }
    class Square extends Rectangle {
      constructor(length) {
        super(length, length)
      }
    }
    
    let square = new Square(3); // true
    

    示例中,创建Rectangle实例时new.target等价于Rectangle,创建Square实例时new.target等价于Square

    优秀文章首发于聚享小站,欢迎关注!
  • 相关阅读:
    xampp服务器搭建和使用
    使用proxyee-down解决百度云下载限速问题
    iOS开发之多线程技术—GCD篇
    iOS 将视频流(h264)和音频流封装成PS流
    iOS 播放音频文件
    iOS 简单socket连接
    ios获取本机网络IP地址方法
    iOS10适配相关
    iOS设备的尺寸和分辨率
    理解NodeJS
  • 原文地址:https://www.cnblogs.com/yesyes/p/15352376.html
Copyright © 2020-2023  润新知