JS数组、对象常用方法底层实现,你真的了解,

337 阅读11分钟

JS数组和对象的方法在我们日常开发中都在频繁使用,对于其底层实现原理,你真的了解?

数组和对象的几种遍历方法请点击此链接查看

Array

1. forEach

    Array.prototype.my_forEach= function (callback) {
         for (let i = 0; i < this.length; i++) {
              callback(this[i], i, this)
         }
    }
    const arr = [1, 2, 3, 4, 5]

    arr.my_forEach((item, index, arr) => {
        console.log(item, index, arr)
    })
    // 1 0 [1, 2, 3, 4, 5]
    // 2 1 [1, 2, 3, 4, 5]
    // 3 2 [1, 2, 3, 4, 5]
    // 4 3 [1, 2, 3, 4, 5]
    // 5 4 [1, 2, 3, 4, 5]

a. 使用return可以终止循环?

arr.my_forEach((item, index, arr) => {
       if (item === 3) {
           return
       }
        console.log(item, index, arr)
    })
    // 1 0 [1, 2, 3, 4, 5]
    // 2 1 [1, 2, 3, 4, 5]
    // 4 3 [1, 2, 3, 4, 5]
    // 5 4 [1, 2, 3, 4, 5]


    function fn() {
        for (let i = 0; i < 5; i++) {
            if (i === 3) {
                return
            }
            console.log('i', i)
        }
    }

    fn()
    // i 0
    // i 1
    // i 2

答: 从上面代码可以看出,在 forEach 中使用 return 无法终止循环;

b. 使用break可以终止?

arr.my_forEach((item, index, arr) => {
   if (item === 3) {
       break;
   }
    console.log(item, index, arr)
})

答: 使用break直接报错

image.png

面试的时候有些可能会到如何终止forEach循环

除非抛出异常,否则没有办法停止或中断 forEach() 循环。
如果有这样的需求,则不应该使用 forEach() 方法。 可以通过像 for、for...of 和 for...in 这样的循环语句来实现提前终止。
当不需要进一步迭代时,诸如 every()、some()、find() 和 findIndex() 等数组方法也会立即停止迭代。

2. filter

Array.prototype.my_filter = function (callback) {
    const newArr = [];
    for (let i = 0; i < this.length; i++) {
        callback(this[i], i, this) && newArr.push(this[i])
    }
    return newArr
}

const arr = [0, 1, 2, 3, 4, 5]
const res = arr.my_filter((item, index, arr) => item >= 3);
console.log(res)
// [3, 4, 5]

3. map

Array.prototype.my_map = function (callback) {
    const newArr = [];
    for (let i = 0; i < this.length; i++) {
        newArr.push(callback(this[i], i, this));
    }
    return newArr
}

const arr = [{
    name: '张三',
    age: 18
}, {
    name: '李四',
    age: 19
}, {
    name: '王五',
    age: 20
}]
const res = arr.my_map((item, index, arr) => `${item.name}-${index}`);
console.log(res)
// ['张三-0', '李四-1', '王五-2']

4. some

Array.prototype.my_some = function (callback) {
    let flag = false
    for (let i = 0; i < this.length; i++) {
        if (callback(this[i], i, this)) {
            flag = true
            break;
        }
    }
    return flag
}

const arr = [{
    name: '张三',
    age: 18
}, {
    name: '李四',
    age: 19
}, {
    name: '王五',
    age: 20
}]
const res_1 = arr.my_some((item, index, arr) => item.age > 18);
const res_2 = arr.my_some((item, index, arr) => item.age > 100);
console.log('res_1', res_1)
console.log('res_2', res_2)
// res_1 true
// res_2 false

5. every

Array.prototype.my_every = function (callback) {
    let flag = true
    for (let i = 0; i < this.length; i++) {
        if (!callback(this[i], i, this)) {
            flag = false
            break;
        }
    }
    return flag
}

