问题:使用 axios 发送请求会拦截响应头中的 set-cookie,导致 cookie 丢失

https://github.com/axios/axios/issues/953

在控制台中也找不到 cookie

console.log(res.headers['set-cookie']) // undefined
console.log(document.cookie) // ''

解决方式:修改 axios 配置

axios.defaults.withCredentials = true // 默认是 false

// Axios.interceptors.request.use(
// config => {
// config.withCredentials = true // 添加
// return config
// })

axios.create({
withCredentials: true // 表示跨域请求时是否需要使用凭证 默认 false
})

设置 withCredentials = true 会造成跨域,需要使用代理解决跨域问题

谷歌浏览器扩展程序管理页面 chrome://extensions/

方式一 .crx

打开扩展程序管理页面,将 .crx 格式插件拖入浏览器即可

若安装时出现 “程序包无效 CRX-HEADER-INVALID” 的报错信息,或出现 该扩展程序未列在 Chrome 网上应用店中,并可能是在您不知情的情况下添加的,且插件无法启用,可使用方式二安装

方式二 .zip

将 .crx 插件后缀改成 .zip,或下载 .zip 格式插件

扩展程序管理页面,开启开发者模式,再按 F5 刷新一下,将 .zip 格式插件拖入浏览器,或者将 .zip 格式插件解压,点击加载已解压的扩展程序

其他

禁用 Chrome 的 “请停用以开发者模式运行的扩展程序” 提示的方法

用于注册临时账号,保护隐私

临时邮箱

https://bccto.me/
https://10minutemail.net/
http://24mail.chacuo.net/
http://www.yopmail.com/zh/
https://shorttimemail.com/zh-Hans/
https://temp-mail.org/zh/
https://t.odmail.cn/
https://9em.org/
https://maildrop.cc

https://eskiimo.com 匿名发送邮件

短信验证码平台

国内平台
https://www.pdflibr.com
https://www.visitorsms.com/cn
https://www.becmd.com
http://www.114sim.com
https://yunduanxin.net
http://www.smszk.com
http://z-sms.com
http://www.shejiinn.com
https://sms.cngrok.com

国外平台

https://ch.freephonenum.com
https://smsreceivefree.com
https://zh.mytrashmobile.com
https://www.receive-sms-online.info
https://receiveasms.com
https://sms-online.co/receive-free-sms
https://receive-sms.com

无法注册原因

  • 此电话号码无法用于进行验证

解决思路

  • 通过其他页面或方式进入谷歌注册入口,如通过 Google Voice 或 Google Analysis 等进入

  • 用安卓手机自带的电子邮件客户端申请

  • 在选择邮箱时,切换 “改用我目前的邮件地址” 为 “注册新的 Gmail 邮箱”

  • 选择适当的语言以及模式(页面左下角语言设置)

  • Chrome 语言设置为 English-United States

成功案例

  • 2019/08/06 小米手机 - 使用自带的电子邮件客户端 - 用国内手机号 - QQ 邮箱 - 注册成功

  • 2020/01/05 小米手机 - 使用自带的电子邮件客户端 - 用国内手机号 - 注册新的 Gmail 邮箱 - 注册成功

  • 2020/01/19 苹果手机 - 使用自带的邮件客户端 - 国内手机号 - 注册新的 Gmail 邮箱 - 注册成功

  • 2020/12/29 苹果手机 - 使用自带的邮件客户端 - 国内手机号 (已被使用过)- 注册新的 Gmail 邮箱 - 注册成功

  • 2021/04/06 w10 - 谷歌浏览器 + WebRTC Leak Prevent 插件(在无痕模式下启用 + IP handling policy:Disabled non-proxied UDp (froce proxy) )- 国内手机号 (已被使用过)- 注册新的 Gmail 邮箱 - 注册成功

https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener

添加事件

// 语法
target.addEventListener(type, listener[, useCapture])
// type: 事件的类型: click mouseover 字符串类型,不带 on
// listener: 函数,每次点击,执行这个函数
// useCapture: 可选,true: 事件在捕获阶段执行,false: 事件在冒泡阶段执行(默认)

target.addEventListener(type, listener[, options])

