【前端冷知识】如何优雅地生成结构化的初始数据

在项目中我们经常会遇到初始化数据的需求,比如创建一个100个元素的数组,让每个元素的初始值为0。

非常传统的一种方式就是创建好数组,然后用循环来赋初始值。

 
  

function initialize(list) {

  for(let i = 0; i < list.length; i++) {

    list[i] = 0;

  }

  return list;

}


const list = new Array(100);

initialize(list);


console.log(list); // [0, 0, ... 0];

有同学可能想用数组的迭代方法来赋值,比如:

 
  

list.forEach((_, i) => {

  list[i] = 0;

});


console.log(list); // [empty x 100]

或者:

 
  

const list = Array(100).map(() => 0);


console.log(list); // [empty x 100]

但是事实上这两种方式都是不行的,因为数组被创建的时候,元素的初始值是empty,而迭代方法并不会遍历数组中empty的元素for...in也一样。

这个规则对于访问稀疏数组中的元素是有帮助的,能提升效率,但不在这篇文章讨论的范围,后续有机会我们会对数组迭代进行单独讨论,有兴趣的同学可以关注。

回到主题,虽然直接创建数组时数组元素的默认值是empty,但是因为数组是一个可迭代对象,所以我们可以用spread操作将它展开,再进行迭代就可以了,所以:

 
  

const list = [...Array(100)].map(() => 0);


console.log(list); // [0, 0, ...0]

此外,我们还可以通过Array.from方法创建初始数组。

?? Array.from方法从一个类数组或可迭代对象中创建一个新的、浅拷贝的数组实例。

一个包含length属性,且值为非负整数的对象会被当做类数组对象,被Array.from处理成一个长度为length的数组。

??Array.from 对象的第二个参数是迭代算子(Map Functor),所以我们不必用Array.from + map转两次,直接一次转换就可以了:

 
  

const list = Array.from({length: 100}, () => 0);


console.log(list); // [0, 0, ...0]

由于我们例子中初始化数组的初始值是固定的数值0,所以这里我们其实可以使用Array.prototype.fill方法。

 
  

const list = Array(100).fill(0);


console.log(list); // [0, 0, ...0]

?注意fill方法用来填充元素的值如果是引用类型,它并不会拷贝这个值,而是直接将引用赋给数组,这也就是说,如果我们企图通过fill来初始化二维数组,是有问题的。

 
  

const matrix = Array(3).fill(Array(3).fill(0));

matrix[0][1] = 1;


console.log(matrix); // [[0, 1, 0], [0, 1, 0], [0, 1, 0]]

上面的代码,matrix[0]、matrix[1]和matrix[2]指向同一个引用。

另外,fill还有两个可选的参数,可以对数组进行部分赋值:

 
  

const list = Array(10);

list.fill(0, 0, 5);

list.fill(1, 5);


console.log(list); // [0, 0, 0, 0, 0, 1, 1, 1, 1, 1]

所以对于初始化相同的非引用类型值的数组,使用Array.prototype.fill是比较简单的方案。

但是,如果我们初始化的值不同呢?

比如我们想初始化一个长度10的数组,初始值分别是0~9。

通过前面的Array.from是一个简单的办法:

 
  

const list = Array.from({length: 10}, (_, i) => i);


console.log(list); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

就这个问题还有个取巧的办法,因为我们要初始化的值恰好是数组元素的下标值,所以:

 
  

const list = [...Array(10).keys()];


console.log(list);

??Array.protoype.keys返回一个以数组下标为迭代值的可迭代对象,将它展开就是我们要的结果了。

使用生成器

对于更复杂的初始化需求,我们可以构建可复用的生成器。

 
  

function *initializer(count, mapFunc = i => i) {

  for(let i = 0; i < count; i++) {

    yield mapFunc(i, count);

  }

}


const list1 = [...initializer(10)];

console.log(list1); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


const list2 = [...initializer(10, i => 10 + 2 * i)];

console.log(list2); // [10, 12, 14, 16, 18, 20, 22, 24, 26, 28]

在这里,有同学可能会说,我们不用生成器,直接将数组构建出来也是可以的:

 
  

