ruirui's blog

ruirui's 备忘录


  • Home

  • Archives

前端请求签名方案

Posted on 2021-01-18

最近参与开发公司一个重要的数据产品,因为数据安全级别要求较高,常用的权限控制方案无法防止用户通过接口或爬虫工具进行恶意访问和数据抓取,故设计此方案用于保证接口安全。

这套方案可以起到以下防护:

  1. 每次发起请求时,需通过特定算法对请求参数进行签名且后端验证通过后才会放行,避免随意抓取数据。
  2. 生成的签名只对本用户的本次请求(参数)有效,避免重放攻击。
  3. 适用于各种请求和传参方式,如GET, POST。

加密算法分类

作为前置知识,先来介绍一下加密算法的分类。

对称加密

会话前双方约定一个固定的密钥,客户端用此密钥进行加密,服务端用此密钥进行解密。

特点:算法公开,计算量小,加密速度快。

常见算法:AES。

非对称加密

两个密钥:公钥和私钥,客户端使用公钥进行加密,服务端用私钥进行解密。

特点:加密和解密时间长、速度慢,只适合对少量的数据进行加密。

使用场景:https 会话前期、CA 数字证书、信息加密等。

常见算法:RSA。

Hash 算法

把任意长度的输入,通过 hash 算法,变换成固定长度的输出。

特点:不可逆,易计算。

使用场景:文件或字符串一致性校验、数字签名、鉴权协议。

常见算法:MD5

方案流程图

flow

关键点

client 端(浏览器)

参数处理

GET 请求

因为 GET 请求传输到服务端是没有数据类型的,即服务端收到的都是字符串。所以使用 url 键值对的格式生成字符串。

POST、PUT、DELETE 请求

  1. 如果 content-type 是 application/json,服务端接收到的是有数据类型的,故将 body 进行 json.stringify 序列化即可
  2. 如果 content-type 是 application/x-www-form-urlencoded,服务端收到的是类似 url 编码的形式,使用类似 GET 请求的方式处理
  3. 如果 content-type 是 multipart/form-data,为文件上传,这里不做处理

签名加密时 timestamp 的作用

  1. 在 server 端使用方案1 的情况下,timestamp 用于判断签名是否超时。
  2. 在 server 端使用方案2 的情况下,timestamp 保证每次发请求的 sign 唯一,类似于加盐,如果不让时间戳参与生成的话,相同的请求参数会生成相同的 sign,用户可以用这个 sign 一直请求,虽然不会有太大危害,但是会有不必要的业务处理。

server 端(node)

方案一,不使用 Redis:

使用 timestamp 来防止接口被重复利用,客户端发请求时带上当前时间的时间戳,服务端接收到 timestamp 后与当前时间进行对比,如果时间差大于3分钟,则认为是无效请求。

这个方案的优点是不依赖任何组件,性能更好,但由于不能保证客户端时间(也是浏览器端时间)的准确性,此方案可能存在隐患。故采用方案二:

方案二,使 Resis:

使用 redis 将每次请求传过来的 sign 进行存储,如果接收到的请求 sign 存在于 redis 里,则认为是无效请求,保证每个请求只能被用一次。这种方案不依赖客户端时间,同时引入Redis后可以实现更多的功能,如服务端限流(挖个坑之后填上)。

server 端注意事项:在进行参数校验前,先判断是不是一个白名单(或文件上传下载)请求,如果是需要直接放行。

方案中配合使用了 hash 算法和对称加密算法,hash 算法用于将参与加密的数据生成一个唯一签名,因为不需要还原这个数据,所以使用了 md5 算法。之后使用 AES 将这个签名和时间戳一起进行了加密,用来传给后端。

2

Node 内存溢出 —— OOM

Posted on 2020-12-28

起因

前几天被叫去排查一个老系统的线上问题,现象是服务不稳定,偶尔不可访问,查看监控发现服务偶尔重启,在重启前会出现内存升高一段时间,查看报错日志,在每次重启前会报 OOM(out of memory),并且每次 OOM 之前都会访问同一个接口, 线下模拟请求这个接口,发现是因为 response 数据量太大导致内存溢出,贴出报错情况。

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed — process out of memory

outofmemory

当为执行应用程序分配的内存少于运行应用程序所需的内存时,会发生此错误。

内存限制

V8的内存限制:64 位系统约为 1.4 GB,32 位系统约为 0.7 GB,即当 64 位系统中 node 使用总内存超过 1.4 G时,会报 OOM(Out Of Memory)。

为什么会有内存限制?

理论上 64 位操作系统可以访问 16TB 的空间,这比 1.4GB 要多的多!但因为以下两个原因,V8 的内存限制实际上要少的多

  1. JS (运行环境里)单线程的执行机制。
  2. V8 垃圾回收机制。

因为 js 是单线程运行的,执行垃圾回收会占用主线程的时间,导致全停顿(Stop-The-World),如果垃圾回收时间过长,浏览器页面上会感受到卡顿,Node 端脚本执行被阻塞。(V8 目前针对全停顿的体验问题也有了优化方式)

按照官方的说法以1.5G的垃圾回收为例,v8做一次小的垃圾回收需要50毫秒以上,做一次非增量的垃圾回收需要1秒以上。

通常在内存使用量快满时,进行垃圾回收(具体机制参考V8 内存体系与垃圾回收),内存越大,越不容易执行垃圾回收,执行一次垃圾回收耗费时间也越长,内存越少,垃圾回收越快,效率越高,所以程序减少内存使用,可以提升服务性能。

什么情况下会耗尽内存?

如果试图装载一个比可用内存还要大的数据,就会出现内存不够的情况,导致内存溢出,进程退出。
另一种情况是内存泄露,本该被回收的变量,没有被回收,常驻在内存里,内存使用量会随着时间的推移而增长,最终导致内存耗尽。
可以通过图示来辨别内存问题
difference

解决办法

一、 产品上解决
数据量太大,浏览器加载及处理耗费时间太长也会导致页面卡顿,影响用户体验。

二、扩充内存
设置 --max-old-space-size=4096 扩充为 4G 内存,
注意:是下划线还是中横线,取决于 node 版本号,通过 node --v8-options 命令来看 V8 参数。我试了几个版本

8版本是下划线
8option

10版本是中横线
10option

14版本是下划线
14option

这里太坑了!!Node 版本不考虑参数兼容性,因为本地 Node 版本和线上机器 Node 版本不一致,本地测试通过发现线上没生效,找了好久原因ಥ_ಥ。

