前端科举八股文-手撕代码篇
手撕排序算法
选择排序
const originalList = [3, 5, 1, 2, 4, 9, 6, 8, 7]
const selectSort = (list) => {
for (let i = 0; i < list.length - 1; i++) {
// 初始化最大值为每次循环的第一个数
let max = list[i]
// 初始化最大值下标当前索引
let maxIndex = i;
for(let j = i+1;j< list.length;j++){
if(list[j] > max){
// 有最大值更新最大值和最大值索引
max = list[j];
maxIndex = j;
}
}
// 把当前循环第一个值和最大值调换位置
const currentNumber = list[i]
list[i] = list[maxIndex]
list[maxIndex] = currentNumber;
}
}
selectSort(originalList)
console.log(originalList)
思路
基本思路,嵌套遍历,每次循环都找出最大值或者最小值与当前遍历的第一个交换
菜鸟讲的比我讲的好
冒泡排序
const originalList = [3, 5, 1, 2, 4, 9, 6, 8, 7]
const popSort = (list) => {
for (let i = 0; i < list.length - 1; i++) {
for (let j = 0 ; j < list.length - 1 - i; j++) {
if (list[j] > list[j + 1]) {
// 每次循环只拿相邻值做比较并且交换
const flag = list[j + 1]
list[j + 1] = list[j];
list[j] = flag;
}
}
}
}
popSort(originalList)
console.log(originalList)
思路
基本思路,还是嵌套遍历,不过比选择排序先进一点,每次遍历不止交换一次,而是相邻的值去比较与交换。
菜鸟讲的还是比我好
这里声明一下里面循环的次数是list.length - 1 - i
,因为每次进循环时本质上最后i个元素已经排序完成了,写成list.length - 1 - i
可以提高算法执行效率
快速排序
const originalList = [3, 5, 1, 2, 4, 9, 6, 8, 7]
const quickSort = (list) => {
if (list.length <= 1) {
return list
}
// 选一个基准值 (随便选)
const middleNumber = list[list.length - 1]
// 找出比基准值小的数组
const leftList = list.filter(item => item < middleNumber)
// 找出比基准值大的数组
const rightList = list.filter(item => item > middleNumber)
// console.log('right', rightList)
return [...quickSort(leftList), middleNumber, ...quickSort(rightList)]
// quickSort([...leftList, middleNumber, ...rightList])
}
console.log(quickSort(originalList))
思路
用到了递归的思路,每次找一个基准值,然后区分出比基准值小的数组,再找出比基准值大的数组,最后递归使用快排来排序小值数组和大值数组,可能在人类理解中比较抽象,但是在计算机中可以更快的得出排序结果
看看动图呗
手撕bind方法
// 只考虑了bind方法改变this的情况 不考虑其他复杂情况
Function.prototype.myBind = function (context, ...args){
const _this = this;
return () => {
_this.call(context,...args)
}
}
思路解析
我们将方法挂载到函数原型上是为了让自定义的bind方法的调用方式和原生bind方法的调用方式一模一样,即function.bind(context)
,其内部实现,const _this = this
其实是为了拿到真实调用的函数,因为js中的函数调用有个潜规则 函数的this,永远都是指向函数的调用者 那么我们如果通过function.bind(context)
调用时,_this
就可以拿到真实的调用者。然后因为bind方法返回的是一个函数,所以我们内部实现也会返回一个函数,在函数内部
手撕js的继承方案
构造继承
// es6之前的继承方式
function Person(name) {
this.name = name
}
function Student(name, age) {
Person.call(this, name)
this.age = age
}
const student = new Student('wxs', 26)
console.log(student)
实现原理
构造继承的本质就是在子类中去调用父类的构造函数,但是这种继承方式有一个很明显的缺陷,和他的继承方式有关,可以从他的本质上是把父类的this指到了子类里面,但是由于并没有生成父类的实例,实际上是无法继承父类原型上的属性或者方法的(因为只有实例才能继承构造函数原型上的属性和方法)。直接看🌰
function Person(name) {
this.name = name
}
Person.prototype.basic = {
eyes: '两只',
nose: '一个',
mouth: '一张',
ears: '两个',
}
function Student(name, age) {
Person.call(this, name)
this.age = age
}
const student_1 = new Student('wxs', 26)
console.log(student_1.basic) // undefined
原型继承
那么为了继承原型上的属性或者方法,我们就要用到原型继承了
function Person(name) {
this.name = name
}
Person.prototype.basic = {
eyes: '两只',
nose: '一个',
mouth: '一张',
ears: '两个',
}
Student.prototype = new Person()
function Student(name, age) {
this.age = age
}
实现原理
原型继承的实现原理实际上是生成一个父类的实例,然后赋值给子类的原型,那么子类的实例就会同时有子类实例属性/方法,子类原型属性/方法,父类实例属性/方法,父类原型属性/方法。但是这个的缺陷也十分之明显,最明显的就是子类构造函数中的name形参无法传到父类中了!第二个,可以看到由于子类继承的是同一个父类原型实例,所以其实所有的子类实例,会共享父类中的引用对象类型数据,在改变时也会造成同步污染。举个🌰
function Person(name) {
this.name = name
}
Person.prototype.basic = {
eyes: '两只',
nose: '一个',
mouth: '一张',
ears: '两个',
}
Student.prototype = new Person()
function Student(name, age) {
this.age = age
}
const student_1 = new Student('wxs', 26)
const student_2 = new Student('someBody', 27)
// 学生1只有一只耳朵
student_1.basic.ears = '一只'
console.log('学生2有几只耳朵', student_2.basic.ears) // 一只
组合继承
function Person(name) {
this.name = name
}
Person.prototype.basic = {
eyes: '两只',
nose: '一个',
mouth: '一张',
ears: '两个',
}
Student.prototype = new Person()
function Student(name, age) {
Person.call(this, name)
this.age = age
}
实现原理
实现原理其实没什么好说的,因为原型继承不能穿参,构造继承不能继承原型上的属性和方法,所以把两者结合起来,但是这种方法也有缺陷,可以看到new Person()
调用了一次父类构造函数,Person.call又调用了一次,一次继承就调用了两次无疑是造成了很大了资源消耗,所以我们来看看终极解决方案—寄生组合继承
寄生组合继承
function extend(son,father){
const midPrototype = Object.create(father.prototype)
midPrototype.constructor = son
son.prototype = midPrototype
}
实现原理
寄生组合继承的终极目的实际上就是为了弥补上述继承方式的不足(父类构造函数多构造了一次、不能继承父类原型上的属性和方法)执行了上述extend方法之后父类子类的原型链关系就会变成 son.prototype === midPrototype
,而midPrototype
又是 father.prototype
通过Object.create方法新建的对象,
所以说midPrototype
是以 father.prototype
为原型,所以可以继承father.prototype
原型上的属性或者方法,同时也避免了重复执行父类构造函数!
手撕instanceOf
function myInstanceOf(targetObejct, constructFn) {
let targetObejctProto = targetObejct.__proto__
do {
if (targetObejctProto === constructFn.prototype) {
return true
}
targetObejctProto = targetObejctProto.__proto__
} while (targetObejctProto)
return false
}
实现原理
要实现instanceof,首先要知道这个api是干什么,根据MDN官方解释
所以要实现起来就很简单了,我们首先拿出实例对象取出实例对象的原型,看看是否指向构造函数的原型,如果不等于我们要递归的一直取【原型】的【原型】,一直取到Object.__proto__
也就是undefined为止。
手撕new 操作符
function myNew(contructor, ...args) {
let obj = new Object()
contructor.call(obj, ...args)
obj.constructor = contructor
obj.__proto__ = constructor.prototype
return obj;
}
手撕防抖
首先我们来看看没有做防抖的情况,比如有一个场景需要在用户下拉的时候获取最新数据。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.body{
height: 3000px;
}
</style>
</head>
<body>
<div class="body">123</div>
<script>
const scrollHandle = () => {
console.log('开始发送ajax请求获取最新的数据')
}
window.addEventListener('scroll',scrollHandle)
</script>
</body>
</html>
实现原理
由于是高频次触发事件,所以在下拉事件中,scrollhandle
事件被多次触发,而由此带来的性能开销也是十分巨大的。而仔细分析后,我们往往只需要在用户停止操作的时候触发一次请求加载动作即可。正好满足防抖的设计思想将多次操作合并成一次接下来看看优化后的代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.body {
height: 3000px;
}
</style>
</head>
<body>
<div class="body">123</div>
<script>
const scrollHandle = () => {
console.log('开始发送ajax请求获取最新的数据')
}
const throttle = (fn, timeOut = 500) => {
return () => {
clearTimeout(fn.tId)
fn.tId = setTimeout(() => {
fn()
}, timeOut);
}
}
window.addEventListener('scroll', throttle(scrollHandle))
</script>
</body>
</html>
手撕节流
const debounce = (fn, timeOut = 1000) => {
return () => {
if (fn.tid) {
return
}
fn.tid = setTimeout(() => {
fn.tid = null
clearTimeout(fn.tid)
fn()
}, timeOut);
}
}
实现原理
其实就是在防抖的基础上加了一个定时触发的逻辑,只要tid还在,就直接return,如果不存在,就重设tid为定时器的唯一id,在定时器的回调里面去清空这个值。
手撕深拷贝
const deepClone = (targetObj) => {
let resultObj = null
if (typeof targetObj !== "object") {
// 基础类型
resultObj = targetObj
return resultObj
} else {
if (Array.isArray(targetObj)) {
// 数组对象
resultObj = []
for (let i = 0; i < targetObj.length; i++) {
resultObj[i] = targetObj[i]
}
return resultObj
} else {
resultObj = {}
// 对象类型
Object.entries(targetObj).forEach(([key, value]) => {
if (typeof value === "object") {
resultObj[key] = deepClone(value)
} else {
resultObj[key] = value
}
})
return targetObj
}
}
}
直接调用api实现深拷贝
const b = JSON.parse(JSON.stringify(originalObj))
但是由于json对象是没有函数和空对象的概念,所以当原对象的value中有null和函数时会报错
手撕Promise
嘿嘿 到这里也算是半个小中级前端CV工程师了,都要手写ES6API了。
// 首先来看看ES6标准的Promise是如何调用的
// const P_1 = new Promise((resolve, reject) => {
// const result = Math.random()
// if (result > 0.5) {
// resolve('成功了')
// } else {
// reject('失败了')
// }
// })
// console.log(P_1)
class WxsPromise {
// 缓存当前promise实例状态 promise状态只有三种 pending resolve reject
promiseState = 'pending'
// 缓存promise的结果值
promiseResult
// 内置resolve函数
static resolve(successResult) {
this.promiseState = 'resolve'
this.promiseResult = successResult
}
// 内置reject函数
static reject(errorResult) {
this.promiseState = 'reject'
this.promiseResult = errorResult
}
constructor(func) {
func(WxsPromise.resolve.bind(this), WxsPromise.reject.bind(this))
}
}
const P_2 = new WxsPromise((resolve, reject) => {
const result = Math.random()
if (result > 0.5) {
resolve('成功了')
} else {
reject('失败了')
}
})
实现原理
这里代码前半部分是ES6标准的promise调用方式,可以看到Promise是通过new调用的,所以我们自己实现第一步就是新建一个Class类,promise接受一个函数作为参数,所以我们自定义类的construcor
的形参也是一个func,由于Promise里传入的函数是同步执行的(这里不理解的话可以去了解EventLoop模型),所以在construcor
中传入形参后就立即调用,后续部分就是promise的关键点了,resolve和reject,resolve和reject都是Promise的静态方法(所谓静态方法就是只能通过构造函数本身去调用),所以我们定义的resolve
和reject
方法都有static前缀,在func调用时传入这两个静态方法,但是我们通过打印es6的promise实例可以得出,每个实例都有自己的promiseState
和promiseResult
,所以在两个实例方法中又需要改变每个实例中的promiseState
和promiseResult
,那么在传入这个静态方法时就使用到了bind
方法去改变实例方法的this指向,指向到了每一个promise实例上。
手撕promise.then方法
老规矩,先来看看原生promise.then的调用方法
// 首先来看看ES6标准的Promise是如何调用的
const P_1 = new Promise((resolve, reject) => {
const result = Math.random()
if (result > 0.5) {
resolve('成功了')
} else {
reject('失败了')
}
})
// 看看标准的then方法调用结果
P_1.then(res => {
console.log('成功回调', res)
}, (errResult) => {
console.log('失败回调', errResult)
})
很明显,我们需要在自定义实例上挂载一个then方法,接着上次的写
class WxsPromise {
// 缓存当前promise实例状态 promise状态只有三种 pending resolve reject
promiseState = 'pending'
// 缓存promise的结果值
promiseResult
// 内置resolve函数
static resolve(successResult) {
this.promiseState = 'resolve'
this.promiseResult = successResult
}
// 内置reject函数
static reject(errorResult) {
this.promiseState = 'reject'
this.promiseResult = errorResult
}
constructor(func) {
func(WxsPromise.resolve.bind(this), WxsPromise.reject.bind(this))
}
then(successCallback, failCallback) {
// promise执行成功
if (this.promiseState === 'resolve') {
successCallback(this.promiseResult)
}
if (this.promiseState === 'reject') {
failCallback(this.promiseResult)
}
}
}
实现原理
如果掌握了上一小节后,promise.then方法实现起来确实不是什么问题,then方法接受两个回调函数作为参数,执行成功执行第一个回调,执行失败执行第二个回调。那么实现思路就直接在类里面定义then方法接受两个参数,并且根据当前实例的promiseState来执行对应的回调即可。
惊天反转!!!
莫要以为这样就实现了promise.then方法,因为promise是为了处理异步函数而诞生的标准API,我们把promise内部的函数进行改写一下。
const P_1 = new Promise((resolve, reject) => {
const result = Math.random()
if (result > 0.5) {
setTimeout(() => {
resolve('成功了')
}, 500);
} else {
setTimeout(() => {
reject('失败了')
}, 500);
}
})
P_1.then(res => {
console.log('成功回调', res)
}, (errResult) => {
console.log('失败回调', errResult)
})
此时P_1.then方法还是能正常调用的,但是如果把自定义promise里面的函数换成了异步之后再调用then方法,那么此时的控制台是没有任何输出的。
// 缓存当前promise实例状态 promise状态只有三种 pending resolve reject
promiseState = 'pending'
// 缓存promise的结果值
promiseResult
// 内置resolve函数
static resolve(successResult) {
this.promiseState = 'resolve'
this.promiseResult = successResult
}
// 内置reject函数
static reject(errorResult) {
this.promiseState = 'reject'
this.promiseResult = errorResult
}
constructor(func) {
func(WxsPromise.resolve.bind(this), WxsPromise.reject.bind(this))
}
then(successCallback, failCallback) {
// promise执行成功
if (this.promiseState === 'resolve') {
successCallback(this.promiseResult)
}
// promise执行失败
if (this.promiseState === 'reject') {
failCallback(this.promiseResult)
}
}
}
const P_2 = new WxsPromise((resolve, reject) => {
const result = Math.random()
if (result > 0.5) {
setTimeout(() => {
resolve('成功了')
}, 500);
} else {
setTimeout(() => {
reject('失败了')
}, 500);
}
})
// console.log(P_2)
P_2.then(res => {
console.log('成功回调', res)
}, (errResult) => {
console.log('失败回调', errResult)
})
没有输出的原因也很简单,由于promise里面执行的是异步方法,在then方法内部执行时判断当前promiseState的状态时异步方法还没有调用,所以promiseState的值还没有改变,所以针对promiseState的判断逻辑不会执行
所以针对异步情况下对promise做一些改动
class WxsPromise {
// 缓存当前promise实例状态 promise状态只有三种 pending resolve reject
promiseState = 'pending'
// 缓存promise的结果值
promiseResult
// 内置resolve函数
static resolve(successResult) {
this.promiseState = 'resolve'
this.promiseResult = successResult
this.successCallback && this.successCallback(this.promiseResult)
}
// 内置reject函数
static reject(errorResult) {
this.promiseState = 'reject'
this.promiseResult = errorResult
this.failCallback && this.failCallback(this.promiseResult)
}
constructor(func) {
func(WxsPromise.resolve.bind(this), WxsPromise.reject.bind(this))
}
then(successCallback, failCallback) {
// promise执行成功
if (this.promiseState === 'resolve') {
successCallback(this.promiseResult)
}
if (this.promiseState === 'reject') {
failCallback(this.promiseResult)
}
if(this.promiseState === 'pending'){
// 还没执行完 将函数缓存起来,待异步函数执行完成再调用
this.successCallback = successCallback
this.failCallback = failCallback
}
}
}
手撕promise.all方法
首先我们知道promise.all方法返回的也是是一个promise实例,如果all参数中的promise数组悉数调用成功,那么.then方法返回的是参数中所有所有promise数组返回结果构成的数组,如果调用失败,那么返回的便是第一个调用失败的结果,看🌰
// 首先来看看原生promise.all方法如何调用
const promiseConstructor = (promiseResult, timeOut, promiseState = 'resolve') => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (promiseState === 'resolve') {
resolve(promiseResult)
}
if (promiseState === 'reject') {
reject(promiseResult)
}
}, timeOut)
})
}
Promise.all([promiseConstructor('第一个成功回调', 2000), promiseConstructor('第二个成功回调', 1000), promiseConstructor('第三个成功回调', 3000)]).then(resList => {
console.log("看一下打印res", resList)
})
Promise.all([promiseConstructor('第一个成功回调', 2000), promiseConstructor('第二个失败回调', 1000, 'reject'), promiseConstructor('第三个成功回调', 3000)]).then(resList => {
console.log("看一下打印res", resList)
}, (errResult) => {
console.log('调用失败啦', errResult)
})
上方代码可以copy运行,也可以直接看看我的返回截图
知道参数,知道运行结果之后,要实现all方法就不难了。
const wxsPromiseAll = (promiseList) => {
return new Promise((resolve, reject) => {
const resultList = []
let dealPromiseCount = 0
promiseList.forEach((promiseItem, promiseIndex) => {
promiseItem.then(res => {
resultList[promiseIndex] = res
dealPromiseCount++;
if (dealPromiseCount === promiseList.length) {
resolve(resultList)
}
},(err) => {
reject(err)
})
})
})
}