0%

Node.js 模块机制采用了 Commonjs 规范,弥补了当前 JavaScript 开发大型应用没有标准的缺陷,类似于 Java 中的类文件,Python 中的 import 机制,Node.js 中可以通过 module.exports、require 来导出和引入一个模块.

在模块加载机制中,Node.js 采用了延迟加载的策略,只有在用到的情况下,系统模块才会被加载,加载完成后会放到 binding_cache 中。

快速导航

面试指南

  • require 的加载机制? ,参考:模块加载机制
  • module.exports 与 exports 的区别,参考:对象引用关系考察
  • 假设有 a.js、b.js 两个模块相互引用,会有什么问题?是否为陷入死循环?,参考正文“模块循环引用问题1”
  • a 模块中的 undeclaredVariable 变量在 b.js 中是否会被打印?,参考正文“模块循环引用问题2”
  • 模块在 require 的过程中是同步还是异步?,参考正文模块加载机制 “文件模块“

模块的分类

系统模块

  • C/C++ 模块,也叫 built-in 内建模块,一般用于 native 模块调用,在 require 出去
  • native 模块,在开发中使用的 Node.js 的 http、buffer、fs 等,底层也是调用的内建模块 (C/C++)。

    第三方模块

    非 Node.js 自带的模块称为第三方模块,其实还分为路径形式的文件模块(以 .../ 开头的)和自定义的模块(比如 express、koa 框架、moment.js 等)
  • javaScript 模块:例如 hello.js
  • json 模块:例如 hello.json
  • C/C++ 模块:编译之后扩展名为 .node 的模块,例如 hello.node

    目录结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ├── benchmark                         一些 Node.js 性能测试代码
    ├── deps Node.js 依赖
    ├── doc 文档
    ├── lib Node.js 对外暴露的 js 模块源码
    ├── src Node.js 的 c/c++ 源码文件,内建模块
    ├── test 单元测试
    ├── tools 编译时用到的工具
    ├── doc api 文档
    ├── vcbuild.bat win 平台 makefile 文件
    ├── node.gyp node-gyp 构建编译任务的配置文件
    ...

模块加载机制

面试中可能会问到能说下 require 的加载机制吗?
在 Node.js 中模块加载一般会经历 3 个步骤,路径分析文件定位编译执行
按照模块的分类,按照以下顺序进行优先加载:

  • 系统缓存:模块被执行之后会会进行缓存,首先是先进行缓存加载,判断缓存中是否有值。
  • 系统模块:也就是原生模块,这个优先级仅次于缓存加载,部分核心模块已经被编译成二进制,省略了 路径分析文件定位,直接加载到了内存中,系统模块定义在 Node.js 源码的 lib 目录下,可以去查看。
  • 文件模块:优先加载 .、..、/ 开头的,如果文件没有加上扩展名,会依次按照 .js、.json、.node 进行扩展名补足尝试,那么在尝试的过程中也是以同步阻塞模式来判断文件是否存在,从性能优化的角度来看待,.json、.node最好还是加上文件的扩展名。
  • 目录做为模块:这种情况发生在文件模块加载过程中,也没有找到,但是发现是一个目录的情况,这个时候会将这个目录当作一个 来处理,Node 这块采用了 Commonjs 规范,先会在项目根目录查找 package.json 文件,取出文件中定义的 main 属性 ("main": "lib/hello.js") 描述的入口文件进行加载,也没加载到,则会抛出默认错误: Error: Cannot find module ‘lib/hello.js’
  • node_modules 目录加载:对于系统模块、路径文件模块都找不到,Node.js 会从当前模块的父目录进行查找,直到系统的根目录
    catalog.jpg
    require 模块加载时序图

模块缓存在哪

上面讲解了模块的加载机制,中间有提到模块初次加载之后会缓存起来,有没有疑问,模块缓存在哪里?

Node.js 提供了 require.cache API 查看已缓存的模块,返回值为对象,为了验证,这里做一个简单的测试,如下所示:

  • 新建 test-module.js 文件
    这里我导出一个变量和一个方法
    1
    2
    3
    4
    module.exports = {
    a: 1,
    test: () => {}
    }
    新建 test.js 文件
    1
    2
    3
    require('./test-module.js');

    console.log(require.cache);
    在这个文件里加载 test-module.js 文件,在之后打印下 require.cache 看下里面返回的是什么?看到以下结果应该就很清晰了,模块的文件名、地址、导出数据都很清楚。
    test-module.png

模块循环引用

问题1

假设有 a.js、b.js 两个模块相互引用,会有什么问题?是否为陷入死循环?看以下例子

1
2
3
4
5
6
7
8
9
10
// a.js
console.log('a模块start');

exports.test = 1;

undeclaredVariable = 'a模块未声明变量'

const b = require('./b');

console.log('a模块加载完毕: b.test值:',b.test);
1
2
3
4
5
6
7
8
9
10
// b.js
console.log('b模块start');

exports.test = 2;

const a = require('./a');

console.log('undeclaredVariable: ', undeclaredVariable);

console.log('b模块加载完毕: a.test值:', a.test);

问题2

a 模块中的 undeclaredVariable 变量在 b.js 中是否会被打印?
控制台执行node a.js,查看输出结果:

1
2
3
4
5
a模块start
b模块start
undeclaredVariable: a模块未声明变量
b模块加载完毕: a.test值: 1
a模块加载完毕: b.test值: 2

问题1,启动 a.js 的时候,会加载 b.js,那么在 b.js 中又加载了 a.js,但是此时 a.js 模块还没有执行完,返回的是一个 a.js 模块的 exports 对象 未完成的副本 给到 b.js 模块(因此是不会陷入死循环的)。然后 b.js 完成加载之后将 exports 对象提供给了 a.js 模块
问题2,因为 undeclaredVariable 是一个未声明的变量,也就是一个挂在全局的变量,那么在其他地方当然是可以拿到的。
在执行代码之前,Node.js 会使用一个代码封装器进行封装,例如下面所示:

1
2
3
(function(exports, require, module, __filename, __dirname) {
// 模块的代码
});

对象引用关系考察

也许是面试考察最多的问题:module.exports 与 exports 的区别?
exports 相当于 module.exports 的快捷方式如下所示:

1
const exports = modules.exports;

但是要注意不能改变 exports 的指向,我们可以通过 exports.test = ‘a’ 这样来导出一个对象, 但是不能向下面示例直接赋值,这样会改变 exports 的指向

1
2
3
4
5
6
7
8
9
10
11
// 错误的写法 将会得到 undefined
exports = {
'a': 1,
'b': 2
}

// 正确的写法
modules.exports = {
'a': 1,
'b': 2
}

栈,英文 Last In First Out 简称 LIFO,遵从后进先出的原则,与 “队列” 相反,在栈的头部添加元素、删除元素,如果栈中没有元素就称为空栈。
在现实生活场景中也很多例子,例如盘子叠放,从上面一个一个放置,取时也是从上面一个一个拿走,不可能从下面直接抽着拿,如下图所示
stack_plate

这也是栈的典型应用,通过这个例子也可总结出栈的两个特性:

仅能从栈顶端存取数据
数据存取遵从后进先出原则

栈的运行机制

关于栈的概念通过前面的学习,应该有了初步的认知,这里从零实现一个栈进一步对栈的运行机制做一个分析,下面看下我们实现栈需要哪些步骤:

  1. Constructor(capacity): 初始化栈内存空间,设定栈的容量
  2. isEmpty(): 检查栈是否为空,是否有元素
  3. isOverflow(): 检查栈空间是否已满,如果满了是不能在入栈的
  4. enStack(element): 栈顶位置入栈,先判断栈是否已满
  5. deStack(): 栈顶位置出栈,先判断栈元素是否为空
  6. len(): 栈空间已有元素长度
  7. clear(): 清空栈元素,内存空间还是保留的
  8. destroy(): 销毁栈,同时内存也要回收(通常高级语言都会有自动回收机制,例如 C 语言这时就需要手动回收)
  9. traversing(): 遍历输出栈元素

初始化栈空间
在构造函数的 constructor 里进行声明,传入 capacity 初始化栈空间同时初始化栈的顶部(top)为 0,底部则无需关注永远为 0。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
*
* @param { Number } capacity 栈空间容量
*/
constructor(capacity) {
if (!capacity) {
throw new Error('The capacity field is required!');
}

this.capacity = capacity;
this.stack = new Array(capacity);
this.top = 0; // 初始化栈顶为 0
}

栈空间是否为空检查
定义 isEmpty() 方法返回栈空间是否为空,根据 top 栈顶位置进行判断。

1
2
3
isEmpty() {
return this.top === 0 ? true : false;
}

栈空间是否溢出检查
定义 isOverflow() 方法返回栈空间是否溢出,根据栈顶位置和栈的空间容量进行判断。

1
2
3
isOverflow() {
return this.top === this.capacity;
}

入栈
定义 enStack(element) 方法进行入栈操作,element 为入栈传入的参数,入栈之前先判断,栈是否已满,栈未满情况下可进行入栈操作,最后栈位置做 ++ 操作。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 入栈
* @param { * } element 入栈元素
*/
enStack(element) {
if (this.isOverflow()) {
throw new Error('栈已满');
}

this.stack[this.top] = element;
this.top++;
}

出栈
定义 enStack(element) 方法进行出栈操作,首先判断栈空间是否为空,未空的情况进行出栈操作,注意这里的栈位置,由于元素进栈之后会进行 ++ 操作,那么在出栈时当前栈位置肯定是没有元素的,需要先做 – 操作。

1
2
3
4
5
6
7
8
deStack() {
if (this.isEmpty()) {
throw new Error('栈已为空');
}

this.top--;
return this.stack[this.top];
}

栈元素长度
这个好判断,根据栈的 top 位置信息即可

1
2
3
len() {
return this.top;
}

清除栈元素
这里有几种实现,你也可以把 stack 的空间进行初始化,或者把 top 栈位置设为 0 也可。

1
2
3
clear() {
this.top = 0;
}

栈销毁
在一些高级语言中都会有垃圾回收机制,例如 JS 中只要当前对象不再持有引用,下次垃圾回收来临时将会被回收。不清楚的可以看看我之前写的 Node.js 内存管理和 V8 垃圾回收机制

1
2
3
destroy() {
this.stack = null;
}

栈元素遍历
定义 traversing(isBottom) 方法对栈的元素进行遍历输出,默认为顶部遍历,也可传入 isBottom 参数为 true 从底部开始遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
traversing(isBottom = false){
const arr = [];

if (isBottom) {
for (let i=0; i < this.top; i++) {
arr.push(this.stack[i])
}
} else {
for (let i=this.top-1; i >= 0; i--) {
arr.push(this.stack[i])
}
}

console.log(arr.join(' | '));
}

做一些测试
做下测试分别看下入栈、出栈、遍历操作,其它的功能大家在练习的过程中可自行实践。

1
2
3
4
5
6
7
8
9
10
const s1 = new StackStudy(4);

s1.enStack('Nodejs'); // 入栈
s1.enStack('技');
s1.enStack('术');
s1.enStack('栈');
s1.traversing() // 栈 | 术 | 技 | Nodejs
console.log(s1.deStack()); // 出栈 -> 栈
s1.traversing() // 术 | 技 | Nodejs
s1.traversing(true) // 从栈底遍历:Nodejs | 技 | 术

stack_machine_made
栈的运行机制源码地址

JavaScript 数组实现栈

JavaScript 中提供的数组功能即可实现一个简单的栈,使用起来也很方便,熟悉相关 API 即可,下面我们来看下基于 JS 数组的入栈、出栈过程实现。
js-array-stack
以上图片展示了栈的初始化、入栈、出栈过程,下面我们采用 JavaScript 原型链的方式实现。
初始化队列
初始化一个存储栈元素的数据结构,如果未传入默认赋值空数组。

1
2
3
function StackStudy(elements) {
this.elements = elements || [];
}