三、 流式解析
使用 buffer,buffer内存的分配是c++层面完成的,不会利用V8的堆内存。

Linux 升级 Node

Posted on 2020-11-02

以下为通过二进制文件升级 node 的方式

升级 Node

  1. 通过官网下载 linux 二进制文件安装包
    推荐安装在/opt目录下

  2. 解压
    tar -xvf node-v14.15.0-linux-x64.tar

  3. 设置环境变量
    方式一: 设置 PATH 环境变量
    修改 /etc/profile 文件,在文件末尾添加以下内容

    1
    2
    export NODE_HOME=/xxx
    export PATH=$NODE_HOME/bin:$PATH

    修改完之后,打开新的命令窗口,或者执行 source /etc/profile 来生效。

    方式二: 设置软链
    通过 which node 查看 node 可执行文件路径,将本次 node 安装路径下的/bin/node 链接到 which node显示的路径下

    which 指令: 查看可执行文件的位置,会在环境变量 $PATH 设置的目录里查找符合条件的文件

    1
    2
    3
    ln -s /opt/node/bin/node /usr/local/bin/node 
    ln -s /opt/node/bin/npx /usr/local/bin/npx
    ln -s /opt/node/bin/npm /usr/local/bin/npm

其他

以下是在操作上述过程中遇到的一些问题及知识点,在此记录

通过 n 包管理器升级

清除 npm 缓存
npm cache clean -f
安装
npm install -g n
下载最新版本
n latest
下载稳定版
n stable
下载某个版本
n 版本号
切换版本
n

链接

硬链接:每个文件都有一个硬链接,硬链接和文件没什么区别。
软链接(符号链接):类似 window 的快捷方式,软链接可以关联一个目录,硬链接不行。

创建链接 ln
-s 表示创建软链

1
ln -s /usr/bin/npm /opt/soft/node-v14.15.0/bin/npm

查看文件路径 ls -l
如果文件或目录是软链,-> 后面是链接的真实路径

1
2
3
4
5
[root@3e5830d31dce bin]# ls -l | grep node
lrwxrwxrwx 1 root root 27 10月 29 19:34 n -> ../lib/node_modules/n/bin/n
lrwxrwxrwx 1 root root 42 10月 29 20:49 node -> /opt/soft/node-v14.15.0-linux-x64/bin/node
lrwxrwxrwx 1 root root 41 10月 29 21:06 npm -> /opt/soft/node-v14.15.0-linux-x64/bin/npm
lrwxrwxrwx 1 root root 41 10月 29 21:07 npx -> /opt/soft/node-v14.15.0-linux-x64/bin/npx

single-spa 源码分析

Posted on 2020-03-26

在了解single-spa基本功能后,可以将其简单概括为:single-spa 的核心就是动态将子应用的资源文件插入到主应用中。那内部是如何管理子应用、如何做到子应用之间的动态切换。带着这些疑问探究 single-spa 源码。

源码主要分为三部分:app 应用、Navigation 路由、生命周期。

application

应用状态

为了更好的管理 app,每个app在运行期间都有自己的状态
每个应用在整个运行期间都有自己的状态,根据不同状态做对应的处理。

  • null : app 不存在
  • NOT_LOADED:已经注册还没加载
  • LOADING_SOURCE_CODE:正在加载 app 代码(registerApplication 的第二个参数)
  • NOT_BOOTSTRAPPED:已经加载还没启动,即未执行 app 的 bootstrap 生命周期函数
  • BOOTSTRAPPING:正在启动,执行 app 的 bootstrap 生命周期函数,只执行一次
  • NOT_MOUNTED:已经启动还没挂载
  • MOUNTING: 正在挂载,执行 app 的 mount 函数
  • MOUNTED:已经挂载
  • UNMOUNTING:正在移除挂载,执行 app 的 unmount 函数
  • UNLOADING: 正在卸载,还没完成
  • SKIP_BECAUSE_BROKEN:执行期间出错

注册应用

框架内部统一管理所有应用来进行应用之间的调度(应用的挂载卸载错误等处理),应用注册成功后,会放到内部统一管理应用的的数组 apps 里。
registerApplication:(appName,applicationOrLoadingFn,activeFn, customProps)

  • appName: 应用唯一标识。
  • applicationOrLoadingFn: 入口 js 文件,这就需要子应用做一些处理,需要打包成特殊的文件格式进行加载
  • activeFn:什么时候激活应用,根据 url 进行匹配。
  • customProps:给子应用传递的参数,例如登录信息权限控制等。

注册成功之后的单个 app 信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apps.push({
loadErrorTime: null,
name: appName,
loadImpl,
activeWhen: activityFn,
status: NOT_LOADED,
parcels: {},
devtools: {
overlays: {
options: {},
selectors: [],
}
},
customProps
});

除了注册传递的参数以外,single-spa 给每个 app 增加了状态 status。注册后的状态为 NOT_LOADED,表示已经注册但未加载。
注册成功后,会执行 reroute 方法,reroute 内部判断是否已经 start,如果未启动,则找到匹配的应用(不报错的、还未 load 的)进行预加载,为挂载做准备。如果已经启动,则取消已经挂载的,找到当前匹配的进行挂载,reroute 是整个 single-app 的核心,后面还会详细分析。

卸载应用

unregisterApplication 会去调用 unloadApplication,然后在 apps 里找到对应的 app 将其删除。
在 unloadApplication 里,会先去看当前应用是否有正在被 unload,如果存在则直接返回。否则需要先 unmount 之后再去 unload。

Navigation

1
2
3
4
5
window.addEventListener('hashchange', urlReroute);
window.addEventListener('popstate', urlReroute);
function urlReroute() {
reroute([], arguments)
}

全局监听了 hashchange 和 popstate 事件来拦截 url 的变化,在路由事件到达应用框架(Vue、React)之前做应用的挂载卸载处理,当触发这两个事件后,也会执行 reroute,同时带上事件参数传递给 reroute。下面重点分析一下 reroute。

