1.call函数封装实现
主要思路:给传入的对象创建一个临时变量(obj.temp),并将传入的函数地址(Fn)给这个对象,让两个变量(Fn与temp)指向同一个内存地址。此时通过obj.temp调用内存中的这个函数,会使得函数内部的this变为obj,最终将调用结果返回。如此一来,就相当于改变了Fn内部this的指向,并同时执行了一次Fn。
export default function call(Fn, obj, ...args) {
// 判断
if (obj === undefined || obj === null) {
obj = globalThis;// 全局对象,nodejs下全局对象不是window,而是global
}
// 为obj对象添加临时的方法
obj.temp = Fn;
// 调用temp方法
let result = obj.temp(...args); // 此时因为temp是由obj调用的,所以内部的this就是temp。
// 删除temp方法
delete obj.temp;
return result;
}
调用:
<!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>
<script type="text/javascript" src="./call.js"></script>
</head>
<body>
<script type="text/javascript">
// 声明一个函数
function add(a, b) {
console.log(this);
return a + b + this.c
}
// 声明一个对象
let obj = {
c: 521
};
// 添加全局属性
window.c = 1314;
// 执行call函数
call(add, obj, 10, 20);
console.log(call(add, null, 30, 40)); // 若传入null,则add函数中会调用window.c。
</script>
</body>
</html>
2.apply函数封装实现
主要思路:与call方法实现类似,区别在于由于apply方法传入的是一个数组,所以我们需要使用隐式参数args来代表整个数组。最终执行的函数时,要用…args来将参数传入(考虑到函数定义时一般的形参格式)。
export function apply(Fn, obj, args) {
// 判断
if (obj === undefined || obj === null) {
obj = globalThis;
}
// 为obj添加临时方法
obj.temp = Fn;
// 执行方法
let result = obj.temp(...args);
// 删除临时属性
delete obj.temp;
// 返回结果
return result;
}
调用:
<!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>
<script type="text/javascript" src="./apply.js"></script>
</head>
<body>
<script type="text/javascript">
// 声明一个函数
function add(a, b) {
console.log(this);
return a + b + this.c
}
// 声明一个对象
let obj = {
c: 521
};
// 添加全局属性
window.c = 1314;
// 执行apply函数
console.log(apply(add, obj, [10, 20]));
console.log(apply(add, null, [30, 40]));
</script>
</body>
</html>
3.bind函数封装实现
思路:具体实现与call相同。不同点在于:bind函数返回一个函数地址。所以我们可以写成return function 的形式。而考虑到这个返回的函数地址可能需要传入额外的参数,形同:console.log(fn4(30, 50));,所以我们可以为返回的函数及bind函数各定义一个形参,并统一传入到call函数的…args中。
import call from './call'
function bind(Fn, obj, ...args) {
// 返回一个新的函数
return function (...arg2) {
// 执行call函数
return call(Fn, obj, ...args, ...arg2)
}
}
调用:
<!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>
<script type="text/javascript" src="./src/bind.js"></script>
<script type="text/javascript" src="./src/call.js"></script>
</head>
<body>
<script type="text/javascript">
// 声明一个函数
function add(a, b, d, f) {
// console.log(this);
console.log(arguments) //10, 20, 30, 50 -> 说明bind传递额的参数是靠前的,而fn3传递的参数是靠后的
return a + b + this.c + d + f
}
// 声明一个对象
let obj = {
c: 521
};
// 添加全局属性
window.c = 1314;
let fn4 = bind(add, obj, 10, 20);
console.log(fn4(30, 50)); //631
</script>
</body>
</html>
4.函数节流
节流的目的:为了防止函数在一定时间内触发多次,故设置一个时间间隔,使得函数在这一时间间隔中只能调用一次。
思路:在定义函数时设置两个参数:回调函数和间隔。返回一个函数对象,当两次函数调用时间间隔大于规定时间时,再调用回调函数。
function throttle(callback, wait) {
// 定义开始时间
let start = 0;
console.log(this) // 此处的this是window对象
// 返回结果是一个函数
return function (event) {
// 获取当前的时间戳
let now = Date.now();
// 判断
if (now - start >= wait) {
// 若满足条件,则执行回调函数
// 此处产生闭包,闭包中包含start和callback,当点击事件执行时,此函数内部的this为事件源。故此时可以使用call传入this来改变闭包中callback函数的this。
// 为何callback中原先的this是window?
// 个人理解:此处整个返回的function覆盖了原先addEventListener的回调函数,而原本的函数变为了callback(),当直接调用callback()时,相当于是window.callback(),所以此函数内部的this为window。
callback.call(this, event);
// 修改开始时间
start = now;
}
}
}
调用:
<!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>节流测试</title>
<script type="text/javascript" src="./src/throttle.js"></script>
<style>
body {
height: 2000px;
background: linear-gradient(#125, #eae);
}
</style>
</head>
<body>
<button class="test">
123456
</button>
<script type="text/javascript">
document.querySelector('.test').addEventListener('click', throttle(function (e) {
console.log(this) // 此处的this是事件绑定对象
}, 10000))
</script>
</body>
</html>
5.函数防抖
思路:传入回调函数和间隔时间。其中间隔时间用于设置定时器延迟时间。当第一次进入开启定时器后,将定时器赋给一个变量。第二次函数执行时,若定时器已经被触发了(此时原先指向定时器的变量被置为了空,这也代表了定时器执行了),就会新增一个定时器,并将变量指向这个新的定时器(此时因为原本的定时器没有变量指向它了,就会自动删除)。若定时器未被触发,此时变量不为空,那我们就需要手动删除原本变量指向的定时器,并重新让其指向一个新的定时器,以达到让定时器内部的逻辑重新计算时间再执行的效果。
function debounce(callback, time) {
// 定时器变量
let timeId = null;
// 返回一个函数
return function (e) {
// 判断
if (timeId != null) {
// 清空定时器
clearTimeout(timeId)
}
// 启动定时器
timeId = setTimeout(() => {
// 执行回调
// 此处使用箭头函数,this指向外层函数的this,及事件源
callback.call(this, e);
// 重置定时器变量
timeId = null
}, time)
}
}
<!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>防抖测试</title>
<script type="text/javascript" src="./src/debounce.js"></script>
</head>
<body>
<input type="text">
<script type="text/javascript">
let input = document.querySelector('input')
input.onkeydown = debounce(function (e) {
console.log(e.keyCode);
}, 1000)
</script>
</body>
</html>
6.数组map() 方法实现
思路:传入数组和回调函数。定义一个新数组。遍历传入的数组,让里面的每一个数都执行一遍回调函数之后push到我们定义的新数组中,最后返回。
function map(arr, callback) {
// 声明空数组
let result = [];
// 遍历数组
for (let i = 0; i < arr.length; i++) {
// 执行回调
result.push(callback(arr[i], i));
}
// 返回结果
return result
}
调用:
// 声明一个数组
let arr = [1, 2, 3, 4, 5]
// map
const result = map(arr, (item, index) => {
return item * 10
})
console.log(result) // [10, 20, 30, 40, 50]
7.数组 reduce()方法实现
思路:由于reduce方法中,会有一个初始值。所以我们可以定义一个要返回的变量,并把这个初始值赋值给这个变量。之后,循环遍历数组中的每一个数字当做回调函数的参数,在第一次循环时,原先定义的变量当做第一个参数,数组的第一个值当做第二个参数。并把回调函数的执行结果返回给变量。以此来达到reduce的功能。
function reduce(arr, callback, initValue) {
// 声明变量
let result = initValue
for (let i = 0; i < arr.length; i++) {
// 执行回调
result = callback(result, arr[i])
}
return result
}
调用:
let result = reduce(arr, function (res, value) {
return res + value
}, 0)
8.数组filter()方法实现
思路:因为filter会传入一个返回值为Boolean类型的回调函数。所以我们只需要循环遍历数组,并定义一个空数组。将数组中的每一个值都作为回调函数的参数进行调用,并把返回值为true情况下的数组值push到空数组中即可。最终返回的空数组,空数组中的值都是满足回调函数中的判断条件的值。
function filter(arr, callback) {
// 声明空数组
let result = [];
// 遍历数组
for (let i = 0; i < arr.length; i++) {
if (callback(arr[i], i)) {
result.push(arr[i])
}
}
return result
}
调用:
const result = filter(arr, item => item % 2 === 1)
console.log(result)
9.数组find()方法实现
思路:与filter()方法类似,不同的是,当数组中的值作为回调函数参数传入时,如果有一个满足,就会直接返回。而如果遍历完后仍没有返回值,则会返回一个undefined。
function find(arr, callback) {
for (let i = 0; i < arr.length; i++) {
if (callback(arr[i], i)) {
return arr[i];
}
}
// 如果没有遇到满足条件的,返回undefined;
return undefined
}
调用:
const result = find(arr, item => {
return item > 1000;
})
10.数组findIndex()方法实现
思路:与find()方法类似,不同的是,findIndex()在遍历数组并执行回调函数时,只要有满足条件的数组值,返回的当前值在该数组中的下标。而若遍历结束后仍没有满足条件的值,就会返回-1。
function findIndex(arr, callback) {
for (let i = 0; i < arr.length; i++) {
if (callback(arr[i], i)) {
return i;
}
}
// 如果没有遇到满足条件的,返回-1;
return -1
}
调用:
const result = findIndex(arr, (item, index) => {
return item > 2;
})
console.log(result);
11.数组every()方法实现
every方法功能:传入一个函数,函数返回一个Boolean,如果遍历完数组后,数组中每一个值作为函数参数传入后都返回true。则整个Array.every()返回true。
思路:将数组中的每个值都在回调函数中执行。判断是否有返回false的情况。如果有,则every函数返回false。若遍历完后仍没有返回,则返回true。
function every(arr, callback) {
// 遍历数组
for (let i = 0; i < arr.length; i++) {
// 执行回调,如果回调执行返回结果为false
if (!callback(arr[i], i)) {
return false;
}
}
// 如果都满足条件 则返回true
return true;
}
调用:
const result = every(arr, item => {
return item > 1;
})
console.log(result)
12.数组some()方法实现
some方法功能:传入一个函数,函数返回一个Boolean,如果遍历完数组后,数组中有一个值作为函数参数传入后可以返回true。则整个Array.every()返回true。
思路:与every()类似,区别在于,当循环遍历数组并执行回调时,当有返回true的情况发生时,则直接返回true。而若遍历结束仍没有返回,则返回false。
function some(arr, callback) {
// 遍历数组
for (let i = 0; i < arr.length; i++) {
// 执行回调,如果回调执行返回结果为false
if (callback(arr[i], i)) {
return true;
}
}
// 如果都满足条件 则返回true
return false;
}
调用:
const result = some(arr, item => {
return item > 1;
})
console.log(result)
13.数组去重
13.1 使用定义空数组的方式
思路:定义一个空数组,遍历要去重的数组,每次遍历时查看定义的空数组中是否有遍历到的这个数组值,如果没有,则添加进去
function unique(arr) {
// 声明一个空数组
const result = [];
// 遍历原始数组
arr.forEach(item => {
// 检测result数组中是否包含这个元素
// 此处为何不用find或findIndex? 这两个API主要用于查找满足一定条件的数,而要查询某个具体的数,应该用IndexOf
if (result.indexOf(item) === -1) {
// 若没有该元素,则插入到数组中
result.push(item)
}
});
return result;
}
13.2 使用定义对象进行辅助判断
思路:定义一个空数组及一个空对象,对象中以{数组值:true}来进行存储。每次循环要去重的数组值时,判断对象中是否有以这个为键的值存在,若没有,则添加进空数组中,并设置为对象添加新属性,新属性的值都为true。
此种方式的优点:对象查询相较于数组查询更快。
function unique2(arr) {
// 声明空数组
const result = []
// 声明空对象
const obj = {};
// 遍历数组
arr.forEach(item => {
// 将item作为下标存储在 obj 中
if (!obj[item]) {
obj[item] = true;
result.push(item)
}
})
return result;
}
13.3 使用ES6新增的Set()
思路:因为Set实例对象中不会有重复的值,所以可以把数组作为参数新建一个Set实例对象。再将实例对象使用扩展运算符加[]变为一个新数组。
function unique3(arr) {
// // 将数组转化为集合Set
// let set = new Set(arr);
// // 将set展开创建一个数组
// let array = [...set]; // 此处Set结构为具有 Iterator 接口的对象,所以可以使用扩展运算符
// return array;
return [...new Set(arr)];
}
14.数组concat()方法实现
思路:考虑到concat方法的参数可能是数组或普通类型变量,且可能有多个参数。所以先使用隐式参数args收集参数。然后定义一个空数组,先将原数组放入,再对传入的参数进行逐一判断,若为数组则解构后在放入空数组,否则直接放入。
function concat(arr, ...args) {
// 声明一个空数组
const result = [...arr];
// 遍历数组
args.forEach(item => {
// 判断item是否为数组
if (Array.isArray(item)) {
result.push(...item);
} else {
result.push(item);
}
});
return result;
}
15.slice数组切片
思路:首先要对传入的begin即end进行判断。判断其是否为undefined及是否arr.length<begin<end。如果满足以上条件,则定义一个空数组,并将原数组中的大于等于begin,小于end的数推入到新数组中。
function slice(arr, begin, end) {
// 若arr数组长度为0
if (arr.length === 0) {
return [];
}
// 判断 begin
begin = begin || 0;
if (begin > arr.length) {
return [];
}
// 判断 end
end = end || arr.length;
if (end < begin) {
end = arr.length;
}
// 声明一个空数组
const result = [];
// 遍历对象
for (let i = 0; i < arr.length; i++) {
if (i >= begin && i < end) {
// 将下标对应的元素压入数组
result.push(arr[i]);
}
}
return result;
}
16.数组扁平化
效果:将[1, 2, [3, 4, [5, 6]], 7] 变为:[1, 2, 3, 4, 5, 6, 7]
16.1 使用递归方式
思路:先声明空数组,然后判断原数组中的每一项,若为普通数字,则使用concat()方法将数字与空数组连接,并将返回值传给原先的空数组。若为数组,则对数组进行递归操作,新建立一个空数组,继续判断数组中的每一项并给与新建立的空数组连接,最终返回连接后的空数组,此时的返回的数组内部可以确保没有数组。则可以与最初的空数组连接。最终返回一个内部全为普通数据的数组。
function flatten1(arr) {
// 声明空数组
let result = [];
// 遍历数组
arr.forEach(item => {
// 判断
if (Array.isArray(item)) {
// result.push(...flatten1(item))
result = result.concat(flatten1(item))
} else {
// result.push(item)
result = result.concat(item)
}
})
return result;
}
16.2 使用Array.some
思路:声明数组result,并对传入的数组进行浅复制从而给数组result赋值,此时定义的数组result与原数组内容相同,地址不同。然后循环判断定义的数组result,使用Array.some()方法判断其内部是否有数组存在,若有,则解构result,并用一个空数组与其进行concat连接,由于concat方法既能连接数字,也能连接数组。所以此时result中的常量会被直接加入到空数组中,数组也会被解构一层后放到空数组中。然后再次循环判断concat一次后的result中是否还有数组,若有则继续上述操作,知道result中全为数字后再返回。
function flatten2(arr) {
// 声明数组
let result = [...arr];
// 循环判断
while (result.some((item) => Array.isArray(item))) {
// [1,2,[3,4,[5,6]],7]
result = [].concat(...result); //[1,2,3,4,[5,6],7]
}
return result;
}
17.数组分块
效果:chunk([1, 2, 3, 4, 5, 6, 7], 3) 返回值: [[1,2,3],[4,5,6],[7]]
思路:定义两个空数组result和tmp,先将tmp数组push进result中,循环遍历传入的数组arr,并将数据push进tmp中,当tmp的长度与传入的第二个参数相同时,则为tmp赋予新的地址(重新定义为空数组)。当tmp的长度又为0时,则继续push进result中,然后继续将原数组中的数据push进tmp中,直到循环结束。
function chunk(arr, size = 1) {
// 判断
if (arr.length === 0) {
return []
}
// 声明两个变量
let result = [];
let tmp = []; // [1,2,3]
// 遍历
arr.forEach(item => {
// 判断tmp元素的长度是否为0
if (tmp.length === 0) {
// 将 tmp 压入到result中
// 个人理解:此处通过push添加一个数组后,result中的第一个值和tmp指向同一个堆空间地址,当tmp=[]时,tmp被赋予了新地址,而result中的值仍指向原本的地址。
result.push(tmp);
}
// 将元素压入到临时数组temp中
tmp.push(item)
// 判断
if (tmp.length === size) {
tmp = [];
}
})
return result;
}
18.数组差集
效果:difference([1, 3, 5, 7], [5, 3, 8]) 返回值: [1, 7]
思路:遍历传入的第一个数组,使用Array.filter()函数过滤掉包含在第二个数组中的数据。最终将filter返回的数组返回。
function difference(arr1 = [], arr2 = []) {
// 判断参数
if (arr1.length === 0) {
return [];
}
if (arr2.length === 0) {
return arr1.slice(); // 此处效果等同于 return arr1
}
return arr1.filter(item => !arr2.includes(item))
}
19.删除数组元素
19.1 传入多个数字
效果:
let arr = [1, 3, 5, 3, 7];pull(arr, 2, 7, 3, 7)
pull函数返回值:[3, 3, 7] ,cosole.log(arr)得到:[1, 5]
思路:首先定义一个空数组result,同时使用隐式参数args收集传入的数字并组合成数组,然后循环遍历传入的数组arr,判断每一项是否包含在args中,若在则将该项push进result中,并使用splice方法删除,最后记得将循环项提前以为后再进行下一次循环。最终将result返回。
function pull(arr, ...args) {
// 声明空数组,保存删掉的元素
const result = [];
// 遍历 arr
for (let i = 0; i < arr.length; i++) {
// 判断当前元素是否存在于 args数组中
if (args.includes(arr[i])) {
// 删除当前的元素
// 将当前元素的值存入到result中
result.push(arr[i]);
// splice可以改变原数组
arr.splice(i, 1);
// 下标自减
i--;
}
}
// 返回
return result;
}
19.2 传入一个数组
效果:
let arr = [1, 3, 5, 3, 7]
console.log(pullAll(arr, [2, 3, 7])) // [3, 3, 7]
console.log(arr) // [1, 5]
思路:只需要把第二个数组解构,然后进行与上方相同操作
function pullAll(arr, arr2) {
return pull(arr, ...arr2)
}
20.获取数组某些元素
效果:
console.log(drop([1, 3, 5, 7, 9, 11], 2)) // [5, 7, 9, 11]
console.log(dropRight([1, 3, 5, 7, 9, 11], 2)); // [1, 3, 5, 7]
思路:使用数组的filter方法,通过比较filter的第二个参数index与函数的第二个参数size的大小,来过滤掉对应项
function drop(arr, size) {
// 过滤原数组,产生新数组
// return arr.filter((value, index) => {
// //
// return index >= size;
// })
return arr.filter((value, index) => index >= size)
}
function dropRight(arr, size) {
return arr.filter((value, index) => {
return index < (arr.length - size)
})
}