options 可选,可用的选项如下:
capture: Boolean,默认 false,等价于以前的 useCapture 参数
once: Boolean,默认 false,如果是 true,表示 listener 在添加之后最多只调用一次。 listener 也会在其被调用之后自动移除
passive: Boolean,默认 false,设置为 true 时,表示 listener 永远不会调用 preventDefault () 如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。

浏览器无法预先知道一个监听器会不会调用 preventDefault (),它能做的只有等监听器执行完后再去执行默认行为,而监听器执行是要耗时的,有些甚至耗时很明显,这样就会导致页面卡顿

.passive 修饰符尤其能够提升移动端的性能
vue .passive 事件修饰符

移除事件

removeEventListener

在第三个参数是布尔值的时候,addEventListener (“foo”, listener, true) 添加的监听器,必须用 removeEventListener (“foo”, listener, true) 才能删除掉,因为这个监听器也有可能还注册在了冒泡阶段,如果第三个参数为 false 则直接通过 removeEventListener (“foo”, listener) 就可以删除

通过 addEventListener (“foo”, listener, {capture: true}) 添加的监听器删除时也同样需要添加 {capture: true} 来删除,当然 {capture: true} 换成 true 也可以

通过 addEventListener (“foo”, listener, {passive: true}) 添加的监听器直接通过 removeEventListener (“foo”, listener) 就可以删除了
因为一个监听器同时是 passive 和非 passive(以及同时是 once 和非 once)是说不通的,如果你添加了多个,那么后添加的会忽略

removeEventListener (“foo”, listener, {capture: true}) // {capture: true} 必须加,当然 {capture: true} 换成 true 也可以

原生 js abort () 方法

let A = $.ajax({})
A.abort()

Axios 提供了一个 CancelToken 的函数,这是一个构造函数,该函数的作用就是用来取消接口请求的

methods: {
getMsg () {
let CancelToken = axios.CancelToken
let that = this
axios.get('', {
cancelToken: new CancelToken(function executor(c) {
that.cancel = c
console.log(c)
// 这个参数 c 就是 CancelToken 构造函数里面自带的取消请求的函数,这里把该函数当参数用
})
params: {}
}).then(res => {
this.items = res.data
}).catch(err => {
console.log(err)
})
},
cancelGetMsg () {
this.cancel()
}
}

1. ios 移动端页面对点击事件有 300ms 延时

使用 fastclick 库 https://github.com/ftlabs/fastclick

使用 FastClick 的时候,在需要使用的层上,实例化它。我们使用 document.body 是因为希望所有的按钮和链接都获得快速点击

import FastClick from 'fastclick'
FastClick.attach(document.body)

2. ios 滚动卡顿

div {
-webkit-overflow-scrolling: touch; /* 当手指从触摸屏上移开,会保持一段时间的滚动 */
/* -webkit-overflow-scrolling: auto; */ /* 当手指从触摸屏上移开,滚动会立即停止 */
}

3. ios 1px border 变宽

以 dpr = 2 为例:
你拿到一张标准的基于 iphone6 的设计稿 (750px)
你看到它设计的一个 border 宽度是 1px
你兴致勃勃地写下了 border: 1px solid #000;
然而 iphone6 实际渲染像素是 375px,那么设计需要 border 的其实是 border: 0.5px solid #000;
然后你的是 1px
不是 1px 变粗了,只是实际只是需要 0.5px 而已

<meta name="viewport" content="width=device-width"> 意思是将物理设备的宽度设置给当前浏览器

在使用 table 标签设置 border: 1px 并使用 border-collapse: collapse; 合并边框后,发现 td 之间的边框宽度并不是 1px,而是比 1px 宽,大概为 1.5px

4. webapp 软键盘弹起时问题

其他参考链接 https://segmentfault.com/a/1190000010693229
https://github.com/ioing/IOING

页面放大:

<meta name="viewport" content="user-scalable=no" />

输入框被遮挡,看不见输入的内容: document.activeElement.scrollIntoView()

页面自动上移,但收回软键盘时页面没有恢复原样少了一截

fixed 定位效果失效: ios 弹出软键盘的时候, webview 的高度没有变化导致超出屏幕范围,并且不会触发 resize 事件

scrollIntoView 与 scrollIntoViewIfNeeded

