Javascript数组研究02_手写实现_at_concat_copyWithin_entries_every
目录
1 Array.at()
1.1基本介绍
1.2 手写实现
2 Array.concat()
2.1 基本介绍
2.2 手写实现-获取构造函数与concat实现
3 Array.copyWithin()
3.1 基本介绍
3.2 手写实现
4 Array.entries()
4.1 基本介绍
4.2 手写实现
4.2.1 手写实现返回迭代器对象
4.2.2 使用generator实现迭代器对象
5 Array.every()
5.1 基本介绍
5.2 手写实现
1 Array.at()
1.1基本介绍
接收一个整数值,返回对应索引的元素,允许正数和负数。负数从-1为数组最后一个元素开始倒数,语法如下:
Array.at(index)
边界条件与输入输出、注意事项如下所示:
输入:输入index,首先被转换为整数。
输出:如果index > length 或者 -index < -length 返回undefined,正数就返回对应下标元素,负数返回length +index对应下标元素。
注意事项:at()方法是通用的。
1.2 手写实现
手写实现代码如下所示:
// 1. 手写at
// 处理空槽时返回undefined
class MyArray extends Array {
}MyArray.prototype.at = function(index){index = Number(index)const length = this.lengthif(index >= length || -index < -length) return undefinedif(index < 0) return this[index + length]else return this[index]
}const arr_1 = new MyArray(1,2,3)
console.log(arr_1) // output: [ 1, 2, 3 ]
console.log(arr_1.at(1)) // output: 2
console.log(arr_1.at(-1)) // output: 3
console.log(arr_1.at(3)) // output: undefined
console.log(arr_1.at(-4)) // output: undefined
console.log(arr_1.at("-1")) // output: 3
const arr_3 = new MyArray(4)
console.log(arr_3.at(1)) // output: undefined
const arr_2 = new Array(5)
console.log(arr_2.at(1)) // output: undefined
- 将索引转换为数组
- 判断索引范围的合法性
- 根据索引正负返回对应下标元素
2 Array.concat()
2.1 基本介绍
concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
concat()
concat(value0)
concat(value0, value1)
concat(value0, value1, /* … ,*/ valueN)
输入: 数组/值,类数组(含有类数组索引和length属性)
输出:返回一个新的数组实例。
注意事项:通用的,是一种复制方法(不改变原数组),返回的是原始参数的浅拷贝。面对[Symbol.isConcatSpreadable]属性设置为真的数组或类数组对象,参数的每个元素将会独立的添加到最终数组当中。普通值将会直接放入新数组当中。如果原数组是稀疏数组将会保留空槽。
2.2 手写实现-获取构造函数与concat实现
因为concat需要返回新的数组实例,所以编写一个函数专门用于获取数组的构造函数,其基本逻辑为下:
- 首先检查[Symbol.species]属性返回的构造函数是否满足条件,满足则返回,否则进入下一步判断。
- 其次检查默认的构造函数,满足则返回,否则进入下一步判断。
- 最后都不满足就返回默认的构造器Array。
const getConstructor = (obj) => {const defaultConstructor = Arrayif(obj[Symbol.species] !== undefined){if(typeof obj[Symbol.species] === "function"){return obj[Symbol.species]}else{throw new TypeError("Symbol.species must be a constructor")}}else if(obj.constructor !== undefined){if(typeof obj.constructor === "function"){return obj.constructor}else{return defaultConstructor}}
}
实现了获取构造函数的函数之后就可以编写concat函数了,如下所示:
MyArray.prototype.concat = function(...args){// 通过Symbol.species获取当前类的构造函数const con = getConstructor(this)const res = new con([])// 将原始数组元素加入新数组for(let i = 0; i < this.length; i ++){if(i in this){res.push(this[i])}else{// 如果原始数组有空槽,新数组长度加1res.length ++}}// 遍历数组元素for(let i = 0; i < args.length; i ++){// 如果是concat可展开(类)数组对象或者数组if((typeof args[i] === "object" && args[i][Symbol.isConcatSpreadable]) || Array.isArray(args[i])){// 遍历展开添加元素for(let j = 0; j < args[i].length; j ++){if(j in args[i]){res.push(args[i][j])}else{res.length ++}}}else{res.push(args[i])}}return res
}const notArray = {0: "hello",1: "world",length: 2,[Symbol.isConcatSpreadable]: true
}
const arr_4 = new MyArray(1,2,3)
const arr_5 = new MyArray(4,5,6)
const arr_6 = arr_4.concat(arr_5, 7, [8, ,[9]], notArray)
console.log(arr_6) // output: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 'hello', 'world'
console.log(arr_6 instanceof MyArray) // output: true
注意以下几点:
- 注意返回实例类型与调用实例类型的一致性,通过getConstructor函数实现。
- 空槽的处理,通过直接增加结果数组的长度来添加空槽。
- 需要展开处理的数组或者类数组的判断条件,如果目标是对象且[Symbol.isConca tSpreadable]设置为true或者是数组的条件下都要进行展开操作。
3 Array.copyWithin()
3.1 基本介绍
copyWithin() 方法浅复制数组的一部分到同一数组中的另一个位置,并返回它。语法如下所示:
copyWithin(target)
copyWithin(target, start?)
copyWithin(target, start?, end?)
输入:target序列开始替换的下标位置。 start是要复制元素的其实位置,默认为0。end为要复制元素的结束位置(不包含end)。
输出:修改后的数组。
注意事项:
- copyWithin方法是修改方法(修改原数组内容,不改变长度),保留空槽,通用的。
- target的有效性,首先将被转换为整数,负数索引需要加上length,如果还是小于0那么使用0。如果正数索引大于length不会拷贝任何内容。复制不会扩展数组,即使target在start后面的情况下。
- start的有效性,首先将被转换为整数,默认为0。索引处理与target一致。
- end的有效性,首先将被转换为整数,负数索引的处理与target一致。如果end大于length或者省略默认为length。end在start之前不会拷贝任何内容。
3.2 手写实现
如下所示是首先copyWithin的代码示例:
// 3. 手写copyWithin
MyArray.prototype.copyWithin = function(target, start = 0, end = this.length){// 索引转换为数字target = Number(target)start = Number(start)end = Number(end)const handleIndex = (index, length) => {if(index < 0) return Math.max(length + index, 0)else return Math.min(index, length)}const length = this.length// 统一处理索引target = handleIndex(target, length)start = handleIndex(start, length)end = handleIndex(end, length)// 处理不需要修改的边界情况if(target >= length || start >= length || end < start) return this// 开始进行copyWithin, 首先创建副本const copy = this.slice(start, end)for(let i = 0; i < copy.length && (target + i < length); i ++){this[target + i] = copy[i]}return this
}// 测试样例
const arr_7 = [1, 2, , , 5]
console.log(arr_7) // output: [ 1, 2, <2 empty items>, 5 ]
console.log(arr_7.copyWithin(0, 4)) // output: [ 5, 2, <2 empty items>, 5 ]
console.log(arr_7.copyWithin(1, 4)) // output: [ 5, 5, <2 empty items>, 5 ]
console.log(arr_7.copyWithin(2, "4", 5)) // output: [ 5, 5, 5, <1 empty item>, 5 ]
// 类数组对象copyWithin
const notArray_1 = {0: "hello",1: "world",length: 2
}
console.log(Array.prototype.copyWithin.call(notArray_1, 0, 1)) // output: { '0': 'world', '1': 'world', length: 2 }
// 边界条件测试
console.log(arr_7.copyWithin(0, 5)) // output: [ 5, 5, 5, <1 empty item>, 5 ]
需要注意:
- 三种索引都使用统一的处理方法
- 边界情况在处理索引完成后一次进行
- 在进行copy过程中需要使用slice方法来支持稀疏数组
- 判断条件还要包含是否原数组不越界
4 Array.entries()
4.1 基本介绍
entries() 方法返回一个新的数组迭代器对象,该对象包含数组中每个索引的键/值对。语法如下所示:
entries()
输入值:空
输出值:一个新的包含可迭代器对象,该对象返回value为数组的键值对。
注意事项:
- 稀疏数组的空槽处理为undefined
- 通用的方法,非数组对象满足条件即可调用
4.2 手写实现
4.2.1 手写实现返回迭代器对象
entries方法要求返沪一个迭代器对象,迭代器对象要求实现迭代器协议[Symbol.iterator],该属性下有迭代器方法,返回一个对象,该对象有next方法,每次调用返回一个对象,包含value和done属性,首先不使用generator实现,如下所示;
MyArray.prototype.entries = function(){const arr = thislet index = 0return {[Symbol.iterator]: ()=>{return {next(){if(index < arr.length){const res = {value:[index, arr[index]], done:false}index ++return res}else{return {done:true}}}}}}
}
- 使用闭包计数,index是外层函数的变量。
- 迭代器协议下的方法需要返回一个对象,对象拥有next方法,每次调用但会含有value和done属性的对象。
4.2.2 使用generator实现迭代器对象
使用generator函数能够更简洁的实现上述功能,如下所示:
// 4. 手写entries-使用generator
MyArray.prototype.entries = function(){const arr = thisreturn {[Symbol.iterator]: () =>{function* gen(){for(let i = 0; i < arr.length; i ++){yield [i, arr[i]]}}return gen()}}
}// 测试样例
const a = new MyArray("a", "b", "c");for (const [index, element] of a.entries()) {console.log(index, element);
}
// 0 'a'
// 1 'b'
// 2 'c'// 空槽处理
let arr_8 = new MyArray("a")
arr_8 = arr_8.concat([,"b"])
for (const element of arr_8.entries()) {console.log(element);
}
// [ 0, 'a' ]
// [ 1, undefined ]
// [ 2, 'b' ]// 类数组对象entries
const arrayLike = {length: 3,0: "a",1: "b",2: "c",};
for (const entry of MyArray.prototype.entries.call(arrayLike)) {console.log(entry);
}
// [ 0, 'a' ]
// [ 1, 'b' ]
// [ 2, 'c' ]
注意:
- 使用generator时done属性由generator管理不需要我们去返回一个含done和value属性的对象
5 Array.every()
5.1 基本介绍
every() 方法测试一个数组内的所有元素是否都能通过指定函数的测试。它返回一个布尔值。语法如下所示:
every(callbackFn)
every(callbackFn, thisArg)
输入:callFn(element, index, array)用于测试元素的执行的函数,应当返回一个真值或假值。thisArg指定回调函数中的this行为。
输出:如果每个元素对象通过测试返回真值则为true,只要有一个不通过则为false。
注意事项:
- 遇到一个测试对象为假值则停止测试
- 对于空数字的测试将始终为真
- 对于稀疏数组,空槽不会调用callFn
- callFn的操作会改变数组元素,并影响后续测试
- 这是一个通用方法
5.2 手写实现
手写实现代码如下所示:
// 手写实现every
// 难点处理稀疏数组
// thisArg处理
MyArray.prototype.every = function(callFn, thisArg = this){if(typeof callFn !== "function"){throw new TypeError(callFn + ' is not a function');}for(let i = 0; i < this.length; i ++){// 检查当前索引是否存在于数组中,跳过空槽if (!(i in this)) continue;if(!callFn.call(thisArg, this[i], i, this)) return false}return true
}function isBigEnough(element, index, array) {return element >= 10;
}const arr_9 = new MyArray(12, 5, 8, 130, 44)
const arr_10 = new MyArray(12, 54, 18, 130, 44)
console.log(arr_9.every(isBigEnough)) // false
console.log(arr_10.every(isBigEnough)) // trueconsole.log(MyArray.prototype.every.call([1, , 3], (x) => x !== undefined)) //true
console.log(MyArray.prototype.every.call([2, , 2], (x) => x === 2)) //trueconst arrayLike_1 = {length: 3,0: "a",1: "b",2: "c",};console.log(MyArray.prototype.every.call(arrayLike_1, (x) => typeof x === "string"),); // true
难点总结如下:
- thisArg理解为改变callFn(不能为箭头函数,箭头函数没有自己的this)中的this行为,所以使用call,apply都可以
- 理解如何处理空槽,使用 in 操作符来判断,如果为false则直接跳过该轮回调函数调用