GraphQL 概览

GraphQL

GraphQL 是一个用于 API 的查询语言,使用基于类型系统来执行查询的服务端运行时,并且它不和任何特定数据库或存储引擎绑定,依靠你现有的代码和数据支撑。

一个 GraphQL 服务通过定义类型和类型上的字段创建,并为每个类型上的字段提供解析函数。

为什么使用 GraphQL

传统的 REST 标准在使用中有一些问题:

  • 获取比较复杂的对象需要向服务器进行多次请求
  • REST 通常在服务端就决定了数据的结构,这导致你获得的数据很多是你不要的,即冗余数据颇多
  • 服务器返回的数据不可预测,你需要付出额外的工作来准确的理解你从服务器获得数据

而通过 GraphQL 你可以从服务器获取你期望的数据,并且清楚的知道你获得的数据是什么样子的。

查询和变更

字段 Fields

字段可以是标量或对象,当子段位对象类型时,你可以对其进行次级选择。GraphQL 查询能够遍历相关对象及其字段,是的客户端可以一次请求查询大量相关数据。

参数 Arguments

在 GraphQL 中,每一个字段和嵌套对象都能有自己的一组参数,从而使得 GraphQL 可以完美替代多次 API 请求。

参数可以是多种不同的类型。GraphQL 自带一套默认类型,但是你可以声明一套自己的定制类型,你要能序列化成的你需要的格式即可。

1
2
3
4
5
6
{
queryFunction(arg: arg) {
field,
...
}
}

别名 Aliases

为字段设置别名,可以使你能够通过不同参数来查询相同的字段。

片段 Fragments

片段是 GraphQL 中的可复用单元,它能够组织一组字段,在需要的地方被引入。

片段也可以访问查询或变更中声明的变量。

操作名称 Operation name

1
query/mutation/subscription operation_name {}

操作类型可以是 query/mutation/subscription,描述你打算做什么类型的操作。操作类型是必须的,除非使用查询简写语法 - 这种情况下,你无法为操作提供名称或变量定义。

操作名称是你的操作的有意义和明确的名称,它仅在有多个操作的文档中是必须的。但是操作名称在调试和服务端日志记录中非常有用,可以帮助快速定位到问题发生的位置。

变量 Variables

1
2
3
4
5
query query_function($variable_name: variable_type) {
obj(variable: $variable_name) {
...
}
}

所有声明的变量都必须是标量、枚举类型或输入对象类型。所以如果想要传递一个复杂对象到一个字段上,你必须知道服务器上其匹配的类型。

变量定义可以是可选的或者必要的。但是如果你传递变量的字段要求非空参数,那变量一定是必要的。

可以通过在查询中对类型定义后面附带默认值的方式,将默认值赋给变量。

1
2
3
4
5
query query_function($variable_name: variable_type = "default_value") {
obj(variable: $variable_name) {
...
}
}

指令 Directives

指令可以动态的改变我们的查询结构。指令可以附着在字段或者字段包含的字段上,然后以任何服务端期待的方式来改变查询的执行。

  • @include(if: Boolean) 仅在参数为 true 时,包含此字段
  • @skip(if: Boolean) 如果参数为 true ,跳过此字段

变更 Mutations

技术上而言,任何查询都能够被实现为导致数据写入的操作,然而,一个规范任何导致写入的操作都应该显式通过变更来发送也是必要的。

一个变更可能包含多个字段,查询是并行执行,变更是线性执行。

内联片段 Inline Fragments

如果你查询的字段返回的是接口或者联合类型,那么你可能需要使用内联片段来取出下层具体类型的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
query numberOFAnimals($sp:Species!) {
animals(species: $sp) {
number,
// 仅在 animals 返回为 Mammal 时执行
... on Mammal {
isRare
}
// 仅在 animals 返回为 Human 时执行
... on Human {
sex
}
}
}

具名片段也可以用于同样的情况,因为具名片段总是附带了一个类型。

原字段

1
2
// 获得那个位置的对象类型名称
__typename

Schema 和类型

