五、你好世界以及更多:您的第一个应用
啊,古老的“Hello World!”脚本。 虽然非常简单,但它是任何语言的良好的首次测试。 不过,让我们做的不仅仅是打个招呼; 让我们使用几个小应用,我们将使用它们来进行操作。 毕竟,编程不仅仅是理论。 我们将看看在编码挑战中出现的一个常见问题,并了解我们的程序如何工作。
本章将涵盖以下主题:
- I/O 与控制台和警报消息
- 处理函数中的输入
- 使用对象作为数据存储
- 理解范围
技术要求
克隆或从https://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers下载这本书的知识库,并准备浏览Chapter-5
材料。
I/O 与控制台和警报消息
到目前为止,我们已经看到了 JavaScript 如何向用户输出信息。 考虑以下代码:
const Officer = function(name, rank, posting) {
this.name = name
this.rank = rank
this.posting = posting
this.sayHello = () => {
console.log(this.name)
}
}
const Riker = new Officer("Will Riker", "Commander", "U.S.S. Enterprise")
现在,如果我们执行Riker.sayHello()
,我们将在控制台中看到以下内容:
Figure 5.1 – Console output
您可以在存储库中的chapter-5
目录中查看一下:https://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-5/alerts-and-prompts/console.html。
好了,好了。 我们有一些控制台输出,但这不是一个非常有效的获取输出的方法,因为用户通常不会打开控制台。 有一种方便的输出方法,虽然对于成熟的 web 应用不实用,但对于测试和调试目的是有用的:alert()
。 这里有一个例子:
const Officer = function(name, rank, posting) {
this.name = name
this.rank = rank
this.posting = posting
this.sayHello = () => {
alert(this.name)
}
}
const Riker = new Officer("Will Riker", "Commander", "U.S.S. Enterprise")
Riker.sayHello()
尝试从https://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-5/alerts-and-prompts/alert.html运行上述代码。 你看到了什么?
Figure 5.2 – Alert message
太棒了! 我们有一个烦人的弹出窗口,你可能在网上见过。 当使用不当时,它们可能会令人恼火,但如果使用得当,它们会非常方便。
让我们看看类似的事情会给我们从用户输入(https://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/blob/master/chapter-5/alerts-and-prompts/prompt.html):
const Officer = function(name, rank, posting) {
this.name = name
this.rank = rank
this.posting = posting
this.ask = () => {
const values = ['name','rank','posting']
let answer = prompt("What would you like to know about this officer?")
answer = answer.toLowerCase()
if (values.indexOf(answer) < 0) {
alert('Value not found')
} else {
alert(this[answer])
}
}
}
const Riker = new Officer("Will Riker", "Commander", "U.S.S. Enterprise")
Riker.ask()
当您加载页面时,您将看到一个带有输入字段的弹出框。 输入name
、rank
或posting
查看结果。 如果刷新并输入其他选项,您应该得到一个“Value not found”的响应。
啊! 但我们也来看看下面这行:
answer = answer.toLowerCase()
由于这是前端 JavaScript,我们不知道用户将输入什么,所以我们应该为轻微的格式错误做好准备。 数据净化是另一个主题,所以现在,让我们同意我们可以用小写字母来匹配整个字符串的期望值。
到目前为止,一切顺利。 现在,让我们看看answer
是如何使用的。
处理函数中的输入
如果我们看一下前面的对象,我们会看到以下内容:
if (values.indexOf(answer) < 0) {
alert('Value not found')
} else {
alert(this[answer])
}
...
因为我们处理的是任意输入,所以我们要做的第一件事是检查我们的答案数组,看看所请求的属性是否存在。 如果没有,则会发出一个简单的错误消息。 如果发现为,则可以提示该值。 如果你记得第 3 章,Nitty-Gritty Grammar,对象属性可以通过点符号和括号符号访问。 在本例中,我们使用一个变量作为键,所以我们不能这样做,因为它将被解释为键。 因此,我们使用括号符号来访问正确的对象值。
练习-斐波那契数列
对于这个练习,构造一个函数来取一个数。 最终结果应该是斐波那契数列(https://en.wikipedia.org/wiki/Fibonacci_number)到您输入的指定数字的和。 序列的前几个数字是[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
。 每个数字都是前两个数字的和; 例如,f[6] = 13
因为f[5] = 8
和f[4] = 5
,所以f[6] = 8+5 = 13
。 您可以在https://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-5/fibonacci/starter-code使用启动器代码。 不要太担心最有效的算法来计算数字; 只是要确保不要硬编码值,而是依赖于输入变量和公式。
斐波那契数列的解决方案
让我们分析一个可能的解决方案:
function fibonacci(num) {
let a = 1, b = 0, temp
while (num >= 0) {
temp = a
a = a + b
b = temp
num--
}
return b
}
let response = prompt("How many numbers?")
alert(`The Fibonacci number is ${fibonacci(response)}`)
让我们先看看函数外部的行。 我们所做的只是简单地询问用户他们想要计算的序列中的哪一点。 然后将response
变量作为fibonacci
的参数输入alert()
语句,fibonacci
的参数为num
。 从这一点开始,while()
循环在num
上执行,num
随着b
的值递增而递减,最后返回到我们的警报消息中。
这就是它的全部内容! 现在,让我们尝试一种变体,因为我们永远不知道用户会输入什么。 如果他们输入的是字符串而不是数字,会发生什么? 我们应该适应这一点,至少呈现一个错误消息。
让我们来看看这个解决方案:
function fibonacci(num) {
let a = 1, b = 0, temp
while (num >= 0) {
temp = a
a = a + b
b = temp
num--
}
return b
}
let response = prompt("How many numbers?")
while (typeof(parseInt(response)) !== "number" || !Number.isInteger(parseFloat(response))) {
response = prompt("Please enter an integer:")
}
alert(`The Fibonacci number is ${fibonacci(response)}`)
You can find the solution on GitHub at https://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-5/fibonacci/solution-code-number-check.
如果我们深入到while()
循环,我们将看到我们的类型匹配魔法。 首先,由于response
本质上是一个字符串,所以我们决定不相信类型强制,而这正是我们之前的解决方案所做的。 我们使用parseInt()
方法将response
直接转换为一个数字。 太棒了! 但这并没有给我们提供用户一开始输入整数时的安全性。 记住,JavaScript 没有int
和float
的概念,所以我们必须通过使用Number.isInteger
方法的反运算来确保输入是一个整数。 这确保我们的输入是一个有效的整数。
作为使用 JSON 的繁重工作的前奏,让我们看看如何使用对象作为数据存储。
使用对象作为数据存储
这是我在编程采访中看到的一个有趣的问题,以及解决它的最有效的方法。 它有一个昂贵的输入时间,但 O(1)检索时间,这通常被认为是算法复杂性的成功度量,当您可以预期更多的读比写。
运动——乘法
const a = [1, 3, 5, 7, 9]
const b = [2, 5, 7, 9, 14]
// compute the products of each permutation for efficient retrieval
const products = { }
// ...
const getProducts = function(a,b) {
// make an efficient means of retrieval
// ...
}
// bonus: get an arbitrary key/value pair. If nonexistent, compute it and store it.
那么,在使用对象的范例中有什么解决方案呢? 让我们看一看,分解它,然后对对象作为数据存储的使用进行逆向工程(破坏者警告:你听说过 NoSQL 吗?)
乘法解决方案
在开始之前,让我们将问题分解为两个步骤:给定两个数组,首先计算数组中每个项的乘积,并将它们存储在一个对象中。 然后,我们将编写一个函数从数组中检索两个给定数字的乘积。 让我们来看看。
步骤 1 -计算和存储
首先,我们的makeProducts
函数将把这两个数组作为它的参数。 使用数组的.forEach()
方法,我们将迭代第一个数组中的每一项,将值命名为multiplicant
:
const makeProducts = function(array1, array2) {
array1.forEach( (multiplicant) => {
if (!products[multiplicant]) {
products[multiplicant] = { }
}
array2.forEach( (multiplier) => {
if (!products[multiplier]) {
products[multiplier] = { }
}
products[multiplicant][multiplier] = multiplicant * multiplier
products[multiplier][multiplicant] = products[multiplicant]
[multiplier]
})
})
}
现在,我们的最终目标是一个对象,会告诉我们“的产品 x和是z”。如果我们这个抽象为使用对象作为一个数据存储,我们可以得出一个这样的结构:**
{
x: {
y: z
},
y: {
x: z
}
}
在这个对象结构中,我们所需要做的就是指定x.y
,也就是z
。 我们也不想假设一个顺序,所以我们也做相反的:y.z
。
那么,我们如何构造这个数据对象呢? 记住,如果不是调用字面键,我们可以对对象使用括号符号; 这里,我们使用了一个变量:
if (!products[multiplicant]) {
products[multiplicant] = { }
}
我们的第一步是检查multiplicant
键是否存在于我们的对象中(x
,在前面的理论讨论中)。 如果没有,则将其设置为一个新对象。
现在,在内部循环中,让我们对乘数做同样的操作:
if (!products[multiplier]) {
products[multiplier] = { }
}
太棒了! 我们设置了x
和y
的钥匙。 现在,我们只需要计算产品并将其存储在两个位置,就像这样:
products[multiplicant][multiplier] = multiplicant * multiplier
products[multiplier][multiplicant] = products[multiplicant][multiplier]
注意将反向键值赋给反向键值的决定,而不是重新计算乘积。 我们为什么要这么做? 事实上,为什么我们要为一个简单的数学运算而费这么大劲呢? 原因是:如果我们做的不是简单的乘法,而是更为复杂的计算,会怎么样? 也许是一个非常复杂的计算,需要一秒钟甚至更长的时间才能返回? 现在我们可以看到我们想要减少我们的时间,这样我们只做一次计算,然后可以重复读取它以获得最佳性能。
*构造完这个函数后,我们将在数组中执行它:
makeProducts(a,b)
这很容易调用!
步骤 2 -检索
现在,让我们写我们的检索函数:
const getProducts = function(a,b) {
// make an efficient means of retrieval
if (products[a]) {
return products[a][b] || null
}
return null
}
如果我们看看这个逻辑,首先我们要确保第一个键存在。 如果它存在,则返回x.y
,如果y
不存在则返回null
。 对象是挑剔的,如果你试图引用一个不存在的键的值,你会得到一个错误。 因此,我们首先需要对密钥进行存在性检查。 如果键存在和键/值对存在,返回计算值; 否则,返回null
。 注意return products[a][b] || null
短路:这是表示“返回值或其他东西”的一种有效方式。 如果products[a][b]
不存在,它将响应一个假值,而OR
操作将接管。 高效!
看一下奖金问题答案的解决方案代码。 存在检验和计算的原则同样适用。
理解范围
在构建更大的应用之前,让我们先讨论一下范围。 简单地说,作用域定义了何时何地可以使用变量或函数。 JavaScript 中的作用域分为两类:局部和全局。 如果我们看一下之前的乘法程序,我们可以看到在任何函数之外都有三个变量; 它们在我们程序的根级
01: const a = [1, 3, 5, 7, 9]
02: const b = [2, 5, 7, 9, 14]
03:
04: // compute the products of each permutation for efficient retrieval
05:
06: const products = { }
07:
08: const makeProducts = function(array1, array2) {
09: array1.forEach( (multiplicant) => {
10: if (!products[multiplicant]) {
11: products[multiplicant] = { }
12: }
13: array2.forEach( (multiplier) => {
14: if (!products[multiplier]) {
15: products[multiplier] = { }
16: }
17: products[multiplicant][multiplier] = multiplicant *
multiplier
18: products[multiplier][multiplicant] = products[multiplicant]
[multiplier]
19: })
20: })
21: }
22:
23: const getProducts = function(a,b) {
24: // make an efficient means of retrieval
25: if (products[a]) {
26: return products[a][b] || null
27: }
28: return null
29: }
30:
31: makeProducts(a,b)
所讨论的变量在第 1、2 和 6 行:a
、b
和products
。 太棒了! 这意味着我们可以在任何地方使用它们,比如在第 10、11、14、15 和更多行,只要我们在定义它们之后使用它们。 现在,如果我们仔细观察,我们还会看到一些全局范围内的函数:makeProducts
和getProducts
。 同样地,只要它们已经被定义了,我们就可以在任何地方使用它们。
很好,这很有道理,因为 JavaScript 是从上到下读取的。 但是等等! 如果你还记得第 3 章,Nitty-Gritty Grammar,中的函数声明,它会被吊到顶部,因此可以在任何地方使用。
让我们重构我们的程序来利用提升,并将数学抽象为理论上的长时间运行的流程。 我们还将使用Promises
作为一个很棒的概念介绍。 在我们深入了解它之前,阅读一下Promises
:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises可能是有用的。
看看https://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-5/matrix-refactored中的index.js
。 我们会一步一步地分解。
首先,在浏览器中打开index.html
。 确保控制台是打开的。 2 秒后,您将在控制台中看到一条简单的消息:9 x 2 = 18。 如果您查看index.js
中的第 44 行,您将看到它使用getProducts
来计算a[4]
和b[0]
的乘积,分别是9
和2
。 太棒了! 到目前为止,我们的功能与添加感知延迟相同。
让我们从头开始:
1: const a = [1, 3, 5, 7, 9]
2: const b = [2, 5, 7, 9, 14]
3:
4: // compute the products of each permutation for efficient retrieval
5:
6: const products = {}
7:
到目前为止,我们的代码是一样的。 那么,我们的makeProducts
函数呢?
08: const makeProducts = async function(array1, array2) {
09: const promises = []
10: array1.forEach((multiplicant) => {
11: if (!products[multiplicant]) {
12: products[multiplicant] = {}
13: }
14: array2.forEach(async (multiplier) => {
15: if (!products[multiplier]) {
16: products[multiplier] = {}
17: }
18:
19: promises.push(new Promise(resolve =>
resolve(calculation(multiplicant, multiplier))))
20: promises[promises.length - 1].then((val) => {
21: products[multiplicant][multiplier] = products[
multiplier][multiplicant] = val
22: })
23: })
24: })
25: return promises
26: }
嗯。 好的,我们有一些相同的作品,但也有一些新的作品。 首先,让我们考虑async
。 当与函数一起使用时,这个关键字意味着该函数的消费者期望的是异步行为,而不是 JavaScript 通常自上而下的行为。 在我们深入分析新的行 19-21 之前,让我们看看为什么这个函数是异步的,通过检查我们的calculation
函数:
37: async function calculation(value1, value2) {
38: await new Promise(resolve => setTimeout(resolve, 2000))
39: return value1 * value2
40: }
在第 37 行再次显示async
,现在我们在第 38 行看到一个新的关键字:await
。 async
和await
是指定我们可以异步工作的一种方式:在第 38 行,我们指定我们正在等待promise
到解析,然后再继续。 我们的promise
在做什么? 嗯,事实证明,并不是很多! 它只是使用setTimeout
来延迟 2000 毫秒。 此延迟旨在模拟长时间运行的流程,例如 Ajax 调用或需要 2 秒完成的复杂流程(甚至需要不确定的时间)。
好了,好了。 到目前为止,我们基本上是在欺骗程序,让它预计在继续之前会有 2 秒的延迟。 让我们看看第 9 行:一个名为promises
的新数组。 现在,回到我们的作用域的概念,您可以注意到我们的数组在makeProducts
中定义了。 这意味着变量只存在于函数的局部范围内。 与产品相反,我们不能从该功能之外访问承诺。 没关系——我们真的不需要。 事实上,将全局作用域中定义的变量数量保持在最小被认为是最佳实践。
现在,让我们看看第 19 行,它看起来更微妙一些:
promises.push(new Promise(resolve => resolve(calculation(multiplicant, multiplier))))
如果我们分析这个,我们首先会看到一些相似的东西:我们将一些东西推到我们的promises
数组。 我们推入的是一个新的Promise
,类似于第 38 行,但在本例中,我们不是在行中等待它,而是说“每当它发生时,用calculation()
的值解析这个promise
。” 到目前为止,一切顺利。 下一部分呢?
20: promises[promises.length - 1].then((val) => {
21: products[multiplicant][multiplier] = products[multiplier]
[multiplicant] = val
22: })
现在,这里有一些语法糖发挥作用:现在我们在数组promises
中有了promise
,我们用[promises.length - 1]
访问它,因为length
返回从1
开始的完整长度。 .then()
子句是我们的魔法:它说一旦promise
完成了,就用结果做一些事情。 在这种情况下,我们的something是将val
分配给产品的两个变体。 最后,在第 25 行,我们返回数组promises
。
我们的getProducts
功能根本没有改变! 我们检索函数的复杂性仍然是:高效。
这个怎么样?
42: makeProducts(a,b).then((arrOfPromises) => {
43: Promise.all(arrOfPromises).then(() => {
44: console.log(`${a[4]} x ${b[0]} = ${getProducts(a[4], b[0])}`)
// 18
45: })
46: })
我们以前见过.then
,所以它的参数是makeProducts
的返回值,该返回值是promises
的数组。 然后,我们可以在.then
之前使用.all()
,有效地表示“当arrOfPromises
中的promises
全部解决后,再执行下一个函数”。 下一个函数是记录答案。 您可以在第 44 行之后添加额外的产品检查; 它们都将与第 44 行同时返回,因为“计算”中的延迟已经发生。
范围链和范围树
进一步深入到范围,我们有了范围链和范围树的概念。 让我们考虑下面的例子:
function someFunc() {
let outerVar = 1;
function zip() {
let innerVar = 2;
}
}
someFunc
可以访问哪些变量? zip
可以访问什么? 如果您猜测someFunc
可以访问outerVar
,而zip
可以访问innerVar
和outerVar
,那么您猜对了。 这是因为两个变量都存在于zip
的范围链中,而someFunc
的范围中只有outerVar
存在。 不清晰吗? 太好了。 让我们看一些图表。
看看下面的代码:
function someFunc() {
function zip() {
function foo() {
}
}
function quux() {
}
}
我们可以从自顶向下的构造中绘制函数的范围树:
Figure 5.3 – Scope tree
这告诉我们什么? quux
种生活在它自己的小世界里someFunc
。 它可以访问someFunc
的变量,但不能访问、、zip
或foo
的变量。 我们也可以用范围链反过来看,从下往上理解它:
Figure 5.4 – Scope chain
在这个例子中,我们看一下foo
可以访问什么。 从下往上,我们可以看到它与代码的其他部分的关系。
闭包
现在,我们将进入闭包,这在 JavaScript 中显然是一个可怕的话题。 然而,闭包的基本概念是可接近的:闭包只是另一个函数中的一个函数,它可以访问其父函数的作用域链。 在这种情况下,它有三个作用域链:它自己的作用域链,在它自己内部定义了变量; 全局的,它可以访问全局作用域中的所有变量; 父函数的作用域。
下面是我们要分析的一个例子:
function someFunc() {
let bar = 1;
function zip() {
alert(bar); // 1
let beep = 2;
function foo() {
alert(bar); // 1
alert(beep); // 2
}
}
}
哪些变量可以被哪些函数访问? 这里有一个图:
Figure 5.5 – Closures
从下往上,foo
可以访问beep
和bar
,zip
只能访问bar
。 到目前为止,一切顺利,对吧? 闭包只是描述每个嵌套函数可用范围的一种方法。 它们本身没有什么可怕的。
这是一个实践中的闭包的基本例子
看看下面的函数:
function sayHello(name) {
const sayAlert = function() {
alert(greeting)
}
let greeting = `Hello ${name}`
return sayAlert
}
sayHello('Alice')()
alert(greeting)
首先,让我们看看这个有趣的结构:sayHello('Alice')()
。 由于我们的sayAlert()
函数是sayHello
的返回值,我们首先用一对带参数的圆括号调用sayHello
函数,然后用第二对圆括号调用它的返回值sayAlert
函数。 注意greeting
是如何在sayHello
的范围内的,当我们调用我们的函数时,我们将有一个 Hello Alice 的警告。 然而,如果我们试图提醒greeting
本身,我们将得到一个错误。 只有sayAlert
可以使用greeting
。 同样,如果我们试图从函数外部访问name
,我们会得到一个错误。
总结
为了使我们的程序有用,它们通常依赖于用户或其他函数的输入。 通过构建灵活的程序,我们还需要记住作用域的概念:何时何地可以使用函数或变量。 我们还了解了如何使用对象有效地存储数据以进行检索。
我们不要忘记闭包,这个看似复杂的概念,实际上只是描述作用域的一种方式。
在下一章中,我们将更多地探索前端,因为我们开始使用文档对象模型(DOM)和操作页面上的信息,而不仅仅是与警报和控制台交互。
问题
考虑以下代码:
function someFunc() {
let bar = 1;
function zip() {
alert(bar); // 1
let beep = 2;
function foo() {
alert(bar); // 1
alert(beep); // 2
}
}
return zip
}
function sayHello(name) {
const sayAlert = function() {
alert(greeting)
}
const sayZip = function() {
someFunc.zip()
}
let greeting = `Hello ${name}`
return sayAlert
}
- 你怎么收到 Hello Bob 的警报?
sayHello()('Bob')
sayHello('Bob')()
sayHello('Bob')
someFunc()(sayHello('Bob'))
- 在前面的代码中,
alert(greeting)
将做什么?- 警报的问候。
- 警报你好爱丽丝。
- 抛出一个错误。
- 以上都不是。
- 我们如何得到 1 的警告信息?
someFunc()()
sayHello().sayZip()
alert(someFunc.bar)
sayZip()
- 我们如何得到 2 的警告信息?
someFunc().foo()
。someFunc()().beep
。- 我们不能,因为它不在范围内。
- 我们不能,因为它没有定义。
- 如何将
someFunc
改为 alert 1 12 ?- 我们不能。
- 在
return zip
之后加return foo
。 - 将
return zip
改为return foo
。 - 在
foo
声明后添加return foo
。
- 给出上述问题的正确答案,我们如何得到三个 1,1,2 警告?
someFunc()()()
someFunc()().foo()
someFunc.foo()
alert(someFunc)
进一步的阅读
- MDN - closures:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
- :http://javascriptissexy.com/understand-javascript-closures-with-ease/**
版权属于:月萌API www.moonapi.com,转载请注明出处