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

67 阅读30分钟

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

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第十三章:Cookies、会话和认证

随着您的 Web 项目变得越来越大和复杂,您将发现越来越需要跟踪您的用户。即使您不提供登录和密码,您也经常需要存储关于用户当前会话的详细信息,并在他们返回您的站点时识别他们。

几种技术支持这种互动,从简单的浏览器 cookies 到会话处理和 HTTP 身份验证。它们之间为您提供了配置您的站点以符合用户偏好并确保流畅愉快过渡的机会。

在 PHP 中使用 Cookies

Cookie是通过 web 浏览器将数据项保存到计算机硬盘的一种数据。它几乎可以包含任何字母数字信息(只要在 4 KB 以下),并且可以从计算机检索并返回到服务器。常见用途包括会话跟踪,跨多次访问保持数据,保存购物车内容,存储登录详细信息等。

由于其隐私影响,cookies只能从发行域中读取。换句话说,如果一个 cookie 是由例如oreilly.com发行的,只有使用该域的 web 服务器才能检索它。这可以防止其他未经授权的网站获取这些细节。

由于互联网的工作方式,网页上的多个元素可以嵌入来自多个域的内容,每个域都可以发行自己的 cookies。当这种情况发生时,它们被称为第三方 cookies。最常见的是由广告公司创建,用于跨多个网站跟踪用户或进行分析。

因此,大多数浏览器允许用户关闭 cookies,无论是对当前服务器的域、第三方服务器还是两者都可以。幸运的是,大多数禁用 cookies 的人只对第三方网站这样做。

Cookies 在传输头部期间交换,在实际 HTML 网页发送之前,一旦 HTML 已经传输,发送 cookie 是不可能的。因此,仔细规划 cookie 的使用非常重要。图 13-1 说明了一个典型的浏览器和 web 服务器之间传递 cookies 的请求和响应对话。

带有 cookies 的浏览器/服务器请求/响应对话框

图 13-1. 带有 cookies 的浏览器/服务器请求/响应对话框

这个交换显示浏览器接收到两个页面:

  1. 浏览器发出请求以检索网站*www.webserver.com*上的主页面*index.html*。第一个头部指定文件,第二个头部指定服务器。

  2. webserver.com 的 Web 服务器收到这对标头时,将返回一些自己的标头。第二个标头定义要发送的内容类型(text/html),第三个标头发送具有名称 name 和值 value 的 cookie。然后才传输网页内容。

  3. 浏览器接收到 cookie 后,将在以后向发出请求的服务器发送该 cookie,直到 cookie 过期或被删除。因此,当浏览器请求新页面 /news.html 时,它还会返回具有值 value 的 cookie name

  4. 由于 cookie 已经设置,当服务器接收到发送 /news.html 的请求时,它不需要重新发送 cookie,而只需返回请求的页面。

注意

可以通过浏览器内置的开发者工具或扩展程序直接编辑 cookie。因此,由于用户可以更改 cookie 的值,您不应在 cookie 中放置用户名等关键信息,否则可能导致您的网站被意外操纵。Cookie 最适合用于存储语言或货币设置等数据。

设置 Cookie

在 PHP 中设置 cookie 很简单。只要还未传输任何 HTML,您可以调用 setcookie 函数,其语法如下(参见表 13-1):

setcookie(name, value, expire, path, domain, secure, httponly);

表 13-1. setcookie 参数

参数描述示例
namecookie 的名称。这是服务器在后续浏览器请求中使用的名称,用于访问 cookie。location
valuecookie 的值或 cookie 的内容。这可以包含最多 4 KB 的字母数字文本。USA
expire可选)过期日期的 Unix 时间戳。通常,您可能会使用 time() 加上一定的秒数。如果未设置,该 cookie 将在浏览器关闭时过期。time() + 2592000
path可选)服务器上 cookie 的路径。如果是 /(斜杠),则该 cookie 在整个域内有效,例如 *www.webserver.com*。如果是子目录,则 cookie 仅在该子目录中有效。默认为设置 cookie 的当前目录,这通常是您会使用的设置。/
domain可选)cookie 的互联网域。如果是 webserver.com,则该 cookie 对所有 webserver.com 及其子域(如 www.webserver.comimages.webserver.com)可用。如果是 images.webserver.com,则该 cookie 仅对 images.webserver.com 及其子域(如 sub.images.webserver.com)可用,但对例如 www.webserver.com 不可用。webserver.com
secure可选)cookie 是否必须使用安全连接(https://)。如果该值为 TRUE,则该 cookie 只能在安全连接上传输。默认为 FALSEFALSE
httponly可选;自 PHP 版本 5.2.0 起实现。)Cookie 是否必须使用 HTTP 协议。如果该值为 TRUE,脚本语言(如 JavaScript)无法访问 Cookie。默认值为 FALSEFALSE

因此,要创建一个名为 location、值为 USA 的 Cookie,在当前域上整个 Web 服务器上都可以访问,并且将在七天内从浏览器缓存中删除,使用以下方法:

setcookie('location', 'USA', time() + 60 * 60 * 24 * 7, '/');

访问 Cookie

读取 Cookie 的值就像访问 $_COOKIE 系统数组一样简单。例如,如果您想要查看当前浏览器是否已经存储了名为 location 的 Cookie,并且如果有的话,读取其值,可以使用以下方法:

if (isset($_COOKIE['location'])) $location = $_COOKIE['location'];

请注意,只有在将 Cookie 发送到 Web 浏览器后,您才能读取 Cookie。这意味着当您发出一个 Cookie 时,直到浏览器重新加载页面(或另一个具有访问权限的页面)从您的网站返回 Cookie 给服务器时,您无法再次读取它。

销毁 Cookie

要删除一个 Cookie,您必须再次发出它并将日期设置为过去。在您的新 setcookie 调用中,除了时间戳之外,所有参数都必须与首次发出 Cookie 时的参数相同;否则,删除操作将失败。因此,要删除之前创建的 Cookie,您将使用以下方法:

setcookie('location', 'USA', time() - 2592000, '/');

只要给定的时间是过去的,Cookie 就应该被删除。然而,我已经在过去使用了 2,592,000 秒(一个月)的时间,以防客户端计算机的日期和时间设置不正确。您也可以为 Cookie 值提供一个空字符串(或一个值为 FALSE 的值),PHP 将自动为您将其时间设置为过去。

HTTP 身份验证

HTTP 身份验证使用 Web 服务器来管理应用程序的用户和密码。对于要求用户登录的简单应用程序来说,这是足够的,尽管大多数应用程序会有专门的需求或更严格的安全要求,需要使用其他技术。

要使用 HTTP 身份验证,PHP 发送一个头部请求,请求与浏览器开始身份验证对话。服务器必须启用此功能才能正常工作,但由于它是如此常见,您的服务器很可能提供此功能。

注意

尽管它通常与 Apache 一起安装,但 HTTP 身份验证模块不一定安装在您使用的服务器上。因此,尝试运行这些示例可能会生成一个错误,告诉您该功能未启用,这种情况下,您必须安装该模块并更改配置文件以加载它,或者要求系统管理员进行这些更改。

在将您的 URL 输入浏览器或通过链接访问页面后,用户将看到一个“需要身份验证”的提示弹出窗口,请求两个字段:用户名和密码(图 13-2 显示了在 Firefox 中的外观)。

HTTP 鉴权登录提示

图 13-2. HTTP 鉴权登录提示

示例 13-1 展示了使其发生的代码。

示例 13-1. PHP 鉴权
<?php
  if (isset($_SERVER['PHP_AUTH_USER']) &&
      isset($_SERVER['PHP_AUTH_PW']))
  {
    echo "Welcome User: " . htmlspecialchars($_SERVER['PHP_AUTH_USER']) .
         " Password: "    . htmlspecialchars($_SERVER['PHP_AUTH_PW']);
  }
  else
  {
    header('WWW-Authenticate: Basic realm="Restricted Area"');
    header('HTTP/1.1 401 Unauthorized');
    die("Please enter your username and password");
  }
?>

程序首先查找两个特定数组值:$_SERVER['PHP_AUTH_USER']$_SERVER['PHP_AUTH_PW']。如果它们都存在,它们代表用户在身份验证提示中输入的用户名和密码。

注意

注意,在显示到屏幕上时,返回到$_SERVER数组中的值首先通过htmlspecialchars函数进行处理。这是因为这些值是用户输入的,因此不能信任,黑客可能通过添加 HTML 字符和其他符号尝试跨站点脚本攻击。htmlspecialchars将任何这样的输入转换为无害的 HTML 实体。

如果任一值不存在,则用户尚未通过身份验证,并且您通过发出以下标题显示图 13-2,其中Basic realm是受保护部分的名称,并作为弹出提示的一部分显示:

WWW-Authenticate: Basic realm="Restricted Area"

如果用户填写了字段,PHP 程序会再次从顶部运行。但如果用户点击取消按钮,则程序继续执行以下两行代码,发送以下标题和错误消息:

HTTP/1.1 401 Unauthorized

die 语句导致文本“请键入您的用户名和密码”被显示(参见图 13-3)。

点击取消按钮的结果

图 13-3. 点击取消按钮的结果
注意

一旦用户已经通过身份验证,您将无法再次弹出身份验证对话框,除非用户关闭并重新打开所有浏览器窗口,因为 Web 浏览器将持续将相同的用户名和密码返回给 PHP。在通过此部分并尝试不同操作时,您可能需要关闭并重新打开浏览器几次。最简单的方法是打开一个新的私密或匿名窗口来运行这些示例,这样您就无需关闭整个浏览器。

现在让我们检查有效的用户名和密码。在示例 13-1 中的代码无需太多更改即可添加此检查,只需修改先前的欢迎消息代码以测试正确的用户名和密码,然后发出欢迎消息。认证失败会导致发送错误消息(参见示例 13-2)。

Example 13-2. PHP 鉴权与输入检查
<?php
  $username = 'admin';
  $password = 'letmein';

  if (isset($_SERVER['PHP_AUTH_USER']) &&
      isset($_SERVER['PHP_AUTH_PW']))
  {
    if ($_SERVER['PHP_AUTH_USER'] === $username &&
        $_SERVER['PHP_AUTH_PW']   === $password)
          echo "You are now logged in";
    else die("Invalid username/password combination");
  }
  else
  {
    header('WWW-Authenticate: Basic realm="Restricted Area"');
    header('HTTP/1.0 401 Unauthorized');
    die ("Please enter your username and password");
  }
?>

在比较用户名和密码时,使用===(全等)运算符,而不是==(等于)运算符。这是因为我们要检查两个值是否完全匹配。例如,'0e123' == '0e456',这对于用户名或密码目的来说不是合适的匹配。

在前面的例子中,0e123 是 0 乘以 10 的 123 次方,结果为零,0e456 也是 0 乘以 10 的 456 次方,同样计算结果为零。因此,使用==运算符,它们会匹配,因为它们的值都评估为零,因此比较的结果将为true,但===运算符表示两个部分在每个方面都必须完全相同,而这两个字符串是不同的,所以测试将返回false

现在已经有了一个机制来验证用户,但只能针对单个用户名和密码进行。此外,密码以明文形式出现在 PHP 文件中,如果有人成功入侵您的服务器,他们将立即知道密码。因此,让我们看看更好地处理用户名和密码的方法。

存储用户名和密码

MySQL 是存储用户名和密码的自然方式。但同样,我们不希望以明文形式存储密码,因为如果数据库被黑客访问,我们的网站可能会受到威胁。相反,我们将使用一个称为单向函数的巧妙技巧。