Element.scrollIntoView(option) 方法让当前的元素滚动到浏览器窗口的可视区域内
option 如果为 true,元素的顶端将和其所在滚动区的可视区域的顶端对齐, 默认
option 如果为 false,元素的底端将和其所在滚动区的可视区域的底端对齐

element.scrollIntoViewIfNeeded() 用来将不在浏览器窗口的可见区域内的元素滚动到浏览器窗口的可见区域。 如果该元素已经在浏览器窗口的可见区域内,则不会发生滚动

var scrollHeight = document.documentElement.scrollTop || document.body.scrollTop || 0
var clientHeight = document.documentElement.clientHeight || document.body.clientHeight
var innerHeight = window.innerHeight
// 键盘弹起时 scrollHeight innerHeight 发生变化
<input @focus="input(1)" @blur="input(2)" />
input (num) {
var u = navigator.userAgent
if (u.indexOf('Android') > -1 || u.indexOf('Linux') > -1) {
// 安卓手机通过 resize 事件监听键盘事件,因为部分手机手动关闭键盘时并不会失焦
} else if (u.indexOf('iPhone') > -1) {
// 苹果手机
if (num === 1) {
// 键盘弹起
document.activeElement.scrollIntoView()
} else {
// 键盘隐藏
var scrollHeight = document.documentElement.scrollTop || document.body.scrollTop || 0
// window.scrollTo(0, Math.max(scrollHeight - 1, 0))
window.scrollTo(0, 0)
}
}
}

附:安卓手机监听 resize

data () {
return {
originalHeight: document.documentElement.clientHeight || document.body.clientHeight,
resizeHeight: document.documentElement.clientHeight || document.body.clientHeight
}
},
watch: {
resizeHeight (val) {
var u = navigator.userAgent
if (u.indexOf('Android') > -1 || u.indexOf('Linux') > -1) {
// var detail = document.querySelector('.detail')
if (this.resizeHeight - 0 < this.originalHeight - 0) {
// detail.style.paddingBottom = '260px'
document.activeElement.scrollIntoView({ behavior: 'auto', block: 'start' })
} else {
// detail.style.paddingBottom = '0'
}
}
}
},
mounted () {
const that = this
window.onresize = () => {
return (() => {
window.resizeHeight = document.documentElement.clientHeight || document.body.clientHeight
that.resizeHeight = window.resizeHeight
})()
}
}

5. 手机端页面文件上传兼容性问题

6. 移动端和 PC 端中的 hover 处理 移动端点击时会有 pc 端 hover 效果

百度一下给出的方式是注册 touchstart 事件

document.body.addEventListener('touchstart', function() {})

但经过测试并没有解决该问题

7. 移动端 touch 时,会触发 pc 端的 mouseenter mouseleave 事件

在事件中通过判断屏幕宽度解决

8. ios a 链接 input type=”file” 等在点击时会出现灰色(touch 高亮)

-webkit-tap-highlight-color: transparent;

9. 禁用浏览器自动调整字体大小

移动端浏览器切换橫向模式时会调整字体大小(字体变大),解决方式:

html {
-webkit-text-size-adjust: none; /* 或 100% */
}

谷歌浏览器已不支持这个属性了,不能通过该方式实现小于 12px 的字体,可使用缩放(transform:scale (0.8))来实现小于 12px 的字体

10. appearance 属性

normal|icon|window|button|menu|field
所有主流浏览器都不支持 appearance 属性

-webkit-appearance: none; 去除默认样式,使 ios 端和安卓端显示效果一样,但有一个问题,input 的 checkbox 和 radio 类型在安卓端可能无法正常显示

11. 禁止长按

在 iOS 上,当你触摸并按住触摸的目标,比如长按一个链接,浏览器将显示链接有关的系统默认菜单,
长按图像弹出选项存储或者拷贝图像,长按文字弹出选择文字菜单

可通过如下方式禁止这些行为

禁止长按图片保存

img {
-webkit-touch-callout: none;
pointer-events: none; // 像微信浏览器还是无法禁止,加上这行样式即可
}

禁用长按复制

user-select: none;

禁止长按呼出菜单

div {
-webkit-touch-callout: none;
}

12. 点击穿透