function initialize(count, mapFunc = i => i) {

  const ret = [];

  for(let i = 0; i < count; i++) {

    ret.push(mapFunc(i, count));

  }

  return ret;

}


const list1 = initialize(10);

console.log(list1); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


const list2 = initialize(10, i => 10 + 2 * i);

console.log(list2); // [10, 12, 14, 16, 18, 20, 22, 24, 26, 28]

这个的确是可以的,不过用生成器将构建迭代器和生成数组的步骤分开,会更灵活。

我们稍微修改一下生成器,构建更复杂的数据:

 
  

function *initializer(count, mapFunc = i => i) {

  for(let i = 0; i < count; i++) {

    const value = mapFunc(i, count);

    if(value[Symbol.iterator]) yield* value;

    else yield value;

  }

}


const mat3 = [...initializer(3, i => initializer(3, j => i === j ? 1 : 0))]

console.log(mat3); // [1, 0, 0, 0, 1, 0, 0, 0, 1]

??yield* 可以将一个可迭代对象委托给一个生成器,所以上面的代码判断如果value返回的是一个可迭代对象,那么递归迭代这个对象并返回,所以我们用它来生成一个3x3的初始矩阵,它是一个长度为9的数组,初始值是[1, 0, 0, 0, 1, 0, 0, 0, 1]

上面的代码有一个问题,就是它会将所有可迭代对象递归展开,这也许不是我们期望的结果,比如:

 
  

function *initializer(count, mapFunc = i => i) {

  for(let i = 0; i < count; i++) {

    const value = mapFunc(i, count);

    if(value[Symbol.iterator]) yield* value;

    else yield value;

  }

}


const list = [...initializer(3, i => [0, 0, 0])];

console.log(list); // [0, 0, 0, 0, 0, 0, 0, 0, 0]

可能我们的预期是初始化成[[0, 0, 0], [0, 0, 0], [0, 0, 0]],而不是完全展开成[0, 0, 0, 0, 0, 0, 0, 0, 0]

当然我们可以把调用代码修改一下,嵌套一层数组:

 
  

function *initializer(count, mapFunc = i => i) {

  for(let i = 0; i < count; i++) {

    const value = mapFunc(i, count);

    if(value[Symbol.iterator]) yield* value;

    else yield value;

  }

}


const list = [...initializer(3, i => [[0, 0, 0]])];

console.log(list); // [[0, 0, 0], [0, 0, 0], [0, 0, 0]]

但是这很容易给使用者造成困扰。因此我们可以修改一下设计,只有mapFunc是生成器函数的时候,才用yield*迭代展开,否则直接yield返回。

 
  

function *initializer(count, mapFunc = i => i) {

  for(let i = 0; i < count; i++) {

    if(mapFunc.constructor.name === 'GeneratorFunction') {

      yield* mapFunc(i, count);

    } else {

      yield mapFunc(i, count);

    }

  }

}


const list = [...initializer(3, i => [0, 0, 0])];

console.log(list); // [[0, 0, 0], [0, 0, 0], [0, 0, 0]]


const mat3 = [...initializer(3, function *(i) {

  yield Number(=== 0);

  yield Number(=== 1);

  yield Number(=== 2);

})]

console.log(mat3); // [1, 0, 0, 0, 1, 0, 0, 0, 1]

最后,再强调一下,生成器是个好东西,用它来写简洁易读的代码来初始化数据吧,比如下面的代码初始化一副扑克牌:[花色, 点数](不包括大小王):

 
  

function *initializer(count, mapFunc = i => i) {

  for(let i = 0; i < count; i++) {

    if(mapFunc.constructor.name === 'GeneratorFunction') {

      yield* mapFunc(i, count);

    } else {

      yield mapFunc(i, count);

    }

  }

}


const cards = [...initializer(13, function *(i) {

  const p = i + 1;

  yield ['♠️', p];

  yield ['♣️', p];

  yield ['♥️', p];

  yield ['♦️', p];

})];


console.log(cards);

关于生成结构化的初始数据,你还有什么想要讨论的,欢迎在issue中讨论。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值