类型系统

GraphQL 查询语言基本上就是关于选择对象上的字段。因此一个 GraphQL 查询的结构和结果非常相似,这使我们能预测服务器会返回什么样的结果。

每一个 GraphQL 服务都会定义一套类型,用来描述你能从服务器能够查询到的数据。每当查询到来,服务器就会根据 schema 验证并执行查询。

类型语言

GraphQL 可以用任何语言编写,因为它并不依赖于任何特定语言的句法来与 GrapahQL schema 沟通,它定义了称之为 GraphQL schema language 的简单语言。

对象类型和字段 Object Types and Fields

GraphQL schema 中最基本的组件是对象类型,它就表示你可以从服务上获取到什么类型的对象,以及这个对象有什么字段。

1
2
3
4
type Object_Name {
field,
...
}

参数 Arguments

GraphQL 对象类型上的每个字段都可能有零个或多个参数,并且所有参数都必须是具名的。参数可能是必选的或可选的,如果参数可选的,可以给它定义默认值。

查询和变更类型 The Query and Mutation Types

每一个 GraphQL 服务中都有一个 query 类型,可能有一个 mutation 类型,它们定义了每一个 GraphQL 查询的入口。

1
2
3
4
schema {
query: Query,
mutation: Mutation
}

标量类型 Scalar Types

一个对象类型有自己的名字和字段,而某些时候,这些字段必然会解析到具体数据,这就是标量,表示对应 GraphQL 查询的叶子节点。

它们包括 :

  • Int : 有符号 32 位证书
  • Float :有符号双精度浮点值
  • String : UTF-8 字符序列
  • Booleantruefalse
  • ID : ID 标量类型表示一个唯一标识符,通常用以重新获取对象或者作为缓存中的键

你可以自定义标量类型,但需要保证其能够序列化、反序列化和验证。

枚举类型 Enumeration Types

枚举类型是一种特殊的标量,它限制在一个特殊的可选值集合内。

列表和非空 List and Non-Null

类型修饰符 ! 可将字段标注为非空,即表示这个字段总是会返回一个非空值。亦可用于修饰定义字段上的参数。

1
2
3
type Object_Name {
field_1: Type!
}

使用 [] 标记一个类型为 List ,表示这个字段会返回这个类型的数组。

1
2
3
query query_name {
field_list: [String],
}

接口 Interface

GraphQL 支持接口,接口是一个抽象类型,它包含某些字段,而想要实现这个接口的对象必须包含这些子段。

1
2
3
4
5
6
7
8
9
10
11
interface Human {
name: String!
age: Int!
}

type Man implements Human {
name: String!
age: Int!
height: Float!

}

联合类型 Union Type

联合类型和接口非常相似,但是不指定类型之间的任何共同字段。

1
union union_type = type_1 | type_2 | ...

联合类型的成员需要是具体对象类型。

输入类型 Input Types

使用输入类型可以将复杂的对象传递给字段,这在变更中特别有用。

1
2
3
4
input input_type {
field,
....
}

输入对象类型上的字段本身也可以只带输入对象类型,但是注意不能混淆输入和输出类型,输入对象类型上的字段不能拥有参数。

验证

通过使用类型系统,可以预判一个查询是否有效。这使得服务器和客户端可以在无效查询创建时就有效的通知开发者,而不用依赖运行时检查。

以下是指的注意的基本原则

  • 片段不能引用自身或创造回环
  • 只能查询给定类型上的字段
  • 如果查询返回的不是标量或枚举类型,就需要指明想要从字段中获取的数据

执行

一个 GraphQL 查询在被验证之后,GraphQL 服务器将会将其执行,并返回与请求的结构相对应的结果,该结果通常会是 JSON 格式。

每个类型的每个字段都由一个 resolver 函数支持,该函数由 GraphQL 服务器开发人员提供 - 当一个字段被执行时,相应的 resolver 函数被调用以产生下一个值。如果字段产生标量值,则执行完成。如果一个字段产生一个对象,则该查询将继续执行该对象对应字段的解析器,直到生成标量值。GraphQL 查询始终以标量值结束。

