三种组织JavaScript程序的方法:类、函数式模块、ES6模块

507 阅读3分钟

1. 类 (Classes)

1.1 类的定义

类是一种自定义的数据类型,其中包含了数据和对数据的操作(方法)。类是对某种数据类型的定义,而不是一个具体的值。如果想获得一个具体的值,我们需要在程序中将类实例化(instantiation)

class Page {
    constructor(text) {
        this.text = text;
    }

    print() {
        console.log(this.text)
    }
}

class Notebook {
    constructor() {
        this.pages = [];
    }

    addPage(text) {
        var page = new Page(text);
        this.pages.push(page);
    }

    print() {
        for (let page of this.pages) {
            page.print();
        }
    }
}

var programNotes = new Notebook();
programNotes.addPage("String is a series of characters.")
programNotes.addPage("Array is a series of values.")

programNotes.print()
/*
String is a series of characters.
Array is a series of values.
*/

在Page类中,数据是存储在text变量中的字符串,对数据的操作是print(),它会把text的内容打印到控制台。

在Notebook类中,数据是由若干个Page的示例组成的数组。操作是addPage(),它可以实例化一个Page对象,然后把这个Page加到Page列表中;print() 会把所有页面的内容打印出来。

类让我们可以把数据和操作打包组织起来。虽然我们不使用类也可以实现上述的功能,但类显著地提高了程序的可读性和可维护性。

1.2 类的继承(Inheritance)

class Publication {
    constructor(title,author,pubDate) {
        this.title = title;
        this.author = author;
        this.pubDate = pubDate;
    }

    print() {
        console.log(`
            Title: ${ this.title }
            By: ${ this.author }
            ${ this.pubDate }
        `);
    }
}

Publication 类定义了所有出版物普遍拥有的数据和操作。

接下来我们来定义几种更具体的出版物,比如书籍和博客。

class Book extends Publication {
    constructor(bookDetails) {
        super(
            bookDetails.title,
            bookDetails.author,
            bookDetails.publishedOn
        );
        this.publisher = bookDetails.publisher;
        this.ISBN = bookDetails.ISBN;
    }

    print() {
        super.print();
        console.log(`
            Publisher: ${ this.publisher }
            ISBN: ${ this.ISBN }
        `);
    }
}

class BlogPost extends Publication {
    constructor(title,author,pubDate,URL) {
        super(title,author,pubDate);
        this.URL = URL;
    }

    print() {
        super.print();
        console.log(this.URL);
    }
}

Book 类和 Publication 类都用了 extends 子句来扩充原有的出版物的定义。构造函数 (constructor)  中的 super() 引用了父类 Publication 的构造函数来进行初始化。在JS中子类如果定义了自己的构造函数,就必须引用 super() ,否则会报错。

var YDKJSY = new Book({
    title: "You Don't Know JS yet",
    author: "Kyle Simpson",
    publishedOn: "June 2020",
    publisher: "O'Reilly",
    ISBN: "123456-789"
});

YDKJSY.print();
// Title: You Don't Know JS yet
// By: Kyle Simpson
// June 2020
// Publisher: O'Reilly
// ISBN: 123456-789

var OrganizeInJS = new BlogPost(
    "How We Organize in IS",
    "Kyle Simpson",
    "October 27, 2020",
    "https://www.inJS.org"
);

OrganizeInJS.print();
// Title: For and against let
// By: Kyle Simpson
// October 27, 2020
// https://www.inJS.org

注意到 Book类和 BlogPost类都有一个 print() 方法,它们覆盖(override)继承(inherit) 于父类 Publication的 print() 方法。这种同个方法可以有不同的行为的现象叫做多态(polymorphism)

2. 模块(Modules)

2.1 传统的函数模块定义法

传统的模块实际上就是定义一个外层函数。调用这个函数会返回一个模块的实例,里面包含了若干个可以操作实例内的数据的函数。

下面我们用传统的函数模块方法来再次定义 Publication, Book和 BlogPost

