基础
判断数据类型
export function getType(x) {
const originType = Object.prototype.toString.call(x); // '[object String]'
const spaceIndex = originType.indexOf(' ');
const type = originType.slice(spaceIndex + 1, -1); // 'String'
return type.toLowerCase(); // 'string'
}
// 功能测试
console.info(getType(null)); // 'null'
console.info(getType(undefined));
console.info(getType(100));
console.info(getType('abc'));
console.info(getType(true));
console.info(getType(Symbol()));
console.info(getType({}));
console.info(getType([]));
console.info(getType(() => { }));
sleep机制链式调用
class LazyMan {
constructor(name) {
this.tasks = []; // 任务列表
this.name = name;
setTimeout(() => {
this.next();
});
}
next() {
const task = this.tasks.shift(); // 取出当前 tasks 的第一个任务
if (task)
task();
}
eat(food) {
const task = () => {
console.info(`${this.name} eat ${food}`);
this.next(); // 立刻执行下一个任务
};
this.tasks.push(task);
return this; // 链式调用
}
sleep(seconds) {
const task = () => {
console.info(`${this.name} 开始睡觉`);
setTimeout(() => {
console.info(`${this.name} 已经睡完了 ${seconds}s,开始执行下一个任务`);
this.next(); // xx 秒之后再执行下一个任务
}, seconds * 1000);
};
this.tasks.push(task);
return this; // 链式调用
}
}
const me = new LazyMan('双越');
me.eat('苹果').eat('香蕉').sleep(2).eat('葡萄').eat('西瓜').sleep(2).eat('橘子');
对象
深度比较lodash.isEaqual
判断类型区分基本数据和对象类型;再对对象类型执行操作,先判断长度相等再递归。
// 判断是否是对象或数组
function isObject(obj) {
return typeof obj === 'object' && obj !== null
}
// 全相等(深度)
function isEqual(obj1, obj2) {
if (!isObject(obj1) || !isObject(obj2)) {
// 值类型(注意,参与 equal 的一般不会是函数)
return obj1 === obj2
}
if (obj1 === obj2) {
return true
}
// 两个都是对象或数组,而且不相等
// 1. 先取出 obj1 和 obj2 的 keys ,比较个数
const obj1Keys = Object.keys(obj1)
const obj2Keys = Object.keys(obj2)
if (obj1Keys.length !== obj2Keys.length) {
return false
}
// 2. 以 obj1 为基准,和 obj2 一次递归比较
for (let key in obj1) {
// 比较当前 key 的 val —— 递归!!!
const res = isEqual(obj1[key], obj2[key])
if (!res) {//有不想等的就不再继续比较了
return false
}
}
// 3. 全相等
return true
}
// 测试
const obj1 = {
a: 100,
b: {
x: 100,
y: 200
}
}
const obj2 = {
a: 100,
b: {
x: 100,
y: 200
}
}
// console.log( obj1 === obj2 )
console.log( isEqual(obj1, obj2) )
const arr1 = [1, 2, 3]
const arr2 = [1, 2, 3, 4]
js深拷贝
/**
* 深拷贝
*/
const obj1 = {
age: 20,
name: 'xxx',
address: {
city: 'beijing'
},
arr: ['a', 'b', 'c']
}
console.log(typeof null)
const obj2 = deepClone(obj1)
obj2.address.city = 'shanghai'
obj2.arr[0] = 'a1'
console.log(obj1.address.city)
console.log(obj1.arr[0])
/**
* 深拷贝
* @param {Object} obj 要拷贝的对象
*/
function deepClone(obj = {}) {
if (typeof obj !== 'object' || obj == null) {//obj == null等价于obj===null||obj===undefined
// obj 是 null ,或者不是对象和数组,直接返回
return obj
}
// 初始化返回结果
let result
if (obj instanceof Array) {
result = []
} else {
result = {}
}
for (let key in obj) {
// 保证 key 不是原型的属性
if (obj.hasOwnProperty(key)) {
// 递归调用
result[key] = deepClone(obj[key])
}
}
// 返回结果
return result
}
注意:Object.assign只有第一层数据是深拷贝
手写instanceof
export function myInstanceof(instance, origin) {
if (instance == null)
return false; // null undefined
const type = typeof instance;
if (type !== 'object' && type !== 'function') {
// 值类型
return false;
}
let tempInstance = instance; // 为了防止修改 instance
while (tempInstance) {
if (tempInstance.__proto__ === origin.prototype) {
return true; // 配上了
}
// 未匹配
tempInstance = tempInstance.__proto__; // 顺着原型链,往上找
}
return false;
}
// 功能测试
console.info(myInstanceof({}, Object));
console.info(myInstanceof([], Object));
console.info(myInstanceof([], Array));
console.info(myInstanceof({}, Array));
console.info(myInstanceof('abc', String));
函数
自己实现new
export function customNew(constructor, ...args) {
// 1. 创建一个空对象,继承 constructor 的原型
const obj = Object.create(constructor.prototype);
// 2. 将 obj 作为 this ,执行 constructor ,传入参数
constructor.apply(obj, args);
// 3. 返回 obj
return obj;
}
class Foo {
constructor(name, n) {
this.name = name;
this.city = '北京';
this.n = n;
}
getName() {
return this.name;
}
}
const f = new Foo('双越', 100);
// const f = customNew<Foo>(Foo, '双越', 100)
console.info(f);
console.info(f.getName());
bind
Function.prototype.customBind = function (context, ...bindArgs) {
// context 是 bind 传入的 this
// bindArgs 是 bind 传入的各个参数
const self = this; // 当前的函数本身
return function (...args) {
// 拼接参数
const newArgs = bindArgs.concat(args);
return self.apply(context, newArgs);
};
};
// 功能测试
function fn(a, b, c) {
console.info(this, a, b, c);
}
// @ts-ignore
const fn1 = fn.customBind({ x: 100 }, 10);
fn1(20, 30);
call/apply
Function.prototype.customCall = function (context, ...args) {
if (context == null)
context = globalThis;
if (typeof context !== 'object')
context = new Object(context); // 值类型,变为对象
const fnKey = Symbol(); // 不会出现属性名称的覆盖
context[fnKey] = this; // this 就是当前的函数
const res = context[fnKey](...args); // 绑定了 this
delete context[fnKey]; // 清理掉 fn ,防止污染
return res;
};
// @ts-ignore
Function.prototype.customApply = function (context, args = []) {
if (context == null)
context = globalThis;
if (typeof context !== 'object')
context = new Object(context); // 值类型,变为对象
const fnKey = Symbol(); // 不会出现属性名称的覆盖
context[fnKey] = this; // this 就是当前的函数
const res = context[fnKey](...args); // 绑定了 this
delete context[fnKey]; // 清理掉 fn ,防止污染
return res;
};
function fn(a, b, c) {
console.info(this, a, b, c);
}
// @ts-ignore
fn.customCall({ x: 100 }, 10, 20, 30);
-ignore;
fn.customApply({ x: 200 }, [100, 200, 300]);
数组
获取数组中的最大值
数组flatern,考虑多层级
function flat(arr) {
// 验证 arr 中,还有没有深层数组 [1, 2, [3, 4]]
const isDeep = arr.some(item => item instanceof Array)
if (!isDeep) {
return arr // 已经是 flatern [1, 2, 3, 4]
}
const res = Array.prototype.concat.apply([], arr)
return flat(res) // 递归
}
const res = flat( [1, 2, [3, 4, [10, 20, [100, 200]]], 5] )
console.log(res)
数组去重
// 传统方式
function unique(arr) {
const res = []
arr.forEach(item => {
if (res.indexOf(item) < 0) {
res.push(item)//没有就push
}
})
return res
}
// 使用 Set (无序,不能重复)
function unique(arr) {
const set = new Set(arr)
return [...set]
}
const res = unique([30, 10, 20, 30, 40, 10])
console.log(res)
函数柯里化
export function curry(fn) {
const fnArgsLength = fn.length; // 传入函数的参数长度
let args = [];
// ts 中,独立的函数,this 需要声明类型
function calc(...newArgs) {
// 积累参数
args = [
...args,
...newArgs
];
if (args.length < fnArgsLength) {
// 参数不够,返回函数
return calc;
}
else {
// 参数够了,返回执行结果
return fn.apply(this, args.slice(0, fnArgsLength));
}
}
return calc;
}
function add(a, b, c) {
return a + b + c;
}
// add(10, 20, 30) // 60
const curryAdd = curry(add);
const res = curryAdd(10)(20)(30); // 60
console.info(res);
BOM
获取当前页面url参数
// 传统方式:location.search
function query(name) {
const search = location.search.substr(1) // 类似 array.slice(1)
// search: 'a=10&b=20&c=30'
const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i')
const res = search.match(reg)
if (res === null) {
return null
}
return res[2]
}
query('d')
//新api: URLSearchParams
function query(name) {
const search = location.search
const p = new URLSearchParams(search)
return p.get(name)
}
console.log( query('b') )
正则解析:
开始或& :abc前面 值不要有& &或没有结尾
整个--前括号--值--后括号
DOM
EventBus自定义事件
export default class EventBus {
/**
* {
* 'key1': [
* { fn: fn1, isOnce: false },
* { fn: fn2, isOnce: false },
* { fn: fn3, isOnce: true },
* ]
* 'key2': [] // 有序
* 'key3': []
* }
*/
constructor() {
this.events = {};
}
on(type, fn, isOnce = false) {//存储函数
const events = this.events;
if (events[type] == null) {
events[type] = []; // 初始化 key 的 fn 数组
}
events[type].push({ fn, isOnce });
}
once(type, fn) {//存储函数
this.on(type, fn, true);
}
off(type, fn) {//删除函数
if (!fn) {
// 解绑所有 type 的函数
this.events[type] = [];
}
else {
// 解绑单个 fn
const fnList = this.events[type];
if (fnList) {
this.events[type] = fnList.filter(item => item.fn !== fn);
}
}
}
emit(type, ...args) {//执行函数
const fnList = this.events[type];
if (fnList == null)
return;
// 注意
this.events[type] = fnList.filter(item => {
const { fn, isOnce } = item;
fn(...args);
// once 执行一次就要被过滤掉
if (!isOnce)
return true;
return false;
});
}
}
//功能测试
const e = new EventBus();
function fn1(a, b) { console.log('fn1', a, b); }
function fn2(a, b) { console.log('fn2', a, b); }
function fn3(a, b) { console.log('fn3', a, b); }
//绑定事件
e.on('key1', fn1);
e.on('key1', fn2);
e.once('key1', fn3);
e.on('xxxxxx', fn3);
//触发事件
e.emit('key1', 10, 20); // 触发 fn1 fn2 fn3
//解绑事件
e.off('key1', fn1);
e.emit('key1', 100, 200); // 触发 fn2
//单元测试
import EventBus from './event-bus-split-on-once'
describe('EventBus 自定义事件', () => {
it('解绑单个事件', () => {
const event = new EventBus()
const fn1 = jest.fn()
const fn2 = jest.fn()
event.on('key1', fn1)
event.on('key1', fn2)
event.off('key1', fn1)
event.emit('key1', 10, 20)
expect(fn1).not.toBeCalled()
expect(fn2).toBeCalledWith(10, 20)
})
it('once', () => {
const event = new EventBus()
let n = 1
const fn1 = jest.fn(() => n++)
const fn2 = jest.fn(() => n++)
event.once('key1', fn1)
event.once('key1', fn2)
// 无论 emit 多少次,只有一次生效
event.emit('key1')
event.emit('key1')
event.emit('key1')
event.emit('key1')
event.emit('key1')
expect(n).toBe(3)
})
})
遍历DOM树
function visitNode(n) {
var _a;
if (n instanceof Comment) {
// 注释
console.info('Comment node ---', n.textContent);
}
if (n instanceof Text) {
// 文本
const t = (_a = n.textContent) === null || _a === void 0 ? void 0 : _a.trim();
if (t) {
console.info('Text node ---', t);
}
}
if (n instanceof HTMLElement) {
// element
console.info('Element node ---', `<${n.tagName.toLowerCase()}>`);
}
}
/**
* 深度优先遍历
* @param root dom node
*/
function depthFirstTraverse1(root) {
visitNode(root);
const childNodes = root.childNodes; // .childNodes 和 .children 不一样
if (childNodes.length) {
childNodes.forEach(child => {
depthFirstTraverse1(child); // 递归
});
}
}
/**
* 深度优先遍历
* @param root dom node
*/
function depthFirstTraverse2(root) {
const stack = [];
// 根节点压栈
stack.push(root);
while (stack.length > 0) {
const curNode = stack.pop(); // 出栈
if (curNode == null)
break;
visitNode(curNode);
// 子节点压栈
const childNodes = curNode.childNodes;
if (childNodes.length > 0) {
// reverse 反顺序压栈
Array.from(childNodes).reverse().forEach(child => stack.push(child));
}
}
}
/**
* 广度优先遍历
* @param root dom node
*/
function breadthFirstTraverse(root) {
const queue = []; // 数组 vs 链表
// 根节点入队列
queue.unshift(root);
while (queue.length > 0) {
const curNode = queue.pop();
if (curNode == null)
break;
visitNode(curNode);
// 子节点入队
const childNodes = curNode.childNodes;
if (childNodes.length) {
childNodes.forEach(child => queue.unshift(child));
}
}
}
const box = document.getElementById('box');
if (box == null)
throw new Error('box is null');
depthFirstTraverse2(box);
Map
哈希表存储数据,时间复杂度是O(1);而且必须是有序的。
export default class LRUCache {//缓存最近使用的,删除沉水数据
constructor(length) {
this.data = new Map();//数据容器
if (length < 1)
throw new Error('invalid length');
this.length = length;//内存长度
}
set(key, value) {
const data = this.data;
if (data.has(key)) {
data.delete(key);//删除
}
data.set(key, value);//重建set
if (data.size > this.length) {
// 如果超出了容量,则删除 Map 最老的元素
const delKey = data.keys().next().value;//iterator第一个元素
data.delete(delKey);
}
}
get(key) {
const data = this.data;
if (!data.has(key))
return null;
const value = data.get(key);
data.delete(key);//删除
data.set(key, value);//重建
return value;//返回val
}
}
const lruCache = new LRUCache(2);
lruCache.set(1, 1); // {1=1}
lruCache.set(2, 2); // {1=1, 2=2}
console.info(lruCache.get(1)); // 1 {2=2, 1=1}此时1=1的优先级更高
lruCache.set(3, 3); // {1=1, 3=3}此时2=2被删除
console.info(lruCache.get(2)); // null
lruCache.set(4, 4); // {3=3, 4=4}
console.info(lruCache.get(1)); // null
console.info(lruCache.get(3)); // 3 {4=4, 3=3}
console.info(lruCache.get(4)); // 4 {3=3, 4=4}
双向链表实现
删除A:删除A->B/ B->A/ head->B
新增D:新增C->D D->C tail->D
B移动到D后面:B与AC断开/AC连接 DB连接/tail->B
export default class LRUCache {
constructor(length) {
this.data = {};
this.dataLength = 0;
this.listHead = null;
this.listTail = null;
if (length < 1)
throw new Error('invalid length');
this.length = length;
}
moveToTail(curNode) {
const tail = this.listTail;
if (tail === curNode)
return;
// -------------- 1. 让 prevNode nextNode 断绝与 curNode 的关系 --------------
const prevNode = curNode.prev;
const nextNode = curNode.next;
if (prevNode) {
if (nextNode) {
prevNode.next = nextNode;
}
else {
delete prevNode.next;
}
}
if (nextNode) {
if (prevNode) {
nextNode.prev = prevNode;
}
else {
delete nextNode.prev;
}
if (this.listHead === curNode)
this.listHead = nextNode;
}
// -------------- 2. 让 curNode 断绝与 prevNode nextNode 的关系 --------------
delete curNode.prev;
delete curNode.next;
// -------------- 3. 在 list 末尾重新建立 curNode 的新关系 --------------
if (tail) {
tail.next = curNode;
curNode.prev = tail;
}
this.listTail = curNode;
}
tryClean() {
while (this.dataLength > this.length) {
const head = this.listHead;
if (head == null)
throw new Error('head is null');
const headNext = head.next;
if (headNext == null)
throw new Error('headNext is null');
// 1. 断绝 head 和 next 的关系
delete headNext.prev;
delete head.next;
// 2. 重新赋值 listHead
this.listHead = headNext;
// 3. 清理 data ,重新计数
delete this.data[head.key];
this.dataLength = this.dataLength - 1;
}
}
get(key) {
const data = this.data;
const curNode = data[key];
if (curNode == null)
return null;
if (this.listTail === curNode) {
// 本身在末尾(最新鲜的位置),直接返回 value
return curNode.value;
}
// curNode 移动到末尾
this.moveToTail(curNode);
return curNode.value;
}
set(key, value) {
const data = this.data;
const curNode = data[key];
if (curNode == null) {
// 新增数据
const newNode = { key, value };
// 移动到末尾
this.moveToTail(newNode);
data[key] = newNode;
this.dataLength++;
if (this.dataLength === 1)
this.listHead = newNode;
}
else {
// 修改现有数据
curNode.value = value;
// 移动到末尾
this.moveToTail(curNode);
}
// 尝试清理长度
this.tryClean();
}
}
requestAnimationFrame
// 3s 把宽度从 100px 变为 640px ,即增加 540px
// 60帧/s ,3s 180 帧 ,每次变化 3px
const $div1 = $('#div1')
let curWidth = 100
const maxWidth = 640
// setTimeout
function animate() {
curWidth = curWidth + 3
$div1.css('width', curWidth)
if (curWidth < maxWidth) {
setTimeout(animate, 16.7) // 自己控制时间
}
}
animate()
// RAF
function animate() {
curWidth = curWidth + 5
$div1.css('width', curWidth)
if (curWidth < maxWidth) {
window.requestAnimationFrame(animate) // 时间不用自己控制
}
}
animate()