五、你好世界以及更多:您的第一个应用

啊,古老的“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()

当您加载页面时,您将看到一个带有输入字段的弹出框。 输入namerankposting查看结果。 如果刷新并输入其他选项,您应该得到一个“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] = 8f[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 没有intfloat的概念,所以我们必须通过使用Number.isInteger方法的反运算来确保输入是一个整数。 这确保我们的输入是一个有效的整数。

作为使用 JSON 的繁重工作的前奏,让我们看看如何使用对象作为数据存储。

使用对象作为数据存储

这是我在编程采访中看到的一个有趣的问题,以及解决它的最有效的方法。 它有一个昂贵的输入时间,但 O(1)检索时间,这通常被认为是算法复杂性的成功度量,当您可以预期更多的读比写。

运动——乘法

考虑以下代码(https://github.com/PacktPublishing/Hands-on-JavaScript-for-Python-Developers/tree/master/chapter-5/matrix/starter-code):

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]
    })
  })
}

现在,我们的最终目标是一个对象,会告诉我们“的产品 xz”。如果我们这个抽象为使用对象作为一个数据存储,我们可以得出一个这样的结构:**

{
  x: {
    y: z
  },
  y: {
    x: z
  }
}

在这个对象结构中,我们所需要做的就是指定x.y,也就是z。 我们也不想假设一个顺序,所以我们也做相反的:y.z

那么,我们如何构造这个数据对象呢? 记住,如果不是调用字面键,我们可以对对象使用括号符号; 这里,我们使用了一个变量:

if (!products[multiplicant]) {
    products[multiplicant] = { }
}

我们的第一步是检查multiplicant键是否存在于我们的对象中(x,在前面的理论讨论中)。 如果没有,则将其设置为一个新对象。

现在,在内部循环中,让我们对乘数做同样的操作:

if (!products[multiplier]) {
    products[multiplier] = { }
}

太棒了! 我们设置了xy的钥匙。 现在,我们只需要计算产品并将其存储在两个位置,就像这样:

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 行:abproducts。 太棒了! 这意味着我们可以在任何地方使用它们,比如在第 10、11、14、15 和更多行,只要我们在定义它们之后使用它们。 现在,如果我们仔细观察,我们还会看到一些全局范围内的函数:makeProductsgetProducts。 同样地,只要它们已经被定义了,我们就可以在任何地方使用它们。

很好,这很有道理,因为 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]的乘积,分别是92。 太棒了! 到目前为止,我们的功能与添加感知延迟相同。

让我们从头开始:

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 行看到一个新的关键字:awaitasyncawait是指定我们可以异步工作的一种方式:在第 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可以访问innerVarouterVar,那么您猜对了。 这是因为两个变量都存在于zip的范围链中,而someFunc的范围中只有outerVar存在。 不清晰吗? 太好了。 让我们看一些图表。

看看下面的代码:

function someFunc() {
  function zip() {
    function foo() {
    }
  }
  function quux() {
  }
}

我们可以从自顶向下的构造中绘制函数的范围树:

Figure 5.3 – Scope tree

这告诉我们什么? quux种生活在它自己的小世界里someFunc。 它可以访问someFunc的变量,但不能访问zipfoo的变量。 我们也可以用范围链反过来看,从下往上理解它:

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可以访问beepbarzip只能访问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
}
  1. 你怎么收到 Hello Bob 的警报?
    1. sayHello()('Bob')
    2. sayHello('Bob')()
    3. sayHello('Bob')
    4. someFunc()(sayHello('Bob'))
  2. 在前面的代码中,alert(greeting)将做什么?
    1. 警报的问候。
    2. 警报你好爱丽丝。
    3. 抛出一个错误。
    4. 以上都不是。
  3. 我们如何得到 1 的警告信息?
    1. someFunc()()
    2. sayHello().sayZip()
    3. alert(someFunc.bar)
    4. sayZip()
  4. 我们如何得到 2 的警告信息?
    1. someFunc().foo()
    2. someFunc()().beep
    3. 我们不能,因为它不在范围内。
    4. 我们不能,因为它没有定义。
  5. 如何将someFunc改为 alert 1 12 ?
    1. 我们不能。
    2. return zip之后加return foo
    3. return zip改为return foo
    4. foo声明后添加return foo
  6. 给出上述问题的正确答案,我们如何得到三个 1,1,2 警告?
    1. someFunc()()()
    2. someFunc()().foo()
    3. someFunc.foo()
    4. alert(someFunc)

进一步的阅读