function Publication(title,author,pubDate) {
    var publicAPI = {     
        print() {
            console.log(`
                Title: ${ title }
                By: ${ author }
                ${ pubDate }
            `);
        }
    };

    return publicAPI;
}

function Book(bookDetails) {
    var pub = Publication(
        bookDetails.title,
        bookDetails.author,
        bookDetails.publishedOn
    );

    var publicAPI = {
        print() {
            pub.print();
            console.log(`
                Publisher: ${ bookDetails.publisher }
                ISBN: ${ bookDetails.ISBN }
            `);
        }
    };

    return publicAPI;
}

function BlogPost(title,author,pubDate,URL) {
    var pub = Publication(title,author,pubDate);

    var publicAPI = {
        print() {
            pub.print();
            console.log(URL);
        }
    };

    return publicAPI;
}

我们来对比一下函数模块和类之间的异同。

类把方法和数据存储在一个对象的实例中,这些方法和数据都要用this.前缀来获取。而在上面这种方法中,数据和方法都可以直接在作用域中获取,不需要this.前缀。

在类中,所有的数据和方法都是公开(public) 的。而在函数定义的模块中,数据和未被引用的方法都是私有(private) 的。

接下来我们来看看如何使用函数定义的模块。 js

var YDKJSY = Book({
    title: "You Don't Know JS yet",
    author: "Kyle Simpson",
    publishedOn: "June 2020",
    publisher: "O'Reilly",
    ISBN: "123456-789"
});

YDKJSY.print();
// Title: You Don't Know JS yet
// By: Kyle Simpson
// June 2020
// Publisher: O'Reilly
// ISBN: 123456-789

var OrganizeInJS = BlogPost(
    "How We Organize in IS",
    "Kyle Simpson",
    "October 27, 2020",
    "https://www.inJS.org"
);

OrganizeInJS.print();
// Title: For and against let
// By: Kyle Simpson
// October 27, 2020
// https://www.inJS.org

不难发现,实际使用中函数模块和类的唯一区别就是没有使用 new 关键字。

2.2 ES模块 (ES Modules)

这是ES6引入的新特性,其目的和上文介绍的函数模块定义方法是相同的。但它们的使用方式大为不同。

首先,ES模块中不需要定义一个外层函数,而是以文件的形式进行组织。一个文件代表一个模块。

其次,你不需要直接定义和引用一个模块的“API”打交道,而是通过 export 关键字来添加变量或方法到你想要给其他程序使用的接口。

最后,你要通过 import 关键字来引用ES模块,而不是像上面两种方法那样创建一个实例。这也就意味着你不能创建多个不同的实例,如果你需要这样,那就得在ES模块中使用传统的模块定义法,或者在其中引入类。

我们通过同样的实例来看看这如何实现

publication.js

function printDetails(title,author,pubDate) {
    console.log(`
        Title: ${ title }
        By: ${ author }
        ${ pubDate }
    `);
}

export function create(title,author,pubDate) {
    var publicAPI = {
        print() {
            printDetails(title,author,pubDate);
        }
    };

    return publicAPI;
}

blogpost.js 这个模块引用了上面的模块

import { create as createPub } from "publication.js";

function printDetails(pub,URL) {
    pub.print();
    console.log(URL);
}
export function create(title,author,pubDate,URL) {
    var pub = createPub(title,author,pubDate);
    var publicAPI = {
        print() {
            printDetails(pub,URL);
        }
    };
    return publicAPI;
}

最后,我们在下面这个 main.js 中使用 blogpost 模块

import { create as newBlogPost } from "blogpost.js";

var forAgainstLet = newBlogPost(
    "For and against let",
    "Kyle Simpson",
    "October 27, 2014",
    "https://davidwalsh.name/for-and-against-let"
);

forAgainstLet.print();
// Title: For and against let
// By: Kyle Simpson
// October 27, 2014
// https://davidwalsh.name/for-and-against-let

第一行的 as newBlogPost 将引用的函数 create 进行了重命名,这样可以提高可读性。