PHP-MySQL-和-JavaScript-学习指南第六版-六-

60 阅读27分钟

PHP、MySQL 和 JavaScript 学习指南第六版(六)

原文:zh.annas-archive.org/md5/4aa97a1e8046991cb9f8d5f0f234943f

译者:飞龙

协议:CC BY-NC-SA 4.0

第十六章:JavaScript 函数、对象和数组

就像 PHP 一样,JavaScript 提供了访问函数和对象的方法。事实上,JavaScript 实际上是基于对象的,因为——正如你已经看到的——它必须访问 DOM,这使得 HTML 文档的每个元素都可以作为一个对象进行操作。

使用和语法也与 PHP 非常相似,因此当我带你深入了解 JavaScript 中的函数和对象使用以及数组处理时,你应该会感到很自在。

JavaScript 函数

除了访问数十个内置函数(或方法),如 write,你已经在 document.write 中看到它被使用,你还可以轻松创建自己的函数。每当你有一个可能会被重复使用的相对复杂的代码片段时,你就可以考虑创建一个函数。

定义函数

函数的一般语法如下所示:

function *`function_name`*([*`parameter`* [, ...]])
{
  *`statements`*
}

语法的第一行指示以下内容:

  • 定义从 function 这个词开始。

  • 名称必须以字母或下划线开头,后跟任意数量的字母、数字、美元符号或下划线。

  • 括号是必需的。

  • 可选一个或多个用逗号分隔的参数(由方括号表示,不是函数语法的一部分)。

函数名区分大小写,因此以下所有字符串都指代不同的函数:getInputGETINPUTgetinput

在 JavaScript 中,函数有一个通用的命名约定:名称中每个单词的第一个字母大写,除了第一个字母外,其他字母都小写。因此,在前面的例子中,大多数程序员会选择使用 getInput 作为首选名称。这种约定通常被称为 bumpyCapsbumpyCase 或(最常见的)camelCase

大括号 { 开始定义函数调用时执行的语句;必须有相匹配的大括号 } 结束它。这些语句可能包括一个或多个 return 语句,它们强制函数停止执行并返回给调用代码。如果 return 语句附带一个值,调用代码可以检索它。

arguments 数组

arguments 数组是每个函数的成员。借助它,你可以确定传递给函数的变量数量及其内容。以函数 displayItems 为例。例子 16-1 展示了一种编写它的方式。

例子 16-1. 定义函数
<script>
  displayItems("Dog", "Cat", "Pony", "Hamster", "Tortoise")

  function displayItems(v1, v2, v3, v4, v5)
  {
    document.write(v1 + "<br>")
    document.write(v2 + "<br>")
    document.write(v3 + "<br>")
    document.write(v4 + "<br>")
    document.write(v5 + "<br>")
  }
</script>

当你在浏览器中调用这个脚本时,它将显示以下内容:

Dog
Cat
Pony
Hamster
Tortoise

这些都很好,但如果你想向函数传递超过五个项目会怎样?此外,多次重复使用document.write调用而不使用循环是一种浪费。幸运的是,arguments数组提供了灵活性,可以处理可变数量的参数。示例 16-2 展示了如何以更高效的方式重写先前的示例。

示例 16-2. 修改使用arguments数组的函数
<script>
  let c = "Car"

  displayItems("Bananas", 32.3, c)

  function displayItems()
  {
    for (j = 0 ; j < displayItems.arguments.length ; ++j)
      document.write(displayItems.arguments[j] + "<br>")
  }
</script>

注意使用了length属性,你已经在上一章中遇到过它,并且我使用变量j作为偏移量引用数组displayItems.arguments。我还选择通过不用花括号将for循环的内容包围起来来保持函数简短而简洁,因为它只包含一个语句。请记住,循环必须在j等于length之前停止,而不是等于length

使用这种技术,你现在有一个函数,可以接受任意多(或少)的参数,并按照你的意愿对每个参数进行操作。

返回值

函数不仅仅用于显示事物。事实上,它们主要用于进行计算或数据操作,然后返回结果。示例 16-3 中的函数fixNames使用了arguments数组(在前一节中讨论过),接收传递给它的一系列字符串,并将它们返回为一个单一的字符串。它执行的“修复”操作是将每个参数中的每个字符转换为小写,除了每个参数的第一个字符,它设置为大写字母。

示例 16-3. 清理完整名称
<script>
  document.write(fixNames("the", "DALLAS", "CowBoys"))

  function fixNames()
  {
    var s = ""

    for (j = 0 ; j < fixNames.arguments.length ; ++j)
      s += fixNames.arguments[j].charAt(0).toUpperCase() +
           fixNames.arguments[j].substr(1).toLowerCase() + " "

    return s.substr(0, s.length-1)
  }
</script>

例如,当使用参数theDALLASCowBoys调用时,该函数返回字符串The Dallas Cowboys。让我们来看看这个函数。

首先将临时(和局部)变量s初始化为空字符串。然后,for循环遍历传递的每个参数,使用charAt方法隔离参数的第一个字符,并使用toUpperCase方法将其转换为大写。此示例中显示的各种方法都内置于 JavaScript 中,并默认可用。

然后使用substr方法获取每个字符串的其余部分,通过toLowerCase方法将其转换为小写。这里substr方法的更完整版本将指定作为第二个参数的子串的长度:

substr(1, (arguments[j].length) - 1 )

换句话说,这个substr方法说,“从位置 1(第二个字符)开始,并返回剩余的字符串(长度减一)。”尽管如此,substr方法会假定,如果你省略第二个参数,你希望返回剩余的字符串。

在整个参数转换为所需大小写之后,向末尾添加空格字符,并将结果追加到临时变量s

最后,再次使用 substr 方法返回变量 s 的内容,除了最后一个不需要的空格。我们通过使用 substr 返回字符串直到最后一个字符之前来移除它。

这个例子特别有趣,因为它展示了在单个表达式中使用多个属性和方法,例如:

fixNames.arguments[j].substr(1).toLowerCase()

您必须在脑海中将该语句分成几个部分来解释。JavaScript 从左到右评估语句的这些元素如下:

  1. 从函数名称本身开始:fixNames

  2. 从代表 fixNames 参数的数组 arguments 中提取元素 j

  3. 调用 substr 并使用参数 1 提取元素。这将除第一个字符外的所有内容传递给表达式的下一部分。

  4. 对已传递的字符串应用 toLowerCase 方法。

这种做法通常被称为 方法链。例如,如果将字符串 mixedCASE 传递给示例表达式,它将经历以下转换:

mixedCASE
ixedCASE
ixedcase

换句话说,fixNames.arguments[j] 生成 “mixedCASE”,然后 substr(1) 获取 “mixedCASE” 并生成 “ixedCASE”,最后 toLowerCase() 获取 “ixedCASE” 并生成 “ixedcase”。

最后提醒一下:函数内部创建的 s 变量是局部的,因此无法在函数外部访问。通过在 return 语句中返回 s,我们使其值可以被调用者使用或存储。但是 s 本身在函数结束时会消失。虽然我们可以让函数操作全局变量(有时是必要的),但最好的方法是只返回想要保留的值,并让 JavaScript 清理函数使用的所有其他变量。

返回一个数组

在 示例 16-3 中,该函数仅返回一个参数——但如果需要返回多个参数怎么办?您可以通过返回一个数组来实现,就像 示例 16-4 中那样。

示例 16-4. 返回一个值数组
<script>
  words = fixNames("the", "DALLAS", "CowBoys")

  for (j = 0 ; j < words.length ; ++j)
    document.write(words[j] + "<br>")

  function fixNames()
  {
    var s = new Array()

    for (j = 0 ; j < fixNames.arguments.length ; ++j)
      s[j] = fixNames.arguments[j].charAt(0).toUpperCase() +
             fixNames.arguments[j].substr(1).toLowerCase()

  return s
}
</script>

这里变量 words 自动定义为数组,并用函数 fixNames 的返回结果填充。然后 for 循环迭代数组并显示每个成员。

至于 fixNames 函数,它与 示例 16-3 几乎完全相同,只是现在变量 s 是一个数组;在处理每个单词后,它作为数组的一个元素存储,并通过 return 语句返回。

此函数允许从其返回值中提取单个参数,例如下面的示例(其输出仅为 The Cowboys):

words = fixNames("the", "DALLAS", "CowBoys")
document.write(words[0] + " " + words[2])

JavaScript 对象

JavaScript 对象比变量更高级,变量一次只能包含一个值。相比之下,对象可以包含多个值,甚至函数。对象将数据与操作数据所需的函数组合在一起。

声明一个类

在创建用于使用对象的脚本时,您需要设计称为的数据和代码组合。基于此类的每个新对象称为该类的实例(或发生)。正如您已经看到的那样,与对象关联的数据称为其属性,而其使用的函数称为方法

让我们看看如何为称为User的对象声明类,该对象将包含有关当前用户的详细信息。要创建类,只需编写一个以类名命名的函数。此函数可以接受参数(稍后将展示如何调用它),并且可以为该类中的对象创建属性和方法。这个函数称为构造函数

示例 16-5 显示了用于User类的构造函数,具有三个属性:forenameusernamepassword。该类还定义了showUser方法。

示例 16-5. 声明User类及其方法
<script>
  function User(forename, username, password)
  {
    this.forename = forename
    this.username = username
    this.password = password

    this.showUser = function()
    {
      document.write("Forename: " + this.forename + "<br>")
      document.write("Username: " + this.username + "<br>")
      document.write("Password: " + this.password + "<br>")
    }
  }
</script>

该函数在几个方面与我们迄今看到的其他函数不同:

  • 每次调用函数时,它都会创建一个新对象。因此,您可以反复调用相同的函数并使用不同的参数来创建具有不同名字的用户,例如。

  • 函数引用一个名为this的对象,该对象引用正在创建的实例。如示例所示,对象使用名称this来设置自己的属性,这些属性将与另一个User不同。

  • 在函数内创建了一个名为showUser的新函数。此处显示的语法是新的且相当复杂的,但其目的是将showUserUser类关联起来。因此,showUser作为User类的方法而存在。

我使用的命名约定是将所有属性保持为小写,并在方法名称中至少使用一个大写字符,遵循本章前面提到的驼峰命名约定。

示例 16-5 遵循编写类构造函数的推荐方式,即在构造函数中包含方法。但是,您还可以参考在构造函数外定义的函数,如示例 16-6。

示例 16-6. 分别定义类和方法
<script>
  function User(forename, username, password)
  {
    this.forename = forename
    this.username = username
    this.password = password
    this.showUser = showUser
  }

  function showUser()
  {
    document.write("Forename: " + this.forename + "<br>")
    document.write("Username: " + this.username + "<br>")
    document.write("Password: " + this.password + "<br>")
  }
