前端面试JS手撕代码(详细注释,包理解~)

999 阅读6分钟

一、数组去重的五种方法

let arr = [1, 0, 2, 3, 4, 5, 2, 3, 4];
//indexOf去重
function removeRepeat(arr) {
  let res = [];
  for (let i of arr) {
    if (res.indexOf(i) == -1) {
      res.push(i);
    }
  }
  return res;
}
// set 去重
function removeRepeat(arr) {
  let res = new Set(arr);
  return Array.from(res);
}
// for循环去重
function removeRepeat(arr) {
  for (let i = 0; i < arr.length; i++) {
    for (let j = i + 1; j < arr.length; j++) {
      if (arr[i] == arr[j]) {
        arr.splice(j, 1);
        j--;
      }
    }
  }
  return arr;
}
// filter 去重
function removeRepeat(arr) {
  return arr.filter((item, index) => {
    return arr.indexOf(item) == index;
  });
}
// includes 去重
function removeRepeat(arr) {
  let res = [];
  for (let i of arr) {
    if (!res.includes(i)) {
      res.push(i);
    }
  }
  return res;
}
let res = removeRepeat(arr);
console.log(res);

二、深拷贝和浅拷贝

区别:

深拷贝:拷贝对象中的所有数据都进行重新赋值给新对象 对于引用类型 开辟一块新的空间 进行指向 和之前的对象没有关联

浅拷贝:只拷贝第一层对象中的数据 里面的引用不会进行拷贝 对于浅拷贝 引用类型的数值指向同一块内存空间

无论是深拷贝还是浅拷贝 :基本数据类型的值都是相互独立的,深拷贝其中的引用类型值独立、浅拷贝 其中的引用类型指向同一块空间

  • 深拷贝

1、for循环

let obj = {
    name: "coderqian",
    hobby: {
        outdoor: "basketball",
        indoor: "watch mv",
    },
};
function deepClone(obj) {
    let newobj = {};
    for (let i in obj) {
        if (typeof obj[i] == "Object") {
            // 如果是object类型 递归调用
            newobj[i] = deepClone(obj[i]);
        } else {
            // 基本类型 直接赋值
            newobj[i] = obj[i];
        }
    }
    return newobj;
}
let newobj = deepClone(obj);
console.log(newobj);

2、Json方法

let obj = {
    name: "coderqian",
    hobby: {
        outdoor: "basketball",
        indoor: "watch mv",
    },
};
// JSON.parse 把json转化为js对象 JSON.stringify 把js对象转化为json对象
let newobj = JSON.parse(JSON.stringify(obj));
  • 浅拷贝

1、for循环遍历一层

let obj = {
    name: "coderqian",
    hobby: {
        outdoor: "basketball",
        indoor: "watch mv",
    },
};
function shallowclone(obj) {
    let newobj = {};
    for (let i in obj) {
        newobj[i] = obj[i];
    }
    return newobj;
}
// 此时第一层非引用类型的值是相互独立的 但是hobby对象中的引用类型指向同一块内存空间
let newobj = shallowclone(obj);

2、Object.assign

let obj = {
    name: "coderqian",
    hobby: {
        outdoor: "basketball",
        indoor: "watch mv",
    },
};
let newobj = {};
Object.assign(newobj, obj); 

3、直接=赋值

三、手写instanceof

function myInstanceof(instance, target) {
    // 定义一个指针指向实例
    let pointer = instance;
    while (pointer) {
        //   一个实例的隐式原型 等于构造函数的显示原型
        if (pointer == target.prototype) {
            return true;
        } else {
            pointer = pointer.__proto__;
        }
    }
    return false;
}
let obj = {};
let res = myInstanceof(obj, Object);
console.log(res); // true

四、手写new

function mynew(Classname) {
    // 1、创建新对象
    let newobj = {};
    // 2、将新对象的隐式原型指向构造函数的显式原型 即部署在原型链上
    newobj.__proto__ = Classname.prototype;
    // 把参数转化成数组 且只取第二个开始的参数
    let args = Array.from(arguments).slice(1);
    // 3、改变构造函数中的this指向新对象 即给新对象赋值 
    let res = Classname.call(newobj, ...args);
    // 4、返回新对象 (若构造函数有返回对象 则返回那个对象 否则返回newobj)
    return typeof res == "object" ? res : newobj;
}
function Person(name, age) {
    this.name = name;
    this.age = age;
}
let person = mynew(Person, "coderqian", 23);
console.log(person);  //Person { name: 'coderqian', age: 23 }

五、手写call、apply、bind

这里解释一下这三者的区别 这三个方法都是为了改变this指向

其中call和apply第一参数就是传入this的值 区别在于:

call 后面的参数 可以分开传递

apply后面的参数 放在一个数组中传递

bind和call 传参类似 只是bind 返回的是一个函数 不会立即执行 需要再次调用 可以看下发具体实现领悟~

一、call

Function.prototype.mycall = function (thisArg) {
    // 如果没传参数 就以window
    let context = thisArg || window;
    //  arguments是伪数组 先转化为数组 之后 从第二个元素(下标为0)开始拷贝剩余的作为参数
    let argus = Array.from(arguments).slice(1);
    // 为这个对象创建一个属性 属性为play函数 这里的this就指向 mycall的调用者 即play函数
    context["fn"] = this;
    context["fn"](...argus); // 执行play函数 此时这个函数的调用者 是context 即 传入的 obj 达到改变this指向的效果
    delete context["fn"];  // 删除刚才添加的属性
};

