阿里前端二面经典手写面试题汇总
实现类的继承
实现类的继承-简版
类的继承在几年前是重点内容,有n种继承方式各有优劣,es6普及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深入理解的去看红宝书即可,我们目前只实现一种最理想的继承方式。
// 寄生组合继承
function Parent(name) {this.name = name
}
Parent.prototype.say = function() {console.log(this.name + ` say`);
}
Parent.prototype.play = function() {console.log(this.name + ` play`);
}function Child(name, parent) {// 将父类的构造函数绑定在子类上Parent.call(this, parent)this.name = name
}/** 1. 这一步不用Child.prototype = Parent.prototype的原因是怕共享内存,修改父类原型对象就会影响子类2. 不用Child.prototype = new Parent()的原因是会调用2次父类的构造方法(另一次是call),会存在一份多余的父类实例属性
3. Object.create是创建了父类原型的副本,与父类原型完全隔离
*/
Child.prototype = Object.create(Parent.prototype);
Child.prototype.say = function() {console.log(this.name + ` say`);
}// 注意记得把子类的构造指向子类本身
Child.prototype.constructor = Child;
// 测试
var parent = new Parent('parent');
parent.say() var child = new Child('child');
child.say()
child.play(); // 继承父类的方法
ES5实现继承-详细
第一种方式是借助call实现继承
function Parent1(){this.name = 'parent1';
}
function Child1(){Parent1.call(this);this.type = 'child1'
}
console.log(new Child1);
这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类中一旦存在方法那么子类无法继承。那么引出下面的方法
第二种方式借助原型链实现继承:
function Parent2() {this.name = 'parent2';this.play = [1, 2, 3]}function Child2() {this.type = 'child2';}Child2.prototype = new Parent2();console.log(new Child2());
看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的不足。举个例子:
var s1 = new Child2();var s2 = new Child2();s1.play.push(4);console.log(s1.play, s2.play); // [1,2,3,4] [1,2,3,4]
明明我只改变了s1的play属性,为什么s2也跟着变了呢?很简单,因为两个实例使用的是同一个原型对象
第三种方式:将前两种组合:
function Parent3 () {this.name = 'parent3';this.play = [1, 2, 3];}function Child3() {Parent3.call(this);this.type = 'child3';}Child3.prototype = new Parent3();var s3 = new Child3();var s4 = new Child3();s3.play.push(4);console.log(s3.play, s4.play); // [1,2,3,4] [1,2,3]
之前的问题都得以解决。但是这里又徒增了一个新问题,那就是Parent3的构造函数会多执行了一次(
Child3.prototype = new Parent3()
;)。这是我们不愿看到的。那么如何解决这个问题?
第四种方式: 组合继承的优化1
function Parent4 () {this.name = 'parent4';this.play = [1, 2, 3];}function Child4() {Parent4.call(this);this.type = 'child4';}Child4.prototype = Parent4.prototype;
这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问,但是我们来测试一下
var s3 = new Child4();var s4 = new Child4();console.log(s3)
子类实例的构造函数是Parent4,显然这是不对的,应该是Child4。
第五种方式(最推荐使用):优化2
function Parent5 () {this.name = 'parent5';this.play = [1, 2, 3];}function Child5() {Parent5.call(this);this.type = 'child5';}Child5.prototype = Object.create(Parent5.prototype);Child5.prototype.constructor = Child5;
这是最推荐的一种方式,接近完美的继承。
实现instanceOf
思路:
- 步骤1:先取得当前类的原型,当前实例对象的原型链
- 步骤2:一直循环(执行原型链的查找机制)
- 取得当前实例对象原型链的原型链(
proto = proto.__proto__
,沿着原型链一直向上查找) - 如果 当前实例的原型链
__proto__
上找到了当前类的原型prototype
,则返回true
- 如果 一直找到
Object.prototype.__proto__ == null
,Object
的基类(null
)上面都没找到,则返回false
- 取得当前实例对象原型链的原型链(
// 实例.__ptoto__ === 类.prototype
function _instanceof(example, classFunc) {// 由于instance要检测的是某对象,需要有一个前置判断条件//基本数据类型直接返回falseif(typeof example !== 'object' || example === null) return false;let proto = Object.getPrototypeOf(example);while(true) {if(proto == null) return false;// 在当前实例对象的原型链上,找到了当前类if(proto == classFunc.prototype) return true;// 沿着原型链__ptoto__一层一层向上查proto = Object.getPrototypeof(proto); // 等于proto.__ptoto__}
}console.log('test', _instanceof(null, Array)) // false
console.log('test', _instanceof([], Array)) // true
console.log('test', _instanceof('', Array)) // false
console.log('test', _instanceof({}, Object)) // true
实现一个队列
基于链表结构实现队列
const LinkedList = require('./实现一个链表结构')// 用链表默认使用数组来模拟队列,性能更佳
class Queue {constructor() {this.ll = new LinkedList()}// 向队列中添加offer(elem) {this.ll.add(elem)}// 查看第一个peek() {return this.ll.get(0)}// 队列只能从头部删除remove() {return this.ll.remove(0)}
}var queue = new Queue()queue.offer(1)
queue.offer(2)
queue.offer(3)
var removeVal = queue.remove(3)console.log(queue.ll,'queue.ll')
console.log(removeVal,'queue.remove')
console.log(queue.peek(),'queue.peek')
实现every方法
Array.prototype.myEvery=function(callback, context = window){var len=this.length,flag=true,i = 0;for(;i < len; i++){if(!callback.apply(context,[this[i], i , this])){flag=false;break;} }return flag;}// var obj = {num: 1}// var aa=arr.myEvery(function(v,index,arr){// return v.num>=12;// },obj)// console.log(aa)
实现LRU淘汰算法
LRU
缓存算法是一个非常经典的算法,在很多面试中经常问道,不仅仅包括前端面试
LRU
英文全称是Least Recently Used
,英译过来就是” 最近最少使用 “的意思。LRU
是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间t
,当须淘汰一个页面时,选择现有页面中其t
值最大的,即最近最少使用的页面予以淘汰
通俗的解释:
假如我们有一块内存,专门用来缓存我们最近发访问的网页,访问一个新网页,我们就会往内存中添加一个网页地址,随着网页的不断增加,内存存满了,这个时候我们就需要考虑删除一些网页了。这个时候我们找到内存中最早访问的那个网页地址,然后把它删掉。这一整个过程就可以称之为
LRU
算法
上图就很好的解释了 LRU
算法在干嘛了,其实非常简单,无非就是我们往内存里面添加或者删除元素的时候,遵循最近最少使用原则
使用场景
LRU
算法使用的场景非常多,这里简单举几个例子即可:
- 我们操作系统底层的内存管理,其中就包括有
LRU
算法 - 我们常见的缓存服务,比如
redis
等等 - 比如浏览器的最近浏览记录存储
vue
中的keep-alive
组件使用了LRU
算法
梳理实现 LRU 思路
- 特点分析:
- 我们需要一块有限的存储空间,因为无限的化就没必要使用
LRU
算发删除数据了。 - 我们这块存储空间里面存储的数据需要是有序的,因为我们必须要顺序来删除数据,所以可以考虑使用
Array
、Map
数据结构来存储,不能使用Object
,因为它是无序的。 - 我们能够删除或者添加以及获取到这块存储空间中的指定数据。
- 存储空间存满之后,在添加数据时,会自动删除时间最久远的那条数据。
- 我们需要一块有限的存储空间,因为无限的化就没必要使用
- 实现需求:
- 实现一个
LRUCache
类型,用来充当存储空间 - 采用
Map
数据结构存储数据,因为它的存取时间复杂度为O(1)
,数组为O(n)
- 实现
get
和set
方法,用来获取和添加数据 - 我们的存储空间有长度限制,所以无需提供删除方法,存储满之后,自动删除最久远的那条数据
- 当使用
get
获取数据后,该条数据需要更新到最前面
- 实现一个
具体实现
class LRUCache {constructor(length) {this.length = length; // 存储长度this.data = new Map(); // 存储数据}// 存储数据,通过键值对的方式set(key, value) {const data = this.data;if (data.has(key)) {data.delete(key)}data.set(key, value);// 如果超出了容量,则需要删除最久的数据if (data.size > this.length) {const delKey = data.keys().next().value;data.delete(delKey);}}// 获取数据get(key) {const data = this.data;// 未找到if (!data.has(key)) {return null;}const value = data.get(key); // 获取元素data.delete(key); // 删除元素data.set(key, value); // 重新插入元素return value // 返回获取的值}
}
var lruCache = new LRUCache(5);
set 方法
:往map
里面添加新数据,如果添加的数据存在了,则先删除该条数据,然后再添加。如果添加数据后超长了,则需要删除最久远的一条数据。data.keys().next().value
便是获取最后一条数据的意思。get 方法
:首先从map
对象中拿出该条数据,然后删除该条数据,最后再重新插入该条数据,确保将该条数据移动到最前面
// 测试// 存储数据 set:lruCache.set('name', 'test');
lruCache.set('age', 10);
lruCache.set('sex', '男');
lruCache.set('height', 180);
lruCache.set('weight', '120');
console.log(lruCache);
继续插入数据,此时会超长,代码如下:
lruCache.set('grade', '100');
console.log(lruCache);
此时我们发现存储时间最久的 name 已经被移除了,新插入的数据变为了最前面的一个。
我们使用 get
获取数据,代码如下:
我们发现此时 sex
字段已经跑到最前面去了
总结
LRU
算法其实逻辑非常的简单,明白了原理之后实现起来非常的简单。最主要的是我们需要使用什么数据结构来存储数据,因为map
的存取非常快,所以我们采用了它,当然数组其实也可以实现的。还有一些小伙伴使用链表来实现LRU
,这当然也是可以的。
实现Promise相关方法
实现Promise的resolve
实现 resolve 静态方法有三个要点:
- 传参为一个
Promise
, 则直接返回它。 - 传参为一个
thenable
对象,返回的Promise
会跟随这个对象,采用它的最终状态作为自己的状态。 - 其他情况,直接返回以该值为成功状态的
promise
对象。
Promise.resolve = (param) => {if(param instanceof Promise) return param;return new Promise((resolve, reject) => {if(param && param.then && typeof param.then === 'function') {// param 状态变为成功会调用resolve,将新 Promise 的状态变为成功,反之亦然param.then(resolve, reject);}else {resolve(param);}})
}
实现 Promise.reject
Promise.reject 中传入的参数会作为一个 reason 原封不动地往下传, 实现如下:
Promise.reject = function (reason) {return new Promise((resolve, reject) => {reject(reason);});
}
实现 Promise.prototype.finally
前面的
promise
不管成功还是失败,都会走到finally
中,并且finally
之后,还可以继续then
(说明它还是一个then方法是关键),并且会将初始的promise
值原封不动的传递给后面的then
.
Promise.prototype.finally最大的作用
finally
里的函数,无论如何都会执行,并会把前面的值原封不动传递给下一个then
方法中- 如果
finally
函数中有promise
等异步任务,会等它们全部执行完毕,再结合之前的成功与否状态,返回值
Promise.prototype.finally六大情况用法
// 情况1
Promise.resolve(123).finally((data) => { // 这里传入的函数,无论如何都会执行console.log(data); // undefined
})// 情况2 (这里,finally方法相当于做了中间处理,起一个过渡的作用)
Promise.resolve(123).finally((data) => {console.log(data); // undefined
}).then(data => {console.log(data); // 123
})// 情况3 (这里只要reject,都会走到下一个then的err中)
Promise.reject(123).finally((data) => {console.log(data); // undefined
}).then(data => {console.log(data);
}, err => {console.log(err, 'err'); // 123 err
})// 情况4 (一开始就成功之后,会等待finally里的promise执行完毕后,再把前面的data传递到下一个then中)
Promise.resolve(123).finally((data) => {console.log(data); // undefinedreturn new Promise((resolve, reject) => {setTimeout(() => {resolve('ok');}, 3000)})
}).then(data => {console.log(data, 'success'); // 123 success
}, err => {console.log(err, 'err');
})// 情况5 (虽然一开始成功,但是只要finally函数中的promise失败了,就会把其失败的值传递到下一个then的err中)
Promise.resolve(123).finally((data) => {console.log(data); // undefinedreturn new Promise((resolve, reject) => {setTimeout(() => {reject('rejected');}, 3000)})
}).then(data => {console.log(data, 'success');
}, err => {console.log(err, 'err'); // rejected err
})// 情况6 (虽然一开始失败,但是也要等finally中的promise执行完,才能把一开始的err传递到err的回调中)
Promise.reject(123).finally((data) => {console.log(data); // undefinedreturn new Promise((resolve, reject) => {setTimeout(() => {resolve('resolve');}, 3000)})
}).then(data => {console.log(data, 'success');
}, err => {console.log(err, 'err'); // 123 err
})
源码实现
Promise.prototype.finally = function (callback) {return this.then((data) => {// 让函数执行 内部会调用方法,如果方法是promise,需要等待它完成// 如果当前promise执行时失败了,会把err传递到,err的回调函数中return Promise.resolve(callback()).then(() => data); // data 上一个promise的成功态}, err => {return Promise.resolve(callback()).then(() => {throw err; // 把之前的失败的err,抛出去});})
}
实现 Promise.all
对于 all 方法而言,需要完成下面的核心功能:
- 传入参数为一个空的可迭代对象,则直接进行
resolve
。 - 如果参数中有一个
promise
失败,那么Promise.all
返回的promise
对象失败。 - 在任何情况下,
Promise.all
返回的promise
的完成状态的结果都是一个数组
Promise.all = function(promises) {return new Promise((resolve, reject) => {let result = [];let index = 0;let len = promises.length;if(len === 0) {resolve(result);return;}for(let i = 0; i < len; i++) {// 为什么不直接 promise[i].then, 因为promise[i]可能不是一个promisePromise.resolve(promise[i]).then(data => {result[i] = data;index++;if(index === len) resolve(result);}).catch(err => {reject(err);})}})
}
实现promise.allsettle
MDN:
Promise.allSettled()
方法返回一个在所有给定的promise
都已经
fulfilled或
rejected后的
promise,并带有一个对象数组,每个对象表示对应的
promise`结果
当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个promise
的结果时,通常使用它。
【译】
Promise.allSettled
跟Promise.all
类似, 其参数接受一个Promise
的数组, 返回一个新的Promise
, 唯一的不同在于, 其不会进行短路, 也就是说当Promise全部处理完成后我们可以拿到每个Promise
的状态, 而不管其是否处理成功。
用法 | 测试用例
let fs = require('fs').promises;let getName = fs.readFile('./name.txt', 'utf8'); // 读取文件成功
let getAge = fs.readFile('./age.txt', 'utf8');Promise.allSettled([1, getName, getAge, 2]).then(data => {console.log(data);
});
// 输出结果
/*[{ status: 'fulfilled', value: 1 },{ status: 'fulfilled', value: 'zf' },{ status: 'fulfilled', value: '11' },{ status: 'fulfilled', value: 2 }]
*/let getName = fs.readFile('./name123.txt', 'utf8'); // 读取文件失败
let getAge = fs.readFile('./age.txt', 'utf8');
// 输出结果
/*[{ status: 'fulfilled', value: 1 },{status: 'rejected',value: [Error: ENOENT: no such file or directory, open './name123.txt'] {errno: -2,code: 'ENOENT',syscall: 'open',path: './name123.txt'}},{ status: 'fulfilled', value: '11' },{ status: 'fulfilled', value: 2 }]
*/
实现
function isPromise (val) {return typeof val.then === 'function'; // (123).then => undefined
}Promise.allSettled = function(promises) {return new Promise((resolve, reject) => {let arr = [];let times = 0;const setData = (index, data) => {arr[index] = data;if (++times === promises.length) {resolve(arr);}console.log('times', times)}for (let i = 0; i < promises.length; i++) {let current = promises[i];if (isPromise(current)) {current.then((data) => {setData(i, { status: 'fulfilled', value: data });}, err => {setData(i, { status: 'rejected', value: err })})} else {setData(i, { status: 'fulfilled', value: current })}}})
}
实现 Promise.race
race 的实现相比之下就简单一些,只要有一个 promise 执行完,直接 resolve 并停止执行
Promise.race = function(promises) {return new Promise((resolve, reject) => {let len = promises.length;if(len === 0) return;for(let i = 0; i < len; i++) {Promise.resolve(promise[i]).then(data => {resolve(data);return;}).catch(err => {reject(err);return;})}})
}
实现一个简版Promise
// 使用
var promise = new Promise((resolve,reject) => {if (操作成功) {resolve(value)} else {reject(error)}
})
promise.then(function (value) {// success
},function (value) {// failure
})
function myPromise(constructor) {let self = this;self.status = "pending" // 定义状态改变前的初始状态self.value = undefined; // 定义状态为resolved的时候的状态self.reason = undefined; // 定义状态为rejected的时候的状态function resolve(value) {if(self.status === "pending") {self.value = value;self.status = "resolved";}}function reject(reason) {if(self.status === "pending") {self.reason = reason;self.status = "rejected";}}// 捕获构造异常try {constructor(resolve,reject);} catch(e) {reject(e);}
}
// 添加 then 方法
myPromise.prototype.then = function(onFullfilled,onRejected) {let self = this;switch(self.status) {case "resolved":onFullfilled(self.value);break;case "rejected":onRejected(self.reason);break;default: }
}var p = new myPromise(function(resolve,reject) {resolve(1)
});
p.then(function(x) {console.log(x) // 1
})
使用class实现
class MyPromise {constructor(fn) {this.resolvedCallbacks = [];this.rejectedCallbacks = [];this.state = 'PENDING';this.value = '';fn(this.resolve.bind(this), this.reject.bind(this));}resolve(value) {if (this.state === 'PENDING') {this.state = 'RESOLVED';this.value = value;this.resolvedCallbacks.map(cb => cb(value)); }}reject(value) {if (this.state === 'PENDING') {this.state = 'REJECTED';this.value = value;this.rejectedCallbacks.map(cb => cb(value));}}then(onFulfilled, onRejected) {if (this.state === 'PENDING') {this.resolvedCallbacks.push(onFulfilled);this.rejectedCallbacks.push(onRejected);}if (this.state === 'RESOLVED') {onFulfilled(this.value);}if (this.state === 'REJECTED') {onRejected(this.value);}}
}
Promise 实现-详细
- 可以把
Promise
看成一个状态机。初始是pending
状态,可以通过函数resolve
和reject
,将状态转变为resolved
或者rejected
状态,状态一旦改变就不能再次变化。 then
函数会返回一个Promise
实例,并且该返回值是一个新的实例而不是之前的实例。因为Promise
规范规定除了pending
状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个then
调用就失去意义了。- 对于
then
来说,本质上可以把它看成是flatMap
// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一个函数参数,该函数会立即执行
function MyPromise(fn) {let _this = this;_this.currentState = PENDING;_this.value = undefined;// 用于保存 then 中的回调,只有当 promise// 状态为 pending 时才会缓存,并且每个实例至多缓存一个_this.resolvedCallbacks = [];_this.rejectedCallbacks = [];_this.resolve = function (value) {if (value instanceof MyPromise) {// 如果 value 是个 Promise,递归执行return value.then(_this.resolve, _this.reject)}setTimeout(() => { // 异步执行,保证执行顺序if (_this.currentState === PENDING) {_this.currentState = RESOLVED;_this.value = value;_this.resolvedCallbacks.forEach(cb => cb());}})};_this.reject = function (reason) {setTimeout(() => { // 异步执行,保证执行顺序if (_this.currentState === PENDING) {_this.currentState = REJECTED;_this.value = reason;_this.rejectedCallbacks.forEach(cb => cb());}})}// 用于解决以下问题// new Promise(() => throw Error('error))try {fn(_this.resolve, _this.reject);} catch (e) {_this.reject(e);}
}MyPromise.prototype.then = function (onResolved, onRejected) {var self = this;// 规范 2.2.7,then 必须返回一个新的 promisevar promise2;// 规范 2.2.onResolved 和 onRejected 都为可选参数// 如果类型不是函数需要忽略,同时也实现了透传// Promise.resolve(4).then().then((value) => console.log(value))onResolved = typeof onResolved === 'function' ? onResolved : v => v;onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;if (self.currentState === RESOLVED) {return (promise2 = new MyPromise(function (resolve, reject) {// 规范 2.2.4,保证 onFulfilled,onRjected 异步执行// 所以用了 setTimeout 包裹下setTimeout(function () {try {var x = onResolved(self.value);resolutionProcedure(promise2, x, resolve, reject);} catch (reason) {reject(reason);}});}));}if (self.currentState === REJECTED) {return (promise2 = new MyPromise(function (resolve, reject) {setTimeout(function () {// 异步执行onRejectedtry {var x = onRejected(self.value);resolutionProcedure(promise2, x, resolve, reject);} catch (reason) {reject(reason);}});}));}if (self.currentState === PENDING) {return (promise2 = new MyPromise(function (resolve, reject) {self.resolvedCallbacks.push(function () {// 考虑到可能会有报错,所以使用 try/catch 包裹try {var x = onResolved(self.value);resolutionProcedure(promise2, x, resolve, reject);} catch (r) {reject(r);}});self.rejectedCallbacks.push(function () {try {var x = onRejected(self.value);resolutionProcedure(promise2, x, resolve, reject);} catch (r) {reject(r);}});}));}
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {// 规范 2.3.1,x 不能和 promise2 相同,避免循环引用if (promise2 === x) {return reject(new TypeError("Error"));}// 规范 2.3.2// 如果 x 为 Promise,状态为 pending 需要继续等待否则执行if (x instanceof MyPromise) {if (x.currentState === PENDING) {x.then(function (value) {// 再次调用该函数是为了确认 x resolve 的// 参数是什么类型,如果是基本类型就再次 resolve// 把值传给下个 thenresolutionProcedure(promise2, value, resolve, reject);}, reject);} else {x.then(resolve, reject);}return;}// 规范 2.3.3.3.3// reject 或者 resolve 其中一个执行过得话,忽略其他的let called = false;// 规范 2.3.3,判断 x 是否为对象或者函数if (x !== null && (typeof x === "object" || typeof x === "function")) {// 规范 2.3.3.2,如果不能取出 then,就 rejecttry {// 规范 2.3.3.1let then = x.then;// 如果 then 是函数,调用 x.thenif (typeof then === "function") {// 规范 2.3.3.3then.call(x,y => {if (called) return;called = true;// 规范 2.3.3.3.1resolutionProcedure(promise2, y, resolve, reject);},e => {if (called) return;called = true;reject(e);});} else {// 规范 2.3.3.4resolve(x);}} catch (e) {if (called) return;called = true;reject(e);}} else {// 规范 2.3.4,x 为基本类型resolve(x);}
}
实现Promisify
const fs = require('fs')
const path = require('path')// node中使用
// const fs = require('fs').promises 12.18版
// const promisify = require('util').promisify// 包装node api promise化 典型的高级函数
const promisify = fn=>{return (...args)=>{return new Promise((resolve,reject)=>{fn(...args, (err,data)=>{if(err) {reject(err)} resolve(data)})})}
}// const read = promisify(fs.readFile)// read(path.join(__dirname, './promise.js'), 'utf8').then(d=>{
// console.log(d)
// })// promise化node所有api
const promisifyAll = target=>{Reflect.ownKeys(target).forEach(key=>{if(typeof target[key] === 'function') {target[key+'Async'] = promisify(target[key])}})return target
}// promise化fs下的函数
const promisifyNew = promisifyAll(fs)promisifyNew.readFileAsync(path.join(__dirname, './promise.js'), 'utf8').then(d=>{console.log(d)
})module.exports = {promisify,promisifyAll
}
完整实现Promises/A+规范
/*** Promises/A+规范 实现一个promise* https://promisesaplus.com/
*/const EMUM = {PENDING: 'PENDING',FULFILLED: 'FULFILLED',REJECTED: 'REJECTED'
}// x 返回值
// promise2 then的时候new的promise
// promise2的resolve, reject
const resolvePromise = (x, promise2, resolve, reject)=>{// 解析promise的值解析promise2是成功还是失败 传递到下层thenif(x === promise2) {reject(new TypeError('类型错误'))}// 这里的x如果是一个promise的话 可能是其他的promise,可能调用了成功 又调用了失败// 防止resolve的时候 又throw err抛出异常到reject了let called// 如果x是promise 那么就采用他的状态// 有then方法是promiseif(typeof x === 'object' && typeof x!== null || typeof x === 'function') {// x是对象或函数try {let then = x.then // 缓存,不用多次取值if(typeof then === 'function') {// 是promise,调用then方法里面有this,需要传入this为x才能取到then方法里面的值this.valuethen.call(x, y=>{// 成功// y值可能也是一个promise 如resolve(new Promise()) 此时的y==new Promise()// 递归解析y,直到拿到普通的值resolve(x出去)if(called) return;called = true;resolvePromise(y, promise2, resolve, reject)},r=>{// 一旦失败直接失败if(called) return;called = true;reject(r)})} else {// 普通对象不是promiseresolve(x)}} catch (e) {// 对象取值可能报错,用defineProperty定义get 抛出异常if(called) return;called = true;reject(e)}} else {// x是普通值resolve(x) // 直接成功}}
class myPromise {constructor(executor) {this.status = EMUM.PENDING // 当前状态this.value = undefined // resolve接收值this.reason = undefined // reject失败返回值/*** 同一个promise可以then多次(发布订阅模式)* 调用then时 当前状态是等待态,需要将当前成功或失败的回调存放起来(订阅)* 调用resolve时 将订阅函数进行执行(发布)*/// 成功队列this.onResolvedCallbacks = []// 失败队列this.onRejectedCallbacks = []const resolve = value =>{// 如果value是一个promise,需要递归解析// 如 myPromise.resolve(new myPromise()) 需要解析valueif(value instanceof myPromise) {// 不停的解析 直到值不是promisereturn value.then(resolve,reject)}if(this.status === EMUM.PENDING) {this.status = EMUM.FULFILLEDthis.value = valuethis.onResolvedCallbacks.forEach(fn=>fn())}}const reject = reason =>{if(this.status === EMUM.PENDING) {this.status = EMUM.REJECTEDthis.reason = reasonthis.onRejectedCallbacks.forEach(fn=>fn())}}try {executor(resolve,reject)} catch(e) {reject(e)}}then(onFulFilled, onRejected) {// 透传 处理默认不传的情况// new Promise((resolve,reject)=>{// resolve(1)// }).then().then().then(d=>{})// new Promise((resolve,reject)=>{// resolve(1)// }).then(v=>v).then(v=>v).then(d=>{})// new Promise((resolve,reject)=>{// reject(1)// }).then().then().then(null, e=>{console.log(e)})// new Promise((resolve,reject)=>{// reject(1)// }).then(null,e=>{throw e}).then(null,e=>{throw e}).then(null,e=>{console.log(e)})onFulFilled = typeof onFulFilled === 'function' ? onFulFilled : v => vonRejected = typeof onRejected === 'function' ? onRejected : err => {throw err}// 调用then 创建一个新的promiselet promise2 = new myPromise((resolve,reject)=>{// 根据value判断是resolve 还是reject value也可能是promiseif(this.status === EMUM.FULFILLED) {setTimeout(() => {try {// 成功回调结果let x = onFulFilled(this.value)// 解析promiseresolvePromise(x, promise2,resolve,reject)} catch (error) {reject(error)}}, 0);}if(this.status === EMUM.REJECTED) {setTimeout(() => {try {let x = onRejected(this.reason)// 解析promiseresolvePromise(x, promise2,resolve,reject)} catch (error) {reject(error)}}, 0);}// 用户还未调用resolve或reject方法if(this.status === EMUM.PENDING) {this.onResolvedCallbacks.push(()=>{try {let x = onFulFilled(this.value)// 解析promiseresolvePromise(x, promise2,resolve,reject)} catch (error) {reject(error)}})this.onRejectedCallbacks.push(()=>{try {let x = onRejected(this.reason)// 解析promiseresolvePromise(x, promise2,resolve,reject)} catch (error) {reject(error)}})}})return promise2}catch(errCallback) {// 等同于没有成功,把失败放进去而已return this.then(null, errCallback)}// myPromise.resolve 具备等待功能的 如果参数的promise会等待promise解析完毕在向下执行static resolve(val) {return new myPromise((resolve,reject)=>{resolve(val)})}// myPromise.reject 直接将值返回static reject(reason) {return new myPromise((resolve,reject)=>{reject(reason)})}// finally传入的函数 无论成功或失败都执行// Promise.reject(100).finally(()=>{console.log(1)}).then(d=>console.log('success',d)).catch(er=>console.log('faild',er))// Promise.reject(100).finally(()=>new Promise()).then(d=>console.log(d)).catch(er=>)finally(callback) {return this.then((val)=>{return myPromise.resolve(callback()).then(()=>val)},(err)=>{return myPromise.resolve(callback()).then(()=>{throw err})})}// Promise.allstatic all(values) {return new myPromise((resolve,reject)=>{let resultArr = []let orderIndex = 0const processResultByKey = (value,index)=>{resultArr[index] = value // 处理完全部if(++orderIndex === values.length) {resolve(resultArr) // 处理完成的结果返回去}}for (let i = 0; i < values.length; i++) {const value = values[i];// 是promiseif(value && typeof value.then === 'function') {value.then((val)=>{processResultByKey(val,i)},reject)} else {// 不是promise情况processResultByKey(value,i)}}})}static race(promises) {// 采用最新成功或失败的作为结果return new myPromise((resolve,reject)=>{for (let i = 0; i < promises.length; i++) {let val = promises[i]if(val && typeof val.then === 'function') {// 任何一个promise先调用resolve或reject就返回结果了 也就是返回执行最快的那个promise的结果val.then(resolve,reject)}else{// 普通值resolve(val)}}})}
}/*** =====测试用例-====*/
// let promise1 = new myPromise((resolve,reject)=>{
// setTimeout(() => {
// resolve('成功')
// }, 900);
// })// promise1.then(val=>{
// console.log('success', val)
// },reason=>{
// console.log('fail', reason)
// })/*** then的使用方式 普通值意味不是promise* * 1、then中的回调有两个方法 成功或失败 他们的结果返回(普通值)会传递给外层的下一个then中* 2、可以在成功或失败中抛出异常,走到下一次then的失败中* 3、返回的是一个promsie,那么会用这个promise的状态作为结果,会用promise的结果向下传递* 4、错误处理,会默认先找离自己最新的错误处理,找不到就向下查找,找打了就执行*/// read('./name.txt').then(data=>{
// return '123'
// }).then(data=>{// }).then(null,err=>{// })
// // .catch(err=>{ // catch就是没有成功的promise// // })/*** promise.then实现原理:通过每次返回一个新的promise来实现(promise一旦成功就不能失败,失败就不能成功)* */// function read(data) {
// return new myPromise((resolve,reject)=>{
// setTimeout(() => {
// resolve(new myPromise((resolve,reject)=>resolve(data)))
// }, 1000);
// })
// }// let promise2 = read({name: 'poetry'}).then(data=>{
// return data
// }).then().then().then(data=>{
// console.log(data,'-data-')
// },(err)=>{
// console.log(err,'-err-')
// })// finally测试
// myPromise
// .resolve(100)
// .finally(()=>{
// return new myPromise((resolve,reject)=>setTimeout(() => {
// resolve(100)
// }, 100))
// })
// .then(d=>console.log('finally success',d))
// .catch(er=>console.log(er, 'finally err'))/*** promise.all 测试* * myPromise.all 解决并发问题 多个异步并发获取最终的结果
*/// myPromise.all([1,2,3,4,new myPromise((resolve,reject)=>{
// setTimeout(() => {
// resolve('ok1')
// }, 1000);
// }),new myPromise((resolve,reject)=>{
// setTimeout(() => {
// resolve('ok2')
// }, 1000);
// })]).then(d=>{
// console.log(d,'myPromise.all.resolve')
// }).catch(err=>{
// console.log(err,'myPromise.all.reject')
// })// 实现promise中断请求
let promise = new Promise((resolve,reject)=>{setTimeout(() => {// 模拟接口调用 ajax调用超时resolve('成功') }, 10000);
})function promiseWrap(promise) {// 包装一个promise 可以控制原来的promise是成功 还是失败let abortlet newPromsie = new myPromise((resolve,reject)=>{abort = reject})// 只要控制newPromsie失败,就可以控制被包装的promise走向失败// Promise.race 任何一个先成功或者失败 就可以获得结果let p = myPromise.race([promise, newPromsie])p.abort = abortreturn p
}let newPromise = promiseWrap(promise)setTimeout(() => {// 超过3秒超时newPromise.abort('请求超时')
}, 3000);newPromise.then(d=>{console.log('d',d)
}).catch(err=>{console.log('err',err)
})// 使用promises-aplus-tests 测试写的promise是否规范
// 全局安装 cnpm i -g promises-aplus-tests
// 命令行执行 promises-aplus-tests promise.js
// 测试入口 产生延迟对象
myPromise.defer = myPromise.deferred = function () {let dfd = {}dfd.promise = new myPromise((resolve,reject)=>{dfd.resolve = resolvedfd.reject = reject})return dfd
}// 延迟对象用户
// ![](http://img-repo.poetries.top/images/20210509172817.png)
// promise解决嵌套问题
// function readData(url) {
// let dfd = myPromise.defer()
// fs.readFile(url, 'utf8', function (err,data) {
// if(err) {
// dfd.reject()
// }
// dfd.resolve(data)
// })
// return dfd.promise
// }
// readData().then(d=>{
// return d
// })module.exports = myPromise
参考 前端进阶面试题详细解答
实现一个简易的MVVM
实现一个简易的
MVVM
我会分为这么几步来:
- 首先我会定义一个类
Vue
,这个类接收的是一个options
,那么其中可能有需要挂载的根元素的id
,也就是el
属性;然后应该还有一个data
属性,表示需要双向绑定的数据 - 其次我会定义一个
Dep
类,这个类产生的实例对象中会定义一个subs
数组用来存放所依赖这个属性的依赖,已经添加依赖的方法addSub
,删除方法removeSub
,还有一个notify
方法用来遍历更新它subs
中的所有依赖,同时Dep类有一个静态属性target
它用来表示当前的观察者,当后续进行依赖收集的时候可以将它添加到dep.subs
中。 - 然后设计一个
observe
方法,这个方法接收的是传进来的data
,也就是options.data
,里面会遍历data
中的每一个属性,并使用Object.defineProperty()
来重写它的get
和set
,那么这里面呢可以使用new Dep()
实例化一个dep
对象,在get
的时候调用其addSub
方法添加当前的观察者Dep.target
完成依赖收集,并且在set
的时候调用dep.notify
方法来通知每一个依赖它的观察者进行更新 - 完成这些之后,我们还需要一个
compile
方法来将HTML模版和数据结合起来。在这个方法中首先传入的是一个node
节点,然后遍历它的所有子级,判断是否有firstElmentChild
,有的话则进行递归调用compile方法,没有firstElementChild
的话且该child.innderHTML
用正则匹配满足有/\{\{(.*)\}\}/
项的话则表示有需要双向绑定的数据,那么就将用正则new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')
替换掉是其为msg
变量。 - 完成变量替换的同时,还需要将
Dep.target
指向当前的这个child
,且调用一下this.opt.data[key]
,也就是为了触发这个数据的get
来对当前的child
进行依赖收集,这样下次数据变化的时候就能通知child
进行视图更新了,不过在最后要记得将Dep.target
指为null
哦(其实在Vue
中是有一个targetStack
栈用来存放target
的指向的) - 那么最后我们只需要监听
document
的DOMContentLoaded
然后在回调函数中实例化这个Vue
对象就可以了
coding :
需要注意的点:
childNodes
会获取到所有的子节点以及文本节点(包括元素标签中的空白节点)firstElementChild
表示获取元素的第一个字元素节点,以此来区分是不是元素节点,如果是的话则调用compile
进行递归调用,否则用正则匹配- 这里面的正则真的不难,大家可以看一下
完整代码如下:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta http-equiv="X-UA-Compatible" content="ie=edge" /><title>MVVM</title></head><body><div id="app"><h3>姓名</h3><p>{{name}}</p><h3>年龄</h3><p>{{age}}</p></div></body>
</html>
<script>document.addEventListener("DOMContentLoaded",function () {let opt = { el: "#app", data: { name: "等待修改...", age: 20 } };let vm = new Vue(opt);setTimeout(() => {opt.data.name = "jing";}, 2000);},false);class Vue {constructor(opt) {this.opt = opt;this.observer(opt.data);let root = document.querySelector(opt.el);this.compile(root);}observer(data) {Object.keys(data).forEach((key) => {let obv = new Dep();data["_" + key] = data[key];Object.defineProperty(data, key, {get() {Dep.target && obv.addSubNode(Dep.target);return data["_" + key];},set(newVal) {obv.update(newVal);data["_" + key] = newVal;},});});}compile(node) {[].forEach.call(node.childNodes, (child) => {if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) {let key = RegExp.$1.trim();child.innerHTML = child.innerHTML.replace(new RegExp("\\{\\{\\s*" + key + "\\s*\\}\\}", "gm"),this.opt.data[key]);Dep.target = child;this.opt.data[key];Dep.target = null;} else if (child.firstElementChild) this.compile(child);});}}class Dep {constructor() {this.subNode = [];}addSubNode(node) {this.subNode.push(node);}update(newVal) {this.subNode.forEach((node) => {node.innerHTML = newVal;});}}
</script>
简化版2
function update(){console.log('数据变化~~~ mock update view')
}
let obj = [1,2,3]
// 变异方法 push shift unshfit reverse sort splice pop
// Object.defineProperty
let oldProto = Array.prototype;
let proto = Object.create(oldProto); // 克隆了一分
['push','shift'].forEach(item=>{proto[item] = function(){update();oldProto[item].apply(this,arguments);}
})
function observer(value){ // proxy reflectif(Array.isArray(value)){// AOPreturn value.__proto__ = proto;// 重写 这个数组里的push shift unshfit reverse sort splice pop}if(typeof value !== 'object'){return value;}for(let key in value){defineReactive(value,key,value[key]);}
}
function defineReactive(obj,key,value){observer(value); // 如果是对象 继续增加getter和setterObject.defineProperty(obj,key,{get(){return value;},set(newValue){if(newValue !== value){observer(newValue);value = newValue;update();}}})
}
observer(obj);
// AOP
// obj.name = {n:200}; // 数据变了 需要更新视图 深度监控
// obj.name.n = 100;
obj.push(123);
obj.push(456);
console.log(obj);
实现JSONP方法
利用
<script>
标签不受跨域限制的特点,缺点是只能支持get
请求
- 创建
script
标签 - 设置
script
标签的src
属性,以问号传递参数,设置好回调函数callback
名称 - 插入到
html
文本中 - 调用回调函数,
res
参数就是获取的数据
function jsonp({url,params,callback}) {return new Promise((resolve,reject)=>{let script = document.createElement('script')window[callback] = function (data) {resolve(data)document.body.removeChild(script)}var arr = []for(var key in params) {arr.push(`${key}=${params[key]}`)}script.type = 'text/javascript'script.src = `${url}?callback=${callback}&${arr.join('&')}`document.body.appendChild(script)})
}
// 测试用例
jsonp({url: 'http://suggest.taobao.com/sug',callback: 'getData',params: {q: 'iphone手机',code: 'utf-8'},
}).then(data=>{console.log(data)})
- 设置
CORS: Access-Control-Allow-Origin:*
postMessage
实现Ajax
步骤
- 创建
XMLHttpRequest
实例 - 发出 HTTP 请求
- 服务器返回 XML 格式的字符串
- JS 解析 XML,并更新局部页面
- 不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON。
了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。
对象数组列表转成树形结构(处理菜单)
[{id: 1,text: '节点1',parentId: 0 //这里用0表示为顶级节点},{id: 2,text: '节点1_1',parentId: 1 //通过这个字段来确定子父级}...
]转成
[{id: 1,text: '节点1',parentId: 0,children: [{id:2,text: '节点1_1',parentId:1}]}
]
实现代码如下:
function listToTree(data) {let temp = {};let treeData = [];for (let i = 0; i < data.length; i++) {temp[data[i].id] = data[i];}for (let i in temp) {if (+temp[i].parentId != 0) {if (!temp[temp[i].parentId].children) {temp[temp[i].parentId].children = [];}temp[temp[i].parentId].children.push(temp[i]);} else {treeData.push(temp[i]);}}return treeData;
}
异步串行 | 异步并行
// 字节面试题,实现一个异步加法
function asyncAdd(a, b, callback) {setTimeout(function () {callback(null, a + b);}, 500);
}// 解决方案
// 1. promisify
const promiseAdd = (a, b) => new Promise((resolve, reject) => {asyncAdd(a, b, (err, res) => {if (err) {reject(err)} else {resolve(res)}})
})// 2. 串行处理
async function serialSum(...args) {return args.reduce((task, now) => task.then(res => promiseAdd(res, now)), Promise.resolve(0))
}// 3. 并行处理
async function parallelSum(...args) {if (args.length === 1) return args[0]const tasks = []for (let i = 0; i < args.length; i += 2) {tasks.push(promiseAdd(args[i], args[i + 1] || 0))}const results = await Promise.all(tasks)return parallelSum(...results)
}// 测试
(async () => {console.log('Running...');const res1 = await serialSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)console.log(res1)const res2 = await parallelSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)console.log(res2)console.log('Done');
})()
手写深度比较isEqual
思路:深度比较两个对象,就是要深度比较对象的每一个元素。=> 递归
- 递归退出条件:
- 被比较的是两个值类型变量,直接用“===”判断
- 被比较的两个变量之一为
null
,直接判断另一个元素是否也为null
- 提前结束递推:
- 两个变量
keys
数量不同 - 传入的两个参数是同一个变量
- 两个变量
- 递推工作:深度比较每一个
key
function isEqual(obj1, obj2){//其中一个为值类型或nullif(!isObject(obj1) || !isObject(obj2)){return obj1 === obj2;}//判断是否两个参数是同一个变量if(obj1 === obj2){return true;}//判断keys数是否相等const obj1Keys = Object.keys(obj1);const obj2Keys = Object.keys(obj2);if(obj1Keys.length !== obj2Keys.length){return false;}//深度比较每一个keyfor(let key in obj1){if(!isEqual(obj1[key], obj2[key])){return false;}}return true;
}
数组中的数据根据key去重
给定一个任意数组,实现一个通用函数,让数组中的数据根据 key 排重:
const dedup = (data, getKey = () => {} ) => {// todo
}
let data = [{ id: 1, v: 1 },{ id: 2, v: 2 },{ id: 1, v: 1 },
];// 以 id 作为排重 key,执行函数得到结果
// data = [
// { id: 1, v: 1 },
// { id: 2, v: 2 },
// ];
实现
const dedup = (data, getKey = () => { }) => {const dateMap = data.reduce((pre, cur) => {const key = getKey(cur)if (!pre[key]) {pre[key] = cur}return pre}, {})return Object.values(dateMap)
}
使用
let data = [{ id: 1, v: 1 },{ id: 2, v: 2 },{ id: 1, v: 1 },
];
console.log(dedup(data, (item) => item.id))// 以 id 作为排重 key,执行函数得到结果
// data = [
// { id: 1, v: 1 },
// { id: 2, v: 2 },
// ];
实现节流函数(throttle)
节流函数原理:指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。总结起来就是: 事件,按照一段时间的间隔来进行触发 。
像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多
手写简版
使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行
时间戳方式:
// func是用户传入需要防抖的函数
// wait是等待时间
const throttle = (func, wait = 50) => {// 上一次执行该函数的时间let lastTime = 0return function(...args) {// 当前时间let now = +new Date()// 将当前时间和上一次执行函数时间对比// 如果差值大于设置的等待时间就执行函数if (now - lastTime > wait) {lastTime = nowfunc.apply(this, args)}}
}setInterval(throttle(() => {console.log(1)}, 500),1
)
定时器方式:
使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数
function throttle(func, delay){var timer = null;returnfunction(){var context = this;var args = arguments;if(!timer){timer = setTimeout(function(){func.apply(context, args);timer = null;},delay);}}
}
适用场景:
DOM
元素的拖拽功能实现(mousemove
)- 搜索联想(
keyup
) - 计算鼠标移动的距离(
mousemove
) Canvas
模拟画板功能(mousemove
)- 监听滚动事件判断是否到页面底部自动加载更多
- 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
- 缩放场景:监控浏览器
resize
- 动画场景:避免短时间内多次触发动画引起性能问题
总结
- 函数防抖 :将几次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
- 函数节流 :使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。
实现一个 sleep 函数,比如 sleep(1000) 意味着等待1000毫秒
// 使用 promise来实现 sleep
const sleep = (time) => {return new Promise(resolve => setTimeout(resolve, time))
}sleep(1000).then(() => {// 这里写你的骚操作
})
树形结构转成列表(处理菜单)
[{id: 1,text: '节点1',parentId: 0,children: [{id:2,text: '节点1_1',parentId:1}]}
]
转成
[{id: 1,text: '节点1',parentId: 0 //这里用0表示为顶级节点},{id: 2,text: '节点1_1',parentId: 1 //通过这个字段来确定子父级}...
]
实现代码如下:
function treeToList(data) {let res = [];const dfs = (tree) => {tree.forEach((item) => {if (item.children) {dfs(item.children);delete item.children;}res.push(item);});};dfs(data);return res;
}
实现lodash的chunk方法–数组按指定长度拆分
题目
/*** @param input* @param size* @returns {Array}*/
_.chunk(['a', 'b', 'c', 'd'], 2)
// => [['a', 'b'], ['c', 'd']]_.chunk(['a', 'b', 'c', 'd'], 3)
// => [['a', 'b', 'c'], ['d']]_.chunk(['a', 'b', 'c', 'd'], 5)
// => [['a', 'b', 'c', 'd']]_.chunk(['a', 'b', 'c', 'd'], 0)
// => []
实现
function chunk(arr, length) {let newArr = [];for (let i = 0; i < arr.length; i += length) {newArr.push(arr.slice(i, i + length));}return newArr;
}
设计一个方法提取对象中所有value大于2的键值对并返回最新的对象
实现:
var obj = { a: 1, b: 3, c: 4 }
foo(obj) // { b: 3, c: 4 }
方法有很多种,这里提供一种比较简洁的写法,用到了ES10
的Object.fromEntries()
:
var obj = { a: 1, b: 3, c: 4 }
function foo (obj) {return Object.fromEntries(Object.entries(obj).filter(([key, value]) => value > 2))
}
var obj2 = foo(obj) // { b: 3, c: 4 }
console.log(obj2)
// ES8中 Object.entries()的作用:
var obj = { a: 1, b: 2 }
var entries = Object.entries(obj); // [['a', 1], ['b', 2]]
// ES10中 Object.fromEntries()的作用:
Object.fromEntries(entries); // { a: 1, b: 2 }
实现一个链表结构
链表结构
看图理解next层级
// 链表 从头尾删除、增加 性能比较好
// 分为很多类 常用单向链表、双向链表// js模拟链表结构:增删改查// node节点
class Node {constructor(element,next) {this.element = elementthis.next = next}
}class LinkedList {constructor() {this.head = null // 默认应该指向第一个节点this.size = 0 // 通过这个长度可以遍历这个链表}// 增加O(n)add(index,element) {if(arguments.length === 1) {// 向末尾添加element = index // 当前元素等于传递的第一项index = this.size // 索引指向最后一个元素}if(index < 0 || index > this.size) {throw new Error('添加的索引不正常')}if(index === 0) {// 直接找到头部 把头部改掉 性能更好let head = this.headthis.head = new Node(element,head)} else {// 获取当前头指针let current = this.head// 不停遍历 直到找到最后一项 添加的索引是1就找到第0个的next赋值for (let i = 0; i < index-1; i++) { // 找到它的前一个current = current.next}// 让创建的元素指向上一个元素的下一个// 看图理解next层级current.next = new Node(element,current.next) // 让当前元素指向下一个元素的next}this.size++;}// 删除O(n)remove(index) {if(index < 0 || index >= this.size) {throw new Error('删除的索引不正常')}this.size--if(index === 0) {let head = this.headthis.head = this.head.next // 移动指针位置return head // 返回删除的元素}else {let current = this.headfor (let i = 0; i < index-1; i++) { // index-1找到它的前一个current = current.next}let returnVal = current.next // 返回删除的元素// 找到待删除的指针的上一个 current.next.next // 如删除200, 100=>200=>300 找到200的上一个100的next的next为300,把300赋值给100的next即可current.next = current.next.next return returnVal}}// 查找O(n)get(index) {if(index < 0 || index >= this.size) {throw new Error('查找的索引不正常')}let current = this.headfor (let i = 0; i < index; i++) {current = current.next}return current}
}var ll = new LinkedList()ll.add(0,100) // Node { ellement: 100, next: null }
ll.add(0,200) // Node { element: 200, next: Node { element: 100, next: null } }
ll.add(1,500) // Node {element: 200,next: Node { element: 100, next: Node { element: 500, next: null } } }
ll.add(300)
ll.remove(0)console.log(ll.get(2),'get')
console.log(ll.head)module.exports = LinkedList
数组去重方法汇总
首先:我知道多少种去重方式
1. 双层 for 循环
function distinct(arr) {for (let i=0, len=arr.length; i<len; i++) {for (let j=i+1; j<len; j++) {if (arr[i] == arr[j]) {arr.splice(j, 1);// splice 会改变数组长度,所以要将数组长度 len 和下标 j 减一len--;j--;}}}return arr;
}
思想: 双重
for
循环是比较笨拙的方法,它实现的原理很简单:先定义一个包含原始数组第一个元素的数组,然后遍历原始数组,将原始数组中的每个元素与新数组中的每个元素进行比对,如果不重复则添加到新数组中,最后返回新数组;因为它的时间复杂度是O(n^2)
,如果数组长度很大,效率会很低
2. Array.filter() 加 indexOf/includes
function distinct(a, b) {let arr = a.concat(b);return arr.filter((item, index)=> {//return arr.indexOf(item) === indexreturn arr.includes(item)})
}
思想: 利用
indexOf
检测元素在数组中第一次出现的位置是否和元素现在的位置相等,如果不等则说明该元素是重复元素
3. ES6 中的 Set 去重
function distinct(array) {return Array.from(new Set(array));
}
思想: ES6 提供了新的数据结构 Set,Set 结构的一个特性就是成员值都是唯一的,没有重复的值。
4. reduce 实现对象数组去重复
var resources = [{ name: "张三", age: "18" },{ name: "张三", age: "19" },{ name: "张三", age: "20" },{ name: "李四", age: "19" },{ name: "王五", age: "20" },{ name: "赵六", age: "21" }
]
var temp = {};
resources = resources.reduce((prev, curv) => {// 如果临时对象中有这个名字,什么都不做if (temp[curv.name]) {}else {// 如果临时对象没有就把这个名字加进去,同时把当前的这个对象加入到prev中temp[curv.name] = true;prev.push(curv);}return prev
}, []);
console.log("结果", resources);
这种方法是利用高阶函数
reduce
进行去重, 这里只需要注意initialValue
得放一个空数组[],不然没法push
相关文章:
阿里前端二面经典手写面试题汇总
实现类的继承 实现类的继承-简版 类的继承在几年前是重点内容,有n种继承方式各有优劣,es6普及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深入理解的去看红宝书即可,我们目前只实现一种最理想…...
【Eye】Fake News Reading on Social Media: An Eye-tracking Study
Fake News Reading on Social Media: An Eye-tracking Study Abstract 在网上传播假新闻(以及一般的虚假信息)最近被认为是威胁整个社会的一个主要问题。这种传播在很大程度上是由于新的媒体形式,即社交网络和在线媒体网站。研究人员和从业…...
想学计算机,应该学什么专业?
我们在考虑想学计算机,应该学什么专业?这个问题的时候,每个人都应该结合自己的兴趣来确定。有的喜欢编程、有的喜欢设计、有的喜欢做产品跟人打交道……自己有兴趣再加上自己的努力,掌握好专业技能,就一定能进入高薪的…...
Android逆向之旅—反编译利器Apktool使用教程
apktool下载软件首先下载apktool.bat和apktool.jar官网地址:https://ibotpeaches.github.io/Apktool/install/配置环境变量具体的apktool命令自行百度apktool 解包与打包解包: apktool d xxx.apk打包: apktool b xxx1.jadx安装与使用下载exe或…...
色环电阻的阻值如何识别
这种是色环电阻,其外表有一圈圈不同颜色的色环,现在在一些电器和电源电路中还有使用。下面的两种色环电阻它颜色还不一样,一个蓝色,一个土黄色,其实这个蓝色的属于金属膜色环电阻,外表涂的是一层金属膜&…...
Dataway 让 Spring Boot 不再需要 Controller、Service、DAO、Mapper 简单接口直接开发。
新的sql语法可以先看一下官网,部署起来之后会用到Dataql: DataQL - 数据查询语言https://www.dataql.net/先看一下效果 接下来来实现一下。 1 创建spring boot项目 导入依赖 <!--begin dataWay--><!--hasor-spring 负责 Spring 和 Hasor 框架之…...
C#窗口介绍
窗口就是打开程序我们所面对的一个面板,里面可以添加各种控件,如下图所示,我们可以在属性栏设置其标题名称、图标、大小等。图1 窗口图 图2 设置面板 图3 设置双击标题框,会生成Load函数,也可以到事件里面去找Load函数…...
SpringBoot:SpringBoot整合Junit 和 MyBatis(3)
SpringBoot整合Junit 和 MyBatis1. SpringBoot整合Junit2. SpringBoot整合MyBatis2.1 定义SpringBoot项目2.2 定义dao接口2.3 定义service层2.4 定义controller层2.5 配置yml/yaml文件2.6 postman测试1. SpringBoot整合Junit 在com.example.service下创建BookService接口 publ…...
Web自动化测试框架Selenium
作者:霍格沃兹测试开发学社 链接:https://www.zhihu.com/question/59854292/answer/2827875817 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 什么是自动化测试 自动化测试就是࿰…...
大数据系统自检
第一章 大数据计算系统概述 1.1 大数据计算框架概述 Hadoop Hadoop的运行过程(5个步骤?) split > map > shuffle > reduce > output Hadoop的详细运行过程?(4个大过程,6662) 创建…...
MySQL数据库操作
查看数据库语法show databases——列出所有的数据库 show databases [ like wild ];——列出和字符串wild名字相同的数据库 这里可以配合SQl的 "%" 和 "_" 通配符使用来查找多个数据库在SQL语句中"%"代表任意字符出现任意次数,"_"代表…...
线程安全实例分析
一、变量的线程安全分析 成员变量和静态变量是否线程安全? ● 如果它们没有共享,则线程安全 ● 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况 —— 如果只有读操作,则线程安全 —— 如果有读写操作&am…...
Tomcat源码分析-启动分析(二) Catalina初始化
Bootstrap Tomcat运行是通过Bootstrap的main方法,在开发工具中,我们只需要运行Bootstrap的main方法,便可以启动tomcat进行代码调试和分析。Bootstrap是tomcat的入口,它会完成初始化ClassLoader,实例化Catalina以及loa…...
基础复习第二十二天 泛型的使用
泛型JDK1.5设计了泛型的概念。泛型即为“类型参数”,这个类型参数在声明它的类、接口或方法中,代表未知的通用的类型。例如:java.lang.Comparable接口和java.util.Comparator接口,是用于对象比较大小的规范接口,这两个…...
【C++进阶】三、二叉搜索树
目录 一、二叉搜索树 1.1 概念 1.2 二叉搜索树操作 二、二叉搜索树实现 2.1 框架总览 2.2 实现接口总览 2.2.1 构造函数 2.2.2 拷贝构造 2.2.3 赋值重载 2.2.4 析构函数 2.2.5 二叉搜索树的遍历 2.2.6 插入函数 2.2.7 查找函数 2.2.8 删除函数 2.3 二叉搜索数完整…...
电脑系统崩溃怎么修复教程
系统崩溃了怎么办? 如今的软件是越来越复杂、越来越庞大。由系统本身造成的崩溃即使是最简单的操作,比如关闭系统或者是对BIOS进行升级都可能会对PC合操作系统造成一定的影响。下面一起来看看电脑系统崩溃修复方法步骤。 工具/原料: 系统版本…...
语义分割数据标注案例分析
语义分割(Semantic Segmentation)是计算机视觉领域中的一种重要任务,它的目的是将图像中的每个像素分配到对应的语义类别中。简单来说,就是将一张图像分割成多个区域,并为每个像素指定一个标签,标识出它属于…...
回归预测 | MATLAB实现GRU(门控循环单元)多输入单输出(多指标评价)
回归预测 | MATLAB实现GRU(门控循环单元)多输入单输出(多指标评价) 文章目录 回归预测 | MATLAB实现GRU(门控循环单元)多输入单输出(多指标评价)预测效果基本介绍程序设计参考资料预测效果 基本介绍 GRU神经网络是LST...
驱动程序开发:Buildroot根文件系统构建并加载驱动文件xxx.ko测试
目录一、buildroot根文件系统简介二、buildroot下载三、buildroot构建根文件系统1、配置 buildroot①配置 Target options②配置 Toolchain③配置 System configuration④配置 Filesystem images⑤禁止编译 Linux 内核和 uboot2、 buildroot 下的 busybox 配置①修改 Makefile&…...
R+VIC模型融合实践技术应用及未来气候变化模型预测
目录 理论专题一:VIC模型的原理及特点 综合案例一:基于QGIS的VIC模型建模 理论专题二:VIC模型率定验证 综合案例二:基于R语言VIC参数率定和优化 理论专题三:遥感技术与未来气候变化 综合案例三:运用V…...
第六章.决策树(Decision Tree)—ID3算法,C4.5算法
第六章.决策树(Decision Tree) 6.1 ID3算法,C4.5算法 1.决策树适用的数据类型 比较适合分析离散数据,如果是连续数据要先转换成离散数据再做分析 2.信息熵 1).概念: 一条信息的信息量大小和它的不确定性有直接的关系,要搞清楚一件非常不确…...
springboot+pgbouncer+postgres数据库连接池集成方案及问题解决
期望通过每一次分享,让技术的门槛变低,落地更容易。 —— around 前言 旨在解决微服务项目全是连接池并影响数据库并发连接,作者的环境是基于sprongboot微服务连接postgres数据库,每个微服务的DAO层配置都使用了连接池技术。后续…...
Mysql 常用日期处理函数
Mysql 常用日期处理函数 1 建表语句 SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS 0; -- ---------------------------- -- Table structure for emp -- ---------------------------- DROP TABLE IF EXISTS emp; CREATE TABLE emp (EMPNO int(4) NOT NULL,ENAME varchar(10…...
Pod中容器的健康检查
健康检查 上篇文章中我们了解了Pod中容器的生命周期的两个钩子函数,PostStart与PreStop,其中PostStart是在容器创建后立即执行的,而PreStop这个钩子函数则是在容器终止之前执行的。除了上面这两个钩子函数以外,还有一项配置会影响…...
信贷系统学习总结(5)—— 简单的风控示例(含代码)
一、背景1.为什么要做风控?目前我们业务有使用到非常多的AI能力,如ocr识别、语音测评等,这些能力往往都比较费钱或者费资源,所以在产品层面也希望我们对用户的能力使用次数做一定的限制,因此风控是必须的!2.为什么要自己写风控?那么多开源的风控组件,为什么还要写呢?是不是想…...
Java知识复习(四)多线程、并发编程
1、进程、线程和程序 进程:进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的;在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程…...
一个9个月测试经验的人,居然在面试时跟我要18K,我都被他吓到了····
2月初我入职了深圳某家创业公司,刚入职还是很兴奋的,到公司一看我傻了,公司除了我一个测试,公司的开发人员就只有3个前端2个后端还有2个UI,在粗略了解公司的业务后才发现是一个从零开始的项目,目前啥都没有…...
zigbee与WIFI同频干扰问题
zigbee与WIFI同频干扰 为了降低Wifi信道与Zigbee信道的同频干扰问题,Zigbee联盟在《Zigbee Home Automation Public Application Profile》中推荐使用11,14,15,19,20,24,25这七个信道。 为什么呢,我们看一下Wifi和Zigbee的信道分布。 WiFi带宽对干扰的…...
git拉取指定的单个或多个文件或文件夹
直接上步骤 初始化仓库 git init拉取远程仓库信息,省略号为仓库地址 git remote add -f origin http://****.git开启 sparse clone git config core.sparsecheckout true配置需要拉取的文件夹 有一个指定一个,有多个指定多个,路径写对即可&a…...
不是,到底有多少种图片懒加载方式?
一、也是我最开始了解到的 js方法,利用滚动事件,判断当时的图片位置是否在可视框内,然后进行渲染。 弊端:代码冗杂,你还要去监听页面的滚动事件,这本身就是一个不建议监听的事件,即便是我们做了…...
网站建设的线框图叫什么/关键词优化的软件
1、RBD介绍 RBD即RADOS Block Device的简称,RBD块存储是最稳定且最常用的存储类型。RBD块设备类似磁盘可以被挂载。 RBD块设备具有快照(RDB的快照在恢复数据的时候就可以直接恢复快照了)、多副本、克隆和一致性等特性,数据以条带化…...
最专业微网站多少钱/淘宝引流推广平台
大家好。以下代码为测试代码:privatevoidPage_Load(objectsender, System.EventArgs e) { // 在此处放置用户代码以初始化页面 if(!IsPostBack) { DataTable dt EOffice.DataAccess.ManuScript.NewsOffice…...
免费网站app源码/广州竞价外包
摘要: 受Reddit网站上讨论区的启发,我决定快速地浏览一下2018年关于GAN最有趣的文章。我很高兴今年参加了一个研究项目,这要求我必须熟悉大量用于计算机视觉方面的深度学习领域的资料。我对过去两、三年内取得的进展感到惊讶,这真…...
wordpress文章相册形式/深圳公司网络推广该怎么做
我用的是bochs 2.6.11 首先进入bochs的调试模式使用的是bochsdbg ,打开bochsdbg。他会让你选择配置文件。 选择完了配置文件,弹出两个窗口,一个是调试命令行,一个是操作系统窗口 最初的时候,他会显示一些日志&#x…...
去掉wordpress页面的分类归档/页面优化
2019独角兽企业重金招聘Python工程师标准>>> 转载于:https://my.oschina.net/suotree/blog/28029...
网站建设需要用到什么/sem广告投放是做什么的
React团队推出了一款新工具,希望帮助开发人员减轻新建React应用所引发的痛苦。 在一篇博文中,Dan Abramov介绍了Create React App。该工具让开发人员可以使用一行命令新建一个React应用程序——包括其构建过程和依赖。这是官方支持的一种React应用程序创…...