</script>

我向您展示这种形式,因为您在查看其他程序员的代码时一定会遇到它。

创建对象

要创建User类的实例,可以使用如下语句:

details = new User("Wolfgang", "w.a.mozart", "composer")

或者您可以创建一个空对象,如下所示:

details = new User()

然后稍后再填充它,如下所示:

details.forename = "Wolfgang"
details.username = "w.a.mozart"
details.password = "composer"

您还可以像这样向对象添加新属性:

details.greeting = "Hello"

您可以验证添加这种新属性是否有效,使用以下语句:

document.write(details.greeting)

访问对象

要访问对象,你可以引用其属性,如以下两个不相关的示例语句:

name = details.forename
if (details.username == "Admin") loginAsAdmin()

因此,要访问类User的对象的showUser方法,你将使用以下语法,其中对象details已经被创建并填充了数据:

details.showUser()

假设之前提供的数据,这段代码将显示如下内容:

Forename: Wolfgang
Username: w.a.mozart
Password: composer

prototype关键字

使用prototype关键字可以节省大量内存。在User类中,每个实例都包含三个属性和一个方法。因此,如果内存中有一千个这些对象,方法showUser也会被复制一千次。然而,因为方法在每种情况下都是相同的,你可以指定新对象应该引用方法的单个实例,而不是创建它的副本。因此,不要在类构造函数中使用以下内容:

this.showUser = function()

你可以用这个替换它:

User.prototype.showUser = function()

示例 16-7 显示了新构造函数的样子。

示例 16-7. 使用prototype关键字声明类的方法
<script>
  function User(forename, username, password)
  {
    this.forename = forename
    this.username = username
    this.password = password

    User.prototype.showUser = function()
    {
      document.write("Forename: " + this.forename + "<br>")
      document.write("Username: " + this.username + "<br>")
      document.write("Password: " + this.password + "<br>")
    }
  }
</script>

这是因为所有函数都有一个prototype属性,用于保存类中任何对象创建时不会复制的属性和方法。相反,它们通过引用传递给其对象。

这意味着你可以随时添加prototype属性或方法,并且所有对象(即使已经创建)都会继承它,如下面的语句所示:

User.prototype.greeting = "Hello"
document.write(details.greeting)

第一个语句将值为Hellogreeting属性添加到类Userprototype属性中。在第二行中,已经创建的对象details正确显示了这个新属性。

你还可以添加或修改类中的方法,如下面的语句所示:

User.prototype.showUser = function()
{
  document.write("Name "  + this.forename +
                 " User " + this.username +
                 " Pass " + this.password)
}

details.showUser()

你可以在脚本中的条件语句(如if)中添加这些行,这样它们将在用户活动导致你需要不同的showUser方法时运行。这些行运行后,即使已经创建了对象details,对details.showUser的进一步调用也会运行新函数。旧的showUser定义已被擦除。

静态方法和属性

在阅读关于 PHP 对象时,你学到了类不仅可以具有与类的特定实例相关联的静态属性和方法,还可以具有静态属性和方法。JavaScript 也支持静态属性和方法,你可以方便地从类的prototype中存储和检索它们。因此,以下语句设置并从User中读取静态字符串:

User.prototype.greeting = "Hello"
document.write(User.prototype.greeting)

扩展 JavaScript 对象

prototype关键字甚至让你能够向内置对象添加功能。例如,假设你想要添加一个功能,将字符串中的所有空格替换为不换行空格以防止其换行。你可以通过向 JavaScript 默认的String对象定义添加一个原型方法来实现这一点,就像这样:

String.prototype.nbsp = function()
{
  return this.replace(/ /g, '&nbsp;')
}

这里使用了replace方法和正则表达式来查找并替换所有单个空格为字符串&nbsp;

注意

如果您还不熟悉正则表达式,它们是一种从字符串中提取信息或操作字符串的方便方法,并且在 第十七章 中有详细解释。总之,目前您可以复制并粘贴上述示例,并按描述的方式运行它们,展示了扩展 JavaScript String 对象的强大功能。

如果您输入以下命令:

document.write("The quick brown fox".nbsp())

它将输出字符串The&nbsp;quick&nbsp;brown&nbsp;fox。或者你可以添加一个方法,用来从字符串中删除前导和尾随空格(再次使用正则表达式):

String.prototype.trim = function()
{
  return this.replace(/^\s+|\s+$/g, '')
}

如果您发出以下语句,输出将是字符串Please trim me(删除了前导和尾随空格):

document.write("  Please trim me    ".trim())

如果我们将表达式分解为其组成部分,两个/字符标记了表达式的开始和结束,最后的g指定了全局搜索。在表达式内部,^\s+部分搜索出现在搜索字符串开头的一个或多个空白字符,而\s+$部分搜索出现在搜索字符串结尾的一个或多个空白字符。中间的|字符用于分隔替代项。

结果是,当任何一个表达式匹配时,匹配项将被替换为空字符串,从而返回一个没有任何前导或尾随空白的修剪过的字符串版本。

警告

关于是否扩展对象是一种好还是坏的做法存在争议。一些程序员认为,如果稍后将对象扩展为正式提供您已添加的功能,可以以另一种方式实现,或者执行与您的扩展完全不同的操作,这可能会导致冲突。然而,其他程序员,如 JavaScript 的发明者 Brendan Eich,认为这是完全可以接受的做法。我倾向于同意后者的观点,但在生产代码中选择最不可能被正式使用的扩展名称。例如,trim 扩展可以重命名为 mytrim,支持代码可能更安全地编写如下:

String.prototype.mytrim = function()
{
  return this.replace(/^\s+|\s+$/g, '')
}

JavaScript 数组

JavaScript 中的数组处理与 PHP 非常相似,尽管语法有些不同。尽管如此,鉴于您已经学到的关于数组的所有知识,这一部分对您来说应该相对简单。

数字数组

要创建一个新数组,请使用以下语法:

arrayname = new Array()

或者您可以使用简写形式,如下所示:

arrayname = []

分配元素值

在 PHP 中,您可以通过简单地分配而不指定元素偏移来向数组添加新元素,如下所示:

$arrayname[] = "Element 1";
$arrayname[] = "Element 2";

但是在 JavaScript 中,您使用push方法来实现相同的操作,就像这样:

arrayname.push("Element 1")
arrayname.push("Element 2")

这允许您在不必跟踪项目数量的情况下继续向数组中添加项目。当您需要知道数组中有多少元素时,可以使用 length 属性,如下所示:

document.write(arrayname.length)

或者,如果你希望自己跟踪元素的位置并将它们放在特定位置,可以使用以下语法:

arrayname[0] = "Element 1"
arrayname[1] = "Element 2"

示例 16-8 展示了创建数组、加载值和显示值的简单脚本。

示例 16-8. 创建、构建和打印一个数组
<script>
  numbers = []
  numbers.push("One")
  numbers.push("Two")
  numbers.push("Three")

  for (j = 0 ; j < numbers.length ; ++j)
    document.write("Element " + j + " = " + numbers[j] + "<br>")
</script>

此脚本的输出如下:

Element 0 = One
Element 1 = Two
Element 2 = Three

使用 Array 关键字赋值

您还可以使用 Array 关键字一起创建一个包含一些初始元素的数组,如下所示:

numbers = Array("One", "Two", "Three")

没有什么可以阻止您之后添加更多元素。

您现在已经看到了可以向数组中添加项目的几种方式,以及引用它们的一种方式。JavaScript 提供了更多选项,我很快会介绍——但首先,我们将看看另一种类型的数组。

关联数组

关联数组 是一种通过名称而不是整数偏移引用元素的数组。然而,JavaScript 不支持这样的事情。相反,我们可以通过创建具有相同功能的属性对象来实现相同的结果。

因此,要创建一个“关联数组”,请在花括号内定义一个元素块。对于每个元素,将键放在冒号 (:) 的左侧,内容放在右侧。示例 16-9 展示了您可能创建一个关联数组来保存在线体育用品零售商“球”部分的内容。

示例 16-9. 创建和显示一个关联数组
<script>
  balls = {"golf":    "Golf balls, 6",
           "tennis":  "Tennis balls, 3",
           "soccer":  "Soccer ball, 1",
           "ping":    "Ping Pong balls, 1 doz"}

  for (ball in balls)
    document.write(ball + " = " + balls[ball] + "<br>")
</script>

为了验证数组已经正确创建并填充,我使用了另一种使用 for 循环和 in 关键字的方式。这会创建一个新变量,仅在数组内部使用(在本例中是 ball),并迭代通过 in 关键字右侧的数组所有元素(在本例中是 balls)。循环作用于 balls 的每个元素,将键值放入 ball 中。

使用存储在 ball 中的键值,你还可以获取 balls 当前元素的值。在浏览器中调用示例脚本的结果如下:

golf = Golf balls, 6
tennis = Tennis balls, 3
soccer = Soccer ball, 1
ping = Ping Pong balls, 1 doz

要获取关联数组的特定元素,可以明确指定键,如下所示(在这种情况下,输出值为 Soccer ball, 1):

document.write(balls['soccer'])

多维数组

要在 JavaScript 中创建一个多维数组,只需将数组放在其他数组中。例如,要创建一个包含二维棋盘(8 × 8 方格)详细信息的数组,您可以使用 示例 16-10 中的代码。

示例 16-10. 创建一个多维数字数组
<script>
  checkerboard = Array(
    Array(' ', 'o', ' ', 'o', ' ', 'o', ' ', 'o'),
    Array('o', ' ', 'o', ' ', 'o', ' ', 'o', ' '),
    Array(' ', 'o', ' ', 'o', ' ', 'o', ' ', 'o'),
    Array(' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '),
    Array(' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '),
    Array('O', ' ', 'O', ' ', 'O', ' ', 'O', ' '),
    Array(' ', 'O', ' ', 'O', ' ', 'O', ' ', 'O'),
    Array('O', ' ', 'O', ' ', 'O', ' ', 'O', ' '))

  document.write("<pre>")

  for (j = 0 ; j < 8 ; ++j)
  {
    for (k = 0 ; k < 8 ; ++k)
      document.write(checkerboard[j][k] + " ")

    document.write("<br>")
  }

  document.write("</pre>")
</script>

在这个例子中,小写字母代表黑色棋子,大写字母代表白色。一对嵌套的 for 循环遍历数组并显示其内容。

外部循环包含两个语句,因此使用大括号将它们括起来。然后内部循环处理每一行中的每个方块,输出位置[j][k]处的字符,后跟一个空格(使输出对齐)。由于此循环只包含一个语句,因此不需要使用大括号将其括起来。使用<pre></pre>标签确保输出正确显示,如下所示:

  o   o   o   o
