记录开发中常用的js技巧,封装常用函数等等

功能类

rgb 和 hex 颜色相互转换

hex color由 6 位16进制字符组成#ffffffrgb color由 3 组0 - 255的数值组成rgb(255,255,255)。两者的对应关系为两个 hex 字符对应一组 rgb 数值,即ff, ff, ff对应255, 255, 255。取出一组为例ff => 255

hexToRgb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function hexToRgb(hexColor: string = ""): number[] | string {
const rgb: number[] = [];
const hexReg = /^#[0-9a-fA-F]{6}?$/;
const shortHexReg = /^#[0-9a-fA-F]{3}$/;
const isShort: boolean = shortHexReg.test(hexColor);

if (!hexReg.test(hexColor) && !isShort) {
return "请输入合法字符,例如#000000或#000";
}

hexColor = hexColor.slice(1);

if (isShort) {
let shortColor = "";

for (let i = 0; i < hexColor.length; i++) {
shortColor += hexColor[i].repeat(2);
}

hexColor = shortColor;
}

for (let i = 0; i < hexColor.length; i += 2) {
const value1 = parseInt(hexColor[i], 16) * 16;
const value2 = parseInt(hexColor[i + 1], 16);
rgb.push(value1 + value2);
}

return rgb;
}

rgbToHex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function rgbToHex(rgbColor: number[]): string {
let hexColor = "#";

if (rgbColor.length !== 3) {
return "参数错误,rgb(255, 255, 255)";
}

rgbColor.forEach((color) => {
if (color > 255 || color < 0)
return "参数错误,rgb值应大于等于0,小于等于255";

const str1 = Math.floor(color / 16);
const str2 = color % 16;
hexColor += str1 + str2;
});

return hexColor;
}

获取 url query 参数

自己实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
interface QueryProps {
[props: string]: string;
}

function formatQueryStrToObj(queryStr?: string): QueryProps {
const query: QueryProps = {};

// 优先处理参数
const params: string[] = (queryStr || window.location.search)
.replace(/^\?/, "")
.split("&");

params.forEach((item) => {
let key = item;
let value = "";

// 判断key-value中是否存在=号
if (/=/.test(item)) {
const pairs = item.split("=");
key = pairs[0];
value = pairs[1];
}

// key为空不保存
key !== "" && (query[decodeURIComponent(key)] = decodeURIComponent(value));
});

return query;
}

使用原生方法

1
2
3
4
5
6
7
8
9
10
function formatQueryStrToObj(): QueryProps {
const query: QueryProps = {};
const search = new URLSearchParams(window.location.search);

for (let [key, value] of search) {
query[key] = value;
}

return query;
}

整理 get 请求参数

自己实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface ParamsProps {
[props: string]: any;
}

function formatQueryParams(
params: ParamsProps,
withPrefix: boolean = true
): string {
let queryUrl = withPrefix ? "?" : "";

for (let key in params) {
queryUrl += `${key}=${params[key]}&`;
}

return queryUrl.slice(0, -1);
}

使用原生方法

1
2
3
4
5
6
7
interface ParamsProps {
[props: string]: any;
}

function formatQueryParams(params: ParamsProps): string {
return new URLSearchParams(params).toString();
}

是否为移动端设备

通过读取userAgent中的信息来判断当前是否为移动端设备

1
2
3
4
5
6
function isMobile(): boolean {
const reg = /Android/;
const ua = window.navigator.userAgent;

return reg.test(ua);
}

相对地址转绝对地址

利用a标签实现

1
2
3
4
5
function transformUrl(url: string): string {
const a = document.createElement("a");
a.setAttribute("href", url);
return a.href;
}

复制文本

