zoukankan      html  css  js  c++  java
  • 【读书笔记】【深入理解ES6】#9-JavaScript中的类

    大多数面向对象的编程语言都支持类和类继承的特性,而JavaScript却不支持这些特性,只能通过其他方法定义并关联多个相似的对象。这个状态一直从ECMAScript 1持续到ECMAScript 5。
    尽管一部分JavaScript开发强烈坚持JavaScript中不需要类,但由于类似的库层出不穷,最终还是在ECMAScript 6中引入了类的特性。
    ECMAScript 6中的类与其他语言中的还是不太一样,其语法的设计实际上借鉴了JavaScript的动态性

    ECMAScript 5 中的近类结构

    首先创建一个构造函数,然后定义另一个方法并赋值给构造函数的原型。

    function PersonType() {
        this.name = name;
    }
    
    PersonType.prototype.sayName = function() {
        console.log(this.name);
    };
    
    var person = new PersonType("JiaJia");
    person.sayName(); // "JiaJia"
    
    console.log(person instanceof PersonType); // true
    console.log(person instanceof Object); // true
    

    类的声明

    基本的类声明语法

    通过 class 关键字声明类

    class PersonClass {
        // 等价于 PersonType 构造函数
        constructor(name) {
            this.name = name;
        }
    
        // 等价于 PersonType.prototype.sayName
        sayName() {
            console.log(this.name);
        }
    }
    
    let person = new PersonClass("JiaJia");
    person.sayName(); // "JiaJia"
    
    console.log(person instanceof PersonClass); // true
    console.log(person instanceof Object); // true
    
    console.log(typeof PersonClass); // "function"
    console.log(typeof PersonClass.prototype.sayName); // "function"
    

    创建方法同之前创建 PersonType 构造函数类似,在这里直接在类中通过 constructor 方法名来定义构造函数。除 constructor 外没有其它的保留方法名。

    私有属性是实例属性,不会出现在原型中。建议在构造函数中定义所有的私有属性,从而只通过一处就可以控制类中的所有私有属性。

    通过上面例子最后两行的输出可以发现,其实 class 关键字只是个语法糖,最终生成的类 PersonClass 仍然是一个函数,而方法也是定义在该函数的原型上的。

    Note

    与函数不同的是,类属性不可被赋予新值,在之前的示例中,PersonClass.prototype 就是这样一个只可读的类属性。

    为何使用类语法

    首先看一下类声明与函数声明的差异:

    1. 函数声明可以被提升,而类声明与 let 类似,不能被提升;真正执行声明语句之前,它们会一直存在于临时死区中。
    2. 类声明中的所有代码将自动运行与严格模式下,而且无法强行让代码脱离严格模式执行。
    3. 在自定义类型中,需要通过 Object.defineProperty() 方法手工指定某个方法为不可枚举;而在类中,所有方法都是不可枚举的。
    4. 每个类都有一个名为 [[Construct]] 的内部方法,通过关键字 new 调用那些不含 [[Construct]] 的方法会导致程序抛出错误。
    5. 使用除 new 关键字外的方式调用类的构造函数会导致程序抛出错误。
    6. 在类中修改类名会导致程序报错。

    使用出了类之外的语法为之前示例中的 PersonClass 声明编写等价代码。

    // 等价于PersonClass
    let PersonType2 = (function () {
        "use strict";
    
        const PersonType2 = function (name) {
            // 确保通过关键字new调用该函数
            if (typeof new.target === "undefined") {
                throw new Error("必须通过new关键字调用构造函数");
            }
    
            this.name = name;
        }
    
        Object.defineProperty(PersonType2.prototype, "sayName", {
            value: function () {
                // 确保不会通过new调用该方法
                if (typeof new.target !== "undefined") {
                    throw new Error("不可使用关键字new调用该方法");
                }
    
                console.log(this.name);
            },
            enumerable: false,
            writable: true,
            configurable: true
        });
    
        return PersonType2;
    }());
    

    注意,这段代码中有两处 PersonType2 声明:

    1. 外部作用域中的 let 声明。
    2. 立即执行函数表达式(IIFE)中的const声明。

    这也从侧门说明了为什么可以在外部修改类名而内部却不可修改。

    从这个示例可以看出,尽管在不使用class关键字的前提下实现类的所有功能,但代码变的极为复杂。

    类表达式

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

    基本的类表达式语法

    let PersonClass = class {
        // 等价于 PersonType 构造函数
        constructor(name) {
            this.name = name;
        }
    
        // 等价于 PersonType.prototype.sayName
        sayName() {
            console.log(this.name);
        }
    }
    
    let person = new PersonClass("JiaJia");
    person.sayName(); // "JiaJia"
    
    console.log(person instanceof PersonClass); // true
    console.log(person instanceof Object); // true
    
    console.log(typeof PersonClass); // "function"
    console.log(typeof PersonClass.prototype.sayName); // "function"
    
    console.log(PersonClass.name); // "PersonClass"
    

    命名类表达式

    let PersonClass = class PersonClass2 {
        // 等价于 PersonType 构造函数
        constructor(name) {
            this.name = name;
        }
    
        // 等价于 PersonType.prototype.sayName
        sayName() {
            console.log(this.name);
        }
    }
    
    console.log(PersonClass.name); // "PersonClass2"
    console.log(typeof PersonClass); // "function"
    console.log(typeof PersonClass2); // "undefined"
    

    类的名称为 PersonClass2,但是在声明外部并不存在一个名为 PersonClass2 的绑定,标识符 PersonClass2 只存在与类定义中。

    将上述示例改成不使用 class 关键字的等价声明:

    // 等价于命名类表达式 PersonClass
    let PersonClass = (function () {
        "use strict";
    
        const PersonClass2 = function (name) {
            // 确保通过关键字new调用该函数
            if (typeof new.target === "undefined") {
                throw new Error("必须通过new关键字调用构造函数");
            }
    
            this.name = name;
        }
    
        Object.defineProperty(PersonClass2.prototype, "sayName", {
            value: function () {
                // 确保不会通过new调用该方法
                if (typeof new.target !== "undefined") {
                    throw new Error("不可使用关键字new调用该方法");
                }
    
                console.log(this.name);
            },
            enumerable: false,
            writable: true,
            configurable: true
        });
    
        return PersonClass2;
    }());
    

    在JS引擎中,类表达式的实现与类声明稍有不同。

    • 类声明
      通过let定义的外部绑定与通过const定义的内部绑定具有相同名称
    • 命名类表达式
      通过const定义名称

    作为一等公民的类

    在程序中,一等公民是指一个可以传入函数,可以从函数返回,并且可以赋值给变量的值。
    JS中函数是一等公民,ES6中也将类设计为一等公民,允许通过多种方式使用类的特性。

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

    类表达式还有另外一种使用方式,通过立即调用类构造函数可以创建单例

    let person = new class {
        constructor(name) {
            this.name = name;
        }
    
        sayName() {
            console.log(this.name);
        }
    }("JiaJia");
    
    person.sayName(); // "JiaJia"
    

    访问器属性

    类支持在原型上定义访问器属性。

    class CustomHTMLElement {
        constructor(element) {
            this.element = element;
        }
    
        // getter
        get html() {
            return this.element.innerHTML;
        }
    
        // setter
        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
    

    下面是上面示例非类形式的等价代码。

    let CustomHTMLElement = (function() {
        "use strict";
    
        const CustomHTMLElement = function(element) {
            // 确保通过关键字new调用该函数
            if (typeof new.target === "undefined") {
                throw new Error("必须通过关键字new调用构造函数");
            }
            this.element = element;
        }
    
        Object.defineProperty(CustomHTMLElement.prototype, "html", {
            enumerable: false,
            configurable: false,
            get: function() {
                return this.element.innerHTML;
            },
            set: function(value) {
                this.element.innerHTML = value;
            }
        });
    
        return CustomHTMLElement;
    }());
    

    比起非类等效实现,类语法可以节省很多代码。

    可计算成员名称

    用方括号包裹一个表达式即可使用可计算名称。

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

    通过相同的方式可以在访问器属性中应用可计算名称:

    let propertyName = "html";
    
    class CustomHTMLElement {
        constructor(element) {
            this.element = element;
        }
    
        get [propertyName]() {
            return this.element.innerHTML;
        }
    
        set [propertyName](value) {
            this.element.innerHTML = value;
        }
    }
    

    生成器方法

    可以在方法前附加一个星号(*)来定义生成器。

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

    可以通过 Symbol.iterator 定义类的默认迭代器。

    class Collection {
        constructor() {
            this.items = [];
        }
    
        *[Symbol.iterator]() {
            yield *this.items;
        }
    }
    
    var collection = new Collection();
    collection.items.push(1);
    collection.items.push(2);
    collection.items.push(3);
    
    for (let x of collection) {
        console.log(x);
    }
    
    // 输出:
    // 1
    // 2
    // 3
    

    静态成员

    在ES5及早期版本中,直接将方法添加到构造函数中来模拟静态成员是一种常见的模式。

    function PersonType(name) {
        this.name = name;
    }
    
    // 静态方法
    Person.create = function(name) {
        return new PersonType(name);
    };
    
    // 实例方法
    PersonType.prototype.sayName = function() {
        console.log(this.name);
    };
    
    var person = PersonType.create("JiaJia");
    

    ES6中简化了创建静态成员的过程,在方法或访问器属性前使用正式的静态注释即可。

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

    类中的所有方法和访问器属性都可以用 static 关键字来定义,唯一的限制是不能将 static 用于定义构造函数方法。

    Note

    不可在实例中访问静态成员,必须要直接在类中访问静态成员。

    继承与派生类

    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,
            configruable: 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() 方法。

    类的出现可以让我们更轻松的实现继承功能。

    class Rectangle {
        constructor(length, width) {
            this.length = length;
            this.width = width;
        }
    
        getArea() {
            return this.length * this.width;
        }
    }
    
    class Square extends Rectangle {
        // 等价于Rectangle.call(this, length, length)
        constructor(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 关键字继承 Rectangle 类,在 Square 的构造函数中通过 super() 调用 Rectangle 构造函数并传入参数。

    继承自其它类的类被称作派生类,如果在派生类中指定了构造函数则必须要调用 super(),如果不这样做程序就会报错。
    如果选择不使用构造函数,则当创建新的实例时会自动调用 super() 并传入所有参数。

    关于 super() 的小贴士

    • 只可在派生类的构造函数中使用 super(),如果尝试在非派生类(不是用 extends 声明的类)或函数中使用则会导致程序抛出错误。
    • 在构造函数中访问 this 之前一定要调用 super(),它负责初始化this,如果在调用 super() 之前尝试访问 this 会导致程序出错。
    • 如果不想调用 super(),则唯一的方法是让类的构造函数返回一个对象。

    类方法遮蔽

    派生类中的方法总会覆盖基类中的同名方法。
    如果想调用基类中的同名方法,需使用 super.method() 的方式调用。

    静态成员继承

    如果基类有静态成员,那么这些静态成员在派生类中也可用。

    派生自表达式的类

    ECMAScript 6最强大的一面或许是从表达式导出类的功能了。只要表达式可以被解析为一个函数并且具有 [[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
    

    extends 强大的功能使得类可以继承自任意类型的表达式。

    function Rectangle(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) {
            super(length, length);
        }
    }
    
    var x = new Square(3);
    console.log(x.getArea()); // 9
    console.log(x instanceof Rectangle); // true
    

    此示例实现的功能同之前的示例等价。
    extends 后面跟的是方法调用 getBase(),Square 类继承自该方法的返回值。

    通过这种方式,可以动态的决定类的继承。

    let SerializableMixin = {
        serialize() {
            return JSON.stringify(this);
        }
    };
    
    let AreaMixin = {
        getArea() {
            return this.length * this.width;
        }
    };
    
    function mixin(...mixins) {
        var base = function() {};
        Object.assign(base.prototype, ...mixins);
        return base;
    }
    
    class Square extends mixin(AreaMixin, SerializableMixin) {
        constructor(length) {
            super();
            this.length = length;
            this.width = length;
        }
    }
    
    var x = new Square(3);
    console.log(x.getArea()); // 9
    console.log(x.serialize()); // {"length":3,"width":3}
    

    内建对象的继承

    通过继承的方式创建属于自己的特殊数组。在ES5及早期版本中,这几乎是不可能的。

    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.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 的值,然后调用基类型的构造函数。这也意味着,this的值开始指向的是 MyArray 的实例,但是随后会被来自 Array 的其它属性所修饰。

    ES6中的类继承则与之相反,先由基类创建 this 的值,然后派生类的构造函数再修改这个值。所以一开始可以通过 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
    

    Symbol.species 属性

    内建对象的一个实用之处是,原本在内建对象中返回实例自身的方法将自动返回派生类的实例。

    class MyArray extends Array {
    
    }
    
    let items = new MyArray(1, 2, 3, 4),
        subitems = items.slice(1, 3);
    
    console.log(items instanceof MyArray); // true
    console.log(subitems instanceof MyArray); // true
    

    在JS引擎背后是通过 Symbol.species 属性实现该功能的。
    Symbol.species 被用于定义返回函数的静态访问器属性。被返回的函数是一个构造函数,每当要在实例的方法中(不是在构造函数中)创建类的实例时必须使用这个构造函数。

    以下这些内建类型均已定义 Symbol.species 属性(该属性返回值为 this,这也意味着该属性总会返回构造函数):

    • Array
    • ArrayBuffer
    • Map
    • Promise
    • RegExp
    • Set
    • Typed arrays

    几个内建类型像这样使用 Symbol.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 {
        static get [Symbol.species]() {
            return MyClass;
        }
    }
    
    let instance1 = new MyDerivedClass1("foo"),
        clone1 = instance1.clone(),
        instance2 = new MyDerivedClass2("foo"),
        clone2 = instance2.clone();
    
    console.log(clone1 instanceof MyClass); // true
    console.log(clone1 instanceof MyDerivedClass1); // true
    console.log(clone2 instanceof MyClass); // true
    console.log(clone2 instanceof MyDerivedClass2); // false
    

    上例中 MyDerivedClass2 重写了 Symbol.species 属性,使其返回的不再是派生类的构造函数,而是基类的构造函数。所以最终 clone() 的结果不再是派生类型。

    在类的构造函数中使用 new.target

    在类的构造函数中,也可以通过 new.target 来确定类是如何被调用的。
    在简单情况下,new.target 等于类的构造函数。

    class Rectangle {
        constructor(length, width) {
            console.log(new.target === Rectangle);
            this.length = length;
            this.width = width;
        }
    }
    
    // new.tareget的值是Rectangle
    var obj = new Rectangle(3, 4); // true
    

    再看看另外一种情况:

    class Rectangle {
        constructor(length, width) {
            console.log(new.target === Rectangle);
            this.length = length;
            this.width = width;
        }
    }
    
    class Square extends Rectangle {
        constructor(length) {
            super(length, length);
        }
    }
    
    // new.tareget的值是Square
    var obj = new Square(3); // false
    

    上例中,通过派生类调用时, new.target 指向的是派生类的构造函数。
    利用这个特性,可以创建一个抽象基类(不能被直接实例化的类)

    // 抽象基类
    class Shape {
        constructor() {
            if (new.target === Shape) {
                throw new Error("这个类不能被直接实例化。");
            }
        }
    }
    
    class Rectangle extends Shape {
        constructor(length, width) {
            super();
            this.length = length;
            this.width = width;
        }
    }
    
    var x = new Shape(); // 抛出错误
    
    var y = new Rectangle(3, 4); // 没有错误
    console.log(y instanceof Shape); // true
    
  • 相关阅读:
    关于Cocos2d-x开发一个游戏的过程自述
    关于JDK环境变量的配置问题
    lnmp、lamp、lnmpa一键安装包(Updated: 2015-10-25)
    成大事者必备的九种能力、九种手段、九种心态
    燕十八mysql笔记
    cmake 安装 mysql5.5 版本
    IOS 中微信 网页授权报 key[也就是code]失效 解决办法
    js 复制 功能
    php 二维数组排序
    php获取前一天,前一个月,前一年的时间
  • 原文地址:https://www.cnblogs.com/Ryukaka/p/7885802.html
Copyright © 2011-2022 走看看