如何简单并优雅的处理一类操作数组元素的问题

1,225 阅读7分钟

本期题目

「 题目 」: 删除给定一维数组(暂不考虑多维)元素中不符合条件的元素,结果返回新的数组

首先,我们在实际开发中会遇到很多处理数组的场景,其实无非就是四个字——"增删查改"。

但是,真的只是简单的增删查改吗?

NO!

如果只是这么简单,那写这篇博文就有点画蛇添足了!

下面让我们分析一下题目所涉及的知识点:

考点思考


  1. 数组的遍历、处理;
    1. 遍历:使用循环遍历数组;
    2. 循环使用 while 即可,结合判断函数的元素判断的返回值;
  2. 对元素的判断处理;
    1. 传递一个判断函数(可以灵活定义,拓展性比较好);
    2. 判断函数:
      1. 符合条件的返回 true;
      2. 不符合条件的返回 false

相关回顾


让我们简单回顾一下数组 Array 对象的方法:

  • push( ):向数组的末尾添加一个或更多元素,并返回新的长度;
  • unshift( ):向数组的开头添加一个或更多元素,并返回新的长度;
  • pop( ):删除并返回数组的最后一个元素;
  • shift( ):删除并返回数组的第一个元素;
  • concat( ):连接两个或更多的数组,并返回结果;
  • join( ):把数组的所有元素放入一个字符串。元素通过指定的分隔符进行分割;
  • reverse( ):颠倒数组中元素的顺序;
  • sort( ):对数组的元素进行排序;
  • slice( ):从某个已有的数组返回特定的元素;
  • splice( ):删除元素,并向数组添加新的元素;
  • valueOf( ):返回数组对象的原始值;
  • toString( ):把数组转化为字符串,并返回结果;
  • toSource( ):返回该对象那个的源码;
  • toLocaleString( ):把数组转化为本地数组,并返回结果;

PS:以上数据来自 W3school

其次,我们分析一下,题目是对数组元素的删除,有关 Array 对象的删除操作就是以下的四种方法:

  1. pop( ):删除并返回数组的最后一个元素;
  2. shift( ):删除并返回数组的第一个元素;
  3. slice(startIndex,endIndex):从某个已有的数组返回特定的元素;
  4. splice(index,howmany,newitem01,newitem02... ):删除/添加元素,并向数组添加新的元素;

简单分析一下区别:

方法名称 方法介绍 是否改变原数组 返回值 备注(适用场景)
pop(  ) 末尾删除 末尾元素
获取数组元素
末尾依次删除数组元素
shift(  ) 开头删除 开头元素
获取数组元素
开头依次删除数组元素
slice(  ) 特定截取 不会 一个新数组 返回新的数组,不改变原数组
splice(  )
特定删除
特定添加
处理后的原数组 直接改变原有数组

再让我们回顾一下常见的几种循环

JavaScript 常见的三种循环

  1. while

  2. do while

  3. for

while 循环

语法结构与使用步骤:
// 语法结构
while(/**条件**/){
// 代码块;改变条件
}

// 使用步骤
// 1. 初始化变量
// 2. 判断条件
// 3. 执行代码块
// 4. 改变初始条件
// 5. 判断条件

// 示例
var i=0,
      _arr=[0,1,2,3,4,5];

while(i<=_arr.length-1){
    // 做点什么
    console.log(i);
    // 改变初始条件
    i--;
}
// 结果
// 0,1,2,3,4,5

do while 循环