const arr = [{
    name: '张三',
    age: 18
}, {
    name: '李四',
    age: 19
}, {
    name: '王五',
    age: 20
}]
const res_1 = arr.my_every((item, index, arr) => item.age > 18);
const res_2 = arr.my_every((item, index, arr) => item.age >= 18);
console.log('res_1', res_1)
console.log('res_2', res_2)
// res_1 false
// res_2 true

6. find —— 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。

Array.prototype.my_find = function (callback) {
    for (let i = 0; i < this.length; i++) {
        if (callback(this[i], i, this)) {
            return this[i]
        }
    }

    return void 0
}

const arr = [{
    name: '张三',
    age: 18
}, {
    name: '李四',
    age: 19
}, {
    name: '王五',
    age: 20
}]
const res = arr.my_find((item, index, arr) => item.age === 19);
console.log(res)
// {name: '李四', age: 19}

const res_2 = arr.my_find((item, index, arr) => item.age === 100);
console.log(res_2)
// undefined

7. findIndex() —— 方法返回数组中满足提供的测试函数的第一个元素的索引。若没有找到对应元素则返回 -1。

Array.prototype.my_findIndex = function (callback) {
    for (let i = 0; i < this.length; i++) {
        if (callback(this[i], i, this)) {
            return i
        }
    }

    return -1
}

const arr = [{
    name: '张三',
    age: 18
}, {
    name: '李四',
    age: 19
}, {
    name: '王五',
    age: 20
}]
const res = arr.my_findIndex((item, index, arr) => item.age === 20);
console.log(res)
// 2

const res_2 = arr.my_findIndex((item, index, arr) => item.age === 100);
console.log(res_2)
// -1

8. indexOf —— 方法返回数组中第一次出现给定元素的下标,如果不存在则返回 -1。

lastIndexOf() 方法返回数组中给定元素最后一次出现的索引,如果不存在则返回 -1。该方法从 fromIndex 开始向前搜索数组。

Array.prototype.my_indexOf = function (value, start = 0) {
    for (let i = start; i < this.length; i++) {
        if (value === this[i]) {
            return i
        }
    }
    return -1
}

const arr = [1, 2, 3, NaN]
const res = arr.my_indexOf(2);
console.log(res)
// 1

const res_2 = arr.my_indexOf(NaN);
console.log(res_2)
// -1

9. includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回 false。

Array.prototype.my_includes = function (value, start = 0) {
    if (start < 0) {
        start = this.length + start
    }
    for (let i = start; i < this.length; i++) {
        if (this[i] === value || (Number.isNaN(value) && Number.isNaN(this[i]))) {
            return true
        }
    }
    return false
}

const arr = [1, 2, 3, NaN]
const res = arr.my_includes(3, 1);
const res_2 = arr.my_includes(3, 4);
console.log(res)
console.log(res_2)
// true
// false

const res_3 = arr.my_includes(NaN);
console.log(res_3);
// true

const res_4 = arr.my_includes(1,1);
console.log(res_4)

includes 和 indexOf 的区别

  • includes是ES6的语法,语义化更强,可判断是否存在 NaN
  • indexOf使用严格相等(===)进行判断,判断是否存在 NaN 时始终返回 -1

补充: isNaN 和 Number.isNaN 的主要区别在于对参数的处理方式:

isNaN 会尝试将传入的参数转换为数字,如果转换结果是 NaN,则返回 true 。
但问题是,如果传入的参数本身不是数字类型,isNaN 会先进行类型转换,这个转换过程可能导致一些不符合预期的结果。<br>
Number.isNaN 只会在参数本身的值是 NaN 时才返回 true ,不会对参数进行类型转换。
总的来说,Number.isNaN 更准确和可靠,用于专门判断一个值是否真正是 NaN

10. reduce —— reduce(callbackFn, initialValue)

Array.prototype.my_reduce = function (callback, initialValue) {
    let start = 0, accumulator;
    if (initialValue) {
        accumulator = initialValue
    } else {
        accumulator = this[0]
        start = 1
    }

    for (let i = start; i < this.length; i++) {
        accumulator = callback(accumulator, this[i], i, this)
    }

    return accumulator
}