reroute

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
/*
reroute 接收两个参数
pendingPromises:表示正在队列中等待执行的 reroute。reroute 在执行期间,可能会有多个 reroute 被调用(路由触发或者应用注册)。
eventArguments:路由事件触发的 event。只有路由改变才会有这个参数
*/
export function reroute(pendingPromises = [], eventArguments) {
// appChangeUnderway 是一个开关,用来表示 reroute 是否正处于执行期间。
// 如果正处于执行期间还会有 reroute 要执行,则会将 reroute 放入队列里等待执行
if (appChangeUnderway) {
return new Promise((resolve, reject) => {
peopleWaitingOnAppChange.push({
resolve,
reject,
eventArguments,
});
});
}

appChangeUnderway = true;
// wasNoOp 为 true 表示没有应用发生变更。
let wasNoOp = true;

if (isStarted()) {
return performAppChanges();
} else {
return loadApps();
}

function loadApps() {
return Promise.resolve().then(() => {
const loadPromises = getAppsToLoad().map(toLoadPromise);
// 获取要 load 的 app(未出错的命中的),如果有说明应用发生了变更
if (loadPromises.length > 0) {
wasNoOp = false;
}
// 执行 load 并且直接 finishUpAndReturn
// 并不进行 mount,因为未 start,此时进行预加载
// 即:页面上没有挂载该应用,但是会去请求对应的资源文件
// 不会去调用应用文件里的bootstrap、mount、unmount等生命周期
return Promise
.all(loadPromises)
.then(finishUpAndReturn)
.catch(err => {
callAllEventListeners();
throw err;
})
})
}

function performAppChanges() {
return Promise.resolve().then(() => {
window.dispatchEvent(new CustomEvent("single-spa:before-routing-event", getCustomEventDetail()));
const unloadPromises = getAppsToUnload().map(toUnloadPromise);

const unmountUnloadPromises = getAppsToUnmount()
.map(toUnmountPromise)
.map(unmountPromise => unmountPromise.then(toUnloadPromise));

const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);
// 获取要 unload 以及要 unmout 的 app,如果有表示应用发生了变更
// 如果此时触发的是子应用内部的路由,则此时 allUnmountPromises 为[],表示没有应用的卸载或挂载
if (allUnmountPromises.length > 0) {
wasNoOp = false;
}

const unmountAllPromise = Promise.all(allUnmountPromises);

const appsToLoad = getAppsToLoad();
// 在其他应用 unmounting 期间将需要 load 的 app 执行 load、bootstrap(并行优势),等所有的 unmounting 都结束之后去挂载当前的 app
const loadThenMountPromises = appsToLoad.map(app => {
return toLoadPromise(app)
.then(toBootstrapPromise)
.then(app => {
return unmountAllPromise
.then(() => toMountPromise(app))
})
})
// 如果有要挂载的 app,也说明应用发生了变更
if (loadThenMountPromises.length > 0) {
wasNoOp = false;
}
// 获取已经 bootstrap 要去 mount 的 app,同上,等到其他 app unmounting 之后执行挂载
const mountPromises = getAppsToMount()
.filter(appToMount => appsToLoad.indexOf(appToMount) < 0)
.map(appToMount => {
return toBootstrapPromise(appToMount)
.then(() => unmountAllPromise)
.then(() => toMountPromise(appToMount))
})
// 同上
if (mountPromises.length > 0) {
wasNoOp = false;
}
// 所有要卸载的执行完之后,执行回调
return unmountAllPromise
.catch(err => {
callAllEventListeners();
throw err;
})
.then(() => {
callAllEventListeners();

return Promise
.all(loadThenMountPromises.concat(mountPromises))
.catch(err => {
pendingPromises.forEach(promise => promise.reject(err));
throw err;
})
.then(() => finishUpAndReturn(false))
})

})
}

function finishUpAndReturn(callEventListeners=true) {
const returnValue = getMountedApps();

if (callEventListeners) {
callAllEventListeners();
}
pendingPromises.forEach(promise => promise.resolve(returnValue));

try {
const appChangeEventName = wasNoOp ? "single-spa:no-app-change": "single-spa:app-change";
window.dispatchEvent(new CustomEvent(appChangeEventName, getCustomEventDetail()));
window.dispatchEvent(new CustomEvent("single-spa:routing-event", getCustomEventDetail()));
} catch (err) {
setTimeout(() => {
throw err;
});
}
// 打开开关,执行队列里的 reroute
appChangeUnderway = false;
// 虽然批量执行所有等待的 reroute,但这个地方还是需要递归的执行,因为在执行 reroute 期间可能又会有 reroute 进来
if (peopleWaitingOnAppChange.length > 0) {
const nextPendingPromises = peopleWaitingOnAppChange;
peopleWaitingOnAppChange = [];
reroute(nextPendingPromises);
}

return returnValue;
}

function callAllEventListeners() {
pendingPromises.forEach(pendingPromise => {
callCapturedEventListeners(pendingPromise.eventArguments);
});

callCapturedEventListeners(eventArguments);
}

function getCustomEventDetail() {
const result = {detail: {}}

if (eventArguments && eventArguments[0]) {
result.detail.originalEvent = eventArguments[0]
}

return result
}
}

画个简易的流程图如下:
reroute

lifecycles

single-spa 的亮点除了顶层路由的设计,另一个亮点就是生命周期的设计。生命周期的设计使得主应用更好的控制子应用。整个生命周期状态变更如下:
lifecycle

app 出错的状态有两个:SKIP_BECAUSE_BROKEN 与 LOAD_ERROR。SKIP_BECAUSE_BROKEN 表示在状态变更时出错,阻止往下个状态变更,LOAD_ERROR 表示加载错误,此时会记录当前的时间戳,当路由再次导航到对应用时还会尝试去加载(时间间隔大于200ms)。
挂载阶段出错时,在状态变成 SKIP_BECAUSE_BROKEN 之前需要先将状态变成 mounted,因为出错后要执行 toUnmountPromise 卸载应用,而 toUnmountPromise 会判断如果状态不是 MOUNTED 时会跳过。

single-spa-react

上面提到,在加载应用资源时,会去检查 app 的三个生命周期状态,single-spa 要求接入的应用都提供这三个生命周期,所以官方官方适配出了各个框架的工具。以single-spa-react 为例,react 应用的入口文件通过 single-spa-react 封装,暴露给 single-spa 三个生命周期函数,并且都是 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
27
28
29
const defaultOpts = {
// required opts
React: null,
ReactDOM: null,
rootComponent: null,
loadRootComponent: null,
suppressComponentDidCatchWarning: false,
domElements: {},

// optional opts
domElementGetter: null,
parcelCanUpdate: true,
}
export default function singleSpaReact(userOpts) {
// ...
const opts = {
...defaultOpts,
...userOpts,
};
// ...
const lifecycles = {
bootstrap: bootstrap.bind(null, opts),
mount: mount.bind(null, opts),
unmount: unmount.bind(null, opts),
};
// ...

return lifecycles
}
  • bootstrap: 如果是 class 或者无状态组件,则直接返回。确保传入的是 React 根组件。
  • mount: 找到 single-spa 传递给应用的 DOM 节点(domElementGetter),执行 reactDomRender 进行渲染。
  • unmount: 从 DOM 中移除 React 应用。

