目录
1. 字符串相关
2. 数组相关
3. 对象相关
4. 其他:
- 4.1 new 的实现
- 4.2 订阅发布模式简易实现
- 4.3 观察者模式简易实现
- 4.4 Promise 完整版本的实现
- 4.5 ajax 实现思路模拟代码
- 4.6 防抖和节流
一, 字符串相关
1.1 (简单算法题) 取字符串 let str = "aabbckdllllaaakjjjmmmdddl"; 中出现次数最多的字符是什么,出现多少次?
// 创建一个存放字符和其出现次数映射关系的 对象
const obj = Object.create(null);
// 可直接 for 循环遍历字符串,也可以转为数组来遍历
[...str].map(item => console.log(item));
二, 数组相关
2.1 数组去重
方法1 indexOf 或 includes
var arr = [1,2,3,'a','a',2,3];
function unique(arr){
if(Object.prototype.toString.call(arr) !== '[object Array]'){
return;
}
const res = [];
arr.map((item, index) => {
// 新数组中没有 item , push 到新数组. 有的话,不做任何处理.
// res.indexOf(item) === -1
if(!res.includes(item)){
res.push(item);
}
});
return res;
};
unique(arr); // [1, 2, 3, 'a']
方法2 ... 与 Set
var arr = [1,2,3,'a','a',2,3];
[...new Set(arr)]; // [1, 2, 3, 'a']
Array.from(new Set(arr)); // [1, 2, 3, 'a']
2.2 数组归并求和 reduce
var arr = [1,2,3,4,5,6];
arr.reduce((prev, item ,index, arr)=>{
// prev 初始值为 100, 第二轮遍历是 cb 语句的返回值,依此类推.
// console.log(prev,item)
return prev + 9;
}, 100);
2.3 数组降维 递归 concat push, reduce
方法1 递归 + concat + push
concat 可以连接
值类型元素, 可代替 push. 注意, concat 的返回值是新数组, push 是直接修改原始数组,返回值是新数组元素个数.
var arr = [1, [2,3], [4,5],[6,['a']], 'b'];
function flat(arr){
if(!Array.isArray(arr)){
console.error('type error');
return
};
let res = [];
arr.forEach((item, index) => {
if(Array.isArray(item)){
res = res.concat(flat(item));
}else{
// concat 或者 push 都行
res = res.concat(item)
// res.push(item);
}
});
return res;
};
flat(arr)
// [1, 2, 3, 4, 5, 6, 'a', 'b']
方法1 简写
只是改成了三目, 把 res 赋值改为统一赋值.
var arr = [1, [2,3], [4,5],[6,['a']], 'b'];
function flat(arr){
if(!Array.isArray(arr)){
console.error('type error');
return
};
let res = [];
arr.forEach((item, index) => {
res = Array.isArray(item) ? res.concat(flat(item)): res.concat(item)
});
return res;
};
flat(arr)
(8) [1, 2, 3, 4, 5, 6, 'a', 'b']
方法2 递归 + reduce + concat
注意每次 reduce 函数体
必须要有返回值,prev 为上次函数执行的返回值, 直到遍历完,reduce的返回值就是prev 最后的值的下一个值,也就是函数体最后一次执行的值.
var arr = [1, [2,3], [4,5],[6,['a']], 'b'];
function flat(arr){
if(!Array.isArray(arr)){
return
}
return arr.reduce((prev,item,index) => {
// 注意 return
if(Array.isArray(item)){
return prev.concat(flat(item))
}else{
return prev.concat(item)
}
},[]);
}
flat(arr); // [1, 2, 3, 4, 5, 6, 'a', 'b']
方法2 简写
function flat(arr){
return arr.reduce((pre, cur)=> pre.concat(Array.isArray(cur) ? flat(cur) : cur), []) };
var arr = [1, 2, 3,[4,[5]],6];
flat(arr); // [1, 2, 3, 4, 5, 6]
方法3 arr.flat(维度值)
var arr = [1, [2,3], [4,5],[6,['a']], 'b'];
arr.flat(2); // [1, 2, 3, 4, 5, 6, 'a', 'b']
2.3 数组中假值,空值去除 ( filter 返回一个新数组 )
var arr = [undefined,null, "", ' ', false, 1,2,3,5,6];
arr.filter((item)=>{
return item
})
三, 对象相关
3.1 对象深拷贝 递归
需要特别注意的是,深拷贝可能会导致性能问题,因为它需要递归遍历整个对象树。此外,深拷贝可能无法处理包含循环引用的对象,因为递归过程可能陷入无限循环。在处理复杂对象时,需要慎重选择浅拷贝和深拷贝,以满足特定需求
深拷贝会递归复制对象及其嵌套对象的所有属性和属性值,创建一个完全独立的新对象,与原始对象没有共享引用。
深拷贝通常需要使用递归函数或专门设计的深拷贝库,因为JavaScript的原生方法通常无法轻松实现深拷贝。
function clone(target) {
if (typeof target === 'object') {
// 支持拷贝值是数组的情况
let res = Array.isArray(target) ? [] : {};
for (const key in target) {
res[key] = clone(target[key]);
}
return res;
} else {
return target;
}
};
var obj = {
a:1,
b:[2,3],
c:{
aaa:11,
bbb:22
},
d:'abc',
e:true,
f:function(){return 1}
}
clone(obj);
// {a:1,b:[2,3],c:{aaa:11,bbb:22},d:'abc',e:true,f:function(){return 1}}
拷贝的对象是循环引用,直接报错
var obj = {
a:1,
b:[2,3],
c:{
aaa:11,
bbb:22
},
d:'abc',
e:true,
f:function(){return 1}
}
// 循环引用
obj.obj = obj;
function clone(target) {
if (typeof target === 'object') {
// 支持拷贝值是数组的情况
let res = Array.isArray(target) ? [] : {};
for (const key in target) {
res[key] = clone(target[key]);
}
return res;
} else {
return target;
}
};
clone(obj);
// Uncaught RangeError: Maximum call stack size exceeded
Map 解决循环引用的问题
解决循环引用问题,我们可以
额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解了循环引用的问题。
这个存储空间,需要可以存储
key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:
- 检查
map中有无克隆过的对象 - 有, 直接返回
- 没有, 将当前对象作为
key,克隆对象作为value进行存储 - 继续克隆
var obj = {
a:1,
b:[2,3],
c:{
aaa:11,
bbb:22
},
d:'abc',
e:true,
f:function(){return 1}
}
obj.obj = obj;
function clone(target, map = new Map()) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
for (const key in target) {
cloneTarget[key] = clone(target[key], map);
}
return cloneTarget;
} else {
return target;
}
};
clone(obj);
Map 和 Set 数据结构是ES6语法,最大优点就是运行时间少大大提高了性能
Map 和 Set 都不允许键重复
Set 对象类似于数组,且成员的值都是唯一的, 接收一个数组做为参数,常用的是 数组去重.
var arr = [...new Set([1,2,3,2,3,1])]
console.log(arr); // [1, 2, 3]
补充 weakMap weakSet symbol
WeakMap 是 ECMAScript 6 中引入的一种特殊的 Map 实现,它的键必须是对象,并且对键的引用是弱引用。这意味着在没有其他引用存在时,键所对应的键值对将被自动回收,不会造成内存泄漏。
Symbol 是 ECMAScript 6 中引入的一种新的原始数据类型,用于表示独一无二的标识符。它的主要特点是创建的每个 Symbol 值都是唯一的,不会与其他 Symbol 值相等。
let sym1 = Symbol("key");
let sym2 = Symbol("key");
console.log(sym1 === sym2); // 输出: false
每个 Symbol 值都是唯一的,即使它们的
描述符相同,它们也不相等。
四, 其他
4.1 new 的实现
function Person(name,age){
this.name = name;
this.age= age;
this.fn=()=>{ console.log('方法')}
}
Person.prototype={
fn1:function(){console.log("fn1")}
}
var p=new Person('andy');//可以不加小括号,不传参的话
p.name//"andy"
p.fn()//"方法"{name: 'andy', age: undefined, fn: ƒ}
console.log(p);// {name: 'andy', age: undefined, fn: ƒ}
function _new(/*constr,params*/){//...arg更方便
let args=[...arguments];
let constructor=args.shift();
var newObj={};
newObj.__proto__=constructor.prototype;
//let newObj = Object.create(constructor);//issue
let result = constructor.apply(newObj ,args);
console.log("result",result,"newObj ",newObj )
return typeof result === "object" ? result: newObj
}
var p = _new(Person,'andy',22); // result undefined newObj {name: 'andy', age: 22, fn: ƒ}
4.2 订阅发布模式简易实现
class EventBus{
constructor(){
// 在event对象中 存放 所有的事件与回调数组,如:
// {a: [ ()=>{console.log(1)}, (a)=>{console.log(a)}], b: [()=>{console.log(1)}]}
this.event=Object.create(null);
};
on(name,fn){
if(!this.event[name]){
//一个事件可能有多个监听者
this.event[name]=[];
};
this.event[name].push(fn);
};
emit(name,...args){
//遍历要触发的事件对应的数组回调函数。依次调用数组当中的函数,并把参数传入每一个cb。
this.event[name] && this.event[name].forEach(fn => {
fn(...args)
});
};
// 只触发一次事件@功能 借助变量cb,同时完成了对该事件的注册、对该事件的触发,并在最后取消该事
once(name,fn){
var cb=(...args)=>{
fn(...args); //触发
this.off(name,fn); //取消
};
this.on(name,cb); //监听
};
off(name,offcb){
if(this.event[name]){
// 找到要取消事件在回调数组中的索引
let index=this.event[name].findIndex((fn)=>{
return offcb===fn;
});
//通过索引删除掉对应回调数组中的回调函数。
this.event[name].splice(index,1);
// 回调数组长度为0时(没有回调数组时)
if(!this.event[name].length){
// 删除事件名
delete this.event[name];
}
}
}
}
var a = new EventBus()
// on emit
a.on('a',()=>{console.log(1)}); a.on('a',(a)=>{console.log(a)}); a.on('b',()=>{console.log(1)});
a.emit('a','2'); // 1 2
a.emit('b'); // 1
console.log(a.event);
// {a: [ ()=>{console.log(1)}, (a)=>{console.log(a)}], b: [()=>{console.log(1)}] }
4.3 观察者模式简易实现
class Observer {
// 回调函数,收到目标对象通知时执行
constructor(cb){
if (typeof cb === 'function') {
this.cb = cb
} else {
throw new Error('Observer构造器必须传入函数类型!')
}
}
// 被目标对象通知时执行
update() {
this.cb()
}
}
// 被观察者(目标对象)
class Subject {
constructor() {
// 维护观察者列表 `Aclass中存的 [Bclass, Bclass, Bclass ...]`
this.observerList = [];
}
// 添加一个观察者 `Aclass.add(Bclass)`
addObserver(observer) {
this.observerList.push(observer);
}
// 通知所有的观察者 `Aclass.notify` -> `Bcalss.update` -> `Bcalss.cb`
notify() {
this.observerList.forEach(observer => {
observer.update()
})
}
}
const observerCallback = function() {
console.log('observerCallback 我被通知了')
}
const observer = new Observer(observerCallback)
const subject = new Subject();
subject.addObserver(observer);
subject.notify(); // observerCallback 我被通知了
4.4 Promise 完整版本的实现
function myPromise(constructor){
let self=this;
self.status="pending" //定义状态改变前的初始状态
self.value=undefined;//定义状态为resolved的时候的状态
self.reason=undefined;//定义状态为rejected的时候的状态
self.onFullfilledArray=[]; // 存储 成功的回调函数
self.onRejectedArray=[];// 存储 失败的回调函数
function resolve(value){
if(self.status==="pending"){ // 保证了状态的改变是不可逆的
self.value=value;
self.status="resolved";
// 在pending时把状态改变为成功并把数据传过来。
// 并遍历调用成功的回调数组
console.log('构造函数中 resolved中的 成功回调数组:', self.onFullfilledArray);
self.onFullfilledArray.forEach(function(f){
f(self.value);
//如果状态从pending变为resolved,
//那么就遍历执行里面的异步方法
});
}
}
function reject(reason){
if(self.status==="pending"){
self.reason=reason;
self.status="rejected";
self.onRejectedArray.forEach(function(f){
f(self.reason);
//如果状态从pending变为rejected,
//那么就遍历执行里面的异步方法
})
}
}
//捕获构造异常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
let promise2;
switch(self.status){
case "pending":
promise2=new myPromise(function(resolve,reject){
self.onFullfilledArray.push(function(){
try{
let temple=onFullfilled(self.value);
resolve(temple)
}catch(e){ reject(e);}
});
self.onRejectedArray.push(function(){
try{
let temple=onRejected(self.reason);
reject(temple)
}catch(e){ reject(e);}
});
})
case "resolved":
promise2=new myPromise(function(resolve,reject){
try{
let temple=onFullfilled(self.value);
//将上次一then里面的方法传递进下一个Promise的状态
resolve(temple);
}catch(e){
reject(e); }
});break;
case "rejected":
promise2=new myPromise(function(resolve,reject){
try{
let temple=onRejected(self.reason);
//将then里面的方法传递到下一个Promise的状态里
resolve(temple);
}catch(e){
reject(e);
}
}); break;
default:
}
return promise2;
}
var p=new myPromise(function(resolve,reject){setTimeout(function(){resolve(111)},1000)});
p.then(function(x){console.log(x)}).then(function(){console.log("链式调用1")}).
then(function(){console.log("链式调用2")})
//输出 链式调用1 链式调用2 111
4.5 ajax 实现思路模拟代码
let xhr = new XMLHttpRequest()
// 初始化
xhr.open(method, url, async)
// 发送请求
xhr.send(data)
// 设置状态变化回调处理请求结果
xhr.onreadystatechange = () => {
if (xhr.readyStatus === 4 && xhr.status === 200) {
console.log(xhr.responseText)
}
}
//2> pipe实现
const pipe = (...args) => x => args.reduce((res, cb) => cb(res), x);
//3> compose实现
const compose = function(){
// 将接收的参数存到一个数组, args == [multiply, add]
const args = [].slice.apply(arguments);
return function(x) {
return args.reduceRight((res, cb) => cb(res), x);
}
};
const add = x => x + 1; const square = x => x ** 2; const sub = x => x - 1;
let calc1 = compose(sub, square, add);
let res = calc1(1);
console.log(res); // 3
4.6 防抖和节流
防抖是触发停止后,
重新记时. 节流是一段时间执行一次
节流
不清除定时器. 防抖清除定时器
// 防抖
function debounce(fn, delay = 300) {
//默认300毫秒
let timer;
return function () {
const args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, args); // 改变this指向为调用debounce所指的对象
}, delay);
};
}
window.addEventListener(
"scroll",
debounce(() => {
console.log(111);
}, 100)
);
// 节流
function debounce(fn, delay = 300) {
//默认300毫秒
let timer;
return function () {
const args = arguments;
if (timer) {
return;
}
timer = setTimeout(() => {
fn.apply(this, args); // 改变this指向为调用debounce所指的对象
}, delay);
};
}
window.addEventListener(
"scroll",
debounce(() => {
console.log(111);
}, 100)
);