一道面试题引发的思考--关于数组占位元素

#1

##起因

今天跟一位程序员朋友日常聊天,聊到了一道面试题:

请用JS实现一个函数InsertItemToArray,
函数接受三个参数:要插入元素的数组、要插入的新元素、新元素所在的位置(arr, item, index),
往原数组arr的index位置插入一个新的元素item,index之后的元素索引依次后移,并返回插入新元素后的新数组。

要求:不允许用splice,slice。

这道题读完题目后,惯性思路就是写一个for循环,比如:

const InsetItemToArray = (arr, item, index) => {
  const resultArr = [];
  for(let i = 0; i < arr.length + 1; i++){
    if (i < index){
      resultArr.push(arr[i]);
    } else if (i > index) {
      resultArr.push(arr[i - 1]);
    } else {
      resultArr[i] = item;
    }
  }
  return resultArr;
}

InsetItemToArray([1,2,3], 'a', 1);

// [1, 'a', 2, 3]

或者换个思路:

const InsetItemToArray = (arr, item, index) => {
  const resultArr = [...arr];
  for(let i = arr.length; i > index; i--){
	resultArr[i] = arr[i - 1];
  }
  resultArr[index] = item;
  return resultArr;
}

InsetItemToArray([1,2,3], 'b', 1);

// [1, 'b', 2, 3]

以上都是比较常用且不用花太多思考时间的写法。

##转折

这时候突然觉得,这道题其实可以用解构赋值的模式匹配来实现。

我们知道,es6的解构赋值是可以这样写的:

const arr = [1, 2, 3, 4, 5];
const [ , ,...resultArr] = arr;

// resultArr =>  [3, 4, 5]

那我们可以把返回结果看成是两个部分:

  • 第一部分是headArr,包含原数组arr中索引值小于index的元素和即将插入的新元素item
  • 第二部分是footArr,包含原数组arr中索引值大于或等于index的元素

最后将两个部分拼在一起,即是要返回的新数组了。

我们先来看怎么得到footArr

// 错误示例
const GetFootArr = (arr, item, index) => {
	const placeArr = new Array(index);
	const [...placeArr, ...footArr] = arr;
	return footArr;
}
// 控制台抛出一个错误:Uncaught SyntaxError: Rest element must be last element

奇怪了,让我们对比一下结果:

// 正常解构
const arr = [1, 2, 3, 4, 5];
const [ , ,...resultArr] = arr;

// resultArr =>  [3, 4, 5]


// 报错
const arr = [1, 2, 3, 4, 5];
const placeArr = new Array(2);
const [...placeArr, ...resultArr] = arr;

// Uncaught SyntaxError: Rest element must be last element

这时候打印placeArr

console.log(placeArr);
// [empty × 2]

打印[...palceArr]

console.log([...placeArr]);
// [undefined, undefined]

我们发现原本用来占位的empty都变成了undefined

##结论
数组的空位指,数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位。

就像刚才打印placeArr得到的结果都是empty一样。

但是要注意:

空位并不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。

0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false

上面代码说明,第一个数组的 0 号位置是有值的,第二个数组的 0 号位置没有值。

扩展运算符(…)是会将空位转为undefined。

[...['a',,'b']]
// [ "a", undefined, "b" ]

除了扩展运算符,entries()keys()values()find()findIndex()都会将空位处理成undefined

// entries()
[...[,'a'].entries()] // [[0,undefined], [1,"a"]]

// keys()
[...[,'a'].keys()] // [0,1]

// values()
[...[,'a'].values()] // [undefined,"a"]

// find()
[,'a'].find(x => true) // undefined

// findIndex()
[,'a'].findIndex(x => true) // 0

而数组解构赋值的模式匹配,并不能匹配undefined作为变量名。所以placeArr就不能被正常的赋值。

关于数组空位的解释,可以参考阮一峰老师的ECMAScript 6 入门 - 数组的扩展
这里不做多的引申。

##PS
既然解构赋值的思路行不通,但是我们仍旧可以按照headArrfootArr的思路来解这道题,只需换一种写法。

比如:

const InsetItemToArray = (arr, item, index) => {
  const headArr = arr.filter((_, i) => i < index);
  const footArr = arr.filter((_, i) => i >= index);
  return [ ...headArr, item, ...footArr];
}

InsetItemToArray([1,2,3], 'c', 2);

// [1, 2, 'c', 3]

这样一来我们就得到了正确的结果了。

完。

#2

new_arr = (arr, item, index) => {
let array = []; array.concat(arr.slice(0, index)).push(item).concat(arr.slice(index))
return array;
}

new_arr ([1,2,3,4], 5, 2);

#3

const InsetItemToArray = (arr, item, index) => {
return arr.reduce((pre, now, i)=> {
if(index === i) pre.push(item);
pre.push(now);
return pre
}, [])
}