2019年读书笔记

Posted on 2020-01-05

自从脱离了学校之后,发现自己对文字和语言的感知特别弱,和别人交流看过的书或者电影时,惊讶的发现大部分时候只记得书名,书里描述的具体场景及情节忘的一干二净哪怕当时阅读时感觉多么好看多么有共鸣,这种感受多了之后其实有点痛苦甚至失望,于是19年想着能把读过的书以及当时的感受记录下来,也算是一种积累或者回忆。

美妙的新世界

2018-12-20
这是我读的第一本乌托邦的书籍,看《奇葩说》辩论时詹青云提到的。

“新世界”相信一种理论:”道德教育都是不能诉诸理论的。” 因此都在下意识进行。
“新世界”有一条规定:”智力和工作是成年人,感情和欲望是孩子”

新世界的宗旨是: 一切为了稳定。当人们出现一点痛苦,服用唆麻便可以化解。
读完这本书,感觉到更多的是可怕和悲哀,新世界人们像是一个个流水线上的生产出来的机器,从最原始材料的不同就被划分不同的社会阶级,人和人之间不是平等的,每个人是有具体社会职责的,都是为了看似和谐美好高效的社会在运转。然而缩短劳动力的时间并不会使社会更稳定,提高效率并不会使每个人更幸福。

局外人

2019-01-01
在整个阅读的过程中,主人公默尔索一直给我这种感觉:无所谓,都行。母亲逝世后,默尔索并没有如别人一般表现痛苦难受,玛丽问他是否想要结婚时,他回答结不结都行,当无意枪杀阿拉伯人时,也没有急于为自己辩护,在将要执行死刑时,被一直逼问是否信仰上帝。

默尔索一切的举动在常人眼里就是不道德、政治不正确。在审判默尔索的罪行时,人们更多的在批判审视默尔索,任何的举动都要以世人道德标准来评判,不在乎事实过程,一遍遍的分析默尔索的动机,本是一个可以从轻处罚的案件,却最终以荒诞的结局来收场。

霍乱时期的爱情

2019-03
一直以来都是用 kindle 在上下班坐公交地铁的时间来读书,《霍乱时期的爱情》买的实体书来看,所以进度明显比用 kindle 慢了许多,看起来也断断续续的,读完不是很满意,以后有时间再二刷写感受吧。

玛格丽特小镇

2019-04-03
这本书的名字和之前做的一种饼干(玛格丽特饼干)名字几乎一样,就勾起了我的好奇心,用比较短的时间读完,读的过程中觉得整个叙述缺乏逻辑条理,随意切换场景,但好像正是这种平淡的叙述方式,读起来反而更加随心,更加吸引人。

某种程度上,你在一个地方认识的人,定义了那个地方对于你的意义。
爱情就像一个学步的贪婪孩子,只认得两个字,那就是”我的”。
对初恋的执恋从来都跟对象无关,都是人们对自己的怀念。
我跑到生命尽头看了看, 看到我们果然白头偕老了。
有时候,我们会言过其实。有时候,我们会说一些不是那么真实的话,暗自希望说出来后即会成真。

他们在小镇上碰到五个玛格丽特,有天真可爱的小孩子梅,青春叛逆的米亚,忧伤的玛吉,怪异的玛琪,还有耳聋的的老玛格丽特。而这些都是不同心静,不同时期的玛格丽特。
就像是每个人都会有多方面的自己,每个时期都可能变成不同的人,而爱一个人就要能接受她的全部,无论美丑琐碎。
看完后看评价才发现是《岛上书店》作者的新作,准备再重温一遍《岛上书店》。

读书的时候有一种状态会让人特别愉悦,就是读着读着,一些文字就会让心里波动一下,有时是温暖,有时是揪心,有时是绝妙,有时是感动,就像是在经历不同的事情,寻找内心的自己,这大概就是读书的乐趣吧。

岛上书店

2019-04-18
二刷《岛上书店》,看之前试着回忆一下故事情节,发现几乎想不起来了… 再刷的过程中,细节慢慢浮现出来。
(当时只记录了上面的话,想着之后补上后续的读书笔记,然而就是因为没及时写,之后想补已经忘的差不多了,我这可怕的记忆力)

挪威的森林

2019-04-28
大学的时候急于拓宽自己的知识面,囫囵吞枣读了一些书,有的是为了读而读,读书时急于看到直给的东西,大多数都体会不到读书的乐趣,《挪威的森林》就属于这种,迷迷糊糊读到一半可能就换下一本了。再次读虽说还是觉得迷糊,但也是有耐心从头看到尾。

读完想了想这到底是一部什么样的小说,气氛低沉、伤感、平静。直子自从姐姐的死、木月的自杀之后陷入抑郁症的痛苦中,渡边几乎是她唯一的希望,然而她最终还是没能拯救自己。初美是一个优秀完美的女性,永泽拈花惹草、放荡不羁的性格以及价值观,让他意识到自己配不上初美,甚至觉得和初美在一起也违背自己的价值观,最终通过伤害初美让初美主动离开,而初美最后的自杀也是让人很惋惜。绿子几乎是唯一一个热情单纯的人,一道光一样的存在,渡边一直心系直子,绿子的所有举动渡边都没放在心上,包括渡边搬家忘记告诉绿子,绿子换发型没注意到,这就是不爱吧,因为不爱所以不关注。

傲慢与偏见

2019-12
某天周末想看电影放松一下,随意打开两个高分电影,一眼就被这部电影的高清开场吸引了,电影里唯美的景色,光看背景都是一种享受,看到伊丽莎白与达西之间产生误会冲突也会跟着揪心,可能也是对美好爱情的向往,在看这部电影时觉得格外心动。

记得之前看完<<被嫌弃的松子的一生>>之后,想再去看电影回顾一下,发现电影破坏了当初看小说的内心想象,所以之后电影和小说基本都会二选一,《傲慢与偏见》是我看完电影强烈想要去读原著想要了解人物当下内心感受的书,并且在一周内很快读完。电影整体节奏比较快,在读完一半小说之后忍不住又去再看了一遍电影,很多场景也有了不一样的感受。

