手写原生函数

Posted MX羽林

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手写原生函数相关的知识,希望对你有一定的参考价值。

手写 Function.prototype.call:

Function.prototype.myCall = function (context, ...args) 
  context = context || window;
  const fn = Symbol(\'fn\');
  context[fn] = this;
  const result = context[fn](...args);
  delete context[fn];
  return result;

建议收藏原生 JavaScript 手写数组 API

本文将会先了解数组 API 的用法再模拟实现这些 API ,如果各位大佬觉得有什么不对的地方麻烦指点以下!

1. forEach 方法

这个方法会对数组元素的每一项运行传入的函数,没有返回值。相当于使用 for 循环来遍历数组。如:

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => {
    // 执行某些操作 
    item += 2
})
console.log(numbers);

我们发现并不会改变数组元素

可以利用 forEach 方法来替代 for 循环来遍历数组

我们再来看看下面的代码,再来总结

let arr1 = [{
    name: 'ljc',
    age: 19
}, {
    name: 'xy',
    age: 18
}]
arr1.forEach(item => {
    item.age += 1
})
console.log(arr1);

从上面两段代码,我们可以看出,两个成员的age属性值都加了 1

所以我们可以简单的得出一个结论:当数组中元素是值类型,forEach 绝对不会改变数组。当数组中元素是引用类型,则可以改变数组

注意:由于 forEach 方法没有返回值,因此 forEach 不支持链式操作

1-1 手写 forEach 方法

原生的forEach方法中接收2个参数 callbackthisArg ,并且 callback 函数传入三个参数,数组当前项的值,索引,数组本身

Array.prototype.myForEach = function (callback, thisArg) {
    // 判断调用该API的元素是否为null
    if (this == null) {
        throw new TypeError('this is null or not defined')
    }
    // 判断是否为function
    if (typeof callback !== "function") {
        throw new TypeError(callback + ' is not a function')
    }
    // 通过this得到调用者arr
    const arr = this
    // 确定循环变量
    let index = 0
    // 循环遍历给每个数组元素调用callback
    while (index < arr.length) {
        // 判断是否存在这个项
        if (index in arr) {
            // 通过call将this指向thisArg,并且传入3个参数
            callback.call(thisArg, arr[index], index, arr)
        }
        index++
    }
}

2. map 方法

与 forEach 方法相比,map 方法有返回值而 forEach 方法没有返回值。

map也叫映射,也就是将原数组映射成一个新数组

  1. 数组中的每一个元素都会调用一个提供的函数后返回结果。
  2. 会新建一个数组,需要有承载对象,也就是会返回一个新的对象
  3. 除非用原有数组去承载,否则原有数组不会改变

使用方法

let arr = [1, 2, 3, 4, 5]
let newArr = arr.map(item => item * 2)
console.log(newArr); //  [2, 4, 6, 8, 10]

map需要有返回值,可以利用箭头函数来简写

易错点

map中的每一个元素都要执行回调函数,所以必须要有 return,因此不能采用map对数组进行过滤

可以看到灰灰的undefined,再见

2-2 手写 map 方法

  1. 首先要排除空数组以及没有回调函数的情况
  2. 根据map的要求需要新建数组,执行函数,返回数组
Array.prototype.myMap = function (callback, thisArg) {
    // 和forEach相同需要进行两个排除
    if (this == undefined) {
        throw new TypeError('this is null or not defined');
    }
    if (typeof callback !== 'function') {
        throw new TypeError(callback + ' is not a function');
    }
    // 与forEach不同的是,map会返回一个新数组
    const ret = []
    // 获得函数调用者
    const arr = this
    // 数组长度
    let len = arr.length
    // 对每一个元素执行回调函数
    for (let i = 0; i < len; i++) {
        // 检查i是否在arr
        if(i in arr) {
            ret[i] = callback.call(thisArg, arr[i], i, arr)
        }
    }
    // 返回一个处理后的数组
    return ret
}

3. filter

filter从名字上看可以知道是它是用来做筛选过滤的。和map一样,会返回一个新的对象数组,并不会改变原数组

使用方法

从而实现了筛选出数组元素小于 3 的元素

3-3 手写 filter 方法

map方法相比,filter需要将满足条件的元素组成新数组返回

