1、冒泡排序
从第一个元素开始,把当前元素和下一个索引元素进行比较。如果当前元素大,那么就交换位置,重复操作直到比较到最后一个元素,那么此时最后一个元素就是该数组中最大的数。下一轮重复以上操作,但是此时最后一个元素已经是最大数了,所以不需要再比较最后一个元素,只需要比较到 length - 1 的位置。
function bubbleSort(list) {
let len = list.length;
for (let i = 0; i < len; i++) {
for (let j = 0; j < len - i - 1; j++) {
if (list[j] > list[j + 1]) {
let tmp = list[j + 1];
list[j + 1] = list[j];
list[j] = tmp;
}
}
}
}
2、快速排序
找到中间位置midValue,遍历数组,小于midValue放在left,否则放在right继续递归,最后concat拼接返回,使用splice会修改原数组,使用slice不会修改原数组(推荐)一层遍历+二分的时间复杂度是O(nlogn)
function quickSort2(arr) {
let len = arr.length;
if (len === 0) return arr;
let midIndex = Math.floor(len / 2);
let midValue = arr.slice(midIndex, midIndex + 1)[0];
let left = [];
let right = [];
for (let i = 0; i < len; i++) {
if (arr[i] !== midValue) {
if (arr[i] < midValue) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
}
return quickSort2(left).concat([midValue], quickSort2(right));
}
3、选择排序
其主要原理是不断地选择剩余元素中的最小值(或最大值)并将其放置到已排序部分的末尾
function selectSort(arr) {
// 缓存数组长度
const len = arr.length;
// 定义 minIndex,缓存当前区间最小值的索引,注意是索引
let minIndex;
// i 是当前排序区间的起点
for (let i = 0; i < len - 1; i++) {
// 初始化 minIndex 为当前区间第一个元素
minIndex = i;
// i、j分别定义当前区间的上下界,i是左边界,j是右边界
for (let j = i; j < len; j++) {
// 若 j 处的数据项比当前最小值还要小,则更新最小值索引为 j
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 如果 minIndex 对应元素不是目前的头部元素,则交换两者
if (minIndex !== i) {
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
}
}
return arr;
}
4、插入排序
工作原理类似于人类按顺序整理一手扑克牌。每次取一张牌并将其插入到已经排序好的牌中的适当位置
function insertSort(arr) {
for (let i = 1; i < arr.length; i++) {
let j = i;
let target = arr[i];
while (j > 0 && arr[j - 1] > target) {
arr[j] = arr[j - 1];
j--;
}
arr[j] = target;
}
return arr;
}
5、归并排序
interview.poetries.top/algorithm/a…
6、深拷贝
- 简便方式
const newObj = JSON.parse(JSON.stringify(oldObj));
-
json.stringify局限性
- 无法解决
循环引用的问题
const a = {val:2}; a.target = a; 拷贝 a 会出现系统栈溢出,因为出现了无限递归的情况。- 无法拷贝一些特殊的对象,诸如
RegExp, Date, Set, Map等 - 无法拷贝
函数
- 无法解决
-
完整实现方式
const getType = obj => Object.prototype.toString.call(obj);
const isObject = (target) => (typeof target === 'object'
|| typeof target === 'function') && target !== null;
const canTraverse = {
'[object Map]': true,
'[object Set]': true,
'[object Array]': true,
'[object Object]': true,
'[object Arguments]': true,
};
const mapTag = '[object Map]';
const setTag = '[object Set]';
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
const handleRegExp = (target) => {
const { source, flags } = target;
return new target.constructor(source, flags);
}
const handleFunc = (func) => {
// 箭头函数直接返回自身
if(!func.prototype) return func;
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=().+(?=)\s+{)/;
const funcString = func.toString();
// 分别匹配 函数参数 和 函数体
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if(!body) return null;
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
}
const handleNotTraverse = (target, tag) => {
const Ctor = target.constructor;
switch(tag) {
case boolTag:
return new Object(Boolean.prototype.valueOf.call(target));
case numberTag:
return new Object(Number.prototype.valueOf.call(target));
case stringTag:
return new Object(String.prototype.valueOf.call(target));
case symbolTag:
return new Object(Symbol.prototype.valueOf.call(target));
case errorTag:
case dateTag:
return new Ctor(target);
case regexpTag:
return handleRegExp(target);
case funcTag:
return handleFunc(target);
default:
return new Ctor(target);
}
}
const deepClone = (target, map = new WeakMap()) => {
if(!isObject(target))
return target;
let type = getType(target);
let cloneTarget;
if(!canTraverse[type]) {
// 处理不能遍历的对象
return handleNotTraverse(target, type);
}else {
// 这波操作相当关键,可以保证对象的原型不丢失!
let ctor = target.constructor;
cloneTarget = new ctor();
}
if(map.get(target))
return target;
map.set(target, true);
if(type === mapTag) {
//处理Map
target.forEach((item, key) => {
cloneTarget.set(deepClone(key, map), deepClone(item, map));
})
}
if(type === setTag) {
//处理Set
target.forEach(item => {
cloneTarget.add(deepClone(item, map));
})
}
// 处理数组和对象
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop], map);
}
}
return cloneTarget;
}
7、call、apply、bind
call实现
Function.prototype.myCall = function(context = window , ...args){
if(typeof context !== 'object'){
context = new Object(context)
}
const fnKey = Symbol();
context[fnKey] = this;
let result = context[fnKey](...args);
delete context[fnKey];
return result
}
apply实现
Function.prototype.myApply = function (context = window, args) {
if (typeof context !== "object") {
context = new Object(context);
}
let fnkey = Symbol();
context[fnkey] = this;
let result = context[fnkey](...args);
delete context[fnkey];
return result;
};
bind实现
Function.prototype.myBind = function (context = window, ...args) {
if (typeof context !== "object") {
context = new Object(context);
}
let self = this;
let result = function (innerArgs) {
return self.apply(
this instanceof result ? this : context,
args.concat(innerArgs)
);
};
// 如果绑定的是构造函数,那么需要继承构造函数原型属性和方法:保证原函数的原型对象上的属性不丢失
// 实现继承的方式: 使用Object.create
result.prototype = Object.create(this.prototype);
return result;
};
8、设计模式相关
设计模式分类
-
单例模式:核心就是保证全局只有一个对象可以访问, 例如vuex和redux的基础设计就是单例模式, 全局只有一个store, 全局缓存也算单例模式, 确保实例只创建一次.全局类型的dom弹层的实现就是单例模式
-
工厂模式:通过传入不同的参数达到实例化不同的对象这一目的。比如: 有一天你去餐厅想吃宫保鸡丁,然后餐厅给你实例化了宫保鸡丁。 例如封装im消息类型的时候采用的就是该模式
-
抽象工厂模式:就是定制了实例的结构,比如宫保鸡丁和西红柿炒鸡蛋都可以吃那么他们有共同的结构吃,又比如饭店有做菜的功能,其他饭店也可以做菜,那么做菜这个方法可以作为抽象类来约束实例。抽象类(对有相同方法(结构)的实例进行约束),
工厂模式主要关注的是当前产品的创建,
优势:良好的封装,符合开放封闭原则
劣势: 增加系统复杂性
抽象工厂模式关注当前的抽象类,以及当前的产品。
劣势:(违反了开放封闭原则,添加系统复杂性)
优势:我们不需要知道产品的具体细节,只要看当前抽象类的方法(结构)进行编程就可以了
-
状态模式:一个对象有状态变化,每一个状态变化都会触发一个逻辑,我们不能总是if...else 来写,所以我们就把状态和当前对象分离开来,比如最常见都红绿灯。红灯状态下是(停下),黄灯状态是(警告),绿灯状态是(通行)。那么我们就可以把这3个状态和方法都抽离出来。提高代码复用,符合开放封闭原则
-
适配器模式: 适配器用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式实现两个接口的正常协作。在
Vue中,我们其实经常使用到适配器模式。比如父组件传递给子组件一个时间戳属性,组件内部需要将时间戳转为正常的日期显示,一般会使用computed来做转换这件事情,这个过程就使用到了适配器模式, 还有就是对于图表和数据展示的时候, 前端图表数据需要中转函数来处理 -
装饰模式: 不需要改变已有的接口,作用是给对象添加功能。就像我们经常需要给手机戴个保护套防摔一样,不改变手机自身,给手机添加了保护套提供防摔功能。在
React中,装饰模式其实随处可见
import { connect } from 'react-redux'
class MyComponent extends React.Component {
// ...
}
export default connect(mapStateToProps)(MyComponent)
-
代理模式:- 代理是为了控制对对象的访问,不让外部直接访问到对象。在现实生活中,也有很多代理的场景。比如你需要买一件国外的产品,这时候你可以通过代购来购买产品。实际用法如事件代理
-
发布-订阅模式(观察者模式):通过一对一或者一对多的依赖关系,当对象发生改变时,订阅方都会收到通知。在现实生活中,也有很多类似场景,比如我需要在购物网站上购买一个产品,但是发现该产品目前处于缺货状态,这时候我可以点击有货通知的按钮,让网站在产品有货的时候通过短信通知我。 实际工作中: 1、点击一个按钮触发了点击事件就是使用了该模式 2、在
Vue中,如何实现响应式也是使用了该模式。对于需要实现响应式的对象来说,在get的时候会进行依赖收集,当改变了对象的属性时,就会触发派发更新。
9、输出问题
1、this问题
let obj1 = {
a: 1,
foo: () => {
console.log(this.a)
}
}
// log1
console.log(obj1.foo())
const obj2 = obj1.foo
// log2
console.log(obj2())
2、事件循环问题
const p1 = () => (new Promise((resolve, reject) => {
console.log(1);
let p2 = new Promise((resolve, reject) => {
console.log(2);
const timeOut1 = setTimeout(() => {
console.log(3);
resolve(4);
}, 0)
resolve(5);
});
resolve(6);
p2.then((arg) => {
console.log(arg);
});
}));
const timeOut2 = setTimeout(() => {
console.log(8);
const p3 = new Promise(reject => {
reject(9);
}).then(res => {
console.log(res)
})
}, 0)
p1().then((arg) => {
console.log(arg);
});
console.log(10);
3、批处理和函数更新的问题
import { useState } from 'react'
const Test = () => {
const [count1, setCount1] = useState(1)
const [count2, setCount2] = useState(100)
console.log('重新渲染了!')
const onOK = () => {
setCount1((state) => state + 1)
console.log(count1)
setCount1((state) => state + 1)
console.log(count1)
setTimeout(() => {
setCount1(count1 + 1)
console.log(count1)
setCount2(count2 + 100)
console.log(count2)
}, 1000)
}
return (
<>
<div onClick={onOK}>按钮</div>
<p>{ count1 }</p>
<p>{ count2 }</p>
</>
)
}
export default Test