const arr = [1, 2, 3, 4, 5]
const res_1 = arr.my_reduce((accumulator, item, index, arr) => {
    console.log('accumulator', accumulator)
    return accumulator + item
}, 10);
// accumulator 10
// accumulator 11
// accumulator 13
// accumulator 16
// accumulator 20

console.log(res_1)
// 25

const res_2 = arr.my_reduce((accumulator,item, index, arr) => {
    console.log('accumulator', accumulator)
    return accumulator + item
});
// accumulator 1
// accumulator 3
// accumulator 6
// accumulator 10
console.log(res_2)
// 15

11. reduceRight —— reduceRight(callbackFn, initialValue)

Array.prototype.my_reduceRight = function (callback, initialValue) {
    let start = this.length - 1, accumulator;
    if (initialValue) {
        accumulator = initialValue
    } else {
        accumulator = this[this.length - 1]
        start = this.length - 2;
    }

    for (let i = start; i >= 0; i--) {
        accumulator = callback(accumulator, this[i], i, this)
    }

    return accumulator
}

const arr = [1, 2, 3, 4, 5]
const res_1 = arr.my_reduceRight((accumulator, item, index, arr) => {
    console.log('accumulator', accumulator)
    return accumulator + item
}, 10);
// accumulator 10
// accumulator 15
// accumulator 19
// accumulator 22
// accumulator 24
console.log(res_1)
// 25

const res_2 = arr.my_reduceRight((accumulator,item, index, arr) => {
    console.log('accumulator', accumulator)
    return accumulator + item
});
// accumulator 5
// accumulator 9
// accumulator 12
// accumulator 14
console.log(res_2)
// 15

12. flat —— flat(depth)

  • flat(depth) 方法属于复制方法。它不会改变 this 数组,而是返回一个浅拷贝,该浅拷贝包含了原始数组中相同的元素。
  • depth —— 指定要提取嵌套数组的结构深度,默认值为 1。
  • 如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数
const arr = [1, 2, 3, [4, 5], [[6, ['a', 'b']], 7]];
console.log('flat',arr.flat(0))
console.log('flat',arr.flat(1))
console.log('flat',arr.flat(2))
console.log('flat', arr.flat(Infinity))
console.log('---------使用for循环实现')

Array.prototype.my_Flat = function(depth = 1) {
    const flatten = (arr, currentDepth) => {
        let result = [];
        for (let item of arr) {
            if (Array.isArray(item) && currentDepth <= depth) {
                result = result.concat(flatten(item, currentDepth + 1));
            } else {
                result.push(item);
            }
        }
        return result;
    };

    return flatten(this, 1);
}

console.log('my_Flat', arr.my_Flat(0))
console.log('my_Flat', arr.my_Flat(1))
console.log('my_Flat', arr.my_Flat(2))
console.log('my_Flat', arr.my_Flat(Infinity))


console.log('---------使用reduce实现')
function arrFlat(arr, i = 1) {
    return arr.reduce((newArr, item) => {
        if (i <= 1) {
            newArr.push(item)
            return newArr
        }
        return newArr.concat((i > 1 && Array.isArray(item)) ? arrFlat(item,i - 1) : item)
    }, [])
}

image.png

13. splice (相对较难一点) —— splice() 方法就地移除或者替换已存在的元素或添加新的元素。

参数:
start ———— 从 0 开始计算的索引,表示要开始改变数组的位置,它会被转换成整数。

  • 负索引从数组末尾开始计算——如果 -buffer.length <= start < 0,使用 start + array.length。
  • 如果 start < -array.length,使用 0。
  • 如果 start >= array.length,则不会删除任何元素,但是该方法会表现为添加元素的函数,添加所提供的那些元素。
  • 如果 start 被省略了(即调用 splice() 时不传递参数),则不会删除任何元素。这与传递 undefined 不同,后者会被转换为 0。

