题目总数:38
实现函数的柯里化 重点
一、柯里化的定义与核心思想
1. 定义
柯里化是将一个接受多个参数的函数转换为 逐次接受单个参数,并最终返回结果的新函数的过程。
2. 数学表达式
原始函数:
f(a, b, c) → result
柯里化后:
f(a)(b)(c) → result
3. 核心思想
- 分步传递参数:每次调用接收部分参数,返回接收剩余参数的函数。
- 闭包保留参数:利用闭包机制保存已传入的参数。
二、柯里化的实现方式
1. 手动柯里化
通过闭包逐层返回新函数:
// 原始函数
function add(a, b, c) {
return a + b + c;
}
// 手动柯里化
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
console.log(curriedAdd(1)(2)(3)); // 6
2. 通用柯里化函数
实现一个自动柯里化的工具函数:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
}
};
}
// 使用示例
const curriedSum = curry((a, b, c) => a + b + c);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
3. 使用 Lodash 的 _.curry
const _ = require('lodash');
const curriedAdd = _.curry((a, b, c) => a + b + c);
console.log(curriedAdd(1)(2)(3)); // 6
三、柯里化的应用场景
1. 参数复用
创建预设参数的专用函数:
// 通用日志函数
const log = curry((level, message) => {
console.log(`[${level}] ${message}`);
});
// 创建预设级别的日志函数
const debugLog = log('DEBUG');
const errorLog = log('ERROR');
debugLog('User logged in'); // [DEBUG] User logged in
errorLog('Connection failed'); // [ERROR] Connection failed
2. 延迟执行
逐步收集参数,最后触发计算:
const fetchData = curry((url, params) => {
return fetch(url + '?' + new URLSearchParams(params));
});
// 分步传递参数
const getUser = fetchData('/api/user');
const getUserById = getUser({ id: 1 });
// 实际发送请求
getUserById().then(/* ... */);
3. 函数组合
配合组合函数(如 compose、pipe)使用:
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const toUpper = str => str.toUpperCase();
const exclaim = str => str + '!';
// 柯里化后的处理函数
const process = compose(exclaim, toUpper);
console.log(process('hello')); // HELLO!
四、柯里化的优缺点
| 优点 | 缺点 |
|---|---|
| 提高函数复用性(参数预设) | 调用链过长可能降低可读性 |
| 支持函数式编程风格 | 性能开销(多次函数调用) |
| 便于组合复杂逻辑 | 需配合类型提示工具优化体验 |
五、柯里化与部分应用(Partial Application)的区别
| 特性 | 柯里化(Currying) | 部分应用(Partial Application) |
|---|---|---|
| 参数传递 | 逐次传递单个参数 | 一次传递多个参数 |
| 返回结果 | 返回接收剩余参数的函数 | 返回接收剩余参数的函数 |
| 最终执行 | 所有参数传递完毕后执行 | 所有参数传递完毕后执行 |
| 示例 | add(1)(2)(3) | add.bind(null, 1, 2) → add(3) |
六、ES6 箭头函数简化柯里化
利用箭头函数隐式返回的特性,简化柯里化写法:
const curriedAdd = a => b => c => a + b + c;
console.log(curriedAdd(1)(2)(3)); // 6
七、总结
- 柯里化本质:将多参数函数转换为单参数函数链。
- 核心价值:增强函数灵活性,支持函数组合与参数复用。
- 适用场景:高频复用参数、函数组合、延迟执行等函数式编程需求。
- 注意事项:平衡可读性与性能,避免过度使用。
掌握柯里化技术能显著提升代码的抽象能力和复用性,是函数式编程中的重要工具。
函数柯里化x3 ⭐️
柯里化,可以理解为提前接收部分参数,延迟执行,不立即输出结果,而是返回一个接受剩余参数的函数。因为这样的特性,也被称为部分计算函数。柯里化,是一个逐步接收参数的过程。在接下来的剖析中,你会深刻体会到这一点。
总结
柯里化,在这个例子中可以看出很明显的行为规范:
- 逐步接收参数,并缓存供后期计算使用
- 不立即计算,延后执行
- 符合计算的条件,将缓存的参数,统一传递给执行方法
柯里化的应用
- 利用柯里化制定约束条件,管控触发机制
- 处理浏览器兼容(参数复用实现一次性判断)
- 函数节流防抖(延迟执行)
- ES5前bind方法的实现
秒懂反柯里化
先上公式,从来没有这么喜欢写公式,简明易懂。
// 反柯里化公式:
curryFn(a)(b)(c)(d) = fn(a, b, c, d);
curryFn(a) = fn(a);
看完公式,是不是似曾相识,这不就是我们日常敲码的普通函数么?没错的,函数柯里化就是把普通函数变成成一个复杂的函数, 。
函数柯里化是把支持多个参数的函数变成接收单一参数的函数,并返回一个函数能接收处理剩余参数:fn(a,b,c,d) => fn(a)(b)(c)(d),而反柯里化就是把参数全部释放出来:fn(a)(b)(c)(d) => fn(a,b,c,d)。
// 反柯里化:最简单的反柯里化(普通函数)
function add(a, b, c, d) {
return a + b + c + d;
}
sleep函数 *3
sleep函数作用是让线程休眠,等到指定时间在重新唤起。
方法一:这种实现方式是利用一个伪死循环阻塞主线程。因为JS是单线程的。所以通过这种方式可以实现真正意义上的sleep()。
function sleep(delay) {
var start = (new Date()).getTime();
while ((new Date()).getTime() - start < delay) {
continue;
}
}
function test() {
console.log('111');
sleep(2000);
console.log('222');
}
test()
方法二:定时器
function sleep1(ms, callback) {
setTimeout(callback, ms)
}
//sleep 1s
sleep1(1000, () => {
console.log(1000)
})
方法三:es6异步处理
const sleep = time => {
return new Promise(resolve => setTimeout(resolve,time)
) }
sleep(1000).then(()=>{ console.log(1) })
写一个函数,可以控制最大并发数
以下是一个用于控制最大并发数的 JavaScript 函数,它能按顺序执行任务队列,保证同时运行的任务数不超过设定的最大值,并返回所有任务结果的 Promise:
function limitConcurrency(tasks, maxConcurrent) {
return new Promise((resolve, reject) => {
// 边界条件处理
if (typeof maxConcurrent !== 'number' || maxConcurrent <= 0) {
reject(new Error('maxConcurrent must be a positive number'));
return;
}
if (!Array.isArray(tasks)) {
reject(new Error('tasks must be an array'));
return;
}
if (tasks.length === 0) {
resolve([]);
return;
}
let index = 0; // 当前任务指针
let running = 0; // 正在运行的任务数
const results = []; // 存储所有任务结果
let errorOccurred = false; // 错误标志
// 递归启动任务
const runNext = () => {
// 如果发生错误或所有任务已完成,直接返回
if (errorOccurred || index >= tasks.length) return;
// 启动尽可能多的任务,直到达到最大并发数
while (running < maxConcurrent && index < tasks.length) {
const currentIndex = index++; // 捕获当前索引
const task = tasks[currentIndex];
running++;
// 执行任务并处理结果
Promise.resolve(task())
.then((result) => {
results[currentIndex] = result; // 按原始顺序存储结果
})
.catch((err) => {
errorOccurred = true;
reject(err); // 遇到第一个错误立即拒绝
})
.finally(() => {
running--;
// 检查是否所有任务都已完成
if (index === tasks.length && running === 0) {
errorOccurred ? reject() : resolve(results);
}
runNext(); // 尝试启动新任务
});
}
};
runNext(); // 初始调用
});
}
函数特性
-
并发控制
- 严格限制同时运行的任务数不超过
maxConcurrent。 - 自动按队列顺序启动任务,确保最大并发数。
- 严格限制同时运行的任务数不超过
-
结果保留
- 按任务原始顺序返回结果数组(即使任务完成顺序不同)。
- 支持同步和异步任务(自动包装为 Promise)。
-
错误处理
- 遇到第一个错误立即拒绝(类似
Promise.all),停止后续任务。 - 可通过调整
.catch逻辑改为收集所有错误(类似Promise.allSettled)。
- 遇到第一个错误立即拒绝(类似
使用示例
// 示例任务:生成异步任务数组
const tasks = [1000, 2000, 300, 1500, 500].map(
(ms, i) => () => new Promise(resolve =>
setTimeout(() => {
console.log(`Task ${i} done`);
resolve(i);
}, ms)
)
);
// 执行并发控制(最大并发数=2)
limitConcurrency(tasks, 2)
.then(results => console.log('All done:', results))
.catch(err => console.error('Error:', err));
// 输出:
// Task 0 done (1000ms)
// Task 2 done (300ms)
// Task 1 done (2000ms)
// Task 4 done (500ms)
// Task 3 done (1500ms)
// All done: [0, 1, 2, 3, 4]
参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
tasks | Array | 是 | 任务数组,每个任务需返回 Promise |
maxConcurrent | Number | 是 | 最大并发任务数(≥1) |
适用场景
- API 请求限流:避免同时发送过多请求导致服务器压力。
- 文件批量处理:控制同时打开的文件句柄数量。
- 爬虫任务:限制并发连接数,防止 IP 被封禁。
通过这个函数,可以有效管理异步任务的执行节奏,优化资源利用率和系统稳定性。
如何优化图像,图像格式的区别
1、不用图片,尽量用css3代替。
2、 使用矢量图SVG替代位图。矢量图更小
3、GIF基本上除了GIF动画外不要使用。或用SVG动画取代。
4、按照HTTP协议设置合理的缓存
5、使用字体图标webfont
6、WebP图片格式能给前端带来的优化。WebP支持无损、有损压缩,动态、静态图片,压缩比率优于GIF、JPEG、PNG等格式,非常适合用于网络等图片传输。
图像格式的区别:
矢量图:图标字体,如 font-awesome;svg
位图:gif,jpg(jpeg),png
优缺点:
1、能在保证最不失真的情况下尽可能压缩图像文件的大小。
2、对于需要高保真的较复杂的图像,PNG虽然能无损压缩,但图片文件较大,不适合应用在Web页面上。
介绍webp这个图片文件格式
WebP是google开发的一种 旨在加快图片加载速度的图片格式 。
WebP 的优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都相当优秀、稳定和统一。
优势强劲所以推动起来也很快,因为压缩效率高,体积小,对节省磁盘空间、网络带宽,加快页面加载速度
Google、Youtube、Facebook、Ebay 以及很多国内较大的公司(TAB,360,美图等)的许多产品都开始使用 WebP 格式的图片,但现在仍有很多地方不支持这种格式,这时就需要用转换工具将图片转变为我们常用的 PNG 或 JPG 格式,在这里介绍一下转换的方法。
1、在线转换网站 cloudconvert
2、转换工具 isparta(Windows & Mac)
3、谷歌转换【翻墙查看】
base64 前端如何转化 mark
在前端开发里,经常会遇到需要将数据转化为 Base64 编码或者把 Base64 编码还原为原始数据的情况。下面将详细介绍几种常见数据类型(如字符串、文件)在前端进行 Base64 转化的方法。
字符串与 Base64 相互转化
字符串转 Base64
在 JavaScript 中,可以使用 btoa() 函数将字符串转化为 Base64 编码。
const originalString = 'Hello, World!';
const base64Encoded = btoa(originalString);
console.log(base64Encoded);
Base64 转字符串
使用 atob() 函数可以将 Base64 编码还原为原始字符串。
const base64Encoded = 'SGVsbG8sIFdvcmxkIQ==';
const originalString = atob(base64Encoded);
console.log(originalString);
注意事项
btoa()和atob()函数只能处理 ASCII 字符串,如果要处理包含非 ASCII 字符的字符串,需要先对字符串进行 UTF - 8 编码。
function utf8ToBase64(str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
function toSolidBytes(match, p1) {
return String.fromCharCode('0x' + p1);
}));
}
function base64ToUtf8(str) {
return decodeURIComponent(atob(str).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
}
const originalString = '你好,世界!';
const base64Encoded = utf8ToBase64(originalString);
const decodedString = base64ToUtf8(base64Encoded);
console.log(base64Encoded);
console.log(decodedString);
文件与 Base64 相互转化
文件转 Base64
可以借助 FileReader 对象把文件转化为 Base64 编码。一般用于处理用户上传的文件。
<body>
<input type="file" id="fileInput">
<script>
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', function () {
const file = this.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function (e) {
const base64Encoded = e.target.result;
console.log(base64Encoded);
};
reader.readAsDataURL(file);
}
});
</script>
</body>
Base64 转文件
要把 Base64 编码还原为文件,可以使用 Blob 对象和 URL.createObjectURL() 方法。
function base64ToFile(base64Data, fileName) {
const byteCharacters = atob(base64Data.split(',')[1]);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: 'image/jpeg' }); // 假设是图片文件
const file = new File([blob], fileName, { type: blob.type });
return file;
}
const base64Data = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD...'; // 省略部分内容
const file = base64ToFile(base64Data, 'example.jpg');
console.log(file);
综上所述,在前端进行 Base64 转化时,对于字符串和文件可以采用不同的方法,并且在处理包含非 ASCII 字符的字符串时要注意编码问题。
BOM属性对象方法
Window对象:open close confirm
Navigator对象: appName online userAgent
Screen对象
History对象方法: back()和forward() go()
Location对象: host hostname protocol hash
for in和for of的区别 ⭐️
for...in 和 for...of 是 JavaScript 中两种不同的遍历方式,核心区别在于遍历目标、适用场景和遍历内容:
1. 遍历目标与内容
-
for...in:遍历对象的可枚举属性(包括原型链上的属性),返回属性名(键)。适用于对象,若遍历数组则返回下标 (字符串形式)。const obj = { a: 1, b: 2 }; for (let key in obj) { console.log(key); // 输出:a、b } const arr = [10, 20]; for (let index in arr) { console.log(index); // 输出:0、1(字符串类型) } -
for...of:遍历可迭代对象(如数组、字符串、Map、Set)的值,不能直接遍历普通对象(需配合Object.keys()等)。const arr = [10, 20]; for (let value of arr) { console.log(value); // 输出:10、20 } const str = 'abc'; for (let char of str) { console.log(char); // 输出:a、b、c }
2. 适用场景
for...in:适合遍历普通对象的属性,但需注意过滤原型链属性(用hasOwnProperty)。for...of:适合遍历数组、字符串等可迭代对象的值,支持break/continue,且能配合entries()获取键值对。
Set 和 Map 数据结构 ⭐️
- ES6 提供了新的数据结构 Set 它类似于数组,但是成员的值都是唯一的,没有重复的值。
- ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是 “键” 的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了 “字符串 — 值” 的对应,Map 结构提供了 “值 — 值” 的对应,是一种更完善的 Hash 结构实现。
WeakMap 和 Map 的区别? ⭐️
- WeakMap 结构与 Map 结构基本类似,唯一的区别是它只接受对象作为键名(null 除外),
不接受其他类型的值作为键名,而且键名所指向的对象,不计入垃圾回收机制。 - WeakMap 最大的好处是可以避免内存泄漏。一个仅被 WeakMap 作为 key 而引用的对象,会被垃圾回收器回收掉。
- WeakMap 拥有和 Map 类似的 set (key, value) 、get (key)、has (key) 、delete (key) 和 clear () 方法,没有任何与迭代有关的属性和方法。
数组中的forEach和map的区别
相同点
1、都是循环遍历数组中的每一项
2、只能遍历数组
3、都不会改变原数组
区别
map方法
1.map方法返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。
2.map方法不会对空数组进行检测,map方法不会改变原始数组。
array.map(function(item,index,arr){},thisValue)
var arr = [0,2,4,6,8];
var str = arr.map(function(item,index,arr){
console.log(this); //window
console.log("原数组arr:",arr); //注意这里执行5次
return item/2;
},this);
console.log(str);//[0,1,2,3,4]
若arr为空数组,则map方法返回的也是一个空数组。
forEach方法
Array.forEach(function(item,index,arr){},this)
var arr = [0,2,4,6,8];
var sum = 0;
var str = arr.forEach(function(item,index,arr){
sum += item;
console.log("sum的值为:",sum); //0 2 6 12 20
console.log(this); //window
},this)
console.log(sum);//20
console.log(str); //undefined
- forEach方法用来调用数组的每个元素,将元素传给回调函数,forEach对于空数组是不会调用回调函数的。
- 无论arr是不是空数组,forEach返回的都是undefined。这个方法只是将数组中的每一项作为callback的参数执行一次。
- map()会分配内存空间存储新数组并返回,forEach()不会返回数据。
forEach()的执行速度 < map()的执行速度
reduce()用来干什么
一、语法
reduce() 是数组的归并方法,与forEach()、map()、filter()等迭代方法一样都会对数组每一项进行遍历,但是reduce() 可同时将前面数组项遍历产生的结果与当前遍历项进行运算,这一点是其他迭代方法无法企及的
arr.reduce(function(prev,cur,index,arr){
...
}, init);
- arr 表示原数组;
- prev 表示上一次调用回调时的返回值,或者初始值 init;
- cur 表示当前正在处理的数组元素;
- index 表示当前正在处理的数组元素的索引,若提供 init 值,则索引为0,否则索引为1;
- init 表示初始值。
先提供一个原始数组:
var arr = [3,9,4,3,6,0,9];
实现以下需求的方式有很多,其中就包含使用reduce()的求解方式,也算是实现起来比较简洁的一种吧。
1. 求数组项之和
var sum = arr.reduce(function (prev, cur) {
return prev + cur;
},0);
由于传入了初始值0,所以开始时prev的值为0,cur的值为数组第一项3,相加之后返回值为3作为下一轮回调的prev值,然后再继续与下一个数组项相加,以此类推,直至完成所有数组项的和并返回。
2. 求数组项最大值
var max = arr.reduce(function (prev, cur) {
return Math.max(prev,cur);
});
由于未传入初始值,所以开始时prev的值为数组第一项3,cur的值为数组第二项9,取两值最大值后继续进入下一轮回调。
3. 数组去重
var newArr = arr.reduce(function (prev, cur) {
prev.indexOf(cur) === -1 && prev.push(cur);
return prev;
},[]);
[1, 2, 3, 4, 5]变成[1, 2, 3, a, b, 5]
let q = [1, 2, 3, 4, 5]
q.splice(2, 0, 'a')
q.splice(3, 0, 'b')
console.log(q);
取数组的最大值(ES5、ES6)
let q = [1, 2, 3, 4, 5]
console.log(Math.max(...q));//es6
console.log(Math.max.apply(this, q));//es5
数组去重
- 利用
Set和Array.from去重(ES6)- 利用
reduce归并去重
在 JavaScript 中,数组去重有多种方法,以下是常见的几种:
利用Set和Array.from去重(ES6)
Set是 ES6 新增的数据结构,它的元素具有唯一性。Array.from可将类数组对象或可遍历对象转换为真正的数组。
let list = [1, 1, 2, 2, 3, 3];
let newList = Array.from(new Set(list));
console.log(newList); // [1, 2, 3]
利用reduce归并去重
var newArr = arr.reduce(function (prev, cur) {
prev.indexOf(cur) === -1 && prev.push(cur);
return prev;
},[]);
利用includes去重
通过includes方法判断数组是否已包含某个值,若不包含则添加到新数组。
let list = [1, 1, 2, 2, 3, 3];
let newList2 = [];
list.forEach((item) => {
if (!newList2.includes(item)) {
newList2.push(item);
}
});
console.log(newList2); // [1, 2, 3]
利用Map去重
Map也是 ES6 的新特性,本质是键值对集合,可利用它来判断元素是否已存在。
let list = [1, 1, 2, 2, 3, 3];
let newList3 = [];
let map = new Map();
list.forEach((item) => {
if (!map.has(item)) {
map.set(item, true);
newList3.push(item);
}
});
console.log(newList3); // [1, 2, 3]
利用indexOf去重
indexOf可返回指定元素在数组中首次出现的位置,以此判断元素是否重复。
let list = [1, 1, 2, 2, 3, 3];
let newList4 = [];
list.forEach((item) => {
if (newList4.indexOf(item) === -1) {
newList4.push(item);
}
});
console.log(newList4); // [1, 2, 3]
利用递归去重
通过递归,每次比较数组末尾相邻元素,若重复就删除。
let list = [1, 1, 2, 2, 3, 3];
function callBack(index) {
if (index >= 1) {
if (list[index] === list[index - 1]) {
list.splice(index, 1);
}
callBack(index - 1);
}
}
callBack(list.sort().length - 1);
console.log(list); // [1, 2, 3]
利用filter和indexOf去重
filter用于筛选元素,结合indexOf判断元素是否首次出现。
let list = [1, 1, 2, 2, 3, 3];
let newList = list.filter((item, index) => list.indexOf(item) === index);
console.log(newList); // [1, 2, 3]
随机打乱一个数组 * 2
注意事项
- 避免修改原数组:上述方法均返回新数组,原数组不变。
- 随机性质量:如需加密级随机性(如抽奖系统),应使用
crypto.getRandomValues()替代Math.random。- 大数据量:Fisher-Yates 是处理大数组的最高效选择。
总结
- 推荐使用 Fisher-Yates 算法(方法1)实现公平且高效的随机打乱。
- 避免依赖
sort方法,除非对随机性要求不高。- 第三方库(如 Lodash)在已引入的项目中可直接使用。
在 JavaScript 中,随机打乱数组(Shuffle) 可以通过多种方法实现。以下是 高效且公平 的解决方案,保证每个元素出现在每个位置的概率均等:
1. Fisher-Yates 洗牌算法(推荐)
这是最经典的随机排序算法,时间复杂度为 O(n),且能保证排列的完全随机性。
function shuffleArray(array) {
// 复制原数组以避免修改原数组
const shuffled = [...array];
// 从后向前遍历
for (let i = shuffled.length - 1; i > 0; i--) {
// 随机生成一个 [0, i] 之间的整数
const j = Math.floor(Math.random() * (i + 1));
// 交换当前位置和随机位置的值
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
// 使用示例
const arr = [1, 2, 3, 4, 5];
console.log(shuffleArray(arr)); // 例如:[3, 1, 5, 2, 4]
算法原理
- 从数组末尾开始向前遍历。
- 对于每个位置
i,随机选择一个[0, i]的索引j。 - 交换
i和j位置的元素。 - 最终得到的数组是完全随机打乱的。
2. 使用 sort 结合随机数(简单但不完全公平)
虽然代码简洁,但这种方法存在 排序偏差,不推荐在需要严格随机性的场景使用。
function shuffleArraySimple(array) {
return [...array].sort(() => Math.random() - 0.5);
}
// 示例
console.log(shuffleArraySimple([1, 2, 3, 4, 5]));
为什么不够公平?
- 不同 JavaScript 引擎的
sort实现可能导致元素分布不均匀。 - 理论上,每个排列的概率应为
1/n!,但这种方法无法保证。
3. 使用 Lodash 的 _.shuffle
第三方库 Lodash 提供了现成的洗牌函数,内部实现也是 Fisher-Yates 算法。
const _ = require('lodash');
const shuffled = _.shuffle([1, 2, 3, 4, 5]);
console.log(shuffled);
4. ES6 单行实现(Fisher-Yates 变种)
更简洁的写法,但可读性稍差:
const shuffleArray = arr => arr.reduceRight(
(a, _, i) => (a.splice(Math.floor(Math.random() * (i + 1)), 0, a[i], a), [...arr]
);
性能对比
| 方法 | 时间复杂度 | 是否公平 | 是否修改原数组 | 适用场景 |
|---|---|---|---|---|
| Fisher-Yates | O(n) | ✅ | ❌(默认不修改) | 通用推荐 |
sort + 随机数 | O(n log n) | ❌ | ❌ | 快速演示场景 |
Lodash _.shuffle | O(n) | ✅ | ❌ | 已使用 Lodash 时 |
Fisher-Yates 的数学证明
每个元素出现在任意位置的概率均为 1/n,其中 n 是数组长度。这是因为:
- 第一次交换时,每个元素被选到最后一个位置的概率是
1/n。 - 第二次交换时,剩余
n-1个元素被选到倒数第二位置的概率是1/(n-1)。 - 依此类推,最终每个排列的概率为
1/n!,保证完全随机。
数组常用方法
some、every、find、filter、map、forEach有什么区别
查找数组重复项
var newArr = arr.reduce(function (prev, cur) {
prev.indexOf(cur) === -1 && prev.push(cur);
return prev;
},[]);
如何找0-5的随机数,95-99呢
Math.random() 是 JavaScript 中的一个内置函数,用于生成一个范围在 [0, 1) 之间的伪随机浮点数,这个数值大于或等于 0,但小于 1。下面从基本使用、生成指定范围随机数以及注意事项几个方面详细介绍它:
let num = Math.floor(Math.random() * 10)
console.log(num > 5 ? num - 5 : num);
扁平化数组 *2 ⭐️
- arr.flat(Infinity)
- arr.join().split(',').map(Number)
- arr.toString().split(',').map(Number)
- flatten api
- [].concat(...[1, 2, 3, [4, 5]]) //一层嵌套可以用
- 递归
const arr = [1, [2, [3], 4], 5]
function flatten(arr) {
for (let i in arr) {
if (Array.isArray(arr[i])) {
arr.splice(i, 1, ...flatten(arr[i]))
}
}
return arr
}
ES6中的map和原生的对象有什么区别
object和Map存储的都是键值对组合。但是:
object的键的类型是 字符串;
map的键的类型是 可以是任意类型;
另外注意,object获取键值使用Object.keys(返回数组);
Map获取键值使用 map变量.keys() (返回迭代器)。
函数式编程
函数式编程是一种 编程范式,你可以理解为一种软件架构的思维模式。它有着独立一套理论基础与边界法则,追求的是 更简洁、可预测、高复用、易测试。其实在现有的众多知名库中,都蕴含着丰富的函数式编程思想,如 React / Redux 等。
函数式编程(FP)深度解析
函数式编程是一种以数学函数为蓝本的编程范式,强调无副作用、声明式风格和数据不可变性。以下从底层原理到应用实践的系统性解析:
一、核心理论体系
二、核心特性详解
1. 纯函数本质
- 引用透明性:表达式可替换为其计算结果
// 非引用透明 const getTime = () => new Date().getTime(); // 引用透明 const add = (a, b) => a + b; - 幂等性:多次执行结果相同
// 非幂等 let count = 0; const increment = () => count++; // 幂等 const pureIncrement = (num) => num + 1;
2. 不可变数据实现
- 结构共享:Immutable.js使用的Trie数据结构
const { Map } = require('immutable'); const map1 = Map({ a: 1, b: 2 }); const map2 = map1.set('a', 3); console.log(map1 === map2); // false - 写时复制:通过Proxy实现惰性拷贝
const produce = require('immer').produce; const nextState = produce(baseState, draft => { draft.push({todo: "Learn FP"}); });
三、高阶函数应用模式
1. 组合子(Combinators)
- Kestrel (K组合子):常量函数
const K = x => _ => x; K(3)(null) // 3 - Bluebird (B组合子):函数组合
const B = f => g => x => f(g(x)); B(x => x*2)(x => x+1)(3) // 8
2. 惰性计算链
const lazyPipe = (...fns) => initVal =>
fns.reduce((val, fn) => ({ get: () => fn(val.get()) }), { get: () => initVal }).get();
const calc = lazyPipe(
x => x * 2,
y => y + 3,
z => z ** 2
);
calc(5); // (5*2+3)^2 = 169
四、类型系统集成
1. 代数数据类型(ADT)
- Sum Types:处理多态情形
class Either { static left(value) { return new Left(value) } static right(value) { return new Right(value) } } class Left extends Either { /* 错误处理 */ } class Right extends Either { /* 正常流程 */ }
2. 类型约束
- Fantasy Land规范:定义代数结构的JavaScript标准
// Functor实现规范 class Box { constructor(value) { this.value = value } map(fn) { return new Box(fn(this.value)) } // 遵守Identity和Composition定律 }
五、性能优化策略
1. 尾递归优化(TCO)
- 原理:复用栈帧避免爆栈
function factorial(n, acc = 1) { if (n <= 1) return acc; return factorial(n - 1, n * acc); // 尾调用形式 }
2. 记忆化(Memoization)
- 自动缓存:
const memoize = fn => { const cache = new WeakMap(); return (...args) => { const key = args[0] || {}; return cache.has(key) ? cache.get(key) : (cache.set(key, fn(...args)), cache.get(key)); }; };
六、工程实践方案
1. React中的FP应用
- 高阶组件:
const withLogger = Component => props => { useEffect(() => console.log('Mounted'), []); return <Component {...props} />; }; - Redux Reducer:
const reducer = (state, action) => produce(state, draft => { switch(action.type) { case 'ADD': draft.items.push(action.payload); } });
2. 流处理方案
const { fromEvent } = rxjs;
const { map, filter, scan } = rxjs.operators;
fromEvent(document, 'click')
.pipe(
filter(ev => ev.clientX > 100),
map(ev => ({x: ev.clientX, y: ev.clientY})),
scan((acc, curr) => [...acc, curr], [])
)
.subscribe(console.log);
七、调试与测试
1. 纯函数调试
- 确定性测试:
test('add函数应正确相加', () => { expect(add(2,3)).toBe(5); // 确定结果 expect(add(-1,1)).toBe(0); // 边界条件 });
2. 副作用追踪
- Effect追踪器:
const withEffectLog = fn => (...args) => { console.log('副作用触发:', fn.name); return fn(...args); };
八、生态系统工具链
| 工具类型 | 代表性工具 | 功能特点 |
|---|---|---|
| 不可变库 | Immutable.js、Immer | 高效不可变数据结构 |
| 函数工具库 | Ramda、Lodash/fp | 提供FP风格工具函数 |
| 类型系统 | TypeScript、Flow | 静态类型检查 |
| 异步处理 | RxJS、Fluture | 响应式/函数式异步处理 |
| 编译工具 | Babel插件(优化尾调用) | 支持ES6+特性转换 |
九、演进趋势
- Wasm集成:通过Rust等语言实现高性能FP核心
- 渐进式采用:React Hooks推动FP在UI层的应用
- 类型系统增强:TypeScript 4.0+ 改进类型推导
- 编译优化:V8引擎对不可变数据结构的底层优化
函数式编程正在从学术研究领域快速渗透到工业实践,成为应对复杂系统的重要工具。其数学严谨性为构建可靠软件提供理论基础,而现代框架和工具链的成熟则大幅降低了工程实践门槛。
介绍纯函数
一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。
为什么要煞费苦心地构建纯函数?因为纯函数非常“靠谱”,执行一个纯函数你不用担心它会干什么坏事,它不会产生不可预料的行为,也不会对外部产生影响。不管何时何地,你给它什么它就会乖乖地吐出什么。如果你的应用程序大多数函数都是由纯函数组成,那么你的程序测试、调试起来会非常方便。
函数的返回结果只依赖于它的参数。
函数执行过程里面没有副作用。
实现一个类,可以on,emit,off,once,注册、调用、取消、注册仅能使用一次的事件
eventEmitter(emit,on,off,once)
页面上有1万个button如何绑定事件
使用事件委托, 利用了事件的冒泡机制.
针对btn绑定事件就一定要判断点击的target是不是btn, 如果做这一步, 点击被委托的父容器其他地方也会触发事件.
不要阻止冒泡事件!
如何判断是button
target:指的是实际触发事件的元素。当事件发生时,它指向用户直接操作的那个元素。例如,若点击了一个按钮内部的图标,target就会指向这个图标元素。currentTarget:指的是绑定事件处理函数的元素。不管事件是从哪个具体的子元素开始触发的,currentTarget始终指向绑定了该事件处理函数的元素。
循环绑定时的index是多少,为什么,怎么解决
在 JavaScript 中,循环绑定时 index 的值会因为循环机制和作用域的不同而产生一些意外情况,下面为你详细分析并给出解决办法。
问题现象
在使用 for 循环为多个元素绑定事件处理函数时,如果直接在事件处理函数中使用循环变量 index,会发现所有事件处理函数中的 index 值都相同,通常是循环结束后的最终值。这是因为 JavaScript 的 var 关键字声明的变量没有块级作用域,并且事件处理函数是在循环结束后才可能被触发,此时 index 已经变成了循环结束时的值。
<body>
<button>Button 1</button>
<button>Button 2</button>
<button>Button 3</button>
<script>
const buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function () {
console.log('Clicked button index:', i);
});
}
</script>
</body>
在这个例子中,无论点击哪个按钮,控制台输出的 index 值都是 3,这并不是我们期望的结果。
原因分析
- 变量作用域:使用
var声明的变量是函数作用域,而不是块级作用域。在上述循环中,i是在整个函数作用域内共享的,当事件处理函数被触发时,循环已经结束,i的值已经变为buttons.length。 - 事件处理函数的延迟执行:事件处理函数是异步执行的,它们会在循环结束后,用户点击按钮时才会被调用。此时,
i的值已经固定为循环结束时的值。
解决办法
1. 使用 let 关键字(ES6 及以上)
let 关键字具有块级作用域,每次循环都会创建一个新的变量副本,这样每个事件处理函数都会引用自己的 index 值。
<script>
const buttons = document.querySelectorAll('button');
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function () {
console.log('Clicked button index:', i);
});
}
</script>
let 的工作原理
-
块级作用域:
let为每个迭代创建一个新的词法环境(块作用域)每次循环迭代时:
- 创建一个新的块作用域
- 将当前
i的值绑定到这个作用域 - 回调函数捕获的是这个块作用域中的
i
-
每次迭代都是独立的:
// 伪代码表示每次迭代 { // 第一次迭代 let i = 0; setTimeout(() => console.log(i)); } { // 第二次迭代 let i = 1; setTimeout(() => console.log(i)); } // ...以此类推 -
闭包机制:每个回调函数都闭包了它自己块作用域中的
i
2. 使用立即执行函数表达式(IIFE)(ES5 及以下)
通过立即执行函数创建一个新的作用域,将当前的 index 值传递给这个作用域,从而让每个事件处理函数都能保存自己的 index 值。
<script>
const buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
(function (index) {
buttons[index].addEventListener('click', function () {
console.log('Clicked button index:', index);
});
})(i);
}
</script>
3. 使用 forEach 方法 mark
forEach 方法会为数组中的每个元素执行一次提供的函数,并且在每次迭代中都会创建一个新的作用域,因此可以正确地保存 index 值。
<script>
const buttons = document.querySelectorAll('button');
buttons.forEach((button, index) => {
button.addEventListener('click', function () {
console.log('Clicked button index:', index);
});
});
</script>
4.用闭包
for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
return function() {
console.log(j);
}
}(i), 1000);
}
如何用原生js给一个按钮绑定两个onclick事件?
// 事件监听 绑定多个事件
var btn = document.getElementById("btn");
btn.addEventListener('click', hello1)
btn.addEventListener("click", hello2);
function hello1() {
console.log(1);
}
function hello2() {
console.log(2);
}
拖拽会用到哪些事件
drag、dragstart、dragend、dragenter、dragover、dragleave、drop
for..in 和 object.keys的区别 ⭐️
for in 遍历对象所有可枚举属性 包括原型链上的属性
Object.keys 遍历对象所有可枚举属性 不包括原型链上的属性
hasOwnProperty 检查对象是自身属性,无法检查原型链上是否具有此属性名
var aa = [12, 34, 5];
Array.prototype.add = function() {}
for(let i in aa) {
console.log(i);
//输出0 1 2 add
}
console.log('--------------')
console.log(Object.keys(aa));
//输出0 1 2
console.log(aa.hasOwnProperty('add'))
//输出false
console.log(aa.hasOwnProperty('0'))
//输出true
总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in循环,而用Object.keys()代替。
ES2017 引入了跟Object.keys配套的Object.values和Object.entries,作为遍历一个对象的补充手段,供for...of循环使用。
let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 };
for (let key of keys(obj)) {
console.log(key); // 'a', 'b', 'c'
}
for (let value of values(obj)) {
console.log(value); // 1, 2, 3
}
for (let [key, value] of entries(obj)) {
console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}
如何判断img加载完成
在前端开发中,判断图片(<img> 元素)是否加载完成是一个常见需求,下面为你介绍几种不同场景下的判断方法。
1. 使用 onload 事件
在 HTML 中,可以直接为 <img> 标签添加 onload 属性,或者在 JavaScript 里使用 addEventListener 方法监听 load 事件。当图片成功加载后,load 事件会被触发。
直接在 HTML 中添加 onload 属性
<body>
<img src="https://picsum.photos/200/300" onload="imageLoaded()">
<script>
function imageLoaded() {
console.log('图片加载完成');
}
</script>
</body>
使用 JavaScript 的 addEventListener 方法
<body>
<img id="myImage" src="https://picsum.photos/200/300">
<script>
const img = document.getElementById('myImage');
img.addEventListener('load', function () {
console.log('图片加载完成');
});
</script>
</body>
</html>
2. 动态创建 img 元素并监听 load 事件
当你需要动态创建 img 元素时,可以在创建过程中监听 load 事件,以确保在图片加载完成后执行相应的操作。
<body>
<div id="imageContainer"></div>
<script>
const container = document.getElementById('imageContainer');
const img = new Image();
img.src = 'https://picsum.photos/200/300';
img.addEventListener('load', function () {
console.log('图片加载完成');
container.appendChild(img);
});
</script>
</body>
3. 处理图片加载失败的情况
除了监听 load 事件,还可以监听 error 事件, 以便在图片加载失败时进行相应的处理。
<img id="myImage" src="转存失败,建议直接上传图片文件 https://nonexistent-image-url.com/image.jpg" alt="转存失败,建议直接上传图片文件">
<script>
const img = document.getElementById('myImage');
img.addEventListener('load', function () {
console.log('图片加载完成');
});
img.addEventListener('error', function () {
console.log('图片加载失败');
});
</script>
4. 检查 complete 属性
在某些情况下,可能需要在代码中手动检查图片是否已经加载完成。可以通过检查 img 元素的 complete 属性来判断。如果 complete 属性为 true,表示图片已经加载完成;如果为 false,则表示图片还在加载中。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<img id="myImage" src="https://picsum.photos/200/300">
<script>
const img = document.getElementById('myImage');
if (img.complete) {
console.log('图片已经加载完成');
} else {
img.addEventListener('load', function () {
console.log('图片加载完成');
});
}
</script>
</body>
</html>
综上所述,判断图片加载完成最常用的方法是监听 load 事件,同时可以结合 error 事件处理图片加载失败的情况,以及使用 complete 属性进行手动检查
ajax请求时,如何解释json数据
JSON.parse() eval 方法
总结:“eval();”方法解析的时候不会去判断字符串是否合法,而且json对象中的js方法也会被执行,这是非常危险的; 而“JSON.parse();”方法的优点就不用多说了,推荐此方法。
对前端路由的理解?前后端路由的区别?⭐️
1.什么是路由
路由是根据不同的 url 地址展示不同的内容或页面;
2、什么是前端路由
很重要的一点是页面不刷新,前端路由就是把不同路由对应不同的内容或页面的任务交给前端来做,每跳转到不同的URL都是使用前端的锚点路由. 随着(SPA)单页应用的不断普及,前后端开发分离,目前项目基本都使用前端路由,在项目使用期间页面不会重新加载。
3、什么是后端路由?
浏览器在地址栏中切换不同的url时,每次都向后台服务器发出请求,服务器响应请求,在后台拼接html 文件传给前端显示, 返回不同的页面, 意味着浏览器会刷新页面,网速慢的话说不定屏幕全白再有新内容。后端路由的另外一个极大的问题就是 前后端不分离。
优点:分担了前端的压力,html和数据的拼接都是由服务器完成。
缺点:当项目十分庞大时,加大了服务器端的压力,同时在浏览器端不能输入制定的url路径进行指定模块的访问。另外一个就是如果当前网速过慢,那将会延迟页面的加载,对用户体验不是很友好。
4,什么时候使用前端路由?
在单页面应用,大部分页面结构不变,只改变部分内容的使用
5,前端路由有什么优点和缺点?
优点:
- 用户体验好,和后台网速没有关系,不需要每次都从服务器全部获取,快速展现给用户
- 实现了前后端的分离,方便开发。有很多框架都带有路由功能模块。
缺点:
- 使用浏览器的前进,后退键的时候会重新发送请求,没有合理地利用缓存
- 单页面无法记住之前滚动的位置,无法在前进,后退的时候记住滚动的位置
如何实现同一个浏览器多个标签页之间的通信
第一种——调用localStorage
在一个标签页里面使用 localStorage.setItem(key,value)添加(修改、删除)内容;在另一个标签页里面监听 storage 事件。
即可得到 localstorge 存储的值,实现不同标签页之间的通信。
标签页1:
<input id="name">
<input type="button" id="btn" value="提交">
<script type="text/javascript">
$(function () {
$("#btn").click(function () {
var name = $("#name").val();
localStorage.setItem("name", name);
});
});
</script>
标签页2:
$(function () {
window.addEventListener("storage", function (event) {
console.log(event.key + "=" + event.newValue);
});
});
第二种——调用cookie+setInterval()
将要传递的信息存储在cookie中,每隔一定时间读取cookie信息,即可随时获取要传递的信息。
页面1:
<input id="name">
<input type="button" id="btn" value="提交">
<script type="text/javascript">
$(function () {
$("#btn").click(function () {
var name = $("#name").val();
document.cookie = "name=" + name;
});
});
</script>
页面2:
<script type="text/javascript">
$(function () {
function getCookie(key) {
return JSON.parse("{"" + document.cookie.replace(/;\s+/gim, "","").replace(/=/gim, "":"") + ""}")[key];
}
setInterval(function () {
console.log("name=" + getCookie("name"));
}, 10000);
});
</script>
require和import有什么不同
所有import的都能用 require,但用require的不能用import;import支持编译时静态分析。动态绑定。node编程中最重要的思想就是模块化,import和require都是被模块化所使用。
遵循规范
- require 是 AMD规范引入方式
- import是es6的一个语法标准,如果要兼容浏览器的话必须转化成es5的语法
调用时间
- require是运行时调用,所以require理论上可以运用在代码的任何地方
- import是编译时调用,所以必须放在文件开头
本质
-
require是赋值过程,其实require的结果就是对象、数字、字符串、函数等,再把require的结果赋值给某个变量
-
import是解构过程,但是目前所有的引擎都还没有实现import,我们在node中使用babel支持ES6,也仅仅是将ES6转码为ES5再执行,import语法会被转码为require
-
import用于引入外部模块。
-
require不仅可以引用文件和模块,而且使用位置不受限制,可以在代码中使用
文件上传如何做断点续传 ⭐️⭐️
文件上传的断点续传是指在文件上传过程中(如网络中断、页面刷新等),无需重新上传整个文件,而是从上次中断的位置继续上传,从而节省带宽和时间。其核心原理是将文件分片上传,并通过记录已上传的分片信息,实现断点续传。
断点续传的核心步骤
-
文件分片处理将大文件分割为固定大小的小分片(如每片 1MB),避免一次性上传大文件导致的超时或内存占用过高。
- 分片依据:通过
File对象的slice(start, end)方法切割文件(浏览器端),或后端根据文件流切割(Node.js 等)。 - 分片标识:为每个分片生成唯一标识(如基于文件内容的哈希 + 分片索引),确保分片的唯一性和顺序性。
- 分片依据:通过
-
记录已上传分片上传前,前端向后端查询 “已上传的分片列表”,后端需要重复上传这些分片。
-
实现方式:
- 前端:可通过本地存储(如
localStorage)临时记录已上传分片(适合单会话断点)。 - 后端:在数据库中记录文件 ID(如文件哈希)对应的已上传分片索引,确保刷新页面或换设备后仍能续传。
- 前端:可通过本地存储(如
-
-
分片上传与校验前端只上传未记录的分片,每个分片携带 “文件 ID”“分片索引”“总分片数” 等信息。
- 后端接收分片后,暂存到临时目录,并校验分片的完整性(如通过 MD5 校验)。
- 若分片上传成功,后端更新 “已上传分片列表”。
-
合并分片当所有分片上传完成后,前端通知后端 “合并分片”,后端将临时目录中的所有分片按索引顺序拼接成完整文件,并存入目标路径。
关键技术点
-
文件唯一标识:为避免同一文件重复上传,可通过哈希算法(如 MD5、SHA-1)对文件内容计算唯一哈希值(文件 ID)。浏览器端可通过
FileReader读取文件内容后计算哈希(注意大文件计算性能,可分片计算后合并哈希)。 -
断点触发与恢复:
- 触发:网络中断、页面关闭、手动暂停等。
- 恢复:重新上传时,前端先查询已上传分片,跳过这些分片,直接上传剩余部分。
-
并发控制:分片可并发上传(如同时上传 3-5 个分片)提升速度,但需控制并发数避免后端压力过大。
示例流程(简化版)
-
前端处理:
// 1. 选择文件 const file = document.getElementById('fileInput').files[0]; // 2. 计算文件哈希(作为唯一ID) const fileId = await calculateFileHash(file); // 3. 分片(每1MB一片) const chunkSize = 1024 * 1024; const totalChunks = Math.ceil(file.size / chunkSize); // 4. 查询已上传分片 const uploadedChunks = await fetch(`/api/getUploaded?fileId=${fileId}`).then(res => res.json()); // 5. 上传未完成的分片 for (let i = 0; i < totalChunks; i++) { if (uploadedChunks.includes(i)) continue; // 跳过已上传分片 const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize); await uploadChunk(fileId, i, totalChunks, chunk); // 上传单个分片 } // 6. 通知后端合并分片 await fetch(`/api/merge?fileId=${fileId}`, { method: 'POST' }); -
后端处理:
- 接收分片:存储到临时目录(如
temp/{fileId}/{chunkIndex}),并记录已上传分片索引。 - 合并分片:收到合并请求后,按索引读取所有分片,拼接成完整文件,删除临时文件。
- 接收分片:存储到临时目录(如
注意事项
- 分片大小:过小会增加请求次数,过大会影响并发效率,通常 1-5MB 为宜。
- 校验机制:分片上传后需校验完整性(如 MD5),避免分片损坏导致合并后文件错误。
- 过期清理:后端需定期清理未完成上传的临时分片(如 24 小时未合并的文件),释放存储空间。
通过以上方式,即可实现可靠的断点续传,大幅提升大文件上传的用户体验。
JS判断设备来源
function deviceType(){
var ua = navigator.userAgent;
var agent = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"];
for(var i=0; i<len,len = agent.length; i++){
if(ua.indexOf(agent[i])>0){
break;
}
}
}
deviceType();
window.addEventListener('resize', function(){
deviceType();
})
微信的 有些不太一样
function isWeixin(){
var ua = navigator.userAgent.toLowerCase();
if(ua.match(/MicroMessenger/i)=='micromessenger'){
return true;
}else{
return false;
}
}