根字段 & 解析器

每一个 GraphQL 服务端应用的顶层,必有一个代表着所有进入 GraphQL API 可能的入口点,称之为 Root 类型或 Query 类型。

1
2
3
4
5
6
7
8
9
10
11
// `obj` 代表上一级对象,如果字段是根节点查询类型通常不会被使用
// `args` 提供在 GraphQL 查询中传入的参数
// `context` 会提供给所有解析器,持有重要的上下文信息
// `info` 保存与当前查询相关的字段的特定信息以及 `schema` 详细信息的值
Query {
query_name(obj, args, context, info) {
return context.db.query(...).then((return_data) => {
return return_data;
});
}
}

当每个字段被解析后,结果被放到键值映射中,字段名称作为键值映射的键,解析器的值作为键值映射的值,这个过程从查询字段的底部叶子节点开始返回,直到根 Query 类型的起始节点。最后合并成为能够镜像到原始查询结果的结果,让后将其发送到请求的客户端。

异步解析器

context 提供了一个数据库访问对象,用来通过查询中传递的参数查询数据,因为从数据库拉取数据是一个异步操作,它会返回一个 Promise 对象,当数据库查询返回结果,就能构造并返回查询结果。

只有解析器能够感知 Promise 的进度,GraphQL 查询只关注一个结果是否返回,在执行期间如果异步操作没有完成,则 GraphQL 会一直等待下去。

内省

GraphQL 通过内省系统使得我们能够直到 GraphQL Schema 支持哪些查询。

我们可以通过 __schema 字段来向 GraphQL 询问哪些类型是可用的。一个查询的根类型总是有 __schema 这个字段。

1
2
3
4
5
{
__schema {
...
}
}

检验特定类型

1
2
3
{
__type() {}
}

JavaScript 正则表达式

JavaScript 正则表达式

JavaScript 正则表达式

什么是正则表达式?

正则表达式是一些用来匹配和处理文本的字符串。

语法

可以使用字面量、构造器和工厂标记来创建正则表达式

1
2
3
/partern/flags
new RegExp(pattern[, flags])
RegExp(pattern[, flags])

其中 flags

  • g 全局匹配
  • i 不区分大小写
  • m 多行匹配
  • u Unicode 字符匹配

正则表达式中的特殊字符

字符类

  • . 匹配除行终止符(\n,\r,\u2028 or \u2029)之外的任何单个字符,若在字符集中则仅仅只匹配.
  • \d 匹配任何一个数字字符,相当于 [0-9]
  • \D 匹配任何一个非数字字符,相当于 [^0-9]
  • \w 匹配任何一个字母数字字符,包括下划线,相当于 [A-Za-z0-9_]
  • \W 匹配任何一个非字母数字字符或非下划线字符,相当于 [^A-Za-z0-9_]
  • \s 匹配任何单个空格字符,相当于 [\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]
  • \S 匹配除了空格外的单个字符,相当于 [^ \f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]
  • \t 匹配一个横向制表符
  • \r 匹配一个回车
  • \n 匹配一个换行符
  • \v 匹配一个垂直制表符
  • \f 匹配一个换页符
  • [\b] 匹配一个删除符
  • \0 匹配 NUL 字符
  • \cX 匹配控制字符

字符集

  • [-a-z-] 匹配方括号中字符中的任何一个,可以使用连字符 - 来指定字符范围,但如果连字符是方括号中的第一个或最后一个字符,则作为普通字符包含在字符集中。也可以在字符集中包含字符类。
  • [^-A-Z-] 匹配除括号中包含的内容外的任何内容

可选字符

  • x|y 匹配 x 或 y 中的一个

边界

  • ^ 匹配开头,如果设置了多行匹配标记,则在换行符后立即匹配
  • $ 匹配结尾
  • \b 匹配单词边界,实际上它匹配的是一个能够构成单词的字符(\w)和一个不能构成单词的字符(\W)之间的位置
  • \B 匹配非单词边界