添加栈元素
实现一个 enStack 方法,向栈添加元素,注意只能是栈头添加,使用 JavaScript 数组中的 push 方法。

1
2
3
StackStudy.prototype.enStack = function(element) {
this.elements.push(element);
}

移除栈元素
实现一个 deStack 方法,栈尾部弹出元素,使用 JavaScript 数组中的 pop 方法(这一点是和队列不同的)。

1
2
3
StackStudy.prototype.deStack = function() {
return this.elements.pop();
}

通过 JavaScript 数组实现

栈的经典应用

通过对前面的讲解,相信已经对栈有了一定的了解,那么它可以用来做什么呢,本节举几个典型的应用案例。
十进制转换为二进制、八进制、十六进制
现在生活中我们使用最多的是十进制来表示,也是人们最易懂和记得的,但是计算机在处理的时候就要转为二进制进行计算,在十进制与二进制的转换过程之间一般还会用八进制或者十六进制作为二进制的缩写。

因此,这里主要讲解十进制、八进制、十六进制、二进制转换过程中在栈中的实际应用。首先你需要先了解这几种数据类型之间的转换规则,也不难通过一张图来告诉你。
data_transformation

上图中我们用十进制整除需要转换的数据类型(二进制、八进制、十六进制),将余数放入栈中,明白这个原理在用代码实现就很简单了。
编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const StackStudy = require('./stack.js');
const str = '0123456789ABCDEF';

function dataConversion(num, type) {
let x = num;
const s1 = new StackStudy(20);

while (x != 0) {
s1.enStack(x % type);
x = Math.floor(x / type);
}

while (!s1.isEmpty()) {
console.log(str[s1.deStack()]);
}

console.log('--------------------');
return;
}

引用我们在栈的运行机制里面讲解的代码,编写 dataConversion 方法,入栈、出栈进行遍历输出。代码中定义的变量 str 是为了十六进制会出现字母的情况做的处理。

以下运行结果完全符合我们的预期,大家也可用电脑自带的计算器功能进行验证。

1
2
3
4
5
6
7
8
9
10
11
12
// 测试八进制
dataConversion(1024, 8); // 2000

// 测试十六进制
dataConversion(1024, 16); // 400

// 测试十六进制带字母的情况
dataConversion(3000, 16); // BB8

// 测试二进制
dataConversion(1024, 2); // 10000000000

十进制转换为二进制、八进制、十六进制源码地址

队列,英文 First In First Out 简称 FIFO,遵从先进先出的原则,与 “栈” 相反,在队列的尾部添加元素,在队列的头部删除元素,如果队列中没有元素就称为空队列。

队列对应到生活场景中有很多例子,例如,我们去火车站窗口购票总要排队,先排队的人先购票,有新的人来了则在队尾排队等待前面的完成了依次购票。另外我们的订单超时队列、活动抢购先到先得等等,队列在生活中应用很广泛。

JavaScript 数组实现队列

JavaScript 中提供的数组功能即可实现一个简单的队列,使用起来也很方便,熟悉相关 API 即可,下面我们来看下基于 JS 数组的入队、出队过程实现。
js-array-queue
以上图片展示了队列的初始化、入队、出队过程,下面我们采用 JavaScript 原型链的方式实现。

  • 初始化队列
    初始化一个存储队列中元素的数据结构,如果未传入默认赋值空数组,传入需先校验类型是否正确。
    1
    2
    3
    4
    5
    6
    7
    function QueueStudy(elements) {
    if (elements && !(elements instanceof Array)) {
    throw new Error('必须为数组格式!');
    }

    this.elements = elements || [];
    }
  • 队列添加元素
    实现一个 enQueue 方法,向队列添加元素,注意只能是队列尾部添加,使用 JavaScript 数组中的 push 方法。
    1
    2
    3
    QueueStudy.prototype.enQueue = function(element) {
    this.elements.push(element);
    }
  • 队列移除元素
    实现一个 deQueue 方法,向队列头部弹出元素,使用 JavaScript 数组中的 shift 方法。
    1
    2
    3
    QueueStudy.prototype.deQueue = function() {
    return this.elements.shift();
    }
    通过 JavaScript 数组实现是很简单的,源码参见 https://github.com/Q-Angelo/project-training/tree/master/algorithm/queue-js.js

优先队列

优先队列,元素的添加、删除是基于优先级进行的。一个现实的例子就是机场登机的顺序。头等舱和商务舱乘客的优先级要高于经济舱乘客。在有些国家,老年人和孕妇(或带小孩的妇女)登机时也享有高于其他乘客的优先级。

优先队列对应到我们生活场景中也有很多例子,例如我们去银行办理业务,一般都会排号先到的先办理,但是呢,还会有 VIP 会员优先办理,又或者去火车站窗口上购票也会有提示军人可以优先办理等等

实现步骤
核心实现继 JavaScript 数组实现队列的例子,对入队函数进行改造如下所示:

  • 声明 queueElement 对象,包含了要添加到队列的元素
  • 如果队列为空直接入队
  • 如果找到一个比 priority 优先级大的元素,插入新元素,这里使用到了 JS 数组中的 splice 方法
  • 最后如果队列中的所有元素的优先级都小于 priority,则直接在队列尾部入队
  • 另外打印输出的方法也做了简单修改

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PriorityQueue.prototype.enQueue = function(element, priority) {
const queueElement = { element, priority };

if (this.isEmpty()) {
return this.elements.push(queueElement);
}

let added = false;
for (let i=0; i < this.elements.length; i++) {
if (priority < this.elements[i]['priority']) {
added = true;
this.elements.splice(i, 0, queueElement)
break;
}
}

if (!added) {
this.elements.push(queueElement);
}
}

PriorityQueue.prototype.print = function() {
console.log(this.elements.map(item => item.element).join(' | '));
}

运行测试

1
2
3
4
5
6
7
8
9
10
const queue = new PriorityQueue();
queue.enQueue('普通会员1', 5);
queue.enQueue('普通会员2', 10);
queue.print() // 普通会员1 | 普通会员2
queue.enQueue('VIP会员1', 3);
queue.print() // VIP会员1 | 普通会员1 | 普通会员2
queue.enQueue('VIP会员2', 3);
queue.print() // VIP会员1 | VIP会员2 | 普通会员1 | 普通会员2
queue.deQueue();
queue.print() // VIP会员2 | 普通会员1 | 普通会员2

图例展示
下面以图例的形式展示以上优先队列程序的运行过程
queue-priority
以上是将优先级最小的元素放置于队列前面,称之为最小优先队列,最大优先队列的实现则反之。源码参见 https://github.com/Q-Angelo/project-training/tree/master/algorithm/queue-priority.js

循环队列

循环队列有些地方也称之为环形队列,其本身是一种环形结构的队列,相较于普通队列有个好处是第一个元素出队之后,剩下元素无需依次向前移位,充分利用了向量空间,在以下介绍中给出了完整的实现过程。

在设计环形队列时即可顺时针也可逆时针两个方向进行实现,在入队时可根据 (tail % capacity) 规则,进行队尾添加元素,tail 表示队尾的指针,capacity 表示容量,出队同样以(head % capacity)规则操作,head 表示队头指针,下面以长度为 6 的队列进行图文形式说明下实现过程。
queue-ring

ES6 实现循环队列
以下采用 EcameScript 6 的 Class 写法,实现一个环形队列,需要做哪些点呢?以下列出需要实现的功能点:

  • 创建队列,初始化队列空间
  • 检查队列是否为空
  • 检查队列是否溢出
  • 入队
  • 出队
  • 队列长度
  • 清空队列
  • 销毁队列,内存空间也将释放
  • 队列遍历输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
const Init = Symbol('QueueStudy#Init');

class QueueStudy {
constructor (capacity) {
if (!capacity) {
throw new Error('The capacity field is required!');
}

this.capacity = capacity; // 初始化容量
this[Init]();
}

/**
* 清空队列,内存保留
*/
clear() {
this[Init]()
}

[Init]() {
this.queue = new Array(this.capacity); // 初始化队列内存空间
this.queueLen = 0; // 初始化队列元素
this.head = 0; // 队头
this.tail = 0; // 尾部
}

/**
* 队列是否为空
*/
isEmpty() {
return this.queueLen === 0 ? true : false;
}

/**
* 队列是否溢出
*/
isOverflow() {
return this.queueLen === this.capacity
}

/**
* 入队
*/
enQueue(element) {
if (this.isOverflow()) {
return false;
}

this.queue[this.tail] = element;
this.tail++;
this.tail = this.tail % this.capacity;
this.queueLen++;
return true;
}

/**
* 出队
*/
deQueue() {
if (this.isEmpty()) {
throw new Error('队列为空');
} else {
const element = this.queue[this.head];
this.head++; // 队头位置移动
this.head = this.head % this.capacity;
this.queueLen--;
return element;
}
}

/**
* 队列长度
*/
len() {
return this.queueLen;
}

/**
* 销毁队列,内存回收
*/
destroy() {
this.queue = null;
}

/**
* 队列元素遍历
*/
traversing() {
console.log('------------traversing start------------');

for (let i=this.head; i<this.queueLen + this.head; i++) {
console.log(this.queue[i % this.capacity]);
}
console.log('------------traversing end------------\n');
}
}

运行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const q1 = new QueueStudy(6);

q1.enQueue('a');
q1.traversing();
q1.enQueue('b');
q1.enQueue('c');
q1.enQueue('d');
q1.enQueue('e');
q1.enQueue('f');
q1.traversing();
console.log('出队: ', q1.deQueue());
q1.enQueue('g');
q1.traversing();
console.log('出队: ', q1.deQueue());
console.log('出队: ', q1.deQueue());
q1.enQueue('h');
console.log('出队: ', q1.deQueue());
console.log('出队: ', q1.deQueue());
console.log('出队: ', q1.deQueue());
q1.traversing();
q1.clear();
q1.traversing();

queue-ring-test
源码参见 https://github.com/Q-Angelo/project-training/tree/master/algorithm/queue-ring.js
推荐我在学习数据结构中看的两本书 学习JavaScript数据结构与算法(第2版)、图解数据结构使用 Python

正则表达式扩展

  • 构造函数 ES5声明对象 情况一

    第一个参数是字符; 第二个是修饰符

    1
    2
    3
    let regex = new RegExp('xyz', 'i');

    console.log(regex.test('xyz123'), regex.test('xyZ123')); // true true
  • 构造函数 ES5声明对象 情况二

    第一个参数是正则表达式; 但是此时不接受第二个参数是一个修饰符,否则会报错

    1
    2
    3
    4
    let regex2 = new RegExp(/xyz/i); // 正确
    let regex3 = new RegExp(/xyz/i, 'i'); // 错误;Uncaught TypeError: Cannot supply flags when constructing one RegExp

    console.log(regex2.test('xyz123'), regex2.test('xyZ123')); // true true
  • 构造函数 ES6中的声明对象

    ES6改变了此行为,第一个参数是正则表达式,第二个参数也可以在指定修饰符。

    1
    2
    3
    let regex3 = new RegExp(/abc/ig, 'i');

    console.log(regex3.flags); // i
    以上示例中,原有正则对象的修饰符是ig,它会被第二个参数i覆盖。