这种类型的函数易于使用,并将文本字符串转换为看似随机的字符串。由于它们的单向性质,这些函数是不可能逆转的,因此它们的输出可以安全地存储在数据库中——窃取它的人将无法知道使用的密码。

在本书的早期版本中,我建议您使用MD5散列算法来保护数据安全。然而,时间过去了,现在 MD5 被认为是易于被破解的,因此不安全。实际上,甚至其先前推荐的替代方案SHA-1也似乎可以被破解。

所以,现在 PHP 5.5 几乎是到处的最低标准,我已经转而使用其内置的散列函数,这在安全性上更为可靠,并且以一种整洁的方式处理所有事务。

以前,要安全地存储密码,您需要对密码进行处理,这是指向密码添加用户未输入的额外字符(以进一步混淆它)。然后,您需要通过单向函数将其结果转换为看似随机的字符集,这曾经是难以破解的。

例如,如下代码(现在非常不安全,因为现代图形处理单元具有如此速度和功率):

echo hash('ripemd128', '`saltstring`mypassword');

将显示此值:

9eb8eb0584f82e5d505489e6928741e7

请记住,这并不是建议您使用的方法。请将其视为应该做的示例,因为它非常不安全。请继续阅读。

使用 password_hash

从 PHP 版本 5.5 开始,有一种更好的方法来创建加盐的密码哈希:password_hash 函数。将 PASSWORD_DEFAULT 作为其第二个(必需的)参数,要求函数选择当前可用的最安全的哈希函数。password_hash 还会为每个密码选择一个随机盐。(不要试图添加额外的盐,因为这可能会损害算法的安全性。)因此,以下代码:

echo password_hash("mypassword", PASSWORD_DEFAULT);

将返回以下形式的字符串,其中包括盐和验证密码所需的所有信息。

$2y$10$k0YljbC2dmmCq8WKGf8oteBGiXlM9Zx0ss4PEtb5kz22EoIkXBtbG
注意

如果你让 PHP 自行选择哈希算法,你应该允许返回的哈希随着时间的推移而扩展,以实现更好的安全性。PHP 的开发者建议你将哈希存储在数据库字段中,该字段至少可以扩展到 255 个字符(即使现在的平均长度为 60–72)。如果你希望,你可以手动选择 BCRYPT 算法,通过向函数的第二个参数提供常量 PASSWORD_BCRYPT 来保证哈希字符串仅为 60 个字符。然而,我不建议这样做,除非你有很好的理由。

你可以提供选项(以可选的第三个参数形式),以进一步定制如何计算哈希值,比如分配给哈希计算的成本或处理器时间的量(更多时间意味着更多安全性,但服务器速度较慢)。成本的默认值为 10,这是你在使用 BCRYPT 时应该使用的最低值。

然而,我不想用比你需要的更多信息来使你困惑,所以如果你希望获取有关可用选项的更多细节,请参考 文档。你甚至可以选择自己的盐(尽管从 PHP 7.0 开始已不建议这样做,因为除非你确切知道自己在做什么,否则这并不安全,就像 WordPress 仍然处理其自己的盐一样)。

使用 password_verify

要验证密码是否与哈希匹配,使用 password_verify 函数,向其传递用户刚输入的密码字符串以及存储在数据库中的该用户密码的哈希值。

所以,假设你的用户之前输入了(非常不安全的)密码 mypassword,现在你在变量 $hash 中存储了他们密码的哈希字符串(用户创建密码时),你可以像这样验证它们是否匹配:

if (password_verify("mypassword", $hash))
  echo "Valid";

如果提供了哈希的正确密码,则 password_verify 返回值 TRUE,因此此 if 语句将显示单词 “Valid.” 如果不匹配,则返回 FALSE,然后你可以要求用户再试一次。

一个示例程序

让我们看看这些函数与 MySQL 结合使用时是如何协同工作的。首先需要创建一个新表来存储密码哈希,因此在程序中键入 示例 13-3 并将其保存为 setupusers.php(或从 GitHub 下载),然后在浏览器中打开它。

示例 13-3. 创建用户表并添加两个帐户
<?php //setupusers.php
  require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (\PDOException $e)
  {
    throw new \PDOException($e->getMessage(), (int)$e->getCode());
  }

  $query = "CREATE TABLE users (
    forename VARCHAR(32) NOT NULL,
    surname  VARCHAR(32) NOT NULL,
    username VARCHAR(32) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL
  )";

  $result = $pdo->query($query);

  $forename = 'Bill';
  $surname  = 'Smith';
  $username = 'bsmith';
  $password = 'mysecret';
  $hash     = password_hash($password, PASSWORD_DEFAULT);

  add_user($pdo, $forename, $surname, $username, $hash);

  $forename = 'Pauline';
  $surname  = 'Jones';
  $username = 'pjones';
  $password = 'acrobat';
  $hash     = password_hash($password, PASSWORD_DEFAULT);

  add_user($pdo, $forename, $surname, $username, $hash);

  function add_user($pdo, $fn, $sn, $un, $pw)
  {
    $stmt = $pdo->prepare('INSERT INTO users VALUES(?,?,?,?)');

    $stmt->bindParam(1, $fn, PDO::PARAM_STR32);
    $stmt->bindParam(2, $sn, PDO::PARAM_STR32);
    $stmt->bindParam(3, $un, PDO::PARAM_STR32);
    $stmt->bindParam(4, $pw, PDO::PARAM_STR, 255);

    $stmt->execute([$fn, $sn, $un, $pw]);
  }
?>

此程序将在 publications 数据库中创建名为 users 的表(或者您为 第十一章 中的 login.php 文件设置的任何数据库)。在此表中,它将创建两个用户:Bill Smith 和 Pauline Jones。他们的用户名和密码分别为 bsmith/mysecretpjones/acrobat

使用此表中的数据,我们现在可以修改 示例 13-2 以正确验证用户,并且 示例 13-4 显示了执行此操作所需的代码。将其键入或从伴随网站下载,然后确保将其保存为 authenticate.php,并在浏览器中调用它。

示例 13-4. 使用 MySQL 进行 PHP 身份验证
<?php // authenticate.php   require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (\PDOException $e)
  {
    throw new \PDOException($e->getMessage(), (int)$e->getCode());
  }

  if (isset($_SERVER['PHP_AUTH_USER']) &&
      isset($_SERVER['PHP_AUTH_PW']))
  {
`$un_temp` `=` `sanitize``(``$pdo``,` `$_SERVER``[``'PHP_AUTH_USER'``]);`
    `$pw_temp` `=` `sanitize``(``$pdo``,` `$_SERVER``[``'PHP_AUTH_PW'``]);`     `$query `  `=` `"``SELECT * FROM users WHERE username=``$un_temp``"``;`
    `$result ` `=` `$pdo``->``query``(``$query``);`

    `if` `(``!``$result``->``rowCount``())` `die``(``"``User not found``"``);`

    `$row` `=` `$result``->``fetch``();`
    `$fn ` `=` `$row``[``'forename'``];`
    `$sn ` `=` `$row``[``'surname'``];`
    `$un ` `=` `$row``[``'username'``];`
    `$pw ` `=` `$row``[``'password'``];`

    `if` `(``password_verify``(``str_replace``(``"``'``"``,` `"``"``,` `$pw_temp``),` `$pw``))`
      `echo` `htmlspecialchars``(``"``$fn` `$sn` `: Hi` `$fn``,         you are now logged in as '``$un``'``"``);`
    `else` `die``(``"``Invalid username/password combination``"``);`
  }
  else
  {
    header('WWW-Authenticate: Basic realm="Restricted Area"');
    header('HTTP/1.1 401 Unauthorized');
    die ("Please enter your username and password");
  }

  function sanitize($pdo, $str)
  {
    $str = htmlentities($str);
    return $pdo->quote($str);
  }
?>

注意

使用 HTTP 身份验证将在使用 BCrypt 哈希化的密码时施加约 80 毫秒的惩罚,其默认成本为 10。此减速作为攻击者的屏障,防止他们以最大速度尝试破解密码。因此,在非常繁忙的站点上,HTTP 身份验证不是一个好的解决方案,您可能更喜欢使用会话(请参阅下一节)。

正如你在本书的这一部分可能期待的那样,其中一些示例开始变得相当长了。但不要灰心。这时你真正需要关注的只有加粗的那些行。它们从使用 sanitize 函数通过传递的用户名和密码开始,将任何 HTML 实体更改为安全字符串,再使用 htmlentities 函数添加单引号到字符串的起始和结束,以及使用 quote 方法。

接下来,向 MySQL 发出查询以查找用户 $un_temp,如果返回结果,则将第一行分配给 $row。因为用户名是唯一的,所以只会有一行。

现在只需检查存储在数据库中的哈希值,即 $row['password'],这是用户创建密码时使用 password_hash 计算的先前哈希值。

如果刚刚提供的哈希和密码验证成功,password_verify 将返回 TRUE,并输出友好的欢迎字符串,称呼用户的名字(参见 图 13-4)。否则,将显示错误消息。因为我们已经使用 quote 对密码进行了清理,所以在调用 password_verify 时,首先会使用 str_replace 删除封装的单引号。

您可以尝试在您的浏览器中调用程序,并输入用户名bsmith和密码mysecret(或pjonesacrobat),这些值已被示例 13-3 保存到数据库中。

Bill Smith has now been authenticated

图 13-4. Bill Smith has now been authenticated
注意

通过在遇到输入后立即对其进行清理,您将阻止任何恶意 HTML、JavaScript 或 MySQL 攻击在继续之前,您将不必再次清理此数据。如果用户的密码中包含字符如<&(例如),这些字符将被htmlentities函数扩展为&lt;&amp;——只要您的代码允许字符串的长度可能大于提供的输入宽度,并且始终通过此清理运行密码,您将一切安好。

使用会话(Sessions)

因为您的程序无法知道其他程序中设置了哪些变量——甚至不知道同一个程序上次运行时设置了什么值——因此有时您希望从一个网页跟踪用户的活动到另一个网页。您可以通过在表单中设置隐藏字段来实现这一点,如第十一章中所示,并在提交表单后检查字段的值,但是 PHP 提供了更强大、更安全和更简单的解决方案,即会话(sessions)。这些是存储在服务器上但只与当前用户相关的变量组。为了确保正确的变量应用于正确的用户,PHP 会在用户的 Web 浏览器中保存一个 cookie 来唯一标识他们。

注意

在 2019 年,谷歌宣布正在通过名为“隐私沙箱(Privacy Sandbox)”的项目逐步淘汰其浏览器中的第三方 cookie。毫无疑问,其他浏览器也将效仿,特别是 Opera 和 Microsoft Edge,它们都依赖于 Google Chrome 的代码库。然而,这引起了监管部门的关注,因为一些公司暗示这可能会导致更多的支出流向谷歌的生态系统,因此其实施可能会发生变化。可以确定的是,cookie 正变得不受欢迎,在您访问的几乎每个网站上都会看到 cookie 警告,其存在的日子已经不多了。总之,谷歌打算将用户分成大约 1000 个具有类似浏览器使用和产品兴趣的群体,以便无法唯一识别或跟踪任何人。然而,一旦 cookie 最终被淘汰,这可能会给您的代码带来问题。因此,我建议您关注可能影响用户与您开发的代码交互方式的这一领域的发展。

此 cookie 仅对 Web 服务器有意义,并不能用于获取用户的任何信息。您可能会问关于那些已禁用 cookie 的用户。在今天这个时代,任何禁用 cookie 的人都不应指望拥有最佳的浏览体验,如果您发现禁用了 cookie,您应该告知这样的用户,如果他们希望充分享受您的网站,则需要启用 cookie,而不是试图绕过 cookie 的使用,这可能会引发安全问题。

