[JS] You Don't Know JavaScript [Scope & Closures] - The Module Pattern

前言

在本章节中将介绍这本书最重要的程序组织之一,module,module会用到我们之前所介绍的所有观念(lexical scope,closure...),我们以不同角度介绍了lexcail scope,从全域作用域到嵌套块状作用域,并利用lexical scope了解了closure,而而本章节的目标是能够理解module如何体现这些特性的重要性。

Encapsulation and Least Exposure (POLE)

封装个概念基础且广泛的被用在物件导向(OO)的程序中,封装的目的是将信息(数据)与行为(功能)捆棒再一起,以发挥共同的作用,而封装的精神可以透过简单的方式实现,例如对於不同专案使用不同文件分别保存,以文件的形式封装。

现代的程序语言的组件体系结构近一步的推广了封装,可以很自然的将类似功能的内容合并到一个程序逻辑中,然後将这个集合定义为一个component

另一个主要目标是控制封装数据和功能的隐藏性,回想在Limiting Scope Exposure ?中提到的POLE原则,我们应该尽可能地将不必要暴露的变量或功能隐藏起来以提高安全性,而在JS中我们经常通过lexcal scope来达到这个功能。

将相同的程序组合在一起,并选择性的限制访问我们认为私有(private)的部分,将不被视为私有(private)的部分设定为公开(public),这样可以更好的组织我们的程序,了避免了过度暴露数据与功能,可以更好的维护。


What is a Module?

Module相关数据和功能(method)的集合,他的特徵是他可以划分为需要隐藏的信息(private)可以公开的信息(public),而公开的部分通常称为public API

Namespaces(Stateless Grouping)

如果将一组相关的funciton组合在一起而没有数据,那麽实际上没有满足module的预期封装,这种对於状态function分组的行为称为namespaces

// namespace, not module
var Utils = {
    cancelEvt(evt) {
        evt.preventDefault();
        evt.stopPropagation();
        evt.stopImmediatePropagation();
    },
    wait(ms) {
        return new Promise(function c(res){
            setTimeout(res,ms);
        });
    },
    isValidEmail(email) {
        return /[^@]+@[^@.]+\.[^@.]+/.test(email);
    }
};

这里的Utils是一组function的集合,但他们都是与状态无关的function,虽然将function组合再一起是一个好习惯,但他并不能成为module。

Data Structures (Stateful Grouping)

如果将数据和function捆绑再一起但却没有设定他的可见性,那麽就没有使用到封装的POLE,那们他也不算是module,通常称这种型态为Data Structures。

// data structure, not module
var Student = {
    records: [
        { id: 14, name: "Kyle", grade: 86 },
        { id: 73, name: "Suzy", grade: 87 },
        { id: 112, name: "Frank", grade: 75 },
        { id: 6, name: "Sarah", grade: 91 }
    ],
    getName(studentID) {
        var student = this.records.find(
            student => student.id == studentID
        );
        return student.name;
    }
};

Student.getName(73); // Suzy

由於records是公开可以访问的数据而不是隐藏在public API後面的,所以此处的Student并不是一个真正个module,他虽然有包含了数据与功能,但没有控制可见性,所以最好是称之为Data Structures。

Modules (Stateful Access Control)

为了体现module的全部经精神,我们不仅需要分组与状态,还需要通过可见性(private/public)进行访问控制,我们可以将上面的student更改为一个module。

var Student = (function defineStudent(){
    var records = [
        { id: 14, name: "Kyle", grade: 86 },
        { id: 73, name: "Suzy", grade: 87 },
        { id: 112, name: "Frank", grade: 75 },
        { id: 6, name: "Sarah", grade: 91 }
    ];

    var publicAPI = {
        getName
    };

    return publicAPI;

    // ************************

    function getName(studentID) {
        var student = records.find(
            student => student.id == studentID
        );
        return student.name;
    }
})();

Student.getName(73);   // Suzy

将Student更改为一个module,他有一个public API(getName(...)),只有这个API可以访问到内部的records

从外部Student.getName(73)来调用内部的function,而records透过closure将他保存在记忆体中让之後调用的getname(...)依然可以使用这个变量,虽然上面的例子中是将public API放在object publicAPI之中,但是实际上可以单纯只返回getname(...),这样也能够满足module的所有核心要求。

var Student = (function defineStudent(){
    var records = [
        { id: 14, name: "Kyle", grade: 86 },
        { id: 73, name: "Suzy", grade: 87 },
        { id: 112, name: "Frank", grade: 75 },
        { id: 6, name: "Sarah", grade: 91 }
    ];

    return function getName(studentID) {
        var student = records.find(
            student => student.id == studentID
        );
        return student.name;
    }
})();

Student(73);   // Suzy

对於lexcial scope的工作原理来说,在外部module定义的函式中定义的变量与函式都会默认为private,只有return出去的public API才可以供外部使用。

Module Factory(Multiple Instances)

如果我们希望程序中定义一个可以支援多个实例的moudle,我们可以调整我们的程序。

// factory function, not singleton IIFE
function defineStudent() {
    var records = [
        { id: 14, name: "Kyle", grade: 86 },
        { id: 73, name: "Suzy", grade: 87 },
        { id: 112, name: "Frank", grade: 75 },
        { id: 6, name: "Sarah", grade: 91 }
    ];

    var publicAPI = {
        getName
    };

    return publicAPI;

    // ************************

    function getName(studentID) {
        var student = records.find(
            student => student.id == studentID
        );
        return student.name;
    }
}

var fullTime = defineStudent();
fullTime.getName(73); // Suzy