o   o   o   o
  o   o   o   o

O   O   O   O
  O   O   O   O
O   O   O   O

你也可以直接使用方括号访问数组中的任何元素:

document.write(checkerboard[7][2])

此语句输出大写字母O,从第八个元素开始,沿着第三个元素的位置—请记住数组索引从 0 开始,而不是 1。

使用数组方法

鉴于数组的强大功能,JavaScript 内置了多种用于操作数组及其数据的方法。以下是其中几个最有用的方法。

some

当需要知道是否至少有一个数组元素与特定条件匹配时,可以使用some函数。该函数会测试所有元素,并在找到匹配的元素后自动停止并返回所需的值。这样可以避免编写自己的代码来执行此类搜索,如下所示:

function isBiggerThan10(element, index, array)
{
  return element > 10
}

result = [2, 5, 8, 1, 4].some(isBiggerThan10); // result will be false
result = [12, 5, 8, 1, 4].some(isBiggerThan10); // result will be true

indexOf

要找出数组中元素的位置,可以在数组上调用indexOf函数,该函数将返回所定位元素的偏移量(从 0 开始),如果未找到则返回-1。例如,以下代码将offset赋值为2

animals = ['cat', 'dog', 'cow', 'horse', 'elephant']
offset = animals.indexOf('cow')

concat

concat方法用于连接两个数组或数组内的一系列值。例如,以下代码输出Banana,Grape,Carrot,Cabbage

fruit = ["Banana", "Grape"]
veg   = ["Carrot", "Cabbage"]

document.write(fruit.concat(veg))

你可以将多个数组指定为参数,此时concat方法会按照数组指定的顺序添加所有元素。

下面是另一种使用concat的方法。此时,普通值与数组pets进行了连接,输出为Cat,Dog,Fish,Rabbit,Hamster

pets      = ["Cat", "Dog", "Fish"]
more_pets = pets.concat("Rabbit", "Hamster")

document.write(more_pets)

forEach

JavaScript 中的forEach方法是实现类似于 PHP foreach关键字功能的另一种方式。要使用它,只需传递一个函数名,该函数将针对数组中的每个元素进行调用。示例 16-11 展示了如何使用它。

示例 16-11. 使用forEach方法
<script>
  pets = ["Cat", "Dog", "Rabbit", "Hamster"]
  pets.forEach(output)

  function output(element, index, array)
  {
    document.write("Element at index " + index + " has the value " +
      element + "<br>")
  }
</script>

在这种情况下,传递给forEach的函数称为output。它接受三个参数:element,其index,以及array。这些参数可以根据函数的需要使用。以下示例仅使用document.write函数显示elementindex的值。

一旦数组被填充,就可以像这样调用方法:

pets.forEach(output)

这是输出结果:

Element at index 0 has the value Cat
Element at index 1 has the value Dog
Element at index 2 has the value Rabbit
Element at index 3 has the value Hamster

join

使用join方法,你可以将数组中的所有值转换为字符串,然后将它们连接成一个大字符串,在它们之间可以放置一个可选的分隔符。示例 16-12 展示了三种使用此方法的方式。

示例 16-12. 使用join方法
<script>
  pets = ["Cat", "Dog", "Rabbit", "Hamster"]

  document.write(pets.join()      + "<br>")
  document.write(pets.join(' ')   + "<br>")
  document.write(pets.join(' : ') + "<br>")
</script>

如果没有参数,join将使用逗号来分隔元素;否则,将插入到join中的字符串在每个元素之间插入。示例 16-12 的输出如下所示:

Cat,Dog,Rabbit,Hamster
Cat Dog Rabbit Hamster
Cat : Dog : Rabbit : Hamster

push 和 pop

您已经看到push方法如何用于向数组中插入值。其逆方法是pop。它从数组中删除最近插入的元素并返回它。示例 16-13 展示了其使用示例。

示例 16-13. 使用pushpop方法
<script>
  `sports` `=` `[``"Football"``,` `"Tennis"``,` `"Baseball"``]`
  document.write("Start = "      + sports +  "<br>")

  `sports``.``push``(``"Hockey"``)`
  document.write("After Push = " + sports +  "<br>")

  `removed` `=` `sports``.``pop``(``)`
  document.write("After Pop = "  + sports +  "<br>")
  document.write("Removed = "    + removed + "<br>")
</script>

此脚本的三个主要语句显示为粗体。首先,脚本创建了一个名为sports的数组,其中包含三个元素,然后向数组中push了第四个元素。随后,它将该元素pop出来。在此过程中,通过document.write显示了各种当前值。脚本输出如下:

Start = Football,Tennis,Baseball
After Push = Football,Tennis,Baseball,Hockey
After Pop = Football,Tennis,Baseball
Removed = Hockey

pushpop函数在需要中断某些活动进行其他活动然后返回的情况下非常有用。例如,假设您想要将一些活动推迟到稍后,同时现在进行更重要的事情。这在我们处理“待办”清单时经常发生,所以让我们在代码中模拟这种情况,将六个项目列表中的第 2 和第 5 项赋予优先状态,如示例 16-14 所示。

示例 16-14. 在循环内外使用pushpop
<script>
  numbers = []

  for (j=1 ; j<6 ; ++j)
  {
    if (j == 2 || j == 5)
    {
      document.write("Processing 'todo' #" + j + "<br>")
    }
    else
    {
      document.write("Putting off 'todo' #" + j + " until later<br>")
      `numbers``.``push``(``j``)`
    }
  }

  document.write("<br>Finished processing the priority tasks.")
  document.write("<br>Commencing stored tasks, most recent first.<br><br>")

  document.write("Now processing 'todo' #" + `numbers``.``pop``(``)` + "<br>")
  document.write("Now processing 'todo' #" + `numbers``.``pop``(``)` + "<br>")
  document.write("Now processing 'todo' #" + `numbers``.``pop``(``)` + "<br>")
</script>

当然,这里实际上没有进行任何处理,只是将文本输出到浏览器,但您可以理解这个想法。此示例的输出如下:

Putting off 'todo' #1 until later
Processing 'todo' #2
Putting off 'todo' #3 until later
Putting off 'todo' #4 until later
Processing 'todo' #5

Finished processing the priority tasks.
Commencing stored tasks, most recent first.

Now processing 'todo' #4
Now processing 'todo' #3
Now processing 'todo' #1

使用 reverse

reverse方法简单地颠倒数组中所有元素的顺序。示例 16-15 展示了其工作原理。

示例 16-15. 使用reverse方法
<script>
  sports = ["Football", "Tennis", "Baseball", "Hockey"]
  sports.reverse()
  document.write(sports)
</script>

原始数组被修改,此脚本的输出如下:

Hockey,Baseball,Tennis,Football

排序

使用sort方法,您可以根据使用的参数将数组的所有元素按字母顺序排列。示例 16-16 展示了四种排序类型。

示例 16-16. 使用sort方法
<script>
  // Alphabetical sort
  sports = ["Football", "Tennis", "Baseball", "Hockey"]
  sports.sort()
  document.write(sports + "<br>")

  // Reverse alphabetical sort
  sports = ["Football", "Tennis", "Baseball", "Hockey"]
  sports.sort().reverse()
  document.write(sports + "<br>")

  // Ascending numeric sort
  numbers = [7, 23, 6, 74]
  numbers.sort(function(a,b){return a - b})
  document.write(numbers + "<br>")

  // Descending numeric sort
  numbers = [7, 23, 6, 74]
  numbers.sort(function(a,b){return b - a})
  document.write(numbers + "<br>")
</script>

四个示例部分中的第一个使用默认的sort方法执行字母排序,而第二个则使用默认的sort,然后应用reverse方法来获得反向字母排序

第三和第四部分稍微复杂一些;它们使用一个函数来比较ab之间的关系。该函数没有名称,因为它仅在排序中使用。您已经看到了名为function的函数用于创建匿名函数;我们用它来定义类中的方法(showUser方法)。

这里,function 创建了一个满足 sort 方法需求的匿名函数。如果函数返回大于零的值,则排序假定 ba 之前。如果函数返回小于零的值,则排序假定 ab 之前。排序通过在数组的所有值上运行此函数来确定它们的顺序。(当然,如果 ab 的值相同,函数返回零,先后顺序无关紧要。)

通过操作返回的值(a - bb - a 相比),示例 16-16 的第三和第四部分选择 升序数值排序降序数值排序

信不信由你,这标志着你对 JavaScript 的介绍结束了。现在你应该掌握本书涵盖的三种主要技术的核心知识。下一章将讨论一些跨这些技术使用的高级技术,如模式匹配和输入验证。

问题

  1. JavaScript 函数和变量名区分大小写吗?

  2. 如何编写一个接受和处理无限数量参数的函数?

  3. 给出一个从函数返回多个值的方法。

  4. 在定义类时,用于引用当前对象的关键字是什么?

  5. 类的所有方法是否都必须在类定义内部定义?

  6. 用于创建对象的关键字是什么?

  7. 如何使类中的所有对象都可以访问一个属性或方法,而无需在对象内部复制该属性或方法?

  8. 如何创建一个多维数组?

  9. 用于创建关联数组的语法是什么?

  10. 编写一个语句以按降序对数字数组进行排序。

查看“第十六章答案” 中的 附录 A 获取这些问题的答案。

第十七章:JavaScript 和 PHP 验证与错误处理

有了你在 PHP 和 JavaScript 方面的坚实基础,现在是时候将这些技术结合起来,创建尽可能用户友好的 Web 表单了。

我们将使用 PHP 创建表单,并使用 JavaScript 执行客户端验证,以确保在提交之前数据尽可能完整和正确。然后 PHP 将对输入进行最终验证,必要时会再次向用户呈现表单以供进一步修改。

在这一过程中,本章将涵盖 JavaScript 和 PHP 中的验证和正则表达式。

使用 JavaScript 验证用户输入

JavaScript 验证应被视为更多地为用户提供帮助而不是为你的网站,因为,正如我已经强调过很多次的那样,你不能信任任何提交到服务器的数据,即使它已经通过 JavaScript 验证过。这是因为黑客可以相当容易地模拟你的 Web 表单并提交任意数据。

你不能完全依赖 JavaScript 来执行所有的输入验证的另一个原因是,一些用户禁用了 JavaScript,或者使用不支持 JavaScript 的浏览器。

因此,在 JavaScript 中最好的验证类型是检查字段是否有内容(如果不允许为空),确保电子邮件地址符合正确的格式,并确保输入的值在预期范围内。

验证.html 文档(第一部分)

让我们从一个通用的注册表单开始,这在大多数提供会员或用户注册的网站上都很常见。请求的输入将是 用户名密码年龄电子邮件地址。示例 17-1 提供了这样一个表单的良好模板。