启动会话

启动会话需要在输出任何 HTML 之前调用 PHP 函数session_start,类似于在头部交换期间发送 cookie。然后,要开始保存会话变量,只需将它们分配为$_SESSION数组的一部分,如下所示:

$_SESSION['variable'] = $value;

然后可以在后续程序运行中轻松读取它们,如下所示:

$variable = $_SESSION['variable'];

现在假设您有一个应用程序,它始终需要访问每个用户在表users中存储的名字和姓氏,而您应该稍早就已创建了这个表。让我们进一步修改 Example 13-4 中的authenticate.php,以便在用户经过验证后设置会话。

Example 13-5 展示了所需的更改。唯一的区别是if (password_verify...部分的内容,我们现在通过打开会话并将这些变量保存到其中来开始。请将此程序键入(或修改 Example 13-4)并保存为authenticate2.php。但是不要立即在浏览器中运行它,因为您稍后还需要创建第二个程序。

Example 13-5. 在成功验证后设置会话
<?php // authenticate2.php   require_once 'login.php';

  try
  {
    $pdo = new PDO($attr, $user, $pass, $opts);
  }
  catch (\PDOException $e)
  {
    throw new \PDOException($e->getMessage(), (int)$e->getCode());
  }

  if (isset($_SERVER['PHP_AUTH_USER']) &&
      isset($_SERVER['PHP_AUTH_PW']))
  {
    $un_temp = sanitize($pdo, $_SERVER['PHP_AUTH_USER']);
    $pw_temp = sanitize($pdo, $_SERVER['PHP_AUTH_PW']);
    $query   = "SELECT * FROM users WHERE username=$un_temp";
    $result  = $pdo->query($query);

    if (!$result->rowCount()) die("User not found");

    $row = $result->fetch();
    $fn  = $row['forename'];
    $sn  = $row['surname'];
    $un  = $row['username'];
    $pw  = $row['password'];

`if` `(``password_verify``(``str_replace``(``"``'``"``,` `"``"``,` `$pw_temp``),` `$pw``))`
    `{`
      `session_start``();`

      `$_SESSION``[``'forename'``]` `=` `$fn``;`
      `$_SESSION``[``'surname'``]`  `=` `$sn``;`

      `echo` `htmlspecialchars``(``"``$fn` `$sn` `: Hi` `$fn``,         you are now logged in as '``$un``'``"``);`
      `die` `(``"``<p><a href='continue.php'>Click here to continue</a></p>``"``);`
    `}`     else die("Invalid username/password combination");
  }
  else
  {
    header('WWW-Authenticate: Basic realm="Restricted Area"');
    header('HTTP/1.0 401 Unauthorized');
    die ("Please enter your username and password");
  }

  function sanitize($pdo, $str)
  {
    $str = htmlentities($str);
    return $pdo->quote($str);
  }
?>

程序的另一个增加部分是“点击这里继续”链接,目标 URL 为continue.php。这将用于说明会话如何转移到另一个程序或 PHP 网页。因此,请通过键入 Example 13-6 中的程序并保存来创建continue.php

Example 13-6. 检索会话变量
<?php // continue.php
  session_start();

  if (isset($_SESSION['forename']))
  {
    $forename = htmlspecialchars($_SESSION['forename']);
    $surname  = htmlspecialchars($_SESSION['surname']);

    echo "Welcome back $forename.<br>
 Your full name is $forename $surname.<br>";
  }
  else echo "Please <a href='authenticate2.php'>click here</a> to log in.";
?>

现在您已经准备好在浏览器中调用authenticate2.php了。在提示时输入用户名bsmith和密码mysecret(或pjonesacrobat),然后点击链接加载continue.php。当您的浏览器调用它时,结果应该类似于 Figure 13-5。

图 13-5. 使用会话维护用户数据

会话将复杂的用户认证和登录代码整洁地限制在单个程序中。一旦用户经过验证并创建了会话,您的程序代码就变得非常简单。您只需调用session_start并查看$_SESSION中您需要访问的任何变量即可。

在示例 13-6 中,快速测试$_SESSION['forename']是否有值就足以让您知道当前用户是否经过身份验证,因为会话变量存储在服务器上(不像 cookie 存储在 Web 浏览器中),因此可以信任。

如果$_SESSION['forename']尚未被赋值,表示没有活动会话,因此在示例 13-6 的最后一行代码将用户重定向到登录页面authenticate2.php

结束会话

当需要结束会话时,通常是用户从您的网站请求注销时,您可以使用session_destroy函数,如示例 13-7 中所示。该示例提供了一个有用的函数来完全销毁会话,注销用户并取消所有会话变量的设置。

示例 13-7. 一个方便的函数来销毁会话及其数据
<?php
  function destroy_session_and_data()
  {
    session_start();
    $_SESSION = array();
    setcookie(session_name(), '', time() - 2592000, '/');
    session_destroy();
  }
?>

要查看其实际操作,您可以修改continue.php,如示例 13-8 所示。

示例 13-8. 检索会话变量然后销毁会话
<?php
  session_start();

  if (isset($_SESSION['forename']))
  {
    $forename = $_SESSION['forename'];
    $surname  = $_SESSION['surname'];

    `destroy_session_and_data``();`

    echo htmlspecialchars("Welcome back $forename");
    echo "<br>";
    echo htmlspecialchars("Your full name is $forename $surname.");

  }
  else echo "Please <a href='authenticate.php'>click here</a> to log in.";

  `function` `destroy_session_and_data``()`
  `{`
    `$_SESSION` `=` `array``();`
    `setcookie``(``session_name``(),` `''``,` `time``()` `-` `2592000``,` `'/'``);`
    `session_destroy``();`
  `}`
?>

第一次从authenticate2.php导航到continue.php时,会显示所有会话变量。但是,由于调用了destroy_session_and_data函数,如果您之后点击浏览器的重新加载按钮,会话将被销毁,然后您将提示返回登录页面。

设置超时时间

还有其他时候,您可能希望自己关闭用户的会话,比如用户忘记或忽略注销时,为了他们自己的安全,您希望程序替他们这样做。您可以通过设置超时时间来实现这一点,超过该时间没有活动则自动注销。

要做到这一点,请使用ini_set函数如下所示。该示例将超时设置为正好一天(字母gc代表垃圾回收):

ini_set('session.gc_maxlifetime', 60 * 60 * 24);

如果您想知道当前超时周期是多少,可以使用以下方法显示它:

echo ini_get('session.gc_maxlifetime');

会话安全

尽管我提到一旦您验证了用户并设置了会话,您可以安全地假定会话变量是可信的,但实际情况并非如此。原因是可能使用数据包嗅探(数据采样)来发现通过网络传输的会话 ID。此外,如果会话 ID 在 URL 的 GET 部分传递,它可能会出现在外部站点服务器日志中。

防止这些信息被发现的唯一真正安全的方法是实施传输层安全(TLS,比安全套接字层更安全的后续版本),并运行 HTTPS 而不是 HTTP 网页。这超出了本书的范围,但您可能希望查看Apache 文档以获取设置安全 Web 服务器的详细信息。

防止会话劫持

当 TLS 不可行时,您可以通过在存储会话时添加像下面这样的行来进一步验证用户,将他们的 IP 地址与其他详细信息一起存储:

$_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];

然后,作为额外的检查,每当加载任何页面并且会话可用时,请执行以下检查。如果存储的 IP 地址与当前 IP 地址不匹配,则调用函数 different_user

if ($_SESSION['ip'] != $_SERVER['REMOTE_ADDR']) different_user();

在你的 different_user 函数中放置的代码由你决定。由于技术错误,我建议你要么删除当前会话并要求用户重新登录,要么,如果你有他们的电子邮件地址,可以通过发送确认链接的方式,让他们确认身份,这将使他们保留会话中的所有数据。

当然,你需要意识到在同一代理服务器上的用户或在家庭或商业网络上共享相同 IP 地址的用户将具有相同的 IP 地址。如果这对你来说是个问题,请再次使用 HTTPS。你还可以存储浏览器的副本 用户代理字符串(开发者在浏览器中放置的标识它们的类型和版本的字符串),这也可能因使用中的各种浏览器类型、版本和计算机平台而区分用户(尽管这不是一个完美的解决方案,如果浏览器自动更新,该字符串将会更改)。使用以下方法存储用户代理:

$_SESSION['ua'] = $_SERVER['HTTP_USER_AGENT'];

使用这段代码来比较当前的用户代理字符串与保存的字符串:

if ($_SESSION['ua'] != $_SERVER['HTTP_USER_AGENT']) different_user();

或者更好的做法是,像这样结合两个检查,并将组合保存为 hash 十六进制字符串:

$_SESSION['check'] = hash('ripemd128', $_SERVER['REMOTE_ADDR'] .
    $_SERVER['HTTP_USER_AGENT']);

使用这段代码来比较当前和存储的字符串:

if ($_SESSION['check'] != hash('ripemd128', $_SERVER['REMOTE_ADDR'] .
    $_SERVER['HTTP_USER_AGENT'])) different_user();

防止会话固定攻击

会话固定攻击 是指恶意第三方获取有效的会话 ID(可能由服务器生成),并使用户使用该会话 ID 进行身份验证,而不是使用其自己的 ID 进行身份验证。当攻击者利用通过 URL 的 GET 部分传递会话 ID 的能力时,就可能发生这种情况,例如:

http://yourserver.com/authenticate.php?PHPSESSID=123456789

在这个例子中,虚构的会话 ID 123456789 被传递到服务器。现在,请考虑示例 13-9,它容易受到会话固定攻击的影响。要了解详细情况,请键入并保存为 sessiontest.php

示例 13-9. 会话易受会话固定攻击影响
<?php // sessiontest.php
  session_start();

  if (!isset($_SESSION['count'])) $_SESSION['count'] = 0;
  else ++$_SESSION['count'];

  echo $_SESSION['count'];
?>