字符串扩展

  • Unicode表示法
    1
    2
    3
    4
    5
    6
    7
    8
    {
    console.log('a',`\u0061`); //a a
    //乱码,因为\u20bb7转换成二进制以大于0xFFFF,会当做两个字符处理
    console.log('s',`\u20BB7`); //s ₻7

    //ES6中处理大于0xFFFF这种情况,用大括号{}把这种Unicode编码包括起来
    console.log('s',`\u{20BB7}`); //s 𠮷
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    {
    let s='𠮷';
    //取长度,四个字节为两个字符
    console.log('length',s.length); //2

    //ES5中charAt()取字符,charCodeAt()取码值
    console.log('0',s.charAt(0)); //0 �
    console.log('1',s.charAt(1)); //1 �
    console.log('at0',s.charCodeAt(0)); //at0 55362
    console.log('at1',s.charCodeAt(1)); //at1 57271

    //ES6中codePointAt()取码值,toString(16)转换成16进制
    let s1='𠮷a';
    console.log('length',s1.length);
    console.log('code0',s1.codePointAt(0)); //code0 134071
    console.log('code0',s1.codePointAt(0).toString(16)); //code0 20bb7
    console.log('code1',s1.codePointAt(1)); //code1 57271
    console.log('code2',s1.codePointAt(2)); //code2 97
    }
    1
    2
    3
    4
    5
    6
    {
    //ES5中fromCharCode()处理大于两个字节,会乱码
    console.log(String.fromCharCode("0x20bb7")); //ஷ
    //ES6中fromCodePoint()处理大于两个字节,正常显示
    console.log(String.fromCodePoint("0x20bb7")); //𠮷
    }
  • 遍历接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //字符串遍历器接口
    let str='\u{20bb7}abc';
    //ES5处理会将{20bb7}按照两个字节处理,造成前一个字符乱码
    for(let i=0;i<str.length;i++){
    console.log('es5',str[i]);
    }
    //输出结果:� � a b c

    //ES6使用for of遍历处理,可以自动处理大于0xFFFF这种情况
    for(let code of str){
    console.log('es6',code);
    }
    //输出结果:𠮷 a b c
  • 模板字符串
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    let name = "张三";
    let info = "我来自China";
    let str = `I am ${name} , ${info}`;
    console.log(str);
    }
    {
    //row对所有的斜杠进行了转义,原样输出
    console.log(String.raw`Hi\n${1+2}`);//Hi\n3
    console.log(`Hi\n${1+2}`);
    }
  • 标签模板

    标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的是函数,紧跟在后面的模板字符串就是它的参数。
    两个作用: 第一在过滤 html 字符串的时候防止 xss 攻击用这个处理,第二可以用于多语言转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    let user = {
    name:'zhangsan',
    info:'hello world'
    }
    console.log(abc`I am ${user.name},${user.info}`);
    function abc(s,v1,v2){
    console.log(s,v1,v2);
    return s+v1+v2;
    }
    }
  • 新增方法(10种)
    padStart()、padEnd() 这两个方法是 ES7 的草案中提案的,在 ES6 中使用,需要安装库npm install babel-polyfill --save-dev打补丁,处理兼容性,在项目中引入 babel-polyfill
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import 'babel-polyfill'

    {
    let str="string";
    //includes()判断是否包含某个字符
    console.log('includes',str.includes("c"));
    //startsWith()判断是否以某个字符为起始
    console.log('start',str.startsWith('str'));
    //endsWith()判断是否以某个字符为结束
    console.log('end',str.endsWith('ng'));
    }

    {
    let str="abc";
    //repeat()使字符串重复多少次
    console.log(str.repeat(3));
    }

    {
    //第一个参数指定要显示的长度,第二个参数表示如果长度不够要添加的字符
    console.log('1'.padStart(2,'0')); //01
    console.log('1'.padEnd(2,'0')); //10
    }

数值扩展

  • Number.isInteger()

    判断是否为整数

    1
    2
    3
    4
    console.log('25',Number.isInteger(25)); //true
    console.log('25.0',Number.isInteger(25.0)); //true
    console.log('25.1',Number.isInteger(25.1)); //false
    console.log('25.1',Number.isInteger('25')); //false
  • Number.isFinite()
    1
    2
    3
    4
    5
    console.log('15',Number.isFinite(15)); //true
    console.log('NaN',Number.isFinite(NaN)); //false
    console.log('1/0',Number.isFinite('true'/0)); //false
    console.log('NaN',Number.isNaN(NaN)); //true
    console.log('0',Number.isNaN(0)); //false
  • Number.isNaN()

    判断一个值是否为NaN

    1
    2
    console.log('NaN',Number.isNaN(NaN)); //true
    console.log('0',Number.isNaN(0)); //false
  • Number.MAX_SAFE_INTEGER

    数的最大上限

  • Number.MIN_SAFE_INTEGER

    数的最小下限

  • Number.isSafeInteger()

    判断给的这个数是否在有效范围内
    注意: ES6中如果一个数不在-2的53方和2的53次方之间就会不准确

    1
    2
    3
    4
    5
    {
    console.log(Number.MAX_SAFE_INTEGER,Number.MIN_SAFE_INTEGER);
    console.log('10',Number.isSafeInteger(10));//10 true
    console.log('a',Number.isSafeInteger('a'));//a false
    }
  • Math.trunc()

    取整

    1
    2
    3
    4
    {
    console.log(4.1,Math.trunc(4.1)); // 4
    console.log(4.9,Math.trunc(4.9)); // 4
    }
  • Math.sign()

    返回-1,0,1 小于0返回-1,等于0返回0,大于0返回1,注意参数为数值

    1
    2
    3
    console.log('-5',Math.sign(-5)); //-1
    console.log('0',Math.sign(0)); //0
    console.log('5',Math.sign(5)); //1
  • Math.cbrt()

    返回一个数的立方根

    1
    2
    3
    4
    {
    console.log('-1',Math.cbrt(-1)); //-1
    console.log('8',Math.cbrt(8)); //2
    }

数组扩展

  • Array.of()

    把一组数组变量转换成数组类型

    1
    2
    3
    4
    5
    6
    7
    8
    {
    let arr = Array.of(3,4,7,9,11);
    console.log('arr=',arr); //arr= [3, 4, 7, 9, 11]

    //返回空数组
    let empty=Array.of();
    console.log('empty',empty); //empty []
    }
  • Array.from()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    {
    //第一种用法,传入一个参数
    <div id="doc3" class="syy">
    <p>p1</p>
    <p>p2</p>
    <p>p3</p>
    </div>
    //获取所有的p标签
    let p=document.querySelectorAll('p');
    let pArr=Array.from(p);
    pArr.forEach(function(item){
    //textContent是ES5的一个原生方法,获取文本
    console.log(item.textContent);
    });
    //输出 p1 p2 p3

    //第二种用法传入两个参数,第二个参数类似于map映射
    console.log(Array.from([1,3,5],function(item){return item*2})); //[2, 6, 10]
    }
  • fill()

    填充,只写一个参数全部替换,三个参数情况下:第一个参数是替换内容,第二个参数是起始位置,第三个参数是结束位置

    1
    2
    3
    4
    {
    console.log('fill-7',[1,'a',undefined].fill(7)); //[7, 7, 7]
    console.log('fill,pos',['a','b','c'].fill(7,1,3));//["a", 7, 7]
    }
  • keys()

    获取索引

  • values()

    获取值,是ES7中的一个提案,存在浏览器兼容性需要加载 import ‘babel-polyfill’;

  • entries()

    既获取索引又获取值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    for(let index of ['1','c','ks'].keys()){
    console.log('keys',index); // 0 1 2
    }
    for(let value of ['1','c','ks'].values()){
    console.log('values',value); //1 c ks
    }
    for(let [index,value] of ['1','c','ks'].entries()){
    console.log('values',index,value); //0 1 1 c 2 ks
    }
    }
  • copyWithin(target,start,end)
  • target(必须):从该位置开始替换数据
  • start(可选):从该位置开始读取数据,默认为0
  • end(可选):到该位置前停止读取数据,默认等于数组长度
    1
    2
    3
    {
    console.log([1,2,3,4,5].copyWithin(0,3,4)); //[4, 2, 3, 4, 5]
    }
  • find(fn)

    查找符合条件的第一个元素,查找不到时返回undefined

  • findIndex(fn)

    查找符合条件的第一个元素的下标值,查找不到时返回-1

    1
    2
    3
    4
    {
    console.log([1,2,3,4,5,6].find(function(item){return item>3})); //4
    console.log([1,2,3,4,5,6].findIndex(function(item){return item>3})); //3
    }
  • 展开运算符…

    数组拼接使用展开运算符可以取代concat的位置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const a = ['a', 'b'];
    const b = ['c', 'd']
    const c = [...a, ...b];

    console.log(c); //["a", "b", "c", "d"]

    //使用concat
    const d = a.concat(b)
    console.log(d); //["a", "b", "c", "d"]

函数扩展

  • 参数默认值

    注意: 默认值后面不能跟没有默认值得变量,如(x, y = ‘world’,c)c没有默认值错误

    1
    2
    3
    4
    5
    6
    7
    {
    function test(x, y = 'world'){
    console.log('默认值',x,y);
    }
    test('hello'); //默认值 hello world
    test('hello','China'); //默认值 hello China
    }
  • 作用域问题
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    {
    let x='test';
    function test2(x,y=x){
    console.log('作用域',x,y);
    }
    //参数中第一个x没有值
    test2(); //作用域 undefined undefined
    test2('kill'); //作用域 kill kill

    //x为上面let定义的x
    function test3(z,y=x){
    console.log('作用域',z,y);
    }
    test3('kill'); //作用域 kill test
    }
  • rest参数

    rest参数就是在你不确定有多少个参数的时候,把你输入的一系列参数转换成了数组

    1
    2
    3
    4
    5
    6
    7
    8
    {
    function test3(...arg){
    for(let v of arg){
    console.log(v);
    }
    }
    test3(1,2,3,4,'a'); // 1 3 4 5 a
    }
  • 扩展运算符

    ES6的扩展运算符则可以看作是rest参数的逆运算。可以将数组转化为参数列表

    1
    2
    3
    4
    5
    {
    // 把一个数组拆分成离散的值
    console.log(...[1,2,4]); //1 2 4
    console.log('a',...[1,2,4]); //a 1 2 4
    }
  • 箭头函数
    1
    2
    3
    4
    5
    6
    {
    let arrow = v => v*2;
    let arrow2 = () => 5;
    console.log('arrow',arrow(3)); //6
    console.log(arrow2()); //5
    }
  • 尾调用

    尾调用存在于函数式编程概念里,函数的最后是不是是一个函数,可以用来提升性能,如果在性能优化过程中,是不断的嵌套其他函数,或者说这个函数依赖于另一个函数的操作,建议用尾调用的形式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    function tail(x){
    console.log('tail',x);
    }
    function fx(x){
    return tail(x)
    }
    fx(123) //tail 123
    }

对象扩展

  • 简洁表示法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    {
    let o=1;
    let k=2;
    //es5属性定义
    let es5={
    o:o,
    k:k
    };
    //es6属性定义
    let es6={
    o,
    k
    };
    console.log(es5,es6);

    //es5定义方法
    let es5_method={
    hello:function(){
    console.log('hello');
    }
    };
    //es6定义方法,更简洁
    let es6_method={
    hello(){
    console.log('hello');
    }
    };
    console.log(es5_method.hello(),es6_method.hello());
    }
  • 属性表达式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    // 属性表达式
    let a='b';
    //es5中key是固定的
    let es5_obj={
    a:'c',
    b:'c'
    };
    //es6中可以使用变量,这块相当于b
    let es6_obj={
    [a]:'c'
    }
    console.log(es5_obj,es6_obj);
    //输出 Object {a: "c", b: "c"} Object {b: "c"}

    }
  • Object.is()
    1
    2
    3
    4
    5
    {
    console.log('字符串',Object.is('abc','abc'),'abc'==='abc'); //字符串 true true
    // 数组是引用类型,虽然以下是两个空数组,在值上都是空,但这两个数组引用的是不同的地址,因此在严格意义上来讲,他两个不是完全相等的
    console.log('数组',Object.is([],[]),[]===[]); //数组 false false
    }
  • Object.assign()

    拷贝函数

    1
    2
    console.log('拷贝',Object.assign({a:'a'},{b:'b'}));
    //拷贝 Object {a: "a", b: "b"}
  • Object.entries()

    遍历

    1
    2
    3
    4
    let test={k:123,o:456};
    for(let [key,value] of Object.entries(test)){
    console.log([key,value]);
    }
  • Object.keys()

    对数组排序

    1
    2
    var anObj = { 100: 'a', 2: 'b', 7: 'c' };
    console.log(Object.keys(anObj).sort( (x,y) => x > y));
  • 对象扩展拷贝

    node v8.5.0版本支持

    1
    2
    3
    4
    const a = {"name": "zhangsan"}
    const b = {"age": 8, "email": "XXX@qq.com"}
    console.log({...a, ...b, "type": "儿童"});
    // {name: "zhangsan", age: 18, email: "XXX@qq.com", type: "成人"}