Array.prototype.myFilter = function(callback,thisArg) {
    if (this == undefined) {
        throw new TypeError('this is null or not defined');
    }
    if (typeof callback !== 'function') {
        throw new TypeError(callback + ' is not a function');
    }
    // 新数组
    const res = []
    // 保存this
    const arr = this
    // 提前计算数组长度
    const len = arr.length
    for(let i = 0;i<len;i++) {
        if(i in arr) {
            // 判断元素经过函数调用后,是否有返回值
            // 从而来判断是否满足筛选规则,
            if(callback.call(thisArg,arr[i],i,arr)) {
                res.push(arr[i])
            }
        }
    }
    // 最后记得返回新数组噢
    return res
}

4. some 方法

some方法用于检查数组中是否有符合条件的值,返回值是个布尔值

使用方法

some方法对于性能来说比较友好,因为不需要全部遍历,只要找到一个符合条件的就会9返回true

我们根据这个原则可以手写一个some方法

4-4 手写 some 方法

Array.prototype.mySome = function (callback, thisArg) {
    if (this == undefined) {
        throw new TypeError('this is null or not defined');
    }
    if (typeof callback !== 'function') {
        throw new TypeError(callback + ' is not a function');
    }
    let arr = this
    let len = arr.length
    for (let i = 0; i < len; i++) {
        if (i in arr) {
            if (callback.call(thisArg, arr[i], i, arr)) {
                return true
            }
        }
    }
    return false
}

5. every 方法

some相比,每个成员都满足条件才返回true,有一个不满足都返回false

只有全满足才会返回true

5-5 手写 every 方法

Array.prototype.myEvery = function (callback, thisArg) {
    if (this == undefined) {
        throw new TypeError('this is null or not defined');
    }
    if (typeof callback !== 'function') {
        throw new TypeError(callback + ' is not a function');
    }
    const arr = this
    const len = arr.length
    for (let i = 0; i < len; i++) {
        if (i in arr) {
            if (!callback.call(thisArg, arr[i], i, arr)) {
                return false
            }
        }
    }
    return true
}

6. find 和 findIndex 方法

找到一个符合条件的元素,找的到就返回当前元素,找不到就返回undefined

和 find 方法同形的还有 findIndex 方法,该方法返回第一个满足条件的元素的索引值

find 使用方法

返回满足的元素

findIndex 使用方法

返回满足的索引

6-6 手写 find 方法

通过循环遍历数组,调用一下传入的函数,如果满足条件则将当前的index对应的数组元素返回,只返回第一个噢

Array.prototype.myFind = function (callback, thisArg) {
    if (this == undefined) {
        throw new TypeError('this is null or not defined');
    }
    if (typeof callback !== 'function') {
        throw new TypeError(callback + ' is not a function');
    }
    // 保存this,也就是调用者
    const arr = this
    const len = arr.length
    for (let i = 0; i < len; i++) {
        if (i in arr) {
            if (callback.call(thisArg, arr[i], i, arr)) {
                return arr[i]
            }
        }
    }
    return undefined
}

findIndex 方法

与 find 不同之处在于返回值,只需要将return arr[i]改成return i即可

小场景

对于上面的6,7个数组方法,会发现其实实现起来的差别也就是那几行代码,记起来也挺不容易的,它们的使用场景更是不知怎么切入,利用一个小场景来展现这些 API 的使用场景

前情提要:在一个公司里,老板正在考虑给员工升职加薪…

公司员工数据

let staff = [
    {name: 'a', salary: 20000, age: 36},
    {name: 'b', salary: 19000, age: 34},
    {name: 'c', salary: 18000, age: 20},
    {name: 'd', salary: 17000, age: 18}
]

🤵老板 :“今年业绩表现不错,所有员工工资涨1000”

👨‍🦲程序员小哥:“简单,用 forEach 就可以了,代码和结果像下面这样”

staff.forEach(item => item.salary += 1000)

🤵老板:“给我整理成一份工资表格”

👨‍🦲程序员小哥:“没问题,map 有返回值,可以用 map”

w = staff.map(item => item.salary += 1000)
console.log(w) // [21000, 20000, 19000, 18000]

🤵老板:“公司成立这么多年了,给我一份我们公司33岁以上的员工名单吧”

👨‍🦲程序员小哥:“好的,没问题,用filter”

w = staff.filter(item => item.age > 33)

👨‍🦲程序员小哥:“a,b员工年龄都33岁以上了”

🤵老板:“那你再帮我看看有没有18岁以下的员工”

