类 class

作者: 小小的白菜 | 来源:发表于2018-10-22 19:27 被阅读0次

    摘自《深入理解ES6》

    尽管一些JS开发者强烈认为这门语言不需要类,但为处理类而创建的代码库如此之多,导致ES6最终引入了类。

    ES5 中的仿类结构

    JSES5及更早版本中都不存在类。与类最接近的是:创建一个构造器,然后将方法指派到该构造器的原型上。这种方式通常被称为创建一个自定义类型。

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

    此代码中的PersonType是一个构造器函数,并创建了单个属性namesayName()方法被指派到原型上,因此在PersonType对象的所有实例上都共享了此方法。接下来,使用new运算符创建了PersonType 的一个新实例 person,此对象会被认为是一个通过原型继承了PersonTypeObject的实例。

    类的声明

    类在 ES6 中最简单的形式就是类声明,它看起来很像其他语言中的类。

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

    这个PersonClass类声明的行为非常类似上个例子中的 PersonType。类声明允许你在其中使用特殊的constructor方法名称直接定义一个构造器,而不需要先定义一个函数再把它当作构造器使用。

    由于类的方法使用了简写语法,于是就不再需要使用 function 关键字。constructor 之外的方法名称则没有特别的含义,因此可以随你高兴自由添加方法。

    PersonClass声明实际上创建了一个拥有 constructor 方法及其行为的函数,这也是typeof PersonClass会得到 "function"结果的原因。

    类与自定义类型之间的区别

    尽管类与自定义类型之间有相似性,但仍然要记住一些重要的区别:

    • 类声明不会被提升,这与函数定义不同。类声明的行为与 let相似,因此在程序的执行到达声明处之前,类会存在于暂时性死区内。
    • 类声明中的所有代码会自动运行在严格模式下,并且也无法退出严格模式。
    • 类的所有方法都是不可枚举的,这是对于自定义类型的显著变化,后者必须用Object.defineProperty() 才能将方法改变为不可枚举。
    • 类的所有方法内部都没有 [[Construct]],因此使用new来调用它们会抛出错误。
    • 调用类构造器时不使用new,会抛出错误。
    • 试图在类的方法内部重写类名,会抛出错误。
      // 直接等价于 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;
      }());
    

    首先要注意这里有两个PersonType2 声明:一个在外部作用域的let声明,一个在IIFE内部的const声明。这就是为何类的方法不能对类名进行重写、而类外部的代码则被允许。构造器函数检查了new.target ,以保证被调用时使用了 new,否则就抛出错误。接下来,sayName() 方法被定义为不可枚举,并且此方法也检查了new.target,它则要保证在被调用时没有使用 new 。最后一步是将构造器函数返回出去。

    不变的类名

    只有在类的内部,类名才被视为是使用const 声明的。这意味着你可以在外部重写类名,但不能在类的方法内部这么做。例如:

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

    在此代码中,类构造器内部的 Foo与在类外部的 Foo 是不同的绑定。内部的Foo就像是用 const定义的,不能被重写,当构造器尝试使用任何值重写Foo时,都会抛出错误。但由于外部的Foo就像是用 let声明的,你可以随时重写类名。

    作为一级公民的类

    ES6延续了传统,让类同样成为一级公民。这就使得类可以被多种方式所使用。例如,它能作为参数传入函数:

      function createObject(classDef) {
        return new classDef();
      }
    
      let obj = createObject(class {
        sayHi() {
          console.log("Hi!");
        }
      });
      obj.sayHi(); // "Hi!"
    

    访问器属性

    自有属性需要在类构造器中创建,而类还允许你在原型上定义访问器属性。为了创建一个getter,要使用 get 关键字,并要与后方标识符之间留出空格;创建setter用相同方式,只是要换用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
    

    此代码中的CustomHTMLElement类用于包装一个已存在的 DOM 元素。它的属性html拥有gettersetter,委托了元素自身的innerHTML 方法。该访问器属性被创建在CustomHTMLElement.prototype上,并且像其他类属性那样被创建为不可枚举属性。非类的等价表示如下:

      // 直接等价于上个范例
      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;
      }());
    

    正如之前的例子,此例说明了使用类语法能够少写大量的代码。仅仅为html访问器属性定义的代码量,就几乎相当于等价的类声明的全部代码量了。

    需计算的成员名

    对象字面量与类之间的相似点还不仅前面那些。类方法与类访问器属性也都能使用需计算的名称。语法相同于对象字面量中的需计算名称:无须使用标识符,而是用方括号来包裹一个表达式。例如:

      let methodName = "sayName";
    
      class PersonClass {
        constructor(name) {
          this.name = name;
        }
    
        [methodName]() {
          console.log(this.name);
        }
      }
    
      let me = new PersonClass("Nicholas");
      me.sayName(); // "Nicholas"
    

    此版本的PersonClass使用了一个变量来命名类定义内的方法。字符串"sayName" 被赋值给了 methodName 变量,而 methodName变量则被用于声明方法。 sayName() 方法在此后能被直接访问。

    生成器方法

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

    此代码创建了一个拥有 createIterator() 生成器的MyClass类。该方法返回了一个迭代器,它的值在生成器内部用硬编码提供。当你使用一个对象来表示值的集合、并要求能简单迭代这些值,那么生成器方法就非常有用。数组、 SetMap都拥有多个生成器方法,负责让开发者用多种方式来操作它们的项。

    静态成员

    直接在构造器上添加额外方法来模拟静态成员,这在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("Nicholas");
    

    在其他编程语言中,工厂方法 PersonType.create() 会被认定为一个静态方法,它的数据不依赖 PersonType的任何实例。

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

    PersonClass的定义拥有名为create() 的单个静态方法,此语法与sayName()基本相同,只多了一个 static关键字。你能在类中的任何方法与访问器属性上使用 static 关键字,唯一限制是不能将它用于 constructor方法的定义。

    使用派生类进行继承

    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
    

    Square继承了Rectangle ,为此它必须使用Rectangle.prototype所创建的一个新对象来重写 Square.prototype,并且还要调用Rectangle.call()方法。

    类让继承工作变得更轻易,使用熟悉的 extends 关键字来指定当前类所需要继承的函数,即可。生成的类的原型会被自动调整,而你还能调用 super()方法来访问基类的构造器。此处是与上个例子等价的 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);
        }
      }
    
      var square = new Square(3);
      console.log(square.getArea()); // 9
      console.log(square instanceof Square); // true
      console.log(square instanceof Rectangle); // true
    

    此次 Square 类使用了extends关键字继承了 RectangleSquare构造器使用了super()配合指定参数调用了Rectangle 的构造器。注意与 ES5版本的代码不同,Rectangle 标识符仅在类定义时被使用了(在 extends之后)。

    屏蔽类方法

    派生类中的方法总是会屏蔽基类的同名方法。例如,你可以将 getArea() 方法添加到Square类,以便重定义它的功能:

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

    由于getArea()已经被定义为Square的一部分,Rectangle.prototype.getArea()方法就不能在Square的任何实例上被调用。当然,你总是可以使用 super.getArea()方法来调用基类中的同名方法,就像这样:

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

    用这种方式使用superthis 值会被自动设置为正确的值,因此你就能进行简单的调用。

    继承静态成员

    如果基类包含静态成员,那么这些静态成员在派生类中也是可用的。继承的工作方式类似于其他语言,但对于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
    

    在此代码中,一个新的静态方法 create() 被添加到Rectangle 类中。通过继承,该方法会以 Square.create() 的形式存在,并且其行为方式与 Rectangle.create()一样。

    从表达式中派生类

    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
    

    Rectangle 被定义为ES5 风格的构造器,而 Square则是一个类。由于 Rectangle具有[[Construct]] 以及原型, Square 类就能直接继承它。

    继承内置对象

    几乎从JS数组出现那天开始,开发者就想通过继承机制来创建他们自己的特殊数组类型。在ES5及早期版本中,这是不可能做到的。试图使用传统继承并不能产生功能正确的代码,例如:

    console.log() 在此代码尾部的输出说明了:对数组使用传统形式的 JS继承,产生了预期外的行为。MyArray实例上的 length属性以及数值属性,其行为与内置数组并不一致,因为这些功能并未被涵盖在 Array.apply() 或数组原型中。

      // 内置数组的行为
      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"
    

    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
    

    MyArray直接继承了Array,因此工作方式与正规数组一致。与数值索引属性的互动更新了length 属性,而操纵 length 属性也能更新索引属性。这意味着你既能适当地继承Array来创建你自己的派生数组类,也同样能继承其他的内置对象。伴随着这些附加功能,ES6与派生类型有效解决了从内置类型进行派生这最后的特殊情况,不过这种情况仍然值得继续探索。

    相关文章

      网友评论

          本文标题:类 class

          本文链接:https://www.haomeiwen.com/subject/qiywzftx.html