deleteCount(可选) ——— 一个整数,表示数组中要从 start 开始删除的元素数量。

  • 如果省略了 deleteCount,或者其值大于或等于由 start 指定的位置到数组末尾的元素数量,那么从 start 到数组末尾的所有元素将被删除。但是,如果你想要传递任何 itemN 参数,则应向 deleteCount 传递 Infinity 值,以删除 start 之后的所有元素,因为显式的 undefined 会转换为 0。
  • 如果 deleteCount 是 0 或者负数,则不会移除任何元素。

item1、…、itemN(可选) ——— 从 start 开始要加入到数组中的元素。

  • 如果不指定任何元素,splice() 将只从数组中删除元素。。

返回值 ————— 一个包含了删除的元素的数组。

  • 如果只移除一个元素,则返回一个元素的数组;
  • 如果没有删除任何元素,则返回一个空数组。
function isNull(val) {
    return val === undefined || val === null || val === ''
}
Array.prototype.my_splice = function(start, deleteCount, ...items) {
    if (!arguments.length || start >= this.length || deleteCount <= 0 || arguments.length >= 2 && isNull(deleteCount)) return []

    if (start < -this.length || isNull(start)) start = 0
    if (-this.length <= start && start < 0) {
        start = this.length + start
    }
    console.log('start', start)

    // newIndex = start + deleteCount
    let length = Infinity;
    if (arguments.length < 2 || start + deleteCount > this.length - 1) {
        length = this.length - start
    } else if (isNull(deleteCount)) {
        length = 0
    } else {
        length = deleteCount
    }
    console.log('length', length)

    if (!length) return []

    const res = [];
    const tempArr = [...this];
    for (let i = start; i < start + items.length; i++) {
        this[i] = items[i - start]
    }
   this.length = start + items.length

    if(items.length > length) {
        for (let i = start + length; i < tempArr.length; i++) {
            this.push(tempArr[i])
        }
    }


    if(items.length < length) {
        // 差值
        const difference = length - items.length;
        for (let i = start + items.length; i < tempArr.length; i++) {
            this[i] = tempArr[i + difference]
        }
        this.length = this.length - difference
    }

    // 处理返回值
    for (let i = start; i < start + length; i++) {
        res.push(tempArr[i])
    }
    return res
}

const arr = [1, 2, 3, 4, 5]
console.log('res:', arr.my_splice(2, 1, 'a', 'b'), '-----arr', arr)
// start 2
// length 1
// res: [3] -----arr: [1, 2, 'a', 'b', 4, 5]

Object

1. Object.keys

  • Object.keys() 静态方法返回一个由给定对象自身的可枚举的字符串键属性名组成的数组。
  • Object.keys() 返回一个数组,其元素是字符串,对应于直接在对象上找到的可枚举的字符串键属性名。
  • 这与使用 for...in 循环迭代相同,只是 for...in 循环还会枚举原型链中的属性。
  • Object.keys() 返回的数组顺序和与 for...in 循环提供的顺序相同。
const obj = {
    name: 'zs',
    age: '18',
    1: '1',
    [Symbol()]: 'symbol',
}

Object.prototype.my_keys = (obj) => {
    const keys =[]
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            keys.push(key)
        }
    }

    return keys
}

console.log(Object.my_keys(obj))    // ['1', 'name', 'age']

2. Object.values

  • Object.values() 静态方法返回一个给定对象的自有可枚举字符串键属性值组成的数组。
  • Object.values() 返回一个数组,其元素是直接在 object 上找到的可枚举字符串键属性值。
const obj = {
    name: 'zs',
    age: '18',
    1: '1',
    [Symbol()]: 'symbol',
}

Object.prototype.my_values = (obj) => {
    const values =[]
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            values.push(obj[key])
        }
    }

    return values
}

console.log(Object.my_values(obj))  // ['1', 'zs', '18']

3. Object.entries

  • Object.entries() 静态方法返回一个数组,包含给定对象自有的可枚举字符串键属性的键值对。
  • Object.entries() 返回一个数组,其元素是直接在 object 上找到相应的可枚举字符串键属性的键值对数组。
