现代JavaScript库开发

1. 从零开发一个JavaScript库

1.1 如何开始

如何确定要开发什么库呢?

  1. 从项目中寻找灵感,将项目中的一些功能进行抽象设计,提取通用逻辑,进行额外处理,形成一个公共库的原型;一般项目中也会存在一些公共函数和组件等,是很好的灵感来源
  2. 从开源项目中寻找灵感,改进或封装开源项目
  3. 记录闪现在脑海里的想法,时常翻看自己的想法清单

切忌好高骛远,个人精力有限,建议开发小而美、功能专一的库.

1.2 编写代码

先介绍深拷贝的含义.JavaScript中有8种基本数据类型(undefined、null、number、string、boolean、symbol、object、bigint),前6种数据类型的数据在赋值操作时都是值拷贝

值拷贝发生之后,两个变量之间就没有任何关联了

let a = 1
let b = a

a = 2 // 对a的修改不会影响变量b

console.log(a) // 2
console.log(b) // 1

而对象类型的数据在进行赋值操作时会发生引用拷贝,此时两个变量会指向相同数据,对其中一个变量进行操作会影响另一个变量

let a = { c: 1 }
let b = a

a.c = 2 // a的修改会影响b

console.log(a.c) // 2
console.log(b.c) // 2

对引用类型数据进完整复制的过程,称为深拷贝.我们提供一个函数来完成该功能

function clone(data){}

我们将对象类型数据看作一个树结构,a是根节点,引用类型对象b是中间结点,值类型属性c和d是叶子节点

这样就将深拷贝问题转化为树的遍历问题,常用遍历方法是深度优先遍历,一般使用递归实现

function clone(source) {
    const t = type(source)
    if (t !== 'object' && t !== 'array') {
        return source
    }

    let target

    if (t === 'object') {
        target = {}
        for (let i in source) {
            if (source.hasOwnProperty(i)) {
                target[i] = clone(source[i]) // !
            }
        }
    } else {
        target = []
        for (let i = 0; i < source.length; i++) {
            target[i] = clone(source[i]) // !
        }
    }

    return target
}

上面的代码使用type函数获取数据的类型,该函数的原理是通过Object类中的toString方法获取数据的内部类型信息

function type(data) {
    return Object.prototype.toString.call(data).slice(8, -1).toLowerCase();
}

测试深拷贝之后,a的修改会不会影响b(预期: 不会)

let a = { c: 1 }
let b = clone(a) // 深拷贝

a.c = 2

console.log(a.c) // 2
console.log(b.c) // 1

1.3 本章小结

本章介绍了从零开发JavaScript库的流程和基本方法,完成第一个版本的深拷贝工具库,但是当把代码分享给别人时,会面临以下问题:

  • 小A使用了CommonJS模块,不知道该如何引用这个库
  • 小B说这个库的代码在IE浏览器中会报错

2. 构建

2.1 模块化解析

ECMAScript 2015(又称ES6)带来了原生的模块规范,在此之前,js没有统一的模块化规范,其中影响力较大的如AMD和CommonJS,现在仍在广泛使用

2.1.1 什么是模块

随着程序规模的扩大,以及引入第三方库,共享全局作用域会带来很多问题,比如命名冲突,很多语言提供了解决方法

  • c语言中的宏编译
  • c++语言中的命名空间
  • python中的模块
  • java中的包
  • php中的命名空间

js社区选择了模块方案,合格的模块方案需要满足以下特性:

  • 独立性: 能够独立完成某个功能,隔绝外部环境的影响
  • 完整性: 能够完成某个特定功能
  • 可依赖: 可以依赖其他模块
  • 被依赖: 可以被其他模块依赖

2.1.2 原始模块

从定义上看,一个函数即是一个模块,在es6(es 2015)之前,只有函数能创建作用域,下面是js社区中原始模块的代码:

(function (mod, $) {
    function clone(source) {
        // 具体逻辑
    }

    mod.clone = clone
})((window.clone = window.clone || {}), jQuery)

这里的mod模块不会被重复定义,依赖通过函数参数注入.但是这种实现并不完美,仍然需要手动维护依赖的顺序,比如这边传入的jQuery需要先于代码被引用,当模块越来越多,这种问题会变得不可维护

一般的库都有对这种模块的支持,因为这种模块可以直接通过script标签引入

2.1.3 AMD

AMD(Asynchronous Module Definition)是一种异步模块加载器,专为浏览器端设计,中文名称是异步模块定义,AMD规范中定义模块的方式如下:

define(id?, dependencies?, factory);

浏览器并不支持AMD模块,需要借助RequireJS才能加载.RequireJS是使用最广泛的AMD模块加载器,但是现在新系统一般不再使用RequireJS,而是使用库自带的支持