分组和反向引用

  • (x) 匹配 x 并记录匹配结果,称为捕获分组
  • \n 引用之前的第 n 个 表达式
  • (?:x) 匹配 x 但不记录匹配结果,称为非捕获分组

量词

  • x* 匹配 x 0次或更多次
  • x+ 匹配 x 1次或更多次
  • x? 匹配 x 0次或1次
  • x{n} 匹配 x n次
  • x{n,} 匹配 x 至少n次
  • x{n,m} 匹配 x 至少n次至多m次
  • ? 非贪婪匹配,在量词后加上 ? 表示量词的非贪婪模式,匹配尽可能少的字符

回溯引用

  • x(?=y) 前向查找,仅当 x 后跟 y 时,才匹配 x

    image-20211110161954405
  • (?<=y)x 后向查找,仅当 x 前面有 y 时,才匹配 x

    image-20211110162115215

常用的方法

  • exec()

    在指定的字符串上执行匹配搜索,返回结果数组或 null

    1
    2
    3
    4
    5
    6
    7
    8
    const str = "My ip address is 255.198.99.101 and your ip address 199.123.44.88";
    const reg = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;

    // Found 255.198.99.101, from index 17, last match from index 31
    // Found 199.123.44.88, from index 52, last match from index 65
    while((res = reg.exec(str)) !== null) {
    console.log(`Found ${res[0]}, from index ${res['index']}, last match from index ${reg.lastIndex}`);
    }
  • test()

    在指定的字符串和一个正则表达式之间执行匹配搜索,返回 truefalse

  • match()

    对给定的字符串进行匹配并返回匹配结果

    1
    2
    3
    4
    5
    const str = "My ip address is 255.198.99.101 and your ip address 199.123.44.88";
    const reg = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;

    // [ '255.198.99.101', '199.123.44.88' ]
    console.log(str.match(reg));
  • replace()

    使用给定的字符串替换匹配到的结果

  • search()

    在给定的字符串中搜索匹配,并返回首次匹配结果的索引

  • split()

    通过将字符串分隔为子字符串,将字符串拆分为数组。

常用正则表达式

  • 匹配中文字符

    1
    /^[\u4e00-\u9fa5]{0,}$/
  • 匹配双字节字符

    1
    /[^\x00-\xff]/
  • 匹配千分位

    1
    /\B(?=(\b{3})+(?!\d))/
  • 匹配两位小数

    1
    /^([1-9][0-9]*)(\.[0-9]{2})?$/
  • 匹配中国固定电话号码

    最开始的一位一定是 0,接着是 2,3,4位数字组成的区号,然后是7位或8位的电话号码,其中首位不为1

    1
    /\(?0[1-9]\d{1,3}\)?[ -]?[2-9]\d{2,3}[ -]?\d{4}/
  • 匹配统一社会信用代码

    统一社会信用代码由18位数字或者大写字母组成,但是字母不包括 I、O、Z、S、V

    一共由五部分组成:

    第一部分:登记管理部门代码1位 (数字或大写英文字母)

    第二部分:机构类别代码1位 (数字或大写英文字母)

    第三部分:登记管理机关行政区划码6位 (数字)

    第四部分:主体标识码(组织机构代码)9位 (数字或大写英文字母)

    第五部分:校验码1位 (数字或大写英文字母)

    1
    /[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}/

    目前还有老的工商注册代码,也就是15位的社会信用代码,正则表达式如下:(弱校验)

    1
    /[1-9]\d{15}/

    同时支持18位和15位社会信用代码

    1
    /^([0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}|[1-9]\d{14})$/
  • 中国邮政编码

    前两位代表省、市、自治区,第三位代表邮区,第四位代表县、市,最后两位代表投递邮局,其中第二位不为 8。

    1
    /\d[0-7|9][0-9]{4}/
  • 中国身份证号码

    前六位是户口所在地编码,其中第一位是 1~8,此后是出生年月日,出生年份只能是 18、19、20,而且是可选的,最后一位校验位是数字或 x

    1
    /[1-8]\d{5}((18)|(19)|(20))?\d{2}(0[1-9]|1[0-2])((0|1|2)[1-9]|3[0-1])\d{3}[\dx]?/

