trpl-zh-cn/docs/ch08-03-hash-maps.html
2017-04-05 23:13:49 +08:00

243 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>哈希 map - Rust 程序设计语言 简体中文版</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="Rust 程序设计语言 简体中文版">
<meta name="viewport" content="width=device-width, initial-scale=1">
<base href="">
<link rel="stylesheet" href="book.css">
<link href='https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800' rel='stylesheet' type='text/css'>
<link rel="shortcut icon" href="favicon.png">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
<link rel="stylesheet" href="highlight.css">
<link rel="stylesheet" href="tomorrow-night.css">
<!-- MathJax -->
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<!-- Fetch JQuery from CDN but have a local fallback -->
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script>
if (typeof jQuery == 'undefined') {
document.write(unescape("%3Cscript src='jquery.js'%3E%3C/script%3E"));
}
</script>
</head>
<body class="light">
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme = localStorage.getItem('theme');
if (theme == null) { theme = 'light'; }
$('body').removeClass().addClass(theme);
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var sidebar = localStorage.getItem('sidebar');
if (sidebar === "hidden") { $("html").addClass("sidebar-hidden") }
else if (sidebar === "visible") { $("html").addClass("sidebar-visible") }
</script>
<div id="sidebar" class="sidebar">
<ul class="chapter"><li><a href="ch01-00-introduction.html"><strong>1.</strong> 介绍</a></li><li><ul class="section"><li><a href="ch01-01-installation.html"><strong>1.1.</strong> 安装</a></li><li><a href="ch01-02-hello-world.html"><strong>1.2.</strong> Hello, World!</a></li></ul></li><li><a href="ch02-00-guessing-game-tutorial.html"><strong>2.</strong> 猜猜看教程</a></li><li><a href="ch03-00-common-programming-concepts.html"><strong>3.</strong> 通用编程概念</a></li><li><ul class="section"><li><a href="ch03-01-variables-and-mutability.html"><strong>3.1.</strong> 变量和可变性</a></li><li><a href="ch03-02-data-types.html"><strong>3.2.</strong> 数据类型</a></li><li><a href="ch03-03-how-functions-work.html"><strong>3.3.</strong> 函数如何工作</a></li><li><a href="ch03-04-comments.html"><strong>3.4.</strong> 注释</a></li><li><a href="ch03-05-control-flow.html"><strong>3.5.</strong> 控制流</a></li></ul></li><li><a href="ch04-00-understanding-ownership.html"><strong>4.</strong> 认识所有权</a></li><li><ul class="section"><li><a href="ch04-01-what-is-ownership.html"><strong>4.1.</strong> 什么是所有权</a></li><li><a href="ch04-02-references-and-borrowing.html"><strong>4.2.</strong> 引用 &amp; 借用</a></li><li><a href="ch04-03-slices.html"><strong>4.3.</strong> Slices</a></li></ul></li><li><a href="ch05-00-structs.html"><strong>5.</strong> 结构体</a></li><li><ul class="section"><li><a href="ch05-01-method-syntax.html"><strong>5.1.</strong> 方法语法</a></li></ul></li><li><a href="ch06-00-enums.html"><strong>6.</strong> 枚举和模式匹配</a></li><li><ul class="section"><li><a href="ch06-01-defining-an-enum.html"><strong>6.1.</strong> 定义枚举</a></li><li><a href="ch06-02-match.html"><strong>6.2.</strong> <code>match</code>控制流运算符</a></li><li><a href="ch06-03-if-let.html"><strong>6.3.</strong> <code>if let</code>简单控制流</a></li></ul></li><li><a href="ch07-00-modules.html"><strong>7.</strong> 模块</a></li><li><ul class="section"><li><a href="ch07-01-mod-and-the-filesystem.html"><strong>7.1.</strong> <code>mod</code>和文件系统</a></li><li><a href="ch07-02-controlling-visibility-with-pub.html"><strong>7.2.</strong> 使用<code>pub</code>控制可见性</a></li><li><a href="ch07-03-importing-names-with-use.html"><strong>7.3.</strong> 使用<code>use</code>导入命名</a></li></ul></li><li><a href="ch08-00-common-collections.html"><strong>8.</strong> 通用集合类型</a></li><li><ul class="section"><li><a href="ch08-01-vectors.html"><strong>8.1.</strong> vector</a></li><li><a href="ch08-02-strings.html"><strong>8.2.</strong> 字符串</a></li><li><a href="ch08-03-hash-maps.html" class="active"><strong>8.3.</strong> 哈希 map</a></li></ul></li><li><a href="ch09-00-error-handling.html"><strong>9.</strong> 错误处理</a></li><li><ul class="section"><li><a href="ch09-01-unrecoverable-errors-with-panic.html"><strong>9.1.</strong> <code>panic!</code>与不可恢复的错误</a></li><li><a href="ch09-02-recoverable-errors-with-result.html"><strong>9.2.</strong> <code>Result</code>与可恢复的错误</a></li><li><a href="ch09-03-to-panic-or-not-to-panic.html"><strong>9.3.</strong> <code>panic!</code>还是不<code>panic!</code></a></li></ul></li><li><a href="ch10-00-generics.html"><strong>10.</strong> 泛型、trait 和生命周期</a></li><li><ul class="section"><li><a href="ch10-01-syntax.html"><strong>10.1.</strong> 泛型数据类型</a></li><li><a href="ch10-02-traits.html"><strong>10.2.</strong> trait定义共享的行为</a></li><li><a href="ch10-03-lifetime-syntax.html"><strong>10.3.</strong> 生命周期与引用有效性</a></li></ul></li><li><a href="ch11-00-testing.html"><strong>11.</strong> 测试</a></li><li><ul class="section"><li><a href="ch11-01-writing-tests.html"><strong>11.1.</strong> 编写测试</a></li><li><a href="ch11-02-running-tests.html"><strong>11.2.</strong> 运行测试</a></li><li><a href="ch11-03-test-organization.html"><strong>11.3.</strong> 测试的组织结构</a></li></ul></li><li><a href="ch12-00-an-io-project.html"><strong>12.</strong> 一个 I/O 项目</a></li><li><ul class="section"><li><a href="ch12-01-accepting-command-line-arguments.html"><strong>12.1.</strong> 接受命令行参数</a></li><li><a href="ch12-02-reading-a-file.html"><strong>12.2.</strong> 读取文件</a></li><li><a href="ch12-03-improving-error-handling-and-modularity.html"><strong>12.3.</strong> 增强错误处理和模块化</a></li><li><a href="ch12-04-testing-the-librarys-functionality.html"><strong>12.4.</strong> 测试库的功能</a></li><li><a href="ch12-05-working-with-environment-variables.html"><strong>12.5.</strong> 处理环境变量</a></li><li><a href="ch12-06-writing-to-stderr-instead-of-stdout.html"><strong>12.6.</strong> 输出到<code>stderr</code>而不是<code>stdout</code></a></li></ul></li><li><a href="ch13-00-functional-features.html"><strong>13.</strong> Rust 中的函数式语言功能</a></li><li><ul class="section"><li><a href="ch13-01-closures.html"><strong>13.1.</strong> 闭包</a></li><li><a href="ch13-02-iterators.html"><strong>13.2.</strong> 迭代器</a></li><li><a href="ch13-03-improving-our-io-project.html"><strong>13.3.</strong> 改进 I/O 项目</a></li><li><a href="ch13-04-performance.html"><strong>13.4.</strong> 性能</a></li></ul></li><li><a href="ch14-00-more-about-cargo.html"><strong>14.</strong> 更多关于 Cargo 和 Crates.io</a></li><li><ul class="section"><li><a href="ch14-01-release-profiles.html"><strong>14.1.</strong> 发布配置</a></li><li><a href="ch14-02-publishing-to-crates-io.html"><strong>14.2.</strong> 将 crate 发布到 Crates.io</a></li><li><a href="ch14-03-cargo-workspaces.html"><strong>14.3.</strong> Cargo 工作空间</a></li><li><a href="ch14-04-installing-binaries.html"><strong>14.4.</strong> 使用<code>cargo install</code>从 Crates.io 安装文件</a></li><li><a href="ch14-05-extending-cargo.html"><strong>14.5.</strong> Cargo 自定义扩展命令</a></li></ul></li><li><a href="ch15-00-smart-pointers.html"><strong>15.</strong> 智能指针</a></li><li><ul class="section"><li><a href="ch15-01-box.html"><strong>15.1.</strong> <code>Box&lt;T&gt;</code>用于已知大小的堆上数据</a></li><li><a href="ch15-02-deref.html"><strong>15.2.</strong> <code>Deref</code> Trait 允许通过引用访问数据</a></li><li><a href="ch15-03-drop.html"><strong>15.3.</strong> <code>Drop</code> Trait 运行清理代码</a></li><li><a href="ch15-04-rc.html"><strong>15.4.</strong> <code>Rc&lt;T&gt;</code> 引用计数智能指针</a></li><li><a href="ch15-05-interior-mutability.html"><strong>15.5.</strong> <code>RefCell&lt;T&gt;</code>和内部可变性模式</a></li><li><a href="ch15-06-reference-cycles.html"><strong>15.6.</strong> 引用循环和内存泄漏是安全的</a></li></ul></li><li><a href="ch16-00-concurrency.html"><strong>16.</strong> 无畏并发</a></li><li><ul class="section"><li><a href="ch16-01-threads.html"><strong>16.1.</strong> 线程</a></li><li><a href="ch16-02-message-passing.html"><strong>16.2.</strong> 消息传递</a></li><li><a href="ch16-03-shared-state.html"><strong>16.3.</strong> 共享状态</a></li><li><a href="ch16-04-extensible-concurrency-sync-and-send.html"><strong>16.4.</strong> 可扩展的并发:<code>Sync</code><code>Send</code></a></li></ul></li><li><a href="ch17-00-oop.html"><strong>17.</strong> 面向对象</a></li><li><ul class="section"><li><a href="ch17-01-what-is-oo.html"><strong>17.1.</strong> 什么是面向对象</a></li></ul></li></ul>
</div>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar" class="menu-bar">
<div class="left-buttons">
<i id="sidebar-toggle" class="fa fa-bars"></i>
<i id="theme-toggle" class="fa fa-paint-brush"></i>
</div>
<h1 class="menu-title">Rust 程序设计语言 简体中文版</h1>
<div class="right-buttons">
<i id="print-button" class="fa fa-print" title="Print this book"></i>
</div>
</div>
<div id="content" class="content">
<a class="header" href="#哈希-map" name="哈希-map"><h2>哈希 map</h2></a>
<blockquote>
<p><a href="https://github.com/rust-lang/book/blob/master/second-edition/src/ch08-03-hash-maps.md">ch08-03-hash-maps.md</a>
<br>
commit 4f2dc564851dc04b271a2260c834643dfd86c724</p>
</blockquote>
<p>最后要介绍的常用集合类型是<strong>哈希 map</strong><em>hash map</em>)。<code>HashMap&lt;K, V&gt;</code>类型储存了一个键类型<code>K</code>对应一个值类型<code>V</code>的映射。它通过一个<strong>哈希函数</strong><em>hashing function</em>来实现映射它决定了如何将键和值放入内存中。很多编程语言支持这种数据结构不过通常有不同的名字哈希、map、对象、哈希表或者关联数组仅举几例。</p>
<p>哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到他们的得分。</p>
<p>本章我们会介绍哈希 map 的基本 API不过还有更多吸引人的功能隐藏于标准库中的<code>HashMap</code>定义的函数中。请一如既往地查看标准库文档来了解更多信息。</p>
<a class="header" href="#新建一个哈希-map" name="新建一个哈希-map"><h3>新建一个哈希 map</h3></a>
<p>可以使用<code>new</code>创建一个空的<code>HashMap</code>,并使用<code>insert</code>来增加元素。这里我们记录两支队伍的分数,分别是蓝队和黄队。蓝队开始有 10 分而黄队开始有 50 分:</p>
<pre><code class="language-rust">use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from(&quot;Blue&quot;), 10);
scores.insert(String::from(&quot;Yellow&quot;), 50);
</code></pre>
<p>注意必须首先<code>use</code>标准库中集合部分的<code>HashMap</code>。在这三个常用集合中,这个是最不常用的,所以并不包含在被 prelude 自动引用的功能中。标准库中对哈希 map 的支持也相对较少;例如,并没有内建的用于构建的宏。</p>
<p>就像 vector 一样,哈希 map 将他们的数据储存在堆上。这个<code>HashMap</code>的键类型是<code>String</code>而值类型是<code>i32</code>。同样类似于 vector哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。</p>
<p>另一个构建哈希 map 的方法是使用一个元组的 vector 的<code>collect</code>方法,其中每个元组包含一个键值对。<code>collect</code>方法可以将数据收集进一系列的集合类型,包括<code>HashMap</code>。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用<code>zip</code>方法来创建一个元组的 vector其中“Blue”与 10 是一对,依此类推。接着就可以使用<code>collect</code>方法将这个元组 vector 转换成一个<code>HashMap</code></p>
<pre><code class="language-rust">use std::collections::HashMap;
let teams = vec![String::from(&quot;Blue&quot;), String::from(&quot;Yellow&quot;)];
let initial_scores = vec![10, 50];
let scores: HashMap&lt;_, _&gt; = teams.iter().zip(initial_scores.iter()).collect();
</code></pre>
<p>这里<code>HashMap&lt;_, _&gt;</code>类型注解是必要的,因为可能<code>collect</code>进很多不同的数据结构,而除非显式指定 Rust 无从得知你需要的类型。但是对于键和值的参数来说,可以使用下划线而 Rust 可以根据 vector 中数据的类型推断出哈希 map 所包含的类型。</p>
<a class="header" href="#哈希-map-和所有权" name="哈希-map-和所有权"><h3>哈希 map 和所有权</h3></a>
<p>对于像<code>i32</code>这样的实现了<code>Copy</code> trait 的类型,其值可以拷贝进哈希 map。对于像<code>String</code>这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者:</p>
<pre><code class="language-rust">use std::collections::HashMap;
let field_name = String::from(&quot;Favorite color&quot;);
let field_value = String::from(&quot;Blue&quot;);
let mut map = HashMap::new();
map.insert(field_name, field_value);
// field_name and field_value are invalid at this point
</code></pre>
<p><code>insert</code>调用将<code>field_name</code><code>field_value</code>移动到哈希 map 中后,将不能使用这两个绑定。</p>
<p>如果将值的引用插入哈希 map这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。第十章生命周期部分将会更多的讨论这个问题。</p>
<a class="header" href="#访问哈希-map-中的值" name="访问哈希-map-中的值"><h3>访问哈希 map 中的值</h3></a>
<p>可以通过<code>get</code>方法并提供对应的键来从哈希 map 中获取值:</p>
<pre><code class="language-rust">use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from(&quot;Blue&quot;), 10);
scores.insert(String::from(&quot;Yellow&quot;), 50);
let team_name = String::from(&quot;Blue&quot;);
let score = scores.get(&amp;team_name);
</code></pre>
<p>这里,<code>score</code>将会是与蓝队分数相关的值,而这个值将是<code>Some(10)</code>。因为<code>get</code>返回<code>Option&lt;V&gt;</code>所以结果被封装进<code>Some</code>;如果某个键在哈希 map 中没有对应的值,<code>get</code>会返回<code>None</code>。程序将需要采用第六章提到的方法中之一来处理<code>Option</code></p>
<p>可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是<code>for</code>循环:</p>
<pre><code class="language-rust">use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from(&quot;Blue&quot;), 10);
scores.insert(String::from(&quot;Yellow&quot;), 50);
for (key, value) in &amp;scores {
println!(&quot;{}: {}&quot;, key, value);
}
</code></pre>
<p>这会以任意顺序打印出每一个键值对:</p>
<pre><code>Yellow: 50
Blue: 10
</code></pre>
<a class="header" href="#更新哈希-map" name="更新哈希-map"><h3>更新哈希 map</h3></a>
<p>虽然键值对的数量是可以增长的,不过每个单独的键同时只能关联一个值。当你想要改变哈希 map 中的数据时,必须选择是用新值替代旧值,还是完全无视旧值。我们也可以选择保留旧值而忽略新值,并只在键<strong>没有</strong>对应一个值时增加新值。或者可以结合新值和旧值。让我们看看着每一种方式是如何工作的!</p>
<a class="header" href="#覆盖一个值" name="覆盖一个值"><h4>覆盖一个值</h4></a>
<p>如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。即便下面的代码调用了两次<code>insert</code>,哈希 map 也只会包含一个键值对,因为两次都是对蓝队的键插入的值:</p>
<pre><code class="language-rust">use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from(&quot;Blue&quot;), 10);
scores.insert(String::from(&quot;Blue&quot;), 25);
println!(&quot;{:?}&quot;, scores);
</code></pre>
<p>这会打印出<code>{&quot;Blue&quot;: 25}</code>。原始的值 10 将被覆盖。</p>
<a class="header" href="#只在键没有对应值时插入" name="只在键没有对应值时插入"><h4>只在键没有对应值时插入</h4></a>
<p>我们经常会检查某个特定的键是否有值,如果没有就插入一个值。为此哈希 map 有一个特有的 API叫做<code>entry</code>,它获取我们想要检查的键作为参数。<code>entry</code>函数的返回值是一个枚举,<code>Entry</code>,它代表了可能存在也可能不存在的值。比如说我们想要检查黄队的键是否关联了一个值。如果没有,就插入值 50对于蓝队也是如此。使用 entry API 的代码看起来像这样:</p>
<pre><code class="language-rust">use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from(&quot;Blue&quot;), 10);
scores.entry(String::from(&quot;Yellow&quot;)).or_insert(50);
scores.entry(String::from(&quot;Blue&quot;)).or_insert(50);
println!(&quot;{:?}&quot;, scores);
</code></pre>
<p><code>Entry</code><code>or_insert</code>方法在键对应的值存在时就返回这个值的<code>Entry</code>,如果不存在则将参数作为新值插入并返回修改过的<code>Entry</code>。这比编写自己的逻辑要简明的多,另外也与借用检查器结合得更好。</p>
<p>这段代码会打印出<code>{&quot;Yellow&quot;: 50, &quot;Blue&quot;: 10}</code>。第一个<code>entry</code>调用会插入黄队的键和值 50因为黄队并没有一个值。第二个<code>entry</code>调用不会改变哈希 map 因为蓝队已经有了值 10。</p>
<a class="header" href="#根据旧值更新一个值" name="根据旧值更新一个值"><h4>根据旧值更新一个值</h4></a>
<p>另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。例如,如果我们想要计数一些文本中每一个单词分别出现了多少次,就可以使用哈希 map以单词作为键并递增其值来记录我们遇到过几次这个单词。如果是第一次看到某个单词就插入值<code>0</code></p>
<pre><code class="language-rust">use std::collections::HashMap;
let text = &quot;hello world wonderful world&quot;;
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!(&quot;{:?}&quot;, map);
</code></pre>
<p>这会打印出<code>{&quot;world&quot;: 2, &quot;hello&quot;: 1, &quot;wonderful&quot;: 1}</code><code>or_insert</code>方法事实上会返回这个键的值的一个可变引用(<code>&amp;mut V</code>)。这里我们将这个可变引用储存在<code>count</code>变量中,所以为了赋值必须首先使用星号(<code>*</code>)解引用<code>count</code>。这个可变引用在<code>for</code>循环的结尾离开作用域,这样所有这些改变都是安全的并被借用规则所允许。</p>
<a class="header" href="#哈希函数" name="哈希函数"><h3>哈希函数</h3></a>
<p><code>HashMap</code>默认使用一个密码学上是安全的哈希函数它可以提供抵抗拒绝服务Denial of Service, DoS攻击的能力。这并不是现有最快的哈希函数不过为了更好的安全性带来一些性能下降也是值得的。如果你监控你的代码并发现默认哈希函数对你来说非常慢可以通过指定一个不同的 <em>hasher</em> 来切换为另一个函数。hasher 是一个实现了<code>BuildHasher</code> trait 的类型。第十章会讨论 trait 和如何实现他们。你并不需要从头开始实现你自己的 hashercrates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。</p>
<a class="header" href="#总结" name="总结"><h2>总结</h2></a>
<p>vector、字符串和哈希 map 会在你的程序需要储存、访问和修改数据时帮助你。这里有一些你应该能够解决的练习问题:</p>
<ul>
<li>给定一系列数字,使用 vector 并返回这个列表的平均数mean, average、中位数排列数组后位于中间的值和众数mode出现次数最多的值这里哈希函数会很有帮助</li>
<li>将字符串转换为 Pig Latin也就是每一个单词的第一个辅音字母被移动到单词的结尾并增加“ay”所以“first”会变成“irst-fay”。元音字母开头的单词则在结尾增加 “hay”“apple”会变成“apple-hay”。牢记 UTF-8 编码!</li>
<li>使用哈希 map 和 vector创建一个文本接口来允许用户向公司的部门中增加员工的名字。例如“Add Sally to Engineering”或“Add Amir to Sales”。接着让用户获取一个部门的所有员工的列表或者公司每个部门的所有员工按照字母顺排序的列表。</li>
</ul>
<p>标准库 API 文档中描述的这些类型的方法将有助于你进行这些练习!</p>
<p>我们已经开始接触可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机!</p>
</div>
<!-- Mobile navigation buttons -->
<a href="ch08-02-strings.html" class="mobile-nav-chapters previous">
<i class="fa fa-angle-left"></i>
</a>
<a href="ch09-00-error-handling.html" class="mobile-nav-chapters next">
<i class="fa fa-angle-right"></i>
</a>
</div>
<a href="ch08-02-strings.html" class="nav-chapters previous" title="You can navigate through the chapters using the arrow keys">
<i class="fa fa-angle-left"></i>
</a>
<a href="ch09-00-error-handling.html" class="nav-chapters next" title="You can navigate through the chapters using the arrow keys">
<i class="fa fa-angle-right"></i>
</a>
</div>
<!-- Local fallback for Font Awesome -->
<script>
if ($(".fa").css("font-family") !== "FontAwesome") {
$('<link rel="stylesheet" type="text/css" href="_FontAwesome/css/font-awesome.css">').prependTo('head');
}
</script>
<!-- Livereload script (if served using the cli tool) -->
<script src="highlight.js"></script>
<script src="book.js"></script>
</body>
</html>