没有将defineStudent()定义为IIFE而是将它定义为普通函式,这样可以将它赋予给多个不同的变量,这个称为module factory

Classic Module Definition

  • 必须有一个外部作用域(通常是module factory),并且至少被呼叫一次。
  • module内部至少要有一个代表着module状况的信息。
  • module必须在public API上return一个对private数据引用的function。

Node CommonJS Modules

Around the Global Scope ?提到了Node Common Module,与前面介绍的经典module不同,Node Module可以将module factory或IIFE与其他程序码(包括其他module)捆绑再一起,CommonJS module以文件为基础,所以每个文件就是一个module。

module.exports.getName = getName;

// ************************

var records = [
    { id: 14, name: "Kyle", grade: 86 },
    { id: 73, name: "Suzy", grade: 87 },
    { id: 112, name: "Frank", grade: 75 },
    { id: 6, name: "Sarah", grade: 91 }
];

function getName(studentID) {
    var student = records.find(
        student => student.id == studentID
    );
    return student.name;
}

虽然recordsgetName(...)他们是处於这个文件中的最上层作用域,但他并不是全域作用域,在预设的情况下这个文件中的所有内容都是private,若要在CommonJS module的public API上加入需要公开的内容,你可以将需要公开的内容加入到module.exports作为他的属性。

若是要在引入其他module的实例,可以使用Node提供的require(...)method

// another module

var Student = require("/path/to/student.js"); //use method in Node

Student.getName(73); // Suzy

上面的Student就reference了其他module的public API,CommonJS module与使用IIFE一样都是单例实例,意味着无论你对同一个module require(...)多次,依然只会得到对单个module的实例。

rqeuire(...)是一种全有或全无的机制,它包括了对整个module中public API的引用,如果只想访问public API期中的一部分

var getName = require("/path/to/student.js").getName;

// or alternately:

var { getName } = require("/path/to/student.js");

与典型的module相似,CommonJS module的API也会透过closure将内部module的数据储存起来。


Modern ES Modules (ESM)

ES Module的格式与CommonJS Module的格式有些类似,ESM也是以文件为基础,module时例为单例并且默认下所有内容都是private,而他们之间明显的区别在於ESM是自动使用严格模式而不需要在开头宣告,并且无法将ESM设定为非严格模式

ESM并非像CommonJS Module一样使用module.exports的方式将public API公开,而是使用export,在引入的部分也从import替换了require(...),我们可以调整student.js以ESM的格式呈现。

export { getName };

// ************************

var records = [
    { id: 14, name: "Kyle", grade: 86 },
    { id: 73, name: "Suzy", grade: 87 },
    { id: 112, name: "Frank", grade: 75 },
    { id: 6, name: "Sarah", grade: 91 }
];

function getName(studentID) {
    var student = records.find(
        student => student.id == studentID
    );
    return student.name;
}

上面的程序码中唯一的变化export{ getName },和以前一样随然他们都被定义在当前文件的最上层作用域,但是他们却不是属於全域作用域中,ESM对於export语句提供了很多变化

export function getName(studentID) {
    // ..
}

将export定义在function关键字前面,这样他依然是一个function declaration并且可以顺利被export,也就是说getName透过function hoisting到此文件作用域的最上方,所以在这个module的整个范围都能用。

还有一种export的方式,他称为default export,这边就要先介绍一下什麽事default export,他与一般的export(name export)差别在哪。


default export vs name export

export可以区分为两种,这两种的汇出手法略有不同,他会影响到其他module的import运用。

  • named export(具名汇出):可以汇出独立的物件变数函式等等,汇出之前需要给予特定名称,使用import的时候也需要使用相同的名称,一个moudle可以有多个named export
  • default export(预设汇出):一个module中只能有一个default export,而不需要给予名称。
    两者可以两者可以共存在同一个module中,但是default export只能有一个

export default function getName(studentID) {
    // ..
}

由上面的介绍可以了解,使用default export的function可以在引入的module中定义它的名字。

至於import需要在其他module的顶层使用,并在语法上也有许多变化。

import { getName } from "/path/to/students.js";

getName(73); // Suzy

上面的程序中,在最上方import了其他module public API的内容,并将它添加到当前module的最顶层作用域中,可以在{...}中列出多个需要引入的API成员,并用逗号区分,也可以使用as关键字将他们重新命名。

import { getName as getStudentName } from "/path/to/students.js";

getStudentName(73); // Suzy

参考文献:
You Don't Know JavaScript -2nd


<<:  [填坑日记] Android Studio plugin to Unity

>>:  【Bootstrap】x 学习笔记 | Grid 格线系统 - 1

Day 10 Dart语言-混合及泛型

混合mixins 介绍:mixin是一种可以把自己的方法提供给别的类别使用,却不需要成为其他类别的父...

Day18 - (补上昨天程序码) + BBT介绍

大家好,我是长风青云。早起跟朋友约、下午无缝接轨去帮弟弟搬宿、晚上一回到家就开始做ppt和发片。累瘫...

Day27 - [丰收款] 永丰线上收款支付API功能实作总结(3) - 如何让机敏性设定值更有保护力

在前一篇文章,我们分析了各个资安防护的强弱要点,但由於固定式的初始四Hash组代码是目前安全性最弱的...

Day 9 - Laravel 8.0的Error Handling

不管是预期或非预期,程序往往会发生一些错误,我们不希望使用者Call API或浏览网页的时候发生错误...

[区块链&DAPP介绍 Day23] Dapp 实战 安装 truffle

今天来介绍一下,要开发dapp 的另一个不可或缺的工具 truffle truffle 跟之前介绍的...