JavaScript 中的特殊字符

Unicode 转义序列 含义 类别
\u0008 \b Backspace
\u0009 \t Tab 空白
\u000A \n 换行符 行终止符
\u000B \v 垂直制表符 空白
\u000C \f 换页 空白
\u000D \r 回车 行终止符
\u0022 " 双引号
\u0027 ' 单引号
\u005C \ \ 反斜杠
\u00A0 不间断空格 空白
\u0028 行分隔符 行终止符
\u0029 段落分割符 行终止符
\uFEFF 字节顺序标记 空白

服务端渲染

服务器端渲染

什么是服务器端渲染?

将组件或页面在服务器端渲染为 HTML 字符串,直接发送到浏览器,最后将这些静态标记激活为客户端上完全可交互的应用程序。

为什么使用服务器端渲染?

SSR 的主要优势

  • 更好的 SEO

    搜索引擎爬虫工具可以直接查看完全渲染的页面

  • 更快的内容达到时间

    无需等待所有的 JavaScript 都完成下载并执行

需要权衡的地方

  • 浏览器特定的代码,只能在某些生命周期钩子函数中使用

  • 涉及构建设置和部署的更多要求

    服务器渲染应用程序,需要处于 Node.js server 环境

  • 更多的服务器端负载

JavaScript 文件操作

在大部分浏览器上,你不能直接存储你创建的文件,因为这会导致大量的安全问题。不过你可以将你的文件通过下载链接的方式提供给用户。

在客户端生成文件

静态方法 window.URL.createObjectURL(object) 会创建一个表示参数中给出的对象 objectURLDOMString ,其生命周期和创建它的 document 生命周期绑定。

object 可以是 FileBlobMediaSource

每次调用 createObjectURL() 都会为指定的对象创建一个新的 URL 对象,而不管是否已经用这个指定对象创建过 URL 对象。因此在创建的 URL 对象不再使用时,应该在安全的时机调用 revokeObjectURL() 方法来释放掉它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const objArrInJson = JSON.stringify([
{
"name": "一般职业",
"value": "0"
},
{
"name": "农牧业",
"value": "1",
"childs": [
{
"name": "机关、团体、公司",
"value": "1"
}
]
},]);

function generateF(raw) {
const datasInBlob = new Blob([raw], { type: 'application/json' });
return window.URL.createObjectURL(datasInBlob);
}

关于 FileReader

在网页应用程序中,使用 FileReader 可以通过使用 BlobFile 指定要异步的读取的文件内容(或数据缓冲区内容)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
</head>

<body>
<input type="file" multiple accept="*" id="fileInput">

<script>
const fileInput = document.getElementById('fileInput');

// change 事件当用户选择了文件时被触发
fileInput.addEventListener('change', (event) => {
const fileI = event.target;
console.log(fileI.files);
})
</script>
</body>

</html>

文件加载完成后返回的 files 是一个 FileList,其中的 File 的就是选择的文件的相关信息。

File 对象包含提供文件有关的信息,使得网页中的代码能够访问文件内容。

1
2
3
4
5
6
7
8
{
lastModified: 00000000000, // 只读属性,以毫秒表示的的文件上次修改时间
lastModifiedDate: '', // 只读属性,文件上次修改时间
name: 'file.text', // 只读属性, File 对象的关联的文件的名称
size: 1024, // 只读属性,以字节表示的文件大小
type: 'text/plain', // 只读属性,文件的 MIME 类型,类型不确定是返回 ""
webkitRelativePath: '' // 文件相关的 `Path` 或 `URL`
}

拖拽文件

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
</head>

<body>
<div class="dropbox" id="dropbox"></div>

<script>
const dropbox = document.getElementById('dropbox');