示例 17-1. 具有 JavaScript 验证的表单(第一部分)
<!DOCTYPE html>
<html>
  <head>
    <title>An Example Form</title>
    <style>
      .signup {
        border:1px solid #999999;
        font:  normal 14px helvetica;
        color: #444444;
      }
    </style>
    `<script``>`
      `function` `validate``(``form``)`
      `{`
        `fail`  `=` `validateForename``(``form``.``forename``.``value``)`
        `fail` `+=` `validateSurname``(``form``.``surname``.``value``)`
        `fail` `+=` `validateUsername``(``form``.``username``.``value``)`
        `fail` `+=` `validatePassword``(``form``.``password``.``value``)`
        `fail` `+=` `validateAge``(``form``.``age``.``value``)`
        `fail` `+=` `validateEmail``(``form``.``email``.``value``)`

        `if`   `(``fail` `==` `""``)`   `return` `true`
        `else` `{` `alert``(``fail``)``;` `return` `false` `}`
      `}`
    `</script>`
  </head>
  <body>
    <table class="signup" border="0" cellpadding="2"
              cellspacing="5" bgcolor="#eeeeee">
      <th colspan="2" align="center">Signup Form</th>
      <form method="post" action="adduser.php" onsubmit="return validate(this)">
        <tr><td>Forename</td>
          <td><input type="text" maxlength="32" name="forename"></td></tr>
        <tr><td>Surname</td>
          <td><input type="text" maxlength="32" name="surname"></td></tr>
        <tr><td>Username</td>
          <td><input type="text" maxlength="16" name="username"></td></tr>
        <tr><td>Password</td>
          <td><input type="text" maxlength="12" name="password"></td></tr>
        <tr><td>Age</td>
          <td><input type="text" maxlength="3"  name="age"></td></tr>
        <tr><td>Email</td>
          <td><input type="text" maxlength="64" name="email"></td></tr>
        <tr><td colspan="2" align="center"><input type="submit"
          value="Signup"></td></tr>
      </form>
    </table>
  </body>
</html>

目前这个表单虽然可以正确显示,但并没有进行自我验证,因为主要的验证函数尚未添加。即便如此,请将其保存为 validate.html,当你在浏览器中调用它时,它将呈现为 图 17-1。

来自示例 16-1 的输出

图 17-1. 来自 示例 17-1 的输出

让我们看看这个文档是如何组成的。开头几行设置了文档并使用了一些 CSS 使表单看起来不那么简单。接下来是与 JavaScript 相关的部分,用粗体显示。

<script></script> 标签之间有一个名为 validate 的函数,它调用其他六个函数来验证表单的每个输入字段。稍后我们会详细介绍这些函数。现在我只想解释一下,它们会返回空字符串(如果字段验证通过)或错误消息(如果验证失败)。如果存在任何错误,脚本的最后一行会弹出一个警告框显示这些错误。

在通过验证后,validate 函数返回 true;否则返回 falsevalidate 的返回值很重要,因为如果它返回 false,则会阻止表单的提交。这允许用户关闭警告弹出窗口并进行更改。如果返回 true,则表单字段中未遇到任何错误,因此表单会被提交。

这个示例的第二部分展示了表单的 HTML 结构,每个字段及其名称都放在表格的单独行中。这是相当直接的 HTML,唯一的例外是在开头的 <form> 标签中的 onSubmit="return validate(this)" 语句。使用 onSubmit,你可以在提交表单时调用自定义的函数。该函数可以进行一些检查,并返回 truefalse 的值,以表示是否允许提交表单。

this 参数是当前对象(即此表单),并且正如前面讨论的,将其传递给 validate 函数。validate 函数接收此参数作为对象 form

如你所见,表单的 HTML 中仅包含嵌入在 onSubmit 属性中的 return 调用的 JavaScript。禁用或不可用 JavaScript 的浏览器将简单地忽略 onSubmit 属性,并且 HTML 将正常显示。

validate.html 文档(第二部分)

现在我们来到示例 17-2,这是六个执行实际表单字段验证的函数。我建议你键入所有这些内容的第二部分,并将其保存在示例 17-1 的 <script>...</script> 部分中,你应该已经将其保存为 validate.html

示例 17-2. 具有 JavaScript 验证的表单(第二部分)
`function` `validateForename``(``field``)`
{
  return (field == "") ? "No Forename was entered.\n" : ""
}

`function` `validateSurname``(``field``)`
{
  return (field == "") ? "No Surname was entered.\n" : ""
}

`function` `validateUsername``(``field``)`
{
  if (field == "") return "No Username was entered.\n"
  else if (field.length < 5)
    return "Usernames must be at least 5 characters.\n"
  else if (/[^a-zA-Z0-9_-]/.test(field))
    return "Only a-z, A-Z, 0-9, - and _ allowed in Usernames.\n"
  return ""
}

`function` `validatePassword``(``field``)`
{
  if (field == "") return "No Password was entered.\n"
  else if (field.length < 6)
    return "Passwords must be at least 6 characters.\n"
  else if (!/[a-z]/.test(field) || ! /[A-Z]/.test(field) ||
           !/[0-9]/.test(field))
    return "Passwords require one each of a-z, A-Z and 0-9.\n"
  return ""
}

`function` `validateAge``(``field``)`
{
  if (field == "" || isNaN(field)) return "No Age was entered.\n"
  else if (field < 18 || field > 110)
    return "Age must be between 18 and 110.\n"
  return ""
}

`function` `validateEmail``(``field``)`
{
  if (field == "") return "No Email was entered.\n"
    else if (!((field.indexOf(".") > 0) &&
               (field.indexOf("@") > 0)) ||
              /[^a-zA-Z0-9.@_-]/.test(field))
      return "The Email address is invalid.\n"
  return ""
}

我们将依次讨论每个这些函数,从 validateForename 开始,这样你就能看到验证的工作原理。

验证名字

validateForename 是一个非常简短的函数,接受参数 field,该参数是由 validate 函数传递给它的名字的值。

如果此值为空字符串,则返回错误消息;否则,返回空字符串以表示未遇到错误。

如果用户在此字段中输入空格,则 validateForename 将接受它,尽管实际上它对所有目的而言是空的。你可以通过添加额外的语句来修复此问题,从字段中修剪空白字符然后检查它是否为空,或者使用正则表达式来确保字段中除空白字符外还有其他内容,或者——就像我在这里做的一样——让用户犯错,并允许 PHP 程序在服务器上捕获它。

验证姓氏

validateSurname函数与validateForename几乎相同,仅当提供的姓氏是空字符串时才返回错误。我选择不限制名称字段中允许的字符,以便包括非英语和带重音字符的可能性。

用户名验证

validateUsername函数稍微有趣,因为它的工作更复杂。它必须仅允许字符a-zA-Z0-9_-通过,并确保用户名至少为五个字符长。

if...else语句开始时,如果field未填写,则返回错误。如果不是空字符串,但长度少于五个字符,则返回另一条错误消息。

然后调用 JavaScript 的test函数,传递一个正则表达式(匹配任何不是允许字符之一的字符),与field进行匹配(参见“正则表达式”)。如果遇到不是允许字符之一的字符,则test函数返回true,因此validateUser返回错误字符串。

密码验证

validatePassword函数中使用类似的技术。首先,函数检查field是否为空,如果是,则返回错误。接下来,如果密码长度少于六个字符,则返回错误消息。

我们对密码施加的要求之一是,它们必须至少包含一个小写字母、一个大写字母和一个数字字符,因此test函数被调用三次,分别对这三种情况进行检查。如果其中任何一次调用返回false,则表示未满足其中一项要求,因此返回错误消息。否则,返回空字符串表示密码符合要求。

年龄验证

validateAge如果field不是数字(通过调用isNaN函数确定)或输入的年龄低于 18 或高于 110,则返回错误消息。您的应用程序可能具有不同或无年龄要求。同样,验证成功后返回空字符串。

邮件验证

在最后一个最复杂的示例中,电子邮件地址使用validateEmail进行验证。检查是否有实际输入内容,如果没有,则返回错误消息,函数将两次调用 JavaScript 的indexOf函数。第一次检查确保字段的第一个字符后面有一个句点(.),第二次检查确保@符号在第一个字符后面出现。

如果这两个检查都满足,将调用test函数来查看字段中是否有任何不允许的字符出现。如果这些测试中有任何失败,将返回一个错误消息。电子邮件地址中允许的字符是大写字母、小写字母、数字以及_-、句点和@字符,如传递给test方法的正则表达式详细说明的那样。如果没有发现错误,则返回空字符串表示验证成功。在最后一行,关闭了脚本和文档。

图 17-2 显示了用户在没有填写任何字段的情况下单击注册按钮的结果。

JavaScript 表单验证实例

图 17-2. JavaScript 表单验证实例

使用单独的 JavaScript 文件

当然,因为它们在结构上是通用的,并且可能适用于你可能需要的许多类型的验证,这六个函数是将理想的候选项移出到一个单独的 JavaScript 文件中。你可以将文件命名为validate_functions.js,并在示例 17-1 的初始脚本部分之后直接包含它,使用以下语句:

<script src="validate_functions.js"></script>

正则表达式

让我们更仔细地查看我们一直在进行的模式匹配。我们已经通过正则表达式实现了它,这在 JavaScript 和 PHP 中都受支持。它们使得能够在一个表达式中构造最强大的模式匹配算法成为可能。

通过元字符匹配

每个正则表达式都必须用斜杠括起来。在这些斜杠内部,某些字符具有特殊含义;它们被称为元字符。例如,星号(*)的含义类似于你在使用 Shell 或 Windows 命令提示符时看到的(但不完全相同)。星号表示“你尝试匹配的文本可以有任意数量的前导字符——或者根本没有。”

例如,假设你正在寻找姓名Le Guin,并且知道有些人可能会带空格或者不带空格拼写。因为文本布局很奇怪(例如,有人可能插入额外的空格来右对齐行),你可能需要搜索类似这样的一行:

The   difficulty  of   classifying Le      Guin's    works

所以你需要匹配LeGuin,以及用任意数量的空格分隔的LeGuin。解决方案是在空格后面跟随一个星号:

/Le *Guin/

在那一行中除了姓名Le Guin之外还有很多内容,但没关系。只要正则表达式匹配行的某些部分,test函数就会返回一个true值。如果确保行中仅包含Le Guin很重要,稍后我会告诉你如何确保这一点。

假设你知道至少有一个空格。在这种情况下,你可以使用加号(+),因为它需要至少一个前导字符:

/Le +Guin/

模糊字符匹配

点号(.)特别有用,因为它可以匹配除换行符以外的任何内容。假设您正在寻找以<开头、以>结尾的 HTML 标签。这里展示了一个简单的方法:

