这篇文档来自于阮一峰老师的周刊,主题是私有方法是否需要测试,罗列了多方的观点和意见,如果对单元测试的范围也不太确定的话,强烈推荐看看这一篇文章。原文:https://jesseduffield.com/Testing-Private-Methods/

昨天,当我在一个Rust工作会议上,我不假思索的说:我想我们都同意在编写单元测试时,除非有特殊情况,否组都不应该直接测试私有方法。一个小型的辩论就展开了,许多人争论着互不相容的观点。我们很快结束了这场辩论,但是我还是有一点尴尬,我有点错判了开发人员的信条。

毫无疑问,在开发人员这个职业中,至少有一种观点大家现在都是都是同意的,对吧?再猜一次。如果你想知道这个讨论上的共识有多少,你可以通读一下Stack Overflow上的帖子: Unit testing private methods in C#, How to unit test this private method?, Should Private/Protected methods be under unit test? 。有人说我们应该总是直接测试私有方法,有的人说我们永远不应该直接测试私有方法。这些看法不可能都是对的!有没有最适合当前软件开发状况的观点?

关于测试私有方法的讨论,有五种流行的观点:

  • 首先就不使用私有方法

  • 总是测试私有方法

  • 永远不要测试私有方法

  • 有时可以测试私有方法

  • 将私有方法提取到类中

在这篇文章中,我将讨论每一个观点,然后总结提炼成我自己的经验法则,希望大多数人都能赞同它。注意,虽然我们将讨论类和方法,但是相同的观点也适用于函数式语言中的匿名函数。

观点1:首先就不使用私有方法

我会把这个观点展示出来,是因为大多数人都会认为它有点极端,如果它是对的,它会这个辩论的其他部分无效化。

这种观点与其说是对私有方法的攻击,不如说是对试图预测未来的攻击,这个想法是,在编写库代码的时候,你不可能事先知道你的使用者想使用什么方法,并且默认使用私有方法将为你和你的客户带来更多的问题相比于默认为公共方法(或者受保护方法)。这似乎是一种库开发人员独一无二的想法(见The case against private methods, )因为应用开发者可以用一下键盘就很容易让方法变成空的,而库的使用者需要分叉代码库或者提出问题等待回应。

这种也有不利的一面:将一个私有方法转变成公开方法很容易,但是从公开方法降级到私有方法是一个破坏的改动。此外,你的公共方法表明了你希望他们如何使用你的库。为了假设的使用场景,使原有的私有API膨胀你的公共API,你会使你的所有使用者更加难受,他们只想知道如何满足已知的使用场景。这些缺点是交织在一起的:使用者使用错误的方法与你的库交互,这反过来也使重构变得更加困难

阅读全文 »

一个常见的问题

在很多情况下,我们需要遍历一个对象的所有Key,就像这样

1
2
3
4
5
6
7
let marks = {
1: 'a',
2: 'b'
}

const values = Object.keys(marks).map(key=>marks[key])
// error: 元素隐式具有 "any" 类型,因为类型为 "string" 的表达式不能用于索引类型 "{ 1: string; 2: string; }"。在类型 "{ 1: string; 2: string; }" 上找不到具有类型为 "string" 的参数的索引签名。

Ts会立即报错,因为Object.keys的类型是这样

1
2
3
interface ObjectConstructor {
keys(o: object): string[];
}

这种时候我们往往需要对key进行断言

1
2
3
4
5
6
7
let marks = {
1: 'a',
2: 'b'
}

const values = Object.keys(marks).map(key=>marks[key as `${keyof typeof marks}`])
// no error

这段代码只是简单的对类型的断言,更准确的断言实际还应该排除symbol类型,因为对象的键也可能是symbol,但是Object.keys只会遍历对象的可枚举的字符串属性。

同时这里也将数字转换为字符串,因为对象的键除了symbol,都会被转换为字符串。

为什么Ts没有处理?

阅读全文 »

一道题目

最近准备回成都,开始了各种面试,这次面试官出了一道函数坷里化的题,题目如下:

实现如下函数

1
2
3
sum(1)       // 1 
sum(1)(2)(3) // 6
sum(1)(2)(3)(4) // 10

面试的时候这道题愣了一下,没有答出来,主要纠结了一下在不定参的时候,如果能够链式调用,那一定需要返回的是一个函数,按照题目要求,值需要既是一个函数,又是一个数字。

面试结束后仔细想了下,正好想到之前看《你不知道的JavaScript》中的对于对象类型的toPrimitive操作,面试后很快写了个Demo出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
function sum(...args){
let previousArgs = []
const func = (...args)=>{
previousArgs = previousArgs.concat(args)
return func
}

func.valueOf = ()=> previousArgs.reduce((acc, currentVal)=> acc + currentVal)

return func(...args)
}

console.log(sum(2)(3) + 2) // 7

这里实际上关于使用toString以及Symbol.toPrimitive可以达成效果的,但具体原理有一些差别。

这里就借助这个机会回顾一下Js整个类型转换逻辑。