dropbox.addEventListener('dragenter', (e) => {
e.stopPropagation();
e.preventDefault();
}, false);
dropbox.addEventListener('dragover', (e) => {
e.stopPropagation();
e.preventDefault();
console.log(e);
}, false);
dropbox.addEventListener('drop', (e) => {
e.stopPropagation();
e.preventDefault();

const dt = e.dataTransfer;
console.log(dt);
const files = dt.files;
}, false);
</script>
<style>
.dropbox {
width: 100vw;
height: 100vh;
background-color: aliceblue;
}
</style>
</body>

</html>

JavaScript 中的深拷贝和浅拷贝

关于深拷贝和浅拷贝的定义:

  • 浅拷贝 - 创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
  • 深拷贝 - 将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

使用 JSON.parse(JSON.stringify()) 来实现深拷贝

可以使用 JSON.parse(JSON.stringify(obj)) 来实现对目标对象的深拷贝,但是这种方法有很多缺陷:

  • 如果 undefinedSymbol 和 函数是对象的属性值或以Symbo值为键的属性值,拷贝的对象会丢失这些属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const originObj = {
    name: 'Trump',
    age: 94,
    phone: undefined,
    idNo: Symbol('1900'),
    speak: () => console.log('I am Trump'),
    [Symbol.for('weight')]: 120,
    }

    const copyObj = JSON.parse(JSON.stringify(originObj));
    console.log(copyObj); // { name: 'Trump', age: 94 }
  • 无法处理循环引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const team = {
    teamName: 'MAGA',
    }

    const originObj = {
    name: 'Trump',
    age: 94,
    phone: undefined,
    idNo: Symbol('1900'),
    speak: () => console.log('I am Trump'),
    [Symbol.for('weight')]: 120,
    team,
    }

    team.leader = originObj;

    const copyObj = JSON.parse(JSON.stringify(originObj));
    console.log(copyObj); // TypeError: Converting circular structure to JSON

一个比较全面的深拷贝实现

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
const TYPE_OBJECT = '[object Object]';
const TYPE_MAP = '[object Map]';
const TYPE_SET = '[object Set]';
const TYPE_ARRAY = '[object Array]';
const TYPE_ARGUMENTS = '[object Arguments]';
const TYPE_BOOLEAN = '[object Boolean]';
const TYPE_DATE = '[object Date]';
const TYPE_NUMBER = '[object Number]';
const TYPE_STRING = '[object String]';
const TYPE_SYMBOL = '[object Symbol]';
const TYPE_ERROR = '[object Error]';
const TYPE_REGEXP = '[object RegExp]';
const TYPE_FUNCTION = '[object Function]';
const TYPE_UNDEFINED = '[object Undefined]';

const forEach = (array, iterator) => {
let index = -1;
let length = array.length;
while(++index < length) {
iterator(array[index], index);
}
return array;
}

const isObj = (value) => {
const type = typeof value;
return value !== null && (type === 'object' || type === 'function');
}

const getType = (value) => {
return Object.prototype.toString.call(value);
}

const canCopyTags = [TYPE_OBJECT, TYPE_ARRAY, TYPE_MAP, TYPE_SET, TYPE_ARGUMENTS];

const getInit = (value) => {
const ctor = value.constructor;
return new ctor();
}

const copyReg = (value) => {
const regFlags = /\w*$/;
const newReg = new value.constructor(value.source, regFlags.exec(value));
newReg.lastIndex = value.lastIndex;
return newReg;
}

const copySymbol = (value) => {
return Object(Symbol.prototype.valueOf.call(value));
}