读完这本书之后想到,我对这本书又何尝不是一种偏见呢,很早就知道了这本书但从没想主动去看,甚至见到这本书也自然而然绕过,内心觉得名著都是晦涩难懂的,可能名著是和『学习』挂钩的,『学习』这个词就给人一种压力,总觉得任何需要『学习』的知识都不容易理解,都不是自然而然可以习到或得到,都需要克服一些条件,即使收获比较大过程也不是享受的。我这种潜意识不仅在是读书,在平时工作生活中也有很大的影响,即使现在意识到了也很难抹平现在及以后的影响,慢慢来吧。

今年下半年换了份工作,感受到了未有的压力和焦虑,更多的感受到交流沟通的困难以及思维方式不同带来的认知差异,度过了几个月满脑子都是工作没有生活乐趣的生活,与此同时读书时间也少了很多,《约翰克里斯朵夫》《漫长的告别》等都还是未完成的状态,希望2020年工作上能顺心点,生活上能尝试更多的可能性,阅读更自然的成为生活的一部分。

Webpack 分析系列-打包后 js 文件分析

Posted on 2019-11-10

网上的 webpack 分析系列文章已经很多了,但从自己理解的角度出发,进行记录和梳理,是一个知识重新构建的过程,更加有利于消化吸收。此次把 webpack 当作一个系列来记录,希望从整体的体系出发,加深对 webpack 的理解。
首先从 webpack 打包生成的文件进行简单分析。

本文代码地址

1
2
// b.js
export default B = 'b';
1
2
3
4
// a.js
import B from './b';
console.log(B);
export const A = 'a';
1
2
3
// index.js
import { A } from './a';
console.log(A);

执行 npm run build(webpack –mode development) 生成 main.js 如下

1
2
3
4
5
6
7
8
9
10
11
12
(function(modules) { // webpackBootstrap
// ...

   // Load entry module and return exports
   return __webpack_require__(__webpack_require__.s = "./src/index.js");
})(
{
"./src/a.js": (function(module, __webpack_exports__, __webpack_require__) {},
"./src/b.js": (function(module, __webpack_exports__, __webpack_require__) {},
"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {}
}
)

可以看到,打包出来的是一个立即执行函数,参数是一个 key-value 的对象,key 是引入文件的路径,value 是对应的文件生成的模块化函数,立即执行函数返回一个从入口文件开始执行的__webpack_require__函数,关键就是__webpack_require__函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// The require function
function __webpack_require__(moduleId) {

// Check if module is in cache
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};

// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

// Flag the module as loaded
module.l = true;

// Return the exports of the module
return module.exports;
}

__webpack_require__ 根据 moduleId 来执行每一个 module 函数,当 installedModules 缓存中有该模块的对象,也就是该模块已经加载过,则从缓存中去取该对象的 exports,否则创建一个新的 module 对象执行 module 函数,module 对象有三个属性,i 表示 moduleId , l 表示模块是否已经加载过,exports 表示 module 的导出内容。通过 call 方法调用 module 的执行函数,执行函数的 this 指向 module.exports,后面三个是传入该函数的参数,__webpack_require__函数最后返回 module.exports。

回到最开始的立即执行函数,执行__webpack_require__(__webpack_require__.s = "./src/index.js"),对应的入口文件的 module 函数如下。

1
2
3
4
5
6
7
8
9
10
11
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ \"./src/a.js\");\n\nconsole.log(_a__WEBPACK_IMPORTED_MODULE_0__[\"A\"]);\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

把无关的注释都删掉,取出 eval 里的代码(eval 函数可执行其中的的 JavaScript 代码)

1
2
3
4
5
6
7
8
9
"./src/index.js":
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/a.js");
console.log(_a__WEBPACK_IMPORTED_MODULE_0__["A"]);

})
1
2
3
4
5
6
7
8
9
10
"./src/a.js":
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, "A", function() { return A; });
var _b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/b.js");
console.log(_b__WEBPACK_IMPORTED_MODULE_0__["default"]);
const A = 'a';
}),
1
2
3
4
5
6
"./src/b.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_exports__["default"] = (B = 'b');
}),

这样拆开来分析就比较一目了然了,从这三个文件可以看出,在执行模块时除了执行本身模块内容外,还会执行__webpack_require__.r,__webpack_require__.d等函数,从打包生成的 main.js 的立即执行函数中找这几个函数的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// define __esModule on exports
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

__webpack_require__.r 函数是 webpack 针对不同模块加了不同的标记处理,因为是 import 引入的,给 exports 对象加上ES harmony规范的标记。
执行__webpack_require__.d(__webpack_exports__, "A", function() { return A; }); 实际上就是生成__webpack_exports__.A = A;

从入口的 index.js 分析,首先对 index.js 进行了 esModule 的模块化标记,因为 index.js 引入了 a.js,接着对 a 模块执行__webpack_require__, 打印出 a 模块的执行结果。a 模块同样进行了 esModule 标记,并且生成了 module.exports.A = A ,将 A 变量导出,index.js 打印出 A 的结果。因为 b.js 使用的是export default,webpack 处理后,会在 module.exports 中增加一个 default 属性。

至此,我们看到,webpack 的输出文件,将各个模块以参数的形式传递给 IIFE 函数,从入口文件开始递归解析依赖,在解析的过程中,分别对不同的模块进行处理,返回模块的exports。

React 数据更新 与 Immutable

Posted on 2019-09-17

8月份入职新公司,刚入职就参与了一个迭代频繁的项目,也开始真正使用 React 做业务项目。接手的一个模块需要处理深层数据,在这过程中也爬了很多坑,终于搞明白了 React 的数据更新机制。在此做一个总结。

React 渲染

最开始熟悉项目的时候发现,在组件 render 函数的地方打印 console.log,会打印很多次,也就是一个组件会调用很多次 render,这样肯定会频繁的触发 React 的 diff 比较进行 patch 更新,这就很奇怪了,因为 Vue 里,render 函数只有在初始化和依赖的数据发生变化时才会触发,难道 React 有什么不同?于是开始研究 React 的渲染机制,什么时候会进行 render 呢?来一张官网的生命周期图。

lifecycle

可以看到,除了第一次挂载会初始化进行一次渲染外,在 props 或 state 有任何一个改变时,会根据 shouldComponentUpdate 值来判断是否进行 render,普通的 component 的 shouldComponentUpdate 默认会返回 true ,可以通过手写 shouldComponentUpdate 判断是否真正需要重新渲染来提高性能,也可以通过继承 React.PureComponent 来实现,PureComponent 内部进行浅比较(shallowEqual),比较前后两次 state 和 props 是否相等(如果是值类型,就进行值比较,引用类型比较地址是否相同),如果相等就不去更新。 这就会引入一个问题:

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
class ListOfWords extends React.PureComponent {
render() {
return <div>{this.props.words.join(',')}</div>;
}
}