👨‍🦲程序员小哥:“好的,用some方法看了一下,我们没有年龄小于18岁的员工”

w = staff.some(item => item.age < 18) // false

🤵老板:“公司现在上市了,你看看我们公司员工工资是不是都1.6w以上”

👨‍🦲程序员小哥:“真不错啊,全都1.6w以上了,还有什么需要吗?”

w = staff.every(item => item.salary > 16000) // true

🤵老板:“那你再帮我找个年龄35岁以上的吧,第一个就好”

👨‍🦲程序员小哥:“简单查了一下第一个35以上的,叫a”

w = staff.find(item => item.age > 35) // {name: "a", salary: 20000, age: 36}

🤵老板:“它在公司的员工数据里排在第几个呀”

👨‍🦲程序员小哥:“你好无聊,这都要看”

w = staff.findIndex(item => item.age > 35) // 0

👨‍🦲程序员小哥:“0,第一个,元老级别”

🤵老板:“挺不错的,你技术不错嘛,那你把工资总和算出来,叫秘书打钱给财务吧”

👨‍🦲程序员小哥:“…稍等,我再学一下 reduce

7. reduce 方法

不同于迭代方法,reduce是一种归并方法,归并并不是对每一项都执行目标函数,可以概括成以下几步:

  1. 不断地对数组地前两项取出,对它执行目标函数,计算得到的返回值
  2. 把返回值插到数组首部,也就是作为ayyay[0]
  3. 持续执行这个过程,直至数组中的每一项都访问一次
  4. 返回最终结果

举例说明

const arr = [1, 2, 3]
const res = arr.reduce((prev, cur) => prev + cur)
console.log(res); // 6

在上面的代码中,reduce 做了一下几步归并操作

[1, 2, 3] // 取出 1 和 2 ,执行 1 + 2 填回 3
[3, 3] // 取出 3 3 ,填回 6
[6] // 最终返回6

7-7 手写 reduce 方法

根据上面的4步规则来写

Array.prototype.myReduce = function (callback, initialValue) {
    // 判断调用该API的元素是否为null
    if (this == null) {
        throw new TypeError('this is null or not defined')
    }
    // 判断是否为function
    if (typeof callback !== "function") {
        throw new TypeError(callback + ' is not a function')
    }
    const arr = this
    const len = arr.length
    // 第二个参数
    let accumulator = initialValue
    let index = 0
    // 如果第二个参数是undefined 则数组的第一个有效值
    // 作为累加器的初始值
    if (accumulator === undefined) {
        // 找到数组中的第一个有效值 不一定就是arr[0]
        while (index < len && !(index in arr)) {
            index++
        }
        if (index >= len) {
            throw new TypeError('Reduce of empty array with no initial value')
        }
        // 输出第一个有效数组元素,作为累加器的第一个元素
        accumulator = arr[index++]
    }
    while (index < len) {
        if (index in arr) {
            // arr[index] 为 accumulator 的下一个元素
            accumulator = callback.call(undefined, accumulator, arr[index], index, arr)
        }
        // 持续后移
        index++
    }
    // 返回结果
    return accumulator
}

7-x 利用 reduce 实现 map

在很多地方都看到了这个题目

实现思路:将每次遍历的元素,作为传入的函数的参数,并将函数执行结果存入一个新数组中返回

核心:map函数接收一个函数作为参数,作为参数的函数接收三个参数值,分别是遍历数组的每一项元素,元素的索引和数组本身。这三个参数刚好和reduce函数接收的第一个函数参数的第2、3、4个参数是对应的

Array.prototype.mapReduce = function (callback, context = null) {
    if (this == null) {
        throw new TypeError('this is null or not defined')
    }
    // 判断是否为function
    if (typeof callback !== "function") {
        throw new TypeError(callback + ' is not a function')
    }
    let arr = this
    return arr.reduce((pre, cur, index, array) => {
        let res = callback.call(context, cur, index, array)
        return [...pre, res]
    })
}

今天的手撕代码就到这里了,如果有什么问题欢迎在评论区补充指正!

以上是关于手写原生函数的主要内容,如果未能解决你的问题,请参考以下文章

原生 JavaScript 手写数组 API

建议收藏原生 JavaScript 手写数组 API

神来之笔原生 JavaScript 手写数组 API

原生JS封装创建多级菜单函数

Java手写原生的LRU算法

js 原生手写AJAX