05 - JS 小技巧与手写实现
目录
- 实用小技巧
- 数组常用 API
- 字符串常用 API
- 获取 URL 参数
- 手写数组方法
- 手写 jQuery(含插件扩展)
- Ajax / Fetch / Axios 区别
- script 标签的加载优化
- 创建二维数组
- 定时器与 requestAnimationFrame
一、实用小技巧
字符串快速转数字
js
// 方式一:parseInt / parseFloat
parseInt('123'); // 123
parseFloat('3.14'); // 3.14
// 方式二:一元加号(推荐简洁场景)
const num = +'123'; // 123
const float = +'3.14'; // 3.14字符串拼接
js
// 传统方式
const greeting = 'Hello, ' + name;
// 模板字符串(推荐)
const greeting = `Hello, ${name}`;
const multi = `第一行
第二行`;防止浅拷贝陷阱
js
// 浅拷贝(Object.assign / 展开运算符)
const b = Object.assign({}, a);
const c = { ...a };
// ⚠️ 若 a 包含嵌套对象,引用类型属性仍是共享的
// 深拷贝(简单对象用 JSON 方式)
const deep = JSON.parse(JSON.stringify(a));
// ⚠️ 无法处理函数/undefined/Symbol/循环引用二、数组常用 API
| 操作 | 方法 | 说明 |
|---|---|---|
| 添加末尾 | push(...items) | 返回新长度 |
| 添加开头 | unshift(...items) | 返回新长度 |
| 删除末尾 | pop() | 返回被删元素 |
| 删除/插入 | splice(start, count, ...items) | 修改原数组,返回被删元素数组 |
| 遍历 | forEach(fn) | 无返回值 |
| 映射 | map(fn) | 返回新数组 |
| 过滤 | filter(fn) | 返回新数组 |
| 查找 | find(fn) | 找到第一个符合条件的元素 |
| 判断 | some(fn) | 至少一个满足则 true |
| 判断 | every(fn) | 全部满足则 true |
| 去重 | [...new Set(arr)] | 仅能去重基本类型 |
| 扁平化 | flat(depth) | 默认展开一层 |
| 归并 | reduce(fn, init) | 累计计算 |
js
// splice 示例
let arr = [1, 2, 3, 4, 5];
arr.splice(1, 2); // 从索引1删除2个: arr → [1, 4, 5]
arr.splice(1, 0, 'a', 'b'); // 在索引1插入: arr → [1, 'a', 'b', 4, 5]三、字符串常用 API
| 操作 | 方法 | 说明 |
|---|---|---|
| 拼接 | str1 + str2 或 concat() | |
| 替换 | replace(search, rep) | 只替换第一个 |
| 全替换 | replaceAll(search, rep) | 替换所有 |
| 截取 | slice(start, end) | 支持负数 |
| 截取 | substring(start, end) | 不支持负数 |
| 截取 | substr(start, length) | 从 start 起取 length 个字符 |
| 分割 | split(delimiter) | 返回数组 |
| 查找 | indexOf(str) | 返回索引,-1 表示不存在 |
| 查找 | includes(str) | 返回布尔值 |
| 去空格 | trim() / trimStart() / trimEnd() | |
| 大小写 | toUpperCase() / toLowerCase() |
四、获取 URL 参数
js
// 方式一:URLSearchParams(推荐)
function getParamsFromUrl(url) {
const paramsStr = url.split('?')[1];
const urlParams = new URLSearchParams(paramsStr);
return Object.fromEntries(urlParams.entries());
}
getParamsFromUrl('https://www.example.com?a=1&b=2&c=3');
// { a: '1', b: '2', c: '3' }
// 方式二:使用 URL 对象
function getParams(url) {
return Object.fromEntries(new URL(url).searchParams);
}五、手写数组方法
手写 unshift(向数组头部添加元素)
js
Array.prototype.myUnshift = function(...items) {
this.reverse();
for (let i = items.length - 1; i >= 0; i--) {
this.push(items[i]);
}
this.reverse();
return this.length;
};
const arr = [1, 2, 3];
console.log(arr.myUnshift(0, -1), arr); // 5, [-1, 0, 1, 2, 3]手写 unique(数组去重)
js
const arr = [1, 1, '1', '1', {}, {}, {age: 10}, {age: 10},"NaN", NaN, NaN, "NaN", null, null]
function uniqueArr(arr) {
let res = []
let set = new Set()
let hasNaN = false
arr.forEach(element => {
if(element!==element) {
// 说明这是个NaN
if(!hasNaN) {
res.push(element)
hasNaN = true
}
}
else if(typeof element === 'object' && element != null) {
// 说明element是一个数组或对象,需要深层比较
let jsonStr = JSON.stringify(element)
if(!set.has(jsonStr)){
res.push(element)
set.add(jsonStr)
}
}
else {
if(!set.has(element)){
res.push(element)
set.add(element)
}
}
});
return res
}
console.log(uniqueArr(arr))
// 注意:
// indexOf 对 NaN 返回 -1(因为 NaN !== NaN)
// includes 能正确识别 NaN
[1, 1, NaN, NaN, null, null].myUnique();
// [1, NaN, null]注意事项
js
// indexOf 无法识别 NaN
[NaN].indexOf(NaN); // -1
// includes 可以识别 NaN
[NaN].includes(NaN); // true六、手写 jQuery(含插件扩展)
基础实现
js
class MyJQuery {
constructor(selector) {
const result = document.querySelectorAll(selector);
this.length = result.length;
for (let i = 0; i < this.length; i++) {
this[i] = result[i];
}
}
get(index) {
return this[index];
}
each(fn) {
for (let i = 0; i < this.length; i++) {
fn(this[i]);
}
return this;
}
on(type, fn) {
return this.each(elem => {
elem.addEventListener(type, fn, false);
});
}
}
// 使用
const $p = new MyJQuery('p');
$p.each(elem => console.log(elem.innerHTML));
$p.on('click', e => console.log('点击:', e.target.innerHTML));扩展机制
js
// 插件形式(扩展原型)
MyJQuery.prototype.dialog = function(info) {
alert(info);
};
// 复写机制(继承后扩展)
class MyJQuery2 extends MyJQuery {
constructor(selector) {
super(selector);
}
addClass(className) {
return this.each(elem => elem.classList.add(className));
}
}七、Ajax / Fetch / Axios 区别
| 特性 | Ajax (XHR) | Fetch | Axios |
|---|---|---|---|
| 定位 | 技术概念/底层 API | 浏览器原生 API | 第三方库 |
| 基于 | 回调函数 | Promise | Promise(基于 XHR 或 Fetch) |
| 错误处理 | 手动判断状态码 | HTTP 错误不会 reject | 自动处理 HTTP 错误 |
| 拦截器 | ❌ | ❌ | ✅ |
| 请求取消 | xhr.abort() | AbortController | CancelToken |
| JSON 自动解析 | ❌ | 需手动调用 .json() | ✅ |
| Cookie 自动携带 | ✅ | ❌(需设置 credentials) | ✅ |
| Node.js 支持 | ❌ | 需 polyfill | ✅ |
Axios 拦截器实践
js
// 请求拦截器:自动添加 Token
axios.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
},
error => Promise.reject(error)
);
// 响应拦截器:统一错误处理
axios.interceptors.response.use(
response => response,
error => {
if (error.response) {
switch (error.response.status) {
case 401: /* 跳转登录 */ break;
case 403: /* 权限不足 */ break;
case 500: /* 服务器错误 */ break;
}
}
return Promise.reject(error);
}
);如何选择
- 追求简洁与原生支持 →
Fetch - 需要完善功能与拦截器 →
Axios(推荐大多数项目) - 维护旧项目或兼容 IE →
XMLHttpRequest+Axiospolyfill
八、script 标签的加载优化
浏览器解析到 <script> 时,会暂停 DOM 构建,等待脚本下载并执行完成。
html
<!-- 默认:阻塞 HTML 解析 -->
<script src="app.js"></script>
<!-- defer:并行下载,HTML 解析完成后按顺序执行 -->
<script defer src="app.js"></script>
<!-- async:并行下载,下载完后立即执行(不保证顺序) -->
<script async src="analytics.js"></script>| 属性 | 下载时机 | 执行时机 | 保证顺序 | 适用场景 |
|---|---|---|---|---|
| 默认 | 阻塞解析 | 立即执行 | ✅ | - |
defer | 并行下载 | HTML 解析完后 | ✅ | 依赖 DOM 的脚本 |
async | 并行下载 | 下载完立即执行 | ❌ | 独立脚本(统计、广告) |
九、创建二维数组
正确写法(每行独立数组)
js
// 方式一:Array.from(推荐)
const arr = Array.from({ length: rows }, () =>
Array.from({ length: cols }, () => 0)
);
// 方式二:fill + map
const arr = new Array(rows).fill(null).map(() => new Array(cols).fill(0));
// 方式三:循环 push
const arr = [];
for (let i = 0; i < rows; i++) arr.push(new Array(cols).fill(0));错误写法(共享引用)
js
// ❌ 错误:所有行共享同一个数组
const arr = new Array(3).fill([]);
arr[0].push(1);
console.log(arr[1]); // [1],被意外修改十、定时器与 requestAnimationFrame
| 定时器 | 特点 | 适用场景 |
|---|---|---|
setTimeout | 延迟执行一次,精度受 JS 线程影响 | 一次性延迟操作 |
setInterval | 周期执行,可能因 JS 繁忙导致跳帧 | 简单周期任务 |
requestAnimationFrame | 与屏幕刷新同步(约 16.7ms/帧),精度高 | 动画、精确计时 |
用 requestAnimationFrame 模拟 setInterval
js
function mySetInterval(callback, interval) {
let timer;
const now = Date.now;
let startTime = now();
const loop = () => {
timer = requestAnimationFrame(loop);
const endTime = now();
if (endTime - startTime >= interval) {
startTime = now();
callback(timer);
}
};
timer = requestAnimationFrame(loop);
return timer;
}
// 使用:每秒执行,最多 3 次
let count = 0;
const t = mySetInterval(timer => {
console.log('执行');
count++;
if (count >= 3) cancelAnimationFrame(timer);
}, 1000);十一、EventBus
js
class EventBus {
/*
eventObj = {
key1 : {
id : function();
id : function();
once_id: func();
},
key2 : ...
}
*/
constructor() {
this.eventObj = {};
this.callbackID = 0;
}
$on(name, callback) {
if (!this.eventObj[name])
this.eventObj[name] = {} //初始化
let id = this.callbackID++;
this.eventObj[name][id] = callback;
return id; // 利用id取消订阅
}
$emit(name, ...args) {
const callbacks = this.eventObj[name];
if (!callbacks) return; // 添加空值检查
Object.keys(callbacks).forEach(id => {
// 注意展开传参
// 执行前再次检查,避免在回调中被 $off
if (callbacks[id]) {
callbacks[id](...args);
if (id?.includes('once')) {
this.$off(name, id);
}
}
});
}
$off(name, id) {
delete this.eventObj[name][id];
console.log(`已取消${name},下对应${id}号监听事件`);
if (Object.keys(this.eventObj[name]).length === 0) {
delete this.eventObj[name];
}
}
$once(name, callback) {
if (!this.eventObj[name])
this.eventObj[name] = {} //初始化
let id = `once_${this.callbackID++}`;
this.eventObj[name][id] = callback;
return id; // 利用id取消订阅
}
}
// 初始化EventBus
let EB = new EventBus();
// 订阅事件
EB.$on('key1', (name, age) => {
console.info("我是订阅事件A:", name, age);
})
let id = EB.$on("key1", (name, age) => {
console.info("我是订阅事件B:", name, age);
})
EB.$on("key2", (name) => {
console.info("我是订阅事件C:", name);
})
// 发布事件key1
EB.$emit('key1', "第一次触发key1", 26);
// 取消订阅事件
EB.$off('key1', id);
// 发布事件key1
EB.$emit('key1', "第二次触发key1", 82);
// 发布事件
EB.$emit('key2', "第一次触发key2");
EB.$once('key1', (msg) => {
console.info("我是仅能触发一次的订阅事件D:", msg);
});
EB.$emit('key1', "第三次触发key1");
EB.$emit('key1', "第四次触发key1");十二、深拷贝
js
function deepClone(target, hash = new WeakMap()) {
// 1. 处理基本类型和函数(函数一般不需要深拷贝,直接返回引用)
if (target === null || typeof target !== 'object') {
return target;
}
// 2. 处理 Date 类型
if (target instanceof Date) {
return new Date(target);
}
// 3. 处理 RegExp 类型
if (target instanceof RegExp) {
return new RegExp(target);
}
// 4. 处理循环引用:如果已经存在于 hash 中,则直接返回已克隆的对象
if (hash.has(target)) {
return hash.get(target);
}
// 5. 根据类型创建拷贝的初始对象(数组或者普通对象)
const cloneTarget = Array.isArray(target) ? [] : {};
// 将当前对象和其克隆对象存入 hash 中,防止循环引用
hash.set(target, cloneTarget);
// 6. 处理对象所有属性,包括 Symbol 属性
Reflect.ownKeys(target).forEach(key => {
// 递归拷贝每个属性值
cloneTarget[key] = deepClone(target[key], hash);
});
return cloneTarget;
}
// 测试示例
const obj2 = {
name: 'Alice',
age: 30,
date: new Date(),
pattern: /abc/gi,
nested: {
arr: [1, 2, { a: 3 }],
func: function () { return 'Hello'; }
}
};
// 添加循环引用
obj2.self = obj2;
const cloned = deepClone(obj2);
console.log(cloned);十三、函数柯里化
js
function curry (fn) {
const originArgsLength = fn.length;
return function curried(...args) {
if (args.length >= originArgsLength) {
return fn.apply(this, args);
} else {
return function(...nextArgs) {
return curried.apply(this, [...args, ...nextArgs]);
}
}
}
}
function sum(a,b,c,d) {
return a+b+c+d;
}
const curriedSum = curry(sum);
console.log(curriedSum(1)(2,3)(4));