假如页面上有两个元素 A 和 B。B 元素在 A 元素之上。我们在 B 元素的 touchstart 事件上注册了一个回调函数,该回调函数的作用是隐藏 B 元素。我们发现,当我们点击 B 元素,B 元素被隐藏了,随后,A 元素触发了 click 事件。
这是因为在移动端浏览器,事件执行的顺序是 touchstart > touchend > click。而 click 事件有 300ms 的延迟,当 touchstart 事件把 B 元素隐藏之后,隔了 300ms,浏览器触发了 click 事件,但是此时 B 元素不见了,所以该事件被派发到了 A 元素身上。如果 A 元素是一个链接,那此时页面就会意外地跳转。
跨页面点击穿透问题 点击页内按钮跳转至新页,然后发现新页面中对应位置元素的 click 事件被触发了

13. 移动端 video 播放时不弹出页面层

<video x5-playsinline="" playsinline="" webkit-playsinline=""></video>

14. vue 移动端监听 scroll

mounted () {
document.querySelector('.list').addEventListener('scroll', () => {
this.scroll = document.querySelector('.list').scrollTop
})
}

15. vue 移动端列表保存滚动位置

beforeRouteEnter (to, from, next) {
next((vm) => {
// console.log(vm.$route.meta.scrollTop)
if (vm.$route.meta.scrollTop) {
document.querySelector('.list').scrollTop = vm.$route.meta.scrollTop
}
})
},

beforeRouteLeave (to, from, next) {
if (to.name === 'invite') {
this.$store.commit('removeIncludeComponent', 'staff')
// next()
}
if (to.name === 'staffDetail') {
let top = document.querySelector('.list').scrollTop
console.log(top)
this.$route.meta.scrollTop = top
}
next()
}

16. 移动端去除 type 为 number 的箭头

input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none !important;
margin: 0;
}

17. 设置状态栏的背景颜色(IOS)

设置状态栏的背景颜色,只有在”apple-mobile-web-app-capable” content=”yes” 时生效

<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />

content 参数:

default :状态栏背景是白色。
black :状态栏背景是黑色。
black-translucent :状态栏背景是半透明。 如果设置为 default 或 black , 网页内容从状态栏底部开始。
如果设置为 black-translucent , 网页内容充满整个屏幕,顶部会被状态栏遮挡。

18. 弹出数字键盘

<!-- 有"#" "*"符号输入 -->
<input type="tel" />

<!-- 纯数字 -->
<input pattern="\d*" />

打开原生应用

某些浏览器会禁用此协议,比如微信内部浏览器(除非开了白名单)

<a href="weixin://">打开微信</a>
<a href="alipays://">打开支付宝</a>
<a href="alipays://platformapi/startapp?saId=10000007">打开支付宝的扫一扫功能</a>
<a href="alipays://platformapi/startapp?appId=60000002">打开支付宝的蚂蚁森林</a>

解决 IOS 下 active 伪类失效

给 body 注册一个空事件即可

<body ontouchstart></body>

最简单的 rem 实现

@media (min-width: 640px) {
html {
font-size: calc(640px / 3.75);
}
}
@media (min-width: 320px) and (max-width: 640px) {
html {
font-size: calc(100vw / 3.75);
}
}

better-scroll 解决移动端滚动问题

https://github.com/ustbhuangyi/better-scroll/

iphone X 适配 - 安全区域 (safe area)

参考:https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/

在 iOS 11 中采用了 viewport-fit 的 meta 标签作为适配方案

viewport-fit 取值

auto 默认:页面内容显示在 safe area 内
cover 页面内容充满屏幕,可以通过添加边距保证网页主要内容处于 safe area 中不被裁剪

iOS 11 的 webview 引入了 constantenv 来处理 viewport-fit=cover 属性,以及一组四个预定义的常量:safe-area-inset-left, safe-area-inset-right, safe-area-inset-topsafe-area-inset-bottom,分别表示 safe area 和可视窗口 viewport 顶部,右边,左边,底部的间距,可以用于设置 margin 和 padding 或者绝对定位时 left 和 top,这四个常量只有在 viewport-fit=cover 时有效

建议使用 viewport-fit=cover 因为会使用 auto 会造成屏幕四周出现白边

