zoukankan      html  css  js  c++  java
  • ES6里关于类的拓展(一)

      大多数面向对象的编程语言都支持类和类继承的特性,而JS却不支持这些特性,只能通过其他方法定义并关联多个相似的对象,这种状态一直延续到了ES5。由于类似的库层出不穷,最终还是在ECMAScript 6中引入了类的特性。

    一、ES5近似结构

      在ES5中没有类的概念,最相近的思路是创建一个自定义类型:首先创建一个构造函数,然后定义另一个方法并赋值给构造函数的原型

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

      这段代码中的personType是一个构造函数,其执行后创建一个名为name的属性给personType的原型添加一个sayName()方法,所以PersonType对象的所有实例都将共享这个方法。然后使用new操作符创建一个personType的实例person,并最终证实了person对象确实是personType的实例,且由于存在原型继承的特性,因而它也是object的实例

      许多模拟类的JS库都是基于这个模式进行开发,而且ES6中的类也借鉴了类似的方法

    二、类的声明

      ES6有一种与其他语言中类似的类特性:类声明。同时,它也是ES6中最简单的类形式

    1、基本的类声明语法

      要声明一个类,首先编写class关键字,紧跟着的是类的名字,其他部分的语法类似于对象字面量方法的简写形式,但不需要在类的各元素之间使用逗号分隔

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

      通过类声明语法定义PersonClass的行为与之前创建PersonType构造函数的过程相似,只是这里直接在类中通过特殊的constructor方法名来定义构造函数,且由于这种类使用简洁语法来定义方法,因而不需要添加function关键字。除constructor外没有其他保留的方法名,所以可以尽情添加方法

      私有属性是实例中的属性,不会出现在原型上,且只能在类的构造函数或方法中创建,此例中的name就是一个私有属性。建议在构造函数中创建所有私有属性,从而只通过一处就可以控制类中的所有私有属性

      类声明仅仅是基于已有自定义类型声明的语法糖。typeof PersonClass最终返回的结果是"function",所以PersonClass声明实际上创建了一个具有构造函数方法行为的函数。此示例中的sayName()方法实际上是PersonClass.prototype上的一个方法;与之类似的是,在之前的示例中,sayName()也是personType.prototype上的一个方法。通过语法糖包装以后,类就可以代替自定义类型的功能,不必担心使用的是哪种方法,只需关注如何定义正确的类

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

    2、为何使用类语法

      尽管类与自定义类型之间有诸多相似之处,但是它们之间仍然有一些差异

      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("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调用,如果是则抛出错误;最后,返回这个构造函数

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

    3、常量类名

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

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

      以上代码中,类的外部有一个Foo声明,而类构造函数里的Foo则是一个独立存在的绑定。内部的Foo就像是通过const声明的,修改它的值会导致程序抛出错误;而外部的Foo就像是通过let声明的,可以随时修改这个绑定值

    三、类表达式

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

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

    1、基本的类表达式语法

      下面这段代码等价于之前PersonClass示例的类表达式

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

      类声明和类表达式仅在代码编写方式略有差异,二者均不会像函数声明和函数表达式一样被提升,所以在运行时状态下无论选择哪一种方式,代码最终的执行结果都没有太大差别

      二者最重要的区别是name属性不同,匿名类表达式的name属性值是一个空字符串,而类声明的name属性值为类名,例如,通过声明方式定义一个类PersonClass,则PersonClass.name的值为"PersonClass"

    2、命名类表达式

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

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

      上面的示例中,类表达式被命名为PersonClass2,由于标识符PersonClass2只存在于类定义中,因此它可被用在像sayName()这样的方法中。而在类的外部,由于不存在一个名为PersonClass2的绑定,因而typeof PersonClass2的值为"undefined"

      在JS引擎中,类表达式的实现与类声明稍有不同。对于类声明来说,通过let定义的外部绑定与通过const定义的内部绑定具有相同名称;而命名类表达式通过const定义名称,从而PersonClass2只能在类的内部使用

      尽管命名类表达式与命名函数表达式有不同的表现,但二者间仍有许多相似之处,都可以在多个场景中作为值使用

    四、一等公民

      在程序中,一等公民是指一个可以传入函数,可以从函数返回,并且可以赋值给变量的值。JS函数是一等公民(也被称作头等函数),这也正是JS中的一个独特之处

      ES6延续了这个传统,将类也设计为一等公民,允许通过多种方式使用类的特性。例如,可以将类作为参数传入函数中

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

      在这个示例中,调用createObject()函数时传入一个匿名类表达式作为参数,然后通过关键字new实例化这个类并返回实例,将其储存在变量obj中

      类表达式还有另一种使用方式,通过立即调用类构造函数可以创建单例。用new调用类表达式,紧接着通过一对小括号调用这个表达式

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

      这里先创建一个匿名类表达式,然后立即执行。依照这种模式可以使用类语法创建单例,并且不会在作用域中暴露类的引用,其后的小括号表明正在调用一个函数,而且可以传参数给这个函数(类似于立即调用函数)

      我们可以通过类似对象字面量的语法在类中创建访问器属性

    五、访问器属性

      尽管应该在类构造函数中创建自己的属性,但是类也支持访问器属性。创建getter时,需要在关键字get后紧跟一个空格和相应的标识符;创建setter时,只需把关键字get替换为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元素的包装器,并通过getter和setter方法将这个元素的innerHTML方法委托给html属性,这个访问器属性是在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("huochai");
    me.sayName(); // "huochai"

      PersonClass通过变量来给类定义中的方法命名,字符串"sayName"被赋值给methodName变量,然后methodName又被用于声明随后可直接访问的sayName()方法

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

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

      在这里通过propertyName变量并使用getter和setter方法为类添加html属性,并且可以像往常一样通过.html访问该属性

      在类和对象字面量诸多的共同点中,除了方法、访问器属性及可计算名称上的共同点外,还需要了解另一个相似之处,也就是生成器方法

    七、生成器方法

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

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

      这段代码创建了一个名为MyClass的类,它有一个生成器方法createIterator(),其返回值为一个硬编码在生成器中的迭代器。如果用对象来表示集合,又希望通过简单的方法迭代集合中的值,那么生成器方法就派上用场了。数组、Set集合及Map集合为开发者们提供了多个生成器方法来与集合中的元素交互

      尽管生成器方法很实用,但如果类是用来表示值的集合的,那么为它定义一个默认迭代器会更有用。通过Symbol.iterator定义生成器方法即可为类定义默认迭代器

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

      这个示例用可计算名称创建了一个代理this.items数组values()迭代器的生成器方法。任何管理一系列值的类都应该引入默认迭代器,因为一些与特定集合有关的操作需要所操作的集合含有一个迭代器。现在可以将collection的实例直接用于for-of循环中或用展开运算符操作它

      如果不介意在对象的实例中出现添加的方法和访问器属性,则可以将它们添加到类的原型中;如果希望它们只出现在类中,那么需要使用静态成员

    八、静态成员

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

      在其他编程语言中,由于工厂方法PersonType.create()使用的数据不依赖personType的实例,因而其会被认为是一个静态方法。

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

      PersonClass定义只有一个静态方法create(),它的语法与sayName()的区别只在于是否使用static关键字。类中的所有方法和访问器属性都可以用static关键字来定义,唯一的限制是不能将static用于定义构造函数方法

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

  • 相关阅读:
    〖Linux〗Kubuntu设置打开应用时就只在打开时的工作区显示
    〖Linux〗Kubuntu, the application 'Google Chrome' has requested to open the wallet 'kdewallet'解决方法
    unity, dll is not allowed to be included or could not be found
    android check box 自定义图片
    unity, ios skin crash
    unity, Collider2D.bounds的一个坑
    unity, ContentSizeFitter立即生效
    类里的通用成员函数应声明为static
    unity, Gizmos.DrawMesh一个坑
    直线切割凹多边形
  • 原文地址:https://www.cnblogs.com/goloving/p/7989983.html
Copyright © 2011-2022 走看看