const copyFunction = (value) => {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
if (value.prototype) {
const body = bodyReg.exec(value.toString());
const param = paramReg.exec(value.toString());
if (body) {
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(value.toString());
}
}

const copyOther = (value, type) => {
const ctor = value.constructor;
switch(type) {
case TYPE_BOOLEAN:
case TYPE_NUMBER:
case TYPE_STRING:
case TYPE_ERROR:
case TYPE_DATE:
return new ctor(value);
break;
case TYPE_REGEXP:
return copyReg(value);
break;
case TYPE_SYMBOL:
return copySymbol(value);
break;
case TYPE_FUNCTION:
return copyFunction(value);
break;
default:
return null;
}
}

const copy = (origin, relationMap = new WeakMap()) => {
if (!isObj(origin)) return origin;

const type = getType(origin);
let target;
if (canCopyTags.includes(type)) {
target = getInit(origin);
} else {
return copyOther(origin, type);
}

if (relationMap.get(origin)) return relationMap.get(origin);
relationMap.set(origin, target);

if (type === TYPE_SET) {
origin.forEach((value) => {
target.add(copy(value));
});
return target;
}

if (type === TYPE_MAP) {
origin.forEach((value, key) => {
target.set(key, copy(value));
});
return target;
}

const keys = type === TYPE_ARRAY ? undefined : Object.keys(origin);
forEach(keys || origin, (value, key) => {
if (keys) {
key = value;
}
target[key] = copy(origin[key], relationMap);
});

return target;
}

const originObj = {
name: 'Trump',
age: 94,
phone: undefined,
idNo: Symbol('1900'),
speak: () => console.log('I am Trump'),
[Symbol.for('weight')]: 120,
team: {
leader: 'Ywank',
office: {
address: 'white house'
}
},
employee: [
{ name: 'pense', },
{ name: 'fox', },
],
}

originObj.president = originObj;

console.log(copy(1)); // 1
console.log(copy(undefined)); // undefined
console.log(copy(/\d/)); // /\d/
console.log(copy(new Date())); // 2018-05-16T08:06:45.361Z

const someFun = function(num1, num2) {
console.log(num1 + num2);
return num1 + num2;
}

const copyFun = copy(someFun);
console.log(copyFun(1, 3)); // 4

const someSet = new Set();
someSet.add(1);
console.log(copy(someSet)); // Set(1) { 1 }

const copyObj = copy(originObj);
console.log(originObj);
console.log(copyObj);
copyObj.speak(); // I am Trump

为什么使用 while 实现遍历

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
const ARRAY_SIZE = 10000000;
const LARGE_ARRY = new Array(ARRAY_SIZE).fill(1);

// for...in
let forInCopy = 0;
console.time('FOR_IN');
for (let key in LARGE_ARRY) {
forInCopy += LARGE_ARRY[key];
}
console.timeEnd('FOR_IN');

// for
let forCopy = 0
console.time('FOR');
for (let index = 0; index < LARGE_ARRY.length; index += 1) {
forCopy += LARGE_ARRY[index];
}
console.timeEnd('FOR');

// while
let whileCopy = 0;
let index = 0;
console.time('WHILE');
while(index < ARRAY_SIZE) {
whileCopy += LARGE_ARRY[index];
index += 1;
}
console.timeEnd('WHILE');

// forEach
let forEachCopy = 0
console.time('FOR_EACH');
LARGE_ARRY.forEach((value) => {
forEachCopy += value;
});
console.timeEnd('FOR_EACH');
遍历方式 for...in for forEach while
100 0.103ms 0.012ms 0.027ms 0.007ms
10 0.088ms 0.005ms 0.022ms 0.003ms
1000 0.313ms 0.036ms 0.052ms 0.028ms
10000 1.332ms 0.299ms 0.316ms 0.252ms
100000 14.167ms 1.691ms 1.977ms 1.428ms
1000000 140.967ms 2.928ms 14.94ms 3.012ms
10000000 2.748s 13.197ms 141.273ms 15.944ms
20000000 7.824s 22.389ms 254.557ms 28.07ms
30000000 17.107s 33.414ms 375.766ms 44.301ms

综合来看,在百万量级的数据下,使用 while 进行遍历所耗费的时间最少。

判断引用类型

1
2
3
4
const isObj = (value) => {
const type = typeof value;
return value !== null && (type === 'object' || type === 'function');
}

获取数据类型

每一个引用类型都有toString方法,默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中type是对象的类型。

1
2
3
const getType = (value) => {
return Object.prototype.toString.call(value);
}