给我们的深拷贝库添加对AMD模块的支持

define(function () {
    function clone(source) {
        // 具体逻辑
    }
    
    return clone
})

这个代码定义了一个匿名AMD模块,假设代码位于clone.js,则在index.js里可以这样使用

define(['clone'], function(clone){
    const a = { a: 1 }
    const b = clone(b)
})

2.1.4 CommonJS

CommonJS是一种同步模块加载规范,主要用于nodejs环境中

define(function(require,exports,module){
    // ...
})

在别的文件里使用

const clone = require("./lib/clone");

let a = { c: 1 };
let b = clone(a); // 深拷贝

2.1.5 UMD

Universal Module Definition,是一种通用模块加载规范,中文: 通用模块定义.它不是一种新规范,而是对前面介绍的3种规范的整合.支持UMD规范的库可以在任何模块环境中工作

(function (root, factory) {
    var clone = factory(root);
    if (typeof clone === 'function' && define.amd) {
        // AMD
        define('clone', function (clone) {
            return clone
        })
    } else if (typeof exports === 'object') {
        // CommonJS
        module.exports = clone
    } else {
        // 原始模块
        var _clone = root.clone

        clone.noConflict = function () {
            if (root.clone === clone) {
                root.clone = _clone
            }

            return clone
        }
        root.clone = clone
    }
})(this, function (root) {
    function clone(source) {
        // ...
    }
    return clone
})

可以看出,UMD规范就是进行了简单的整合,稍微不同的是,给原始模块添加了noConflict,用于解决全局名称冲突的问题

2.1.6 ES Module

ES6(2015)带来的原生模块系统.目前,部分浏览器支持直接使用ES Module,而不兼容的浏览器则通过构建工具来使用

ES Module只需要在函数面前加上export就可以

export function clone(source){
    //
}

别的文件引用clone.js

import { clone } from './clone.js'
const a = { a: 1 };
const b = clone(a);

为了提高各种模块的支持,开源库可以提供两个入口文件

2.2 技术体系解析

2009年是前端技术体系的分水岭,nodejs的发布推动前端技术体系的发展,从”刀耕火种”迈入工具化时代,开源库需要支持不同的技术体系

场景: clone库有一个type函数,现在假设别的库也需要依赖type函数,于是将type函数抽象为一个库,此时有两个库,clone库依赖type库

一般一个js库都会依赖另外一些库,不同技术体系下,依赖关系有不同的解决方案

2.2.1 传统体系

一般是在HTML文件里用script标签引入JavaScript文件,这种体系下,每个库都要提供一个js文件

如果想使用一个库,需要想手动引入要用的库以及该库所依赖的库

<script src="lib/type.js"></script>
<script src="lib/clone.js"></script>
<script>
    let a = {c:1}
    let b = clone(a)
</script>

这种方式对大型库很不友好,使用者要手动引入很多文件,为了兼容传统体系,需要将所有代码及其依赖的库的代码合并成一个文件.也存在例外,比如jquery需要依赖jquery,react要依赖react

2.2.2 nodejs体系

nodejs遵循commonjs规范,内置依赖解析系统,使用require系统函数就可以依赖一个模块

const clone = require('./clone.js')

使用require引用文件时,被引用文件的路径需要遵循规则,引用支持相对路径、绝对路径、第三方包,如果忽略后缀,则会被当作nodejs的模块去解析

nodejs模块目录下需要有一个package.json文件,用于定义模块的一些属性.如果要新建模块,可以用npm工具快速初始化,然后在lib目录下新建并初始化clone模块

mkdir clone
cd clone
npm init

package.json里会有一个main字段,定义的是当前模块对应的逻辑入口文件,当该模块被其他模块引用时,nodejs会找到main字段对应的文件

在index.js里引入

const clone = require('./lib/clone')

let a = {c:1}
let b = clone(a)

nodejs体系下,只需要提供对commonjs或UMD模块的支持即可

2.2.3 工具化体系


  转载请注明: malred-blog 现代JavaScript库开发

 上一篇
function type(data) { return Object.prototype.toString.call(data).slice(8, -1).toLowerCase(); } function clone(sour
2023-06-22 malred
下一篇 
垃圾回收算法设计与实现 垃圾回收算法设计与实现
序章GC的定义 GC是Garbage Collection的简称 垃圾的回收 垃圾就是程序不用的内存空间 GC要做两件事 找到内存空间里的垃圾 回收垃圾,让程序能再次利用这部分空间 没有GC的世界 没有GC,程序员就需要手动内存管理,
2023-06-22
  目录