前11个(及更多!)必须了解的JavaScript函数
聪明编码!通过掌握这些语言中最重要且经常使用的函数,成为一个更快、更高效和更快乐的 JavaScript 开发者。
无论是后端还是前端(甚至还有 spaceships),JavaScript 都无处不在。它也是一种非常灵活的语言(意味着它具有强大的函数式编程模式和传统的类),它与其他“C-like”语言的相似性使得从其他语言过渡到 JavaScript 对开发人员来说很容易。
如果你想要 level up your JS game,我建议你学习、实践并最终掌握语言中提供的以下核心函数。这些函数并不是解决问题时都”必需”的,但在某些情况下它们可以为你做很多重活,在其他情况下,它们可以减少你需要编写的代码量。
map()
如果没有提到 map()
,那么在重要的 JavaScript 函数上撰写一篇文章将是异端邪说!😆😆 与 filter()
和 reduce()
一起,map()
形成了一种神圣的三位一体。这些函数在你的职业生涯中将反复使用,所以它们绝对值得一看。让我们逐个解决它们,从 map()
开始。
map()
是给正在学习 JavaScript 的人带来最多麻烦的函数之一。为什么呢?并不是因为它本身有什么复杂之处,而是因为它工作的方式是从所谓的函数式编程中借来的一个思想。因为我们没有接触到函数式编程——我们的学校和行业都充斥着面向对象的语言——所以这种工作方式对于我们有偏见的大脑来说似乎很奇怪甚至是错误的。
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 (
{product.name}
{product.description}
);
});
};
在这里,我们有一个功能性的 React 组件,它接收一个产品列表作为其属性。从这个列表(数组)中,它构建出一个 HTML“div”列表,实际上将每个产品对象转换为 HTML。原始的 products
对象保持不变。
您可以争辩说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("上周总共有多少天气更冷: " + colderDays.length); // 1
请注意,我们传递给filter()
的匿名函数必须返回一个布尔值:true
或false
。这是filter()
知道是否将该项包含在过滤后的数组中的方式。您可以在此匿名函数中编写任意复杂的逻辑;您可以进行API调用和读取用户输入等操作,只要最后确保返回一个布尔值即可。
注意:这是一个我根据作为JavaScript开发人员的经验而感到必须提供的附注。由于粗心大意或错误的基础知识,许多程序员在使用filter()
时在其程序中创建了微妙的错误。让我们将上面的代码重写以包含这个错误:
const weeklyReadings = [20, 22, 20.5, 19, 21, 21.5, 23];
const colderDays = weeklyReadings.filter(dayTemperature => {
return dayTemperature < 20;
});
if(colderDays) {
console.log("是的,上周有更冷的天气");
} else {
console.log("不,上周没有更冷的天气");
}
注意到了吗?如果您注意到了,那就太棒了!最后的if
条件检查的是colderDays
,而它实际上是一个数组!您会惊讶地发现,人们在追求截止日期或在低落情绪下编写代码时(出于任何原因),犯这种错误的次数有多少。这个条件的问题在于JavaScript是一种奇怪和不一致的语言,在许多方面都是如此,其中之一是“真实性”。尽管[] == true
返回false,让您认为上面的代码没有问题,但实际上,在if
条件中,[]
的求值结果为true!换句话说,我们编写的代码永远不会说上周没有更冷的天气。
修复非常简单,就像在上面的代码之前给出的那样。我们检查colderDays.length
,这保证给我们一个整数(零或更大),因此在逻辑比较中能够始终一致地工作。请注意,filter()
将始终返回一个数组,无论是空数组还是非空数组,因此我们可以依靠它并自信地编写我们的逻辑比较。
这个回顾比我计划的要长,但像这种错误一样的bug值得用一万个字来强调,如果需要的话用大写字母。我希望您不会受到这个问题的困扰,并节省数百小时的调试工作!🙂
reduce()
在本文和标准JavaScript库中的所有函数中,reduce()
是“令人困惑和奇怪”的佼佼者之一。虽然这个函数在许多情况下非常重要并可以生成优雅的代码,但大多数JavaScript开发人员都会避免使用它,而更喜欢编写更冗长的代码。
原因是——我在这里诚实地说吧!——reduce()
很难理解,无论是概念还是执行。当你阅读它的描述时,你需要多次重新阅读它,但仍然会怀疑自己是否读错了;当你看到它在工作中,并尝试想象它是如何工作的时候,你的大脑就会扭成一千个结!🤭
现在,不要害怕。 reduce()
函数在复杂性和威慑力上都远远不及,比如 B+ Trees 及其算法。只是这种逻辑在普通程序员的日常工作中很少遇到。
所以,在把你吓尽之后,立即告诉你不要担心,我最后想给你展示一下这个函数是什么,以及为什么我们可能需要它。
顾名思义,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)
。“accumulator”只是一个变量的花哨名称,用于收集和保存计算结果;就像使用一个名为total
的变量来对数组中的所有项求和一样,total += arr[i]
。这正是reducer函数在reduce()
中的应用方式:累加器最初设置为您提供的初始值,然后依次访问数组中的元素,执行计算,结果存储在累加器中,依此类推。。。
那么,reducer函数中的“current value”是什么?如果我让您遍历一个数组,您会在脑海中想象一个变量从索引0开始,一次向前移动一步。在这个过程中,如果我突然叫你停下来,你会发现自己在数组的一个元素上,对吧?这就是我们所说的current value:它是用于表示当前正在考虑的数组项的变量的值(如果有帮助的话,请考虑循环遍历数组)。
说了这么多,现在是时候看一个简单的例子,并了解如何在实际的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()
?
这是一个非常好的问题,老实说,我没有一个确定的答案。无论是通过循环、forEach()
等方式,都可以完成reduce()
的工作。然而,这些技术产生了更多的代码,使得阅读起来更加困难,尤其是在赶时间的情况下。然后还有对不可变性的关注:使用reduce()
和类似的函数,您可以确保原始数据没有被改变;这本身就消除了整个类别的错误,特别是在分布式应用程序中。
最后,reduce()
更加灵活,累加器可以是对象、数组,甚至是函数;同样,起始值和函数调用的其他部分也是如此——几乎任何内容都可以输入,几乎任何内容都可以输出,因此在设计可重用代码时具有极大的灵活性。
如果你仍然不相信,那也完全没关系;JavaScript社区本身对于reduce()
的“紧凑性”、“优雅性”和“强大性”存在严重分歧,所以如果你不使用它也没关系😊 但请确保在决定放弃reduce()
之前先看一些链接4。
some()
假设你有一个对象数组,每个对象表示一个人。你想知道数组中是否有年龄超过35岁的人。注意,没有必要计算这样的人有多少,更不用说获取它们的列表了。我们这里说的是“一个或多个”或“至少一个”的等价说法。
你该如何做到这一点?
是的,你可以创建一个标志变量并循环遍历数组来解决这个问题,就像这样:
const persons = [
{
name: 'Person 1',
age: 32
},
{
name: 'Person 2',
age: 40
},
];
let foundOver35 = false;
for (let i = 0; i 35) {
foundOver35 = true;
break;
}
}
if(foundOver35) {
console.log("是的,这里有几个人!")
}
问题是,我认为这段代码太像C语言或Java,”冗长”是另一个浮现在脑海中的词。有经验的JS可能认为是”丑陋”、”可怕”等等😝 因此,我认为改进这段代码的一种方法是使用类似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("找到一些人!")
}
输如相同,结果与之前一样;但请注意代码量的大幅减少!还请注意,认知负担大大减轻了,因为我们不再需要像解释器一样逐行解析代码!现在,代码几乎读起来就像自然语言。
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("所有条目都有一个有效的id")
}
显然,该代码检查数组中的所有对象是否具有有效的id
属性。关于“有效”的定义取决于问题的上下文,但如你所见,对于这段代码,我将其定义为非负整数。再次看到,代码的简单和优雅程度是阅读的唯一目标,这也是这个(以及类似的)函数的唯一目标。
includes()
你如何检查子字符串和数组元素的存在?如果你像我一样,你会迅速使用indexOf()
,然后查阅文档以确保了解其可能的返回值。这是一个相当不方便的事情,而且很难记住返回值的含义(快,返回值为2
意味着什么?)。
但我们有一个很好的替代方法可以使用: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如何查看和管理内存有关,这是一个非常复杂的领域。如果你想深入了解,可以随时深入研究(也许从这里开始here),但我会在这里停止。
如果我们将上面的代码重写如下,那么它就能正常工作了,但在我看来,这几乎成了一个笑话:
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()相同的方法并保持原始数组不变,但我猜我们可以对一种语言created in merely ten days更宽容。
尽管我有些抱怨,还是让我们来看看splice()是如何工作的。我将展示一个从数组中删除几个元素的例子,因为这是你会在使用这个方法时遇到的最常见的用法。我将不提供添加和插入的示例,因为这些可以很容易地找到并且也很简单。
const items = ['eggs', 'milk', 'cheese', 'bread', 'butter'];
items.splice(2, 1);
console.log(items); // [ 'eggs', 'milk', 'bread', 'butter' ]
上面对splice()的调用表示:从数组的索引2(也就是第三个位置)开始,删除一个项。在给定的数组中,'cheese'是第三个项,所以它被从数组中删除,并且数组的长度会相应缩短。顺便说一句,被删除的项会以数组的形式被splice()返回,所以如果我们愿意,我们可以将'cheese'存储在一个变量中。
根据我的经验,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()
fill()
find()
flat()
我鼓励你至少查阅一下这些函数,以便了解这些便利之处。
结论
JavaScript 是一门庞大的语言,尽管要学习的核心概念很少。可用的许多函数(方法)构成了这个庞大的规模。然而,由于 JavaScript 对大多数 developers 来说只是一门次要语言,我们没有深入学习,错过了它提供的许多美丽和有用的函数。实际上,对于函数式编程概念也是如此,但这是另一个话题! 😅
无论何时,都要花些时间探索核心语言(如果可能的话,还有一些著名的实用库,比如 Lodash)。哪怕只花几分钟的时间来努力,也会带来巨大的生产力提升和更干净、更简洁的代码。