/<.*>/

点号匹配任何字符,而*扩展它以匹配零个或多个字符,因此它表示:“匹配介于<>之间的任何内容,即使其中什么也没有。” 您将匹配<><em><br>等等。但如果您不想匹配空情况<>,您应该使用+而不是*,如下所示:

/<.+>/

加号扩展了点号以匹配一个或多个字符,即表示:“匹配介于<>之间的任何内容,只要它们之间至少有一个字符。” 您将匹配到<em></em><h1></h1>,以及带有属性的标签,如下所示:

<a href="www.mozilla.org">

不幸的是,加号会持续匹配直到行尾的最后一个>,因此您可能会得到如下结果:

<h1><b>Introduction</b></h1>

不仅仅是一个标签!稍后在本节中我将展示一个更好的解决方案。

注意

如果您在尖括号之间单独使用点号,而不是跟随一个+*,那么它将匹配单个字符;您将匹配到<b><i>,但不会匹配<em><textarea>

如果要匹配点号字符本身(.),必须在其前面放置反斜杠(\)进行转义,因为否则它是一个元字符,可以匹配任何内容。例如,假设您想匹配浮点数5.0,则正则表达式如下:

/5\.0/

反斜杠可以转义任何元字符,包括另一个反斜杠(如果您试图在文本中匹配反斜杠)。不过,有点让人困惑的是,稍后您将看到反斜杠有时会赋予其后的字符特殊含义。

我们刚刚匹配了一个浮点数。但也许您想匹配5.以及5.0,因为它们在浮点数表示上是相同的。您还想匹配5.005.000等,允许任意数量的零。您可以通过添加一个星号来实现,正如您所见:

/5\.0*/

通过括号进行分组

假设您想匹配单位增量的幂,例如 kilo、mega、giga 和 tera。换句话说,您希望以下所有内容都匹配:

1,000
1,000,000
1,000,000,000
1,000,000,000,000
...

加号在这里也适用,但您需要将字符串,000进行分组,以便加号匹配整个内容。正则表达式如下:

/1(,000)+ /

括号意味着“在应用加号等操作时将其视为一个组”。1,00,0001,000,00不会匹配,因为文本必须包含一个 1,后跟一个或多个逗号,后跟三个零的完整组。

加号后面的空格表示匹配在遇到空格时必须结束。如果没有它,1,000,00会因为只考虑到第一个1,000而错误匹配,而剩下的,00会被忽略。要求后面有空格确保匹配将持续到数字的末尾。

字符类

有时你想匹配模糊的内容,但又不想使用点号匹配得太广泛。正则表达式的伟大之处在于它们允许你尽可能精确或模糊地匹配。

支持模糊匹配的关键特性之一是方括号对,[]。它匹配单个字符,像点号一样,但是在方括号内你放的是一个可以匹配的列表。如果这些字符中的任何一个出现,文本就匹配。例如,如果你想匹配美式拼写gray和英式拼写grey,你可以指定如下内容:

/gr[ae]y/

在你匹配的文本中的gr后面,可以是ae。但必须只有一个:方括号里放的东西刚好匹配一个字符。方括号里的字符组称为字符类

指示范围

在方括号内,你可以使用连字符(-)来表示一个范围。一个非常常见的任务是匹配单个数字,你可以用范围来做到如下所示:

/[0-9]/

数字在正则表达式中是如此常见,以至于提供了一个单字符来代表它们:\d。你可以用它代替方括号中的正则表达式来匹配一个数字:

/\d/

否定

方括号的另一个重要特性是对字符类的否定。你可以通过在开放的方括号后面放置一个插入符号(^)来颠倒整个字符类。这里的意思是“匹配任何字符除了以下内容”。所以,假设你想找到缺少以下感叹号的Yahoo实例。(公司的官方名称中正式包含一个感叹号!)你可以像这样做:

/Yahoo[^!]/

字符类别由一个单字符组成——一个感叹号,但它被前面的^反转。这实际上并不是解决问题的好方法——例如,如果Yahoo在行尾,它就不再后面跟着任何东西,而方括号必须匹配一个字符。一个更好的解决方案涉及到负向前瞻(匹配后面没有其他东西的内容),但这超出了本书的范围,请参考在线文档

一些更复杂的例子

了解字符类和否定后,你现在准备好看到更好的解决方案来匹配 HTML 标签的问题了。这个解决方案避免了越过单个标签的末尾,但仍然匹配像<em></em>这样的标签,以及带属性的标签,比如这样的:

<a href="www.mozilla.org">

这是一个解决方案:

/<[^>]+>/

那个正则表达式看起来好像我把茶杯掉在键盘上,但它是完全有效和非常有用的。让我们分解它。图 17-3 展示了各个元素,我会逐一描述它们。

典型正则表达式的分解

图 17-3. 典型正则表达式的分解

元素如下:

/

指示这是正则表达式的开头斜杠。

<

HTML 标签的开始括号。这是精确匹配;不是元字符。

[^>]

字符类。嵌入的^>表示“匹配除了闭合尖括号之外的任何内容”。

+

允许任意数量的字符匹配前一个[^>],但至少要有一个。

>

HTML 标签的闭合括号。这是精确匹配。

/

表示正则表达式的结束斜杠。

注意

另一个解决匹配 HTML 标签问题的方法是使用非贪婪操作。默认情况下,模式匹配是贪婪的,返回可能的最长匹配。非贪婪(或惰性)匹配找到可能的最短匹配。这本书不涉及其使用,但可以在JavaScript.info website上找到更多细节。

我们现在将查看来自 Example 17-1 中的表达式之一,其中使用了validateUsername函数:

/[^a-zA-Z0-9_-]/

Figure 17-4 显示了各种元素。

图 17-4. validateUsername正则表达式的详细分解

让我们详细看看这些元素:

/

指示这是正则表达式的开头斜杠。

[

开始字符类的开放方括号。

^

反向字符:反转方括号内的所有其他内容。

a-z

表示任何小写字母。

A-Z

表示任何大写字母。

0-9

表示任何数字。

_

一个下划线。

-

一个破折号。

]

结束字符类的闭合括号。

/

表示正则表达式的结束斜杠。

还有两个重要的元字符。它们通过要求出现在特定位置来“锚定”正则表达式。如果^出现在正则表达式的开头,则表达式必须出现在文本行的开头;否则,它不匹配。同样地,如果$符号出现在正则表达式的末尾,则表达式必须出现在文本行的末尾。

注意

可能会有些混淆,^在方括号内可以表示“否定字符类”,而在正则表达式的开头可以表示“匹配行的开头”。不幸的是,同一个字符用于两种不同的含义,因此在使用时要小心。

我们将通过回答前面提出的问题来完成对正则表达式基础的探索:假设你想确保一行除了正则表达式之外没有任何其他内容,该怎么办?如果你想要一个包含“Le Guin”而没有其他内容的行?我们可以通过修改先前的正则表达式来实现这一点,将两端锚定:

/^Le *Guin$/

元字符总结

表 17-1 展示了正则表达式中可用的元字符。

表 17-1. 正则表达式元字符