<meta name="viewport" content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
/*  top 为 0 的 fixed 定位的 heade */
header {
position: fixed;
top: 0;
/* ... */
/* Status bar height on iOS 10 */
padding-top: 20px;
/* Status bar height on iOS 11.0 */
padding-top: constant(safe-area-inset-top);
/* Status bar height on iOS 11+ */
padding-top: env(safe-area-inset-top);
}
body {
padding-top: constant(safe-area-inset-top); // 为导航栏+状态栏的高度 88px
padding-left: constant(safe-area-inset-left); // 如果未竖屏时为0
padding-right: constant(safe-area-inset-right); // 如果未竖屏时为0
padding-bottom: constant(safe-area-inset-bottom); // 为底下圆弧的高度 34px
}

类型识别

获取数据类型,返回结果为 Number、String、Object、Array 等

// 返回数据类型
function getRawType(value) {
return Object.prototype.toString.call(value).slice(8, -1)
}

// 正则 => RegExp
// 时间对象 => Date
// 字符串 => String
// 对象 => Object
// 数组 => Array

判断变量是不是字符串类型

Object.prototype.toString.call('str') // '[object String]'
typeof 'str' // 'string'

判断变量是不是引用类型

例如: arrays, functions, objects, regexes, new Number (0), 以及 new String (‘’)

function isObject(value) {
let type = typeof value
return value != null && (type == 'object' || type == 'function')
}

判断变量是不是 Object 类型的数据

function isPlainObject(value) {
return Object.prototype.toString.call(value) === '[object Object]'
}

判断变量是不是数组类型

function isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]'
}

function isArray(arr) {
return Array.isArray(arr)
}

将 isArray 挂载到 Array 上

Array.isArray = Array.isArray || isArray

格式转换

数字格式化:小于 10 的数值前面加上 0

/**
* @param {number} num 要格式化的数值
* @return {string} 把小于10的数值前面加上0
*/
function prefix_zero(num) {
return num >= 10 ? num : '0' + num
}

千分位格式化数字

(1234567 => 1,234,567.00)

/**
* @param {number} price 价格
* @returns {string} 1234567 => 1,234,567.00
*/
function formatPrice(price) {
if (typeof price !== 'number') return price
return String(Number(price).toFixed(2)).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}

// \b 匹配单词边界
// \B 匹配非单词边界

其他方式

let a = 123456789 // a 为number类型
a.toLocaleString('en-US') // '123,456,789'

let b = 123456789 // b 可为number类型或string类型
Intl.NumberFormat().format(b) //'123,456,789'

手机号格式化:隐藏中间四位数字

/**
* @param {string} mobile 手机号
* @returns {string}
*/
function formatMobile(mobile) {
mobile = String(mobile)
if (!/\d{11}/.test(mobile)) {
return mobile
}
return mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}

手机号格式化:3-4-4 分割

watch: {
phoneNum(newValue, oldValue) {
this.phoneNum = newValue.length > oldValue.length ? newValue.replace(/\s/g, '').replace(/(\d{3})(\d{0,4})(\d{0,4})/, '$1 $2 $3') : this.phoneNum.trim()
}
}

进制转换

parseInt(str, radix) // 任意进制转换为 10 进制整数值

Number.toString(radix) //返回表示该数字的指定进制形式的字符串

检测平台(设备)类型

isWechat = /micromessenger/i.test(navigator.userAgent)
isWeibo = /weibo/i.test(navigator.userAgent)
isQQ = /qq/i.test(navigator.userAgent)
isIOS = /(iphone|ipod|ipad|ios)/i.test(navigator.userAgent)
isAndroid = /android/i.test(navigator.userAgent)

数组顺序上移下移

// 对象数组顺序上移下移
// arr: 目标数组

// 上移
arr[index] = arr.splice(index - 1, 1, arr[index])[0]

// 下移
arr[index] = arr.splice(index + 1, 1, arr[index])[0]

快速创建 a 标签

let a = '超链接'.link('https://wqdy.top')
console.log(a) // <a href="https://wqdy.top">超链接</a>

正则进阶:

捕获括号:

匹配 'wqdy' 并且记住匹配项
/(wqdy)/

非捕获括号:

匹配 'wqdy' 但是不记住匹配项
/(?:wqdy)/

先行断言:

匹配'wqdy'仅仅当'wqdy'后面跟着'top'
/wqdy(?=top)/

