原型链的作用
当访问一个对象的属性或方法时,JavaScript 会沿着原型链逐层查找,直到找到该属性或方法,或者到达 null(即原型链的终点)为止。
深浅拷贝
方法 1:使用 JSON.stringify() 和 JSON.parse() 优点:实现简单,适用于大多数简单对象的深拷贝。 缺点: 无法处理函数、Date 对象、RegExp、Map、Set 等非 JSON 格式化数据。 如果对象中存在循环引用(即对象的某个子对象引用了父对象),使用该方法会报错。
方法 2:手写递归函数 优点:灵活强大,能够处理 Date、RegExp、循环引用等特殊情况。 缺点:代码较复杂,性能较低。
方法 3:使用 Lodash 的 cloneDeep 优点:功能强大,支持多种数据类型,包括 Date、RegExp、Map、Set 等复杂类型。 缺点:需要安装第三方依赖。
方法 4:使用 structuredClone (现代浏览器支持) 优点:浏览器原生支持,性能较好,支持更复杂的数据结构。 缺点: 目前不支持所有环境(如旧浏览器或 Node.js)。 不支持拷贝函数(类似于 JSON 的限制)。
- 简单对象 如果对象没有嵌套或者较为简单(如只包含基本数据类型的属性),可以使用: JSON.stringify/parse:简单快捷。
- 复杂对象 如果对象包含嵌套结构、数组、特殊类型(如 Date、RegExp),可以选择: 手写递归函数:用于完全自定义场景。 lodash.cloneDeep:使用方便,功能可靠。 structuredClone:原生方法(如果支持),性能更优。
/**
* 深拷贝函数
* @param {any} obj 需要深拷贝的对象
* @return {any} 拷贝后的对象
*/
function deepClone(obj, hash = new WeakMap()) {
// 处理 null 或非对象类型
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理日期对象
if (obj instanceof Date) {
return new Date(obj);
}
// 处理正则对象
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
}
// 处理 Map
if (obj instanceof Map) {
const result = new Map();
obj.forEach((value, key) => {
result.set(key, deepClone(value, hash));
});
return result;
}
// 处理 Set
if (obj instanceof Set) {
const result = new Set();
obj.forEach(value => {
result.add(deepClone(value, hash));
});
return result;
}
// 处理函数
if (typeof obj === 'function') {
return function() {
return obj.apply(this, arguments);
};
}
// 检查循环引用,如果对象已经被拷贝过,直接返回之前拷贝的结果
if (hash.has(obj)) {
return hash.get(obj);
}
// 创建一个新的对象或数组
const result = Array.isArray(obj) ? [] : {};
// 将当前对象存入 hash 表,用于检测循环引用
hash.set(obj, result);
// 递归复制所有属性
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
result[key] = deepClone(obj[key], hash);
}
}
return result;
}
循环引用
循环引用(Circular Reference)是指在数据结构中,一个对象直接或间接地引用自身,形成一个闭环的引用链。在 JavaScript 对象中,循环引用发生在对象的属性指向该对象自身或通过引用链最终指向自身的情况。
循环引用的示例 直接循环引用 ✨ Apply JavaScript const obj = {}; obj.self = obj; // obj 直接引用了自身 在这个例子中,obj 对象有一个名为 self 的属性,这个属性直接指向 obj 自身,形成了最简单的循环引用。
间接循环引用 ✨ Apply JavaScript const objA = {}; const objB = {};
objA.ref = objB; // objA 引用了 objB objB.ref = objA; // objB 引用了 objA,形成了循环 在这个例子中,objA 引用了 objB,而 objB 又引用了 objA,形成了一个循环引用的链条。
更复杂的循环引用 ✨ Apply JavaScript const parent = { name: 'Parent' };
const child = { name: 'Child', parent: parent };
parent.child = child; // 创建了一个循环引用 循环引用导致的问题 JSON 序列化错误:当你尝试使用 JSON.stringify() 序列化一个包含循环引用的对象时,会抛出 "TypeError: Converting circular structure to JSON" 错误。
内存泄漏:在一些旧的浏览器中,循环引用可能会导致垃圾回收器无法正确释放内存,造成内存泄漏(现代浏览器的垃圾回收器已经能够处理这种情况)。
栈溢出:在处理包含循环引用的对象时,如果使用递归而没有适当的检测机制,很容易导致栈溢出错误,例如普通的深拷贝函数遇到循环引用时。
闭包的作用
数据持久化
function createCounter() {
let count = 0;
return function() {
return ++count;
}
}
const counter = createCounter();
counter(); // 1
counter(); // 2
封装私有 延迟调用
箭头函数和普通函数的区别
| 特性 | 普通函数 | 箭头函数 |
|---|---|---|
| this 的绑定 | 动态绑定,取决于调用方式(可以通过call,apply,bind改变this绑定方式) | 静态绑定(词法作用域),继承外层作用域的 (不可以通过call,apply,bind改变this绑定方式) |
| arguments 对象 | 有自己的 arguments,包含所有实参 | 没有自己的 arguments(可以通过rest参数获取) |
| 构造函数 | 可以作为构造函数使用,允许通过 new 创建实例 | 不能作为构造函数,调用 new 会报错 |
| prototype 属性 | 有 prototype 属性 | 没有 prototype 属性 |
| 语法 | 需要使用 function 关键字 | 更简洁,省略 function 和 {} |
| 使用场景 | 适合动态绑定 this 的场景,复杂逻辑函数,构造函数 | 适合回调函数、轻量函数以及保持 this 的场景 |
| 性能 | 普通性能 | 更高性能(但差异通常微乎其微) |
this指向实例考察: juejin.cn/post/732050…
Generator函数
| 优点 | 缺点 |
|---|---|
| 支持流程暂停与恢复,适合复杂逻辑控制 | 手动推进流程(next()),代码复杂 |
| 惰性求值,节省内存资源 | 可读性较差,尤其对于新手和复杂场景 |
| 支持同步风格的异步代码编写 | 无法直接处理异步任务,需工具配合 |
| 天生支持迭代器模式 | 被 async/await 在某些场景取代 |
| 双向通信能力强,增强动态交互 | 易引发性能开销,执行效率略低 |
| 与现代框架(如 Redux-Saga)很好地结合使用 | 单线程特性,仍受限于 JavaScript 本身 |
Generator 是怎么样使用的以及各个阶段的变化如何?(高级)
- 首先生成器是一个函数,用来返回迭代器的
- 调用生成器后不会立即执行,而是通过返回的迭代器来控制这个生成器的一步一步执行的
- 通过调用迭代器的 next 方法来请求一个一个的值,返回的对象有两个属性,一个是 value,也就是值;另一个是done,是个布尔类型,done为true 说明生成器函数执行完毕,没有可返回的值了
- done为true后继续调用迭代器的 next 方法,返回值的value为undefined
状态变化:
- 每当执行到yield属性的时候,都会返回一个对象
- 这时候生成器处于一个非阻塞的挂起状态
- 调用迭代器的 next 方法的时候,生成器又从挂起状态改为执行状态,继续上一次的执行位置执行
- 直到遇到下一次yield依次循环
- 直到代码没有yield了,就会返回一个结果对象done为true,value为undefined
数组和伪数组的区别
| 特性 | 数组 | 伪数组 |
|---|---|---|
| 数据类型 | Array 类型 | 一般是 Object 类型 |
| 是否有 Array.prototype | 有,继承 Array.prototype | 没有,不继承Array.prototype |
| 是否可以直接使用数组方法 | 可以(如 slice、map) | 不可以,需先转换成数组 |
| 是否有 length 属性 | 有 | 有 |
| 存储方式 | 按索引存储值 | 按索引存储值 |
| 灵活性 | 支持数组方法,功能更全 | 功能有限,通常只能遍历 |
将伪数组转成数组的方式
当你需要操作伪数组,可以将伪数组转换为真正的数组。以下是几种方法:
- 使用 Array.prototype.slice
const argsArray = Array.prototype.slice.call(arguments);
- 使用 Array.from (推荐,支持更简单的语法)
const argsArray = Array.from(arguments);
const nodeArray = Array.from(document.querySelectorAll("div"));
- 使用扩展运算符(...)
const argsArray = [...arguments];
const nodeArray = [...document.querySelectorAll("div")];
4.arr.concat([])
call,apply,bind
Math.max.call(null, 1,2,3)
Math.max.apply(null, [1,2,3])
Math.max.bind(null, 1,2,3) // 返回函数(不执行),加上()返回3
构造函数(Fn),原型(Fn.prototype),实例(instance)之间的关系
instance.__proto__ === Fn.prototype
Fn.prototype.constructor === Fn
for...in 和 for...of
const object = { a: 1, b: 2, c: 3 };
for (const property in object) {
console.log(`${property}: ${object[property]}`);
}
const array1 = ["a", "b", "c"];
for (const element of array1) {
console.log(element);
}
| 特性 | for...in | for...of |
|---|---|---|
| 遍历目标 | 对象的键名 | 可迭代对象的值 |
| 返回值类型 | 键名(字符串) | 值 |
| 遍历对象 | 可用于对象,且会遍历原型链上的可枚举属性 | 不适用对象,适用于数组、字符串、Map、Set 等 |
| 是否适用数组 | 虽然可以,但不推荐 | 推荐 |
| 是否检查原型链 | 会遍历原型链 | 不会遍历原型链 |
require和import的区别
| 特性 | require | import |
|---|---|---|
| 规范 | CommonJS | ESModule |
| 加载方式 | 运行时动态加载 | 编译期静态加载 |
| 同步/异步 | 同步 | 支持异步(通过 import()) |
| 默认导入/命名导入 | 不支持,导出单一对象const myModule = require('./myModule'); | 支持默认导入和命名导入// 默认导入:import myModule from './myModule';// 命名导import{funcA,funcB}from'./myModule'; |
| 浏览器支持 | 不直接支持,需要转译 | 受支持(包括现代浏览器) |
| 写法 | const myModule = require('./myModule');myModule.doSomething() | import myModule from './myModule';myModule.doSomething(); |
大文件下载
直传阿里云oss上传,如何优化文件上传功能,减少服务器带宽压力,提升用户体验
直传阿里云 OSS(Object Storage Service)是一种避免文件先传到应用服务器、再上传到 OSS 的高效方法,能够直接从客户端上传文件到 OSS,减少对应用服务器的带宽压力。以下是常见的优化方法,通过这些技巧,你可以进一步减少服务器负载,并优化用户体验。
1. 前端直传 OSS
最基本的优化是实现从前端直接上传到 OSS,通过生成签名 URL 或使用 STS(临时授权票据)来绕过应用服务器,直接将文件上传到 OSS。
实现方案:
-
创建上传签名(服务器端)
- 在客户端上传文件之前,由服务器生成上传的临时授权信息(签名或 STS)。
- 客户端使用这个签名或者 STS 直接上传文件到 OSS。
- 授权信息需要有时间限制,确保安全性。
生成签名伪代码:
const OSS = require('ali-oss'); const client = new OSS({ region: '<your-region>', bucket: '<your-bucket>', accessKeyId: '<your-access-key-id>', accessKeySecret: '<your-access-key-secret>', }); const policy = { expiration: '2023-12-31T12:00:00Z', // 当前签名的过期时间 conditions: [ ['content-length-range', 0, 10485760], // 限制文件大小 ['starts-with', '$key', 'user-uploads/'], // 文件存储路径限制 ] }; const signature = client.generatePostPolicy(policy); return signature; // 返回给前端 -
前端通过签名上传到 OSS
- 使用签名或 STS,直接调用阿里云 OSS 提供的 REST 接口或上传 SDK。
- 示例代码(前端上传文件):
const formData = new FormData(); formData.append("key", "<file-path>"); // 上传路径 formData.append("policy", "<policy>"); formData.append("OSSAccessKeyId", "<accessKeyId>"); formData.append("success_action_status", "200"); formData.append("signature", "<signature>"); formData.append("file", file); // 要上传的文件 const response = await fetch('<oss-endpoint>', { method: 'POST', body: formData });
好处:
- 文件完全绕过应用服务器,直接上传到 OSS,降低对服务器的带宽消耗。
- 客户端上传效率更高,用户体验更好。
2. 分片(断点续传)上传
对于大型文件(如视频、压缩文件),单次上传可能会消耗较多的网络资源,且失败后需要重新上传整个文件。阿里云 OSS 支持分片上传,从客户端处理大文件时可使用分片上传功能。
优化方案:
- 在客户端将文件拆分为多片(分片大小建议 100KB~5MB)。
- 调用 OSS 的分片上传接口分块并发上传文件。
- 上传成功后,通过 OSS 的
CompleteMultipartUpload来合并分片文件,生成最终的完整文件。
使用阿里云 SDK 的分片上传(示例代码)
const OSS = require('ali-oss');
const client = new OSS({
region: '<your-region>',
bucket: '<your-bucket>',
accessKeyId: '<your-access-key-id>',
accessKeySecret: '<your-access-key-secret>'
});
const uploadFile = async () => {
try {
await client.multipartUpload('your-object-key', file, {
parallel: 5, // 并发上传的片数量
partSize: 100 * 1024, // 每片大小(字节)
progress: async (percentage, checkpoint) => {
console.log(`Upload progress: ${percentage * 100}%`);
}
});
console.log('Upload success');
} catch (err) {
console.error('Multipart upload failed: ', err);
}
};
uploadFile();
好处:
- 支持断点续传:即使上传中断,仅需要重新上传失败的分片。
- 并发上传提高上传速度,减少一次性大文件上传的失败率。
3. 文件类型与大小限制
为了减少不必要的上传流量,可以在上传前对文件进行预验证和限制:
-
限制文件大小 在客户端和服务端限制上传文件的大小(content-length-range)。例如:
if (file.size > MAX_FILE_SIZE) { alert('文件过大,请上传小于 10MB 的文件!'); return; } -
检查文件类型 验证文件扩展名或 MIME 类型,避免用户上传不符合要求的文件。
前端示例:
const allowedTypes = ['image/png', 'image/jpeg']; if (!allowedTypes.includes(file.type)) { alert('只允许上传 PNG 和 JPEG 文件!'); return; }
4. 文件压缩与预处理
在将文件上传到 OSS 之前,可对文件进行压缩和预处理:
-
图片压缩 对于图片文件,可以使用第三方库(如 compressor.js),将图片压缩后再上传:
import Compressor from 'compressorjs'; new Compressor(file, { quality: 0.8, // 压缩率(0-1) success(result) { // 将 result(压缩后的 Blob 对象)上传到 OSS uploadToOSS(result); }, error(err) { console.error('Compression failed:', err); } }); -
视频/音频压缩 对于视频文件,可以利用 FFmpeg 或其他工具来对文件进行前置压缩和裁剪。
5. 并发控制与防止重复上传
文件上传功能可以通过并发控制增强稳定性,同时避免重复上传浪费资源:
-
并发控制 使用如
p-limit的工具限制同时上传的文件数量,避免过多的并发请求对用户网络和 OSS 服务造成影响:import pLimit from 'p-limit'; const limit = pLimit(2); // 限制并发数量为 2 const uploadPromises = files.map(file => limit(() => uploadFile(file))); await Promise.all(uploadPromises); -
文件校验(防止重复上传) 通过计算文件的唯一标识(如 MD5 哈希),在上传时检查是否已存在相同文件,避免重复上传:
const fileHash = await calculateFileHash(file); // MD5 或 SHA1 const isExist = await checkFileInOSS(fileHash); // 检查文件是否已存在 if (isExist) { console.log('文件已存在,无需重复上传'); return; } await uploadFile(file);
6. 提升用户体验的优化
-
上传进度显示 在文件上传时显示进度条:
progress: async (percentage, checkpoint) => { console.log(`Upload progress: ${Math.round(percentage * 100)}%`); } -
异步上传 允许用户在后台完成上传,并提供上传完成后的通知。
-
断点续传体验 在网络中断或上传失败时,通过分片上传机制,告知用户继续上传。
7. 安全措施
-
避免暴露 AccessKeyId 和 AccessKeySecret 永远不要将 AccessKey 写死在前端!应该通过服务端生成签名或 STS 授权,并设置仅允许特定的文件上传时间和路径有效。
-
配置 OSS CORS(跨域) OSS 支持跨域请求,需要在 OSS 的 Bucket 设置中正确配置 CORS 规则,允许客户端通过浏览器直接向 OSS 发起请求。
总结
通过上面的方法,可以实现更高效、安全且用户体验良好的 OSS 文件上传功能:
- 使用前端直传和分片上传,大幅降低服务器带宽压力。
- 添加压缩、文件验证、防止重复上传等机制,提高效率、降低存储成本。
- 显示上传进度、支持断点续传,提升用户体验。