Promise

在JavaScript的世界中,所有代码都是单线程执行的。为了使程序不阻塞执行有了异步(I/O操作、事件操作),但是异步也有其不好之处,例如:异步回调callback回调地狱的问题,伴随着这些问题有了解决方案Promise。
面试指南

  • Promise 中 .then 的第二参数与 .catch 有什么区别?,参考:错误捕获
  • 怎么让一个函数无论promise对象成功和失败都能被调用?,参考:finally

promise的基本使用和原理

  1. 如何异常捕获(Error、reject)通过catch捕获
  2. 多个串联-链式执行的好处
  3. Promise.all和Promise.race
  4. Promise标准-状态变化(Pending —— Fulfilled/Rejected)
  5. then函数,不明文指定返回实例,返回本身的promise实例,否则返回指定的promise实例

callback方式书写

回调函数方式书写,如果异步请求多了,将会很难维护,程序看着很乱,最终会导致回调地狱。

1
2
3
4
5
6
7
8
9
10
11
12
{
let ajax = function(callback){
console.log('执行');
setTimeout(function(){
callback && callback()
});
}

ajax(function(){
console.log('执行 ajax方法');
})
}

promise方式书写

  • resove:执行下一步操作
  • reject:中断当前操作
  • then:是Promise返回的对象,执行下一个,如果有两个函数,第一个表示resolved(已成功),第二个表示rejected(已失败)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let ajax = function(){
    console.log('promise','执行');
    return new Promise(function(resolve,reject){
    setTimeout(function(){
    resolve()
    },1000);
    });
    }

    ajax().then(function(){
    console.log('promise','执行ajax方法');
    });
  • 执行两个Promise的效果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    {
    let ajax = function(){
    console.log('promise','执行');
    return new Promise(function(resolve,reject){
    setTimeout(function(){
    resolve()
    },1000);
    });
    }
    ajax()
    .then(function(){
    return new Promise(function(resolve,reject){
    setTimeout(function(){
    resolve();
    },1000);
    });
    })
    .then(function(){
    console.log('promise3','执行3');
    })
    }
  • 多个Promise实例实现串行操作

    执行a b c d 如果中间出了错误使用catch来捕获

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    {
    let ajax = function(num){
    console.log('执行4');
    return new Promise(function(resolve,reject){
    if (num > 5) {
    resolve();
    }else{
    throw new Error('出错了')
    }
    });
    }
    ajax(6).then(function(){
    console.log('log','6');
    }).catch(function(err){
    console.log('catch',err);
    });
    ajax(3).then(function(){
    console.log('log','3');
    }).catch(function(err){
    console.log('catch','err');
    });
    // 输出:
    // 执行4
    // 执行4
    // log 6
    // catch err
    }

finally

finally() 方法返回一个Promise,在promise执行结束时,无论结果是fulfilled或者是rejected,在执行then()和catch()后,都会执行finally指定的回调函数。这为指定执行完promise后,无论结果是fulfilled还是rejected都需要执行的代码提供了一种方式,避免同样的语句需要在then()和catch()中各写一次的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Promise.resolve('success').then(result => {
console.log('then: ', result)

return Promise.resolve(result);
}).catch(err => {
console.error('catch: ', err);

return Promise.reject(err);
}).finally(result => {
console.info('finally: ', result);
})

// then: success
// finally: undefined
// Promise {<resolved>: "success"}

promise并行执行

  • Promise.all()

    Promise.all是将多个Promise实例当成一个Promise实例,all方法里是一个数组,数组传进来多个Promise实例,当多个Promise实例状态发生改变的时候,这个新的Promise实例才会发生变化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    //所有图片加载完在添加到页面上
    function loadImg(src){
    return new Promise((resolve,reject) => {
    let img = document.createElement('img');
    img.src = src;
    img.onload = () => {
    resolve(img);
    }
    img.onerror = (err) => {
    reject(err)
    }
    })
    }

    function showImgs(imgs){
    imgs.forEach(function(img){
    document.body.appendChild(img)
    })
    }

    // 每个loadImg()方法都是一个Promise实例只有当三个都发生该变化,才会执行新的Promise实例既Promise.all()
    Promise.all([
    loadImg('http://www.qzfweb.com/uploads/20170512190539489.jpeg'),
    loadImg('http://www.qzfweb.com/uploads/20170225143135972.jpg'),
    loadImg('http://www.qzfweb.com/uploads/20170217225453679.jpg')
    ]).then(showImgs)

promise率先执行

  • Promise.race只要其中一个实例率先发生改变,Promise.race实例也将发生改变,其他的将不在响应。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    {
    // 有一个图片加载完就添加到页面上
    function loadImg(src){
    return new Promise((resolve,reject) => {
    let img = document.createElement('img');
    img.src = src;
    img.onload = () => {
    resolve(img);
    }
    img.onerror = (err) => {
    reject(err)
    }
    })
    }

    function showImgs(img){
    let p = document.createElement('p');
    p.appendChild(img);
    document.body.appendChild(p);
    }

    Promise.race([
    loadImg('http://www.qzfweb.com/uploads/20170512190539489.jpeg'),
    loadImg('http://www.qzfweb.com/uploads/20170225143135972.jpg'),
    loadImg('http://www.qzfweb.com/uploads/20170217225453679.jpg')
    ]).then(showImgs)
    }

错误捕获

Promise.then第二个参数与catch捕获错误的区别?

  • .then第二参数捕获错误

    .then第二个回调参数捕获错误具有就近的原则,不会影响后续then的进行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    {
    const ajax = function(){
    console.log('promise开始执行');
    return new Promise(function(resolve,reject){
    setTimeout(function(){
    reject(`There's a mistake`);
    },1000);
    });
    }

    ajax()
    .then(function(){
    console.log('then1');

    return Promise.resolve();
    }, err => {
    console.log('then1里面捕获的err: ', err);
    })
    .then(function(){
    console.log('then2');

    return Promise.reject(`There's a then mistake`);
    })
    .catch(err => {
    console.log('catch里面捕获的err: ', err);
    })

    // 输出
    // promise开始执行
    // then1里面捕获的err: There's a mistake
    // then2
    // catch里面捕获的err: There's a then mistake
    }
  • catch捕获错误

    Promise抛错具有冒泡机制,能够不断传递,可以使用catch统一处理,下面代码中不会输出then1 then2会跳过,直接执行catch处理错误

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    {
    const ajax = function(){
    console.log('promise开始执行');
    return new Promise(function(resolve,reject){
    setTimeout(function(){
    reject(`There's a mistake`);
    },1000);
    });
    }

    ajax()
    .then(function(){
    console.log('then1');

    return Promise.resolve();
    })
    .then(function(){
    console.log('then2');

    return Promise.reject(`There's a then mistake`);
    })
    .catch(err => {
    console.log('catch里面捕获的err: ', err);
    })

    // 输出
    // promise开始执行
    // catch里面捕获的err: There's a then mistake
    }
    总结: 不论是Promise还是async/await在写法上解决了异步回调的问题,但是任何写法都不会改变JS单线程、异步的本质,除非js执行引擎发生变化。

手写 Promise 代码

