在本文的part-one中,我们开始探索JavaScript规定的广泛功能世界,或更精确的eCmascript ECMA-262。现在,我们将在讨论一些更奇特的功能JS提供的一些更奇特的功能形式之前,发现JS的异步和功能编程方面。
异步
实际上有多种方法支持异步操作,其中许多使用Event Loop (see MDN)来控制执行函数的顺序以及何时。
我们将首先调查具有多种用途的呼叫回扣。然后,我们将在最后查看使用异步和等待说明的实际同步操作之前讨论承诺。
呼叫
“呼叫回来”是通常给出的函数,该函数传递给另一个函数作为参数。目的是,在某个时候,主要功能可以执行“呼叫”功能。有很多用例,这种机制派上用场,但是对于我们大多数人来说,活动处理程序将是我们的第一次相遇。
活动处理程序
JS开发人员可能会遇到的第一个事件处理程序很可能是以下内容之一。
按钮单击
除非您正在开发的网页试图最大程度地减少JavaScript的使用(贵族目标),否则很可能会有屏幕的按钮在用户单击时需要反应(做某事)。<<<<<<<<< /p>
有几种配置此方法的方法,许多JS框架无论如何都将大部分“接线”从开发人员的手中拿出来,但是这是可以在引擎盖下完成的一种方法。出于我们的目的,我们将在HTML文件的相同片段中包含所有“活动部件”。
<body>
<button id="btnClickMe">Click Me</button>
<script>
const domButton = document.querySelector('#btnClickMe');
domButton.addEventListener('click', sayHello);
function sayHello() {
alert('Hello there');
}
</script>
</body>
上面的HTML代码不是完整的文档,但是可以将片段复制到HTML文件并在Web浏览器中查看。网络浏览器非常宽容,将填充空白。
在屏幕上,将有一个带有文字“单击我”的按钮。这就是HTML按钮的性质,我们可以使用鼠标单击按钮,也可以通过键盘选择它。对于后者,我们可能需要进行圆形标签,直到按下按钮焦点,然后再按下ENTER
键来启动事件。无论哪种方式,结果都将是包含文本“ Hello helly”的警报框的呈现,该框将需要按下OK
按钮才能取消。
页面启动
另一种情况是,当屏幕加载后,我们需要立即执行一些JavaScript。同样,此类用例由许多JS框架管理,因为其他考虑可能会影响用户体验。
<script>
document.addEventListener('DOMContentLoaded', sayHello);
function sayHello() {
alert('Hello there');
}
</script>
在上面的示例中,发生的事情甚至更少。当浏览器完成加载文档时,将执行任何注册为DomContentLoaded
事件的侦听器的功能,在这种情况下,这是我们用于按钮的sayHello
函数。
计时器/间隔
事件处理程序呼叫的用例很多
<script>
setTimeout(sayHello, 2000);
function sayHello() {
alert('Hello there');
}
</script>
在此示例中,屏幕完成加载(和处理脚本)后2秒(或2000毫米秒)将执行呼叫后背部并显示警报横幅。
这是一个教科书示例,构成了一个非常常见的技术访谈问题的基础,但我发现需要使用koude4或其表弟koude5在生产代码中很少。
排序比较器
很长一段时间以来,可能是最常见的呼叫式用例之一是在数组中使用sort项目。 JS中的koude6对象具有多种方法可以在数组本身的内容上执行操作,有时将更新的内容留在适当的位置(如排序操作),否则会生成新的数组或其他一些输出,作为一个结果。
我们将稍后探索其他一些方法,但首先让我们深入研究分类。我们将简单地从名称列表开始。
const names = ['Yvonne', 'Wesley', 'Andrew',
'Terry', 'Brian', 'Xavier'];
names.sort();
console.table(names);
上面的示例使用默认的简单字母顺序排序比较器函数,该比较器函数使用console.table
输出以下。
索引 | 值 |
---|---|
0 | Andrew |
1 | Brian |
2 | Terry |
3 | 卫斯理 |
4 | Xavier |
5 | yvonne |
显然,默认排序比较器相当有限。如果我们想要反向顺序的名称(好的,有一个方法reverse
)或我们要排序的数据是数字(我们不希望像[1, 10, 2]
这样的输出)或更可能的用例是对象而不是原始值。然后我们需要编码自己的排序比较器。
const objs = [
{ name: 'Yvonne', birthMonth: 8 },
{ name: 'Wesley', birthMonth: 10 },
{ name: 'Andrew', birthMonth: 2 },
{ name: 'Terry', birthMonth: 12 },
{ name: 'Brian', birthMonth: 1 },
{ name: 'Xavier', birthMonth: 4 },
];
objs.sort(sortComparator);
console.table(objs);
function sortComparator(objA, objB) {
return objA.birthMonth - objB.birthMonth;
}
在上面的代码示例中观察我们如何为Sort方法提供了称为sortComparator
的呼叫函数(但可以称为任何东西),该函数期望从数组中传递两个元素并返回数字值。简而言之,0表示将订单保持不变(即使值相同),<0表示在OBJA的顺序中排列,然后objb,> 0表示重新安排OBJB,然后是OBJA(有关更多详细信息,请参见MDN)。与上述生育特性的上述比较,输出如下。
索引 | 名称 | 出生月 |
---|---|---|
0 | Brian | 1 |
1 | Andrew | 2 |
2 | Xavier | 4 |
3 | yvonne | 8 |
4 | 卫斯理 | 10 |
5 | Terry | 12 |
sort
方法消除了在JavaScript中实现算法的需求,并且可以作为较低,更性能的级别实现。 for
循环用于遍历数组的元素的频率如此之高,以至于将遍历阵列的代码移动到较低,性能更高,级别也很有意义。但是有很多原因穿越数组。
// A common array traversal pattern
const arr = [ ... ];
var index = 0;
for (index = 0; index < arr.length; index++) {
// Code to act on each item of the array (arr[index]) in turn.
}
上面的代码并不难编写或理解,但是很常见且容易抽象。
接下来的三个数组方法都使用呼叫回扣来产生输出,而无需更改(突变)原始数组。这三种方法(以及其他将要提及的方法)都是对语言的最新添加。在Ecmascript 6(2015)中添加。
过滤谓词
使用以前使用的名称数组,我们用如下编写过滤代码。在下面的示例中,我们正在过滤(保留)包含字母“ y”的名称;无论在任何情况下,任何地方的任何地方。
const names = ['Yvonne', 'Wesley', 'Andrew',
'Terry', 'Brian', 'Xavier'];
let namesContainingYs = [];
var index = 0;
for (index = 0; index < names.length; index++) {
if (names[index].toLowerCase().includes('y')) {
namesContainingYs.push(names[index]);
}
}
console.table(namesContainingYs);
索引 | 值 |
---|---|
0 | yvonne |
1 | 卫斯理 |
2 | Terry |
使用新的filter
方法可以简化代码。
const names = ['Yvonne', 'Wesley', 'Andrew',
'Terry', 'Brian', 'Xavier'];
const namesContainingYs = names.filter(name =>
name.toLowerCase().includes('y'));
console.table(namesContainingYs);
关键观察包括:
- 过滤器的结果存储在常数阵列
namesContainingYs
中。 - 过滤减少到一条线,不需要
for
循环或index
变量。 - 条件是所有意图和目的都没有变化的。
- 呼叫函数仅返回
true
或false
。此类函数给出了谓词的名称,并由许多其他数组方法(例如:enver,find,findIndex,findIndexlast,foreach,foreach)和某些。 使用。
值得注意的是,在这些示例中,我们将使用一个或两个参数调用数组方法,但其中任何一个都可以选择期望更多参数(有关详细信息,请参见MDN)。
地图变压器
遍历数组的另一种常见用例是转换每个元素从一种类型到另一种类型。例如,让我们将所有名称转换为大写。
const names = ['Yvonne', 'Wesley', 'Andrew',
'Terry', 'Brian', 'Xavier'];
const upperCaseNames = names.map(name => name.toUpperCase());
console.table(upperCaseNames);
索引 | 值 |
---|---|
0 | yvonne |
1 | 卫斯理 |
2 | Andrew |
3 | Terry |
4 | Brian |
5 | Xavier |
filter
和map
都穿过整个数组,因此,如果您的用例需要(一个接一个),则应在可能的情况下首先执行过滤器,以减少转换为最小值的项目数量。但是我们也许可以使用下一个方法reduce
做得更好。
减少还原器
reduce
方法更加复杂,是混乱的常见来源,这就是为什么我对该主题有written的原因。使用reduce
方法,可以在不诉诸for
循环的情况下实现filter
和map
的版本。但是是什么?
filter
方法将始终返回包含的数组,该数组不超过源数组,并且可能更少,而这些项目以相同的序列和不变(转换)为单位。 map
方法将产生一个新数组,其中包含与源数量相同数量的项目,不再,少于。
reduce
可以做到这两者。实际上,它甚至不必返回数组,而可以返回其他数据类型,例如计数,平均值或总和。为什么我们没有数学。
一种完全人为的用例,但这是我们可以使用reduce
添加所有出生月的方式。
const objs = [
{ name: 'Yvonne', birthMonth: 8 },
{ name: 'Wesley', birthMonth: 10 },
{ name: 'Andrew', birthMonth: 2 },
{ name: 'Terry', birthMonth: 12 },
{ name: 'Brian', birthMonth: 1 },
{ name: 'Xavier', birthMonth: 4 },
];
const totalBirthMonths = objs.reduce((tot, obj) =>
tot + obj.birthMonth, 0);
console.log(`Total = ${totalBirthMonths}`); // "Total = 37"
请注意,使用reduce
方法的第二个参数如何初始化运行总数为0。第一个参数称为降低方法,因为在将两个输入值减少为一个输出值中。使用reducer
,第一个输入是运行总数,第二个输入是来自数组的每个项目。
如上所述,呼叫函数仍然是最新的,也是一个重要的理解技术。但是,对于异步代码,它们可能很快变得难以管理。在长时间之前,需要在另一个呼叫中筑巢。缩放此模式是有限的,因为必须在许多层次的嵌套中,将难以理解代码,难以调试,几乎不可能进行单位测试。
Promises
承诺不是函数,而是创建时通过呼叫函数传递的一种特殊类型的对象。呼叫函数本身是通过两个函数作为参数传递的。称为resolve
和reject
。
当承诺开始时,它将执行初始呼叫函数以执行其主要目的。当功能结论时,它将根据功能是否成功(resolve
)或失败(reject
)来调用所提供的辅助功能之一。
这是执行实际硬币并返回单个硬币的承诺对象的函数。
function tossCoin() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const coinToss = ~~(Math.random() * 5);
if (coinToss) resolve(coinToss % 2 ? 'Tails' : 'Heads');
reject('Dropped');
}, 300);
});
}
示例一(下)使用上面创建的承诺实现的“当时”协议来处理结果。当硬币会导致头部或尾部产生头部时,Then
被称为catch
当硬币掉落时。
for (let i = 1; i <= 10; i++) {
tossCoin()
.then(result => console.log(i, result))
.catch(result => console.error(i, result));
}
在上面的代码示例中,我们正在模拟10个硬币的扔。 tossCoin
函数创建了一个新的Promise
,这是对硬币折腾结果的期望。但是,扔硬币的人有点笨拙,将硬币掉落在20%的时间中,因此不是50:50的头或尾巴。上述示例的输出和接下来的两个,看起来像这样:
1 Heads
2 Tails
3 Dropped
4 Tails
5 Tails
6 Dropped
7 Tails
8 Heads
9 Heads
10 Heads
将硬币翻转需要300ms,并产生结果。传统的for
循环(第一个调用)结果开始流动10次,大约300ms调用该功能。请注意,在以前的硬币结果之后,硬币键盘未开始,但几乎同时又不开始。这表明,当承诺函数启动时,处理不会停止,这给出了异步操作的有效印象,但这只是事件环优化主线程。关于事件环的主题,我强烈推荐Lydia Hallie的文章。
tossCoin
函数内部的Math.random
方法在0到1之间生成一个值。该值乘以5,并将其四舍五入为0和4之间的整数。每个代表20%概率的整数。如果值为零,则认为该值被认为是掉落的,否则结果是尾巴,如果true(奇数)或false(偶数),则结果为尾部。当丢弃硬币时,请通过呼叫(通过)reject
呼叫呼叫的承诺报告,该呼叫可行,该呼叫行使catch
路径。有效的硬币折腾结果通过then
路径的承诺报道了呼叫resolve
的承诺。
类似于exceptions的常规报告,承诺的“当时”模式还包括一种finally
方法,一旦承诺完成,无论结果是清理的一种形式。
承诺提供了一系列支持方法,以帮助开发人员编写更好的异步代码,但是一段时间以后阅读复杂的异步代码仍然很困难。至少,不像读取同步代码那样容易,这是以下异步/等待构造的位置。
Async/Await
JavaScript是一种不寻常的语言,原因有很多,尤其是因为它在单个线程中运行。但这并不意味着它无法支持异步操作。
基本的呼叫后,JS得到了承诺,但现在我们也具有特殊的(异步/等待)功能。在引擎盖下,这些功能是承诺和发电机的组合(请参阅稍后),但恰到好处的范围。
async/等待与承诺一起工作,但以代码同步读取的方式,这简化了理解。
(async function () {
for (let i = 1; i <= 10; i++) {
try {
const result = await tossCoin();
console.log(i, result);
} catch (err) {
console.error(i, err);
}
}
})();
尽管这三个示例都产生相同的输出,但行为的基本差异。在第一个承诺示例中,在快速连续产生所有结果之前,有300ms的延迟。在上面的异步/等待示例中,在接下来的300ms延迟启动之前出现第一个硬币折腾的结果之前,有300毫秒的延迟。这取决于tossCoin
函数在for
循环中使用await
关键字。
在第二个示例中,所有十枚硬币花了大约3秒钟才能完成,但是第一个示例花费了超过300ms才能完成。但是,大多数开发人员发现第二个示例的语法更容易。略有下降的是,await
需要在async
函数内部操作,这就是为什么for
循环包裹在Immediately Invoked Function Expression (IIFE)中的原因。
const coins = [];
for (let i = 1; i <= 10; i++) {
coins.push(tossCoin());
}
Promise.allSettled(coins).then(results =>
results.forEach(({ status, value, reason }, i) => {
if (status === 'fulfilled') {
console.log(i + 1, value);
} else {
console.error(i + 1, reason);
}
})
);
在上面的第三个示例中,for
循环反复调用toscoin函数,并收集返回数组中的承诺。然后,我们在数组上使用koude56调用来处理所有10个完成后处理结果。这种行为就像第一个示例,其中所有10个硬币都以300毫秒以上的时间完成。差异包括;没有包装async
功能,也没有await
。结果阵列的轻微并发症(通常有某种内容)阵列涉及每个结果包含三个属性:
- 状态:承诺的结果,“实现”或“被拒绝”。
- 值:如果“实现”这是主要功能的输出。
- 原因:如果“拒绝”这就是为什么主要功能失败的原因。
功能编程样式
JavaScript不是FP语言,但这也不是完全OOP语言。它是越来越多的“多范式”语言之一,在某种程度上支持各种范式中的功能,这可能就是为什么它具有许多不同类型的功能。
与本文讨论的许多函数类型不同,以下各节描述了通过JavaScript中的语法直接支持的函数;他们不是“惯用的”。但是,他们确实大量利用了JS对功能作为一流对象,高阶功能和关闭的支持。
Curried and Partial Application
在我的脑海中,咖喱更多地是关于过程而不是一种函数。 “咖喱”功能是期望一次性参数一次,一次返回一个新功能,直到提供了所有强制性参数为止。只有这样,实际执行的函数。 “咖喱”是采用常规功能并生成咖喱功能的过程。
咖喱功能的教科书示例可能是计算常规固体形状的体积。
function calculateVolume(width) {
return function(breadth) {
return function(height) {
return width * breadth * height;
}
}
}
可以使用四个置换函数,但这是两个极端。全部或通过单独的电话。
const allInOne = calculateVolume(2)(3)(7); // 42
const supplyBreadth = calculateVolume(2); // new function
const supplyHeight = supplyBreadth(3); // new function
console.log(supplyHeight(7)); // 42
所有排列仅接受每个呼叫的单个参数/参数绑定,但是部分应用程序更适合。将常规函数转换为支持部分应用程序的功能更为复杂。因此,建议使用诸如Lodash的库。
上一个示例以部分应用程序实现,可以以上面显示的方式调用,但此外允许以下调用。
const firstTwo = calculateVolume(2, 3);
console.log(firstTwo(7)); // 42
const onlyFirst = calculateVolume(2); // new function
console.log(onlyFirst(3, 7)); // 42
在上面的两个示例中,第一个呼叫返回了一个函数期望更多(但不一定是所有其余)参数。
那么有什么好处?
在我相关的post中,我详细讨论了这一点,但一个很好的例子是提供动态的属性排序比较器。可以在JSFiddle中找到示例源代码。
我们从一组18个物体开始,每个对象都为Fleetwood Mac,Cream,Queen和The Doors的成员定义了Forename,姓氏和乐队名称。例如,
const bandsArray = [
{
forename: 'Jack',
surname: 'Bruce',
band: 'Cream',
},
// :
{
forename: 'John',
surname: 'Deacon',
band: 'Queen',
},
];
我们可以创建一个使用部分应用程序返回A 给定属性的函数。
function dynamicSortComparator(propertyName) {
return (objA, objB) =>
(objA[propertyName] < objB[propertyName] ? -1 : 1);
}
通常,我们可以使用比较器函数调用sort
方法,但只能比较特定的对象属性。相反,使用上述函数,我们可以按照以下方式按姓氏或forename进行排序:
bandsArray.sort(dynamicSortComparator('surname'));
console.table(bandsArray);
bandsArray.sort(dynamicSortComparator('forename'));
console.table(bandsArray);
我在this post中更详细介绍了部分应用。
Recursion
递归是一种由许多编程语言支持的技术,有些是比其他编程更好的。应该注意的是,JS缺少一个称为Tail-call optimisation的特定功能,该功能大大提高了递归功能的性能,但这并不意味着它是JS中某些问题的不适当候选解决方案。
。不受语言或要解决的问题,在创建递归功能时需要考虑一些技巧。从根本上讲,递归功能自称为自称,因此我们需要考虑如何停止执行函数,而不会返回。
用于证明递归的教科书示例包括计算factorials或Fibonacci numbers,但我们将使用它来计算复合兴趣(利息利息),很长一段时间。
数学家:我完全知道有一个方便的配方可靠,但这不会证明递归。
因此,参数将是:
- 校长:初始贷款的金额。
- 利率:假定在整个贷款期间都是恒定的,并以百分比表示,这是贷款将增长的利率(年份),而不是考虑还款。
- 持续时间:在几年中,贷款将运行。
function calculateInterest(principal, interestRate, duration) {
// recursion happens here
}
是的,我们可以使用简单的for
循环来解决此问题,但是有一些问题for
循环不足。我希望使用此问题,这应该是一个简单的例子。
以其核心功能,我们需要一些简单的数学来计算一年的贷款的增加。
newPrincipal = principal + principal * interestRate;
,但我们需要防止无限循环的风险,我们可以使用贷款的持续时间进行。
function calculateInterest(principal, interestRate, duration) {
if (duration === 0) { // guard
return principal;
}
return calculateInterest(
principal + principal * interestRate,
interestRate,
--duration // decrease the duration by 1 year each cycle
);
}
这将产生以下结果:
校长 | 年 | 利息 | 总贷款 |
---|---|---|---|
1000 | 1 | 200 | 1200 |
1200 | 2 | 240 | 1440 |
1440 | 3 | 288 | 1728 |
1728 | 4 | 345.6 | 2073.6 |
2073.6 | 5 | 414.72 | 2488.32 |
但是可以将功能简化为:
function calculateInterest(principal, interestRate, duration) {
return duration ? calculateInterest(
principal + principal * interestRate,
interestRate,
--duration
) : principal;
}
现在,要获得更多异国情调的功能
简短的“将脚趾浸入脚趾”陷入了不熟悉的东西。
Generators
可以说,JS发电机支持一些异常用例。如果我对它们更熟悉或更聪明,我可能会发现更多使用它们的机会,但我认为我从未专业使用过它们。
简而言之,生成器提供了一种创建“可重复可口”功能的方法。每次称为生成的函数都会以不同的方式行为,并在其yield
控件中重新输入该函数。
试图解释复杂或新颖的概念时,使用更熟悉的技术复制主题是有用的。在本文的一部分中,我们讨论了封闭式,现在我们将用来模拟发电机的行为。我们将基于MDN网页的一个示例复制以下示例。
{
const foo = function* () {
yield 'a';
yield 'b';
yield 'c';
};
exercise('Idiomatic generator', foo);
}
我们还将演示通过以下练习使用的行为和iteration protocols。
// Exercise one: Using a for iterator
function exercise(exerciseName, foo) {
console.log(exerciseName);
let str = '';
for (const val of foo()) {
str += val;
}
console.log(str); // Output: 'abc'
}
// Exercise two: Calling the next method
function exercise(exerciseName, foo) {
console.log(exerciseName);
const gen = foo();
let str = gen.next().value;
str += gen.next().value;
str += gen.next().value;
console.log(str); // Output: 'abc'
}
以下代码片段是上面的foo
发电机的模拟,但使用闭合来维护呼叫之间的状态。
{
const foo = (function (...yieldResults) {
let yieldIndex = 0;
return next;
function next() {
const mdnIterator = {
next() {
return {
done: yieldIndex === yieldResults.length,
value: yieldResults[yieldIndex++],
};
},
[Symbol.iterator]() {
return this;
},
};
return mdnIterator;
}
})('a', 'b', 'c');
exercise('Simulated generator', foo);
}
模拟比MDN示例更重要,因为它必须手动实施迭代协议,但两种实现都以相同的效果执行练习。
要观察的要点包括:
- 每次被称为该函数。
-
yield
命令的行为有点像return
命令,可以从函数内部发送一个值。 - 随后的呼叫简历是从最后使用的
yield
点开始的。
如果您想阅读更多有关发电机LuisPa García的帖子的信息,您可能会觉得很有趣。
AsyncGenerators
更进一步,甚至还有异步发电机,但它们无法解释它们何时使用。
读取器:如果您有用例演示如何使用这些类型的功能,我很想了解更多。请在下面写下评论或更好地写一篇文章,然后在评论中添加链接。