后行断言:

匹配'top'仅仅当'top'前面是'wqdy'
/(?<=wqdy)top/

正向否定查找:

匹配'wqdy'仅仅当'wqdy'后面不跟着'gkd'
/wqdy(?!gkd)/

判断是否有滚动条

function isScroll() {
return window.innerWidth - $(document).width() !== 0
}

mixin 混合

可以在 mixin 中使用类选择器和 id 选择器

.bgc 定义了一个属性集,在任何需要使用 .bgc 属性集的选择器中,只需像下面这样调用:(小括号是可选的)

.bgc {
background-color: #ccc;
}
div {
color: #f00;
.bgc();
}

编译后的 CSS 代码为:

.bgc {
background-color: #ccc;
}

div {
color: #f00;
background-color: #ccc;
}

总结:mixin 其实就是一种嵌套,简单的讲,mixin 就是规则级别的复用

mixin 的定义也会被原封不动的输出到编译生成的 CSS 代码中

如果希望编译生成的 CSS 代码中不包含 mixin 的定义,在定义 mixin 时,只需在 class、id 的后面添加一对小括号即可。如:

.bgc() {
background-color: #ccc;
}
div {
.bgc;
}

编译后的 CSS 代码为:

div {
background-color: #ccc;
}

mixin 可以包含选择器

.hover() {
&:hover {
background-color: #ccc;
}
}
div {
.hover;
}

编译后的 CSS 代码为:

div:hover {
background-color: #ccc;
}

命令空间 Namespaces

如果想要在一个更复杂的选择器中混合属性,可以堆叠多个 id 或类

可以将 mixin 置于 id 选择器之下,这样可以确保它不会和另一个库冲突

#bgc {
.inner() {
color: red;
}
}

div {
#bgc.inner;
}

!important 关键字

在 mixin 后使用!important 关键字,将会标记调用所有继承的属性为!important

.bgc {
background-color: #ccc;
}

div {
.bgc !important;
}

// 编译后 css 为
div {
background-color: #ccc !important;
}

带参数的 Mixin

mixin 还可以接受参数,这些参数在混合时传递给选择器块

从上面的代码可以看出:mixin 其实就是一种嵌套,简单的讲,mixin 就是规则级别的复用。除了类选择器外,你也可以使用 id 选择器来定义 mixin。

高阶函数

高阶函数定义:将函数作为参数或者返回值是函数的函数
常见的 sort、reduce 等函数就是高阶函数

function add(a) {
return function(b) {
return a + b
}
}

// es6写法
let add = (a) => (b) => a + b

var sum = add(1)(2) // 3

柯里化

wiki 的柯里化定义:把接受多个参数的函数变换成接受一个单一参数的函数,并且返回(接受余下的参数而且返回结果的)新函数的技术

柯里化后,将第一个参数变量存在函数里面了 (闭包),然后本来需要 n 个参数的函数变成只需要剩下的(n - 1 个)参数就可以调用

let add = (a) => (b) => a + b
let add1 = add(1)
add1(2) // 1 + 2 = 3

add 函数按照定义可以理解成只柯里化了一次,n 个连续箭头组成的函数实际上就是柯里化了 n - 1 次
前 n - 1 次调用,其实是提前将参数传递进去,并没有调用最内层函数体,最后一次调用才会调用最内层函数体,并返回最内层函数体的返回值

所以多个连续箭头函数就是多次柯里化的 es6 写法

应用:

函数懒执行
函数式编程

柯里化函数使用场景:

减少重复传递不变的参数

function discount(price, discount) {
return price * discount
}

// 每次都要重复传入 discount 参数,可以将这个函数柯里化
function discount(discount) {
return (price) => {
return price * discount
}
}
const tenPercentDiscount = discount(0.9)
const twentyPercentDiscount = discount(0.8)

// 现在每次计算价格只需要:
tenPercentDiscount(500) // 500 * 0.9
twentyPercentDiscount(1000) // 1000 * 0.8

柯里化实现

function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args)
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2))
}
}
}
}

function sum(a, b, c) {
return a + b + c
}

let curriedSum = curry(sum)
console.log(curriedSum(1, 2, 3)) // 6
console.log(curriedSum(1)(2, 3)) // 6

偏函数