精通JavaScript:核心功能助你高效编程
无论是在后端、前端,甚至航天器中,JavaScript都无处不在。它是一种非常灵活的语言,融合了函数式编程和传统的面向对象编程模式。其与C语言的相似性也使开发人员能够轻松地从其他语言过渡。
如果你希望提升你的JavaScript技能,我强烈建议你学习并掌握以下核心功能。它们不仅能解决具体问题,还能大幅减少代码量,提高开发效率。
map()方法
谈论JavaScript,却不提及map()
,简直是不可思议!map()
、filter()
和reduce()
共同构成了JavaScript函数式编程的基石。这些都是你在开发过程中会频繁使用的功能,因此非常值得深入了解。我们先从map()
开始。
map()
常常让初学者感到困惑,并非因为它本身复杂,而是因为它体现了函数式编程的思想。由于我们通常接触的是面向对象的编程语言,这种函数式编程的工作方式可能显得有些陌生甚至不适应。
JavaScript的功能远比面向对象强大得多,尽管其现代版本试图隐藏这一事实。但这又是一个复杂的话题,我们以后再讨论。现在,让我们回到map()
。
map()
是一个非常简单的函数。它被附加到一个数组上,可以将数组中的每个元素转换成其他东西,从而生成一个新的数组。具体的转换规则由一个匿名函数提供。
这就是map()
的全部内容!语法可能需要适应一段时间,但其本质就是这样。为什么要使用map()
?这取决于你的目标。例如,假设你记录了上周每天的温度,并将其存储在一个数组中。现在,你知道测量仪器不精确,报告的温度比实际低1.5度。
你可以使用map()
函数来修正这些数据,如下所示:
const weeklyReadings = [20, 22, 20.5, 19, 21, 21.5, 23]; const correctedWeeklyReadings = weeklyReadings.map(reading => reading + 1.5); console.log(correctedWeeklyReadings); // 输出 [ 21.5, 23.5, 22, 20.5, 22.5, 23, 24.5 ]
另一个实用的例子来自React世界,其中从数组创建DOM元素列表是一种常见模式。例如:
export default ({ products }) => { return products.map(product => { return ( <div className="product" key={product.id}> <div className="p-name">{product.name}</div> <div className="p-desc">{product.description}</div> </div> ); }); };
这里,我们有一个React组件,它接收一个产品列表作为props。然后,它从这个列表(数组)构建一个HTML ‘div’列表,将每个产品对象转换为HTML。原始的产品对象保持不变。
你可能会说,map()
不过是一个美化的for
循环,你是完全正确的。但是请记住,函数式编程强调统一性、简洁性和优雅性。
filter()方法
filter()
是一个非常有用的函数,你会在许多情况下反复使用它。顾名思义,这个函数根据你提供的规则/逻辑过滤一个数组,并返回一个包含满足这些规则的元素的新数组。
让我们再次使用天气示例。假设你有一个数组,包含上周每天的最高温度。现在,你想知道有多少天的温度低于20度。我们可以使用filter()
函数来实现:
const weeklyReadings = [20, 22, 20.5, 19, 21, 21.5, 23]; const colderDays = weeklyReadings.filter(dayTemperature => { return dayTemperature < 20; }); console.log("Total colder days in week were: " + colderDays.length); // 输出 1
注意,传递给filter()
的匿名函数必须返回一个布尔值:true
或false
。这是filter()
决定是否将该元素包含在过滤后的数组中的依据。你可以在匿名函数中编写任意复杂的逻辑,只要确保最终返回的是一个布尔值。
请注意:根据我的经验,许多程序员在使用filter()
时会犯一个微妙的错误。让我们重写之前的代码,并包含这个错误:
const weeklyReadings = [20, 22, 20.5, 19, 21, 21.5, 23]; const colderDays = weeklyReadings.filter(dayTemperature => { return dayTemperature < 20; }); if(colderDays) { console.log("Yes, there were colder days last week"); } else { console.log("No, there were no colder days"); }
注意到了吗?最后的if
条件检查colderDays
,它实际上是一个数组!在匆忙编写代码时,许多人会犯这种错误。问题在于,JavaScript在许多方面都是一种奇怪且不一致的语言,事物的“真值”就是其中之一。尽管[] == true
返回false
,让你认为代码没有问题,但实际上,在if
条件中,[]
会被评估为true
!换句话说,我们编写的代码永远不会说上周没有更冷的日子。
正确的做法是检查colderDays.length
,它保证返回一个整数(零或更大),因此在逻辑比较中始终如一地工作。请记住,filter()
总是返回一个数组,无论是空的还是非空的,因此我们可以依赖它并自信地编写逻辑比较。
我花了太多时间来强调这个错误,但它确实值得用一万字来强调。我希望你不要被这个问题困扰,从而节省数百小时的调试时间!
reduce()方法
在所有JavaScript函数中,reduce()
常常被认为是“令人困惑和奇怪”的代表。尽管它非常重要,并且在许多情况下可以生成优雅的代码,但大多数JavaScript开发人员都避免使用它,而更愿意编写更冗长的代码。
原因很简单,reduce()
在概念和执行上都难以理解。当你阅读它的描述时,你可能需要反复阅读几次,仍然怀疑自己是否读错了;当你看到它运行时,你的大脑可能会变得一团糟!
别担心,reduce()
函数并没有像B+树及其算法那样复杂。只是这种逻辑在普通程序员的日常工作中很少遇到。
在吓唬你之后,现在我终于要向你展示这个函数是什么以及为什么我们需要它了。
顾名思义,reduce()
用于减少某些东西。它减少的是一个数组,并将数组转换为一个单一的值(数字、字符串、函数、对象等等)。简单来说,reduce()
将数组转换为单个值。请注意,reduce()
的返回值不是数组,而map()
和filter()
的返回值是数组。理解这一点你已经成功了一半!
显然,如果要转换(减少)一个数组,我们需要提供必要的逻辑。作为JavaScript开发人员,你可能已经猜到我们使用函数来实现这一点。这个函数就是我们所说的reducer函数,它是reduce()
的第一个参数。第二个参数是一个起始值,例如数字、字符串等(稍后我会解释这个“起始值”是什么)。
根据我们目前的理解,我们可以说对reduce()
的调用看起来像这样:array.reduce(reducerFunction, startingValue)
。现在,让我们来处理核心部分:reducer函数。如前所述,reducer函数告诉reduce()
如何将数组转换为单个值。它有两个参数:一个用作累加器的变量(别担心,我也会解释这一点),以及一个存储当前值的变量。
我知道,我知道,这在一个函数中涉及了大量的术语,这在JavaScript中甚至不是强制性的。这就是人们避免使用reduce()
的原因。但是如果你一步一步地学习它,你不仅会理解它,而且会欣赏它。
回到正题。传递给reduce()
的“起始值”是计算的起始值。例如,如果你要在reducer函数中进行乘法运算,则起始值为1是有意义的;对于加法,你可以从0开始,依此类推。
现在让我们看看reducer函数的签名。传递给reduce()
的reducer函数具有以下形式:reducerFunction(accumulator, currentValue)
。“累加器”只是收集和保存计算结果的变量的别称;这就像使用一个名为total
的变量来使用类似total += arr[i]
这样的方法对数组中的所有元素求和。这正是reduce()
中reducer函数的使用方式:累加器最初设置为你提供的起始值,然后一个接一个地访问数组中的元素,执行计算,并将结果存储在累加器中。
那么,reducer函数中的“当前值”是什么?如果你要求你遍历一个数组,你会在脑海中想象同样的画面:你将一个变量从索引零开始,然后一次向前移动一步。当你这样做的时候,如果我让你突然停下来,你会发现自己停留在数组的某个元素上,对吧?这就是我们所说的当前值:它是表示当前正在考虑的数组元素的变量的值(如果这有助于理解,可以把它想象成遍历数组的过程)。
现在,是时候看一个简单的例子,看看这些术语如何在实际的reduce()
调用中结合在一起了。假设我们有一个包含前n个自然数(1, 2, 3 . . n)的数组,我们想要找到n的阶乘。我们知道要找到n!,我们只需要将所有数字相乘,这导致了我们的实现:
const numbers = [1, 2, 3, 4, 5]; const factorial = numbers.reduce((acc, item) => acc * item, 1); console.log(factorial); // 输出 120
在这短短的三行代码中,发生了很多事情,所以让我们在我们迄今为止进行的讨论的背景下,逐一分解它。很明显,numbers
是一个数组,它包含了我们想要相乘的所有数字。接下来,查看numbers.reduce()
调用,它表示acc
的起始值应为1(因为它不会影响或破坏任何乘法运算)。接下来,查看reducer函数体,(acc, item) => acc * item
,它只是说每次迭代数组时,返回值应该是该元素乘以累加器中已有的值。迭代并将乘法显式存储在累加器中是在幕后发生的事情,这也是reduce()
成为JavaScript开发人员的绊脚石的最大原因之一。
为什么要使用reduce()
?
这是一个非常好的问题,老实说,我没有确定的答案。无论reduce()
做什么,都可以通过循环、forEach()
等来完成。但是,这些技术会产生更多的代码,使其难以阅读。此外,使用reduce()
和类似函数,你可以确保原始数据没有被更改。这本身就消除了一类错误,尤其是在分布式应用程序中。
最后,reduce()
更加灵活,因为累加器可以是一个对象、一个数组,甚至是一个函数;起始值和函数调用的其他部分也是如此——几乎任何东西都可以输入,几乎任何东西都可以输出,因此在设计可重用代码时具有极大的灵活性。
如果你仍然不相信,那也没关系;JavaScript社区本身在reduce()
的“紧凑性”、“优雅”和“强大”方面存在明显的分歧,所以如果你不使用它也没关系。但是一定要看一些简洁的例子,在你决定放弃使用reduce()
之前。
some()方法
假设你有一个对象数组,每个对象代表一个人。你想知道数组中是否有35岁以上的人。请注意,你不需要计算有多少这样的人,更不用说检索他们的列表。我们在这里讨论的是“一个或多个”或“至少一个”的情况。
你怎么做到这一点?
是的,你可以创建一个标志变量并遍历数组来解决这个问题,如下所示:
const persons = [ { name: 'Person 1', age: 32 }, { name: 'Person 2', age: 40 }, ]; let foundOver35 = false; for (let i = 0; i < persons.length; i ++) { if(persons[i].age > 35) { foundOver35 = true; break; } } if(foundOver35) { console.log("Yup, there are a few people here!") }
问题?代码看起来太像C或Java了。“冗长”是我想到的另一个词。更有经验的JavaScript程序员可能会想到“丑陋”、“可怕”等。我认为他们是正确的。改进这段代码的一种方法是使用类似map()
的方法,但即便如此,解决方案也有些笨拙。
幸运的是,我们有一个相当简洁的函数,称为some()
,它已经存在于核心语言中。此函数适用于数组,并接受一个自定义的“过滤”函数,返回布尔值true
或false
。本质上,它正在做我们过去几分钟一直在尝试做的事情,只是非常简洁和优雅。以下是如何使用它:
const persons = [ { name: 'Person 1', age: 32 }, { name: 'Person 2', age: 40 }, ]; if(persons.some(person => { return person.age > 35 })) { console.log("Found some people!") }
同样的输入,和以前一样的结果。但请注意代码量的减少!还要注意认知负担是如何显著减少的,因为我们不再需要像自己是解释器一样逐行解析代码!现在代码读起来几乎像自然语言一样。
every()方法
与some()
类似,我们还有另一个有用的函数,叫做every()
。你现在可能已经猜到,它也会返回一个布尔值,具体取决于数组中的所有元素是否都通过了给定的测试。当然,要通过的测试通常是作为匿名函数提供的。我将省略代码的幼稚版本,直接展示如何使用every()
:
const entries = [ { id: 1 }, { id: 2 }, { id: 3 }, ]; if(entries.every(entry => { return Number.isInteger(entry.id) && entry.id > 0; })) { console.log("All the entries have a valid id") }
代码显然会检查数组中的所有对象是否具有有效的id
属性。“有效”的定义取决于问题的上下文,但在上面的代码中,我将其定义为非负整数。再次,我们看到代码是如何简单和优雅,这是这个(和类似)函数的唯一目标。
includes()方法
你如何检查子字符串和数组元素是否存在?如果像我一样,你可能会立即想到indexOf()
,然后查找文档以了解其可能的返回值。这相当不方便,并且返回值很难记住。但是我们可以使用一个不错的替代方法:includes()
。它的用法和名字一样简单,产生的代码非常简洁。请记住,includes()
执行的匹配区分大小写,但这符合我们的直觉。现在,让我们编写一些代码!
const numbers = [1, 2, 3, 4, 5]; console.log(numbers.includes(4)); const name = "Ankush"; console.log(name.includes('ank')); // 输出 false,因为第一个字母是小写 console.log(name.includes('Ank')); // 输出 true,符合预期
但是,不要对这种看似简单的方法抱有太多期望:
const user = {a: 10, b: 20}; console.log(user.includes('a')); // 报错,因为对象没有 "includes" 方法
它无法查看对象内部,因为它根本没有为对象定义。但是,我们知道它适用于数组,所以我们可以在这里做一些小技巧… 🤔。
const persons = [{name: 'Phil'}, {name: 'Jane'}]; persons.includes({name: 'Phil'});
这段代码会发生什么?它不会报错,但输出结果令人失望:false
。这与对象、指针以及JavaScript如何查看和管理内存有关。如果你想深入了解,可以自己尝试一下,但我在这里停止讨论。
如果我们像下面这样重写代码,我们可以让上面的代码正常工作,但在我看来,这或多或少成了一个笑话:
const phil = {name: 'Phil'}; const persons = [phil, {name: 'Jane'}]; persons.includes(phil); // 输出 true
尽管如此,它确实表明我们可以让includes()
对对象起作用,所以我想这并不是一场彻底的灾难。 😄
slice()方法
假设我们有一个字符串,我要求你返回其中以“r”开头并以“z”结尾的部分。你会如何处理?也许你会创建一个新字符串,用来存储所有需要的字符并返回它们。或者,如果像大多数程序员一样,你会给我两个数组索引:一个表示子字符串的开始,另一个表示结束。
这两种方法都很好,但是有一个称为切片的概念,它在这种情况下提供了一个巧妙的解决方案。值得庆幸的是,没有深奥的理论需要学习;切片的意思和听起来的一样——从给定的字符串/数组创建一个更小的字符串/数组,就像我们创建水果切片一样。让我们用一个简单的例子来说明我的意思:
const headline = "And in tonight's special, the guest we've all been waiting for!"; const startIndex = headline.indexOf('guest'); const endIndex = headline.indexOf('waiting'); const newHeadline = headline.slice(startIndex, endIndex); console.log(newHeadline); // 输出 guest we've all been
当我们使用slice()
时,我们为JavaScript提供了两个索引——一个是我们想要开始切片的位置,另一个是我们想要它停止的位置。slice()
的结束索引不包含在最终结果中,这就是为什么我们看到上面代码中的新标题中缺少“waiting”一词的原因。
切片的概念在其他语言中更为突出,尤其是Python。那些开发人员会说,他们无法想象没有这个功能的生活,当语言为切片提供非常简洁的语法时,这是正确的。
切片非常简洁,非常方便,没有理由不用它。它也不是充满性能损失的语法糖,因为它创建了原始数组/字符串的浅拷贝。对于JavaScript开发人员,我强烈建议熟悉slice()
并将其添加到你的工具箱中!
splice()方法
splice()
方法听起来像是slice()
的表亲,在某些方面,我们可以说它是。两者都从原始数组/字符串创建新的数组/字符串,但有一个小但重要的区别——splice()
删除、更改或添加元素,但会修改原始数组。这种对原始数组的“破坏”可能会产生巨大的问题。我想知道是什么阻止了开发人员使用与slice()
相同的方法并保持原始数组不变,但我们也许可以对一种仅用了十天就创建出来的语言更加宽容。
尽管我有些抱怨,让我们看看splice()
是如何工作的。我将展示一个示例,其中我们从数组中删除一些元素,因为这是此方法最常见的用法。我不会提供添加和插入的示例,因为这些示例很容易查找并且也很简单。
const items = ['eggs', 'milk', 'cheese', 'bread', 'butter']; items.splice(2, 1); console.log(items); // 输出 [ 'eggs', 'milk', 'bread', 'butter' ]
上面对splice()
的调用是说:从数组的索引2(即第三个位置)开始,删除一项。在给定的数组中,'cheese'
是第三个元素,因此它被从数组中删除,并且数组的长度被缩短,正如预期的那样。顺便说一句,删除的元素由splice()
以数组的形式返回,因此如果需要,我们可以将其保存在一个变量中。
根据我的经验,indexOf()
和splice()
有很好的协同作用——我们找到一个元素的索引,然后从给定的数组中删除它。但是,请注意,它并不总是最有效的方法,并且通常使用对象(相当于哈希映射)键会快得多。
shift()方法
shift()
是一种方便的方法,可以删除数组的第一个元素。请注意,同样的事情可以用splice()
完成,但是当您只需要删除第一个元素时,shift()
更容易记住和直观。
const items = ['eggs', 'milk', 'cheese', 'bread', 'butter']; items.shift() console.log(items); // 输出 [ 'milk', 'cheese', 'bread', 'butter' ]
unshift()方法
就像shift()
从数组中删除第一个元素一样,unshift()
将一个新元素添加到数组的开头。它的使用同样简单紧凑:
const items = ['eggs', 'milk']; items.unshift('bread') console.log(items); // 输出 [ 'bread', 'eggs', 'milk' ]
尽管如此,我还是忍不住要警告那些新手:与流行的push()
和pop()
方法不同,shift()
和unshift()
效率非常低(因为底层算法的工作方式)。因此,如果在大型数组(例如超过2000个元素)上进行操作,则过度使用这些函数调用可能会使您的应用程序陷入停顿。
fill()方法
有时,你需要将几个元素更改为单个值,甚至可以说“重置”整个数组。在这些情况下,fill()
可以避免循环和差一错误。它可用于用给定的值替换部分或全部数组。让我们看几个例子:
const heights = [1, 2, 4, 5, 6, 7, 1, 1]; heights.fill(0); console.log(heights); // 输出 [0, 0, 0, 0, 0, 0, 0, 0] const heights2 = [1, 2, 4, 5, 6, 7, 1, 1]; heights2.fill(0, 4); console.log(heights2); // 输出 [1, 2, 4, 5, 0, 0, 0, 0]
其他值得一提的功能
尽管上面的列表是大多数JavaScript开发人员在他们的职业生涯中会遇到并使用的,但这绝不是一个完整的列表。JavaScript中还有许多次要但有用的函数(方法),不可能在一篇文章中涵盖所有这些。以下是一些我想到的:
- reverse()
- sort()
- entries()
- flat()
- find()
- flatMap()
我鼓励你至少查阅一下这些函数,以便了解这些便捷工具的存在。
结论
JavaScript是一门庞大的语言,尽管需要学习的核心概念很少。我们可用的许多功能(方法)构成了这门语言的庞大主体。然而,由于JavaScript是大多数开发人员的第二门语言,我们并没有深入研究,错过了它提供的许多优雅和有用的功能。实际上,函数式编程概念也是如此,但那是另一天的话题!🤗
尽可能花一些时间探索核心语言(如果可能,探索知名的实用程序库,例如Lodash)。即使花几分钟来完成这项工作,也会带来巨大的生产力提升和更简洁、更紧凑的代码。