原理

  1. 核心函数document.execCommand('copy', false)会将选区(window.getSelection中的内容复制到剪切板
  2. 现在要做的就是把想要复制的内容添加到选区中即可
  3. 如果内容在表单元素中,那么调用表单元素$el.select()方法即可将内容添加至选区
  4. 如果内容在普通元素中,需要创建一个range来获取该元素的内容,然后将range添加到选区中即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function copy($el: Element): void {
const selection = window.getSelection();

if ($el instanceof HTMLInputElement || $el instanceof HTMLTextAreaElement) {
$el.select();
} else {
const range = document.createRange();
const end = $el.childNodes.length;
range.setStart($el, 0);
range.setEnd($el, end);
if (selection) {
selection.removeAllRanges();
selection.addRange(range);
}
}

document.execCommand("copy", false);
selection?.removeAllRanges();
}

数字千位分隔符

西方人习惯将 10000 说成10 个 1000,如10,000

使用系统方法

1
2
3
function formatNum(num: number): string {
return num.toLocaleString("en-US");
}

自己实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function formatNum(num: number): string {
const str = num + "";
const result: string[] = [];
const length = str.length;

for (let i = length - 1; i >= 0; i--) {
result.push(str[i]);
// 最左侧不需要逗号
if ((length - i) % 3 === 0 && i !== 0) {
result.push(",");
}
}

return result.reverse().join("");
}

工具类

防抖

timeout时间内连续触发防抖函数并不会每次都执行,只会执行最后一次

1
2
3
4
5
6
7
8
9
10
11
function debounce(fn: () => {}, timeout: number): () => void {
let timer: number | undefined = undefined;

return function (...args: []) {
clearTimeout(timer);

timer = setTimeout(() => {
fn.call(null, ...args);
}, timeout);
};
}

节流

连续触发节流函数并不会每次都执行,而是每隔timeout时间执行一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function throttle(fn: () => {}, timeout: number): () => void {
let flag = true;

return function (...args: []) {
if (flag) {
flag = false;

setTimeout(() => {
flag = true;
fn.call(null, ...args);
}, timeout);
}
};
}

compose

compose函数接受多个函数做为参数,这些函数会从右至左一次执行,并且上一个函数的返回值会作为下一个函数的输入值

compose函数的优势在于可以像搭建积木一样任意搭配函数的组合,最终得到不同的结果

1
2
3
4
5
6
7
8
9
function compose(...fns: Array<() => {}>): () => {} {
return function (...params: any[]) {
return fns.reduceRight(function (state: any, fn, index) {
return index === fns.length - 1
? fn.apply(null, state)
: fn.call(null, state);
}, params);
};
}

pipe

pipe函数与compose函数类型,只是函数的执行方向是从左至右

1
2
3
4
5
6
7
function pipe(...fns: Array<() => {}>): () => {} {
return function (...params: any[]) {
return fns.reduce(function (state: any, fn, index) {
return index === 0 ? fn.apply(null, state) : fn.call(null, state);
}, params);
};
}

类型判断

使用Object.prototype.toString.call()可以获取到任意值的类型,返回值的格式为[Object Type]

1
2
3
4
5
6
7
function getType(value: any): string {
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}

function isType(value: any, expectType: string): boolean {
return getType(value) === expectType.toLowerCase();
}

手写类

参考

柯里化

作用

  1. sum(1,2,3)的执行格式改为sum(1)(2)(3)
  2. 可复用参数,const fn = sum(1)(2) => fn(3)或者fn(4),共用1和2这两个参数
1
2
3
4
5
6
7
8
9
10
11
12
13
function curry(fn: (...args: any[]) => any): (...args: any[]) => any {
const args: any[] = [];

function helper(...left: any[]): any {
args.push(...left);
// fn的形参个数和实参个数相等时执行fn函数,否则继续返回helper收集参数
if (args.length === fn.length) {
return fn(args);
}
return helper;
}
return helper;
}
1
2
3
4
5
6
7
8
9
10
function curry(fn: (...args: any[]) => any): (...args: any[]) => any {
function helper(...args: any[]): any {
if (args.length === fn.length) {
return fn(...args);
}
// 重点关注
return helper.bind(null, ...args);
}
return helper;
}

深拷贝

javascript中有两种数据类型,一种是基础数据类型,另一种是引用数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function deepClone(target) {
// 判断某个变量是不是引用类型
const isObject = (val) => typeof val === "object" && val !== null;

// 如果是基础类型,直接return
if (!isObject(target)) return target;

// 是引用类型,则进行拷贝
const copyTarget = Array.isArray(target) ? [] : {};
// 获取数组或者对象的key
const targetKeys = Object.keys(target);

// 遍历key,判断对应value的类型
targetKeys.forEach((key) => {
// 将值填充到备份中
copyTarget[key] = deepClone(target[key]);
});

return copyTarget;
}

字符串模板

思路

  1. 利用字符串的replace方法找出所有的${}
  2. 在利用取出${}括号内的值作为属性名参数对象中找对应的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface TemplateParams {
[prop: string]: number | string | boolean;
}

/**
* 实现模板字符串函数
* @param temp 模板字符串
* @param params 需要填充的参数
* 格式:'我叫${name}'
*/
function template(temp: string, params: TemplateParams): string {
return temp.replace(/\$\{([^\{\}\$]*)\}/g, function (subStr, match) {
return typeof params[match] !== "undefined"
? String(params[match])
: subStr;
});
}

JSON.stringify

1

JSON.parse

1

EventEmitter

1

setInterval

使用setTimeout实现setInterval

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface SetIntervalReturn {
clear: () => void;
}

function _setInterval(fn: () => any, timeout: number = 0): SetIntervalReturn {
let timer: number;

function interval() {
timer = setTimeout(() => {
fn();
interval();
}, timeout);
}

interval();

return {
clear() {
clearTimeout(timer);
},
};
}

reduce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function _reduce(
arr: any[],
cb: (preVal: any, currVal: any, index?: number, self?: any[]) => any,
initVal?: any
): any {
let start = 0;
let result: any = initVal;

// 初始值不存在,则将数组的第一个值当作初始值,并从第二项开始遍历
if (!initVal) {
result = arr[0];
if (!result) return;
start = 1;
}

for (let i = start; i < arr.length; i++) {
// 更新累加器的值
result = cb(result, arr[i], i, arr);
}

return result;
}

flat

flat扁平化数组的函数

原理

  1. concat函数可以实现数组的一层扁平化,[].concat([1,2,3]) => [1,2,3]
  2. 结构符可以实现深度为 1的数组扁平化,[].concat(...[1,2,3,[4,5]]) => [1,2,3,4,5]
  3. 遍历数组,判断当前项的类型,如果是数组类型,则递归执行当前函数,如果不是则直接返回
1
2
3
4
5
6
7
8
9
10
// 递归版
function flat(arr: any[], depth: number = 1): any[] {
return depth >= 1
? arr.reduce((result, curr) => {
return result.concat(
Array.isArray(curr) ? flat(curr, depth - 1) : curr
);
}, [])
: arr;
}
1
2
3
4
5
6
7
8
9
// 非递归版
function flat(arr: any[], depth: number = 1): any[] {
while (arr.some((item) => Array.isArray(item)) && depth > 0) {
arr = [].concat(...arr);
depth--;
}

return arr;
}

apply

原理

  1. 函数内部的this指向调用该函数的上下文(对象)
  2. 因此如果想将函数的this指向某个上下文,只需要让这个上下文执行此函数即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function _apply(
ctx: null | any,
fn: (...[]) => void | any,
params: any[] = []
): any {
// 上下文如果是null,直接执行fn函数 ,不需要window.fn
if (ctx === null) return fn(...params);

// 将不是object的值转成object
ctx = Object(ctx);
const fnName = Symbol();
// 将函数添加到ctx上
ctx[fnName] = fn;
// 调用ctx上的fn函数,这样fn内部的this就会指向ctx
const result: any = ctx[fnName](...params);
// 执行完后,将ctx上的fn删掉
delete ctx[fnName];

return result;
}

发现一个问题使用let声明的变量不会添加到window上,使用var声明的变量可以

call

call函数和apply函数的作用一样,只是给函数传参的方式不同。apply函数只能传递一个数组作为参数,call函数可以传递任意多个参数

1
2
3
4
5
6
7
8
9
10
11
function _call(ctx: any, fn: (...[]) => any, ...args: any[]): any {
if (!ctx) return fn(...args);

ctx = Object(ctx);
const fnName = Symbol();
ctx[fnName] = fn;
const result = ctx[fnName](...args);
delete ctx[fnName];

return result;
}

bind

bind方法会返回一个绑定好作用域的函数,除此之外和call函数一致

1
2
3
4
5
6
7
function _bind(
ctx: object | null,
fn: (...args: any[]) => any,
...args: any[]
): (...args: any[]) => any {
return (...left: any[]) => fn.call(ctx, ...args, ...left);
}

instanceof

原理

  1. 创建一个构造函数function Person() {}
  2. new一个构造函数const person = new Person()会创建一个实例
  3. 实例__proto__属性会指向构造函数Person.prototype属性
  4. 因此可以判断实例原型链上__proto__属性和构造函数prototype属性是否相等即可知道某个对象是否为某个构造函数的实例

自己实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function _instanceof(instance: any, constructor: any): boolean {
// 使用可选链操作符 '?.'
const prototype = constructor?.prototype;
let __proto__ = instance?.__proto__;

while (__proto__ && prototype) {
if (__proto__ === prototype) {
return true;
}

__proto__ = __proto__.__proto__;
}

return false;
}

使用原生方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function _instanceof(instance: any, constructor: any): boolean {
const prototype = constructor?.prototype;
// Object.getPrototypeOf(object),获取object的原型,不存在返回null,相比__proto__属性,兼容性更好
let __proto__ = Object.getPrototypeOf(instance);

while (__proto__ && prototype) {
if (__proto__ === prototype) {
return true;
}

__proto__ = Object.getPrototypeOf(__proto__);
}

return false;
}

new

1

Promise

Array

去重

indexOf

遍历数组,判断当前元素在数组中第一次出现的下标是否与当前下标一致

1
2
3
4
5
6
7
8
9
10
11
function unique(arr: any[]): any[] {
const result: any[] = [];

for (let i = 0; i < arr.length; i++) {
if (arr.indexOf(arr[i]) === i) {
result.push(arr[i]);
}
}

return result;
}

排序

对数组排序后,当前元素下一个元素不同,则将当前元素添加到新数组

1
2
3
4
5
6
7
8
9
10
11
12
function unique(arr: Array<number | string>): Array<number | string> {
const result: Array<string | number> = [];
arr.sort();

for (let i = 0; i < arr.length; i++) {
if (arr[i] !== arr[i + 1]) {
result.push(arr[i]);
}
}

return result;
}

Set

利用Sethash特性以及不能存在重复元素的特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function unique(arr: any[]): any[] {
const result: any[] = [];
const set = new Set();

arr.forEach((item) => {
if (!set.has(item)) {
set.add(item);
result.push(item);
}
});

return result;
}

function unique(arr: any[]): any[] {
return Array.from(new Set(arr));
}

排序

冒泡排序

1
2
3
4
5
6
7
8
9
10
11
12
13
function bubbleSort(arr: number[]): number[] {
const length = arr.length;

for (let i = 0; i < length - 1; i++) {
for (let j = 0; j < length - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}

return arr;
}

选择排序

1
2
3
4
5
6
7
8
9
10
11
12
13
function selectSort(arr: number[]): Array<number> {
const length = arr.length;

for (let i = 0; i < length - 1; i++) {
for (let j = i + 1; j < length; j++) {
if (arr[i] > arr[j]) {
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
}

return arr;
}

快排

数组中找一个基准值,遍历整个数组(除基准值外),比基准值小的放到基准值的左侧,否则放在右侧。再对左右两侧的值进行相同操作即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function quickSort(arr: number[]): number[] {
if (arr.length <= 1) return arr;

const left: number[] = [];
const right: number[] = [];
const index: number = Math.floor(arr.length / 2);
const middle: number = arr[index];

for (let i = 0; i < arr.length; i++) {
// 这个if很重要
if (i !== index) {
const item = arr[i];

if (item > middle) {
right.push(item);
} else {
left.push(item);
}
}
}

return quickSort(left).concat([middle]).concat(quickSort(right));
}

Object

is 空对象

1
2
3
function isEmptyObj(obj: object): boolean {
return JSON.stringfy(obj) === "{}";
}

String

repeat

padStart

padEnd

Number

是不是整数

一个数取整后还等于它本身就是整数

1
2
3
function isInteger(num: number): boolean {
return Math.floor(num) === num;
}

Date

今天是周几

日期对象toString()方法返回的内容中包含的信息

1
2
3
4
5
function getDate(): string {
const date = new Date();

return date.toString().slice(0, 3);
}

使用Date自带方法getDay

1
2
3
4
5
function getDate(): number {
const date = new Date();

return date.getDay();
}

日期字符串格式是否正确

正确的日期格式YYYY-MM-DDYYYY-M-D

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function isExactDateString(dateStr: string): boolean {
const dateReg = /^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$/;
// 一年12个月所对应的天数
const monthMap: number[] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

if (!dateReg.test(dateStr)) return false;

const datePair = dateStr.split("-");
const month: number = +datePair[1];
const date: number = +datePair[2];

// 判断月份是否正确
if (!(month >= 1 && month <= 12)) return false;

// 判断date是否正确
if (!(date >= 1 && date <= monthMap[month - 1])) return false;

return true;
}

两天是否在同一周

注意

  1. getDay方法的返回值范围为0 - 6,0 表示周天

思路

  1. 日期字符串转为日期毫秒值,经过计算可以得知两个日期相差多少天dayGap
  2. 在以其中一个日期为基准,获取该日期是周几dateDay
  3. dayGapdateDay之间的关系只要满足指定条件,即在同一周,具体过程看代码最后if else部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* @param date1
* @param date2
*/
function twoDayInSameWeek(date1: string, date2: string): boolean | string {
const dateReg = /[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}/;

if (!dateReg.test(date1) || !dateReg.test(date2)) {
return "日期格式错误:应为YYYY-MM-DD";
}

const oneDayTime = 24 * 60 * 60 * 1000;
// 根据日期创建Date实例
const date1Obj = new Date(date1);
const date2Obj = new Date(date2);
// 获取date1是周几
const date1Day: number = date1Obj.getDay() || 7;
// 获取日期的毫秒值
const date1Time = date1Obj.getTime();
const date2Time = date2Obj.getTime();
// 计算两天相隔几天
const dayGap: number = Math.abs(date1Time - date2Time) / oneDayTime;

if (date1Time <= date2Time) {
return date1Day + dayGap <= 7;
} else {
return date1Day - dayGap >= 0;
}
}

某一年的第几天

参数

  1. 传入YYYY-MM-DD格式的日期,判断该日期是YYYY年的第几天
  2. 不传参数则表示今天今年的第几天
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* @param date YYYY-MM-DD日期
* @returns
*/
function dayOfYear(date?: string): number | string {
const reg = /[0-9]{4}-[0-9]{2}-[0-9]{2}/;
const oneDayTime = 24 * 60 * 60 * 1000;

if (date && !reg.test(date)) return "日期格式错误:应为YYYY-MM-DD";

const calcDayOfYear = (date: string): number => {
const year = date.slice(0, 4);
const beginningTime = new Date(`${year}-1-1`).getTime();
const currentTime = new Date(date).getTime();

return Math.floor((currentTime - beginningTime) / oneDayTime) + 1;
};

if (!date) {
const currDate = new Date();
const year = currDate.getFullYear();
const month = String(currDate.getMonth() + 1).padStart(2, "0");
const day = String(currDate.getDate()).padStart(2, "0");

return calcDayOfYear(`${year}-${month}-${day}`);
}

return calcDayOfYear(date);
}

编码/解码

encodeURIComponent

escape