保存之后,使用以下 URL 在浏览器中调用它(请用正确的路径名作为前缀,例如 *http://localhost*):

sessiontest.php?PHPSESSID=1234

点击重新加载几次,你会看到计数器增加。现在尝试浏览到:

sessiontest.php?PHPSESSID=5678

在这里点击重新加载几次,你应该看到计数器继续增加。将计数器留在与第一个 URL 不同的数字上,返回第一个 URL,然后查看数字如何变化。在这里,你已经创建了两个自己选择的不同会话,你也可以轻松地创建所需数量的会话。

这种方法如此危险的原因在于恶意攻击者可能会尝试向毫不知情的用户分发这些类型的 URL,如果其中任何一个用户跟随这些链接,攻击者将能够返回并接管未被删除或过期的任何会话——想象一下,如果那个会话是对购物网站或者更糟的情况是银行的。

为了防止这种情况发生,请尽快使用session_regenerate_id更改会话 ID。此函数保留所有当前会话变量值,但用一个攻击者无法知道的新 ID 替换会话 ID。为此,请检查一个您任意发明的特殊会话变量。如果不存在,您就知道这是一个新会话,因此您只需更改会话 ID 并设置特殊会话变量来记录更改。示例 13-10 展示了如何通过使用会话变量initiated的代码来实现这一点。

示例 13-10. 会话再生成
<?php
  session_start();

  if (!isset($_SESSION['initiated']))
  {
    session_regenerate_id();
    $_SESSION['initiated'] = 1;
  }

  if (!isset($_SESSION['count'])) $_SESSION['count'] = 0;
  else ++$_SESSION['count'];

  echo $_SESSION['count'];
?>

这样,攻击者可以使用他们生成的任何会话 ID 返回到您的网站,但它们中没有一个会调用另一个用户的会话,因为它们都已被重新生成的 ID 替换。

强制仅限于 Cookie 的会话

如果您准备好要求用户在您的网站上启用 Cookie,您可以使用ini_set函数,如下所示:

ini_set('session.use_only_cookies', 1);

有了这个设置,?PHPSESSID=的技巧将被完全忽略。如果您使用这种安全措施,我还建议您告知用户您的网站需要使用 Cookie(但仅在用户禁用 Cookie 的情况下,特别是用户所在地区要求 Cookie 通知时),这样他们就知道如果未能获得他们想要的结果,出了什么问题。

使用共享服务器

在与其他帐户共享的服务器上,您不希望将所有会话数据保存在与他们相同的目录中。相反,您应选择一个只有您的帐户有访问权限(并且不可见于 Web)的目录来存储您的会话,方法是在程序开头附近放置一个ini_set调用,就像这样:

ini_set('session.save_path', '/home/user/myaccount/sessions');

配置选项仅在程序执行期间保留此新值,并在程序结束时恢复原始配置。

这个sessions文件夹可能会很快填满;您可能希望根据服务器的繁忙程度定期清除旧会话。它被使用得越多,您希望保留会话的时间就越少。

注意

请记住,您的网站可能会受到黑客攻击的影响。有自动化机器人在互联网上肆虐,试图找到易受攻击的站点。因此,无论您做什么,每当处理不完全由您自己的程序生成的数据时,都应该极度小心对待它。

现在您应该对 PHP 和 MySQL 有很好的掌握,所以第十四章介绍了本书涵盖的第三种主要技术,JavaScript。

问题

  1. 为什么必须在程序开始时传输 cookie?

  2. 哪个 PHP 函数在 web 浏览器中存储 cookie?

  3. 如何销毁一个 cookie?

  4. 当使用 HTTP 认证时,在 PHP 程序中用户名和密码存储在哪里?

  5. password_hash 函数为什么是强大的安全措施?

  6. 加盐 一个字符串是什么意思?

  7. PHP 会话是什么?

  8. 如何启动 PHP 会话?

  9. 什么是会话劫持?

  10. 会话固定是什么?

请查看 “第十三章答案”,了解这些问题的答案。

第十四章:探索 JavaScript

JavaScript 为您的网站带来了动态功能。每当您在浏览器中鼠标悬停在某个项目上时看到弹出的内容,或者看到页面上出现新的文本、颜色或图片时,或者在页面上抓取一个对象并将其拖动到新位置时——这些通常是通过 JavaScript 完成的(尽管 CSS 越来越强大,也可以完成许多这些功能)。它提供了否则无法实现的效果,因为它在浏览器内运行,并直接访问 Web 文档中的所有元素。

JavaScript 最早出现在 Netscape Navigator 浏览器中,与浏览器中的 Java 技术支持同时推出。由于最初错误地认为 JavaScript 是 Java 的衍生物,长期以来有关它们关系的混淆。然而,命名只是一种营销策略,旨在借助 Java 编程语言的流行来推广新的脚本语言。

当网页的 HTML 元素在所谓的文档对象模型(DOM)中得到更正式、结构化的定义时,JavaScript 获得了新的力量。DOM 使得添加新段落或关注文本的一部分并更改它变得相对容易。

由于 JavaScript 和 PHP 都支持 C 编程语言中使用的大部分结构化编程语法,它们在语法上看起来非常相似。它们也都是相当高级的语言。此外,它们都是弱类型语言,因此通过在新的上下文中使用它,很容易将变量更改为新类型。

现在您已经学会了 PHP,您应该会发现 JavaScript 更容易。您会感到高兴,因为它是提供流畅 Web 前端的异步通信技术的核心(与 HTML5 功能一起),这是精明的 Web 用户现在期望的。

JavaScript 和 HTML 文本

JavaScript 是一种客户端脚本语言,完全运行在 Web 浏览器内或Node.js下。要调用它,您需要将其放置在开启 <script> 和闭合 </script> HTML 标签之间。一个典型的使用 JavaScript 的“Hello World” 文档可能看起来像 示例 14-1。

示例 14-1. 使用 JavaScript 显示“Hello World”
<html>
  <head><title>Hello World</title></head>
  <body>
    <script type="text/javascript">
      document.write("Hello World")
    </script>
    <noscript>
      Your browser doesn't support or has disabled JavaScript
    </noscript>
  </body>
</html>
注意

您可能已经看到使用 HTML 标签 <script language="javascript"> 的网页,但这种用法现在已经不推荐使用了。本示例使用了更新和更受欢迎的 <script type="text/javascript">,或者您也可以只使用 <script>

<script> 标签内是一行 JavaScript 代码,它使用其类似于 PHP echoprint 命令的 document.write。正如您所预期的那样,它只是将提供的字符串输出到当前文档中,然后在其中显示。

你可能还注意到,与 PHP 不同的是,JavaScript 中没有分号 (;)。这是因为换行在 JavaScript 中起到了分号的作用。然而,如果你希望在一行上有多个语句,除了最后一个外,每个命令都需要加上分号。当然,如果你愿意,你可以在每个语句的末尾加上分号,你的 JavaScript 也会正常工作。我个人的偏好是省略分号,因为它是多余的,因此我也避免可能会引起问题的做法。不过,最终,选择可能取决于你所在的团队,大多数情况下可能会要求使用分号,以确保安全。所以,如果有疑问,就加上分号吧。

在这个例子中还要注意的是 <noscript></noscript> 标签对。当你希望为不支持 JavaScript 或禁用 JavaScript 的用户提供替代的静态 HTML 时,可以使用这些标签。是否使用这些标签取决于你,因为它们并不是必需的,但通常情况下应该使用它们,因为为使用 JavaScript 提供的操作提供静态 HTML 替代通常并不困难。然而,本书的其余示例将省略 <noscript> 标签,因为我们专注于 JavaScript 的用法,而不是不使用 JavaScript 的情况。

当加载 示例 14-1 时,启用 JavaScript 的 Web 浏览器将输出以下内容(见 图 14-1):

Hello World

JavaScript,已启用并正常工作

图 14-1. JavaScript,已启用并正常工作

浏览器禁用 JavaScript 将显示此消息(见 图 14-2):

Your browser doesn't support or has disabled JavaScript

JavaScript 已禁用

图 14-2. JavaScript,禁用

在文档头部使用脚本

除了将脚本放在文档主体中之外,还可以将其放在 <head> 部分,这是在页面加载时执行脚本的理想位置。如果在那里放置关键代码和函数,还可以确保它们能够立即被文档中依赖它们的任何其他脚本部分使用。

将脚本放在文档头部的另一个原因是为了使 JavaScript 能够将诸如 meta 标签之类的东西写入 <head> 部分,因为你的脚本位置默认会写入文档的那部分。

旧版和非标准浏览器

如果你需要支持不支持脚本的浏览器(在今天几乎是不可能的),你需要使用 HTML 注释标签 (<!---->) 来防止它们遇到不应看到的脚本代码。示例 14-2 显示了如何将它们添加到你的脚本代码中。

示例 14-2. 针对非 JavaScript 浏览器修改的“Hello World”示例
<html>
  <head><title>Hello World</title></head>
  <body>
    <script type="text/javascript">`<!--`
      document.write("Hello World")
    `// -->`
    </script>
  </body>
</html>

这里在 <script> 开始语句之后直接添加了一个开放的 HTML 注释标签 (<!--),并在用 </script> 关闭脚本之前直接添加了一个关闭注释标签 (// -->)。

双斜杠 (//) 用于 JavaScript 表示该行剩余部分是注释。它存在的目的是让支持 JavaScript 的浏览器忽略后面的 -->,但非 JavaScript 浏览器将忽略前面的 // 并执行 --> 以关闭 HTML 注释。

尽管解决方案有点复杂,但你真正需要记住的只是在希望支持非常旧或非标准浏览器时使用以下两行来包含你的 JavaScript:

`<``script` `type``=``"text/javascript"``>``<!--`
  *`(``Your` `JavaScript` `goes` `here``...``)`*
`// -->` `<``/``s``c``r``i``p``t``>`

然而,这些注释的使用对于过去几年中发布的任何浏览器都是不必要的,但是你需要注意这一点,以防万一。

包含 JavaScript 文件

除了直接在 HTML 文档中编写 JavaScript 代码外,您还可以包含来自您的网站或互联网任何地方的 JavaScript 代码文件。其语法如下:

<script type="text/javascript" src="script.js"></script>

要从互联网中引入文件,请使用此方法(这里不包括 type="text/javascript",因为这是可选的):

<script src="http://someserver.com/script.js"></script>

至于脚本文件本身,它们必须 包含任何 <script></script> 标签,因为这些标签是不必要的:浏览器已经知道正在加载 JavaScript 文件。如果将它们放入 JavaScript 文件中,将会导致错误。

包含脚本文件是您在网站上使用第三方 JavaScript 文件的首选方法。

注意

可以省略 type="text/javascript" 参数;所有现代浏览器默认假定脚本包含 JavaScript。

调试 JavaScript 错误

当你学习 JavaScript 时,能够跟踪打字或其他编码错误非常重要。与 PHP 不同,它在浏览器中显示错误消息,JavaScript 处理错误消息的方式会根据所使用的浏览器而改变。Table 14-1 列出了如何访问最常用浏览器中的 JavaScript 错误消息。

表 14-1. 在不同浏览器中访问 JavaScript 错误消息

浏览器如何访问 JavaScript 错误消息
Apple Safari打开 Safari 并选择 Safari > Preferences > Advanced。然后选择在菜单栏中显示“开发”菜单。选择 Develop > Show Error Console。
Google Chrome, Microsoft Edge, Mozilla Firefox, & Opera在 PC 上按 Ctrl-Shift-J 或在 Mac 上按 Command-Shift-J。

请参阅浏览器开发者网站上的完整详细信息。

使用注释

由于它们共同继承自 C 编程语言,PHP 和 JavaScript 有许多相似之处,其中之一是注释。首先是单行注释,如下所示:

// This is a comment

这种风格使用一对斜杠字符(//)告知 JavaScript 忽略后续所有内容。您也可以使用多行注释,就像这样:

/* This is a section
 of multiline comments
 that will not be
 interpreted */

你可以使用序列/*开启多行注释,并用*/结束它。请记住,不能嵌套多行注释,因此请确保不要注释掉已包含多行注释的大代码段。

分号

与 PHP 不同,如果一行上只有一个语句,JavaScript 通常不需要分号。因此,以下写法是有效的:

x += 10

然而,当你希望在一行上放置多个语句时,必须用分号分隔它们,就像这样:

x += 10; y -= 5; z = 0

通常可以省略最后一个分号,因为换行符会终止最后一个语句。

警告

分号规则有例外情况。如果您编写 JavaScript 书签工具,或在语句结尾处有变量或函数引用,并且下一行的第一个字符是左括号或左方括号,必须记住追加分号,否则 JavaScript 将失败。因此,当有疑问时,请使用分号。

变量

在 JavaScript 中,没有像 PHP 中的美元符号那样特殊标识变量。相反,变量使用以下命名规则:

  • 变量可以包含字母a–zA–Z0–9$符号和下划线(_)。

  • 变量名中不允许包含除了a–zA–Z0–9$和下划线(_)以外的任何字符。

  • 变量名的第一个字符只能是a–zA–Z$_(不能是数字)。

  • 变量名区分大小写。CountcountCOUNT都是不同的变量。

  • 变量名长度没有固定限制。

是的,你没错:在允许的字符列表中包含$。JavaScript 允许这样,并且$可能是变量或函数名的第一个字符。虽然我不建议保留$字符,但这条规则允许您更快地将大量 PHP 代码移植到 JavaScript 中。

字符串变量

JavaScript 字符串变量应该用单引号或双引号括起来,像这样:

greeting = "Hello there"
warning  = 'Be careful'

在双引号字符串或单引号字符串中可以包含单引号或双引号。但必须使用反斜杠字符来转义相同类型的引号,例如:

greeting = "\"Hello there\" is a greeting"
warning  = '\'Be careful\' is a warning'

要从字符串变量中读取数据,可以将其赋值给另一个变量,例如:

newstring = oldstring

或者你可以在函数中使用它,就像这样:

status = "All systems are working"
document.write(status)

数值变量

创建数值变量就像简单地赋值一样,例如:

count       = 42
temperature = 98.4

像字符串一样,数值变量可以被读取并用于表达式和函数中。

数组

JavaScript 数组与 PHP 中的数组非常相似,因为数组可以包含字符串或数值数据,以及其他数组。要为数组分配值,请使用以下语法(在这种情况下创建一个字符串数组):

toys = ['bat', 'ball', 'whistle', 'puzzle', 'doll']

要创建多维数组,可以将较小的数组嵌套在较大的数组中。因此,要创建一个二维数组,包含魔方的一个面的颜色(红、绿、橙、黄、蓝和白,分别用其大写的首字母表示),可以使用以下代码:

face =
[
  ['R', 'G', 'Y'],
  ['W', 'R', 'O'],
  ['Y', 'W', 'G']
]

前面的示例已经格式化,以便明确显示正在进行的操作,但也可以这样写:

face = [['R', 'G', 'Y'], ['W', 'R', 'O'], ['Y', 'W', 'G']]

或者像这样:

top = ['R', 'G', 'Y']
mid = ['W', 'R', 'O']
bot = ['Y', 'W', 'G']

face = [top, mid, bot]

要访问此矩阵中从下到上第二个位置和从左到右第三个位置的元素,您将使用以下方法(因为数组元素从位置 0 开始):

document.write(face[1][2])

此语句将输出字母O来表示橙色

JavaScript 数组是强大的存储结构,因此第十六章更深入地讨论了它们。

运算符

在 JavaScript 中,就像在 PHP 中一样,运算符可能涉及数学运算、对字符串的更改以及比较和逻辑运算(andor等)。JavaScript 数学运算符看起来非常像普通算术运算,例如,以下语句输出15

document.write(13 + 2)

以下各节将教你各种运算符。

算术运算符

算术运算符 用于进行数学计算。您可以使用它们进行四种主要操作(加法、减法、乘法和除法),以及找到模数(除法后的余数),以及增加或减少一个值(见表 14-2)。

表 14-2. 算术运算符

运算符描述示例
+加法`j` **`+`** `12`
减法`j` **`–`** `22`
*乘法`j` **`*`** `7`
/除法`j` **`/`** `3.13`
%取模(除法余数)`j` **`%`** `6`
++递增**`++`**`j`
--递减**`--`**`j`

赋值运算符

分配运算符 用于将值分配给变量。它们从非常简单的=开始,然后移到+=–=等。运算符+=将右侧的值添加到左侧的变量中,而不是完全替换左侧的值。因此,如果count从值6开始,该语句:

count += 1

count设置为7,就像更常见的赋值语句一样:

count = count + 1

表 14-3 列出了各种可用的赋值运算符。

表 14-3. 赋值运算符

运算符示例相当于
=j **`=`** 99j = 99
+=j **`+=`** 2j = j + 2
+=j **+=** 'string'j = j + 'string'
–=j **`–=`** 12j = j – 12
*=j **`*=`** 2j = j * 2
/=j **`/=`** 6j = j / 6
%=j **`%=`** 7j = j % 7

比较运算符

比较运算符通常用于诸如 if 语句之类的结构中,用于比较两个项。例如,您可能希望知道您正在递增的变量是否达到了特定值,或者另一个变量是否小于设定值等(参见表 14-4)。

表 14-4. 比较运算符

运算符描述示例
==等于j **==** 42
!=不等于j **`!=`** 17
>大于j **`>`** 0
<小于j **`<`** 100
>=大于或等于j **`>=`** 23
<=小于或等于j **`<=`** 13
===等于(且类型相同)j **===** 56
!==不等于(且类型相同)j **`!==`** '1'

逻辑运算符

与 PHP 不同,JavaScript 的逻辑 运算符 不包括 andor 的等价物 &&||,也没有 xor 运算符(参见表 14-5)。

表 14-5. 逻辑运算符

运算符描述示例
&&j == 1 **&&** k == 2
&#124;&#124;j < 100 **&#124;&#124;** j > 0
!**`!`** (j == k)

增减和简写赋值

您学会在 PHP 中使用的后增和前增减形式及简写赋值运算符,在 JavaScript 中也受支持:

++x
--y
x += 22
y -= 3

字符串连接

JavaScript 处理字符串连接的方式与 PHP 稍有不同。它使用加号 (+) 而不是点号 (.) 运算符,例如:

document.write("You have " + messages + " messages.")

假设变量 messages 的值设定为 3,则此行代码的输出如下:

You have 3 messages.

正如您可以使用 += 运算符将值添加到数值变量中一样,您也可以使用相同方式将一个字符串附加到另一个字符串上:

name =  "James"
name += " Dean"

转义字符

转义字符,您已经看到它们用于在字符串中插入引号,也可以用于插入各种特殊字符,例如制表符、换行符和回车符。以下是一个使用制表符布局标题的示例——这仅用于说明转义字符的用法,在网页中,有更好的布局方式:

heading = "Name\tAge\tLocation"

表 14-6 详细描述了可用的转义字符。

表 14-6. JavaScript 的转义字符

字符含义
\b退格
\f换页符
\n换行符
\r回车符
\t制表符
\'单引号(或撇号)
\"双引号
\\反斜杠
\*`XXX`*介于 000377 之间的八进制数,表示与拉丁-1 字符等效的字符(例如 \251 表示 © 符号)
\x*`XX`*介于 00FF 之间的十六进制数,表示与拉丁-1 字符等效的字符(例如 \xA9 表示 © 符号)
\u*`XXXX`*介于0000FFFF之间的十六进制数字,表示相应的 Unicode 字符(例如\u00A9表示©符号)

变量类型

类似于 PHP,JavaScript 是一种非常宽松的类型语言;变量的类型仅在赋值时确定,并且随着变量在不同上下文中的出现而变化。通常情况下,您不必担心类型;JavaScript 会弄清楚您想要的并直接执行。

查看示例 14-3,其中:

  1. 变量n被赋予字符串值'838102050'。下一行打印出它的值,并使用typeof运算符查找类型。

  2. 当数字1234567890相乘时,n被赋予的值是返回的值。这个值也是838102050,但它是一个数字,而不是字符串。然后查找并显示变量的类型。

  3. 一些文本被附加到数字n上,并显示结果。

示例 14-3. 通过赋值设置变量类型
<script>
  n = '838102050'        // Set 'n' to a string
  document.write('n = ' + n + ', and is a ' + typeof n + '<br>')

  n = 12345 * 67890;     // Set 'n' to a number
  document.write('n = ' + n + ', and is a ' + typeof n + '<br>')

  n += ' plus some text' // Change 'n' from a number to a string
  document.write('n = ' + n + ', and is a ' + typeof n + '<br>')
</script>

此脚本的输出如下所示:

n = 838102050, and is a string
n = 838102050, and is a number
n = 838102050 plus some text, and is a string

如果对变量的类型存有任何疑问,或者需要确保变量具有特定类型,可以通过使用以下语句(分别将字符串转换为数字和将数字转换为字符串)来强制执行它:

n = "123"
n *= 1    // Convert 'n' into a number

n = 123
n += ""   // Convert 'n' into a string

或者您可以以同样的方式使用以下函数:

n = "123"
n = parseInt(n)   // Convert 'n' into an integer number
n = parseFloat(n) // Convert 'n' into a floating point number

n = 123
n = n.toString()  // Convert 'n' into a string

您可以在 JavaScript 在线阅读更多有关类型转换。并且您可以通过使用typeof运算符随时查找变量的类型。

函数

与 PHP 类似,JavaScript 函数用于将执行特定任务的代码段分离出来。要创建函数,请按照示例 14-4 中所示的方式声明。

示例 14-4. 简单函数声明
<script>
  function product(a, b)
  {
    return a*b
  }
</script>

此函数获取传递的两个参数,将它们相乘,并返回乘积。

全局变量

全局变量是指在任何函数之外定义的变量(或在函数内部定义但没有使用var关键字)。它们可以以下列方式定义:

    a = 123               // Global scope
var b = 456               // Global scope
if (a == 123) var c = 789 // Global scope

无论您是否使用了var关键字,只要变量在函数之外定义,它就具有全局作用域。这意味着脚本的每个部分都可以访问它。

局部变量

自动传递给函数的参数具有局部作用域,即只能在该函数内部引用。但是,也有一个例外。数组是通过引用传递给函数的,因此如果修改数组参数中的任何元素,则会修改原始数组的元素。

要定义仅在当前函数中具有作用域且未作为参数传递的局部变量,请使用var关键字。示例 14-5 展示了一个创建一个具有全局作用域和两个局部作用域的变量的函数。

示例 14-5。创建具有全局和局部作用域变量的函数
<script>
  function test()
  {
        a = 123               // Global scope
    var b = 456               // Local scope
    if (a == 123) var c = 789 // Local scope
  }
</script>

要测试 PHP 中作用域设置是否有效,可以使用isset函数。但在 JavaScript 中没有这样的函数,因此示例 14-6 使用typeof运算符,当变量未定义时返回字符串undefined

示例 14-6。检查在函数测试中定义的变量的作用域
<script>
  test()

  if (typeof a != 'undefined') document.write('a = "' + a + '"<br>')
  if (typeof b != 'undefined') document.write('b = "' + b + '"<br>')
  if (typeof c != 'undefined') document.write('c = "' + c + '"<br>')

  function test()
  {
    a     = 123
    var b = 456

    if (a == 123) var c = 789
  }
</script>

此脚本的输出是以下单行:

a = "123"

这表明只有变量a被赋予了全局作用域,这正是我们期望的,因为变量bc通过使用var关键字赋予了局部作用域。

如果您的浏览器发出关于b未定义的警告,该警告是正确的,但可以忽略。

使用 let 和 const

JavaScript 现在提供了两个新关键字:letconstlet关键字基本上可以替代var,但它有一个优势,一旦用let声明过变量,就不能重新声明变量,尽管使用var可以。

你看,使用var可以重新声明变量的事实导致了一些难以调试的 bug,例如下面的情况:

var hello   = "Hello there"
var counter = 1

if (counter > 0)
{
  var hello = "How are you?"
}

document.write(hello)

看出问题了吗?因为counter大于 0(因为我们将其初始化为 1),字符串hello被重新定义为“你好吗?”然后显示在文档中。

现在,如果您用let替换var(如下所示),第二个声明将被忽略,原始字符串“你好”将被显示:

let hello   = "Hello there"
let counter = 1

if (counter > 0)
{
  let hello = "How are you?"
}

document.write(hello)

var关键字可以是全局作用域(如果在任何块或函数之外)或函数作用域,用它声明的变量初始化为undefined,但let关键字可以是全局或作用域,变量不会被初始化。

使用let分配的任何变量的作用域要么在整个文档中声明(如果在任何块之外),要么在由{}界定的块内(包括函数),其作用域仅限于该块(及其任何嵌套的子块)。如果在块内声明变量,但试图从该块外部访问它,将返回错误,如下面的示例,document.write将失败,因为hello将没有值:

let counter = 1

if (counter > 0)
{
  let hello = "How are you?"
}

document.write(hello)

可以使用let声明与先前声明的同名变量,只要它在新的作用域内,这种情况下,前一个作用域中同名变量的任何先前赋值将对新作用域不可访问,因为同名的新变量被视为与先前的完全不同。它仅在当前块或任何子块中具有作用域(除非使用另一个let声明在子块中声明同名的另一个变量)。

尽量避免重用有意义的变量名称是个好习惯,否则你可能会引起混淆。不过,循环或索引变量如i(或其他简短简单的名称)通常可以在新的作用域中重复使用而不会引起混淆。

通过声明一个变量具有常量值(即不能更改的值),你可以进一步增加对作用域的控制。这在你创建一个视为常量的变量但只使用varlet声明时很有用,因为你可能在代码中的某些地方尝试更改该值,这是允许的但可能是一个错误。

但是,如果你使用const关键字声明变量并分配其值,稍后尝试更改该值将被禁止,并且你的代码将在控制台中显示类似于以下的错误消息:

Uncaught TypeError: Assignment to constant variable

以下代码将导致该错误:

const hello = "Hello there"
let counter = 1

if (counter > 0)
{
  hello = "How are you?"
}

document.write(hello)

就像let一样,const声明也是块作用域的(在{}和任何子块内部),这意味着你可以在代码片段的不同作用域中有相同名称的常量变量但具有不同的值。然而,我强烈建议你尽量避免名称重复,并且在每个程序中为一个唯一值使用一个新的常量名称。

总结:var具有全局或函数作用域,而letconst具有全局或块作用域。varlet都可以在声明时不初始化,而const在声明时必须初始化。var关键字可以重新声明var变量,但letconst不能。最后,const既不能重新声明也不能重新赋值。

注意

你可能更喜欢使用开发者控制台进行测试,比如这些(以及本书其他地方),正如之前在“调试 JavaScript 错误”中所解释的那样,在这种情况下,你可以将document.write替换为console.log,输出将显示在控制台而不是浏览器中。这也是在文档完全加载后运行的 JavaScript 的更好选择,因为此时document.write会替换当前文档而不是附加到其上,这可能不是您的意图。

文档对象模型

JavaScript 的设计非常聪明。与其只是创建另一种脚本语言(这在当时仍然是一个相当大的改进),更好的做法是围绕已经存在的 HTML 文档对象模型构建它。这将 HTML 文档的各个部分分解为离散的对象,每个对象都有自己的属性方法,并且受 JavaScript 控制。

JavaScript 使用句点(一个很好的理由是 + 是 JavaScript 中的字符串连接运算符,而不是句点)来分隔对象、属性和方法。例如,我们可以把名片看作一个名为 card 的对象。该对象包含诸如姓名、地址、电话号码等属性。在 JavaScript 的语法中,这些属性看起来像这样:

card.name
card.phone
card.address

它的方法是检索、更改和其他操作属性的函数。例如,要调用一个显示 card 对象属性的方法,你可以使用这样的语法:

card.display()

请查看本章早些示例中的一些例子,并注意 document.write 语句的使用位置。现在您了解了 JavaScript 基于对象的原理,您将看到 write 实际上是 document 对象的一个方法。

在 JavaScript 中,存在一种父子对象的层次结构,这就是所谓的文档对象模型(DOM;见 图 14-3)。

DOM 对象层次结构示例

图 14-3. DOM 对象层次结构示例

该图使用您已经熟悉的 HTML 标签来说明文档中各种对象之间的父子关系。例如,链接内的 URL 是 HTML 文档的一部分。在 JavaScript 中,它可以这样引用:

url = document.links.linkname.href

请注意这如何沿着中心列向下进行。第一部分 document 指的是 <html><body> 标签;links.linkname 指的是 <a> 标签,href 指的是 href 属性。

让我们将其转换为一些 HTML 和一个脚本来读取链接的属性。键入 示例 14-7,将其保存为 linktest.html,然后在浏览器中调用它。

示例 14-7. 使用 JavaScript 读取链接 URL
<html>
  <head>
    <title>Link Test</title>
  </head>
  <body>
    <a id="mylink" href="http://mysite.com">Click me</a><br>
    <script>
      url = document.links.mylink.href
      document.write('The URL is ' + url)
    </script>
  </body>
</html>

注意 <script> 标签的简写形式,我省略了参数 type="text/JavaScript",以节省您的输入时间。如果您愿意,仅为测试此(以及其他示例)的目的,您也可以省略 <script></script> 标签外的所有内容。该示例的输出如下所示:

Click me
The URL is http://mysite.com

输出的第二行来自于 document.write 方法。请注意代码是如何从 document 开始按照文档树向下移动到 linksmylink(链接的 id)和 href(URL 目标值)的。

也有一种同样有效的简写形式,它以 id 属性的值作为起点:mylink.href。因此,您可以将其替换为

url = document.links.mylink.href

以下是具体内容:

url = mylink.href

$ 符号的另一种用法

正如前面提到的,JavaScript 变量和函数名称中允许使用 $ 符号。因此,有时您可能会遇到像这样看起来奇怪的代码:

url = $('mylink').href

一些富有创造力的程序员决定 getElementById 函数在 JavaScript 中非常普遍,因此编写了一个名为 $ 的函数来替换它,就像 jQuery 中使用 $ 一样(尽管 jQuery 将 $ 用于更多用途——请参见第二十二章)。如示例 14-8 所示。

示例 14-8. 一个替代 getElementById 方法的函数
<script>
  function $(id)
  {
    return document.getElementById(id)
  }
</script>

因此,只要您在代码中包含了 $ 函数,就可以使用如下的语法:

$('mylink').href

可以替换如下代码:

document.getElementById('mylink').href

使用 DOM

links 对象实际上是一个 URL 数组,因此在所有浏览器中可以安全地像 示例 14-7 中的 mylink URL 一样进行引用(因为它是第一个也是唯一的链接):

url = document.links[0].href

如果您想知道整个文档中有多少链接,可以像这样查询 links 对象的 length 属性:

numlinks = document.links.length

您可以这样提取并显示文档中的所有链接:

for (j=0 ; j < document.links.length ; ++j)
  document.write(document.links[j].href + '<br>')

某物的 length 是每个数组以及许多对象的属性。例如,您可以这样查询浏览器的浏览历史记录中的项目数量:

document.write(history.length)

为了防止网站窥探您的浏览历史,history 对象仅存储数组中的站点数量:您无法从中读取或写入这些值。但是,如果您知道页面在历史记录中的位置,可以用另一页面替换当前页面。在某些情况下,这非常有用,例如您知道历史记录中某些页面来自您的站点,或者您只是希望将浏览器退回一些页面,您可以使用 history 对象的 go 方法。例如,要将浏览器退回三页,可以执行以下命令:

history.go(-3)

您还可以使用以下方法逐页向后或向前移动一页:

history.back()
history.forward()

类似地,您可以像这样替换当前加载的 URL 为您选择的 URL:

document.location.href = 'http://google.com'

当然,DOM 的内容远不止于阅读和修改链接。随着您在 JavaScript 的以下章节中的进展,您将对 DOM 及其访问方式非常熟悉。

关于 document.write

在教授编程时,有必要有一种快速简便的方法来显示表达式的结果。例如,在 PHP 中有 echoprint 语句,它们只是将文本发送到浏览器,因此很简单。但是在 JavaScript 中,有以下替代方法。

使用 console.log

console.log 函数将输出传递给它的任何值或表达式的结果到当前浏览器的控制台中。这是一个特殊模式,具有与浏览器窗口分离的帧或窗口,可以在其中显示错误和其他消息。虽然对经验丰富的程序员来说很好,但对初学者来说并不理想,因为输出不靠近浏览器中的网页内容。

使用 alert

alert 函数在弹出窗口中显示传递给它的值或表达式,需要点击按钮才能关闭。显然,这可能会非常迅速地变得相当恼人,并且它的缺点是只显示当前消息——之前的消息都会被擦除。

写入元素中

可以直接编写到 HTML 元素的文本中,这是一个相当优雅的解决方案(也是生产网站的最佳方案)——但是,对于这本书中的每个示例,都需要创建这样一个元素,并编写一些代码行来访问它。这样做会妨碍教授示例的核心内容,并使代码看起来过于笨重和混乱。

使用 document.write

document.write 函数在当前浏览器位置写入一个值或表达式,因此非常适合快速显示结果。通过将输出放置在浏览器中的网页内容和代码旁边,可以使所有示例保持简洁明了。

然而,你可能听说过这个函数被一些开发人员视为不安全,因为当你在网页完全加载后调用它时,它会覆盖当前文档。尽管这是正确的,但它并不适用于本书中的任何示例,因为它们都是按照 document.write 最初预期的方式使用的:作为页面创建过程的一部分,在页面完成加载和显示之前仅调用它。

然而,尽管我在简单示例中以这种方式使用 document.write,但我从不在生产代码中使用它(除非在确实必要的非常罕见情况下)。相反,我几乎总是使用前面介绍的方法,即直接编写到专门准备好的元素中,根据第十八章以后的更复杂示例(访问元素的 innerHTML 属性进行程序输出)。

因此,请记住,在本书中看到 document.write 被调用时,它只是为了简化一个示例,我建议你也仅在同样的方式下使用该函数——用于获取快速的测试结果。

在解释了这一注意事项后,在接下来的章节中,我们将继续探讨 JavaScript 的控制程序流和编写表达式的方法。

问题

  1. 用什么标签来包含 JavaScript 代码?

  2. 默认情况下,JavaScript 代码会将输出写入文档的哪个部分?

  3. 如何在文档中包含来自另一个来源的 JavaScript 代码?

  4. 哪个 JavaScript 函数相当于 PHP 中的 echoprint

  5. 如何在 JavaScript 中创建注释?

  6. JavaScript 字符串连接运算符是什么?

  7. 在 JavaScript 函数中,你可以使用哪个关键字来定义具有局部作用域的变量?

  8. 给出两种跨浏览器的方法来显示分配给具有 idthislink 的链接的 URL。

  9. 哪两个 JavaScript 命令会使浏览器加载其历史数组中的前一页?

  10. 你会用什么 JavaScript 命令来替换当前文档为oreilly.com网站的主页面?

查看“第十四章答案”在附录 A 中这些问题的答案。

第十五章:JavaScript 中的表达式和控制流

在前一章中,我介绍了 JavaScript 和 DOM 的基础知识。现在是时候看看如何在 JavaScript 中构建复杂的表达式,并通过使用条件语句来控制脚本的程序流了。

表达式

JavaScript 表达式与 PHP 中的表达式非常相似。正如您在第四章中学到的那样,表达式是值、变量、运算符和函数的组合,其结果是一个值;结果可以是数字、字符串或布尔值(评估为truefalse)。

示例 15-1 展示了一些简单的表达式。对于每一行,它打印出ad之间的字母,后跟一个冒号和表达式的结果。<br>标签用于创建换行并将输出分隔成四行(请记住,在 HTML5 中<br><br />都是可接受的,因此我选择使用前一种形式以简洁为主)。

示例 15-1. 四个简单的布尔表达式
<script>
  document.write("a: " + (42 > 3) + "<br>")
  document.write("b: " + (91 < 4) + "<br>")
  document.write("c: " + (8 == 2) + "<br>")
  document.write("d: " + (4 < 17) + "<br>")
</script>

此代码的输出如下:

a: true
b: false
c: false
d: true

注意,a:d:两个表达式都评估为true,但b:c:评估为false。与 PHP 不同(它将分别打印数字1和空白),实际的字符串truefalse将被显示。

在 JavaScript 中,当您检查一个值是否为truefalse时,除了以下值评估为false:字符串false本身,0-0,空字符串,nullundefinedNaN(不是一个数字,计算机工程中用于非法浮点操作的结果,例如除以零)。

请注意,我在 JavaScript 中引用的truefalse都是小写。这是因为,与 PHP 不同,这些值必须是小写的。因此,仅以下两个语句的第一个将会显示,打印小写单词true,因为第二个将导致'TRUE'未定义错误:

if (1 == true) document.write('true') // True
if (1 == TRUE) document.write('TRUE') // Will cause an error
注意

请记住,任何您希望在 HTML 文件中键入并尝试的代码片段都需要包含在<script></script>标签中。

文字和变量

表达式的最简单形式是文字,这意味着它评估为自身,例如数字22或字符串Press Enter。表达式也可以是一个变量,它评估为已分配给它的值。它们都是表达式的类型,因为它们返回一个值。

示例 15-2 展示了三种不同的文字和两个变量,所有这些都返回值,尽管类型不同。

示例 15-2. 五种文字类型
<script>
  myname = "Peter"
  myage  = 24
  document.write("a: " + 42     + "<br>") // Numeric literal
  document.write("b: " + "Hi"   + "<br>") // String literal
  document.write("c: " + true   + "<br>") // Constant literal
  document.write("d: " + myname + "<br>") // String variable
  document.write("e: " + myage  + "<br>") // Numeric variable
</script>

而且,正如您所期望的那样,您将在以下输出中看到所有这些的返回值:

a: 42
b: Hi
c: true
d: Peter
e: 24

运算符允许您创建更复杂的表达式,这些表达式评估为有用的结果。当您将赋值或控制流构造与表达式结合时,结果是一个语句

示例 15-3 展示了其中的一个例子。第一个将表达式 366 - day_number 的结果赋给变量 days_to_new_year,第二个仅在表达式 days_to_new_year < 30 评估为 true 时输出友好消息。

示例 15-3. 两个简单的 JavaScript 语句
<script>
  day_number       = 127  // For example
  days_to_new_year = 366 - day_number
  if (days_to_new_year < 30) document.write("It's nearly New Year")
  else                       document.write("It's a long time to go")
</script>

运算符

JavaScript 提供了许多强大的运算符,从算术、字符串和逻辑运算符到赋值、比较等(参见表 15-1)。

表 15-1. JavaScript 运算符类型

运算符描述示例
算术基本数学a + b
数组数组操作a + b
赋值赋值a = b + 23
位运算在字节内操作位12 ^ 9
比较比较两个值a < b
递增/递减加或减一a++
逻辑布尔a && b
字符串连接a + 'string'

每个运算符接受不同数量的操作数:

  • 一元 运算符,如递增(a++)或否定(-a),只接受一个操作数。

  • 二元 运算符占据了 JavaScript 运算符的大部分,包括加法、减法、乘法和除法,需要两个操作数。

  • 三元 运算符,形式为 ? x : y,需要三个操作数。它是一个简洁的单行 if 语句,根据第三个表达式选择两个表达式中的一个。

运算符优先级

像 PHP 一样,JavaScript 使用运算符优先级,其中表达式中的某些运算符先处理,因此首先进行评估。表 15-2 列出了 JavaScript 的运算符及其优先级。

表 15-2. JavaScript 运算符优先级(从高到低)

运算符类型
() [] .括号、调用和成员
++ --递增/递减
+ - ~ !一元、位和逻辑
* / %算术
+ -算术和字符串
<< >> >>>位运算
< > <= >=比较
== != === !==比较
& ^ &#124;位运算
&&逻辑
&#124;&#124;逻辑
? :三元
= += -= *= /= %=赋值
<<= >>= >>>= &= ^= &#124;=赋值
,分隔符

结合性

大多数 JavaScript 运算符在等式中按照从左到右的顺序处理。但有些运算符需要从右到左进行处理。处理方向称为运算符的结合性

当你没有明确强制运算优先级时,这种结合性变得很重要(顺便说一句,你应该总是这样做,因为它使代码更易读,减少错误)。例如,看看以下赋值运算符,这三个变量都被设置为值0

level = score = time = 0

仅当表达式的最右部分首先进行评估,然后以从右到左的顺序继续处理时,才能进行多重赋值。表 15-3](#operators_and_associativity)列出了 JavaScript 运算符及其结合性。

表 15-3. 运算符及其结合性

运算符描述结合性
++ --增减
new创建一个新对象
+ - ~ !一元和位运算
?:三元
= *= /= %= += -=赋值
<<= >>= >>>= &= ^= &#124;=赋值
,分隔符
+ - * / %算术
<< >> >>>位运算
< <= > >= == != === !==算术

关系运算符

关系运算符测试两个操作数并返回布尔结果truefalse。有三种类型的关系运算符:等式比较逻辑

相等运算符

等号运算符是==(不要与赋值运算符=混淆)。在示例 15-4 中,第一条语句赋值,第二条语句测试其是否相等。目前不会输出任何内容,因为month被赋予了字符串值July,因此检查其是否为October的条件将失败。

示例 15-4. 赋值和相等测试
<script>
  month = "July"
  if (month == "October") document.write("It's the Fall")
</script>

如果相等表达式的两个操作数类型不同,JavaScript 将它们转换为最合理的类型。例如,任何完全由数字组成的字符串与数字比较时将被转换为数字。在示例 15-5 中,ab 是两个不同的值(一个是数字,另一个是字符串),因此我们通常不希望任何if语句输出结果。

示例 15-5. 相等和恒等运算符
<script>
  a = 3.1415927
  b = "3.1415927"
  if (a == b)  document.write("1")
  if (a === b) document.write("2")
</script>

然而,如果运行此示例,您将看到它输出数字1,这意味着第一个if语句评估为true。这是因为b的字符串值临时转换为数字,因此等式两侧都有数值3.1415927

相比之下,第二个if语句使用恒等运算符,即连续三个等号,这会阻止 JavaScript 自动转换类型。因此,发现ab是不同的,因此不会输出任何内容。

与强制运算符优先级类似,每当您对 JavaScript 如何转换操作数类型感到困惑时,可以使用恒等运算符关闭此行为。

比较运算符

使用比较运算符,你不仅可以测试相等和不等,JavaScript 还为你提供了>(大于)、<(小于)、>=(大于或等于)和<=(小于或等于)进行使用。示例 15-6 展示了这些运算符的使用。

示例 15-6. 四个比较运算符
<script>
  a = 7; b = 11
  if (a > b)  document.write("a is greater than b<br>")
  if (a < b)  document.write("a is less than b<br>")
  if (a >= b) document.write("a is greater than or equal to b<br>")
  if (a <= b) document.write("a is less than or equal to b<br>")
</script>

在这个例子中,当a7b11时,输出如下(因为 7 小于 11 且也小于等于 11):

a is less than b
a is less than or equal to b

逻辑运算符

逻辑运算符会产生truefalse结果,也被称为布尔运算符。JavaScript 中有三种逻辑运算符(见表 15-4)。

表 15-4. JavaScript 的逻辑运算符

逻辑运算符描述
&& (and)如果两个操作数都为true,则返回true
&#124;&#124; (or)如果任一操作数为true,则返回true
! (not)如果操作数为false,则返回true;如果操作数为true,则返回false

你可以看到这些如何在示例 15-7 中使用,该示例输出01true

示例 15-7. 逻辑运算符的使用
<script>
  a = 1; b = 0
  document.write((a && b) + "<br>")
  document.write((a || b) + "<br>")
  document.write((  !b  ) + "<br>")
</script>

&&语句要求两个操作数都为true才会返回true||语句在任一值为true时会返回true,第三个语句对b的值执行NOT操作,将其从0转换为true

||运算符可能会引起意外问题,因为如果第一个操作数被评估为true,则第二个操作数将不会被评估。在示例 15-8 中,如果finished的值为1,则getnext函数将永远不会被调用(这些仅为示例,getnext的具体行为与此解释无关——只需将其视为在调用时执行某些操作的函数)。

示例 15-8. 使用||运算符的语句
<script>
  if (finished == 1 || getnext() == 1) done = 1
</script>

如果你需要在每个if语句中调用getnext,你应该按照示例 15-9 重新编写代码。

示例 15-9. 修改后的if...or语句以确保调用getnext
<script>
  gn = getnext()
  if (finished == 1 OR gn == 1) done = 1;
</script>

在这种情况下,在if语句之前,将执行getnext函数的代码,并将其返回值存储在gn中。

表 15-5 展示了使用逻辑运算符的所有可能变体。你还应该注意,!true等于false!false等于true

表 15-5. 所有可能的逻辑表达式

输入运算符及结果
ab&&&#124;&#124;
------------
truetruetruetrue
truefalsefalsetrue
falsetruefalsetrue
falsefalsefalsefalse

with语句

with语句不是您在早期关于 PHP 的章节中看到的语句,因为它是 JavaScript 专用的,并且是您需要了解但不应使用的语句(见???)。使用它,您可以通过减少对一个对象的多次引用来简化某些类型的 JavaScript 语句,将with块内的属性和方法引用视为适用于该对象。

例如,看看示例 15-10 中的代码,其中document.write函数从未直接引用string变量名。

示例 15-10. 使用with语句
<script>
  string = "The quick brown fox jumps over the lazy dog"

  with (string)
  {
    document.write("The string is " + length + " characters<br>")
    document.write("In upper case it's: " + toUpperCase())
  }
</script>

即使string从未被document.write直接引用,此代码仍然成功输出以下内容:

The string is 43 characters
In upper case it's: THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG

代码工作方式如下:JavaScript 解释器识别出length属性和toUpperCase方法必须应用于某个对象。因为它们单独存在,解释器假定它们适用于您在with语句中指定的string对象。

注意

在 ECMAScript 5 严格模式中,不再推荐使用with,并且现已禁止。推荐的替代方法是将要访问其属性的对象分配给临时变量。请注意这一点,以便在看到其他人的代码中使用时进行更新(如果需要),但不要自己使用。

使用onerror

使用onerror事件或trycatch关键字的组合,可以捕获 JavaScript 错误并自行处理。

事件是 JavaScript 可以检测到的操作。网页上的每个元素都有特定的事件,可以触发 JavaScript 函数。例如,按钮元素的onclick事件可以设置为调用一个函数,并使其在用户点击按钮时运行。

示例 15-11 说明了如何使用onerror事件。

示例 15-11. 使用onerror事件的脚本
<script>
  onerror = errorHandler
  document.writ("Welcome to this website") // Deliberate error

  function errorHandler(message, url, line)
  {
    out  = "Sorry, an error was encountered.\n\n";
    out += "Error: " + message + "\n";
    out += "URL: "   + url     + "\n";
    out += "Line: "  + line    + "\n\n";
    out += "Click OK to continue.\n\n";
    alert(out);
    return true;
  }
</script>

此脚本的第一行告诉错误事件从现在开始使用新的errorHandler函数。此函数接受三个参数——messageurlline编号——因此可以简单地在警报弹出窗口中显示所有这些内容。

然后,为了测试新函数,我们故意在代码中引入一个语法错误,调用document.writ而不是document.write(最后一个e丢失)。图 15-1 展示了在浏览器中运行此脚本的结果。在调试过程中,使用onerror这种方式也非常有用。

使用onerror事件与警报方法弹出窗口

图 15-1. 使用带有警报方法弹出窗口的onerror事件

使用 try...catch

trycatch关键字比前一节中展示的onerror技术更加标准和灵活。这些关键字允许您捕获代码中特定部分的错误,而不是文档中所有脚本的错误。然而,它们无法捕获语法错误,这种错误需要使用onerror

try...catch结构被所有主要浏览器支持,当您希望捕获特定部分代码中可能发生的某个条件时,它非常方便。

例如,在第十八章中,我们将探讨利用XMLHttpRequest对象的 Ajax 技术。因此,我们可以使用trycatch来捕捉这种情况,并在函数不可用时执行其他操作。示例 15-12 展示了如何操作。

示例 15-12. 使用trycatch捕获错误
<script>
  try
  {
    request = new XMLHTTPRequest()
  }
  catch(err)
  {
    // Use a different method to create an XMLHTTPRequest object
  }
</script>

trycatch之外,还有一个与之关联的关键字叫做finally,无论在try子句中是否发生错误,它都会始终被执行。要使用它,只需在catch语句后添加如下语句:

finally
{
  alert("The 'try' clause was encountered")
}

条件语句

条件语句可以改变程序流程。它们使您能够针对某些事物提出问题,并根据所得到的答案以不同方式做出响应。非循环条件语句有三种类型:if语句,switch语句和?运算符。

if语句

本章中的几个示例已经使用了if语句。只有在给定表达式评估为true时,才会执行此类语句中的代码。多行if语句需要用大括号括起来,但与 PHP 一样,对于单个语句,您可以省略大括号,尽管在编写代码时,特别是在if语句内部的操作数量可能会随着开发的进行而改变时,使用大括号通常是一个好习惯。因此,以下语句是有效的:

if (a > 100)
{
  b=2
  document.write("a is greater than 100")
}

if (b == 10) document.write("b is equal to 10")

else语句

当条件未被满足时,您可以使用else语句执行替代操作,如下所示:

if (a > 100)
{
  document.write("a is greater than 100")
}
else
{
  document.write("a is less than or equal to 100")
}

不像 PHP,JavaScript 没有elseif语句,但这并不是问题,因为您可以使用else后面再跟一个if来实现类似于elseif语句的效果,如下所示:

if (a > 100)
{
  document.write("a is greater than 100")
}
else if(a < 100)
{
  document.write("a is less than 100")
}
else
{
  document.write("a is equal to 100")
}

正如您所见,您可以在新的if后面再使用一个else,同样可以跟随另一个if语句,以此类推。虽然我在这些语句中使用了大括号,因为每个语句都是单行的,前面的示例也可以写成如下形式:

if     (a > 100) document.write("a is greater than 100")
else if(a < 100) document.write("a is less than 100")
else             document.write("a is equal to 100")

switch语句

当一个变量或表达式的结果可以有多个值时,switch语句非常有用,您可以针对每个值执行不同的函数。

例如,下面的代码将我们在第四章中组合的 PHP 菜单系统转换为 JavaScript。它通过根据用户的输入传递一个字符串给主菜单代码来工作,并将变量page设置为其中之一:Home、About、News、Login 和 Links。

使用if...else if...编写的代码看起来可能类似于示例 15-13。

示例 15-13. 多行的if...else if...语句
<script>
  if      (page == "Home")  document.write("You selected Home")
  else if (page == "About") document.write("You selected About")
  else if (page == "News")  document.write("You selected News")
  else if (page == "Login") document.write("You selected Login")
  else if (page == "Links") document.write("You selected Links")
</script>

使用switch结构,代码可能看起来像示例 15-14。

示例 15-14. 一个switch结构
<script>
  switch (page)
  {
    case "Home":
      document.write("You selected Home")
      break
    case "About":
      document.write("You selected About")
      break
    case "News":
      document.write("You selected News")
      break
    case "Login":
      document.write("You selected Login")
      break
    case "Links":
      document.write("You selected Links")
      break
  }
</script>

变量page仅在switch语句的开头提到一次。此后,case命令检查匹配项。当匹配发生时,执行相应的条件语句。当然,一个真实的程序将在这里有代码来显示或跳转到一个页面,而不仅仅是告诉用户选择了什么。

注意

你也可以为单个操作提供多个情况。例如:

switch (heroName)
{
  case "Superman":
  case "Batman":
  case "Wonder Woman":
    document.write("Justice League")
    break
  case "Iron Man":
  case "Captain America":
  case "Spiderman":
    document.write("The Avengers")
    break
}

跳出

正如你在示例 15-14 中所看到的,与 PHP 一样,break命令允许你的代码在满足条件后跳出switch语句。请记住包含break,除非你希望继续执行下一个case下的语句。

默认操作

当没有任何条件被满足时,你可以使用default关键字为switch语句指定默认操作。示例 15-15 展示了一个可以插入到示例 15-14 中的代码片段。

示例 15-15. 添加到示例 15-14 的默认语句
default:
  document.write("Unrecognized selection")
  break

?操作符

ternary操作符(?),结合:字符,提供了一种快速进行if...else测试的方式。你可以编写一个表达式进行评估,然后跟随一个?符号和在表达式为true时要执行的代码。之后,放置一个:和在表达式评估为false时要执行的代码。

示例 15-16 展示了使用三元操作符来打印变量a是否小于或等于 5,并在任何情况下打印出一些内容。

示例 15-16. 使用三元操作符的例子
<script>
  document.write(
    a <= 5 ?
    "a is less than or equal to 5" :
    "a is greater than 5"
  )
</script>

为了清晰起见,该语句已被分成多行,但你更可能在单行中使用这样的语句,如下所示:

size = a <= 5 ? "short" : "long"

循环

当涉及到循环时,JavaScript 和 PHP 之间有许多相似之处。两种语言都支持whiledo...whilefor循环。

while 循环

JavaScript 的while循环首先检查表达式的值,并仅在该表达式为true时开始执行循环内的语句。如果为false,执行将跳过到下一个 JavaScript 语句(如果有)。

在完成循环的迭代后,表达式再次被测试以查看它是否为true,并且进程将继续直到表达式评估为false或执行被另行停止。示例 15-17 展示了这样的循环。

示例 15-17. while循环
<script>
  counter=0

  `while` `(``counter` `<` `5``)`
  `{`
    document.write("Counter: " + counter + "<br>")
    ++counter
  `}`
</script>

此脚本输出以下内容:

Counter: 0
Counter: 1
Counter: 2
Counter: 3
Counter: 4
警告

如果在循环内部不增加变量counter,某些浏览器可能会因为无限循环而变得无响应,甚至可能无法通过 Esc 键或停止按钮轻松终止页面。所以,请小心处理你的 JavaScript 循环。

do...while循环

当你需要在进行任何测试之前至少执行一次循环时,请使用do...while循环,它类似于while循环,只是测试表达式仅在每次循环迭代后检查。因此,要输出 7 的乘法表的前七个结果,你可以使用类似于示例 15-18 中的代码。

示例 15-18. do...while循环
<script>
  count = 1

  `do`
  `{`
    document.write(count + " times 7 is " + count * 7 + "<br>")
  `}` `while` `(``++``count` `<=` `7``)`
</script>

正如你所预料的,此循环输出以下内容:

1 times 7 is 7
2 times 7 is 14
3 times 7 is 21
4 times 7 is 28
5 times 7 is 35
6 times 7 is 42
7 times 7 is 49

for循环

for循环将所有优点结合到单个循环结构中,允许你为每个语句传递三个参数:

  • 一个初始化表达式

  • 一个条件表达式

  • 一个修改表达式

这些由分号分隔,如下所示:for (expr1 ; expr2 ; expr3)。初始化表达式在循环的第一次迭代开始时执行。对于用于输出 7 的乘法表的代码,count将被初始化为值1。然后,每次循环时,条件表达式(在本例中为count <= 7)被测试,只有当条件为true时才进入循环。最后,在每次迭代结束时,修改表达式被执行。对于用于输出 7 的乘法表的代码,变量count被递增。示例 15-19 展示了代码的样子。

示例 15-19. 使用for循环
<script>
  `for` `(``count` `=` `1` `;` `count` `<=` `7` `;` `++``count``)`
  `{`
    document.write(count + "times 7 is " + count * 7 + "<br>");
  `}`
</script>

就像在 PHP 中一样,你可以在for循环的第一个参数中使用逗号分隔多个变量,如下所示:

for (i = 1, j = 1 ; i < 10 ; i++)

同样,你可以在最后一个参数中执行多个修改,如下所示:

for (i = 1 ; i < 10 ; i++, --j)

或者你可以同时执行两者:

for (i = 1, j = 1 ; i < 10 ; i++, --j)

退出循环

break命令,你可能还记得在switch语句内部很重要,在for循环中也是可用的。例如,当搜索某种匹配时可能需要使用此功能。一旦找到匹配,你就知道继续搜索只会浪费时间并让访问者等待。示例 15-20 展示了如何使用break命令。

Example 15-20. 在for循环中使用break命令
<script>
  haystack     = new Array()
  haystack[17] = "Needle"

  for (j = 0 ; j < 20 ; ++j)
  {
    if (haystack[j] == "Needle")
    {
      document.write("<br>- Found at location " + j)
      `break`
    }
    else document.write(j + ", ")
  }
</script>

此脚本输出以下内容:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
- Found at location 17

continue 语句

有时,你不想完全退出循环,而是希望跳过本次循环中的其余语句。在这种情况下,你可以使用 continue 命令。示例 15-21 展示了其使用方法。

示例 15-21. 在 for 循环中使用 continue 命令
<script>
  haystack     = new Array()
  haystack[4]  = "Needle"
  haystack[11] = "Needle"
  haystack[17] = "Needle"

  for (j = 0 ; j < 20 ; ++j)
  {
    if (haystack[j] == "Needle")
    {
      document.write("<br>- Found at location " + j + "<br>")
      `continue`
    }

    document.write(j + ", ")
  }
</script>

请注意第二个 document.write 调用不必像以前那样被包含在 else 语句中,因为 continue 命令将在找到匹配项时跳过它。该脚本的输出如下:

0, 1, 2, 3,
- Found at location 4
5, 6, 7, 8, 9, 10,
- Found at location 11
12, 13, 14, 15, 16,
- Found at location 17
18, 19,

显式类型转换

不像 PHP,JavaScript 没有显式类型转换,比如 (int)(float)。相反,当你需要一个值是特定类型时,使用 JavaScript 内置的函数之一,如 表 15-6 中所示。

表 15-6. JavaScript 类型转换函数

类型更改使用的函数
Int, IntegerparseInt()
Bool, BooleanBoolean()
Float, Double, RealparseFloat()
StringString()
Arraysplit()

因此,例如,要将浮点数转换为整数,可以使用以下代码(显示值为 3):

n = 3.1415927
i = parseInt(n)
document.write(i)

或者你可以使用复合形式:

document.write(parseInt(3.1415927))

控制流和表达式就是这些内容。下一章将重点讲解 JavaScript 中函数、对象和数组的使用。

问题

  1. PHP 和 JavaScript 如何处理布尔值?

  2. JavaScript 变量名的定义要用到哪些字符?

  3. 一元、二元和三元操作符有什么区别?

  4. 如何强制自己的运算符优先级?

  5. 何时会使用 ===(恒等)运算符?

  6. 什么是最简单的两种表达式形式?

  7. 列举三种条件语句类型的名称。

  8. ifwhile 语句如何解释不同数据类型的条件表达式?

  9. 为什么 for 循环比 while 循环更强大?

  10. with 语句的目的是什么?

参见 “第十五章答案” ,在 附录 A 中找到这些问题的答案。