Skip to content

05 - JS 小技巧与手写实现

目录


一、实用小技巧

字符串快速转数字

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 + str2concat()
替换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)FetchAxios
定位技术概念/底层 API浏览器原生 API第三方库
基于回调函数PromisePromise(基于 XHR 或 Fetch)
错误处理手动判断状态码HTTP 错误不会 reject自动处理 HTTP 错误
拦截器
请求取消xhr.abort()AbortControllerCancelToken
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);
  }
);

如何选择

  1. 追求简洁与原生支持 → Fetch
  2. 需要完善功能与拦截器 → Axios(推荐大多数项目)
  3. 维护旧项目或兼容 IE → XMLHttpRequest + Axios polyfill

八、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));

<< 返回首页