-
实现防抖函数(debounce)
防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
那么与节流函数的区别直接看这个动画实现即可。
手写简化版:
= null; return (...args) => { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; };
适用场景:
- 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
- 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似
生存环境请用lodash.debounce
模板引擎实现
data = { name: '姓名', age: 18 } render(template, data); // 我是姓名,年龄18,性别undefined
/\{\{(\w+)\}\}/; // 模板字符串正则 if (reg.test(template)) { // 判断模板里是否有模板字符串 const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段 template = template.replace(reg, data[name]); // 将第一个模板字符串渲染 return render(template, data); // 递归的渲染并返回渲染后的结构 } return template; // 如果模板没有模板字符串直接返回 }
实现日期格式化函数
输入:
2020/12/01 dateFormat(new Date('2020-04-01'), 'yyyy/MM/dd') // 2020/04/01 dateFormat(new Date('2020-04-01'), 'yyyy年MM月dd日') // 2020年04月01日
var day = dateInput.getDate() var month = dateInput.getMonth() + 1 var year = dateInput.getFullYear() format = format.replace(/yyyy/, year) format = format.replace(/MM/,month) format = format.replace(/dd/,day) return format }
实现bind方法
bind
的实现对比其他两个函数略微地复杂了一点,涉及到参数合并(类似函数柯里化),因为bind
需要返回一个函数,需要判断一些边界问题,以下是bind
的实现bind
返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过new
的方式,我们先来说直接调用的方式- 对于直接调用来说,这里选择了
apply
的方式实现,但是对于参数需要注意以下情况:因为bind
可以实现类似这样的代码f.bind(obj, 1)(2)
,所以我们需要将两边的参数拼接起来 - 最后来说通过
new
的方式,对于new
的情况来说,不会被任何方式改变this
,所以对于这种情况我们需要忽略传入的this
简洁版本
- 对于普通函数,绑定
this
指向 - 对于构造函数,要保证原函数的原型对象上的属性不能丢失
...args) { // this表示调用bind的函数 let self = this; //返回了一个函数,...innerArgs为实际调用时传入的参数 let fBound = function(...innerArgs) { //this instanceof fBound为true表示构造函数的情况。如new func.bind(obj) // 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true,可以让实例获得来自绑定函数的值 // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context return self.apply( this instanceof fBound ? this : context, args.concat(innerArgs) ); } // 如果绑定的是构造函数,那么需要继承构造函数原型属性和方法:保证原函数的原型对象上的属性不丢失 // 实现继承的方式: 使用Object.create fBound.prototype = Object.create(this.prototype); return fBound; } ``` ```javascript // 测试用例 function Person(name, age) { console.log('Person name:', name); console.log('Person age:', age); console.log('Person this:', this); // 构造函数this指向实例对象 } // 构造函数原型的方法 Person.prototype.say = function() { console.log('person say'); } // 普通函数 function normalFun(name, age) { console.log('普通函数 name:', name); console.log('普通函数 age:', age); console.log('普通函数 this:', this); // 普通函数this指向绑定bind的第一个参数 也就是例子中的obj } var obj = { name: 'poetries', age: 18 } // 先测试作为构造函数调用 var bindFun = Person.myBind(obj, 'poetry1') // undefined var a = new bindFun(10) // Person name: poetry1、Person age: 10、Person this: fBound {} a.say() // person say // 再测试作为普通函数调用 var bindNormalFun = normalFun.myBind(obj, 'poetry2') // undefined bindNormalFun(12) // 普通函数name: poetry2 普通函数 age: 12 普通函数 this: {name: 'poetries', age: 18} ``` > 注意: `bind`之后不能再次修改`this`的指向,`bind`多次后执行,函数`this`还是指向第一次`bind`的对象 参考:[前端手写面试题详细解答](https://kc7474.com/archives/1333?url=handwritten) ### 模拟Object.create Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。 ```javascript // 模拟 Object.create function create(proto) { function F() {} F.prototype = proto; return new F(); }
throttle(节流)
高频时间触发,但n秒内只会执行一次,所以节流会稀释函数的执行频率。
return function() { if (!flag) return; flag = false; setTimeout(() => { fn.apply(this, arguments); flag = true; }, time); } }
节流常应用于鼠标不断点击触发、监听滚动事件。
实现 add(1)(2)(3)
函数柯里化概念:
柯里化(Currying)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。1)粗暴版
return function (c) { return a + b + c; } } } console.log(add(1)(2)(3)); // 6
2)柯里化解决方案
- 参数长度固定
return add(m + n); } temp.toString = function () { return m; } return temp; }; console.log(add(3)(4)(5)); // 12 console.log(add(3)(6)(9)(25)); // 43
对于add(3)(4)(5),其执行过程如下:
-
先执行add(3),此时m=3,并且返回temp函数;
-
执行temp(4),这个函数内执行add(m+n),n是此次传进来的数值4,m值还是上一步中的3,所以add(m+n)=add(3+4)=add(7),此时m=7,并且返回temp函数
-
执行temp(5),这个函数内执行add(m+n),n是此次传进来的数值5,m值还是上一步中的7,所以add(m+n)=add(7+5)=add(12),此时m=12,并且返回temp函数
-
由于后面没有传入参数,等于返回的temp函数不被执行而是打印,了解JS的朋友都知道对象的toString是修改对象转换字符串的方法,因此代码中temp函数的toString函数return
m值,而m值是最后一步执行函数时的值m=12,所以返回值是12。
- 参数长度不固定
//求和 return args.reduce((a, b) => a + b) } function currying (fn) { let args = [] return function temp (...newArgs) { if (newArgs.length) { args = [ ...args, ...newArgs ] return temp } else { let val = fn.apply(this, args) args = [] //保证再次调用时清空 return val } } } let addCurry = currying(add) console.log(addCurry(1)(2)(3)(4, 5)()) //15 console.log(addCurry(1)(2)(3, 4, 5)()) //15 console.log(addCurry(1)(2, 3, 4, 5)()) //15
字符串查找
请使用最基本的遍历来实现判断字符串 a 是否被包含在字符串 b 中,并返回第一次出现的位置(找不到返回 -1)。
-1 a='355';b='12354355'; // 返回 5 isContain(a,b);
if (a[0] === b[i]) { let tmp = true; for (let j in a) { if (a[j] !== b[~~i + ~~j]) { tmp = false; } } if (tmp) { return i; } } } return -1; }
将数字每千分位用逗号隔开
数字有小数版本:
let num = n.toString() // 转成字符串 let decimals = '' // 判断是否有小数 num.indexOf('.') > -1 ? decimals = num.split('.')[1] : decimals let len = num.length if (len <= 3) { return num } else { let temp = '' let remainder = len % 3 decimals ? temp = '.' + decimals : temp if (remainder > 0) { // 不是3的整数倍 return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',') + temp } else { // 是3的整数倍 return num.slice(0, len).match(/\d{3}/g).join(',') + temp } } } format(12323.33) // '12,323.33'
数字无小数版本:
let num = n.toString() let len = num.length if (len <= 3) { return num } else { let remainder = len % 3 if (remainder > 0) { // 不是3的整数倍 return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',') } else { // 是3的整数倍 return num.slice(0, len).match(/\d{3}/g).join(',') } } } format(1232323) // '1,232,323'
手写类型判断函数
(value === null) { return value + ""; } // 判断数据是引用类型的情况 if (typeof value === "object") { let valueClass = Object.prototype.toString.call(value), type = valueClass.split(" ")[1].split(""); type.pop(); return type.join("").toLowerCase(); } else { // 判断数据是基本数据类型的情况和函数的情况 return typeof value; } }
循环打印红黄绿
下面来看一道比较典型的问题,通过这个问题来对比几种异步编程方法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s
亮一次;如何让三个灯不断交替重复亮灯?三个亮灯函数:
console.log('red'); } function green() { console.log('green'); } function yellow() { console.log('yellow'); }
这道题复杂的地方在于需要“交替重复”亮灯,而不是“亮完一次”就结束了。
(1)用 callback 实现
setTimeout(() => { if (light === 'red') { red() } else if (light === 'green') { green() } else if (light === 'yellow') { yellow() } callback() }, timer) } task(3000, 'red', () => { task(2000, 'green', () => { task(1000, 'yellow', Function.prototype) }) })
这里存在一个 bug:代码只是完成了一次流程,执行后红黄绿灯分别只亮一次。该如何让它交替重复进行呢?
上面提到过递归,可以递归亮灯的一个周期:
task(3000, 'red', () => { task(2000, 'green', () => { task(1000, 'yellow', step) }) }) } step()
注意看黄灯亮的回调里又再次调用了 step 方法 以完成循环亮灯。
(2)用 promise 实现
new Promise((resolve, reject) => { setTimeout(() => { if (light === 'red') { red() } else if (light === 'green') { green() } else if (light === 'yellow') { yellow() } resolve() }, timer) }) const step = () => { task(3000, 'red') .then(() => task(2000, 'green')) .then(() => task(2100, 'yellow')) .then(step) } step()
这里将回调移除,在一次亮灯结束后,resolve 当前 promise,并依然使用递归进行。
(3)用 async/await 实现
await task(3000, 'red') await task(2000, 'green') await task(2100, 'yellow') taskRunner() } taskRunner()
实现发布-订阅模式
let handlers = {} // 2. 添加事件方法,参数:事件名 事件方法 addEventListener(type, handler) { // 创建新数组容器 if (!this.handlers[type]) { this.handlers[type] = [] } // 存入事件 this.handlers[type].push(handler) } // 3. 触发事件,参数:事件名 事件参数 dispatchEvent(type, params) { // 若没有注册该事件则抛出错误 if (!this.handlers[type]) { return new Error('该事件未注册') } // 触发事件 this.handlers[type].forEach(handler => { handler(...params) }) } // 4. 事件移除,参数:事件名 要删除事件,若无第二个参数则删除该事件的订阅和发布 removeEventListener(type, handler) { if (!this.handlers[type]) { return new Error('事件无效') } if (!handler) { // 移除事件 delete this.handlers[type] } else { const index = this.handlers[type].findIndex(el => el === handler) if (index === -1) { return new Error('无该绑定事件') } // 移除事件 this.handlers[type].splice(index, 1) if (this.handlers[type].length === 0) { delete this.handlers[type] } } } }
手写防抖函数
函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n
秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。null; return function() { let context = this, args = arguments; // 如果此时存在定时器的话,则取消之前的定时器重新记时 if (timer) { clearTimeout(timer); timer = null; } // 设置定时器,使事件间隔指定事件后执行 timer = setTimeout(() => { fn.apply(context, args); }, wait); }; }
图片懒加载
可以给img标签统一自定义属性
data-src='default.png'
,当检测到图片出现在窗口之后再补充src属性,此时才会进行图片资源加载。document.getElementsByTagName('img'); const len = imgs.length; // 视口的高度 const viewHeight = document.documentElement.clientHeight; // 滚动条高度 const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop; for (let i = 0; i < len; i++) { const offsetHeight = imgs[i].offsetTop; if (offsetHeight < viewHeight + scrollHeight) { const src = imgs[i].dataset.src; imgs[i].src = src; } } } // 可以使用节流优化一下 window.addEventListener('scroll', lazyload);
实现浅拷贝
浅拷贝是指,一个新的对象对原始对象的属性值进行精确地拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值,如果是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生变化。
(1)Object.assign()
Object.assign()
是ES6中对象的拷贝方法,接受的第一个参数是目标对象,其余参数是源对象,用法:Object.assign(target, source_1, ···)
,该方法可以实现浅拷贝,也可以实现一维对象的深拷贝。注意:
- 如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性。
- 如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回。
- 因为
null
和undefined
不能转化为对象,所以第一个参数不能为null
或undefined
,会报错。
= {c: 3}; Object.assign(target,object2,object3); console.log(target); // {a: 1, b: 2, c: 3}
(2)扩展运算符
使用扩展运算符可以在构造字面量对象的时候,进行属性的拷贝。语法:
let cloneObj = { ...obj };
= 2; console.log(obj1); //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}} obj1.b.c = 2; console.log(obj1); //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}
(3)数组方法实现数组浅拷贝
1)Array.prototype.slice
slice()
方法是JavaScript数组的一个方法,这个方法可以从已有数组中返回选定的元素:用法:array.slice(start, end)
,该方法不会改变原始数组。- 该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。
[1,2,3,4] console.log(arr.slice() === arr); //false
2)Array.prototype.concat
concat()
方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。- 该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。
[1,2,3,4] console.log(arr.concat() === arr); //false
(4)手写实现浅拷贝
function shallowCopy(object) { // 只拷贝对象 if (!object || typeof object !== "object") return; // 根据 object 的类型判断是新建一个数组还是对象 let newObject = Array.isArray(object) ? [] : {}; // 遍历 object,并且判断是 object 的属性才拷贝 for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = object[key]; } } return newObject; }// 浅拷贝的实现; function shallowCopy(object) { // 只拷贝对象 if (!object || typeof object !== "object") return; // 根据 object 的类型判断是新建一个数组还是对象 let newObject = Array.isArray(object) ? [] : {}; // 遍历 object,并且判断是 object 的属性才拷贝 for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = object[key]; } } return newObject; }// 浅拷贝的实现; function shallowCopy(object) { // 只拷贝对象 if (!object || typeof object !== "object") return; // 根据 object 的类型判断是新建一个数组还是对象 let newObject = Array.isArray(object) ? [] : {}; // 遍历 object,并且判断是 object 的属性才拷贝 for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = object[key]; } } return newObject; }
手写 Promise.race
该方法的参数是 Promise 实例数组, 然后其 then 注册的回调方法是数组中的某一个 Promise 的状态变为
fulfilled 的时候就执行. 因为 Promise 的状态只能改变一次, 那么我们只需要把 Promise.race
中产生的 Promise 对象的 resolve 方法, 注入到数组中的每一个 Promise 实例中的回调函数中即可.Promise((resolve, reject) => { for (let i = 0, len = args.length; i < len; i++) { args[i].then(resolve, reject) } }) }
实现prototype继承
所谓的原型链继承就是让新实例的原型等于父类的实例:
this.flag1 = flag1; } //子方法 function SubFunction(flag2){ this.flag2 = flag2; } //父实例 var superInstance = new SupperFunction(true); //子继承父 SubFunction.prototype = superInstance; //子实例 var subInstance = new SubFunction(false); //子调用自己和父的属性 subInstance.flag1; // true subInstance.flag2; // false
实现一个call
call做了什么:
- 将函数设为对象的属性
- 执行&删除这个函数
- 指定this到函数并传入给定参数执行函数
- 如果不传入参数,默认指向为 window
Function.prototype.myCall = function(context) { //此处没有考虑context非object情况 context.fn = this; let args = []; for (let i = 1, len = arguments.length; i < len; i++) { args.push(arguments[i]); } context.fn(...args); let result = context.fn(...args); delete context.fn; return result; };
实现节流函数(throttle)
防抖函数原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
// 手写简化版
flag = true; return (...args) => { if (!flag) return; flag = false; setTimeout(() => { fn.apply(this, args); flag = true; }, delay); }; };
适用场景:
- 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
- 缩放场景:监控浏览器resize
- 动画场景:避免短时间内多次触发动画引起性能问题
手写 Promise.all
1) 核心思路
- 接收一个 Promise 实例的数组或具有 Iterator 接口的对象作为参数
- 这个方法返回一个新的 promise 对象,
- 遍历传入的参数,用Promise.resolve()将参数"包一层",使其变成一个promise对象
- 参数所有回调成功才是成功,返回值数组与参数顺序一致
- 参数数组其中一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息作为 Promise.all 的错误信息。
2)实现代码
一般来说,Promise.all
用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了Promise(function(resolve, reject) { if(!Array.isArray(promises)){ throw new TypeError(`argument must be a array`) } var resolvedCounter = 0; var promiseNum = promises.length; var resolvedResult = []; for (let i = 0; i < promiseNum; i++) { Promise.resolve(promises[i]).then(value=>{ resolvedCounter++; resolvedResult[i] = value; if (resolvedCounter == promiseNum) { return resolve(resolvedResult) } },error=>{ return reject(error) }) } }) } // test let p1 = new Promise(function (resolve, reject) { setTimeout(function () { resolve(1) }, 1000) }) let p2 = new Promise(function (resolve, reject) { setTimeout(function () { resolve(2) }, 2000) }) let p3 = new Promise(function (resolve, reject) { setTimeout(function () { resolve(3) }, 3000) }) promiseAll([p3, p1, p2]).then(res => { console.log(res) // [3, 1, 2] })
前端js手写面试题汇总(二)
最新推荐文章于 2024-06-17 12:40:18 发布
本文汇集了前端面试中常见的手写JavaScript题目,包括防抖函数、节流函数的实现及其应用场景,还涉及模板引擎、日期格式化、bind方法、柯里化、类型判断、异步编程方法对比、发布-订阅模式、浅拷贝、Promise相关方法及原型继承等知识点,是JS开发者进阶的宝贵资料。
摘要由CSDN通过智能技术生成