function play(name, age) {
    console.log(name + age); // coderqian22
    console.log(this.name); //obj-coderqian this指向obj
}

let obj = {
    name: "obj-coderqian",
};
play.mycall(obj, "coderqian", 22);

二、apply

// apply的实现和call思路一样 只是需要区别有参数和没参数的情况
Function.prototype.myapply = function (thisArg) {
    let context = thisArg || window;
    context["fn"] = this;
    if (arguments[1]) {
        // 传入参数
        let argus = Array.from(arguments)[1];
        context["fn"](...argus);
    } else {
        // 没参数
        context["fn"]();
    }
    delete context["fn"];
};

function play(name, age) {
    console.log(name + age); //coderqian22
    console.log(this.name); //obj-coderqian 
}

let obj = {
    name: "obj-coderqian",
};
play.myapply(obj, ["coderqian", 22]);

三、bind

Function.prototype.mybind = function (thisArg) {
    // 保存this变量 因为闭包 不好找到this (也就是mybind的调用者 play)
    let self = this;
    //   第一个括号的参数
    let argus = Array.from(arguments).slice(1);
    return function F() {
        //   第二个括号的参数
        let argus1 = Array.from(arguments);
        // 参数进行拼接
        let argssum = argus.concat(argus1);
        // 判断是不是new出来的 因为new里面this会丢失
        if (this instanceof F) {
            // 是new出来的话 就让当前的这个实例作为this
            self.apply(this, argssum);
        } else {
            // 不是new的话 就直接是传入的这个对象作为this
            self.apply(thisArg, argssum); 
        }
    };
};
function play(name, age, sex) {
    console.log(name + age + sex);
}

let obj = {
    name: "coderqian111",
};
let res = play.mybind(obj, "coderqian", 22);
res("男");

六、防抖 、节流

一、防抖

// 防抖 就是在没触发的一段时间后执行 若是在这一段时间内触发了 则会重新计时 不会执行该函数
// 防抖:在停止操作的一段时间后执行
// func 是要处理的函数 delay是延迟时间
function debounce(func, delay) {
    let timer;
    return function () {
        if (timer) clearTimeout(timer); // 如果上次的计时器还在 就清空计数器 重新计时
        timer = setTimeout(() => {
            func.apply(this, arguments);
        }, delay);
    };
}
function paymoney() {
    console.log("付钱防抖!");
}
let button = document.getElementById("debounce");
button.onclick = debounce(paymoney, 1000);

二、节流

// 节流:每隔一个固定的时间就执行
// 方法一
function throttle(func, delay) {
    let timer;
    return function () {
        //   如果timer存在 说明 上一个计时器还没结束 也就是 在这段时间内 不会执行函数
        if (timer) {
            return;
        }
        timer = setTimeout(() => {
            func();
            timer = null; // delay 时间过完了 timer置为null 表示后面的出发可以再次执行
        }, delay);
    };
}
// 方法二 通过时间戳 这个方法节流的第一次会立马执行
function throttle(func, delay) {
    let prev = 0;
    return function () {
        let now = new Date();
        // 如果当前的时间减去上一次执行的时间大于延迟时间 就会可以执行
        // 并将这次执行的时间作为下一次的开始时间
        if (now - prev > delay) {
            func();
            prev = now;
        }
    };
}

function paymoney() {
    console.log("付钱节流!");
}
let button1 = document.getElementById("throttle");
button1.onclick = throttle(paymoney, 1000);  

七、原生ajax

function sendajax() {
    // 1、 初始化xhr对象
    const xhr = new XMLHttpRequest();
    //  2、 建立连接 设置请求方法和url
    xhr.open("get", "./data.json");
    //   3、发送请求
    xhr.send();
    //   4、状态改变时 进行回调
    xhr.onreadystatechange = function () {
        // readyState 有0-4 五个值
        // 0 代表 未初始化 1 代表 初始化成功 2 代表发送请求
        // 3 代表返回了部分数据 4 代表返回了全部数据
        if (xhr.readyState == 4) {
            if (xhr.status >= 200 && xhr.status < 300) {
                //   进行成功的操作
                console.log(xhr.responseText);
            }
        }
    };
}
sendajax();

八、数组扁平化

//传入参数 决定扁平化的阶数
Array.prototype._flat = function (n) {
    let result = [];
    let num = n;
    for (let item of this) {
        // 如果是数组
        if (Array.isArray(item)) {
            n--;
            //   没有扁平化的空间 直接推入
            if (n < 0) {
                result.push(item);
            }
            // 继续扁平化 并将n传入 决定item这一个数组中的扁平化
            else {
                result.push(...item._flat(n));
            }
        }
        // 不是数组直接推入
        else {
            result.push(item);
        }
        // 每次循环 重置n 为传入的参数 因为每一项都需要扁平化 需要进行判断
        n = num;
    }
    return result;
};
let arr = [1, 2, [3, 4], [5, 6, [7, 8]]];
let res = arr._flat(1);
console.log(res); // [ 1, 2, 3, 4, 5, 6, [ 7, 8 ] ]