算法
字符串反转
function reverseString(str) {
return str.split("").reverse().join("");
}
数组去重
function uniqueArray(arr) {
return Array.from(new Set(arr));
}
防抖函数(Debounce)
所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
function debounce(fn, delay) {
let timer;
return function() {
const context = this;
const args = arguments;
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
}, delay);
};
}
节流函数(Throttle)
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。
连续点击我,每一秒只会执行一次点击事件
function throttle(fn, delay) {
let timer;
let previous = 0;
return function() {
const context = this;
const args = arguments;
const now = new Date();
if (now - previous > delay) {
previous = now;
fn.apply(context, args);
} else {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
}, delay);
}
};
}
LRU 缓存算法(Least Recently Used)
LRU(Least Recently Used)缓存算法是一种常见的缓存淘汰策略,用于管理计算机系统内的缓存。简单来说,LRU算法会根据数据最近被访问的时间从缓存中清除最久未使用的数据,以腾出空间供新的数据使用
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.map = new Map();
}
get(key) {
const val = this.map.get(key);
if (typeof val === "undefined") return -1;
this.map.delete(key);
this.map.set(key, val);
return val;
}
put(key, value) {
if (this.map.has(key)) {
this.map.delete(key);
}
this.map.set(key, value);
if (this.map.size > this.capacity) {
const oldestKey = this.map.keys().next().value;
this.map.delete(oldestKey);
}
}
}
快速排序
function quickSort(arr) {
if (arr.length <= 1) return arr;
const left = [];
const right = [];
const pivotIndex = Math.floor(arr.length / 2);
const pivot = arr[pivotIndex];
for (let i = 0; i < arr.length; i++) {
if (i === pivotIndex) continue;
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return [...quickSort(left), pivot, ...quickSort(right)];
}
归并排序
归并排序是一种分治算法,它的核心思想是将待排序数组分成两个子数组,对每个子数组进行递归排序,然后将两个子数组合并成一个有序数组。它的时间复杂度是O(nlogn)。
具体实现步骤如下:
-
将待排序数组分成两个子数组,分别是左子数组和右子数组。
-
对左子数组和右子数组分别进行递归排序,直到子数组长度为1为止。
-
将左子数组和右子数组合并为一个有序数组。合并时,从左右两个子数组中取出一个元素进行比较,将较小的元素放入新数组中,直到其中一个子数组为空,然后将另一个子数组的剩余元素依次放入新数组中。
-
返回有序数组。
下面是JavaScript实现代码:
function mergeSort(arr) {
if (arr.length <= 1) {
return arr;
}
const mid = Math.floor(arr.length / 2);
const leftArr = arr.slice(0, mid);
const rightArr = arr.slice(mid);
const leftSorted = mergeSort(leftArr);
const rightSorted = mergeSort(rightArr);
return merge(leftSorted, rightSorted);
}
function merge(leftArr, rightArr) {
const merged = [];
let leftIndex = 0;
let rightIndex = 0;
while (leftIndex < leftArr.length && rightIndex < rightArr.length) {
if (leftArr[leftIndex] <= rightArr[rightIndex]) {
merged.push(leftArr[leftIndex]);
leftIndex++;
} else {
merged.push(rightArr[rightIndex]);
rightIndex++;
}
}
return merged.concat(leftArr.slice(leftIndex), rightArr.slice(rightIndex));
}
堆排序
function heapSort(arr) {
function buildHeap() {
for (let i = Math.floor(arr.length / 2); i >= 0; i--) {
heapify(i, arr.length);
}
}
function heapify(start, end) {
let leftChild = start * 2 + 1;
let rightChild = start * 2 + 2;
let largestIndex = start;
if (leftChild < end && arr[leftChild] > arr[largestIndex]) {
largestIndex = leftChild;
}
if (rightChild < end && arr[rightChild] > arr[largestIndex]) {
largestIndex = rightChild;
}
if (largestIndex !== start) {
[arr[start], arr[largestIndex]] = [arr[largestIndex], arr[start]];
heapify(largestIndex, end);
}
}
buildHeap();
for (let i = arr.length - 1; i > 0; i--) {
[arr[0], arr[i]] = [arr[i], arr[0]];
heapify(0, i);
}
return arr;
}
二分查找
二分查找(Binary Search)是一种常见的查找算法,也称折半查找。二分查找针对的是一个有序的数组,算法每次都从数组的中间位置开始查找,如果中间位置的元素与要查找的元素相等,则查找成功;如果中间位置的元素比要查找的元素大,则在左半部分继续查找;如果中间位置的元素比要查找的元素小,则在右半部分继续查找。每次查找都缩小一半的查找范围,因此时间复杂度为O(logn)。但是,二分查找只适用于静态有序的数据,一旦数据发生变化,就需要重新构建有序的数组。
function binarySearch(arr, val) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] > val) {
right = mid - 1;
} else if (arr[mid] < val) {
left = mid + 1;
} else {
return mid;
}
}
return -1;
}
斐波那契数列
可以使用循环迭代和递归两种方式实现斐波那契数列。
- 循环迭代方法:
function fibonacci(n) {
if(n == 0 || n == 1) {
return n;
}
let a = 0, b = 1, c;
for(let i = 2; i <= n; i++) {
c = a + b;
a = b;
b = c;
}
return b;
}
或
function fibonacci(n) {
if (n <= 1) return n;
let pre = 0;
let next = 1;
for (let i = 2; i <= n; i++) {
[pre, next] = [next, pre + next];
}
return next;
}
这里使用a和b来分别存储前两个斐波那契数列的值,然后用循环来计算斐波那契数列的元素,最后返回第n个元素的值。
- 递归方法:
function fibonacci(n) {
if(n == 0 || n == 1) {
return n;
}
return fibonacci(n-1) + fibonacci(n-2);
}
这里使用递归的方式来实现斐波那契数列,当n为0或1时,直接返回n,否则递归计算前两个斐波那契数列的元素的和。需要注意的是,递归方法的时间复杂度为O(2^n),因此在计算大量数据时可能会极其缓慢或出现堆栈溢出的问题。
二叉树
二叉树是一种数据结构,它由节点组成,每个节点最多有两个子节点。在二叉树中,我们通常称左侧节点为“左子树”,右侧节点为“右子树”。二叉树的实现方式比较灵活,可以用数组或者链表来表示。
以下是基于链表的二叉树代码示例:
class Node{
constructor(val){
this.val = val; // 节点值
this.leftChild = null; // 左子节点
this.rightChild = null; // 右子节点
}
}
class BinaryTree{
constructor(){
this.root = null;
}
insertNode(node, newNode){
if (newNode.val < node.val){
if (node.leftChild === null){
node.leftChild = newNode;
} else {
this.insertNode(node.leftChild, newNode);
}
} else {
if (node.rightChild === null){
node.rightChild = newNode;
} else {
this.insertNode(node.rightChild, newNode);
}
}
}
insert(val){
let newNode = new Node(val);
if (this.root === null){
this.root = newNode;
} else {
this.insertNode(this.root, newNode);
}
}
}
链表
链表也是一种数据结构,它由节点组成,每个节点包含一个数据元素和一个指向下一个节点的指针。链表分为单向链表和双向链表两种,单向链表只有一个方向,即从前往后,而双向链表则可以从后往前遍历。
class Node{
constructor(val){
this.val = val; // 节点值
this.next = null; // 下一个节点指针
}
}
class LinkedList{
constructor(){
this.head = null;
this.tail = null;
this.length = 0;
}
append(val){ // 添加节点
let newNode = new Node(val);
if (this.head === null){
this.head = newNode;
this.tail = newNode;
} else {
this.tail.next = newNode;
this.tail = newNode;
}
this.length++;
}
delete(val){ // 删除节点
let currentNode = this.head;
let previousNode = null;
if (currentNode.val === val){
this.head = currentNode.next;
} else {
while (currentNode.val !== val){
previousNode = currentNode;
currentNode = currentNode.next;
}
previousNode.next = currentNode.next;
}
this.length--;
}
}
手写JS
实现柯里化
柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
function curry(fn, args) {
let length = fn.length;
let args = args || [];
return function(){
newArgs = args.concat(Array.prototype.slice.call(arguments));
if (newArgs.length < length) {
return curry.call(this,fn,newArgs);
}else{
return fn.apply(this,newArgs);
}
}
}
function multiFn(a, b, c) {
return a * b * c;
}
let multi = curry(multiFn);
multi(1)(2)(3);
multi(1,2,3);
multi(1)(2,3);
手写Promise实现
解决回调地域
- 三种状态pending| fulfilled(resolved) | rejected
- 当处于pending状态的时候,可以转移到fulfilled(resolved)或者rejected状态
- 当处于fulfilled(resolved)状态或者rejected状态的时候,就不可变
class MyPromise {
constructor(executor) {
this.initValue()
this.initBind()
executor(this.resolve, this.reject)
}
initBind() {
this.resolve = this.resolve.bind(this)
this.reject = this.reject.bind(this)
}
initValue() {
this.PromiseState = 'pending'
this.PromiseResult = null
this.onFulfilledCallbacks = [] // 保存成功回调
this.onRejectedCallbacks = [] // 保存失败回调
}
resolve(value) {
if (this.PromiseState !== 'pending') return
// 如果执行resolve,状态变为fulfilled
this.PromiseState = 'fulfilled'
// 终值为传进来的值
this.PromiseResult = value
// 执行保存的成功回调
while (this.onFulfilledCallbacks.length) {
this.onFulfilledCallbacks.shift()(this.PromiseResult)
}
}
reject(reason) {
if (this.PromiseState !== 'pending') return
// 如果执行reject,状态变为rejected
this.PromiseState = 'rejected'
// 终值为传进来的reason
this.PromiseResult = reason
// 执行保存的失败回调
while (this.onRejectedCallbacks.length) {
this.onRejectedCallbacks.shift()(this.PromiseResult)
}
}
then(onFulfilled, onRejected) {
// 接收两个回调 onFulfilled, onRejected
// 参数校验,确保一定是函数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
if (this.PromiseState === 'fulfilled') {
// 如果当前为成功状态,执行第一个回调
onFulfilled(this.PromiseResult)
} else if (this.PromiseState === 'rejected') {
// 如果当前为失败状态,执行第二哥回调
onRejected(this.PromiseResult)
} else if (this.PromiseState === 'pending') {
// 如果状态为待定状态,暂时保存两个回调
this.onFulfilledCallbacks.push(onFulfilled.bind(this))
this.onRejectedCallbacks.push(onRejected.bind(this))
}
}
}
const test1 = new MyPromise((resolve, reject) => { resolve('success') reject('fail') })
console.log(test1) // MyPromise { PromiseState: 'fulfilled', PromiseResult: 'success' }
const test2 = new MyPromise((resolve, reject) => {
setTimeout(
() => {
resolve('success') }, 1000)
}).then(res => console.log(res), err => console.log(err))
function myPromise(constructor) {
let self = this;
self.status = "pending";
self.value = undefined;
self.reason = undefined;
function resolve(value) {
if (self.status === "pending") {
self.value = value;
self.status = "resolved";
}
}
function reject(value) {
if (self.status === "pending") {
self.reason = reason;
self.status = "rejected";
}
}
try {
constructor(resolve, reject);
} catch (e) {
reject(e);
}
}
myPromise.prototype.then = function (onFullfilled, onRejected) {
let self = this;
switch (self.status) {
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
};
实现一个继承 - 寄生组合式继承
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function() {
console.log('parent name:', this.name);
}
function Child(name, parentName) {
Parent.call(this, parentName);
this.name = name;
}
function create(proto) {
function F(){}
F.prototype = proto;
return new F();
}
Child.prototype = create(Parent.prototype);
Child.prototype.sayName = function() {
console.log('child name:', this.name);
}
Child.prototype.constructor = Child;
let parent = new Parent('father');
parent.sayName();
let child = new Child('son', 'father');
实现bind()方法
bind()会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
Function.prototype.bind2 = function(content) {
if(typeof this != "function") {
throw Error("not a function")
}
let fn = this;
let args = [...arguments].slice(1);
let resFn = function() {
return fn.apply(this instanceof resFn ? this : content,args.concat(...arguments) )
}
function tmp() {}
tmp.prototype = this.prototype;
resFn.prototype = new tmp();
return resFn;
}
实现一个call或apply方法
call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。
Function.prototype.myCall = function (context) {
let context = context || window;
context.fn = this;
let args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
let result = eval('context.fn(' + args +')');
delete context.fn
return result;
}
// 测试一下
let value = 2;
let obj = {
value: 1
}
function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}
bar.call2(null); // 2
console.log(bar.call2(obj, 'wawa', 27));
// 1
// Object {
// value: 1,
// name: 'wawa',
// age: 27
// }
apply 的实现跟 call 类似
Function.prototype.myApply = function (context, arr) {
let context = Object(context) || window;
context.fn = this;
let result;
if (!arr) {
result = context.fn();
}
else {
let args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}
delete context.fn
return result;
}
实现防抖
当一次事件发生后,事件处理器要等一定阈值的时间,如果这段时间过去后 再也没有 事件发生,就处理最后一次发生的事件。假设还差 0.01 秒就到达指定时间,这时又来了一个事件,那么之前的等待作废,需要重新再等待指定时间。
// 防抖动函数
function debounce(fn,wait=50,immediate) {
let timer;
return function() {
if(immediate) {
fn.apply(this,arguments)
}
if(timer) clearTimeout(timer)
timer = setTimeout(()=> {
fn.apply(this,arguments)
},wait)
}
}
// 结合实例
function go(){
console.log("Success");
}
// 采用了防抖动
window.addEventListener('scroll',debounce(go,500));
// 没采用防抖动
window.addEventListener('scroll',go);
实现节流
可以理解为事件在一个管道中传输,加上这个节流阀以后,事件的流速就会减慢。实际上这个函数的作用就是如此,它可以将一个函数的调用频率限制在一定阈值内,例如 1s,那么 1s 内这个函数一定不会被调用两次
function throttle(fn, wait) {
let prev = new Date();
return function() {
const args = arguments;
const now = new Date();
if (now - prev > wait) {
fn.apply(this, args);
prev = new Date();
}
}
手写一个JS深拷贝
(1)简单直接版
let newObj = JSON.parse( JSON.stringify( obj ) );
(2)常规版
function deepCopy(obj){
//判断是否是简单数据类型,
if(typeof obj == "object"){
//复杂数据类型
let result = obj.constructor == Array ? [] : {};
for(let i in obj){
result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
}
}else {
//简单数据类型 直接 == 赋值
let result = obj;
}
return result;
}
实现一个new操作符
1.先创建一个空对象用来存放实例
2.将构造函数的this指向空对象并执行函数体
3.将对象的__proto__属性指向构造函数的原型
4.返回新对象,如果构造函数返回值为引用类型,就返回这个引用类型,没有返回值或者返回值为基本类型就返回你的实例对象。
let obj = {}
Let result = Person.call(obj)
obj.__proto__ = Person.prototype
If (typeof(result) === ‘object’) {
return result
} else {
return p
}
简单实现
function myNew(fn, ...args) {
const obj = {}
obj.__proto__ = fn.prototype
fn.apply(obj, args)
return obj
}
let obj = myNew(A, 1, 2);
// equals to
let obj = new A(1, 2);
实现数组去重
(1)递归方式实现
let kkb = ["个人介绍", "工作记录", ["css", "html", [1, 2, 3]]];
let newArr = [];
function flat(arr) {
for (let i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
console.log("是数组");
flat(arr[i]);
} else {
console.log("不是数组");
newArr.push(arr[i]);
}
}
}
flat(kkb);
console.log("最后输出", newArr);
(2)Set去重
function flat(arr) {
return [...new Set(arr)]
}
使用setTimeout实现setInterval
function mySetTimout(fn, delay) {
let timer = null
const interval = () => {
fn()
timer = setTimeout(interval, delay)
}
setTimeout(interval, delay)
return {
cancel: () => {
clearTimeout(timer)
}
}
}
// 测试
const { cancel } = mySetTimout(() => console.log(888), 1000)
setTimeout(() => {
cancel()
}, 4000)
实现函数结果累加
function fn1(x) {
return x + 1;
}
function fn2(x) {
return x + 2;
}
function fn3(x) {
return x + 3;
}
function fn4(x) {
return x + 4;
}
const a = compose(fn1, fn2, fn3, fn4);
实现方式
function compose(...fn) {
if (fn.length === 0) return (num) => num
if (fn.length === 1) return fn[0]
return fn.reduce((pre, next) => {
return (num) => {
return next(pre(num))
}
})
}
实现发布订阅者模式
实现on ,emit , once, off 方法,类似eventBus实现
class EventEmitter {
constructor() {
this.cache = {}
}
on(name, fn) {
const tasks = this.cache[name]
if (tasks) {
this.cache[name].push(fn)
} else {
this.cache[name] = [fn]
}
}
off(name, fn) {
const tasks = this.cache[name]
if (task) {
const index = tasks.findIndex(item => item === fn)
if (index >= 0) {
this.cache[name].splice(index, 1)
}
}
}
emit(name, ...args) {
// 复制一份。防止回调里继续on,导致死循环
const tasks = this.cache[name].slice()
if (tasks) {
for (let fn of tasks) {
fn(...args)
}
}
}
once(name, cb) {
function fn(...args) {
cb(args)
this.off(name, fn)
}
this.on(name, fn)
}
}
实现dom树转化成树结构对象
<div>
<span></span>
<ul>
<li></li>
<li></li>
</ul>
</div>
将上方的DOM转化为下面的树结构对象
{
tag: 'DIV',
children: [
{ tag: 'SPAN', children: [] },
{
tag: 'UL',
children: [
{ tag: 'LI', children: [] },
{ tag: 'LI', children: [] }
]
}
]
}
实现方式
function dom2tree(dom) {
const obj = {}
obj.tag = dom.tagName
obj.children = []
dom.childNodes.forEach(child => obj.children.push(dom2tree(child)))
return obj
}
计算对象的层级数
function loopGetLevel(obj) {
let res = 1;
function computedLevel(obj, level) {
let level = level ? level : 0;
if (typeof obj === 'object') {
for (let key in obj) {
if (typeof obj[key] === 'object') {
computedLevel(obj[key], level + 1);
} else {
res = level + 1 > res ? level + 1 : res;
}
}
} else {
res = level > res ? level : res;
}
}
computedLevel(obj)
return res
}
const obj = {
a: { b: [1] },
c: { d: { e: { f: 1 } } }
}
console.log(loopGetLevel(obj)) // 4
尾递归
尾递归是一种特殊的递归形式,在递归过程中,尽可能把所有的计算都放到最后一步完成,这样就可以优化递归的性能。
在传统的递归中,通常是先递归一些操作,然后再将递归的结果与剩余的计算结果进行处理。在尾递归中,函数的返回值就是递归函数的返回值,并且没有任何后续的计算。这种形式的递归可以使编译器对代码进行优化,消除递归调用的计算和内存开销。
对于尾递归的函数,只需要调用它一次,就可以完成整个递归,而不必反复地调用自身。这样,在递归层数很大的情况下,不会导致栈的爆炸,从而提高了函数的性能和效率。
下面是一个尾递归的例子,计算斐波那契数列:
function fib(n, a = 1, b = 1) {
if (n <= 1) return a;
return fib(n - 1, b, a + b);
}
**
在上面的代码中,尾递归的优化使得计算斐波那契数列的过程中只需要一个栈帧,而不需要创建多个栈帧,从而可以有效地减少内存的占用。
// 传统递归
function factorial(n) {
if (n <= 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
// 尾递归
function factorialTail(n, acc = 1) {
if (n <= 1) {
return acc;
} else {
return factorialTail(n - 1, n * acc);
}
}
console.log(factorial(5)); // 120
console.log(factorialTail(5)); // 120
**
上述示例中,我们分别使用了传统递归和尾递归的方式来计算5的阶乘。其中,传统递归的方式比较容易造成栈溢出,而尾递归则可以避免这个问题,并且执行效率更高。