class WordAdder extends React.Component {
constructor(props) {
super(props);
this.state = {
words: ['marklar']
};
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
// 这部分代码很糟,而且还有 bug
const words = this.state.words;
words.push('marklar');
this.setState({words: words});
}

render() {
return (
<div>
<button onClick={this.handleClick} />
<ListOfWords words={this.state.words} />
</div>
);
}
}

点击按钮时,会通过 push 方法添加一个单词,words 的引用地址没变,而 PureComponent 会进行浅比较,shoulComponentUpdate 会返回 false , 所以 ListOfWords 不会被重新渲染,这就是可变数据带来的问题。

不可变数据

所以避免该问题的方式就是使用不可变数据,不可变数据就是一旦对象创建就不可再修改,当需要改变时不可直接修改状态,需要通过生成一个新对象的方式来修改。

上面例子的的解决方式就是不去直接修改 props 或者 state,而是通过生成新的引用来替换

  • 原生写法 Object.assign()、concat 等
  • ES6 扩展运算符

上述两种方法是处理简单对象常用的方式,Object.assign 和 对象扩展运算符 都是对对象做了一个浅拷贝,所以如果在深层嵌套对象里,要去改变嵌套对象里面的值,使用上述两种方式并达不到预期的效果,借用 这个例子 来看(一定要点开看啊!)

1
2
3
4
5
6
7
8
9
10
11
state = {
user: {
id: 1,
name: "Cory",
address: {
city: "Kansas City",
state: "Kansas",
modified: new Date().toLocaleTimeString() // 现在需要改变这个值
}
}
};

user 是个深层对象,需要改变 modified 的值,对 user 的引用有三种组件,分别是 A 组件(React.Component)、B 组件(React.PureComponent)、C组件(React.Component,里面嵌套了Address 组件(React.PureComponent))。对 modified 的修改分别采用三种方式,看对应的渲染情况。

  1. Mutate 直接修改
1
2
3
4
5
mutate = () => {
const user = this.state.user; // still mutating user since just a ref!
user.address.modified = new Date().toLocaleTimeString();
this.setState({ user });
};
  • A 组件:因为 shouldComponent 都会返回 true,会重新渲染。
  • B 组件:因为 user 的引用地址没有变化, B 组件使用 PureComponent,会进行浅比较,所以B组件不会重新渲染。
  • C 组件:同 A 组件会重新渲染,但是因为 address 的引用地址没变, Address 组件使用的 PureComponent, 所以 Address 不会重新渲染。
  1. Shallow 浅拷贝
1
2
3
4
5
shallow = () => {
const user = { ...this.state.user };
user.address.modified = new Date().toLocaleTimeString();
this.setState({ user });
};
  • A 组件:重新渲染。
  • B 组件:重新渲染。
  • C 组件:同 A 组件会重新渲染,但是因为 address 的引用地址没变, Address 组件使用的 PureComponent, 所以 Address 不会重新渲染。
  1. Deep 深拷贝
1
2
3
4
5
6
7
8
deep = () => {
const user = {
...this.state.user,
address: { ...this.state.user.address }
};
user.address.modified = new Date().toLocaleTimeString();
this.setState({ user });
};
  • A 组件:重新渲染。
  • B 组件:重新渲染。
  • C 组件:重新渲染,子组件 Address 因为 address 引用地址改变也会重新渲染。

不提倡使用深拷贝的方式,他的代价是昂贵的,并且深拷贝会导致 React 进行不必要的渲染,因为嵌套的每个对象引用地址都改变了,引用这些数据的组件全部都会重新渲染。对于深层对象的处理,我们要做的只是拷贝已经改变的对象。有一些库例如 immutability-helper、immer、immutable-js 等都可以实现不可变数据结构。

immutable.js

immutable.js 是 Facebook 推出的能让开发者建立不可变数据的函数库,内部实现了一套完整的持久化数据结构,也就是说,对数据的所有的更新操作最后都会生成一个新的数据结构,原有结构保持不变,这也意味着所有的数据都是不可变的,有了这个限制前提,更新操作就有了很多优化的空间,例如更新一个深层节点的数据, Immutable 的实现原理如下

immutable

当需要更新某个节点数据时,只需要顺着链路更新分支上的节点,尽可能的复用现有的节点,这样既提升了性能,也降低了内存开销,immutable 的这个特点也称为结构共享。

其他

除了上面说的渲染性能优化,因为 Redux 设计是以几个原则为优先的:状态可追踪,可重复,可维护,不可变数据也是 Redux 运行的基础,因为有了不可变数据,当 store 发生变化时,任何时候都能记录变化之前和变化之后的状态,方便计算 diff,平时开发调试用的 chrome 插件就是利用了此机制进行追踪。

扁平化

在项目开发中,我们应该尽可能的减少使用深层数据结构,尽量将 store 组织的扁平化和范式化,扁平化的意思是:只要不存在“实体下面再挂实体”的现象,应该就可以认为是扁平。

React 与 Vue 在渲染上的不同

  • React 和 Redux 都提倡不可变性,更新需要维持不可变原则,Vue 不需要。
  • React 应用需要考虑优化机制,当某个组件发生变化时,会以该组件为根,重新渲染整个组件子树,所以需要尽可能的使用 PureComponent 和 shouldComponentUpdate 方法,同时使用不可变数据结构来使的组件更容易被优化。
  • Vue 采用依赖追踪机制,能精确的知道哪些组件需要重新渲染,不会存在过渡重渲染的性能问题,默认就是优化状态。
    React 渲染功能依赖 jsx,Vue 支持 jsx,但更多的使用 template 模板,这两个在性能上也有点区别,jsx 属于动态渲染,所有的 DOM 节点都是动态生成的,所以页面节点越多,DOM开销就会越大,并且无法根据初始状态进行优化,template在初始编译时,会根据节点类型找出静态节点并进行标记,数据变化时可以跳过这些静态节点的对比,避免进行无意义的 diff。

Javascript 面向对象、原型与继承

Posted on 2019-07-14

最近面试,有被问到关于原型链及继承的知识,于是系统的整理一下。

对象

编程语言的对象,成功的流派使用”类”的方式来描述对象。Javascript早期用『原型』的方式来描述对象。

