1.promise.all
- 实现思路
1.由于promise.all只能接受数组,所以需要判断传进来的参数是不是数组
2.如果参数全都是resolve状态,则需要返回这个数组,则需要创建一个变量result存储。如果是需要返回这个数组,为什么不能直接返回参数promises呢?因为all方法返回的是执行了一遍之后的promise对象,而不是参数中还未执行过的promise
3.由于需要计算resolve状态的promise和参数promises是否一致,所以需要创建变量count用于计数
4.前期工作准备完,我们就要开始遍历参数promises了,由于promises里每个元素都是promise对象,因此可以直接调用then方法,如果这个元素resolve的话,count就要+1,并且result也要把这个元素的执行结果存储进去。当count === 参数的长度时,那么就把all返回的promise的状态变成resolve(result)。
5.遍历参数的时候如果遇到reject的话,直接把状态调成reject(item)即可,这里reject的参数只是这个报错的元素
function myPromiseAll(promises){
return new Promiese((resolve,reject)=>{
if(Array.isArray(promise)){
throw new TypeError("promises must be an array")
}
let result = []
let count
promises.forEach((item,index) => {
item().then(res=>{
result[index] = res
count++
count == promises.length && reslove(result)
},rea=>{
reject(item)
})
})
})
}
2.promise.race
- 实现思路
1.也是返回一个promise对象,所有的逻辑都写在这个新的promise对象里
2.race是赛跑,传进来的参数哪个跑得快返回的就是哪个,然后开始遍历,这里相当于给每个item都用上了then方法,谁跑得快谁就改变返回的promise对象的状态
function myPromiseRace(promises){
return new promise((resolve,reject)=>{
//本身就是看谁跑得快就先执行谁,同步代码的执行顺序就是从上到下,他的执行方式就是按照事件执行顺序定的
promises.forEach(item => {
item.then(res=>{
reslove(res)
},rea=>{
reject(rea)
})
})
})
}
3.手写防抖函数
- 实现思路:函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
1.首先我们要明白每一个函数的this指向,在下面的注释中已经写清楚
2.为什么在debounce里定义了timer,在其他地方定义不行?
timer是判断这个计时器还是否存在的标识。每次debounce返回的函数是否执行实际代码,要根据上一次的timer的值来执行,在debounce里定义,是为了形成闭包然后缓存timer这个值,让我们知道上一次timer值是否为空
3.clearTimeout(timer)之后,为什么还要timer = null来置空timer值?
因为clearTimeout这是清除了计时器,但是timer是这个计时器的标识符,并没有被删除,需要手动置空
4.let context = this,args = arguments;有什么用,能不能删掉?
可以。但是这里能删掉,因为这里setTimeout使用的是箭头函数,箭头函数的this指向上一层函数作用域,上一层的函数作用域的this指向就是btn,这个情况下可以删除。但如果写成setTimeout(function(){fn.apply(this,arguments)},500)的话,这里的this指向的是window,题目中的这句话就不能删除,因为这是setTimeout内部调用function(){fn.apply(this,arguments)是直接调用,this指向window
5.为什么要用fn.apply(context, args),不能直接fn()?
fn是实际执行函数,this需要指向btn,因为我们业务时需要btn的数据,所以需要强制绑定this
btn.onclick = debounceFn('1','2')
const debounceFn = debounce(function(){
console.log(111) //这个函数目前在未被调用前,this还不确定,如果直接调用就是指向window,但我们需要显示绑定,不能让其指向window
},500)
// 函数防抖的实现
function debounce(fn, wait) {
let timer,context,args
// console.log(this) 这里的this指向window,因为在调用debounce时,相当于debounce()这样调用,只是括号里存在参数。
//这里不是btn调用的,因为在未点击btn时就已经触发debounce事件,点击btn触发的是debounce返回的函数
return function() {
context = this,
//这里的this指向的是btn,因为这是debounce返回的函数,而点击了才可以触发该事件
args = arguments;
// 如果此时存在定时器的话,则取消之前的定时器重新记时
if (timer) {
clearTimeout(timer);
timer = null;
}
// 设置定时器,使事件间隔指定事件后执行
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
};
}
其实上面写的那么多,实际的思路很简单,就是如果有定时器,那就清空定时器,然后再让定时器控制执行函数,那么防抖函数的第三个参数就可以实现了。第三个参数是immediate,一旦设置了这个参数,执行的就是另一个逻辑
function debounce(fn,wait,immediate){
let timer,context,callback,args
return function(){
context = this
args = arguments
if(timer){clearTimeout(timer)}
//若immediate的值为true,则一开始就要执行,因此需要把timer取反,然后通过callback来控制函数的执行
if(immediate){
callback = !timer
if(callback){fn.apply(context,args)}
timer = setTimeout(()=>{
timer = null
},wait)
}else{
timer = setTimeout(() => {
fn.apply(context,args)
}, wait);
}
}
}
4.手写节流函数
节流函数是在一定时间内,只触发一次,在同一单位时间内触发,只能有一次生效
function throttle(fn, delay) {
//利用了闭包把开始时间缓存起来
let curTime = Date.now();
return function() {
let context = this,
args = arguments,
nowTime = Date.now();
// 如果两次时间间隔超过了指定时间,则执行函数。
if (nowTime - curTime >= delay) {
curTime = Date.now();
return fn.apply(context, args);
}
};
}
5.手写call方法
- 实现思路:call方法是改变this指向,那么在无法使用call方法改变指向的时候,就可以用this的默认绑定来修改指向
1.call方法是挂载再函数的实例对象上的,所以我们也需要挂载再这上面
2.typeof this是什么意思?
this指向的是实际执行函数,也就是fn,判断这个类型是不是函数,不是就报错,因为call就是修改函数指向,调用的总不能不是函数吧
3.context.fn = this是什么意思
我们已经知道了this指向实际执行函数,而context是一个执行上下文对象,所以 context.fn = this是给context添加一个属性,属性的值就是执行函数,然后在context中调用这个函数,就达成了隐式调用修改this的目的了
4.最后我想聊聊为什么context要写进function的参数中,而其他参数则通过arguments获取
我们知道context是一定要有的,没有就会传进underfined,这有利于我们调用typeof判断。而call的实际参数有多少个我们是不确定的,所以通过arguments获取,再搭配上es6的扩展运算符会方便很多
const fn(){//这是实际执行函数}
const obj = {}
fn.myCall(obj)
Function.prototype.myCall = function(context){
if(typeof this !== 'function'){
throw new Error('type error')
}
//如果context没有传入参数,那么就指定为window
context = context || window
context.fn = this
//获得参数
let args = [...arguments].slice(1)
let result = context.fn(...args)
delete context.fn
return result
}
6.手写apply方法
- 实现思路,apply方法和call不同的仅仅是参数需要是一个数组,所以这里的arguments只用看第二个就行了,如果有数组,则通过扩展运算符展开(函数不能传进去数组);没有数组的话直接调用就好了
1.明白了call,apply应该没有什么大问题,聊聊我的收获吧,因为现在声明变量都是用let,let会有块级作用域,所以要在一开始就定义result,而不是在if判断时再定义,这样会导致return的不是我们想要的值
Function.prototype.myApply = function(context) {
if (typeof this !== "function") {
throw new TypeError("Error");
}
let result = null;
// 判断 context 是否存在,如果未传入则为 window
context = context || window;
context.fn = this;
if (arguments[1]) {
result = context.fn(...arguments[1]); //不要let result = context.fn(...arguments[1]);不会报错,但是结果不对
} else {
result = context.fn();
}
// 将属性删除
delete context.fn;
return result;
};
7.带有并发限制的promise
题目要求最多只能包含两行的promise,因此就不能使用promise.all和promise.race
思路:
- 利用队列先进先出的特性,add操作在队列中插入promise
- 判断是否小于执行数量,小于2就不执行
- 当一个promise执行完毕的时候,调用他的then方法递归执行runQueue
class Scheduler {
constructor () {
this.queue = []
this.maxCount = 2
this.runCount = 0
}
// promiseCreator执行后返回的是一个Promise
add(promiseCreator) {
// 小于等于2,直接执行
this.queue.push(promiseCreator)
this.runQueue()
}
runQueue () {
// 队列中还有任务才会被执行
if (this.queue.length && this.runCount < this.maxCount) {
// 执行先加入队列的函数
const promiseCreator = this.queue.shift()
// 开始执行任务 计数+1
this.runCount += 1
promiseCreator().then(() => {
// 任务执行完毕,计数-1
this.runCount -= 1
this.runQueue()
})
}
}
}
//模仿异步函数
const timeout = time => {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
const scheduler = new Scheduler()
const addTask = (time,order) => {
scheduler.add(() => timeout(time).then(()=>console.log(order)))
}
addTask(1000, '1')
addTask(500, '2')
addTask(300, '3')
addTask(400, '4')
8. 实现js普通数据对象的深度克隆
- 判断是数组,遍历每一个元素,遍历第一个的时候通过递归开启深度遍历(回溯算法)
- 对象同理
- 最后写跳出递归的条件,如果不是数组或者对象,那么直接放回就行
function deepCode(obj){
if(Array.isArray(obj)){
const result = []
// 遍历原始数组,并对每个元素进行递归克隆
for(let i of obj){
result.push(deepCode(i))
}
return result
}else if(typeof obj === 'object' && obj !== null){
let result = {}
// 遍历原始对象的属性,并对每个属性进行递归克隆
for(let key in obj){
result[key] = deepCode(obj[key])
}
return result
}
else{
// 如果是基本数据类型或者函数等,直接返回原始值
return obj
}
}
const originalArray = [1, 2, { a: 3, b: [4, 5] }]
const cloneArr = deepCode(originalArray)
console.log(cloneArr);
9. var array = [{name:'张三',id:'1'},{name:'李四',id:'2'},{name:'张六',id:'1'}]如果array里的id相同,就去重
- 定义一个对象,遍历array,让id作为key,将item作为value存进去
- 通过Object.values()获取对象的value组成的数组
var array = [
{ name: '张三', id: '1' },
{ name: '李四', id: '2' },
{ name: '张六', id: '1' }
];
function remove(obj){
const result = {}
obj.forEach(item => {
result[item['id']] = item
})
return Object.values(result)
}
console.log(remove(array));
10.请给string对象定义一个repeatify方法,该方法接受一个整数参数,作为字符串重复次数,最后返回重复指定次数的字符串。例如repeatify(3,'abc'),输出为abcabcabc
String.prototype.repeatify = function(count){
if(count <= 0){
return ''
}else{
let result = ''
for(let i = 0;i < count;i++){
result += this //使用 this 引用当前字符串
}
return result
}
}
console.log('abc'.repeatify(4));
11. 如何获取url中的参数
function getUrlParams() {
var myHref = window.location.href;
var args = myHref.split('?');
if(args[0] == myHref) {
return '';
}
var arr = args[1].split('&');
var obj = {};
for(var i = 0; i < arr.length; i++) {
var arg = arr[i].split('=')
obj[arg[0]] = arg[1];
}
return obj;
12. 实现一个可以设置过期时间的localStorage
- 在存入localStorage的时候,将当前时间戳+过期时间戳也存进去
- 去除localStorage的时候,判断一下当前时间戳是否在存的时间戳范围内
const customLocalStorage = {
setItem:function(key,value,time){
let expiredTime = time * 60 * 1000
const record = {
key:value,
expiredTime :new Data().getTime() + expiredTime
}
localStorage.setItem(key,JSON.stringify(record))
},
getItem:function(key){
let value = JSON.parse(localStorage.getItem(key))
if(new Data().getTime() <= value.expiredTime){
return value[key]
}
return null
}
}
// 存储一个名为 "myKey" 的值,并设置过期时间为 10 分钟
customLocalStorage.setItem('myKey', 'myValue', 10);
// 获取存储的值
const retrievedValue = customLocalStorage.getItem('myKey');
console.log(retrievedValue); // 输出存储的值
13. 封装一个判断两个对象是否相等的函数
- 如果对象的一个value值也是对象的话,那么就要递归调用这个函数,因此要按照递归的想法来想这道题
- 递归首先要写出跳出递归的条件,当obj1或者obj2不是Object类型时,就直接return obj1 === obj2
- 然后正常写单层逻辑,获取obj1的所有key值组成的数组,如果长度不相同则直接return false
- 然后一旦在obj2中找不到key值,或者开始递归的时候返回了false,就直接返回false
- 最后一行的return true的作用是,如果递归结束了都没有执行到return false,此时就没有返回值,因此需要加上一个return true
function deepEqual(obj1,obj2){
if(typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 === null || obj2 === null){
return obj1 === obj2
}
const key1 = Object.keys(obj1)
const key2 = Object.keys(obj2)
if(key1.length !== key2.length){
return false
}
for(let key of key1){
if(!obj2.hasOwnProperty(key) || !deepEqual(obj1[key],obj2[key])){
return false
}
}
return true
}
console.log(deepEqual({a:1},{a:1}));