保持代码的手感
前言
slice 是 JS 中经常被用到的一个 API, 之前也没有细致的研究过它是怎么实现的,这里还是想拿来再深入学习一下,加深印象。
学习的过程主要分成以下几步:
基础功能
先实现一个基础功能的版本
// + 基础功能
Array.prototype.mySlice = function(start, end) {
const array = this;
const newArray = [];
for(let i=start; i<end; i++) {
newArray.push(array[i]);
}
return newArray;
}
function testCase() {
var fruits = ['Banana', 'Orange', 'Lemon', 'Apple', 'Mango'];
var citrus = fruits.mySlice(1, 3);
console.log('mySlice citrus', citrus);
citrus = fruits.slice(1, 3);
console.log('slice citrus', citrus);
}
testCase();
复制代码
OUTPUT
// OUTPUT
"mySlice citrus" // [object Array] (2)
["Orange","Lemon"]
"slice citrus" // [object Array] (2)
["Orange","Lemon"]
复制代码
基础功能实现比较简单,这里就不加解释了
异常值处理
上述基础功能并没有对异常值进行处理,这里参考 MDN 的描述和自己的一些理解加上以下几条
- 如果 end 被省略,则 slice 会一直提取到原数组末尾。
- 如果 end 大于数组的长度,slice 也会一直提取到原数组末尾。
- 支持 String 和 Boolean 转成 Number
// 基础功能
// + 如果 end 被省略,则 slice 会一直提取到原数组末尾。
// + 如果 end 大于数组的长度,slice 也会一直提取到原数组末尾。
// + 支持 String 和 Boolean 转成 Number
Array.prototype.mySlice = function(start, end) {
const array = this;
const arrayLength = array.length;
const newArray = [];
start = Number(start), end = Number(end);
if(!end || end > arrayLength) {
end = arrayLength;
}
for(let i=start; i<end; i++) {
newArray.push(array[i]);
}
return newArray;
}
function testCase() {
var fruits = ['Banana', 'Orange', 'Lemon', 'Apple', 'Mango'];
var citrusEndBig = fruits.mySlice(1, 9);
console.log('mySlice citrusEndBig', citrusEndBig);
var citrusEndNull = fruits.mySlice(1);
console.log('mySlice citrusEndNull', citrusEndNull);
var citrusString = fruits.mySlice('1', '9');
console.log('mySlice citrusString', citrusString);
var citrusBoolean = fruits.mySlice(false, true);
console.log('mySlice citrusBoolean', citrusBoolean);
var citrusObject = fruits.mySlice({"a":"b"}, {});
console.log('mySlice citrusObject', citrusObject);
citrusEndBig = fruits.slice(1, 9);
console.log('slice citrusEndBig', citrusEndBig);
citrusEndNull = fruits.slice(1);
console.log('slice citrusEndNull', citrusEndNull);
citrusString = fruits.slice('1', '9');
console.log('mySlice citrusString', citrusString);
citrusBoolean = fruits.slice(false, true);
console.log('mySlice citrusBoolean', citrusBoolean);
var citrusObject = fruits.slice({"a":"b"}, {});
console.log('mySlice citrusObject', citrusObject);
}
testCase();
复制代码
OUTPUT
"mySlice citrusEndBig" // [object Array] (4)
["Orange","Lemon","Apple","Mango"]
"mySlice citrusEndNull" // [object Array] (4)
["Orange","Lemon","Apple","Mango"]
"mySlice citrusString" // [object Array] (4)
["Orange","Lemon","Apple","Mango"]
"mySlice citrusBoolean" // [object Array] (1)
["Banana"]
"mySlice citrusObject" // [object Array] (0)
[]
"slice citrusEndBig" // [object Array] (4)
["Orange","Lemon","Apple","Mango"]
"slice citrusEndNull" // [object Array] (4)
["Orange","Lemon","Apple","Mango"]
"mySlice citrusString" // [object Array] (4)
["Orange","Lemon","Apple","Mango"]
"mySlice citrusBoolean" // [object Array] (1)
["Banana"]
"mySlice citrusObject" // [object Array] (0)
[]
复制代码
这里直接用了 Number 转,由于其他类型转成 Number 基本都会是 NaN,在执行判断逻辑的时候恒为 false,所以即使传入也不会报错
支持负数参数
我们都知道,slice 是支持负数参数,这里我们再看一下 MDN 的描述,思考一下怎么实现比较好
- 如果 begin 为负数,则表示从原数组中的倒数第几个元素开始提取,slice(-2) 表示提取原数组中的倒数第二个元素到最后一个元素(包含最后一个元素)。
- 如果 end 为负数, 则它表示在原数组中的倒数第几个元素结束抽取。 slice(-2,-1) 表示抽取了原数组中的倒数第二个元素到最后一个元素(不包含最后一个元素,也就是只有倒数第二个元素)。
和平时使用的时候差不多,本质上就是从数组尾部开始找索引位置,那就引入数组 length 计算吧
// 基础功能
// 如果 end 被省略,则 slice 会一直提取到原数组末尾。
// 如果 end 大于数组的长度,slice 也会一直提取到原数组末尾。
// 支持 String 和 Boolean 转成 Number
// + 如果 begin 为负数,则表示从原数组中的倒数第几个元素开始提取,slice(-2) 表示提取原数组中的倒数第二个元素到最后一个元素(包含最后一个元素)。
// + 如果 end 为负数, 则它表示在原数组中的倒数第几个元素结束抽取。 slice(-2,-1) 表示抽取了原数组中的倒数第二个元素到最后一个元素(不包含最后一个元素,也就是只有倒数第二个元素)。
Array.prototype.mySlice = function(start, end) {
const array = this;
const arrayLength = array.length;
const newArray = [];
start = Number(start), end = Number(end);
start = start < 0 ? arrayLength + start : start;
end = !end || end > arrayLength ? arrayLength : end
end = end < 0 ? arrayLength + end : end;
for(let i=start; i<end; i++) {
newArray.push(array[i]);
}
return newArray;
}
function testCase() {
var fruits = ['Banana', 'Orange', 'Lemon', 'Apple', 'Mango'];
var citrus = fruits.mySlice(-3, -1);
console.log('mySlice citrus', citrus);
var citrus2 = fruits.mySlice(0, -1);
console.log('mySlice citrus2', citrus2);
citrus = fruits.slice(-3, -1);
console.log('slice citrus', citrus);
citrus2 = fruits.slice(0, -1);
console.log('slice citrus2', citrus2);
}
testCase();
复制代码
OUTPUT
"mySlice citrus" // [object Array] (2)
["Lemon","Apple"]
"mySlice citrus2" // [object Array] (4)
["Banana","Orange","Lemon","Apple"]
"slice citrus" // [object Array] (2)
["Lemon","Apple"]
"slice citrus2" // [object Array] (4)
["Banana","Orange","Lemon","Apple"]
复制代码
核心就是在 arrayLength + start ,比较简单
元素对象是否同步更新
至此 slice 的核心基本功能差不多了,由于我的数组元素赋值是引用赋值,理论上是支持对象更新的,所以这里就直接测试一下 MDN 上的 case,我们先来看一下 MDN 的描述
- 如果该元素是个对象引用 (不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
function testCase() {
// 使用 slice 方法从 myCar 中创建一个 newCar。
var myHonda = { color: 'red', wheels: 4, engine: { cylinders: 4, size: 2.2 } };
var myCar = [myHonda, 2, "cherry condition", "purchased 1997"];
var newCar = myCar.slice(0, 2);
// 输出 myCar、newCar 以及各自的 myHonda 对象引用的 color 属性。
console.log(' myCar = ' + JSON.stringify(myCar));
console.log('newCar = ' + JSON.stringify(newCar));
console.log(' myCar[0].color = ' + JSON.stringify(myCar[0].color));
console.log('newCar[0].color = ' + JSON.stringify(newCar[0].color));
// 改变 myHonda 对象的 color 属性.
myHonda.color = 'purple';
console.log('The new color of my Honda is ' + myHonda.color);
//输出 myCar、newCar 中各自的 myHonda 对象引用的 color 属性。
console.log(' myCar[0].color = ' + myCar[0].color);
console.log('newCar[0].color = ' + newCar[0].color);
}
testCase();
复制代码
OUTPUT
" myCar = [{'color':'red','wheels':4,'engine':{'cylinders':4,'size':2.2}},2,'cherry condition','purchased 1997']"
"newCar = [{'color':'red','wheels':4,'engine':{'cylinders':4,'size':2.2}},2]"
" myCar[0].color = 'red'"
"newCar[0].color = 'red'"
"The new color of my Honda is purple"
" myCar[0].color = purple"
"newCar[0].color = purple"
复制代码
和 MDN 上的结果是一致的,符合预期
参考 ECMA 规范实现
我们先来看一下 ECMA 的伪代码
粗略看了一下大部分和自己实现的差不多,一步一步来实现一下看看
Array.prototype.mySlice = function(start, end) {
// 1. Let A be a new array created as if by the expression new Array().
const newArray = [];
// 2. Call the [[Get]] method of this object with argument "length".
// 3. Call ToUint32(Result(2))
const arrayLength = Number(this.length);
// 4. Call ToInteger(start).
start = Number(start);
// 5. If Result(4) is negative, use max((Result(3)+Result(4)),0); else use min(Result(4),Result(3)).
// 6. Let k be Result(5).
let kStart = start < 0 ? Math.max(arrayLength + start, 0) : Math.min(start, arrayLength);
// 7. If end is undefined, use Result(3); else use ToInteger(end).
end = !end ? arrayLength : Number(end);
// 8. If Result(7) is negative, use max((Result(3)+Result(7)),0); else use min(Result(7),Result(3)).
end = end < 0 ? Math.max(arrayLength + end, 0) : Math.min(end, arrayLength);
// 9. Let n be 0.
let n = 0;
while(true) {
// 10. If k is greater than or equal to Result(8), go to step 19.
if( kStart >= end) {
// 19. Call the [[Put]] method of A with arguments "length" and n.
// 20. Return A.
return newArray;
}
// 11. Call ToString(k).
kStart = kStart.toString();
// 12. If this object has a property named by Result(11), go to step 13; but if this object has no property named by Result(11), then go to step 16.
if(this.hasOwnProperty(kStart)) {
// 13. Call ToString(n).
n = n.toString();
// 14. Call the [[Get]] method of this object with argument Result(11).
// 15. Call the [[Put]] method of A with arguments Result(13) and Result(14).
this[n] = this[kStart];
}
// 16. Increase k by 1.
// 17. Increase n by 1.
kStart++;
n++;
}
}
复制代码
当把伪代码翻译完后,我发现主要有两个不太一样的地方
- 规范里面使用了 max 和 min 来处理,思考了一下这样的好处是当传入的索引位置 index 超出总长度时,自动设置为最大值,负数的时候为 0 ,正数的时候就是 length,这个是之前我的代码里面也没有考虑到的,并且这样的代码也很简洁,避免写很多 if else
let kStart = start < 0 ? Math.max(arrayLength + start, 0) : Math.min(start, arrayLength);
复制代码
- 把 start 转成了 string 去匹配,这里一开始没太看懂,后面思考了一下主要是为了兼容输入值为 array-like ,因为 array-like 的 key 可能是一个 string
最后
通过手撸了一遍代码,可以对 slice 有了更多更全面的理解,本例中主要可以学到
- 规范的伪代码还是有很多思考很全面的地方,并且实现得比较简洁,在日常开发中可以借鉴一些思路
- slice 的包容性很强,不论是数据源,还是参数,都支持多种类型,和 js 本身一样是一把双刃剑,考虑代码可维护性在开发过程中还是让变量可控一些比较好
欢迎各种拍砖,讨论