对象的特点

  • 对象具有唯一标识性:即使两个完全一样的对象,也并非同一个对象。
  • 对象有状态:同一个对象可能处于不同的状态之下。
  • 对象有行为:对象的状态可能因为它的行为产生变迁。

唯一标识性:

1
2
3
var o1 = {a: 1};
var o1 = {a: 1};
console.log(o1 == 02); // false

状态和行为特征在 javascript 中统一抽象为”属性”。
除对象基本特征外,javascript 的对象具体高度的动态性,因为 javascript 赋予了使用者在运行时为对象添改状态和行为的能力。也就是说:

1
2
3
var o = {a: 1};
o.b = 2;
console.log(o.a o.b); // 1 2

可以在定义对象之后再去添加属性。
为了提高抽象能力,javascript 属性提供了数据属性和访问器属性(getter/setter) 两类。

创建对象

Javascript 通过 new 关键字来创建一个对象,new 关键字做了什么?

  • 创建一个对象
  • 将对象的原型(__proto__)指向构造函数的原型(prototype属性)
  • 将构造函数内部的 this 指向这个空对象,执行构造函数的逻辑
  • 如果构造函数内部没有返回对象,则返回创建的对象。

具体实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function objectFactory(...args) {

// 取出第一个参数(构造器),并从参数中删除。
const Constructor = args.shift();

// 创建一个空对象,并将空对象的_proto_属性指向构造函数的 prototype 属性
// 不建议直接操作_proto_属性(obj.__proto__ = Constructor.prototype)
const obj = Object.create(constructor.prototype);

// 执行构造器, 将构造函数内的 this 指向为 obj
var result = Constructor.apply(obj, args);

// 如果构造器返回对象则返回这个对象,否则返回新建的对象。
return (typeof result === 'object' && result != null) ? result : obj;
};

// 使用方式
function Person(name) {
this.name = name
}
const person = new objectFactory(Person, 'season')
console.log(person)

原型及原型链

用 <>中的图来表示
prototype
总结下来:

  • 每个函数都有一个 prototype 指向其原型(构造函数.prototype === 原型)
  • 每个原型的 constructor 属性都指向该构造函数(原型.constructor === 构造函数)
  • 每个实例(对象)都有一个 __proto__ 属性指向该构造函数的原型(实例.__proto__ === 原型)

继承

原型链继承

关键点:子类原型继承父类的实例

1
Child.prototype = new Parent()

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Child(name) {
this.name = name;
}
Child.prototype = new Parent()
function Parent(name) {
this.name = name;
this.list = [1, 2, 3];
}
Parent.prototype.getName = function() {
return this.name
}

var child1 = new Child('child1');
console.log(child1.getName()); // child1
child1.list.push(4);

var child2 = new Child('child2');
console.log(child2.getName()); // child2
console.log(child2.list); // [1, 2, 3, 4]

缺点:

  • 当父类构造函数有引用类型时,该引用类型会被所有子类所共享(修改 child1 的 list 属性时,chil2 也会修改)
  • 创建子类时,不能向父类构造函数传递参数

构造函数继承

关键点:在子类构造函数内调用父类构造函数方法,可以向父类传递参数。

1
2
3
4
function Child (args) {
// ...
Parent.call(this, args)
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Child(name, args) {
this.name = name;
Parent.call(this, args)
}

function Parent(name) {
this.name = name;
this.list = [1, 2, 3];
}
Parent.prototype.getName = function() {
return this.name
}

var child1 = new Child('child1', 'parent1');
console.log(child1.getName()); // Uncaught TypeError: child1.getName is not a function
child1.list.push(4);
console.log(child1.list) // [1, 2, 3, 4]

var child2 = new Child('child2', 'parent2');
console.log(child2.list); // [1, 2, 3]

可以向父类传递参数,也解决了上述共享父类引用类型的问题
缺点:不能继承父类原型上的方法。(上述child1.getName()获取不到)

组合继承

关键点:结合前面两种方式,使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承。

1
2
3
4
5
6
7
function Child (args1, args2) {
// ...
this.args1 = args1
Parent.call(this, args2)
}
Child.prototype = new Parent()
Child.prototype.constrcutor = Child

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Child(name, args) {
this.name = name;
Parent.call(this, args)
}
Child.prototype = new Parent();
Child.prototype.constrcutor = Child;
function Parent(name) {
this.name = name;
this.list = [1, 2, 3];
}
Parent.prototype.getName = function() {
return this.name
}

var child1 = new Child('child1', 'parent1');
console.log(child1.getName()); // parent1
child1.list.push(4);
console.log(child1.list) // [1, 2, 3, 4]

var child2 = new Child('child2', 'parent2');
console.log(child2.list); // [1, 2, 3]

缺点:会调用两次父类构造函数,Parent.call(this, args)会调用一次,Child.prototype = new Parent();还会调用一次。

原型式继承

原型式继承没有严格意义上的构造函数,他的想法主要是借助原型可以基于已有的对象创建新对象,也就是一个对象以另一个对象为基础,根据需求传入自己的属性。

1
2
3
4
5
function object(o) {
function F() {}
F.prototype = o;
return new F();
}

在 object 内部先创建了一个临时构造函数,将传入的对象作为构造函数的原型,返回临时构造函数的实例。
ECMAScript5 通过加入Object.create()方法规范化了原型式继承。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var parent = {
name: 'parent',
list: [1, 2, 3]
}

var child1 = object(parent);
child1.name = "child1";
console.log(child1.name); // 'child1'
child1.list.push(4);
console.log(child1.list); //  [1, 2, 3, 4]

var child2 = object(parent);
console.log(child2.list); // [1, 2, 3, 4]

可以看到用 object 的方式,父类的引用类型共享的问题还是存在,因为 object 方法其实是做了一层浅拷贝,所以父类的引用类型始终会共享。
在不考虑构造函数,只想基于一个对象生成另一个对象的情况下,原型式继承是比较适用的。

寄生式继承

和原型式继承类似,也是通过创建一个封装继承过程的函数,在函数内部以某种方式增强对象,最后返回对象。

1
2
3
4
5
6
7
function createAnother(original){
var clone = object(original);
clone.sayHi = function(){ // 以某种方式来增强这个对象,给该对象增加一个 sayHi 方法
alert("hi");
};
return clone;
}

同上面原型式继承类似,在不考虑构造函数的情况,寄生式继承是一种有用的模式。

寄生组合式继承

因为组合式继承会调用两次构造函数,寄生组合式继承通过寄生式继承来继承超类的原型,避免在指定子类原型的时候调用父类构造函数。

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
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function Parent(name) {
this.name = name;
this.list = [1, 2, 3];
}
Parent.prototype.getName = function() {
return this.name
}

function Child(name, args) {
this.name = name;
Parent.call(this, args); // 通过构造函数来继承属性
}
inheritPrototype(Child, Parent); // 通过寄生式继承来继承父类的原型


var child1 = new Child('child1', 'parent1');
console.log(child1.getName()); // parent1
child1.list.push(4);
console.log(child1.list) // [1, 2, 3, 4]

var child2 = new Child('child2', 'parent2');
console.log(child2.list); // [1, 2, 3]

使用 Object.create() 替代上述的 object();

1
2
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

ES6 继承

ES6 通过 extend 实现继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Parent {
constructor(){
this.name = 'parent';
this.list = [1, 2, 3];
}
getName() {
return this.name
}
}
class Child extends Parent {
constructor(){
super()
}
}
var child1 = new Child();
console.log(child1.name); // parent
console.log(child1.getName()) // parent
child1.list.push(4) // parent
console.log(child1.list) // [1, 2, 3, 4]

var child2 = new Child();
console.log(child2.name); // parent
console.log(child2.getName()) // parent
console.log(child2.list) // [1, 2, 3]

区别

  • ES5 实现继承的方式是:先构造子类的实例对象 this,再将父类方法添加进去(parent.apply(this))。
  • ES6 通过 extend 实现继承,先将父类的属性和方法添加到 this 上(先调用 super 方法),再调用子类的构造函数修改 this

ES6 继承实际上是 ES5 原型的语法糖,将上述继承通过 babel 在线编译工具进行转换,得到

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
// ... 省略一些方法定义
function _inherits(subClass, superClass) {
// 父类必须是函数并且不能为null
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
// 通过Object.create实现继承,第二个参数用来修复子类的 constructor
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
// 设置子类的 __proto__ 属性指向父类
if (superClass)
_setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p; return o;
};
return _setPrototypeOf(o, p);
}
// 两个参数,一个参数是指向子类实例的this,另一个参数是 调用父类构造函数的返回值
function _possibleConstructorReturn(self, call) {
// 如果父类返回的是对象或者函数,则返回 父类构造函数生成的this(Parent.call(this)),否则返回子类的this
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}