这是一个经典的面试问题了,我将它放了最后,不废话直接上代码,共分为 5 部份完成,实现思路如下,理清了 Promise 的实现原理,很多问题自然就迎刃而解了。

  1. 声明 MayJunPromise 类
    主要在构造函数里做一些初始化操作
  • 行 {1} 初始化一些默认值,Promise 的状态、成功时的 value、失败时的原因
  • 行 {2} onResolvedCallbacks 用于一些异步处理 const p = new Promise(resolve => { setTimeout(function(){ resolve(1) }, 5000) }),当 resolve 在 setTimeout 里时,我们调用 p.then() 此时的状态为 pending,因此我们需要一个地方来保存,此处就是用于保存 Promise resolve 时的回调函数集合
  • 行 {3} onRejectedCallbacks 与行 {2} 同理,保存 Promise reject 回调函数集合
  • 行 {4} 成功时回调,先进行状态判断是不可逆的,如果 status = pending 修改状态和成功时的 value
  • 行 {5} 失败时回调,与上面行 {4} 同理,例如 resolve(1); reject(‘err’); 第二个 reject 就无法覆盖
  • 行 {6} 自执行
  • 行 {7} 运行失败错误捕获
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 封装一个自己的 Promise
*/
class MayJunPromise {
constructor(fn) {
// {1} 初始化一些默认值
this.status = 'pending'; // 一个 promise 有且只有一个状态 (pending | fulfilled | rejected)
this.value = undefined; // 一个 JavaScript 合法值(包括 undefined,thenable,promise)
this.reason = undefined; // 是一个表明 promise 失败的原因的值
this.onResolvedCallbacks = []; // {2}
this.onRejectedCallbacks = []; // {3}

// {4} 成功回调
let resolve = value => {
if (this.status === 'pending') {
this.status = 'fulfilled'; // 终态
this.value = value; // 终值
this.onResolvedCallbacks.forEach(itemFn => {
itemFn()
});
}
}

// {5} 失败回调
let reject = reason => {
if (this.status === 'pending') { // 状态不可逆,例如 resolve(1);reject('err'); 第二个 reject 就无法覆盖
this.status = 'rejected'; // 终态
this.reason = reason; // 终值
this.onRejectedCallbacks.forEach(itemFn => itemFn());
}
}

try {
// {6} 自执行
fn(resolve, reject);
} catch(err) {
reject(err); // {7} 失败时捕获
}
}
}
  1. Then 方法
  • 一个 promise 必须提供一个 then 方法以访问其当前值、终值和据因

  • 行 {8} onFulfilled、onRejected 这两个参数可选,由于 Promise .then 是可以链式调用的,对于值穿透的场景要做判断,如果不传,则返回一个函数,也就是将上个结果进行传递

  • 行 {9} then 方法必须返回一个 promise 对象

  • 行 {10}、{11} 、{12} 也是 then 方法内实现的三种情况,相类似,次数只拿状态等于 fulfilled 进行说明

  • 行 {10.1} Promise/A+ 规范定义:要确保 onFulfilled、onRejected 在下一轮事件循环中被调用,你可以使用 setTimeout 来实现,因为我这里是在 Node.js 环境下,因此推荐使用了 setImmediate 来注册事件(因为可以避免掉 setTimeout 的延迟)

  • 行 {10.2} Promise/A+ 标准规定:如果 onFulfilled 或 onRejected 返回的是一个 x,那么它会以 [[Resolve]](promise2, x) 处理解析,我们定义解析的函数 resolveMayJunPromise,也是一个核心函数,下面进行讲解 ```javascript /**

  • 封装一个自己的 Promise

  • / class MayJunPromise { …

  • 一个 promise 必须提供一个 then 方法以访问其当前值、终值和据因

  • @param { Function } onFulfilled 可选,如果是一个函数一定是在状态为 fulfilled 后调用,并接受一个参数 value

  • @param { Function } onRejected 可选,如果是一个函数一定是在状态为 rejected 后调用,并接受一个参数 reason

  • @returns { Promise } 返回值必须为 Promise

  • / then(onFulfilled, onRejected) { // {8} 值穿透,把 then 的默认值向后传递,因为标准规定 onFulfilled、onRejected 是可选参数 // 场景:new Promise(resolve => resolve(1)).then().then(value => console.log(value)); onFulfilled = Object.prototype.toString.call(onFulfilled) === ‘[object Function]’ ? onFulfilled : function(value) {return value}; onRejected = Object.prototype.toString.call(onRejected) === ‘[object Function]’ ? onRejected : function(reason) {throw reason};

    // {9} then 方法必须返回一个 promise 对象 const promise2 = new MayJunPromise((resolve, reject) => { // {10} if (this.status === ‘fulfilled’) { // 这里的 this 会继承外层上下文绑定的 this
    // {10.1} Promise/A+ 规定:确保 onFulfilled、onRejected 在下一轮事件循环中被调用
    // 可以使用宏任务 (setTimeout、setImmediate) 或微任务(MutationObsever、process.nextTick)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    setImmediate(() => {
    try {
    // {10.2} Promise/A+ 标准规定:如果 onFulfilled 或 onRejected 返回的是一个 x,那么它会以 [[Resolve]](promise2, x) 处理解析
    const x = onFulfilled(this.value);
    // 这里定义解析 x 的函数为 resolveMayJunPromise
    resolveMayJunPromise(promise2, x, resolve, reject);
    } catch (e) {
    reject(e);
    }
    });

    }
    // {11} if (this.status === ‘rejected’) {

    1
    2
    3
    4
    5
    6
    7
    8
    setImmediate(() => {
    try {
    const x = onRejected(this.reason)
    resolveMayJunPromise(promise2, x, resolve, reject);
    } catch (e) {
    reject(e);
    }
    });

    }
    // {12} // 有些情况无法及时获取到状态,初始值仍是 pending,例如: // return new Promise(resolve => { setTimeout(function() { resolve(1) }, 5000) }) // .then(result => { console.log(result) }) if (this.status === ‘pending’) {

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    this.onResolvedCallbacks.push(() => {
    setImmediate(() => {
    try {
    const x = onFulfilled(this.value);
    resolveMayJunPromise(promise2, x, resolve, reject);
    } catch (e) {
    reject(e);
    }
    });
    });

    this.onRejectedCallbacks.push(() => {
    setImmediate(() => {
    try {
    const x = onRejected(this.reason)
    resolveMayJunPromise(promise2, x, resolve, reject);
    } catch (e) {
    reject(e);
    }
    });
    });

    } });
    return promise2; } }

  1. Promise 解决过程
    声明函数 resolveMayJunPromise(),Promise 解决过程是一个抽象的操作,在这里可以做到与系统的 Promise 或一些遵循 Promise/A+ 规范的 Promise 实现相互交互,以下代码建议跟随 Promise/A+ 规范进行阅读,规范上面也写的很清楚。

注意:在实际编码测试过程中规范 [2.3.2] 样写还是有点问题,你要根据其它的 Promise 的状态值进行判断,此处注释掉了,建议使用 [2.3.3] 也是可以兼容的 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* Promise 解决过程
* @param { Promise } promise2
* @param { any } x
* @param { Function } resolve
* @param { Function } reject
*/
function resolveMayJunPromise(promise2, x, resolve, reject){
// [2.3.1] promise 和 x 不能指向同一对象,以 TypeError 为据因拒绝执行 promise,例如:
// let p = new MayJunPromise(resolve => resolve(1))
// let p2 = p.then(() => p2); // 如果不做判断,这样将会陷入死循环
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}

// [2.3.2] 判断 x 是一个 Promise 实例,可以能使来自系统的 Promise 实例,要兼容,例如:
// new MayJunPromise(resolve => resolve(1))
// .then(() => new Promise( resolve => resolve(2)))
// 这一块发现也无需,因为 [2.3.3] 已经包含了
// if (x instanceof Promise) {
// // [2.3.2.1] 如果 x 是 pending 状态,那么保留它(递归执行这个 resolveMayJunPromise 处理程序)
// // 直到 pending 状态转为 fulfilled 或 rejected 状态
// if (x.status === 'pending') {
// x.then(y => {
// resolveMayJunPromise(promise2, y, resolve, reject);
// }, reject)
// } else if (x.status === 'fulfilled') { // [2.3.2.2] 如果 x 处于执行态,resolve 它
// x.then(resolve);
// } else if (x.status === 'rejected') { // [2.3.2.3] 如果 x 处于拒绝态,reject 它
// x.then(reject);
// }
// return;
// }

// [2.3.3] x 为对象或函数,这里可以兼容系统的 Promise
// new MayJunPromise(resolve => resolve(1))
// .then(() => new Promise( resolve => resolve(2)))
if (x != null && (x instanceof Promise || typeof x === 'object' || typeof x === 'function')) {
let called = false;
try {
// [2.3.3.1] 把 x.then 赋值给 then
// 存储了一个指向 x.then 的引用,以避免多次访问 x.then 属性,这种预防措施确保了该属性的一致性,因为其值可能在检索调用时被改变。
const then = x.then;

// [2.3.3.3] 如果 then 是函数(默认为是一个 promise),将 x 作为函数的作用域 this 调用之。
// 传递两个回调函数作为参数,第一个参数叫做 resolvePromise (成功回调) ,第二个参数叫做 rejectPromise(失败回调)
if (typeof then === 'function') {

// then.call(x, resolvePromise, rejectPromise) 等价于 x.then(resolvePromise, rejectPromise),笔者理解此时会调用到 x 即 MayJunPromise 我们自己封装的 then 方法上
then.call(x, y => { // [2.3.3.3.1] 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise, y)
if (called) return;
called = true;
resolveMayJunPromise(promise2, y, resolve, reject);
}, e => { // [2.3.3.3.2] 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise
if (called) return;
called = true;

reject(e);
});
} else {
// [2.3.3.4 ] 如果 then 不是函数,以 x 为参数执行 promise
resolve(x)
}
} catch(e) { // [2.3.3.2] 如果取 x.then 的值时抛出错误 e ,则以 e 为据因拒绝 promise
if (called) return;
called = true;

reject(e);
}
} else {
resolve(x);
}
}
  1. 验证你的 Promise 是否正确
    Promise 提供了一个测试脚本,进行正确性验证。

同时需要暴露出一个 deferred 方法。

1
2
3
4
5
6
7
8
9
10
MayJunPromise.defer = MayJunPromise.deferred = function () {
let dfd = {}
dfd.promise = new MayJunPromise((resolve,reject)=>{
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}

module.exports = MayJunPromise;
  1. catch、resolve、reject、all、race 方法实现
    Promise/A+ 规范中只提供了 then 方法,但是我们使用的 catch、Promise.all、Promise.race 等都可以在 then 方法的基础上进行实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    class MayJunPromise {
    constructor(fn){...}
    then(){...},
    /**
    * 捕获错误
    * @param { Function } onRejected
    */
    catch(onRejected) {
    return this.then(undefined, onRejected);
    }
    }

    /**
    * 仅返回成功态,即 status = fulfilled
    */
    MayJunPromise.resolve = function(value) {
    return (value instanceof Promise || value instanceof MayJunPromise) ? value // 如果是 Promise 实例直接返回
    : new MayJunPromise(resolve => resolve(value));
    }

    /**
    * 仅返回失败态,即 status = rejected
    */
    MayJunPromise.reject = function(value) {
    return (value instanceof Promise || value instanceof MayJunPromise) ? value : new MayJunPromise(reject => reject(value));
    }

    /**
    * MayJunPromise.all() 并行执行
    * @param { Array } arr
    * @returns { Array }
    */
    MayJunPromise.all = function(arr) {
    return new MayJunPromise((resolve, reject) => {
    const length = arr.length;
    let results = []; // 保存执行结果
    let count = 0; // 计数器

    for (let i=0; i<length; i++) {
    MayJunPromise.resolve(arr[i]).then(res => {
    results[i] = res;
    count++;

    if (count === length) { // 全部都变为 fulfilled 之后结束
    resolve(results);
    }
    }, err => reject(err)); // 只要有一个失败,就将失败结果返回
    }
    });
    }

    /**
    * MayJunPromise.race() 率先执行,只要一个执行完毕就返回结果;
    */
    MayJunPromise.race = function(arr) {
    return new MayJunPromise((resolve, reject) => {
    for (let i=0; i<arr.length; i++) {
    MayJunPromise.resolve(arr[i])
    .then(result => resolve(result), err => reject(err));
    }
    })
    }
  2. 并发请求控制
    Promise.all 同时将请求发出,假设我现在有上万条请求,势必会造成服务器的压力,如果我想限制在最大并发 100 该怎么做?例如,在 Chrome 浏览器中就有这样的限制,Chrome 中每次最大并发链接为 6 个,其余的链接需要等待其中任一个完成,才能得到执行,下面定义 allByLimit 方法实现类似功能。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    /**
    * 并发请求限制
    * @param { Array } arr 并发请求的数组
    * @param { Number } limit 并发限制数
    */
    MayJunPromise.allByLimit = function(arr, limit) {
    const length = arr.length;
    const requestQueue = [];
    const results = [];
    let index = 0;

    return new MayJunPromise((resolve, reject) => {
    const requestHandler = function() {
    console.log('Request start ', index);
    const request = arr[index].then(res => res, err => {
    console.log('Error', err);

    return err;
    }).then(res => {
    console.log('Number of concurrent requests', requestQueue.length)
    const count = results.push(res); // 保存所有的结果

    requestQueue.shift(); // 每完成一个就从请求队列里剔除一个

    if (count === length) { // 所有请求结束,返回结果
    resolve(results);
    } else if (count < length && index < length - 1) {
    ++index;
    requestHandler(); // 继续下一个请求
    }
    });

    if (requestQueue.push(request) < limit) {
    ++index;
    requestHandler();
    }
    };

    requestHandler()
    });
    }
    测试,定义一个 sleep 睡眠函数,模拟延迟执行
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    /**
    * 睡眠函数
    * @param { Number } ms 延迟时间|毫秒
    * @param { Boolean } flag 默认 false,若为 true 返回 reject 测试失败情况
    */
    const sleep = (ms=0, flag=false) => new Promise((resolve, reject) => setTimeout(() => {
    if (flag) {
    reject('Reject ' + ms);
    } else {
    resolve(ms);
    }
    }, ms));

    MayJunPromise.allByLimit([
    sleep(1000),
    sleep(1000),
    sleep(1000),
    sleep(5000, true),
    sleep(10000),
    ], 3).then(res => {
    console.log(res);
    })

    // 以下为运行结果

    Request start 0
    Request start 1
    Request start 2
    Number of concurrent requests 3
    Request start 3
    Number of concurrent requests 3
    Request start 4
    Number of concurrent requests 3
    Error Reject 5000
    Number of concurrent requests 2
    Number of concurrent requests 1
    [ 1000, 1000, 1000, 'Reject 5000', 10000 ]
  3. Promise reference

数据结构Set、Map

在整个的数据开发过程中,涉及到数据结构,能用Map就不使用数组,尤其是复杂的数据结构。如果对要求存储的数据有唯一性要求,推荐使用Set。

set

类似于数组,但它的一大特性就是集合中的所有元素都是唯一,没有重复。

  • 方法介绍
  • add:添加一个元素
  • delete:删除一个元素
  • clear:清空所有元素
  • has:查看集合中是否包含指定元素
  • size:相当于数组中的length

    weakset

    weakset的元素只能是对象,WeakSet中的对象是弱引用,只是把地址拿过来,没有clear属性,不能遍历

    1
    2
    3
    4
    5
    6
    7
    8
    {
    let weakList=new WeakSet();
    let arg={a:'1'};
    weakList.add(arg);
    weakList.add({b:'2'});
    console.log('weakList',weakList);
    //weakList WeakSet {Object {b: "2"}, Object {a: "1"}}
    }

    map

    Map中的key可以是任意数据类型:字符串、数组、对象等 要注意集合Set添加元素用add(),而集合Map添加元素用set()

    weakmap

    同WeakSet一样接收的key值必须是对象,没有size属性,clear方法,也是不能遍历

    1
    2
    3
    4
    5
    6
    {
    let weakmap=new WeakMap();
    let o={};
    weakmap.set(o,123);
    console.log(weakmap.get(o)); //123
    }

    map与array对比

    Map与Array横向对比增、查、改、删

    1
    2
    let map=new Map();
    let array=[];
  • 1
    2
    3
    4
    map.set('t',1);
    array.push({t:1});

    console.info('map-array',map,array); // map-array Map {"t" => 1} [Object]
  • 1
    2
    3
    4
    let map_exist=map.has('t');
    let array_exist=array.find(item=>item.a);

    console.info('map-array',map_exist,!!array_exist); // map-array true false
  • 1
    2
    3
    4
    map.set('t',2);
    array.forEach(item=>item.t?item.t=2:'');

    console.info('map-array-modify',map,array); // map-array-modify Map {"t" => 2} [Object]
  • 1
    2
    3
    4
    5
    map.delete('t');
    let index=array.findIndex(item=>item.t);
    array.splice(index,1);

    console.info('map-array-empty',map,array); // map-array-empty Map {} []

    set与array

    Set与Array增、查、改、删对比

    1
    2
    let set=new Set();
    let array=[];
  • 1
    2
    3
    4
    5
    set.add({t:1});
    array.push({t:1});

    // set-array Set {Object {t: 1}} [Object]
    console.info('set-array',set,array);
  • 1
    2
    3
    4
    5
    let set_exist=set.has({t:1}); // 没有对象引用,将一直为false
    let array_exist=array.find(item=>item.t);

    // set-array false Object {t: 1}
    console.info('set-array',set_exist,array_exist);
  • 1
    2
    3
    4
    5
    set.forEach(item=>item.t?item.t=2:'');
    array.forEach(item=>item.t?item.t=2:'');

    // set-array-modify Set {Object {t: 2}} [Object]
    console.info('set-array-modify',set,array);
  • 1
    2
    3
    4
    5
    6
    set.forEach(item=>item.t?set.delete(item):'');
    let index=array.findIndex(item=>item.t);
    array.splice(index,1);

    // set-array-empty Set {} []
    console.info('set-array-empty',set,array);

    集合map集合set对象三者对比

    Map、Set、Object三者增、查、改、删对比

    1
    2
    3
    4
    let item={t:1};
    let map=new Map();
    let set=new Set();
    let obj={};
  • 1
    2
    3
    4
    5
    6
    map.set('t',1);
    set.add(item);
    obj['t']=1;

    // map-set-obj Object {t: 1} Map {"t" => 1} Set {Object {t: 1}}
    console.info('map-set-obj',obj,map,set);
  • 1
    2
    3
    4
    5
    6
    // Object {map_exist: true, set_exist: true, obj_exist: true}
    console.info({
    map_exist:map.has('t'),
    set_exist:set.has(item),
    obj_exist:'t' in obj
    })
  • 1
    2
    3
    4
    5
    6
    map.set('t',2);
    item.t=2;
    obj['t']=2;

    // map-set-obj-modify Object {t: 2} Map {"t" => 2} Set {Object {t: 2}}
    console.info('map-set-obj-modify',obj,map,set);
  • 1
    2
    3
    4
    5
    6
    map.delete('t');
    set.delete(item);
    delete obj['t'];

    // map-set-obj-empty Object {} Map {} Set {}
    console.info('map-set-obj-empty',obj,map,set);

在 JavaScript 中浮点数运算时经常出现 0.1+0.2=0.30000000000000004 这样的问题,除了这个问题之外还有一个不容忽视的大数危机(大数处理丢失精度问题),也是近期遇到的一些问题,做下梳理同时理解下背后产生的原因和解决方案。

JavaScript 最大安全整数

IEEE 754 双精确度浮点数(Double 64 Bits)中尾数部分是用来存储整数的有效位数,为 52 位,加上省略的一位 1 可以保存的实际数值为 $[-(2^{53}-1), 2^{53}]$。

1
2
3
4
Math.pow(2, 53) // 9007199254740992

Number.MAX_SAFE_INTEGER // 最大安全整数 9007199254740991
Number.MIN_SAFE_INTEGER // 最小安全整数 -9007199254740991

只要不超过 JavaScript 中最大安全整数和最小安全整数范围都是安全的。

  • 大数处理精度丢失问题复现
  • 例一
    当你在 Chrome 的控制台或者 Node.js 运行环境里执行以下代码后会出现以下结果,What?为什么我定义的 200000436035958034 却被转义为了 200000436035958050,在了解了 JavaScript 浮点数存储原理之后,应该明白此时已经触发了 JavaScript 的最大安全整数范围。
    1
    2
    const num = 200000436035958034;
    console.log(num); // 200000436035958050
  • 例二
    以下示例通过流读取传递的数据,保存在一个字符串 data 中,因为传递的是一个 application/json 协议的数据,我们需要对 data 反序列化为一个 obj 做业务处理。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    const http = require('http');

    http.createServer((req, res) => {
    if (req.method === 'POST') {
    let data = '';
    req.on('data', chunk => {
    data += chunk;
    });

    req.on('end', () => {
    console.log('未 JSON 反序列化情况:', data);

    try {
    // 反序列化为 obj 对象,用来处理业务
    const obj = JSON.parse(data);
    console.log('经过 JSON 反序列化之后:', obj);

    res.setHeader("Content-Type", "application/json");
    res.end(data);
    } catch(e) {
    console.error(e);

    res.statusCode = 400;
    res.end("Invalid JSON");
    }
    });
    } else {
    res.end('OK');
    }
    }).listen(3000)
    运行上述程序之后在 POSTMAN 调用,200000436035958034 这个是一个大数值。
    1
    2
    3
    4
    未 JSON 反序列化情况: {
    "id": 200000436035958034
    }
    经过 JSON 反序列化之后: { id: 200000436035958050 }
    这个问题也实际遇到过,发生的方式是调用第三方接口拿到的是一个大数值的参数,结果 JSON 之后就出现了类似问题,下面做下分析。

JSON 序列化对大数值解析有什么猫腻?

先了解下 JSON 的数据格式标准,Internet Engineering Task Force 7159,简称(IETF 7159),是一种轻量级的、基于文本与语言无关的数据交互格式,源自 ECMAScript 编程语言标准.
https://www.rfc-editor.org/rfc/rfc7159.txt 访问这个地址查看协议的相关内容。
我们本节需要关注的是 “一个 JSON 的 Value 是什么呢?” 上述协议中有规定必须为 object, array, number, or string 四个数据类型,也可以是 false, null, true 这三个值。

到此,也就揭开了这个谜底,JSON 在解析时对于其它类型的编码都会被默认转换掉。对应我们这个例子中的大数值会默认编码为 number 类型,这也是造成精度丢失的真正原因。

大数运算的解决方案

  1. 常用方法转字符串
    在前后端交互中这是通常的一种方案,例如,对订单号的存储采用数值类型 Java 中的 long 类型表示的最大值为 2 的 64 次方,而 JS 中为 Number.MAX_SAFE_INTEGER (Math.pow(2, 53) - 1),显然超过 JS 中能表示的最大安全值之外就要丢失精度了,最好的解法就是将订单号由数值型转为字符串返回给前端处理,这是再和一个供应商对接过程中实实在在遇到的一个坑。
  2. 新的希望 BigInt
    Bigint 是 JavaScript 中一个新的数据类型,可以用来操作超出 Number 最大安全范围的整数。
  • 创建 BigInt 方法一
    一种方法是在数字后面加上数字 n
    1
    200000436035958034n; // 200000436035958034n
  • 创建 BigInt 方法二
    另一种方法是使用构造函数 BigInt(),还需要注意的是使用 BigInt 时最好还是使用字符串,否则还是会出现精度问题,看官方文档也提到了这块 github.com/tc39/proposal-bigint#gotchas–exceptions 称为疑难杂症
    1
    2
    3
    4
    BigInt('200000436035958034') // 200000436035958034n

    // 注意要使用字符串否则还是会被转义
    BigInt(200000436035958034) // 200000436035958048n 这不是一个正确的结果
  • 检测类型
    BigInt 是一个新的数据类型,因此它与 Number 并不是完全相等的,例如 1n 将不会全等于 1。
    1
    2
    3
    typeof 200000436035958034n // bigint

    1n === 1 // false
  • 运算
    BitInt 支持常见的运算符,但是永远不要与 Number 混合使用,请始终保持一致。
    1
    2
    3
    4
    5
    6
    // 正确
    200000436035958034n + 1n // 200000436035958035n
    // 错误
    200000436035958034n + 1
    ^
    TypeError: Cannot mix BigInt and other types, use explicit conversions
  • BigInt 转为字符串
    1
    2
    3
    4
    String(200000436035958034n) // 200000436035958034

    // 或者以下方式
    (200000436035958034n).toString() // 200000436035958034
  • 与 JSON 的冲突
    使用 JSON.parse(‘{“id”: 200000436035958034}’) 来解析会造成精度丢失问题,既然现在有了一个 BigInt 出现,是否使用以下方式就可以正常解析呢?
    1
    JSON.parse('{"id": 200000436035958034n}');
    运行以上程序之后,会得到一个 SyntaxError: Unexpected token n in JSON at position 25 错误,最麻烦的就在这里,因为 JSON 是一个更为广泛的数据协议类型,影响面非常广泛,不是轻易能够变动的。
    在 TC39 proposal-bigint 仓库中也有人提过这个问题 github.comtc39/proposal-bigint/issues/24 截至目前,该提案并未被添加到 JSON 中,因为这将破坏 JSON 的格式,很可能导致无法解析。
  • BigInt 的支持
    BigInt 提案目前已进入 Stage 4,已经在 Chrome,Node,Firefox,Babel 中发布,在 Node.js 中支持的版本为 12+。
  • BigInt 总结
    我们使用 BigInt 做一些运算是没有问题的,但是和第三方接口交互,如果对 JSON 字符串做序列化遇到一些大数问题还是会出现精度丢失,显然这是由于与 JSON 的冲突导致的,下面给出第三种方案。
  1. 第三方库
    通过一些第三方库也可以解决,但是你可能会想为什么要这么曲折呢?转成字符串大家不都开开心心的吗,但是呢,有的时候你需要对接第三方接口,取到的数据就包含这种大数的情况,且遇到那种拒不改的,业务总归要完成吧!这里介绍第三种实现方案。
    还拿我们上面大数处理精度丢失问题复现的第二个例子进行讲解,通过 json-bigint 这个库来解决。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    const http = require('http');
    const JSONbig = require('json-bigint')({ 'storeAsString': true});

    http.createServer((req, res) => {
    if (req.method === 'POST') {
    let data = '';
    req.on('data', chunk => {
    data += chunk;
    });

    req.on('end', () => {
    try {
    // 使用第三方库进行 JSON 序列化
    const obj = JSONbig.parse(data)
    console.log('经过 JSON 反序列化之后:', obj);

    res.setHeader("Content-Type", "application/json");
    res.end(data);
    } catch(e) {
    console.error(e);

    res.statusCode = 400;
    res.end("Invalid JSON");
    }
    });
    } else {
    res.end('OK');
    }
    }).listen(3000)
    再次验证会看到以下结果,这次是正确的,问题也已经完美解决了!
    1
    JSON 反序列化之后 id 值: { id: '200000436035958034' }

    总结

    本文提出了一些产生大数精度丢失的原因,同时又给出了几种解决方案,如遇到类似问题,都可参考。还是建议大家在系统设计时去遵循双精度浮点数的规范来做,在查找问题的过程中,有看到有些使用正则来匹配,个人角度还是不推荐的,一是正则本身就是一个耗时的操作,二操作起来还要查找一些匹配规律,一不小心可能会把返回结果中的所有数值都转为字符串,也是不可行的。

    Reference

    [v8.dev/features/bigint github.com/tc39/proposal-bigint en.wikipedia.org/wiki/Double-precision_floating-point_format](v8.dev/features/bigint github.com/tc39/proposal-bigint en.wikipedia.org/wiki/Double-precision_floating-point_format)

https://mp.weixin.qq.com/s/EnXEdK8F8GWpKbeGOUGqqQ整理

先修知识

以下是一些基础的,可能被你所忽略的知识,了解它很有用,因为这些基础知识在我们的下文讲解中都会应用到,如果你已掌握了它,可以跳过本节。

  1. 计算机的内部是如何存储的?一个浮点数 float a = 1 会存储成 1.0 吗?
    计算机内部都是采用二进制进行表示,即 0 1 编码组成。在计算机中是没有 1.0 的,它只认 0 1 编码。
  2. 1bit 可以存储多少个整数?8bit 可以存储多少个整数?
    N 个 bit 可以存储的整数是 2 的 N 次方个。8bit 为 2 的 8 次方($2^{8}=256$)。
  3. 了解下科学计数法,下文讲解会用到
    在日常生活中遇到一个比较的大的数字,例如全国总人口数、每秒光速等,在物理上用这些大数表达很不方便,通常可以采用科学计数法表达。

以下为 10 进制科学计数法表达式,底数为 10 ,其中 1≤|a|<10,n 为整数
$$ a*10^n $$
例如,0.1 的科学计数法表示为 $0.1 = 1 * 10^{-1}$。(一个数的 -1 次方等于该数的倒数,例如 $10^{-1}$ = $\frac{10}{1}$)
在 IEEE 754 标准中也类似,只不过它是以一个二进制数来表示,底数为 2,以下为 0.1 的二进制表达式:
$$ 1.10011001100110011(0011 无限循环) * 2^{-4} $$
4. 十进制小数如何转二进制?
十进制小数转二进制,小数部分,乘 2 取整数,若乘之后的小数部分不为 0,继续乘以 2 直到小数部分为 0 ,将取出的整数正向排序。
例如: 0.1 转二进制

1
2
3
4
5
6
7
8
9
10
0.1 * 2 = 0.2 --------------- 取整数 0,小数 0.2
0.2 * 2 = 0.4 --------------- 取整数 0,小数 0.4
0.4 * 2 = 0.8 --------------- 取整数 0,小数 0.8
0.8 * 2 = 1.6 --------------- 取整数 1,小数 0.6
0.6 * 2 = 1.2 --------------- 取整数 1,小数 0.2
0.2 * 2 = 0.4 --------------- 取整数 0,小数 0.4
0.4 * 2 = 0.8 --------------- 取整数 0,小数 0.8
0.8 * 2 = 1.6 --------------- 取整数 1,小数 0.6
0.6 * 2 = 1.2 --------------- 取整数 1,小数 0.2
...

最终 0.1 的二进制表示为 0.00110011… 后面将会 0011 无限循环,因此二进制无法精确的保存类似 0.1 这样的小数。那这样无限循环也不是办法,又该保存多少位呢?也就有了我们接下来要重点讲解的 IEEE 754 标准。

IEEE 754

IEEE 754 是 IEEE 二进制浮点数算术标准的简称,在这之前各家计算机公司的各型号计算机,有着千差万别的浮点数表示方式,这对数据交换、计算机协同工作造成了极大不便,该标准的出现则解决了这一乱象,目前已成为业界通用的浮点数运算标准。

双精确度(64位)

这里重点讲解下双精确度(64位)(JS 中使用),单精确度(32 位)同理。
在 JavaScript 中不论小数还是整数只有一种数据类型表示,这就是 Number 类型,其遵循 IEEE 754 标准,使用双精度浮点数(double)64 位(8 字节)来存储一个浮点数(所以在 JS 中 1 === 1.0)。其中能够真正决定数字精度的是尾部,即 $2^{53-1}$
64Bits 分为以下 3 个部分:

  • sign bit(S,符号):用来表示正负号,0 为 正 1 为 负(1 bit)
  • exponent(E,指数):用来表示次方数(11 bits)
  • mantissa(M,尾数):用来表示精确度 1 <= M < 2(53 bits)
    RUNOOB 图标

二进制数公式 V
根据 IEEE 754 标准,任意二进制数 V 都可用如下公式表示:
$$ V = (-1)^s * M * 2^{E} $$
符号 S
符号位的作用是什么?你可能会有此疑惑,在计算机中一切万物都以二进制表示,那么二进制中又以 0 1 存储,你可能想用负号(-)表示负数,对不起这是不支持的,为了表示负数通常把最高位当作符号位来表示,这个符号位就表示了正负数,0 表示正数(+),1 表示负数(-)。

  1. 计算机的世界中是否有减法?1 - 1 是如何实现的?
  2. 十进制数 1 的二进制为 0000 0001,-1 对应的二进制是什么?用 1000 0001 表示 -1 对吗?

尾数 M
IEEE 754 规定,在计算机内部保存 M 时,默认这个数的第一位总是 1,因此可以被舍去,只保存后面部分,这样可以节省 1 位有效数字,对于双精度 64 位浮点数,M 为 52 位,将第一位的 1 舍去,可以保存的有效数字为 52 + 1 = 53 位。
在双精确度浮点数下二进制数公式 V 演变如下所示:
$$ V = (-1)^s * M + 1 * 2^{E} $$
指数 E
E 为一个无符号整数,在双精度浮点数中 E 为 11 位,取值范围为 $2^{11} = 2048$,即表示的范围为 0 ~ 2047。
中间值: 由于科学计数法中的 E 是可以出现负数的,IEEE 754 标准规定指数偏移值的固定值为 $2^{e-1}-1$,以双精度浮点数为例:$2^{11-1}-1=1023$,这个固定值也可以理解为中间值。同理单精度浮点数为 $2^{8-1}-1=127$。
正负范围: 双精确度 64 位中间值为 1023,负数为 [0, 1022] 正数为 [1024, 2047]。
双精确度浮点数下二进制数公式 V 最终演变如下所示:

$$ V = (-1)^s * M + 1 * 2^{E + 1023} $$

0.1 在 IEEE 754 标准中是如何存储的?

  1. “0.1” 转为二进制
    不知道怎么转换的,参考上面 先修知识 的 十进制小数转二进制
    1
    0.000110011001100110011(0011) // 0011 将会无限循环
  2. 二进制浮点数的科学计数法表示
    任何一个数都可以用科学计数法表示,0.1 的二进制科学计数法表示如下所示:
    $$ 1.10011001100110011(0011 无限循环) * 2^{-4} $$
    以上结果类似于十进制科学计数法表示:
    $$ 0.0001234567 = 1.234567 * 10^{-4} $$
  3. IEEE 754 存储
  • 0.1 的二进制表示如下所示:
    1
    $$ 1.1001100110011001100110011001100110011001100110011001*2^{-4} $$
  • 符号位
    由于 0.1 为整数,所以符号位 S = 0
  • 指数位
    E = -4,实际存储为 -4 + 1023 = 1019,二进制为 1111111011,E 为 11 位,最终为 01111111011
  • 尾数位
    在 IEEE 754 中,循环位就不能在无限循环下去了,在双精确度 64 位下最多存储的有效整数位数为 52 位,会采用 就近舍入(round to nearest)模式(进一舍零) 进行存储
    1
    2
    3
    11001100110011001100110011001100110011001100110011001 // M 舍去首位的 1,得到如下
    1001100110011001100110011001100110011001100110011001 // 0 舍 1 入,得到如下
    1001100110011001100110011001100110011001100110011010 // 最终存储
  • 最终存储结果
    1
    0    01111111011    1001100110011001100110011001100110011001100110011010

    binaryconvert.com/convert_double.html?decimal=048046049

    0.1 + 0.2 等于多少?

    上面我们讲解了浮点数 0.1 采用 IEEE 754 标准的存储过程,0.2 也同理,可以自己推理下,0.1、0.2 对应的二进制分别如下所示:
    1
    2
    3
    S  E            M
    0 01111111011 1001100110011001100110011001100110011001100110011010 // 0.1
    0 01111111100 1001100110011001100110011001100110011001100110011010 // 0.2

    浮点数运算三步骤

    对阶
    求和
    规格化

    对阶

    浮点数加减首先要判断两数的指数位是否相同(小数点位置是否对齐),若两数指数位不同,需要对阶保证指数位相同。
    对阶时遵守小阶向大阶看齐原则,尾数向右移位,每移动一位,指数位加 1 直到指数位相同,即完成对阶。
    本示例,0.1 的阶码为 -4 小于 0.2 的阶码 -3,故对 0.1 做移码操作
    1
    2
    3
    4
    5
    6
    7
    8
    // 0.1 移动之前
    0 01111111011 1001100110011001100110011001100110011001100110011010

    // 0.1 右移 1 位之后尾数最高位空出一位,(0 舍 1 入,此处舍去末尾 0)
    0 01111111100 100110011001100110011001100110011001100110011001101(0)

    // 0.1 右移 1 位完成
    0 01111111100 1100110011001100110011001100110011001100110011001101
    尾数右移 1 位之后最高位空出来了,如何填补呢?涉及两个概念:
  • 逻辑右移:最高位永远补 0
  • 算术右移:不改变最高位值,是 1 补 1,是 0 补 0,尾数部分我们是有隐藏掉最高位是 1 的,不明白的再看看上面 3.3 尾数位 有讲解舍去 M 位 1。

    尾数求和

    两个尾数直接求和
    1
    2
    3
      0  01111111100   1100110011001100110011001100110011001100110011001101 // 0.1 
    + 0 01111111100 1001100110011001100110011001100110011001100110011010 // 0.2
    = 0 01111111100 100110011001100110011001100110011001100110011001100111 // 产生进位,待处理
    或者以下方式:
    1
    2
    3
      0.1100110011001100110011001100110011001100110011001101
    + 1.1001100110011001100110011001100110011001100110011010
    10.0110011001100110011001100110011001100110011001100111

    规格化和舍入

    由于产生进位,阶码需要 + 1,对应的十进制为 1021,此时阶码为 1021 - 1023(64 位中间值)= -2,此时符号位、指数位如下所示:
    1
    2
      S  E
    = 0 01111111101
    尾部进位 2 位,去除最高位默认的 1,因最低位为 1 需进行舍入操作(在二进制中是以 0 结尾的),舍入的方法就是在最低有效位上加 1,若为 0 则直接舍去,若为 1 继续加 1
    1
    2
    3
    4
      100110011001100110011001100110011001100110011001100111 // + 1
    = 00110011001100110011001100110011001100110011001101000 // 去除最高位默认的 1
    = 00110011001100110011001100110011001100110011001101000 // 最后一位 0 舍去
    = 0011001100110011001100110011001100110011001100110100 // 尾数最后结果
    IEEE 754 中最终存储如下:
    1
    0  01111111101 0011001100110011001100110011001100110011001100110100
    最高位为 1,得到的二进制数如下所示:
    1
    2^-2 * 1.0011001100110011001100110011001100110011001100110100
    转换为十进制如下所示:
    1
    0.30000000000000004

    只有 JavaScript 中存在吗?

    这显然不是的,这在大多数语言中基本上都会存在此问题(大都是基于 IEEE 754 标准),让我们看下 0.1 + 0.2 在一些常用语言中的运算结果。
  • JavaScript
    推荐一个用于任意精度十进制和非十进制算术的 JavaScript 库 github.com/MikeMcl/bignumber.js
    1
    2
    3
    4
    5
    6
    7
    8
    console.log(.1 + .2); // 0.30000000000000004

    // bignumber.js 解决方案
    const BigNumber = require('bignumber.js');
    const x = new BigNumber(0.1);
    const y = 0.2

    console.log(parseFloat(x.plus(y)));
  • Python
    Python2 的 print 语句会将 0.30000000000000004 转换为字符串并将其缩短为 “0.3”,可以使用 print(repr(.1 + .2)) 获取所需要的浮点数运算结果。这一问题在 Python3 中已修复。
    1
    2
    3
    4
    5
    6
    # Python2
    print(.1 + .2) # 0.3
    print(repr(.1 + .2)) # 0.30000000000000004

    # Python3
    print(.1 + .2) # 0.30000000000000004
  • Java
    Java 中使用了 BigDecimal 类内置了对任意精度数字的支持。
    1
    2
    3
    System.out.println(.1 + .2); // 0.30000000000000004

    System.out.println(.1F + .2F); // 0.3

    总结

    最后做个总结,由于计算机底层存储都是基于二进制的,需要事先由十进制转换为二进制存储与运算,这整个转换过程中,类似于 0.1、0.2 这样的数是无穷尽的,无法用二进制数精确表示。JavaScript 采用的是 IEEE 754 双精确度标准,能够有效存储的位数为 52 位,所以就需要做舍入操作,这无可避免的会引起精度丢失。另外我们在 0.1 与 0.2 相加做对阶、求和、舍入过程中也会产生精度的丢失。

    Reference

  • 0.30000000000000004.com/
  • www.cnblogs.com/yilang/p/11277201.html
  • www.ruanyifeng.com/blog/2010/06/ieee_floating-point_representation.html

正则表达式

模式修饰符参数

  • i:忽略大小写
  • g:全局匹配
  • m:多行匹配
  • /hello/:两个反斜杠是正则表达式的字面量表示法

    两个测试方法

  • test
    1
    2
    const test = new RegExp('hello world', 'ig');
    console.log(test.test('hello world')); // true
  • exec

    返回的是数组,有就返回数组的值,没有返回为null。

    1
    2
    const test = new RegExp('hello world', 'ig');
    console.log(test.exec('hello')); // null

    4个正则表达式方法

  • match(pattern)

    将所有匹配的字符串组合成数组返回

    1
    2
    3
    4
    const pattern=/Box/ig;
    const str="This is a Box! The is a box!";

    console.log(str.match(pattern));
  • search(pattern)

    返回字符串中pattern开始位置,忽略全局匹配

    1
    2
    3
    4
    const pattern=/Box/i;    //
    const str="This is a Box! The is a box!";

    console.log(str.search(pattern)); // 10
  • replace(pattern)

    替换匹配到的字符串

    1
    2
    3
    4
    const pattern=/Box/ig;
    const str="This is a Box! The is a box!";

    console.log(str.replace(pattern,'Tom'));
  • split(pattern)

    返回字符串指定pattern拆分数组

    1
    2
    3
    4
    5
    6
    7
    const pattern = / /ig;    //空格
    const str = "This is a Box! The is a box!";

    console.log(str.split(pattern)); //以空格进行分割,返回的是数组

    // 输出结果
    // [ 'This', 'is', 'a', 'Box!', 'The', 'is', 'a', 'box!' ]

    匹配模式

  • \w表示a-zA-Z0-9_
  • 锚元字符匹配(^ $) ^强制首匹配$强制尾匹配,并且只匹配一个
    1
    2
    3
    const pattern=/^[a-z]oogle\d$/;
    const str="aoogle2";
    console.log(pattern.test(str)); // true
    注意: ^符号在[]里面表示 非 在外边表示强制首匹配,并且只匹配一个 要想匹配多个值,使用+
  • \b表示到达边界
  • |表示匹配或选择模式
    1
    2
    3
    const pattern=/baidu|google|bing/; //匹配或选择其中某个字符,不是相等,包含的意思
    const str = "baidu a google";
    console.log(pattern.test(str)); //返回true

    常用正则表达式

  • 检查邮政编码
    1
    2
    3
    4
    const pattern = /^[1-9]{1}[0-9]{5}$/;
    const str = "122534"; //共6位数,第一位不能为0

    console.log(pattern.test(str)); // true
  • 压缩包后缀名
    \w等于a-zA-Z0-9_ 使用^限定从首字母匹配 .是特殊符号需要\n进行转义 |选择符必须使用()进行分组
    1
    2
    3
    const pattern = /^[\w]+\.(zip|gz|rar)$/;  
    const str="a12_.zip"; //文件名 字母_数字.zip,gz,rar
    console.log(pattern.test(str)); // true
  • 删除多余空格

    方法一:使用replace只匹配一个,所以使用+匹配多个

    1
    2
    3
    4
    5
    6
    7
    var pattern=/^\s+/; 
    var str=" google ";
    var result=str.replace(pattern,'');
    pattern=/\s+$/;
    result=result.replace(pattern,'');

    console.log('|'+result+'|'); // |google|

    方法二:(.+)贪婪模式,使用惰性模式,后面的空格不让匹配

    1
    2
    3
    4
    5
    var pattern=/^\s+(.+?)\s+$/;
    var str=" google ";
    var result=pattern.exec(str,'')[1];

    console.log('|'+result+'|');

    方法三:(.+)贪婪模式,改为惰性模式,使用分组模式,只取匹配的内容

    1
    2
    3
    4
    5
    var pattern=/^\s+(.+?)\s+$/;
    var str=" google ";
    var result=str.replace(pattern,'$1'); //使用分组模式

    console.log('|'+result+'|'); // |google|
  • 简单邮箱验证
    1
    2
    3
    var pattern=/^([\w\.\_]+)@([\w\_]+)\.([a-zA-Z]){2,4}$/;
    var str="qzfweb@gmail.com";
    console.log(pattern.test(str)); // true

原型概念

我们所创建的每个原型都有一个(原型)属性,这个属性是一个对象。
原型模式的执行流程
1.先查找构造函数实例里的属性或方法,如果有,立刻返回
2.如果构造函数实例里没有,则去它的原型对象里找,如果有,就返回

构造函数实例属性方法

1
2
3
4
5
6
7
8
9
10
11
function Box(name,age){
this.name=name; //实例属性
this.age=age;
this.run=function(){ //实例方法
return this.name+this.age+"运行中.....";
};
}

var box1=new Box('zhangsan',20);
var box2=new Box('lisi',18);
alert(box1.run==box2.run); //false

构建原型属性方法

构造函数体内什么都没有,这里如果有,叫作实例属性,实例方法

1
2
3
4
5
6
7
function Box(){}

Box.prototype.name='lee'; //原型属性
Box.prototype.age=23;
Box.prototype.run=function(){//原型方法
return this.name+this.age+"运行中......";
};

如果是实例化方法,不同的实例化,他们的地址是不一样的,是唯一的,如果是原型方法,那么他们地址是共享的,大家都一样,看以下示例box1.run==box2.run

1
2
3
var box1=new Box();
var box2=new Box();
alert(box1.run==box2.run); // true

这个属性是一个对象,访问不到

1
alert(box1.prototype);

这个属性是一个指针指向prototype原型对象

1
alert(box1.__proto__);

构造属性可以获取构造函数本身,作用是被原型指针定位,然后得到构造函数本身,其实就是对象实例对应的原型对象的作用

1
alert(box1.constructor);

原型字面量创建对象

使用构造函数创建原型对象和使用字面量创建对象在使用上基本相同,但还是有一些区别,字面量创建的方式使用constructor属性不会指向实例,而会指向Object,构造函数则相反。

字面量创建对象

1
function Box(){}

使用字面量的方式创建原型对象,这里{}就是对象(Object),new Object就相当于{}

1
2
3
4
5
6
7
8
9
10
Box.prototype={
name:'lee',
age:20,
run:function(){
return this.name+this.age+"运行中.......";
}
};

var box1=new Box();
alert(box1.constructor);//返回function Object(){}对象

构造函数创建对象

1
2
3
4
5
6
7
8
9
10
function Box(name,age){
this.name=name;
this.age=age;
this.run=function(){
return this.name+this.age+"运行中....";
};
}

var box1=new Box('zhangsan',20);
alert(box1.constructor); //返回的是function Box(){}

原型对象的重写需要注意的问题

1.重写原型对象之后,不会保存之前原型的任何信息
2.把原来的原型对象和构造函数对象实例之间的关系切断了

1
2
3
4
5
6
7
8
9
10
function Box(){}

Box.prototype={
constructor:Box,//让它强制指向Box
name:'lee',
age:20,
run:function(){
return this.name+this.age+"运行中.......";
}
};

重写原型

1
2
3
4
5
Box.prototype={
age:21
}
var box1=new Box();
alert(box1.name); // undefined

可以使用addstring()方法向原型添加内容,这样可以避免原型重写

1
2
3
4
5
6
String.prototype.addstring=function(){
return this+',被添加了!';
};

var box1=new Box();
alert(box1.name.addstring()); // lee,被添加了!

原型的实际应用

  • 先找到入口函数window.$
  • 根据入口函数找到构造函数new ...
  • 根据构造函数找到原型的定义zepto.Z.prototype

    实例

    以下实例中通过Jquery或Zepto操作dom元素,例如css方法、text方法都是操作的原型上的的方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <title>Zepto</title>
    <!--<script src="https://cdn.bootcss.com/zepto/1.1.6/zepto.js"></script>
    <script src="zepto.js"></script>
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script> -->
    <script src="jquery.js"></script>
    </head>
    <body>
    <div> 这是一个测试 </div>
    <div> 这是一个测试2 </div>
    <script>
    var div = $('div'); // 得到一个实例
    div.css('color', 'red'); // 原型方法css
    alert(div.text()); // 原型方法text
    </script>
    </body>
    </html>

    zepto中原型的应用

    以下实例也是取了关于原型部分的源码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    var Zepto = (function() {
    var $, zepto={}, emptyArray=[], slice=emptyArray.slice, document=window.document;

    // 构造函数
    zepto.Z = function(dom, selector) {
    dom = dom || []
    dom.__proto__ = $.fn
    dom.selector = selector || '';

    return dom
    }

    zepto.init = function(selector) {
    var dom;

    // 如果选择器不存在,返回一个空的Zepto集合
    if (!selector) return zepto.Z();

    // 优化字符串选择器
    if (typeof selector === 'string') {
    selector = selector.trim();

    // 还有一系列的判断此处忽略,进行简化 ...
    dom = slice.call(document.querySelectorAll(selector))
    } else {
    // 更多可以去查看源码 ...
    }

    return zepto.Z(dom, selector)
    }

    $ = function(selector) {
    return zepto.init(selector);
    }

    $.fn = {
    text: function() {
    return (0 in this ? this[0].textContent : null)
    },
    css: function() {
    alert('css');
    }
    }

    // $.fn赋值给构造函数的原型
    zepto.Z.prototype = $.fn;

    return $;
    })()

    window.Zepto = Zepto;
    window.$ === undefined && (window.$ = Zepto); // 如果window.$不存在,赋予window.$为Zepto;

    jquery中原型应用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    (function(global, factory) {
    // 浏览器环境、Node环境判断
    if ( typeof module === "object" && typeof module.exports === "object" ) {
    // Node环境处理,这里不做阐述,具体参考源码
    // factory(global, true);
    } else {
    // 进入浏览器环境
    factory(global);
    }
    })(typeof window !== "undefined" ? window : this, function(window, noGlobal) {
    var Jquery = function(selector) {
    return new jQuery.fn.init(selector);
    }

    Jquery.fn = Jquery.prototype = {
    css: function() {
    alert('css');
    },
    text: function() {
    return (0 in this ? this[0].textContent : null);
    }
    };

    // 定义构造函数
    var init = Jquery.fn.init = function(selector) {
    var slice = Array.prototype.slice;
    var dom = slice.call(document.querySelectorAll(selector));

    var i, len=dom ? dom.length : 0;
    for (i=0; i<len; i++) {
    this[i] = dom[i];
    }
    this.length = len;
    this.selector = selector || '';
    }

    // 构造函数原型赋值
    init.prototype = Jquery.fn;

    if ( !noGlobal ) { // 判断是否为浏览器环境
    window.jQuery = window.$ = Jquery;
    }
    })

    原型的扩展

1.插件扩展在$.fn之上,并不是扩展在构造函数的原型
2.对外开放的只有$,构造函数并没有开放
实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<body>
<script>
// 插件扩展:获取tagName
$.fn.getTagName = function() {
return (0 in this ? this[0].tagName : '');
}
</script>
<div> 这是一个测试 </div>
<div> 这是一个测试2 </div>
<script>
var div = $('div'); // 得到一个实例
alert(div.getTagName()); // 封装的插件
</script>
</body>