元字符描述
/开始和结束正则表达式
.匹配除换行符外的任何单个字符
*`元素`**匹配零次或多次的*元素*
*`元素`*+匹配至少一次的*元素*
*`元素`*?匹配零次或一次的*元素*
[*`字符`*]匹配括号内的任意字符
[^*`字符`*]匹配不在括号内的单个字符
(*`正则表达式`*)将*正则表达式*视为计数或后续 *+? 的组
*`左`*&#124;*`右`*匹配**
[*`l`*-*`r`*]匹配介于*lr*之间的字符范围
^要求匹配字符串的开头
$要求匹配字符串的结尾
\b匹配单词边界
\B匹配非单词边界
\d匹配单个数字
\D匹配单个非数字字符
\n匹配换行符
\s匹配空白字符
\S匹配非空白字符
\t匹配制表符
\w匹配单词字符(a-zA-Z0-9_
\W匹配非单词字符(除了 a-zA-Z0-9_ 以外的任何字符)
\*`x`*匹配*x(如果x是元字符,但你真正想要的是x*)
{*`n`*}精确匹配*n*次
{*`n`*,}匹配至少*n*次或更多
{*`最小`*,*`最大`*}匹配至少*最小次,最多最大*次

通过这张表格的提供,并重新审视表达式 /[^a-zA-Z0-9_]/,你可以看到它很容易被简化为 /[^\w]/,因为小写 w 的单个元字符 \w 指定了字符 a-zA-Z0-9_

实际上,我们可以比这更聪明,因为元字符 \W(大写 W)指定除了 a-zA-Z0-9_ 之外的所有字符。因此,我们也可以省略 ^ 元字符,直接使用 /[\W]/ 或者甚至进一步简化为 /\W/,因为它只匹配单个字符。

为了让你更清楚地了解这一切是如何工作的,表 17-2 展示了一系列表达式及其匹配的模式。

表 17-2. 一些示例正则表达式

示例匹配结果
rThe quick brown 中的第一个 r
rec[ei][ei]vereceiverecieve(同时也包括 receevereciive
rec[ei]{2}vereceiverecieve(同时也包括 receevereciive
rec(ei&#124;ie)vereceiverecieve(但不是 receevereciive
catI like cats and dogs 中的单词 cat
cat&#124;dogI like cats and dogs 中的单词 cat(匹配先遇到的 catdog
\..(因为.是一个元字符,所以\是必需的)
5\.0*5.5.05.005.000
[a-f]任意字符 a, b, c, d, e, 或 f 中的一个
cats$My cats are friendly cats 中的最后一个 cats
^my只匹配 my cats are my pets 中的第一个 my
\d{2,3}任意两到三位数字(00999
7(,000)+7,0007,000,0007,000,000,0007,000,000,000,000
[\w]+任意一个或多个字符的单词
[\w]{5}任意五个字母的单词

通用修饰符

一些额外的修饰符可用于正则表达式:

  • /g 使得匹配为全局匹配。在使用替换函数时,要指定此修饰符以替换所有匹配项,而不仅仅是第一个。

  • /i 使得正则表达式匹配时忽略大小写。因此,可以用 /[a-z]/i/[A-Z]/i 替代 /[a-zA-Z]/

  • /m 使得多行模式启用,在这种模式下,插入符 (^) 和美元符 ($) 将在主题字符串中的任何新行之前和之后匹配。通常情况下,多行字符串中,^ 仅匹配字符串的开始,$ 仅匹配字符串的结尾。

例如,表达式 /cats/g 将匹配句子 “I like cats, and cats like me.” 中的两个 cats。同样,表达式 /dogs/gi 将匹配句子 “Dogs like other dogs.” 中的两个 dogsDogsdogs),因为你可以结合使用这些修饰符。

在 JavaScript 中使用正则表达式

在 JavaScript 中,你将主要使用两种方法来处理正则表达式:test(你已经见过)和 replacetest 方法仅告诉你其参数是否与正则表达式匹配,而 replace 则需要第二个参数:替换匹配文本的字符串。与大多数函数一样,replace 返回一个新字符串作为其返回值;它不会改变输入。

为了比较这两种方法,以下语句返回 true,告诉我们 cats 这个词在字符串中至少出现一次。

document.write(/cats/i.test("Cats are funny. I like cats."))

但以下语句将两次出现的 cats 替换为 dogs,并打印结果。搜索必须是全局的 (/g) 才能找到所有匹配,且大小写不敏感 (/i) 才能找到大写 Cats

document.write("Cats are friendly. I like cats.".replace(/cats/gi,"dogs"))

如果您尝试这个语句,您会看到 replace 的一个限制:因为它精确地使用您告诉它使用的字符串替换文本,所以第一个单词 Cats 被替换为 dogs 而不是 Dogs

在 PHP 中使用正则表达式

在 PHP 中,您可能会经常使用的正则表达式函数是 preg_matchpreg_match_allpreg_replace

要测试字符串中是否出现单词 cats,不论大小写,您可以像这样使用 preg_match

$n = preg_match("/cats/i", "Cats are crazy. I like cats.");

由于 PHP 使用 1 表示 TRUE,使用 0 表示 FALSE,所以上述语句将 $n 设置为 1。第一个参数是正则表达式,第二个参数是要匹配的文本。但是 preg_match 实际上更加强大和复杂,因为它接受第三个参数,显示匹配的文本是什么:

$n = preg_match("/cats/i", "Cats are curious. I like cats.", $match);
echo "$n Matches: $match[0]";

第三个参数是一个数组(在这里,命名为 $match)。函数将匹配的文本放入第一个元素中,因此如果匹配成功,您可以在 $match[0] 中找到匹配的文本。在这个例子中,输出告诉我们匹配的文本是大写的:

1 Matches: Cats

如果您希望查找所有匹配项,可以使用 preg_match_all 函数,如下所示:

$n = preg_match_all("/cats/i", "Cats are strange. I like cats.", $match);
echo "$n Matches: ";
for ($j=0 ; $j < $n ; ++$j) echo $match[0][$j]." ";

与之前一样,将 $match 传递给函数,并将元素 $match[0] 分配为所做的匹配,但这次作为子数组。为了显示子数组,这个例子使用 for 循环进行迭代。

当您想要替换字符串的一部分时,可以使用 preg_replace,如此例所示。这个例子替换了所有出现的单词 cats,不区分大小写地替换为 dogs

echo preg_replace("/cats/i", "dogs", "Cats are furry. I like cats.");
注意

正则表达式的主题是一个庞大的主题,已经有整本书写关于它。如果您需要进一步的信息,我建议查看 维基百科条目Regular-Expressions.info

重新显示 PHP 验证后的表单

好的,回到表单验证。到目前为止,我们已经创建了 HTML 文档 validate.html,它将通过到 PHP 程序 adduser.php,但只有当 JavaScript 验证字段或 JavaScript 禁用或不可用时。

现在是时候创建 adduser.php,接收提交的表单,执行自己的验证,如果验证失败则再次向访客显示表单。示例 17-3 包含了您应该输入和保存的代码(或从配套网站下载)。

示例 17-3. adduser.php 程序
<?php // adduser.php 
  `// The PHP code`

  `$forename` `=` `$surname` `=` `$username` `=` `$password` `=` `$age` `=` `$email` `=` `"``"``;`

  `if` `(``isset``(``$_POST``[``'forename'``]))`
    `$forename` `=` `fix_string``(``$_POST``[``'forename'``]);`
  `if` `(``isset``(``$_POST``[``'surname'``]))`
    `$surname`  `=` `fix_string``(``$_POST``[``'surname'``]);`
  `if` `(``isset``(``$_POST``[``'username'``]))`
    `$username` `=` `fix_string``(``$_POST``[``'username'``]);`
  `if` `(``isset``(``$_POST``[``'password'``]))`
    `$password` `=` `fix_string``(``$_POST``[``'password'``]);`
  `if` `(``isset``(``$_POST``[``'age'``]))`
    `$age`      `=` `fix_string``(``$_POST``[``'age'``]);`
  `if` `(``isset``(``$_POST``[``'email'``]))`
    `$email`    `=` `fix_string``(``$_POST``[``'email'``]);`

  `$fail`  `=` `validate_forename``(``$forename``);`
  `$fail` `.=` `validate_surname``(``$surname``);`
  `$fail` `.=` `validate_username``(``$username``);`
  `$fail` `.=` `validate_password``(``$password``);`
  `$fail` `.=` `validate_age``(``$age``);`
  `$fail` `.=` `validate_email``(``$email``);`

  `echo` `"``<!DOCTYPE html>``\n``<html><head><title>An Example Form</title>``"``;`

  `if` `(``$fail` `==` `"``"``)`
  `{`
    `echo` `"``</head><body>Form data successfully validated:`
      `$forename``,` `$surname``,` `$username``,` `$password``,` `$age``,` `$email``.</body></html>``"``;`

    `// This is where you would enter the posted fields into a database,`
    `// preferably using hash encryption for the password.`

    `exit``;`
  `}`

  `echo` `<<<_END` <!-- The HTML/JavaScript section -->

    <style>
      .signup {
        border: 1px solid #999999;
      font:   normal 14px helvetica; color:#444444;
      }
    </style>

    <script>
      function validate(form)
      {
        fail  = validateForename(form.forename.value)
        fail += validateSurname(form.surname.value)
        fail += validateUsername(form.username.value)
        fail += validatePassword(form.password.value)
        fail += validateAge(form.age.value)
        fail += validateEmail(form.email.value)

        if (fail == "")     return true
        else { alert(fail); return false }
      }

      function validateForename(field)
      {
        return (field == "") ? "No Forename was entered.\\n" : ""
      }

      function validateSurname(field)
      {
        return (field == "") ? "No Surname was entered.\\n" : ""
      }

      function validateUsername(field)
      {
        if (field == "") return "No Username was entered.\\n"
        else if (field.length < 5)
          return "Usernames must be at least 5 characters.\\n"
        else if (/[^a-zA-Z0-9_-]/.test(field))
          return "Only a-z, A-Z, 0-9, - and _ allowed in Usernames.\\n"
        return ""
      }

      function validatePassword(field)
      {
        if (field == "") return "No Password was entered.\\n"
        else if (field.length < 6)
          return "Passwords must be at least 6 characters.\\n"
        else if (!/[a-z]/.test(field) || ! /[A-Z]/.test(field) ||
                 !/[0-9]/.test(field))
          return "Passwords require one each of a-z, A-Z and 0-9.\\n"
        return ""
      }

      function validateAge(field)
      {
        if (isNaN(field)) return "No Age was entered.\\n"
        else if (field < 18 || field > 110)
          return "Age must be between 18 and 110.\\n"
        return ""
      }

      function validateEmail(field)
      {
        if (field == "") return "No Email was entered.\\n"
          else if (!((field.indexOf(".") > 0) &&
                     (field.indexOf("@") > 0)) ||
                    /[^a-zA-Z0-9.@_-]/.test(field))
            return "The Email address is invalid.\\n"
        return ""
      }
    </script>
  </head>
  <body>

    <table border="0" cellpadding="2" cellspacing="5" bgcolor="#eeeeee">
      <th colspan="2" align="center">Signup Form</th>

        <tr><td colspan="2">Sorry, the following errors were found<br>
          in your form: <p><font color=red size=1><i>$fail</i></font></p>
        </td></tr>

      <form method="post" action="adduser.php" onSubmit="return validate(this)">
        <tr><td>Forename</td>
          <td><input type="text" maxlength="32" name="forename" value="$forename">
        </td></tr><tr><td>Surname</td>
          <td><input type="text" maxlength="32" name="surname"  value="$surname">
        </td></tr><tr><td>Username</td>
          <td><input type="text" maxlength="16" name="username" value="$username">
        </td></tr><tr><td>Password</td>
          <td><input type="text" maxlength="12" name="password" value="$password">
        </td></tr><tr><td>Age</td>
          <td><input type="text" maxlength="3"  name="age"      value="$age">
        </td></tr><tr><td>Email</td>
          <td><input type="text" maxlength="64" name="email"    value="$email">
        </td></tr><tr><td colspan="2" align="center"><input type="submit"
          value="Signup"></td></tr>
      </form>
    </table>
  </body>
</html>

_END; 
  `// The PHP functions`

  `function` `validate_forename``(``$field``)`
  `{`
    `return` `(``$field` `==` `"``"``)` `?` `"``No Forename was entered<br>``"``:` `"``"``;`
  `}`

  `function` `validate_surname``(``$field``)`
  `{`
    `return``(``$field` `==` `"``"``)` `?` `"``No Surname was entered<br>``"` `:` `"``"``;`
  `}`

  `function` `validate_username``(``$field``)`
  `{`
    `if` `(``$field` `==` `"``"``)` `return` `"``No Username was entered<br>``"``;`
    `else` `if` `(``strlen``(``$field``)` `<` `5``)`
      `return` `"``Usernames must be at least 5 characters<br>``"``;`
    `else` `if` `(``preg_match``(``"``/[^a-zA-Z0-9_-]/``"``,` `$field``))`
      `return` `"``Only letters, numbers, - and _ in usernames<br>``"``;`
    `return` `"``"``;`
  `}`

  `function` `validate_password``(``$field``)`
  `{`
    `if` `(``$field` `==` `"``"``)` `return` `"``No Password was entered<br>``"``;`
    `else` `if` `(``strlen``(``$field``)` `<` `6``)`
      `return` `"``Passwords must be at least 6 characters<br>``"``;`
    `else` `if` `(``!``preg_match``(``"``/[a-z]/``"``,` `$field``)` `||`
             `!``preg_match``(``"``/[A-Z]/``"``,` `$field``)` `||`
             `!``preg_match``(``"``/[0-9]/``"``,` `$field``))`
      `return` `"``Passwords require 1 each of a-z, A-Z and 0-9<br>``"``;`
    `return` `"``"``;`
  `}`

  `function` `validate_age``(``$field``)`
  `{`
    `if` `(``$field` `==` `"``"``)` `return` `"``No Age was entered<br>``"``;`
    `else` `if` `(``$field` `<` `18` `||` `$field` `>` `110``)`
      `return` `"``Age must be between 18 and 110<br>``"``;`
    `return` `"``"``;`
  `}`

  `function` `validate_email``(``$field``)`
  `{`
    `if` `(``$field` `==` `"``"``)` `return` `"``No Email was entered<br>``"``;`
      `else` `if` `(``!``((``strpos``(``$field``,` `"``.``"``)` `>` `0``)` `&&`
                 `(``strpos``(``$field``,` `"``@``"``)` `>` `0``))` `||`
                  `preg_match``(``"``/[^a-zA-Z0-9.@_-]/``"``,` `$field``))`
        `return` `"``The Email address is invalid<br>``"``;`
    `return` `"``"``;`
  `}`

  `function` `fix_string``(``$string``)`
  `{`
    `if` `(``get_magic_quotes_gpc``())` `$string` `=` `stripslashes``(``$string``);`
    `return` `htmlentities` `(``$string``);`
  `}`
?>
注意

在这个例子中,所有输入在使用前都经过了净化处理,甚至包括密码,因为密码可能包含用于格式化 HTML 的字符,将被转换为 HTML 实体。例如,& 将变成 &amp;< 将变成 &lt;,依此类推。如果将使用哈希函数存储加密密码,只要稍后检查输入的密码时以相同方式进行净化处理,这将不成问题,因此将可以比较相同的输入。

在禁用 JavaScript 并且两个字段填写不正确的情况下提交表单的结果显示在 图 17-5 中。

PHP 验证失败后表单的呈现方式

图 17-5. PHP 验证失败后表单的呈现方式

我已经将这段代码的 PHP 部分(以及对 HTML 部分的更改)用粗体标记,以便你更清楚地看到这与示例 17-1 和 17-2 之间的区别。

如果你浏览过这个示例,或者从 书中的示例存储库 中下载或者键入它,你会看到 PHP 代码几乎是 JavaScript 代码的克隆;相似的函数用于验证每个字段的正则表达式。

但有几点需要注意。首先,fix_string 函数(就在最后)用于清理每个字段,防止任何代码注入尝试成功。

此外,你会看到 HTML 来自于 示例 17-1,在 PHP 代码中通过 <<<_END..._END; 结构重复显示表单,展示上次访问者输入的值。你只需简单地为每个 <input> 标签添加额外的 value 参数(例如 value="$forename")即可做到这一点。强烈建议这样做,这样用户只需编辑先前输入的值,而无需再次在字段中输入这些值。

注意

在现实世界中,你可能不会从 示例 17-1 这样的 HTML 表单开始。相反,你更可能直接编写 示例 17-3 中包含所有 HTML 的 PHP 程序。当然,对于第一次调用程序时需要进行一些微小的调整,以防止在所有字段为空时显示错误。此外,你可能还会将六个 JavaScript 函数放入它们自己的 .js 文件中进行单独包含,正如 “使用单独的 JavaScript 文件” 中所述。

现在你已经看到如何将 PHP、HTML 和 JavaScript 结合在一起,下一章将介绍Ajax(异步 JavaScript 和 XML),它利用 JavaScript 在后台向服务器发出调用,无缝更新网页的部分内容,而无需重新提交整个页面到服务器。

问题

  1. 在提交之前,你可以使用什么 JavaScript 方法来发送表单进行验证?

  2. 用于将字符串与正则表达式匹配的 JavaScript 方法是什么?

  3. 编写一个正则表达式来匹配任何不符合正则表达式语法定义的单词字符。

  4. 编写一个正则表达式来匹配单词foxfix中的任意一个。

  5. 编写一个正则表达式来匹配任意单词后面紧跟任何非单词字符。

  6. 使用正则表达式,编写一个 JavaScript 函数来测试字符串The quick brown fox中是否存在单词fox

  7. 使用正则表达式,编写一个 PHP 函数,在字符串The cow jumps over the moon中将所有出现的单词the替换为my

  8. 哪个 HTML 属性用于在表单字段中预先填充值?

参见《“第十七章答案”》(app01_split_016.xhtml#chapter_17_answers)附录 A 中的解答。

第十八章:使用异步通信

Ajax 这个术语首次在 2005 年被创造出来。它代表 Asynchronous JavaScript and XML,简而言之,就是使用内置在 JavaScript 中的一组方法在浏览器和服务器之间后台传输数据。这个术语现在大多已被放弃,转而简单地讨论异步通信。

这项技术的一个绝佳例子是谷歌地图(见 图 18-1),在需要时从服务器下载地图的新部分,而无需刷新页面。

使用异步通信不仅大幅减少了需要来回发送的数据量,而且使网页能够无缝动态地运行,使其更像是独立的应用程序。结果是大大改进的用户界面和更好的响应能力。

谷歌地图是 Ajax 的一个绝佳例子

图 18-1. 谷歌地图是异步通信的一个优秀例子

什么是异步通信?

当今使用的异步通信始于 1999 年发布的 Internet Explorer 5,引入了一个新的 ActiveX 对象,XMLHttpRequest。ActiveX 是微软的技术,用于签名插件,安装额外的软件到您的计算机上。其他浏览器开发者随后效仿,但是他们都将这个功能作为 JavaScript 解释器的本地部分实现,而不是使用 ActiveX。

然而,早在此之前,一种早期形式的技术已经出现,它在页面上使用隐藏框架与服务器在后台交互。聊天室是早期的采用者,使用它来轮询并显示新的消息帖子,而不需要页面重新加载。

所以,让我们看看如何通过 JavaScript 实现异步通信。

使用 XMLHttpRequest

在过去,由于各种浏览器之间的不同实现,特别是在不同版本的 Microsoft Internet Explorer 之间,制作 Ajax 调用是一件真正的痛苦。幸运的是,如今情况大为改善,只需简单的 XMLHttpRequest 对象即可统一处理。

例如,要进行 GET 请求,您可以使用以下代码:

let XHR = new XMLHttpRequest()

XHR.open("`GET`""resource.info"true)
XHR.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
XHR.send()

或者,对于 POST 请求,只需将 GET 替换为 POST;就是这么简单。

您的第一个异步程序

输入并保存代码到 示例 18-1 中,文件名为 urlpost.html,但不要立即加载到浏览器中。

示例 18-1. urlpost.html
<!DOCTYPE html>
<html> <!-- urlpost.html -->
  <head>
    <title>Asynchronous Communication Example</title>
  </head>
  <body style='text-align:center'>
    <h1>Loading a web page into a DIV</h1>
    <div id='info'>This sentence will be replaced</div>

    <script>
      let XHR = new XMLHttpRequest()

      XHR.open("POST", "http://127.0.0.1/18/urlpost.php", true)
      XHR.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
      XHR.send("url=news.com")

      XHR.onreadystatechange = function()
      {
        if (this.readyState == 4 && this.status == 200)
        {
          document.getElementById("info").innerHTML = this.responseText
        }
      }      
    </script>
  </body>
</html>

让我们逐步浏览这份文档,看看它做了什么,从第一行开始,简单地设置了一个 HTML 文档并显示了一个标题。接下来的一行创建了一个带有 ID info<div>,其中包含文本 This sentence will be replaced。稍后,调用返回的文本将被插入到这里。

接下来,创建一个名为 XHR 的新 XMLHttpRequest 对象。使用它,通过调用 XHR.open 打开要加载的资源。在本例中,为了避免现代浏览器中的跨域 Ajax 问题,选择了 http://127.0.0.1 的本地主机 IP 地址,然后是章节文件夹 18,接着是 PHP 程序 urlpost.php,我们马上就会介绍到。

注意

如果您使用 AMPPS(或类似的 WAMP、LAMP 或 MAMP)设置开发服务器在第二章中下载示例文件从GitHub并将它们保存在 web 服务器的文档根目录中(如该章节中所述),则第十八章文件夹将在正确的位置使此代码能够正常工作。如果您的设置有任何不同部分,或者在使用您选择的域名的开发服务器上运行此代码,则必须相应地更改此代码中的这些值。

指定要加载的资源后,调用 XHR.setRequestHeader,传递要发送到资源服务器的必需头部,并在调用 XHR.send 中发送要发布的值。在这种情况下,是主页面在 news.com

readyState 属性

现在我们进入异步调用的核心,所有这些都依赖于 readyState 属性。这使得浏览器可以继续接受用户输入和更改屏幕,而我们的程序设置 onreadystatechange 属性,每次 readyState 改变时调用我们选择的函数。在本例中,使用了一个无名(或匿名)内联函数,而不是一个单独命名的函数。这种类型的函数被称为回调函数,因为每次 readyState 改变时都会调用它。

使用内联匿名函数设置回调函数的语法如下:

XHR.onreadystatechange = function()
{
  if (this.readyState == 4 && this.status == 200)
  {
    // do something
  }
}

如果您希望使用单独命名的函数,语法略有不同:

XHR.onreadystatechange = asyncCallback

function asyncCallback()
{
  if (this.readyState == 4 && this.status == 200)
  {
    // do something
  }
}

事实上,readyState 可以有五个值,但我们只关心一个,即值为 4,表示调用已完成。因此,每次调用新函数时,它会在 readyState 的值为 4 之前不做任何操作。当我们的函数检测到该值时,接下来会检查调用的 status,确保其值为 200,这表示调用成功。

注意

您会注意到,所有这些对象属性都是使用 this.readyStatethis.status 等引用的,而不是对象的当前名称 XHR,如 XHR.readyStateXHR.status。这是为了您可以轻松复制粘贴代码,并且它能在任何对象名称上工作,因为 this 关键字始终引用当前对象。

因此,确认 readyState4status200 后,我们将 responseText 中的值放入 <div> 的内部 HTML 中,该 <div>idinfo

document.getElementById("info").innerHTML = this.responseText

在这一行中,通过getElementById方法引用了info元素,然后将其innerHTML属性赋值为调用返回的值。效果是网页的这个元素发生了变化,而其他一切保持不变。

异步过程的服务器端

现在我们来看看等式的 PHP 部分,你可以在例子 18-2 中看到它。输入这段代码并将其保存为urlpost.php

例子 18-2. urlpost.php
<?php // urlpost.php
  if (isset($_POST['url']))
  {
    echo file_get_contents('http://' . SanitizeString($_POST['url']));
  }

  function SanitizeString($var)
  {
    $var = strip_tags($var);
    $var = htmlentities($var);
    return stripslashes($var);
  }
?>

正如你所看到的,这很简短且简洁,并且还使用了非常重要的SanitizeString函数,应该对所有发布的数据进行这样的操作。在这种情况下,未经过清理的数据可能会导致用户插入 JavaScript 并对你的代码获得优势。

这个程序使用file_get_contents PHP 函数加载了通过变量$_POST['url']提供的 URL 的网页内容。file_get_contents函数非常灵活,可以从本地或远程服务器加载文件或网页的整个内容;它甚至考虑了移动页面和其他重定向。

当你输入完程序后,可以在浏览器中调用urlpost.html,几秒钟后,你应该会看到news.com首页的内容加载到我们为此目的创建的<div>中。

注意

跨域安全性使得使用 Ajax 比以前更加困难,因为你必须对加载文件的方式非常准确和清晰。例如,在本地开发服务器 localhost 上的这个例子中,你需要使用其 IP 地址来引用文件。因此,例如,如果你将示例文件保存在 AMPPS 服务器的文档根目录中,如第二章中所述,则所有文件将位于名为18的子文件夹中。

要测试程序,请在浏览器中输入以下内容:

http://127.0.0.1/18/urlpost.html

它不会像直接加载网页那样快,因为它转移两次——一次到服务器,再从服务器到你的浏览器——但结果应该看起来像图 18-2。

图 18-2. news.com 的首页

我们不仅成功地进行了异步调用,并在 JavaScript 中返回了响应,而且还利用了 PHP 的能力来合并一个完全不相关的网络对象。顺便说一句,如果我们试图找到一种不借助 PHP 服务器端模块直接异步获取这个网页的方法,我们是不会成功的,因为有其他安全阻止跨域异步通信。因此,这个例子还展示了一个实际问题的方便解决方案。

使用 GET 而不是 POST

就像当你从表单提交任何数据时,你可以选择以 GET 请求的形式提交你的数据,如果这样做,你将节省几行代码。然而,有一个缺点:一些浏览器可能会缓存 GET 请求,而 POST 请求永远不会被缓存。你不希望缓存请求,因为浏览器会直接重新显示上次获取的内容,而不是从服务器获取新的输入。解决此问题的方法是使用一个方法来添加一个随机参数到每个请求中,确保每个请求的 URL 都是唯一的。

示例 18-3 展示了如何通过 GET 请求实现与示例 18-1 相同的结果。

示例 18-3. urlget.html
<!DOCTYPE html>
<html> <!-- urlget.html -->
  <head>
    <title>Asynchronous Communication Example</title>
  </head>
  <body style='text-align:center'>
    <h1>Loading a web page into a DIV</h1>
    <div id='info'>This sentence will be replaced</div>

    <script>
      `let` `nocache` `=` `"&nocache="` `+` `Math``.``random``(``)` `*` `1000000`
      let XHR     = new XMLHttpRequest()

      `XHR``.``open``(``"GET"``,` `"http://127.0.0.1/18/urlget.php?url=news.com"` `+` `nocache``,` `true``)`
      `XHR``.``send``(``)`

      XHR.onreadystatechange = function()
      {
        if (this.readyState == 4 && this.status == 200)
        {
          document.getElementById("info").innerHTML = this.responseText
        }
      }      
    </script>
  </body>
</html>

需要注意两个文档之间的差异已用粗体突出,并描述如下:

  • 对于 GET 请求,不需要发送头部信息。

  • 我们使用 GET 请求调用open方法,提供一个字符串构成的 URL,该字符串由?符号后跟参数/值对url=news.com组成。

  • 我们使用&符号提供第二个参数/值对,然后将参数nocache的值设置为 0 到 100 万之间的随机值。这样做是为了确保每个请求的 URL 都是不同的,因此不会被缓存。

  • 调用send现在没有参数,因为没有通过POST请求传递任何内容需要它。

为了配合这个新文档,PHP 程序必须修改以响应 GET 请求,就像示例 18-4 中的urlget.php一样。

示例 18-4. urlget.php
<?php
  if (isset(`$_GET`['url']))
  {
    echo file_get_contents("http://".sanitizeString(`$_GET`['url']));
  }

  function sanitizeString($var)
  {
    $var = strip_tags($var);
    $var = htmlentities($var);
    return stripslashes($var);
  }
?>

这与示例 18-2 唯一的不同之处在于,将对$_POST的引用替换为$_GET。在浏览器中加载urlget.html的最终结果与加载urlpost.html完全相同。

要测试程序的这个修订版本,请在浏览器中输入以下内容,你应该看到与之前完全相同的结果,只是通过 GET 而不是 POST 请求加载:

http://127.0.0.1/18/urlget.html

发送 XML 请求

虽然我们创建的对象称为XMLHttpRequest对象,但我们迄今为止并未使用 XML。正如你所见,我们已经能够异步请求整个 HTML 文档,但我们同样可以请求文本页面、字符串或数字,甚至是电子表格数据。

因此,让我们修改前面的示例文档和 PHP 程序,以获取一些 XML 数据。为此,请先查看 PHP 程序xmlget.php,如示例 18-5 所示。

示例 18-5. xmlget.php
<?php
  if (isset($_GET['url']))
  {
    `header``(``'Content-Type: text/xml'``);`
    echo file_get_contents("http://".sanitizeString($_GET['url']));
  }

  function sanitizeString($var)
  {
    $var = strip_tags($var);
    $var = htmlentities($var);
    return stripslashes($var);
  }
?>

这个程序已经略微修改(以粗体突出显示),在返回获取的文档之前输出正确的 XML 头。这里没有进行任何检查,因为假定调用代码将请求一个实际的 XML 文档。

现在转到 HTML 文档xmlget.html,如示例 18-6 所示。

示例 18-6. xmlget.html
<!DOCTYPE html>
<html> <!-- xmlget.html -->
  <head>
    <title>Asynchronous Communication Example</title>
  </head>
  <body>
    <h1>Loading XML data into a DIV</h1>
    <div id='info'>This sentence will be replaced</div>

    <script>
      `let out     = ''` let nocache = "&nocache=" + Math.random() * 1000000 `let url     = "rss.news.yahoo.com/rss/topstories"` let XHR     = new XMLHttpRequest()

      XHR.open("POST", "http://127.0.0.1/18/xmlget.php?url=" + url + nocache, true)
      XHR.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
      XHR.send()

      XHR.onreadystatechange = function()
      {
        if (this.readyState == 4 && this.status == 200)
        {`let titles = this.responseXML.getElementsByTagName('title')             for (let j = 0 ; j` `< titles.length` `;` `+``+``j``)`
           `{`
             `o``u``t` `+``=` `t``i``t``l``e``s``[``j``]``.``c``h``i``l``d``N``o``d``e``s``[``0``]``.``n``o``d``e``V``a``l``u``e` `+` `'``<``b``r``>``'            }            document.getElementById('info').innerHTML = out `}
      } </script>
  </body>
</html>

再次,差异已经用粗体标出。正如你所见,这段代码与以前的版本基本相似,唯一的不同是现在请求的 URL,rss.news.yahoo.com/rss/topstories,包含一个 XML 文档,Yahoo! News Top Stories 订阅。

另一个重要变化是使用 responseXML 属性替代了 responseText 属性。每当服务器返回 XML 数据时,responseXML 将包含返回的 XML。

然而,responseXML并非简单地包含一个 XML 文本字符串:实际上它是一个完整的 XML 文档对象,我们可以使用 DOM 树的方法和属性来检查和解析它。这意味着它是可访问的,例如,可以通过 JavaScript 的 getElementsByTagName 方法访问它。

关于 XML

一个 XML 文档通常采用如 Example 18-7 中所示的 RSS 订阅的形式。然而,XML 的美妙之处在于我们可以将这种类型的结构存储在 DOM 树中(参见 Figure 18-3)中,以便快速搜索:

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
    <channel>
        <title>RSS Feed</title>
        <link>http://website.com</link>
        <description>website.com's RSS Feed</description>
        <pubDate>Mon, 10 May 2027 00:00:00 GMT</pubDate>
        <item>
            <title>Headline</title>
            <guid>http://website.com/headline</guid>
            <description>This is a headline</description>
        </item>
        <item>
            <title>Headline 2</title>
            <guid>http://website.com/headline2</guid>
            <description>The 2nd headline</description>
        </item>
    </channel>
</rss>

Example 17-8 的 DOM 树

图 18-3. Example 18-7 的 DOM 树

然后,使用 getElementsByTagName 方法,我们可以快速提取与各种标签相关联的值,而无需进行大量的字符串搜索。这正是我们在 Example 18-6 中所做的,其中发出了以下命令:

let titles = this.responseXML.getElementsByTagName('title')

这个单一命令的效果是将所有 <title> 元素的值放入数组 titles 中。然后,可以通过以下表达式轻松提取它们(其中 j 已经被赋值为表示要访问的标题的整数):

titles[j].childNodes[0].nodeValue

所有标题然后都附加到字符串变量 out 中,一旦它们全部被处理,结果将被插入到文档开头的空 <div> 中。

总结一下,每个实体如 title 都是一个节点,因此例如标题文本被视为标题内的一个节点。但即使获取了子节点,你还必须要求它作为文本,这就是 .nodeValue 的目的。此外,与所有表单数据一样,请记住,在请求 XML 数据时可以使用 POST 或 GET 方法;选择不会对结果造成太大差异。

要测试这个 XML 程序,输入以下内容到你的浏览器中,你应该看到类似 Figure 18-4 的东西:

http://127.0.0.1/18/xmlget.html

图 18-4. 异步获取 Yahoo! XML 新闻订阅

为什么使用 XML?

你可能会问为什么要使用 XML,除了获取如 RSS 订阅之类的 XML 文档。嗯,简单的答案是你不必使用,但如果你希望将结构化数据传递回应用程序,传递一个简单的、无组织的文本混乱可能需要在 JavaScript 中进行复杂的处理。

相反,您可以创建一个 XML 文档,并将其传递回调用函数,该函数将自动将其放入 DOM 树中,就像您现在熟悉的 HTML DOM 对象一样轻松访问。

现在,程序员更倾向于使用 JavaScript 对象表示法(JSON)作为其首选的数据交换格式,因为它是 JavaScript 的一个简单子集。

使用框架进行异步通信

现在您知道如何编写自己的异步程序,您可能想调查一些免费框架,这些框架可以使其更加轻松,并提供许多更高级的功能。特别是,我建议您查看非常流行的 jQuery 或者可能是增长最快的框架 React。不过,在接下来的章节中,我们将讨论如何使用 CSS 为您的网站应用样式。

问题

  1. 在 Web 服务器和 JavaScript 客户端之间进行异步通信时必须创建哪个对象?

  2. 如何判断异步调用何时完成?

  3. 如何知道异步调用是否成功完成?

  4. 哪个 XMLHttpRequest 对象属性返回异步调用的文本响应?

  5. 哪个 XMLHttpRequest 对象属性返回异步调用的 XML 响应?

  6. 如何指定回调函数以处理异步响应?

  7. 哪个 XMLHttpRequest 方法用于启动异步请求?

  8. 异步 GET 和 POST 请求的主要区别是什么?

请参阅 “第十八章答案”,以获取这些问题的答案。