实现函数的柯里化 重点
JavaScript 中的 函数柯里化(Currying) 是一种将多参数函数转换为一系列单参数函数的技术。通过柯里化,可以实现 参数复用、延迟执行 和 函数组合 等高级功能。以下是柯里化的详细解析:
一、柯里化的定义与核心思想
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;
}
如何去除url中的#号
正则匹配
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) })
实现一个sleep函数 promise.retry 将一个同步callback包装成promise形式
写一个函数,可以控制最大并发数
以下是一个用于控制最大并发数的 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 被封禁。
通过这个函数,可以有效管理异步任务的执行节奏,优化资源利用率和系统稳定性。
浏览器如何预览图片,假设我要上传图片,未上传前我想在浏览器看到我待上传的图片
file协议 选中图片后,获取到图片的本地路径,然后显示在浏览中。
或者用终端命令查看图片本地路径加上一个file协议
file:///Users/aniugel/Desktop/timg.jpeg
如何优化图像,图像格式的区别
1、不用图片,尽量用css3代替。 比如说要实现修饰效果,如半透明、边框、圆角、阴影、渐变等,在当前主流浏览器中都可以用CSS达成。
2、 使用矢量图SVG替代位图。对于绝大多数图案、图标等,矢量图更小,且可缩放而无需生成多套图。现在主流浏览器都支持SVG了,所以可放心使用!
3.、使用恰当的图片格式。我们常见的图片格式有JPEG、GIF、PNG。基本上,内容图片多为照片之类的,适用于JPEG。而修饰图片通常更适合用无损压缩的PNG。GIF基本上除了GIF动画外不要使用。且动画的话,也更建议用video元素和视频格式,或用SVG动画取代。
4、按照HTTP协议设置合理的缓存。
5、使用字体图标webfont、CSS Sprites等。
6、用CSS或JavaScript实现预加载。
7、WebP图片格式能给前端带来的优化。WebP支持无损、有损压缩,动态、静态图片,压缩比率优于GIF、JPEG、JPEG2000、PG等格式,非常适合用于网络等图片传输。
图像格式的区别:
矢量图:图标字体,如 font-awesome;svg
位图:gif,jpg(jpeg),png
区别:
1、gif:是是一种无损,8位图片格式。具有支持动画,索引透明,压缩等特性。适用于做色彩简单(色调少)的图片,如logo,各种小图标icons等。
2、JPEG格式是一种大小与质量相平衡的压缩图片格式。适用于允许轻微失真的色彩丰富的照片,不适合做色彩简单(色调少)的图片,如logo,各种小图标icons等。
3、png:PNG可以细分为三种格式:PNG8,PNG24,PNG32。后面的数字代表这种PNG格式最多可以索引和存储的颜色值。
关于透明:PNG8支持索引透明和alpha透明;PNG24不支持透明;而PNG32在24位的PNG基础上增加了8位(256阶)的alpha通道透明;
优缺点:
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 前端如何转化
在前端开发里,经常会遇到需要将数据转化为 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…in循环是以字符串作为键名“0”、“1”、“2”等等。
②for…in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
③某些情况下,for…in循环会以任意顺序遍历键名。
④for…in循环主要是为遍历对象而设计的,不适用于遍历数组。
⑤for...in 循环会自动跳过那些没被赋值的元素,而 for 循环则不会,它会显示出 undefined。
for…of循环 es6方法
①有着同for…in一样的简洁语法,但是没有for…in那些缺点。
②不同于forEach方法,它可以与break、continue和return配合使用。
③for in 会遍历自定义属性,for of不会,推荐数组的时候用for of
一个数据结构只要部署了Symbol.iterator属性,就被视为具有iterator接口,就可以用for…of循环遍历它的成员。也就是说,for…of循环内部调用的是数据结构的Symbol.iterator方法。提供了遍历所有数据结构的统一操作接口.for…of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串
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、forEach和map方法里每次执行匿名函数都支持3个参数,参数分别是item(当前每一项),index(索引值),arr(原数组)
3、匿名函数中的this都是指向window
4、只能遍历数组
5、都不会改变原数组
区别
map方法
1.map方法返回一个新的数组,数组中的元素为原始数组调用函数处理后的值。
2.map方法不会对空数组进行检测,map方法不会改变原始数组。
3.浏览器支持:chrome、Safari1.5+、opera都支持,IE9+,
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方法
1.forEach方法用来调用数组的每个元素,将元素传给回调函数
2.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
无论arr是不是空数组,forEach返回的都是undefined。这个方法只是将数组中的每一项作为callback的参数执行一次。
forEach()可以做到的东西,map()也同样可以。反过来也是如此。
map()会分配内存空间存储新数组并返回,forEach()不会返回数据。
forEach()允许callback更改原始数组的元素。map()返回新的数组。
forEach()的执行速度 < map()的执行速度
.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
}
实现数组flat、filter等方法
手动实现parseInt
parseInt
是 JavaScript 中用于将字符串转换为整数的函数。下面我将手动实现一个简化版的 parseInt
,包含其核心功能。
基本实现
function myParseInt(str, radix = 10) {
// 处理空字符串
if (str === "") return NaN;
// 去除前导空白字符
str = str.trim();
// 确定符号
let sign = 1;
if (str[0] === "-") {
sign = -1;
str = str.slice(1);
} else if (str[0] === "+") {
str = str.slice(1);
}
// 处理基数
if (radix === undefined || radix === 0) {
radix = 10;
} else if (radix < 2 || radix > 36) {
return NaN;
}
// 特殊处理0x/0X前缀
if (radix === 16 && str.length >= 2 && str[0] === "0" && (str[1] === "x" || str[1] === "X")) {
str = str.slice(2);
}
let result = 0;
let valid = false;
for (let i = 0; i < str.length; i++) {
const char = str[i];
let digit;
if (char >= "0" && char <= "9") {
digit = char.charCodeAt(0) - "0".charCodeAt(0);
} else if (char >= "a" && char <= "z") {
digit = char.charCodeAt(0) - "a".charCodeAt(0) + 10;
} else if (char >= "A" && char <= "Z") {
digit = char.charCodeAt(0) - "A".charCodeAt(0) + 10;
} else {
// 遇到无效字符,停止解析
break;
}
if (digit >= radix) {
// 数字超出当前基数范围,停止解析
break;
}
valid = true;
result = result * radix + digit;
}
if (!valid) return NaN;
return sign * result;
}
功能说明
这个实现包含以下核心功能:
-
字符串处理:
- 去除前导空白字符
- 处理正负号
-
基数处理:
- 默认基数为10
- 支持2-36的基数范围
- 特殊处理16进制的前缀0x/0X
-
数字解析:
- 逐个字符解析
- 支持0-9、a-z、A-Z字符
- 遇到无效字符停止解析
-
边界情况:
- 空字符串返回NaN
- 无效基数返回NaN
- 无有效数字返回NaN
与原生的区别
这个简化版实现与原生parseInt
有以下区别:
- 不处理科学计数法(如"1e2")
- 不处理前导0的特殊基数推断(原生中"070"可能被解析为56或70)
- 不处理BigInt范围外的数值
- 更简单的错误处理
使用示例
console.log(myParseInt("123")); // 123
console.log(myParseInt("-123")); // -123
console.log(myParseInt(" 123 ")); // 123
console.log(myParseInt("12.3")); // 12
console.log(myParseInt("1010", 2)); // 10 (二进制)
console.log(myParseInt("FF", 16)); // 255
console.log(myParseInt("0xFF")); // 255
console.log(myParseInt("xyz")); // NaN
这个实现涵盖了parseInt
的大部分常见用例,可以作为理解其工作原理的良好起点。
ES6中的map和原生的对象有什么区别
object和Map存储的都是键值对组合。但是:
object的键的类型是 字符串;
map的键的类型是 可以是任意类型;
另外注意,object获取键值使用Object.keys(返回数组);
Map获取键值使用 map变量.keys() (返回迭代器)。
什么是组件,他是怎么封装的
组件(Component)是对数据和方法的简单封装
js前端组件的封装方法
定义一个类
类中增加一个方法
body中定义一个dom节点
脚本中把dom节点和类定义结合起来 , 实现特定的组件功能
vue组件封装
建立组件的模板,先把架子搭起来,写写样式,考虑你的组件的基本逻辑
然后在引用得组件中 用import引入组件
通过component定义组件名称
在把组件以标签的形式写出来。
函数式编程
函数式编程是一种 编程范式,你可以理解为一种软件架构的思维模式。它有着独立一套理论基础与边界法则,追求的是 更简洁、可预测、高复用、易测试。其实在现有的众多知名库中,都蕴含着丰富的函数式编程思想,如 React / Redux 等。
函数式编程(FP)深度解析
函数式编程是一种以数学函数为蓝本的编程范式,强调无副作用、声明式风格和数据不可变性。以下从底层原理到应用实践的系统性解析:
一、核心理论体系
1. Lambda演算基础
- 数学根源:Alonzo Church的λ演算(1930s)
- 核心要素:
- 变量绑定:
λx. x + 1
- 应用规则:
(λx. x+1)(5) → 6
- 规约策略:惰性求值 vs 严格求值
- 变量绑定:
2. 范畴论映射
- 函子(Functor):实现
map
方法的结构class Maybe { constructor(value) { this.value = value } map(fn) { return this.value ? new Maybe(fn(this.value)) : this } }
- 单子(Monad):处理嵌套结构的
flatMap
// 解包嵌套 Maybe Maybe.of(Maybe.of(5)).flatMap(x => x.map(v => v*2)) // Maybe(10)
二、核心特性详解
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
始终指向绑定了该事件处理函数的元素。
页面上生成一万个button,并且绑定事件,如何做(JS原生操作DOM)
- 立即执行函数
- 闭包
- let作用域
- forEach
循环绑定时的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
方法
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
介绍defineProperty方法,什么时候需要用到
Object.defineProperty是es5上的方法,这也就是为什么vue.js不兼容ie8及其以下浏览器的原因。
Object.defineProperty()
是 JavaScript 中的一个重要方法,它允许你直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。以下为你详细介绍它的使用方法、参数以及适用场景。
语法
Object.defineProperty(obj, prop, descriptor)
obj
:要在其上定义属性的对象。prop
:要定义或修改的属性的名称或Symbol
。descriptor
:要定义或修改的属性描述符,它是一个对象,包含了属性的各种特性,主要有以下两种类型:
数据描述符
包含以下可选键值:
value
:该属性对应的值,可以是任何有效的 JavaScript 值(数值、对象、函数等),默认值为undefined
。writable
:一个布尔值,表示该属性的值是否可以被修改,默认值为false
。enumerable
:一个布尔值,表示该属性是否可以在for...in
循环和Object.keys()
中被枚举,默认值为false
。configurable
:一个布尔值,表示该属性是否可以被删除,以及除value
和writable
之外的其他特性是否可以被修改,默认值为false
。
存取描述符
包含以下可选键值:
get
:一个获取属性值的函数,当访问该属性时会调用此函数,默认值为undefined
。set
:一个设置属性值的函数,当给该属性赋值时会调用此函数,默认值为undefined
。
示例代码
数据描述符示例
const obj = {};
Object.defineProperty(obj, 'property1', {
value: 42,
writable: false,
enumerable: true,
configurable: false
});
console.log(obj.property1); // 输出: 42
obj.property1 = 77;
console.log(obj.property1); // 输出: 42,因为 writable 为 false,属性值不能被修改
存取描述符示例
const obj = {
_value: 0
};
Object.defineProperty(obj, 'value', {
get() {
return this._value;
},
set(newValue) {
if (newValue >= 0) {
this._value = newValue;
}
}
});
obj.value = 10;
console.log(obj.value); // 输出: 10
obj.value = -5;
console.log(obj.value); // 输出: 10,因为设置的值小于 0,不满足条件,属性值未被修改
使用场景
1. 数据劫持与响应式编程
在前端框架(如 Vue.js 1.x 和 2.x)中,Object.defineProperty()
被用于实现数据劫持,从而实现响应式编程。当一个对象的属性被修改时,可以自动更新与之绑定的 DOM 元素。
const data = {
message: 'Hello, World!'
};
function observe(obj) {
if (typeof obj!== 'object' || obj === null) {
return;
}
Object.keys(obj).forEach(key => {
let value = obj[key];
Object.defineProperty(obj, key, {
get() {
console.log(`Getting ${key}: ${value}`);
return value;
},
set(newValue) {
console.log(`Setting ${key} to ${newValue}`);
value = newValue;
}
});
});
}
observe(data);
data.message = 'Hello, Vue!'; // 输出: Setting message to Hello, Vue!
console.log(data.message); // 输出: Getting message: Hello, Vue!
2. 实现私有属性和方法
通过 Object.defineProperty()
可以模拟实现对象的私有属性和方法,将属性的 enumerable
和 configurable
设置为 false
,并使用 getter
和 setter
来控制属性的访问和修改。
const person = {};
let _age = 0;
Object.defineProperty(person, 'age', {
get() {
return _age;
},
set(newAge) {
if (newAge >= 0 && newAge <= 120) {
_age = newAge;
}
},
enumerable: false,
configurable: false
});
person.age = 25;
console.log(person.age); // 输出: 25
3. 控制属性的行为
可以使用 Object.defineProperty()
来控制属性的读写权限、枚举性和可配置性,从而更好地管理对象的属性。
const obj = {};
Object.defineProperty(obj, 'readOnlyProperty', {
value: 'This is a read-only property',
writable: false,
enumerable: true,
configurable: false
});
obj.readOnlyProperty = 'Try to change the value';
console.log(obj.readOnlyProperty); // 输出: This is a read-only property,属性值不能被修改
综上所述,Object.defineProperty()
是一个非常强大的方法,在需要精确控制对象属性的行为、实现数据劫持和响应式编程以及模拟私有属性和方法等场景中都有广泛的应用。不过需要注意的是,Object.defineProperty()
存在一些局限性,例如无法检测对象属性的新增和删除,在 ES6 及以后的版本中,也可以使用 Proxy 来实现类似的功能。
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
事件,以便在图片加载失败时进行相应的处理。
<body>
<img id="myImage" src="https://nonexistent-image-url.com/image.jpg">
<script>
const img = document.getElementById('myImage');
img.addEventListener('load', function () {
console.log('图片加载完成');
});
img.addEventListener('error', function () {
console.log('图片加载失败');
});
</script>
</body>
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支持编译时静态分析,便于JS引入宏和类型检验。动态绑定。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不仅可以引用文件和模块,而且使用位置不受限制,可以在代码中使用
js模块化(commonjs/AMD/CMD/ES6)
图片懒加载、预加载
所谓懒加载:就是通过某些特定的条件,然后再给图片的src赋值,常见的懒加载方式有点击加载和滚动加载。如果是点击加载,那么一般是通过点击事件
如果是加载,那么就有些复杂,首先你要先明白触发加载的条件,一般都是文档的高度-浏览器窗高度-浏览器距离顶部的高度<规定的尺寸。达到一定条件的后,向for循环的图片数组(笔者使用的是vue)添加元素。最困难是获取文档的高度,浏览器窗高度,浏览器距离顶部的高度。当然你要注意滚动的上下方向,如果向上的话就不需要添加元素。\
图片预加载:很简单通过创建img元素,并将要预加载的src赋值给img元素,通过src的onload达到预加载的目的,但是此时图片只是存在了浏览器,但是并没有显示在页面上,通过观察network的可以发现图片已经加载完毕。
function loadImage(url,callback) {
var img = new Image();
img.src = url;
if(img.complete) { // 如果图片已经存在于浏览器缓存,直接调用回调函数
callback.call(img);
return; // 直接返回,不用再处理onload事件
}
img.onload = function(){
img.onload = null;
callback.call(img);
}
}
实现页面加载进度条
以下是实现页面加载进度条的详细步骤:
方案二:真实加载进度监控(需配合后端)
<div id="progress-container" style="/* 同上 */">
<div id="progress-bar"></div>
</div>
<script>
// 使用Fetch API监控真实加载进度
function updateProgress(loaded, total) {
const progress = (loaded / total) * 100;
document.getElementById('progress-bar').style.width = progress + '%';
}
// 监听所有资源加载
window.addEventListener('load', function() {
const resources = window.performance.getEntriesByType('resource');
const totalSize = resources.reduce((acc, r) => acc + r.transferSize, 0);
let loadedSize = 0;
resources.forEach(resource => {
loadedSize += resource.transferSize;
updateProgress(loadedSize, totalSize);
});
// 最终隐藏
setTimeout(() => {
document.getElementById('progress-container').style.opacity = '0';
}, 500);
});
</script>
方案三:使用第三方库(推荐NProgress)
<!-- 引入NProgress -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/nprogress/0.2.0/nprogress.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/nprogress/0.2.0/nprogress.min.js"></script>
<script>
// 初始化配置
NProgress.configure({
showSpinner: false,
trickleSpeed: 200
});
// 开始加载
NProgress.start();
// 监听资源加载
document.addEventListener('readystatechange', () => {
if (document.readyState === 'complete') {
NProgress.done();
}
});
// 或者AJAX请求监控
$(document).ajaxStart(() => NProgress.start());
$(document).ajaxStop(() => NProgress.done());
</script>
实现要点说明:
-
视觉设计原则:
- 使用固定定位覆盖页面顶部
- 高度建议2-4px,颜色与网站主题色一致
- 加载完成时添加渐隐动画
-
性能优化:
- 使用CSS transition代替JavaScript动画
- 避免频繁重绘(requestAnimationFrame优化)
- 加载完成后及时移除DOM元素
-
高级功能扩展:
// 分段加载监控 const loadSteps = { DOMLoaded: 30, CSSLoaded: 60, ImagesLoaded: 90, Complete: 100 }; document.addEventListener('DOMContentLoaded', () => updateProgress(loadSteps.DOMLoaded)); window.addEventListener('load', () => updateProgress(loadSteps.Complete));
-
错误处理:
window.addEventListener('error', () => { progressBar.style.backgroundColor = '#ff4444'; clearInterval(interval); });
选择方案时:
- 快速实现:使用方案三(NProgress)
- 纯展示需求:方案一
- 精确监控:方案二(需配合服务端统计)
实际效果应确保进度条:
- 初始快速到达25%建立用户信任
- 中间保持持续移动(即使缓慢)
- 最后100%时保持短暂可见后消失
lazyMan
实现eventEmitter 实现instanceof new的过程 lazyMan 函数currying
函数currying
节流 防抖 生拷贝 数组乱序 flat filter 手写call & apply & bind
文件上传如何做断点续传
前端大文件上传网上的大部分文章已经给出了解决方案,核心是利用 Blob.prototype.slice 方法,和数组的 slice 方法相似,调用的 slice 方法可以返回原文件的某个切片
这样我们就可以根据预先设置好的切片最大数量将文件切分为一个个切片,然后借助 http 的可并发性,同时上传多个切片,这样从原本传一个大文件,变成了同时传多个小的文件切片,可以大大减少上传时间
另外由于是并发,传输到服务端的顺序可能会发生变化,所以我们还需要给每个切片记录顺序
iframe的缺点有哪些
getBoundingClientRect 方法的弊端;
静态资源加载和更新的策略;
大公司的静态资源优化方案,基本上要实现这么几个东西:
1.配置超长时间的本地缓存 —— 节省带宽,提高性能
2.采用内容摘要
作为缓存更新依据 —— 精确的缓存控制
3.静态资源CDN部署 —— 优化网络请求
4.更资源发布路径实现非覆盖式发布
—— 平滑升级
5.合并打包,压缩,各种其他优化
缓存静态资源的注意事项
在前端开发中,缓存静态资源(如 CSS、JavaScript、图片等)能显著提升网站性能和用户体验,但在使用缓存时需要注意以下方面:
缓存策略选择
-
强缓存
- 适用场景:对于不常更新的静态资源,像第三方库(如 jQuery、React 等),可以使用强缓存。因为这些资源更新频率低,设置较长的缓存时间能减少请求,提高页面加载速度。
- 控制方法:通过设置 HTTP 响应头中的
Cache-Control
和Expires
字段来控制。Cache-Control
是 HTTP/1.1 标准,优先级高于Expires
(HTTP/1.0 标准)。例如,设置Cache-Control: max-age=31536000
表示资源在 1 年内有效。
-
协商缓存
- 适用场景:对于可能会更新的静态资源,如项目内部的 CSS、JavaScript 文件,使用协商缓存更合适。这样既能保证资源更新时能及时获取到最新版本,又能在资源未更新时利用缓存。
- 控制方法:通过设置
ETag
和Last-Modified
响应头来实现。ETag
是资源的唯一标识,Last-Modified
是资源的最后修改时间。浏览器下次请求时会携带If-None-Match
(对应ETag
)和If-Modified-Since
(对应Last-Modified
)请求头,服务器根据这些信息判断资源是否有更新。
资源版本管理
- 文件名加哈希值:为静态资源文件名添加哈希值,如
main.123456.css
。当资源内容发生变化时,哈希值也会改变,从而使浏览器认为是新的资源,避免使用旧缓存。可以使用构建工具(如 Webpack)来自动生成带哈希值的文件名。 - 版本号管理:在 URL 中添加版本号参数,如
main.css?v=2
。当资源更新时,更新版本号,让浏览器重新请求资源。但这种方法需要手动管理版本号,相对繁琐。
缓存失效处理
- 更新资源后及时更新缓存:当静态资源更新时,要确保服务器端的缓存策略也相应更新。例如,如果修改了 CSS 文件,需要更新
ETag
或Last-Modified
信息,让浏览器能获取到最新的资源。 - 缓存穿透问题:在某些情况下,可能会出现缓存穿透问题,即浏览器绕过缓存直接请求服务器。这可能是由于缓存策略设置不当或浏览器设置问题导致的。可以通过检查浏览器请求头和服务器响应头来排查问题。
不同环境的缓存设置
- 开发环境:在开发环境中,为了方便调试,通常不建议使用过长的缓存时间,甚至可以禁用缓存。可以在服务器配置中设置
Cache-Control: no-cache
或Cache-Control: no-store
。 - 生产环境:在生产环境中,为了提高性能,应合理设置缓存策略,根据资源的更新频率选择合适的缓存时间。
兼容性问题
- 不同浏览器的支持:不同浏览器对缓存策略的支持可能存在差异,特别是一些老旧浏览器。在设置缓存策略时,要考虑到这些兼容性问题,可以使用一些工具(如 Can I Use)来查询浏览器对缓存相关特性的支持情况。
- CDN 缓存:如果使用 CDN 来分发静态资源,要了解 CDN 提供商的缓存策略和更新机制。有些 CDN 可能会有自己的缓存时间设置,需要根据 CDN 的规则来管理资源缓存。
安全性考虑
- 防止缓存中毒:要确保缓存的资源是安全的,避免缓存被恶意篡改。可以通过使用 HTTPS 协议来传输资源,防止中间人攻击。
- 敏感信息缓存:避免将包含敏感信息的资源进行缓存,如用户的个人信息、认证信息等。因为缓存可能会被多个用户共享,存在信息泄露的风险。
CDN 服务器的了解和使用
CDN的全称是Content Delivery Network,即内容分发网络。其基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。CDN可以理解为一个普通缓存,如代理缓存或者说边缘缓存,即便不关心用户的具体地理位置,也应该考虑使用cdn的代理缓存来提高用户体验。
CDN的优势
很明显:
(1)CDN节点解决了跨运营商和跨地域访问的问题
,访问延时大大降低;
(2)大部分请求在CDN边缘节点完成,CDN起到了分流作用,减轻了源站的负载。
CDN的缺点
当网站更新时,如果CDN节点上数据没有及时更新,即便用户再浏览器使用Ctrl +F5的方式使浏览器端的缓存失效,也会因为CDN边缘节点没有同步最新数据而导致用户访问异常。
CDN缓存策略
CDN边缘节点缓存策略因服务商不同而不同,但一般都会遵循http标准协议,通过http响应头中的Cache-control: max-age的字段来设置CDN边缘节点数据缓存时间。
当客户端向CDN节点请求数据时,CDN节点会判断缓存数据是否过期,若缓存数据并没有过期,则直接将缓存数据返回给客户端;否则,CDN节点就会向源站发出回源请求,从源站拉取最新数据,更新本地缓存,并将最新数据返回给客户端。
CDN服务商一般会提供基于文件后缀、目录多个维度来指定CDN缓存时间,为用户提供更精细化的缓存管理。
CDN缓存时间会对“回源率”产生直接的影响。若CDN缓存时间较短,CDN边缘节点上的数据会经常失效,导致频繁回源,增加了源站的负载,同时也增大的访问延时;若CDN缓存时间太长,会带来数据更新时间慢的问题。开发者需要增对特定的业务,来做特定的数据缓存时间管理。
CDN缓存刷新
CDN边缘节点对开发者是透明的,相比于浏览器Ctrl+F5的强制刷新来使浏览器本地缓存失效,开发者可以通过CDN服务商提供的“刷新缓存”接口来达到清理CDN边缘节点缓存的目的。这样开发者在更新数据后,可以使用“刷新缓存”功能来强制CDN节点上的数据缓存过期,保证客户端在访问时,拉取到最新的数据。
webkit基础知识
请简单实现双向数据绑定mvvm
vuejs是利用Object.defineProperty来实现的MVVM,采用的是订阅发布模式。 每个data中都有set和get属性,这种点对点的效率,比Angular实现MVVM的方式的效率更高。
<body>
<input type="text">
<script>
const input = document.querySelector('input')
const obj = {}
Object.defineProperty(obj, 'data', {
enumerable: false, // 不可枚举
configurable: false, // 不可删除
set(value){
input.value = value
_value = value
// console.log(input.value)
},
get(){
return _value
}
})
obj.data = '123'
input.onchange = e => {
obj.data = e.target.value
}
</script>
</body>
Dom操作
增删改查, 如:
document.elementById
document.querySelectAll
document.appendChild
docuemnt.removeChild
实现一个函数,判断是不是回文字符串
原文的思路是将字符串转化成数组=>反转数组=>拼接成字符串。这种做法充分利用了js的BIF(内置函数),但性能有所损耗:
function run(input) {
if (typeof input !== 'string') return false;
return input.split('').reverse().join('') === input;
}
复制代码其实正常思路也很简单就能实现,性能更高,但是没有利用js的特性:
// 回文字符串
const palindrome = (str) => {
// 类型判断
if(typeof str !== 'string') {
return false;
}
let len = str.length;
for(let i = 0; i < len / 2; ++i){
if(str[i] !== str[len - i - 1]){
return false;
}
}
return true;
}
场景题:一个气球从右上角移动到中间,然后抖动,如何实现
文件上传如何实现?,除了input还有什么别的方法?
用flash,如uploadify
h5 file API:HTML5拖拽多文件上传asp.net示例
还可以在浏览器把图片转成base64字符串,然后把字符串发到服务器端存起来
使用js实现一个持续的动画效果
juejin.im/post/5ab0da…
安排直接看连接
解析url后的参数
实现一个简单的模版引擎:
如何快速让字符串变成已千为精度的数字
function exchange(num) {
num += ''; //转成字符串
if (num.length <= 3) {
return num;
}
num = num.replace(/\d{1,3}(?=(\d{3})+$)/g, (v) => {
console.log(v)
return v + ',';
});
return num;
}
console.log(exchange(1234567));
逻辑分辨率和物理分辨率有什么区别,如何利用js互相转化
image.png
utf-8和unicode的区别
image.png
Unicode(统一码、万国码、单一码)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。1990年开始研发,1994年正式公布。
类似的,日文和韩文等其他语言也有这个问题。为了统一所有文字的编码,Unicode应运而生。Unicode把所有语言都统一到一套编码里,这样就不会再有乱码问题了。
世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。
可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是 Unicode,就像它的名字都表示的,这是一种所有符号的编码。
Unicode 当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字严。具体的符号对应表,可以查询unicode.org,或者专门的汉字对应表。
在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。
用记事本编辑的时候,从文件读取的UTF-8字符被转换为Unicode字符到内存里,编辑完成后,保存的时候再把Unicode转换为UTF-8保存到文件:
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;
}
}
让文本不可复制
-webkit-user-select: none;
-ms-user-select: none;
-moz-user-select: none;
-khtml-user-select: none;
user-select: none;
1、答案区域监听copy事件,并阻止这个事件的默认行为。
2、获取选中的内容(window.getSelection())加上版权信息,然后设置到剪切板(clipboarddata.setData())。
js的 for 跟for in 循环它们之间的区别?
遍历数组时的异同: for循环 数组下标的typeof类型:number,
for in 循环数组下标的typeof类型:stringvar southSu = ['苏南','深圳','18','男'];
for(var i=0;i<southSu.length;i++){
console.log(typeof i); //number
console.log(southSu[i]);// 苏南 , 深圳 , 18 , 男
}
var arr = ['苏南','深圳','18','男','帅气'];
for(var k in arr){
console.log(typeof k);//string
console.log(arr[k]);// 苏南 , 深圳 , 18 , 男 , 帅气
}
复制代码遍历对象时的异同:for循环 无法用于循环对象,获取不到obj.length;
for in 循环遍历对象的属性时,原型链上的所有属性都将被访问,解决方案:
使用hasOwnProperty方法过滤或Object.keys会返回自身可枚举属性组
成的数组 Object.prototype.test = '原型链上的属性';//首席填坑官∙苏南的专栏,梅斌的专栏
var southSu = {name:'苏南',address:'深圳',age:18,sex:'男',height:176};
for(var i=0;i<southSu.length;i++){
console.log(typeof i); //空
console.log(southSu[i]);//空
}
for(var k in southSu){
console.log(typeof k);//string
console.log(southSu[k]);// 苏南 , 深圳 , 18 , 男 , 176 , 原型链上的属性
}
请简述js中的this指针绑定
这个几乎是常问到的知识点,我在看《你不知道的javascript》书中有过很详细的描述。js中的this指针绑定总结下包含四种形式:默认绑定,隐式绑定,显式绑定,new绑定,优先级从小到大,有分别阐述了四种形式的代码。
// 默认绑定
function foo(){
console.info(this.a)
}
var a = 'hello'
foo()
// 隐式绑定
// 调用位置是否有上下文对象
function foo() {
console.info(this.a)
}
var obj = {
a: 1,
foo: foo
}
obj.foo()
// 显式绑定
// 对应apply call bind
// 其中bind又叫硬绑定
// bind(..) 会返回一个硬编码的新函数,它会把参数设置为this 的上下文并调用原始函数。
垃圾回收机制
// [防爬虫标识-沙海听雨]
// new绑定
// 使用new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
// 1. 创建(或者说构造)一个全新的对象。
// 2. 这个新对象会被执行[[ 原型]] 连接。
// 3. 这个新对象会绑定到函数调用的this。
// 4. 如果函数没有返回其他对象,那么new 表达式中的函数调用会自动返回这个新对象。