JS 高频面试题汇总一

179 阅读9分钟

目录

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)
);