面试必备--JS函数工具库手写

1 篇文章 0 订阅

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)
    })
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值