隐式类型转换

总所周知,js作为一种弱类型语言,在需要的场景下,会进行隐式的类型转换,为此 ECMA262 Type Conversion 中规定了一系列类型转换的抽象方法,以及在各种运算符操作及函数中规定了如何去调用这些类型转换方法。

阅读全文 »

缘由

最近重新准备写博客的时候,发现hexo已经更新了,支持以包的形式引用主题,按照文档指引重新配置了主题。之前博客构建使用的是hexo官网的Travis Ci,配置直接复制过去改改就好了,这次更新主题看到仓库上大大的GitHub Actions,手痒痒,就想试一下用GitHub Actions 进行自动构建。

构建产物

GitHub Actions的快速上手文档异常的贴心,而且还是以npm构建为例子,前端基本可以很快上手~

GitHub Actions 的构建任务有四个层级

  • workflow: 工作流,整个运行过程
  • job: 任务,工作流可以包含一个或多个任务,每个任务都是独立的机器运行
  • step: 步骤。每一个任务可以执行一个或多个步骤,按顺序执行
  • action: 动作,每一个步骤可以运行一个或者多个命令

其中job是分别在不同机器上并发执行的,也可以通过needs参数和条件判断决定执行先后顺序,不过目前我们不需要多个机器并发执行。

上面其实是一些概念,实际写一个简单的构建配置是十分简单的。

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
name: Deploy
on:
# 指定在main分支push时触发构建
push:
branches: [ main ]
# 允许手动点击触发构建
workflow_dispatch:

jobs:
# 任务名称
build-and-deploy:
# 运行环境为最新稳定版本的
# ubuntu
runs-on: ubuntu-latest
steps:
# 拉取这次提交后的代码
- uses: actions/checkout@v2
# 安装并设置node版本为14,并启用缓存
- uses: actions/setup-node@v2
with:
node-version: '14'
cache: 'npm'
# 安装依赖并执行构建命令
- name: Clean install dependencies and build
run: |
npm ci
npm run build

其中use类似于npm包,可以使用其他人预先写好的构建指令及脚本,通过with传递参数。

阅读全文 »

最近有空阅读了一下 Vue3 的文档,发现 Vue 也新增了类似 React Hooks 中的 useEffect 的 watchEffect,两者基本很相似,这里就来比较比较。

useEffect

React Hooks 带有一种函数式的设计理念,期望 UI = f(x),UI 是纯函数的渲染结果,useEffect 则用来处理副作用,也就是对外部环境的影响,在 Hooks 中基本取代了生命周期的概念,类似于原本的componentDidMountcomponentWillUnmount

常见的在 useEffect 中请求数据

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
const example = (props) => {
const [count, setCount] = useState(0);

useEffect(() => {
let hasCancle = false;
query("/xxxx").then((data) => {
// 避免副作用被取消后仍然调用
if (!hasCancle) {
const { count } = data;
setCount(data);
}
});
return () => {
// 清除副作用
hasCancle = true;
};
}, []);

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
};

watchEffect

watchEffect 作用基本与 useEffect 作用一致,上面的代码可以很方便的改写为 Vue 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
setup(props){
const count = ref(0);
watchEffect( onInvalidate => {
let hasCancle = false;
query("/xxxx").then((data) => {
if (!hasCancle) {
const { count } = data;
setCount(data);
}
});

onInvalidate(()=>{
hasCancle = false
})
})
}

尽管功能上 watchEffect 与 useEffect 十分相似,但由于实现不一,实质还是有很多不同

调用时机

阅读全文 »

Cookie由来

谈到Cookie不得不谈到http协议,wiki上的定义⬇

设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法

由于这个设计目的,早期http协议被设计为无状态的协议,一个HTTP协议通信过程往往是建立连接>传输内容>关闭连接,整个过程十分简单。

然而万维网发展的比想象中快太多,HTML不再只是单纯的文档,还被用于交互,有了登陆注册等保存状态的需要,然而http协议是无状态的,这个场景需要下,Cookie就诞生了。

Cookie具体是怎么样的?

Cookie其实就是一段文本信息,由浏览器储存在本地硬盘上,服务器通过Set-Cookie设置Cookie,浏览器通过Cookie字段附上设置的Cookie信息。

emm,还是看一个完整的请求吧


响应

阅读全文 »

最近突然在 chrome 85 版本上遇到下载后会出现”xxx 下载方式实属异常 因此它可能存在危险”,去看了下相关的谷歌博客,定位是谷歌的浏览器安全策略引起的。
实际上浏览器下载还有不少坑,这次就接这个机会总结一下。

前端如何实现文件下载?

window.open

在 download 属性出现之前,前端实现文件下载实际上依赖于浏览器的默认行为,也就是打开链接。

通常我们可以直接这样实现

1
2
const url = "http://xxxx";
window.open(url);

这会以指定的 url 打开一个新窗口,浏览器在根据 url 获得到服务器的响应,判断出这不是一个浏览器可以打开的文件类型,就自动会转为下载。