语法结构与使用步骤
// 语法结构
do {
// 代码块
} while (// 条件);

// 使用步骤:
// 1.初始化变量
// 2.无条件执行一次
// 3.判断条件
// 4.执行代码块
// 5.改变初始条件
// 6.判断条件

// 示例
var i=0,
    _arr=[0,1,2,3,4,5,6,7,8];

do {
    // 做点什么
    console.log(_arr[i]);
    i++;
} while(i<=_arr.length)

// 结果
// 0,1,2,3,4,5,6,7,8

for 循环

语法结构与使用步骤
// 语法结构
for(//初始化变量;//判断条件;//改变初始化变量){
// 代码块
}

// 使用步骤
// 1. 初始化变量(只会执行一次)
// 2. 判断条件
// 3. 执行代码块
// 4. 改变初始化变量(在循环体里面去做这件事,避免死循环)
// PS:建议不要嵌套多层循环(三层以上),
// 防止处理逻辑复杂,易进去死循环,造成CPU使用率飙升,
// 内存泄漏,要考虑代码执行的性能问题

// 示例
// for循环的较优写法
var i,
    _arr=[1,3,6,12,3,7];

for(i=_arr.length;i--;){
    // 使用_arr[i]做点什么吧
    console.log(_arr[i]);
}
// 结果
// 7,3,12,6,3,1

  • do while 会无条件执行一次,再进行条件判断
  • 使用 while 时不知道循环次数,使用 for 时知道循环次数
  • while 是条件循环,for 是限定了循环次数

代码实现


结合以上基本知识点回顾与分析,现给出以下的实现方案:

  1. 定义一个 dropElements 方法:两个传参 arr,judgefn;
    1. arr:原数组参数;
    2. judgefn:条件判断函数[灵活定义判断条件,结果 return true/false];
  2. 对传参 arr,进行代码健壮性测试:类型/空值判断;
  3. 循环对 arr[0]做判断;
  4. 当检测到数组元素不符合 judgefn 中的条件时,对数组进行截取操作;
  5. 截取使用 _arrNew = arr.slice(1); // Array.slice(stratindex,[endindex]);
  6. 截取后的新数组赋值给中间临时变量_arrNew;
  7. 最后 return 处理后的 _arrNew
// 代码如下 :
function dropElements(_arr, judgefn) {
	// arr健壮性
	// 1.是否是数组
	if (!Array.isArray(_arr)) {
		// console.error("不是数组,请检查参数!");
		return false;
		// 如果是需要对结果做容错处理,可以直接返回空数组 : [];
		// return [];
	}
	// 2.是否为空,为空时,直接返回原值
	else if (_arr.length === 0) {
		console.debug("数组为空,原值返回!");
		return _arr;
	}

	// 数组元素判断处理
	// 判断参数是否是空数组,同时判断是否符合 judgeefn 的判断条件
	// 不是空数组的话,并且不满足条件的话--judgefn(_arr[0])为false时,做截取操作并返回新的数组;
	let _arrNew;
	while (_arr.length > 0 && !judgefn(_arr[0])) {
		_arrNew = _arr.slice(1);
	}
	// 返回结果
	return _arrNew;
}

结果测试


function dropElements(_arr, judgefn) {
	// 健壮性
	// 1.是否是数组
	if (!Array.isArray(_arr)) {
		// console.error("不是数组,请检查参数!");
		return false;
	}
	// 2.是否为空,为空,直接返回原值
	else if (_arr.length === 0) {
		console.debug("数组为空,原值返回!");
		return _arr;
	}

	// 数组元素判断处理
	// 判断参数是否是空数组,同时判断是否符合 judgeefn 的判断条件
	// 不是空数组的话,并且不满足条件的话--judgefn(_arr[0])为false时,做截取操作并返回新的数组;
	while (_arr.length > 0 && !judgefn(_arr[0])) {
		_arr = _arr.slice(1);
	}
	// 返回结果
	return _arr;
}

// _arr健壮性测试:String,Object,Array,Boolean,Null,Undefined,Number,Symbol

// 测试环境(参数,方法)搭建
var _undefined, // 提前定义基本类型Undefined的数值
	// 条件判断函数
	testJudgeFn = (n) => n >= 3,
	// 测试数据
	testData = [
		{ type: "string", value: "_string", result: "" },
		{ type: "object", value: { a: 1, b: 2 }, result: "" },
		{ type: "array", value: [0, 1, 2, 3, 4, 5, 6, 7, 8], result: "" },
		{ type: "boolean", value: true, result: "" },
		{ type: "boolean", value: false, result: "" },
		{ type: "null", value: null, result: "" },
		{ type: "number", value: 100, result: "" },
		{ type: "number", value: 0, result: "" },
		{ type: "number", value: NaN, result: "" },
		{ type: "undefined", value: _undefined, result: "" },
	];

// 结果测试并打印
for (let i = 0, testLength = testData.length; i < testLength; i++) {
	testData[i].result =
		dropElements(testData[i].value, testJudgeFn) === false
			? `Params Type Error:${testData[i].type}`
			: dropElements(testData[i].value, testJudgeFn);

	// 打印结果
	console.table(`第${i + 1}次测试结果:`, testData[i].result);
}

贴出测试结果的截图:

测试结果

对于这个简单的数组问题,你想到了什么?

 小总结


你是否会有着以下问题:

  • 为什么使用传递一个函数而不是把具体判断条体放进函数体内部?
  • 为什么使用 while 循环,而不是 for 循环等其他方式?
  • 为什么还要考虑它的传参类型,空值,容错...等等?

简单解答:

  • 定义一个判断函数,是为了增加它的判断灵活度,可能它的期望是返回数组中的字符串对象...;
  • 循环的使用应该根据实际处理的方式,选择相应的,而不是用到循环就是 for 循环,还有时候 do while,while 循环相比 for 循环更优;
  • 对于与一个函数,它的传参多数情况下是不可控制,必要的类型校验,空值校验处理,容错处理,代码的健壮性都需要考虑,只有这样才可以保证代码逻辑的稳定性...

更深的思考:

  1. 数组的 slice()、splice()用法,还使用在哪些具体的实际应用,它的高级使用有哪些?
  2. 为什么使用单 var 的变量命名方式,有什么好处?
  3. 一般我们是如何写 for 循环,如何结合实际场景写一个较优的 for 循环处理逻辑?
  4. 如何拆分一个功能,实现高内聚低耦合、细分确定功能逻辑的单一职责...