高频 JavaScript 手写面试题
- 2023-01-30 北京
本文字数:4909 字
阅读完需:约 16 分钟

前言
大家好,我是梁木由,是一个已经躺平了两年的废柴,目前在现在这家公司已经工作了近两年半了。没加薪,没升职,没走的原因是因为工作氛围很舒服,制度很宽松。这不2023来了,我及时醒悟,要有目标,目前就先为年后跳槽,做些准备,先复习回顾下一些手写代码,
目录
实现 new 函数
实现 instanceof
实现 call 函数
实现 apply 函数
实现 bind 函数
实现 debounce 防抖函数
实现 throttle 节流函数
实现数组对象去重
实现数组扁平化
实现深拷贝
1.实现 new 函数
首先需要了解 new 做了什么事情:
首先创建了一个空对象。
将空对象
proto指向构造函数的原型prototype。使
this指向新创建的对象,并执行构造函数。执行结果有返回值并且是一个对象, 返回执行的结果, 否则返回新创建的对象。
// 代码实现function mu_new(fn,...arg){ // 首先创建空对象 const obj = {}; // 将空对象的原型proto指向构造函数的原型prototype Object.setPrototypeOf(obj, fn.prototype) // 将this指向新创建的对象,并且执行构造函数 const result = fn.apply(obj,arg); // 执行结果有返回值并且是一个对象,返回执行的结果,否侧返回新创建的对象 return result instanceof Object ? result : obj;}
// 验证mu_new函数function Dog(name){ this.name = name; this.say = function(){ console.log('my name is' + this.name); }}
const dog = mu_new(Dog, "傻🐶");dog.say() //my name is傻🐶
2.实现 instanceof
优缺点:
「优点」:能够区分 Array、Object 和 Function,适合用于判断自定义的类实例对象
「缺点」:Number,Boolean,String 基本数据类型不能判断
实现步骤:
传入参数为左侧的实例 L,和右侧的构造函数 R
处理边界,如果要检测对象为基本类型则返回 false
分别取传入参数的原型
判断左侧的原型是否取到了 null,如果是 null 返回 false;如果两侧原型相等,返回 true,否则继续取左侧原型的原型。
// 传入参数左侧为实例L, 右侧为构造函数Rfunction mu_instanceof(L,R){ // 处理边界:检测实例类型是否为原始类型 const baseTypes = ['string','number','boolean','symbol','undefined'];
if(baseTypes.includes(typeof L) || L === null) return false;
// 分别取传入参数的原型 let Lp = L.__proto__; let Rp = R.prototype; // 函数才拥有prototype属性
// 判断原型 while(true){ if(Lp === null) return false; if(Lp === Rp) return true; Lp = Lp.__proto__; }}
// 验证const isArray = mu_instanceof([],Array);console.log(isArray); //trueconst isDate = mu_instanceof('2023-01-09',Date);console.log(isDate); // false
3.实现 call 函数
实现步骤:
处理边界:
对象不存在,this 指向 window;
将「调用函数」挂载到「this 指向的对象」的 fn 属性上。
执行「this 指向的对象」上的 fn 函数,并传入参数,返回结果。
Function.prototype.mu_call = function (context, ...args) { //obj不存在指向window if (!context || context === null) { context = window; } // 创造唯一的key值 作为我们构造的context内部方法名 let fn = Symbol();
//this指向调用call的函数 context[fn] = this;
// 执行函数并返回结果 相当于把自身作为传入的context的方法进行调用了 return context[fn](...args); };
// 测试 var value = 2; var obj1 = { value: 1, }; function bar(name, age) { var myObj = { name: name, age: age, value: this.value, }; console.log(this.value, myObj); } bar.mu_call(null); //打印 2 {name: undefined, age: undefined, value: 2} bar.mu_call(obj1, 'tom', '110'); // 打印 1 {name: "tom", age: "110", value: 1}
4.实现 apply 函数
实现步骤:
与 call 一致
区别于参数的形式
Function.prototype.mu_apply = function (context, args) { //obj不存在指向window if (!context || context === null) { context = Window; } // 创造唯一的key值 作为我们构造的context内部方法名 let fn = Symbol();
//this指向调用call的函数 context[fn] = this;
// 执行函数并返回结果 相当于把自身作为传入的context的方法进行调用了 return context[fn](...args);};
// 测试var value = 2;var obj1 = { value: 1,};function bar(name, age) { var myObj = { name: name, age: age, value: this.value, }; console.log(this.value, myObj);}bar.mu_apply(obj1, ["tom", "110"]); // 打印 1 {name: "tom", age: "110", value: 1}
5.实现 bind 函数
Function.prototype.mu_bind = function (context, ...args) { if (!context || context === null) { context = window; } // 创造唯一的key值 作为我们构造的context内部方法名 let fn = Symbol(); context[fn] = this; let _this = this; // bind情况要复杂一点 const result = function (...innerArgs) { // 第一种情况 :若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象 // 此时由于new操作符作用 this指向result实例对象 而result又继承自传入的_this 根据原型链知识可得出以下结论 // this.__proto__ === result.prototype //this instanceof result =>true // this.__proto__.__proto__ === result.prototype.__proto__ === _this.prototype; //this instanceof _this =>true if (this instanceof _this === true) { // 此时this指向指向result的实例 这时候不需要改变this指向 this[fn] = _this; this[fn](...[...args, ...innerArgs]); //这里使用es6的方法让bind支持参数合并 delete this[fn]; } else { // 如果只是作为普通函数调用 那就很简单了 直接改变this指向为传入的context context[fn](...[...args, ...innerArgs]); delete context[fn]; } }; // 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法 // 实现继承的方式: 使用Object.create result.prototype = Object.create(this.prototype); return result; }; function Person(name, age) { console.log(name); //'我是参数传进来的name' console.log(age); //'我是参数传进来的age' console.log(this); //构造函数this指向实例对象 } // 构造函数原型的方法 Person.prototype.say = function () { console.log(123); };
// 普通函数 function normalFun(name, age) { console.log(name); //'我是参数传进来的name' console.log(age); //'我是参数传进来的age' console.log(this); //普通函数this指向绑定bind的第一个参数 也就是例子中的obj console.log(this.objName); //'我是obj传进来的name' console.log(this.objAge); //'我是obj传进来的age' }
let obj = { objName: '我是obj传进来的name', objAge: '我是obj传进来的age', };
// 先测试作为构造函数调用 // let bindFun = Person.mu_bind(obj, '我是参数传进来的name'); // let a = new bindFun('我是参数传进来的age'); // a.say(); //123
// 再测试作为普通函数调用a; let bindFun = normalFun.mu_bind(obj, '我是参数传进来的name'); bindFun('我是参数传进来的age');
6.实现 debounce 防抖函数
函数防抖是在事件被触发 n 秒后再执行回调,如果在「n 秒内又被触发」,则「重新计时」
function debounce(fn, wait) { let timer = null; return function () { if (timer != null) { clearTimeout(timer); } timer = setTimeout(() => { fn(); }, wait); }; } // 测试 function handle() { console.log(Math.random()); } // 窗口大小改变,触发防抖,执行handle window.addEventListener('resize', debounce(handle, 1000));
7.实现 throttle 节流函数
当事件触发时,保证一定时间段内只调用一次函数。例如页面滚动的时候,每隔一段时间发一次请求
实现步骤:
传入参数为执行函数 fn,等待时间 wait。
保存初始时间 now。
返回一个函数,如果超过等待时间,执行函数,将 now 更新为当前时间。
function throttle(fn, wait, ...args) { var pre = Date.now(); return function () { // 函数可能会有入参 var context = this; var now = Date.now(); if (now - pre >= wait) { // 将执行函数的this指向当前作用域 fn.apply(context, args); pre = Date.now(); } }; }
// 测试 var name = 'mu'; function handle(val) { console.log(val + this.name); } // 滚动鼠标,触发防抖,执行handle window.addEventListener('scroll', throttle(handle, 1000, '木由'));
8.实现数组对象去重
利用 map 的键不能重复,去掉某个属性相同的项
function uniqBy(arr, key) { return [...new Map(arr.map((item) => [item[key], item])).values()]; } const list = [ { id: 1, name: 'tom' }, { id: 1, name: 'jey' }, { id: 2, name: 'joy' }, ]; console.log(uniqBy(list, 'id')); // [{id:1,name:"jey"},{id:2,name:"joy"}]
9.实现数组扁平化
flat
let arr = [1,2,[3,4],[5,6,[7,8,9]]]console.log(arr.flat(Infinity))//[1, 2, 3, 4, 5, 6, 7, 8, 9]
join/ split
let arr = [1,2,[3,4],[5,6,[7,8,9]]]console.log(arr.toString().split(",").map(Number))//[1, 2, 3, 4, 5, 6, 7, 8, 9] console.log(arr.join().split(",").map(Number))//[1, 2, 3, 4, 5, 6, 7, 8, 9]
函数版
let arr = [1,2,[3,4],[5,6,[7,8,9]]]function flatter(arr) { if (!arr.length) return; while (arr.some((item) => Array.isArray(item))) { arr = [].concat(...arr); } return arr; }// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));
10.实现深拷贝
对象的深拷贝,实质上就是递归方法实现深度克隆:遍历对象、数组直到里边都是基本数据类型,然后再去复制。
简单版
function deepCopy(obj){ //判断是否是简单数据类型, if(typeof obj == "object"){ //复杂数据类型 var result = obj.constructor == Array ? [] : {}; for(let i in obj){ result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i]; } }else { //简单数据类型 直接 == 赋值 var result = obj; } return result;}
进阶版
function deepClone(obj, hash = new WeakMap()) { // 如果是null或者undefined我就不进行拷贝操作 if (obj == null) return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝 if (typeof obj !== 'object') return obj;
// 是对象的话就要进行深拷贝 if (hash.get(obj)) return hash.get(obj);
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身 let cloneObj = new obj.constructor();
hash.set(obj, cloneObj);
for (let key in obj) { if (obj.hasOwnProperty(key)) { // 实现一个递归拷贝 cloneObj[key] = deepClone(obj[key], hash); } } return cloneObj;}
let obj = { name: 1, address: { x: 100 } };obj.o = obj; // 对象存在循环引用的情况let d = deepClone(obj);obj.address.x = 200;console.log(d);
梁木由
公众号:前端新气象 2023-01-16 加入
一个有想头的前端,对2023年充满希望、怀抱期待,相信自己做个自律的人。要学习,拒绝躺平,从我做起。⛽️










评论