实现数组的乱序输入
次要的实现思路就是:
- 取出数组的第一个元素,随机产生一个索引值,将该第一个元素和这个索引对应的元素进行替换。
- 第二次取出数据数组第二个元素,随机产生一个除了索引为 1 的之外的索引值,并将第二个元素与该索引值对应的元素进行替换
- 依照下面的法则执行,直到遍历实现
var arr = [1,2,3,4,5,6,7,8,9,10];
for (var i = 0; i < arr.length; i++) {const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i;
[arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
}
console.log(arr)
还有一办法就是倒序遍历:
var arr = [1,2,3,4,5,6,7,8,9,10];
let length = arr.length,
randomIndex,
temp;
while (length) {randomIndex = Math.floor(Math.random() * length--);
temp = arr[length];
arr[length] = arr[randomIndex];
arr[randomIndex] = temp;
}
console.log(arr)
Array.prototype.filter()
Array.prototype.filter = function(callback, thisArg) {if (this == undefined) {throw new TypeError('this is null or not undefined');
}
if (typeof callback !== 'function') {throw new TypeError(callback + 'is not a function');
}
const res = [];
// 让 O 成为回调函数的对象传递(强制转换对象)const O = Object(this);
// >>>0 保障 len 为 number,且为正整数
const len = O.length >>> 0;
for (let i = 0; i < len; i++) {
// 查看 i 是否在 O 的属性(会查看原型链)if (i in O) {
// 回调函数调用传参
if (callback.call(thisArg, O[i], i, O)) {res.push(O[i]);
}
}
}
return res;
}
模仿 new
new 操作符做了这些事:
- 它创立了一个全新的对象
- 它会被执行[[Prototype]](也就是__proto__)链接
- 它使 this 指向新创建的对象
- 通过 new 创立的每个对象将最终被 [[Prototype]] 链接到这个函数的 prototype 对象上
- 如果函数没有返回对象类型 Object(蕴含 Functoin, Array, Date, RegExg, Error),那么 new 表达式中的函数调用将返回该对象援用
// objectFactory(name, 'cxk', '18')
function objectFactory() {const obj = new Object();
const Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
const ret = Constructor.apply(obj, arguments);
return typeof ret === "object" ? ret : obj;
}
实现节流函数(throttle)
节流函数原理: 指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的工夫才会执行回调函数。总结起来就是:事件,依照一段时间的距离来进行触发。
像 dom 的拖拽,如果用消抖的话,就会呈现卡顿的感觉,因为只在进行的时候执行了一次,这个时候就应该用节流,在肯定工夫内屡次执行,会晦涩很多
手写简版
应用工夫戳的节流函数会在第一次触发事件时立刻执行,当前每过 wait 秒之后才执行一次,并且最初一次触发事件不会被执行
工夫戳形式:
// func 是用户传入须要防抖的函数
// wait 是等待时间
const throttle = (func, wait = 50) => {
// 上一次执行该函数的工夫
let lastTime = 0
return function(...args) {
// 以后工夫
let now = +new Date()
// 将以后工夫和上一次执行函数工夫比照
// 如果差值大于设置的等待时间就执行函数
if (now - lastTime > wait) {
lastTime = now
func.apply(this, args)
}
}
}
setInterval(throttle(() => {console.log(1)
}, 500),
1
)
定时器形式:
应用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最初一次进行触发后,还会再执行一次函数
function throttle(func, delay){
var timer = null;
returnfunction(){
var context = this;
var args = arguments;
if(!timer){timer = setTimeout(function(){func.apply(context, args);
timer = null;
},delay);
}
}
}
实用场景:
DOM
元素的拖拽性能实现(mousemove
)- 搜寻联想(
keyup
) - 计算鼠标挪动的间隔(
mousemove
) Canvas
模仿画板性能(mousemove
)- 监听滚动事件判断是否到页面底部主动加载更多
- 拖拽场景:固定工夫内只执行一次,避免超高频次触发地位变动
- 缩放场景:监控浏览器
resize
- 动画场景:防止短时间内屡次触发动画引起性能问题
总结
- 函数防抖:将几次操作合并为一次操作进行。原理是保护一个计时器,规定在 delay 工夫后触发函数,然而在 delay 工夫内再次触发的话,就会勾销之前的计时器而从新设置。这样一来,只有最初一次操作能被触发。
- 函数节流:使得肯定工夫内只触发一次函数。原理是通过判断是否达到肯定工夫来触发函数。
实现非负大整数相加
JavaScript 对数值有范畴的限度,限度如下:
Number.MAX_VALUE // 1.7976931348623157e+308
Number.MAX_SAFE_INTEGER // 9007199254740991
Number.MIN_VALUE // 5e-324
Number.MIN_SAFE_INTEGER // -9007199254740991
如果想要对一个超大的整数 (> Number.MAX_SAFE_INTEGER
) 进行加法运算,然而又想输入个别模式,那么应用 + 是无奈达到的,一旦数字超过 Number.MAX_SAFE_INTEGER
数字会被立刻转换为迷信计数法,并且数字精度相比以前将会有误差。
实现一个算法进行大数的相加:
function sumBigNumber(a, b) {
let res = '';
let temp = 0;
a = a.split('');
b = b.split('');
while (a.length || b.length || temp) {temp += ~~a.pop() + ~~b.pop();
res = (temp % 10) + res;
temp = temp > 9
}
return res.replace(/^0+/, '');
}
其次要的思路如下:
- 首先用字符串的形式来保留大数,这样数字在数学示意上就不会发生变化
- 初始化 res,temp 来保留两头的计算结果,并将两个字符串转化为数组,以便进行每一位的加法运算
- 将两个数组的对应的位进行相加,两个数相加的后果可能大于 10,所以可能要仅为,对 10 进行取余操作,将后果保留在以后位
- 判断以后位是否大于 9,也就是是否会进位,若是则将 temp 赋值为 true,因为在加法运算中,true 会主动隐式转化为 1,以便于下一次相加
- 反复上述操作,直至计算完结
转化为驼峰命名
var s1 = "get-element-by-id"
// 转化为 getElementById
var f = function(s) {return s.replace(/-\w/g, function(x) {return x.slice(1).toUpperCase();})
}
Promise
// 模仿实现 Promise
// Promise 利用三大伎俩解决回调天堂:// 1. 回调函数提早绑定
// 2. 返回值穿透
// 3. 谬误冒泡
// 定义三种状态
const PENDING = 'PENDING'; // 进行中
const FULFILLED = 'FULFILLED'; // 已胜利
const REJECTED = 'REJECTED'; // 已失败
class Promise {constructor(exector) {
// 初始化状态
this.status = PENDING;
// 将胜利、失败后果放在 this 上,便于 then、catch 拜访
this.value = undefined;
this.reason = undefined;
// 胜利态回调函数队列
this.onFulfilledCallbacks = [];
// 失败态回调函数队列
this.onRejectedCallbacks = [];
const resolve = value => {
// 只有进行中状态能力更改状态
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
// 胜利态函数顺次执行
this.onFulfilledCallbacks.forEach(fn => fn(this.value));
}
}
const reject = reason => {
// 只有进行中状态能力更改状态
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
// 失败态函数顺次执行
this.onRejectedCallbacks.forEach(fn => fn(this.reason))
}
}
try {
// 立刻执行 executor
// 把外部的 resolve 和 reject 传入 executor,用户可调用 resolve 和 reject
exector(resolve, reject);
} catch(e) {
// executor 执行出错,将谬误内容 reject 抛出去
reject(e);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function'? onRejected :
reason => {throw new Error(reason instanceof Error ? reason.message : reason) }
// 保留 this
const self = this;
return new Promise((resolve, reject) => {if (self.status === PENDING) {self.onFulfilledCallbacks.push(() => {
// try 捕捉谬误
try {
// 模仿微工作
setTimeout(() => {const result = onFulfilled(self.value);
// 分两种状况:// 1. 回调函数返回值是 Promise,执行 then 操作
// 2. 如果不是 Promise,调用新 Promise 的 resolve 函数
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
})
} catch(e) {reject(e);
}
});
self.onRejectedCallbacks.push(() => {
// 以下同理
try {setTimeout(() => {const result = onRejected(self.reason);
// 不同点:此时是 reject
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
})
} catch(e) {reject(e);
}
})
} else if (self.status === FULFILLED) {
try {setTimeout(() => {const result = onFulfilled(self.value);
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
});
} catch(e) {reject(e);
}
} else if (self.status === REJECTED) {
try {setTimeout(() => {const result = onRejected(self.reason);
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
})
} catch(e) {reject(e);
}
}
});
}
catch(onRejected) {return this.then(null, onRejected);
}
static resolve(value) {if (value instanceof Promise) {
// 如果是 Promise 实例,间接返回
return value;
} else {
// 如果不是 Promise 实例,返回一个新的 Promise 对象,状态为 FULFILLED
return new Promise((resolve, reject) => resolve(value));
}
}
static reject(reason) {return new Promise((resolve, reject) => {reject(reason);
})
}
static all(promiseArr) {
const len = promiseArr.length;
const values = new Array(len);
// 记录曾经胜利执行的 promise 个数
let count = 0;
return new Promise((resolve, reject) => {for (let i = 0; i < len; i++) {// Promise.resolve()解决,确保每一个都是 promise 实例
Promise.resolve(promiseArr[i]).then(
val => {values[i] = val;
count++;
// 如果全副执行完,返回 promise 的状态就能够扭转了
if (count === len) resolve(values);
},
err => reject(err),
);
}
})
}
static race(promiseArr) {return new Promise((resolve, reject) => {
promiseArr.forEach(p => {Promise.resolve(p).then(val => resolve(val),
err => reject(err),
)
})
})
}
}
实现 Event(event bus)
event bus 既是 node 中各个模块的基石,又是前端组件通信的依赖伎俩之一,同时波及了订阅 - 公布设计模式,是十分重要的根底。
简略版:
class EventEmeitter {constructor() {this._events = this._events || new Map(); // 贮存事件 / 回调键值对
this._maxListeners = this._maxListeners || 10; // 设立监听下限
}
}
// 触发名为 type 的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
// 从贮存事件键值对的 this._events 中获取对应事件回调函数
handler = this._events.get(type);
if (args.length > 0) {handler.apply(this, args);
} else {handler.call(this);
}
return true;
};
// 监听名为 type 的事件
EventEmeitter.prototype.addListener = function(type, fn) {
// 将 type 事件以及对应的 fn 函数放入 this._events 中贮存
if (!this._events.get(type)) {this._events.set(type, fn);
}
};
面试版:
class EventEmeitter {constructor() {this._events = this._events || new Map(); // 贮存事件 / 回调键值对
this._maxListeners = this._maxListeners || 10; // 设立监听下限
}
}
// 触发名为 type 的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
// 从贮存事件键值对的 this._events 中获取对应事件回调函数
handler = this._events.get(type);
if (args.length > 0) {handler.apply(this, args);
} else {handler.call(this);
}
return true;
};
// 监听名为 type 的事件
EventEmeitter.prototype.addListener = function(type, fn) {
// 将 type 事件以及对应的 fn 函数放入 this._events 中贮存
if (!this._events.get(type)) {this._events.set(type, fn);
}
};
// 触发名为 type 的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
handler = this._events.get(type);
if (Array.isArray(handler)) {
// 如果是一个数组阐明有多个监听者, 须要顺次此触发外面的函数
for (let i = 0; i < handler.length; i++) {if (args.length > 0) {handler[i].apply(this, args);
} else {handler[i].call(this);
}
}
} else {
// 单个函数的状况咱们间接触发即可
if (args.length > 0) {handler.apply(this, args);
} else {handler.call(this);
}
}
return true;
};
// 监听名为 type 的事件
EventEmeitter.prototype.addListener = function(type, fn) {const handler = this._events.get(type); // 获取对应事件名称的函数清单
if (!handler) {this._events.set(type, fn);
} else if (handler && typeof handler === "function") {
// 如果 handler 是函数阐明只有一个监听者
this._events.set(type, [handler, fn]); // 多个监听者咱们须要用数组贮存
} else {handler.push(fn); // 曾经有多个监听者, 那么间接往数组里 push 函数即可
}
};
EventEmeitter.prototype.removeListener = function(type, fn) {const handler = this._events.get(type); // 获取对应事件名称的函数清单
// 如果是函数, 阐明只被监听了一次
if (handler && typeof handler === "function") {this._events.delete(type, fn);
} else {
let postion;
// 如果 handler 是数组, 阐明被监听屡次要找到对应的函数
for (let i = 0; i < handler.length; i++) {if (handler[i] === fn) {postion = i;} else {postion = -1;}
}
// 如果找到匹配的函数, 从数组中革除
if (postion !== -1) {
// 找到数组对应的地位, 间接革除此回调
handler.splice(postion, 1);
// 如果革除后只有一个函数, 那么勾销数组, 以函数模式保留
if (handler.length === 1) {this._events.set(type, handler[0]);
}
} else {return this;}
}
};
实现具体过程和思路见实现 event
字符串查找
请应用最根本的遍从来实现判断字符串 a 是否被蕴含在字符串 b 中,并返回第一次呈现的地位(找不到返回 -1)。
a='34';b='1234567'; // 返回 2
a='35';b='1234567'; // 返回 -1
a='355';b='12354355'; // 返回 5
isContain(a,b);
function isContain(a, b) {for (let i in b) {if (a[0] === b[i]) {
let tmp = true;
for (let j in a) {if (a[j] !== b[~~i + ~~j]) {tmp = false;}
}
if (tmp) {return i;}
}
}
return -1;
}
参考:前端手写面试题具体解答
实现 apply 办法
apply 原理与 call 很类似,不多赘述
// 模仿 apply
Function.prototype.myapply = function(context, arr) {var context = Object(context) || window;
context.fn = this;
var result;
if (!arr) {result = context.fn();
} else {var args = [];
for (var i = 0, len = arr.length; i < len; i++) {args.push("arr[" + i + "]");
}
result = eval("context.fn(" + args + ")");
}
delete context.fn;
return result;
};
Promise.all
Promise.all
是反对链式调用的,实质上就是返回了一个 Promise 实例,通过 resolve
和reject
来扭转实例状态。
Promise.myAll = function(promiseArr) {return new Promise((resolve, reject) => {const ans = [];
let index = 0;
for (let i = 0; i < promiseArr.length; i++) {promiseArr[i]
.then(res => {ans[i] = res;
index++;
if (index === promiseArr.length) {resolve(ans);
}
})
.catch(err => reject(err));
}
})
}
实现深拷贝
- 浅拷贝: 浅拷贝指的是将一个对象的属性值复制到另一个对象,如果有的属性的值为援用类型的话,那么会将这个援用的地址复制给对象,因而两个对象会有同一个援用类型的援用。浅拷贝能够应用 Object.assign 和开展运算符来实现。
- 深拷贝: 深拷贝绝对浅拷贝而言,如果遇到属性值为援用类型的时候,它新建一个援用类型并将对应的值复制给它,因而对象取得的一个新的援用类型而不是一个原有类型的援用。深拷贝对于一些对象能够应用 JSON 的两个函数来实现,然而因为 JSON 的对象格局比 js 的对象格局更加严格,所以如果属性值里边呈现函数或者 Symbol 类型的值时,会转换失败
(1)JSON.stringify()
JSON.parse(JSON.stringify(obj))
是目前比拟罕用的深拷贝办法之一,它的原理就是利用JSON.stringify
将js
对象序列化(JSON 字符串),再应用JSON.parse
来反序列化 (还原)js
对象。- 这个办法能够简略粗犷的实现深拷贝,然而还存在问题,拷贝的对象中如果有函数,undefined,symbol,当应用过
JSON.stringify()
进行解决之后,都会隐没。
let obj1 = { a: 0,
b: {c: 0}
};
let obj2 = JSON.parse(JSON.stringify(obj1));
obj1.a = 1;
obj1.b.c = 1;
console.log(obj1); // {a: 1, b: {c: 1}}
console.log(obj2); // {a: 0, b: {c: 0}}
(2)函数库 lodash 的_.cloneDeep 办法
该函数库也有提供_.cloneDeep 用来做 Deep Copy
var _ = require('lodash');
var obj1 = {
a: 1,
b: {f: { g: 1} },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
(3)手写实现深拷贝函数
// 深拷贝的实现
function deepCopy(object) {if (!object || typeof object !== "object") return;
let newObject = Array.isArray(object) ? [] : {};
for (let key in object) {if (object.hasOwnProperty(key)) {newObject[key] =
typeof object[key] === "object" ? deepCopy(object[key]) : object[key];
}
}
return newObject;
}
实现类数组转化为数组
类数组转换为数组的办法有这样几种:
- 通过 call 调用数组的 slice 办法来实现转换
Array.prototype.slice.call(arrayLike);
- 通过 call 调用数组的 splice 办法来实现转换
Array.prototype.splice.call(arrayLike, 0);
- 通过 apply 调用数组的 concat 办法来实现转换
Array.prototype.concat.apply([], arrayLike);
- 通过 Array.from 办法来实现转换
Array.from(arrayLike);
验证是否是身份证
function isCardNo(number) {var regx = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
return regx.test(number);
}
深克隆(deepclone)
简略版:
const newObj = JSON.parse(JSON.stringify(oldObj));
局限性:
- 他无奈实现对函数、RegExp 等非凡对象的克隆
- 会摈弃对象的 constructor, 所有的构造函数会指向 Object
- 对象有循环援用, 会报错
面试版:
/**
* deep clone
* @param {[type]} parent object 须要进行克隆的对象
* @return {[type]} 深克隆后的对象
*/
const clone = parent => {
// 判断类型
const isType = (obj, type) => {if (typeof obj !== "object") return false;
const typeString = Object.prototype.toString.call(obj);
let flag;
switch (type) {
case "Array":
flag = typeString === "[object Array]";
break;
case "Date":
flag = typeString === "[object Date]";
break;
case "RegExp":
flag = typeString === "[object RegExp]";
break;
default:
flag = false;
}
return flag;
};
// 解决正则
const getRegExp = re => {
var flags = "";
if (re.global) flags += "g";
if (re.ignoreCase) flags += "i";
if (re.multiline) flags += "m";
return flags;
};
// 保护两个贮存循环援用的数组
const parents = [];
const children = [];
const _clone = parent => {if (parent === null) return null;
if (typeof parent !== "object") return parent;
let child, proto;
if (isType(parent, "Array")) {
// 对数组做非凡解决
child = [];} else if (isType(parent, "RegExp")) {
// 对正则对象做非凡解决
child = new RegExp(parent.source, getRegExp(parent));
if (parent.lastIndex) child.lastIndex = parent.lastIndex;
} else if (isType(parent, "Date")) {
// 对 Date 对象做非凡解决
child = new Date(parent.getTime());
} else {
// 解决对象原型
proto = Object.getPrototypeOf(parent);
// 利用 Object.create 切断原型链
child = Object.create(proto);
}
// 解决循环援用
const index = parents.indexOf(parent);
if (index != -1) {
// 如果父数组存在本对象, 阐明之前曾经被援用过, 间接返回此对象
return children[index];
}
parents.push(parent);
children.push(child);
for (let i in parent) {
// 递归
child[i] = _clone(parent[i]);
}
return child;
};
return _clone(parent);
};
局限性:
- 一些非凡状况没有解决: 例如 Buffer 对象、Promise、Set、Map
- 另外对于确保没有循环援用的对象,咱们能够省去对循环援用的非凡解决,因为这很耗费工夫
原理详解实现深克隆
实现千位分隔符
// 保留三位小数
parseToMoney(1234.56); // return '1,234.56'
parseToMoney(123456789); // return '123,456,789'
parseToMoney(1087654.321); // return '1,087,654.321'
function parseToMoney(num) {num = parseFloat(num.toFixed(3));
let [integer, decimal] = String.prototype.split.call(num, '.');
integer = integer.replace(/\d(?=(\d{3})+$)/g, '$&,');
return integer + '.' + (decimal ? decimal : '');
}
正则表达式(使用了正则的前向申明和反前向申明):
function parseToMoney(str){
// 仅仅对地位进行匹配
let re = /(?=(?!\b)(\d{3})+$)/g;
return str.replace(re,',');
}
循环打印红黄绿
上面来看一道比拟典型的问题,通过这个问题来比照几种异步编程办法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯一直交替反复亮灯?
三个亮灯函数:
function red() {console.log('red');
}
function green() {console.log('green');
}
function yellow() {console.log('yellow');
}
这道题简单的中央在于 须要“交替反复”亮灯,而不是“亮完一次”就完结了。
(1)用 callback 实现
const task = (timer, light, callback) => {setTimeout(() => {if (light === 'red') {red()
}
else if (light === 'green') {green()
}
else if (light === 'yellow') {yellow()
}
callback()}, timer)
}
task(3000, 'red', () => {task(2000, 'green', () => {task(1000, 'yellow', Function.prototype)
})
})
这里存在一个 bug:代码只是实现了一次流程,执行后红黄绿灯别离只亮一次。该如何让它交替反复进行呢?
下面提到过递归,能够递归亮灯的一个周期:
const step = () => {task(3000, 'red', () => {task(2000, 'green', () => {task(1000, 'yellow', step)
})
})
}
step()
留神看黄灯亮的回调里又再次调用了 step 办法 以实现循环亮灯。
(2)用 promise 实现
const task = (timer, light) =>
new Promise((resolve, reject) => {setTimeout(() => {if (light === 'red') {red()
}
else if (light === 'green') {green()
}
else if (light === 'yellow') {yellow()
}
resolve()}, timer)
})
const step = () => {task(3000, 'red')
.then(() => task(2000, 'green'))
.then(() => task(2100, 'yellow'))
.then(step)
}
step()
这里将回调移除,在一次亮灯完结后,resolve 以后 promise,并仍然应用递归进行。
(3)用 async/await 实现
const taskRunner = async () => {await task(3000, 'red')
await task(2000, 'green')
await task(2100, 'yellow')
taskRunner()}
taskRunner()
函数珂里化
指的是将一个承受多个参数的函数 变为 承受一个参数返回一个函数的固定模式,这样便于再次调用,例如 f(1)(2)
经典面试题:实现add(1)(2)(3)(4)=10;
、add(1)(1,2,3)(2)=9;
function add() {const _args = [...arguments];
function fn() {_args.push(...arguments);
return fn;
}
fn.toString = function() {return _args.reduce((sum, cur) => sum + cur);
}
return fn;
}
Function.prototype.apply()
第一个参数是绑定的 this,默认为window
,第二个参数是数组或类数组
Function.prototype.apply = function(context = window, args) {if (typeof this !== 'function') {throw new TypeError('Type Error');
}
const fn = Symbol('fn');
context[fn] = this;
const res = context[fn](...args);
delete context[fn];
return res;
}
函数柯里化的实现
函数柯里化指的是一种将应用多个参数的一个函数转换成一系列应用一个参数的函数的技术。
function curry(fn, args) {
// 获取函数须要的参数长度
let length = fn.length;
args = args || [];
return function() {let subArgs = args.slice(0);
// 拼接失去现有的所有参数
for (let i = 0; i < arguments.length; i++) {subArgs.push(arguments[i]);
}
// 判断参数的长度是否曾经满足函数所需参数的长度
if (subArgs.length >= length) {
// 如果满足,执行函数
return fn.apply(this, subArgs);
} else {
// 如果不满足,递归返回科里化的函数,期待参数的传入
return curry.call(this, fn, subArgs);
}
};
}
// es6 实现
function curry(fn, ...args) {return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}