在段代码在正常情况下是能够工作正常的,然而实际上有些业务场景,下载链接是后台生成的,我们需要这样做

1
2
3
4
5
6
7
8
9
10
11
clickHandle() {
api.get('xxx', params).then(response => {
let data = response.data;
if(data.code === 0 ) {
let url = data.data.url || '';
if (url) {
window.open(url);
}
}
});
}

这在几乎所有的现代浏览器上都会被阻止,在 Chrome 上会在地址栏显示被拦截的标志,用户需要手动点击才能成功,Safari 会静默失效。
这是由于早期网页经常在页面插入自动打开广告页面,带来很糟糕的用户体验,因此浏览器做了限制,会禁止所有的异步回调中调用的 window.open()

阅读全文 »

自己来实现观察者模式

今天写自己一个小demo时使用mvc的时候,想自己来实现观察者模式
也就是view层会观察model层数据的变化,相应渲染出改变后的数据

使用发布订阅模式

最开始想的办法是自己model拥有多个修改model.data的方法,如fetch(),patch(),delete(),post(),只需要每个方法里加一句

1
event.emit('dataChanged',this.data)

就能通知在监听的view层进行渲染了

不过有点不优雅,我需要在每个改变数据的方法后都加一句,很麻烦。

使用Object.defineProperty来自动化

之后想到了ES5提供的**Object.defineProperty**提供的getset存取描述符,对这个属性的访问和读取会分别触发getset函数, 也就是如果对data进行设置,就会触发set函数。

get()set()又是什么?

阅读全文 »

这部分其实应该叫做吐槽合集,作者在这部分大量吐槽了把 js 中原型理解成类的做法。现在的前端社区中,如 React,Vue 也是更少利用类的一些特性,而是使用了混入,类似于该书中讲的行为委托模式;而在 React Hooks 和 Vue composition api 中,则更激进的几乎完全去掉了 Class 风格的代码。

读了这部分,就能了解社区为什么不太喜欢 Class 风格的代码。

从 this 讲起

this

this 也是 js 中新手很头疼的问题了,各种归纳总结 this 的方法随处可见。这里也贴下书中对 this 的定义

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。
this 就是这个记录的一个属性,会在函数执行的过程中用到。

借助 this 我们可以很方便的来传递上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 隐式绑定this
const Foo = {
name: "Foo",
speak() {
console.log(this.name);
},
};

Foo.speak(); // Foo

const Boo = {
name: "Boo",
};

// 显式绑定this
Foo.speak.call(Boo); // Boo

// 传统传递上下文
function speak(ctx) {
console.log(ctx.name);
}

speak(Foo); // Foo
speak(Boo); // Boo

书里归纳了五种 this 的绑定方式,就以上面的例子为例

  1. 默认绑定 speak() this = window
  2. 隐式绑定 Foo.speak() this = Foo
  3. 显式绑定 Foo.speak.call(Boo) this = Boo
  4. 硬绑定 bind Foo.speak.bind(Boo) this = Boo
  5. new 绑定 new Foo.speak this = {}(es5)
阅读全文 »

前言

自己初中的时候有过一段时间很好奇密码学相关的问题,当时就了解到了非对称加密,觉得很神奇,只要被公钥加密,就只有私钥才能解开,当时想,加密过程不过就是按照规则进行一些数学计算而已,怎么会不可逆呢?非对称加密是怎么做到这些的?怎么让网络变成可信的?
当时百度还挺可靠的,搜索到了一篇讲的很好文章,大概有了认知,可惜那篇文章找不到了;今天正好看到一个前端问题:https 原理是什么?就再学习了一次,写这篇博客记录下来。

https 为什么出现?

因为网络链路默认是不可信任的,HTTP 在整个传输过程中无法保证数据不被窥探、篡改,常见的中间人攻击就是利用了这个原理,日常经常发现的网页出现莫名奇妙的新增广告,通常叫做运营商劫持,就是一种中间人攻击。

https 如何防止中间人攻击的?

https 在 TCP 传输层和 HTTP 应用层之间再加了一层 SSL/TSL(传输层安全协议),用于对 HTTP 报文进行加密。

SSL/TSL 采用基于公钥的加密算法,比如最常用的RSA 算法

RSA 是一种非对称加密算法,在了解非对称加密前,先了解一下对称加密,对称加密指的是在加密或者解密时使用相同的密钥。就像是为数据上一把锁,同时也要把钥匙拿给解密的人。
使用对称加密的过程往往是这样的:

  • A 生成随机密钥 K
  • A 与 B 通过某种方式
  • A 密钥 K 加密明文数据
  • A 传输密文数据
  • B 使用密钥 K 解密密文
  • 得到明文数据
    对称加密的一大问题就是存在密钥交换环节,想要解密必须传输密钥,而传输密钥这个环节,往往容易泄露密钥,整个加密环节也就不安全了。

非对称加密算法就解决了这个问题,密钥加密解密使用不同的密钥,避免了密钥交换环节,最著名的非对称加密算法就是 RSA 算法

阅读全文 »
0%