const obj = {
    name: 'zs',
    age: '18',
    1: '1',
    [Symbol()]: 'symbol',
}

Object.prototype.my_entries = (obj) => {
    const entries =[]
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            entries.push([key, obj[key]])
        }
    }

    return entries
}

console.log(Object.my_entries(obj)) // [['1', '1'],['name', 'zs'], ['age', '18']]

4. Object.fromEntries

  • Object.fromEntries() 静态方法将键值对列表转换为一个对象。
  • Object.fromEntries() 方法接收一个键值对列表,并返回一个新对象,该对象的属性由这些条目给定。
  • iterable 参数应该是实现了 Symbol.iterator 方法的可迭代对象。
  • 该方法返回一个可迭代对象,产生包含两个元素的类数组对象。第一个元素是将用作属性键的值,第二个元素是要与该属性键关联的值。
Object.prototype.my_fromEntries = function (arr) {
    const obj = {};
    // for (let i = 0; i < arr.length; i++) {
    //     const [key, value] = arr[i]
    //     obj[key] = value
    // }

    // 或者
    for (const [key, value] of arr) {
        obj[key] = value
    }
    return obj
}

console.log(Object.my_fromEntries(arr))
// {1: 'a', a: 1, b: 2, c: 3}

4. Object.is

  • Object.is() 静态方法确定两个值是否为相同值。。
  • Object.is() 和 === 之间的唯一区别在于它们处理带符号的 0 和 NaN 值的时候。
  • === 运算符(和 == 运算符)将数值 -0 和 +0 视为相等,但是会将 NaN 视为彼此不相等。
Object.prototype.my_is = (x, y) => {
    if (x === y) {
        // 处理特殊情况: +0 和 -0
        return x !== 0 || 1 / x === 1 / y
    }

    // 处理 NaN 的情况
   return x !== x && y !== y
}

console.log(Object.my_is(+0, -0)) // false
console.log(Object.my_is(NaN, NaN)) // true
console.log(Object.my_is(1, 1)) // true
console.log(Object.my_is(1, 2)) // false
console.log(Object.my_is(1, 'true')) // false

5. Object.assign —— Object.assign(target, ...sources) 静态方法将一个或者多个源对象中所有可枚举的自有属性复制到目标对象,并返回修改后的目标对象。

  • 如果目标对象与源对象具有相同的键(属性名),则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的同名属性。
  • Object.assign() 方法只会拷贝源对象可枚举的的自有属性到目标对象。
  • 该方法在源对象上使用 [[Get]],在目标对象上使用 [[Set]],因此它会调用 getter 和 setter。
  • 故它对属性进行赋值,而不仅仅是复制或定义新的属性。如果合并源对象包含 getter 的新属性到原型中,则可能不适合使用此方法。
Object.prototype.my_assign = function (target, ...sources) {
    if (target === null || target === undefined) {
        throw new TypeError('Cannot convert undefined or null to object');
    }

    //  1. 确保 target 是一个对象:
    //      如果 target 是原始类型的值(如数字、字符串、布尔值等),Object(target) 会将其转换为对应的包装对象(如 Number、String、Boolean 的实例)。
    //      如果 target 已经是一个对象,那么它会保持不变。这样可以确保后续的属性赋值操作是在一个对象上进行。
    // 2. 创建一个新的对象引用:
    //      避免直接修改原始的 target 对象。通过创建一个新的对象引用,即使在赋值过程中出现问题,也不会破坏原始的 target。
    //      同时,这也符合 Object.assign 的行为,即返回一个新的对象,而不是修改原始的目标对象。
    const obj = Object(target);
    console.log(obj)

    for (let source of sources) {
        for (let key in source) {
            if (source.hasOwnProperty(key)) {
                obj[key] = source[key];
            }
        }
    }

    return obj;
}

console.log(Object.my_assign({a: 1}, {b: 2}, {c: 3}))
// {a: 1, b: 2, c: 3}

如果对您有帮助,收藏⭐点赞👍哦