function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}


var Parent =
/*#__PURE__*/
function () {
function Parent() {
_classCallCheck(this, Parent);

this.name = 'parent';
this.list = [1, 2, 3];
}

_createClass(Parent, [{
key: "getName",
value: function getName() {
return this.name;
}
}]);

return Parent;
}();

var Child =
/*#__PURE__*/
function (_Parent) {
// 通过 Object.create 实现继承
_inherits(Child, _Parent);

function Child() {
// 确保类是通过 new 作为构造函数调用而不是直接调用
_classCallCheck(this, Child);
// _getPrototypeOf(Child) 返回 子类的原型(父类构造函数),通过子类 this 执行父类构造函数的方法
return _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this));
}

return Child;
}(Parent);

简化:子类首先执行_inherits(Child, _Parent);,内部通过Object.create建立子类与父类原型链关系,然后再通过调用Parent.call(this)。

『转』Linux 安装 node 和 npm

Posted on 2019-06-04

原文来自https://jpanj.com/2019/install-node-and-npm-on-linux/

网上介绍 Node 如何安装的文章数不胜数,但我还是决定自己写一篇记录一下,最主要的原因是网上的文章比较混乱,有的建议通过包管理工具安装,还有的让一步步编译源码来安装。

通过包管理工具安装的通常版本不会太新,通过源码安装的方式非常麻烦,还需要提前安装 gcc 之类的,只有极少部分良心博主介绍了通过二进制文件直接安装的方式,但操作上都不是特别规范。

网上已有的文章还有一个很严重的问题,就是没有考虑国内的网络环境,不管从 Node 官方下载源码包还是二进制包,都巨慢无比,所以我把已经下载好的包放在 CDN 上供自己和大家之后使用。同时我还提供了其他常用软件的安装包,如 Nginx,Java,Neo4j 等等,后边有机会列个清单出来,并准备长期维护更新版本。


下边进入正题:

我推荐以下操作在 /opt 目录下进行

下载压缩包

wget http://developer.jpanj.com/node-v10.15.3-linux-x64.tar.xz

解压为 tar 包

xz -d node-v10.15.3-linux-x64.tar.xz

解压

tar -xvf node-v10.15.3-linux-x64.tar

当前目录下软链一个 node 目录出来

这样做的好处是,未来升级版本非常方便,只需要更新这个软链就行

ln -s ./node-v10.15.3-linux-x64 ./node

通过软链接,将可执行程序放入系统环境变量的路径中

  • 查看当前系统中都有哪些环境变量路径
1
2
# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

可以看到我的列表中有:

  • /usr/local/bin
  • /usr/bin

大家约定成俗逻辑是:

  • /usr/bin下面的都是系统预装的可执行程序,会随着系统升级而改变。
  • /usr/local/bin 目录是给用户放置自己的可执行程序的地方

所以我推荐将软链放在 /usr/local/bin 目录下:

1
2
ln -s /opt/node/bin/node /usr/local/bin/node
ln -s /opt/node/bin/npm /usr/local/bin/npm

检查是否安装成功

1
2
3
4
[root@dc8 ~]# node -v
v10.15.3
[root@dc8 ~]# npm -v
6.4.1

Done

整理 Linux 常用命令

Posted on 2019-06-01

查看某个端口号被什么程序占用并杀掉此进程:

1
2
3
1. 查看某个端口的网络连接情况:`lsof -i:<port>`
2. 根据返回结果中的进程号检查进程名称 `ps -ef | grep <pid>`
3. 确认进程无误后杀掉该进程 `kill -9 <pid>`

scp 复制

1
2
3
4
# 文件
scp local_file remote_username@remote_ip:remote_folder
# 整个目录
scp -r local_folder remote_username@remote_ip:remote_folder

查看某个ip的端口号是否连通

1
telnet <ip> <port>

查看本机出口网络ip

1
curl ip.gs

压缩 tar.gz

1
tar -zcvf xxx.tar.gz xxx/

解压 tar.gz

1
tar -zxvf xxx.tar.gz

给一个文件添加可执行权限

1
sudo chmod +x xxx.sh

查看磁盘使用情况

1
df -h

查看内存使用情况

1
free -h
12…4

ruirui

37 posts
7 tags
© 2022 ruirui
Powered by Hexo
|
Theme — NexT.Muse v6.0.0