Skip to content

《JavaScript全栈教程》09:浏览器与DOM操作——从window对象到Canvas绘图全解

约 5724 字大约 19 分钟

JavaScript全栈教程JavaScript

2026-04-12

本系列前几期我们深入了JavaScript语言核心,本期将带您进入浏览器环境,掌握如何用JS操作浏览器窗口、DOM树、表单、文件上传,并理解AJAX、Promise、async异步编程,最后还会解锁Canvas绘图。学完本篇,您将能独立编写交互丰富、数据驱动的现代Web页面。

本篇核心收获

  • 掌握浏览器核心对象:window、navigator、screen、location、document、history的属性和方法
  • 精通DOM节点的增删改查,用原生JS实现动态页面更新
  • 学会获取和设置表单控件的值,实现表单验证与安全提交
  • 理解文件上传与File API,实现图片本地预览
  • 掌握AJAX同源策略与CORS跨域原理,使用Fetch发送请求
  • 深入理解Promise与async/await,写出优雅的异步代码
  • 入门Canvas 2D绘图,完成天气预报图表绘制

一、浏览器环境与全局对象

JavaScript诞生之初就是为了在浏览器中运行,因此浏览器是JS开发者必须关注的平台。目前主流浏览器包括:

  • IE 6~11:国内使用广泛,对W3C标准支持较差。IE10开始支持ES6。
  • Chrome:Google出品,基于WebKit内核,内置强大的V8引擎。自动升级,最新版已支持ES6。
  • Safari:Apple Mac系统自带,基于WebKit。OS X 10.7 Lion起支持ES6,现版本9.x早已支持。
  • Firefox:Mozilla自研Gecko内核和OdinMonkey引擎。现已采用自动升级,保持最新。
  • 移动设备:iOS和Android分别使用Safari和Chrome,均为WebKit核心,对HTML5和ES6支持良好。

其他浏览器如Opera市场份额太小可忽略。国产浏览器(如某某安全浏览器)大多只是壳,内核实际调用IE或双核(IE+Webkit)。

不同浏览器对JS支持的差异主要体现在API接口(如AJAX、File接口)以及对ES6特性的支持度。编写JS时必须考虑这些差异,尽量让同一份代码运行在不同浏览器中。

1.1 window对象

window 对象既充当全局作用域,又表示浏览器窗口。它提供以下常用属性:

  • innerWidth / innerHeight:获取浏览器窗口内部的净宽高(除去菜单栏、工具栏、边框等占位元素后用于显示网页的区域)。
  • outerWidth / outerHeight:获取浏览器窗口的整个宽高(包括菜单栏、边框等)。
// 可以调整浏览器窗口大小试试:
console.log('window inner size: ' + window.innerWidth + ' x ' + window.innerHeight);

兼容性:IE <= 8 不支持 innerWidth/innerHeight

1.2 navigator对象

navigator 对象表示浏览器的信息,常用属性:

属性说明
navigator.appName浏览器名称
navigator.appVersion浏览器版本
navigator.language浏览器设置的语言
navigator.platform操作系统类型
navigator.userAgent浏览器设定的User-Agent字符串
console.log('appName = ' + navigator.appName);
console.log('appVersion = ' + navigator.appVersion);
console.log('language = ' + navigator.language);
console.log('platform = ' + navigator.platform);
console.log('userAgent = ' + navigator.userAgent);

⚠️ 避坑指南navigator 的信息可以被用户轻易修改,因此读取的值不一定准确。切勿if 判断浏览器版本来编写差异化代码。正确方法是利用JavaScript对不存在属性返回 undefined 的特性,用短路运算符 ||

let width = window.innerWidth || document.body.clientWidth;

1.3 screen对象

screen 对象表示屏幕的信息,常用属性:

  • screen.width:屏幕宽度(像素)
  • screen.height:屏幕高度(像素)
  • screen.colorDepth:颜色位数(如8、16、24)
console.log('Screen size = ' + screen.width + ' x ' + screen.height);

1.4 location对象

location 对象表示当前页面的URL信息。例如一个完整的URL:
http://www.example.com:8080/path/index.html?a=1&b=2#TOP

可以通过 location.href 获取完整URL,也可分别获取各部分:

location.protocol; // 'http:'
location.host;     // 'www.example.com'
location.port;     // '8080'
location.pathname; // '/path/index.html'
location.search;   // '?a=1&b=2'
location.hash;     // '#TOP'

要加载一个新页面,调用 location.assign();重新加载当前页面,调用 location.reload()

if (confirm('重新加载当前页' + location.href + '?')) {
    location.reload();
} else {
    location.assign('/'); // 设置一个新的URL地址
}

1.5 document对象

document 对象表示当前页面,是整个DOM树的根节点。常用操作:

  • 修改页面标题:document.title = '努力学习JavaScript!';
  • 查找DOM节点:getElementById()getElementsByTagName()getElementsByClassName()
  • 读取Cookie:document.cookie

示例:查找DOM节点

<dl id="drink-menu" style="border:solid 1px #ccc;padding:6px;">
    <dt>摩卡</dt>
    <dd>热摩卡咖啡</dd>
    <dt>酸奶</dt>
    <dd>北京老酸奶</dd>
    <dt>果汁</dt>
    <dd>鲜榨苹果汁</dd>
</dl>
let menu = document.getElementById('drink-menu');
let drinks = document.getElementsByTagName('dt');
let s = '提供的饮料有:';
for (let i = 0; i < drinks.length; i++) {
    s = s + drinks[i].innerHTML + ',';
}
console.log(s);

Cookie与安全

Cookie是服务器发送的key-value标识符,用于区分用户。JavaScript可通过 document.cookie 读取当前页面的Cookie:

document.cookie; // 'v=123; remember=true; prefer=zh'

⚠️ 安全隐患:页面中引入第三方JS(如 http://www.foo.com/jquery.js)时,恶意代码可读取本页Cookie,窃取用户登录信息。

解决方案:服务器设置Cookie时使用 httpOnly 选项,带此标记的Cookie将不能被JavaScript读取(主流浏览器均支持,IE6 SP1开始支持)。服务器端应始终坚持使用httpOnly

1.6 history对象

history 对象保存浏览器的历史记录,提供 back()forward() 方法,相当于点击浏览器的后退/前进按钮。

⚠️ 避坑指南:新手常在登录成功后调用 history.back() 试图回到登录前页面,这是一种错误做法。对于现代AJAX页面,应使用 history.pushState() 方法:

// AJAX完成后
let state = 'any-data';
let url = '/ajax.html#signin';
history.pushState(state, '', url);

当用户点击“后退”时,浏览器不会刷新页面,而是触发 popstate 事件,可由JavaScript捕获并更新页面部分内容。

参考文档:MDN上的Window、Navigator、Screen、Location、Document、History对象。

二、操作DOM

DOM(文档对象模型)是一棵树形结构。操作DOM本质上就是四种操作:更新遍历添加删除

2.1 获取DOM节点

最常用的方法:

方法说明返回值
document.getElementById(id)根据ID获取单个节点
document.getElementsByTagName(tag)根据标签名获取节点集合
document.getElementsByClassName(class)根据类名获取节点集合
document.querySelector(selector)根据CSS选择器获取第一个单个节点
document.querySelectorAll(selector)根据CSS选择器获取所有节点集合

示例

// 返回ID为'test'的节点:
let test = document.getElementById('test');

// 先定位ID为'test-table'的节点,再返回其内部所有tr节点:
let trs = document.getElementById('test-table').getElementsByTagName('tr');

// 先定位ID为'test-div'的节点,再返回其内部所有class包含red的节点:
let reds = document.getElementById('test-div').getElementsByClassName('red');

// 获取节点test下的所有直属子节点:
let cs = test.children;

// 获取节点test下第一个、最后一个子节点:
let first = test.firstElementChild;
let last = test.lastElementChild;

// 通过querySelector获取ID为q1的节点:
let q1 = document.querySelector('#q1');

// 通过querySelectorAll获取q1节点内的符合条件的所有节点:
let ps = q1.querySelectorAll('div.highlighted > p');

兼容性:低版本IE<8不支持 querySelectorquerySelectorAll;IE8仅有限支持。

注意:DOM节点严格来说是指Element,但实际还有Comment、CDATA等类型,我们最关心的是Element。根节点Document已自动绑定为全局变量 document

2.2 练习:选择指定条件的节点

给定HTML结构:

<div id="test-div">  
  <div class="c-red">  
    <p id="test-p">JavaScript</p>  
    <p>Java</p>  
  </div>  
  <div class="c-red c-green">  
    <p>Python</p>  
    <p>Ruby</p>  
    <p>Swift</p>  
  </div>  
  <div class="c-green">  
    <p>Scheme</p>  
    <p> Haskell</p>  
  </div>  
</div>

请选择出指定条件的节点:

// 选择<p>JavaScript</p>:
let js = document.getElementById('test-p');

// 选择<p>Python</p>,<p>Ruby</p>,<p>Swift</p>:
let arr = document.querySelectorAll('.c-red.c-green p');

// 选择<p>Haskell</p>:
let haskell = document.querySelector('.c-green p:last-of-type');

// 测试(略)

2.3 更新DOM

方法一:修改innerHTML
不仅可以修改文本内容,还可以直接插入HTML片段,重构子树。

let p = document.getElementById('p-id');
// 设置文本为abc:
p.innerHTML = 'ABC'; // <p id="p-id">ABC</p>

// 设置HTML:
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';

⚠️ 安全提醒:如果写入的字符串来自网络,要注意对字符编码,避免XSS攻击。

方法二:修改 innerTexttextContent
自动对字符串进行HTML编码,无法设置任何HTML标签。

let p = document.getElementById('p-id');
p.innerText = "<script>alert(\"Hi\")</script>";
// 实际效果:<p id="p-id">&lt;script&gt;alert("Hi")&lt;/script&gt;</p>

innerText 不返回隐藏元素的文本,而 textContent 返回所有文本。IE<9不支持 textContent

修改CSS:通过节点的 style 属性,使用驼峰命名法。

let p = document.getElementById('p-id');
p.style.color = "#ff0000";
p.style.fontSize = '20px';
p.style.paddingTop = '2em';

2.4 练习:更新DOM节点

给定HTML结构:

<div id="test-div">  
  <p id="test-js">javascript</p>  
  <p>Java</p>  
</div>

获取节点并修改:

let js = document.getElementById('test-js');
js.innerText = 'JavaScript';
js.style.color = '#ff0000';
js.style.fontWeight = 'bold';

2.5 插入DOM

情况一:目标节点为空
直接使用 innerHTML = '<span>child</span>'

情况二:目标节点非空

  • appendChild:将子节点添加到父节点的最后一个子节点之后。
  • insertBefore:将子节点插入到指定子节点之前。

示例:appendChild

<!-- 初始结构 -->
<p id="js">JavaScript</p>

<div id="list">
  <p id="java">Java</p>
  <p id="python">Python</p>
  <p id="scheme">Scheme</p>
</div>
let js = document.getElementById('js'),
    list = document.getElementById('list');
list.appendChild(js);

注意:如果插入的节点已存在于文档树,会先从原位置删除,再插入到新位置。

动态创建新节点

let list = document.getElementById('list'),
    haskell = document.createElement('p');
haskell.id = 'haskell';
haskell.innerHTML = 'Haskell';
list.appendChild(haskell);

实用场景:动态添加CSS样式

let d = document.createElement('style');
d.setAttribute('type', 'text/css');
d.innerHTML = 'p { color: red }';
document.getElementsByTagName['head'](0).appendChild(d);

示例:insertBefore

<div id="list">  
  <p id="java">Java</p>  
  <p id="python">Python</p>  
  <p id="scheme">Scheme</p>  
</div>

把Haskell插入到Python之前:

let list = document.getElementById('list'),
    ref = document.getElementById('python'),
    haskell = document.createElement('p');
haskell.id = 'haskell';
haskell.innerHTML = 'Haskell';
list.insertBefore(haskell, ref);

遍历子节点:通过 children 属性迭代。

let i, c, list = document.getElementById('list');
for (i = 0; i < list.children.length; i++) {
  c = list.children[i];
}

2.6 练习:按字符串顺序排序DOM节点

给定HTML结构:

<ol id="test-list">  
  <li class="lang">Scheme</li>  
  <li class="lang">JavaScript</li>  
  <li class="lang">Python</li>  
  <li class="lang">Ruby</li>  
  <li class="lang">Haskell</li>  
</ol>

按字符串顺序重新排序:

let ol = document.getElementById('test-list');
let items = Array.from(ol.children);
items.sort((a, b) => a.innerText.localeCompare(b.innerText));
items.forEach(item => ol.appendChild(item));

2.7 删除DOM

步骤:1)获得待删除节点;2)获得其父节点;3)调用父节点的 removeChild

let self = document.getElementById('to-be-removed');
let parent = self.parentElement;
let removed = parent.removeChild(self);
removed === self; // true

删除后的节点虽不在文档树中,但仍存在于内存中,可随时重新添加。

⚠️ 易错点children 属性是只读且实时更新的。遍历并删除多个节点时,注意索引会动态变化。

<div id="parent">
  <p>First</p>
  <p>Second</p>
</div>

错误示例:

let parent = document.getElementById('parent');
parent.removeChild(parent.children[0]);
parent.removeChild(parent.children[1]); // 报错!因为删除第一个后children长度变为1

正确做法:先收集要删除的节点,再统一删除。

2.8 练习:删除非Web技术节点

给定HTML结构:

<ul id="test-list">
  <li>JavaScript</li>  
  <li>Swift</li>  
  <li>HTML</li>  
  <li>ANSI C</li>  
  <li>CSS</li>  
  <li>DirectX</li>  
</ul>

删除与Web开发无关的节点(只保留JavaScript、HTML、CSS):

let list = document.getElementById('test-list');
let itemsToRemove = [];

for (let i = 0; i < list.children.length; i++) {
  let item = list.children[i];
  if (item.innerText !== 'JavaScript' && item.innerText !== 'HTML' && item.innerText !== 'CSS') {
    itemsToRemove.push(item);
  }
}

itemsToRemove.forEach(item => list.removeChild(item));

三、操作表单

表单本身也是DOM树,但表单控件可接收用户输入。常用表单控件:

控件类型HTML标签说明
文本框<input type="text">输入文本
口令框<input type="password">输入密码
单选框<input type="radio">选择一项
复选框<input type="checkbox">选择多项
下拉框<select>选择一项
隐藏文本<input type="hidden">用户不可见,随表单提交

3.1 获取值

对于 textpasswordhiddenselect,使用 value 属性:

let input = document.getElementById('email');
input.value; // '用户输入的值'

对于单选框和复选框,使用 checked 属性判断是否勾选:

let mon = document.getElementById('monday');  
let tue = document.getElementById('tuesday');  
mon.value; // '1'
tue.value; // '2'  
mon.checked; // true或false  
tue.checked; // true或false

3.2 设置值

同样,对于文本框等直接设置 value

let input = document.getElementById('email');
input.value = 'test@example.com';

对于单选框/复选框,设置 checked = true/false

3.3 HTML5控件

HTML5新增的常用控件:datedatetime-localcolor 等。

<input type="date" value="2021-12-02">
<input type="datetime-local" value="2021-12-02T20:21:12">
<input type="color" value="#ff0000">

不支持HTML5的浏览器会将这些控件当作 type="text" 显示。支持HTML5的浏览器会保证 value 为有效格式(如 YYYY-MM-DD)。

3.4 提交表单

方式一:调用 submit() 方法

<form id="test-form">
  <input type="text" name="test">
  <button type="button" onclick="doSubmitForm()">Submit</button>
</form>

<script>
function doSubmitForm() {
  let form = document.getElementById('test-form');
  // 可在此修改表单输入...
  form.submit();
}
</script>

这种方式扰乱了浏览器对form的正常提交(如回车键提交)。

方式二:响应 onsubmit 事件(推荐)

<form id="test-form" onsubmit="return checkForm()">
  <input type="text" name="test">
  <button type="submit">Submit</button>
</form>

<script>
function checkForm() {
  let form = document.getElementById('test-form');
  // 可在此修改表单输入...
  return true; // 返回false则阻止提交
}
</script>

安全传输示例(MD5加密口令)
为避免明文传输口令,使用隐藏字段。

<form id="login-form" method="post" onsubmit="return checkForm()">
  <input type="text" id="username" name="username">
  <input type="password" id="input-password">
  <input type="hidden" id="md5-password" name="password">
  <button type="submit">Submit</button>
</form>

<script>
function checkForm() {
  let input_pwd = document.getElementById('input-password');
  let md5_pwd = document.getElementById('md5-password');
  md5_pwd.value = toMD5(input_pwd.value); // 假设toMD5已实现
  return true;
}
</script>

关键:只有 name 属性的输入框数据才会被提交。用户输入的口令框没有 name 属性,因此不会发送明文。

3.5 练习:注册表单验证

实现以下验证规则:

  • 用户名必须是3-10位英文字母或数字
  • 口令必须是6-20位
  • 两次输入口令必须一致
<form id="test-register" action="#" target="_blank" onsubmit="return checkRegisterForm()">
  <p id="test-error" style="color:red"></p>
  <p>用户名: <input type="text" id="username" name="username"></p>
  <p>口令: <input type="password" id="password" name="password"></p>
  <p>重复口令: <input type="password" id="password-2"></p>
  <p><button type="submit">提交</button> <button type="reset">重置</button></p>
</form>

<script>
window.checkRegisterForm = function () {
  let username = document.getElementById('username').value;
  let password = document.getElementById('password').value;
  let password2 = document.getElementById('password-2').value;
  let error = document.getElementById('test-error');

  error.innerHTML = '';

  if (!/^[a-zA-Z0-9]{3,10}$/.test(username)) {
    error.innerHTML = '用户名必须是3-10位英文字母或数字';
    return false;
  }
  if (password.length < 6 || password.length > 20) {
    error.innerHTML = '口令必须是6-20位';
    return false;
  }
  if (password !== password2) {
    error.innerHTML = '两次输入口令必须一致';
    return false;
  }
  return true;
};
</script>

四、操作文件

文件上传的唯一控件是 <input type="file">
注意:当表单包含文件上传时,必须指定 enctype="multipart/form-data"method="post"

出于安全,JavaScript无法对 value 赋值,也无法获取真实路径(显示为 C:\fakepath\...)。可以在提交前检查文件扩展名:

let f = document.getElementById('test-file-upload');
let filename = f.value;
if (!filename || !(filename.endsWith('.jpg') || filename.endsWith('.png') || filename.endsWith('.gif'))) {
    alert('只能上传图片文件');
    return false;
}

4.1 File API 与图片预览

HTML5提供了 FileFileReader 对象,可读取文件内容。

let fileInput = document.getElementById('test-image-file'),
    info = document.getElementById('test-file-info'),
    preview = document.getElementById('test-image-preview');

fileInput.addEventListener('change', function () {
    preview.style.backgroundImage = '';
    if (!fileInput.value) {
        info.innerHTML = '没有选择文件';
        return;
    }
    let file = fileInput.files[0];
    info.innerHTML = '文件: ' + file.name + '<br>' +
                     '大小: ' + file.size + '<br>' +
                     '修改: ' + file.lastModified;
    if (file.type !== 'image/jpeg' && file.type !== 'image/png' && file.type !== 'image/gif') {
        alert('不是有效的图片文件!');
        return;
    }
    let reader = new FileReader();
    reader.onload = function (e) {
        let data = e.target.result; // data:image/jpeg;base64,...
        preview.style.backgroundImage = 'url(' + data + ')';
    };
    reader.readAsDataURL(file);
});

以DataURL形式读取后,字符串形如 data:image/jpeg;base64,/9j/4AAQSk...,可用于设置图像。如需服务器处理,可将 base64, 之后的字符发送给服务器解码。

4.2 回调与异步

JavaScript是单线程执行模式,多任务通过异步回调实现。例如 reader.readAsDataURL(file) 发起异步读取,必须预先设置 onload 回调函数,文件读取完成后自动调用。

五、AJAX

AJAX(Asynchronous JavaScript and XML)即用JavaScript执行异步网络请求,让用户停留在当前页面同时刷新数据。最早大规模使用是Gmail。

5.1 原生 Fetch API(现代推荐)

现代浏览器支持Fetch API,以Promise方式提供:

async function get(url) {
    let resp = await fetch(url);
    let result = await resp.text();
    return result;
}

// 发送异步请求:
get('/content.html').then(data => {
    let textarea = document.getElementById('fetch-response-text');
    textarea.value = data;
});

更详细用法参考MDN文档。

5.2 同源策略与安全限制

同源策略:AJAX请求的URL必须与当前页面协议、域名、端口号完全一致,否则请求被浏览器阻止。

跨域解决方案

  1. Flash插件:麻烦且逐渐淘汰。
  2. 代理服务器:在同源域名下架设代理,转发请求到外域。
  3. JSONP:只支持GET请求,利用 <script> 可跨域引用JS的特性,返回函数调用如 foo('data')
  4. CORS(推荐):HTML5规范,通过响应头 Access-Control-Allow-Origin 授权跨域。

5.3 CORS详解

CORS全称Cross-Origin Resource Sharing。当浏览器向外域发起请求时,检查响应头中的 Access-Control-Allow-Origin 是否包含本域(或为 *),若包含则请求成功,否则失败。

简单请求:满足以下条件:

  • 方法为GET、HEAD、POST
  • POST的Content-Type为 application/x-www-form-urlencodedmultipart/form-datatext/plain
  • 无自定义头

对于PUT、DELETE以及 application/json 的POST请求,浏览器会先发送一个 OPTIONS预检请求(preflighted),询问服务器是否允许。服务器必须正确响应 Access-Control-Allow-Methods 等头信息。

无论是否使用CORS,都要理解其原理。引用第三方CDN字体时,若对方未设置正确的CORS头,浏览器也无法加载字体。

六、Promise

JavaScript是单线程,所有网络操作、浏览器事件都必须异步执行。传统回调写法容易形成“回调地狱”。Promise对象将“承诺将来会执行”的动作优雅地封装起来。

6.1 Promise基础

最简单的Promise例子:生成0-2随机数,小于1则成功,否则失败。

function test(resolve, reject) {
    let timeOut = Math.random() * 2;
    console.log('set timeout to: ' + timeOut + ' seconds');
    setTimeout(function () {
        if (timeOut < 1) {
            console.log('call resolve()...');
            resolve('200 OK');
        } else {
            console.log('call reject()...');
            reject('timeout in ' + timeOut + ' seconds');
        }
    }, timeOut * 1000);
}

let p1 = new Promise(test);
p1.then(function (result) {
    console.log('成功:' + result);
}).catch(function (reason) {
    console.log('失败:' + reason);
});

链式简化

new Promise(test)
    .then(function (result) {
        console.log('成功:' + result);
    })
    .catch(function (reason) {
        console.log('失败:' + reason);
    });

执行效果

Promise最大的好处:把执行代码和处理结果的代码清晰分离。

6.2 串行执行多个异步任务

job1.then(job2).then(job3).catch(handleError);

模拟串行计算

// 0.5秒后返回 input*input
function multiply(input) {
    return new Promise(function (resolve, reject) {
        setTimeout(resolve, 500, input * input);
    });
}
// 0.5秒后返回 input+input
function add(input) {
    return new Promise(function (resolve, reject) {
        setTimeout(resolve, 500, input + input);
    });
}

let p = new Promise(function (resolve, reject) {
    resolve(123);
});

p.then(multiply)
 .then(add)
 .then(multiply)
 .then(add)
 .then(function (result) {
     console.log('Got value: ' + result);
 });

6.3 并行执行:Promise.all / Promise.race

Promise.all:等待所有任务完成,返回结果数组。

let p1 = new Promise(resolve => setTimeout(resolve, 500, 'P1'));
let p2 = new Promise(resolve => setTimeout(resolve, 600, 'P2'));
Promise.all([p1, p2]).then(results => console.log(results)); // ['P1', 'P2']

Promise.race:返回最先完成的任务结果,其余丢弃。

let p1 = new Promise(resolve => setTimeout(resolve, 500, 'P1'));
let p2 = new Promise(resolve => setTimeout(resolve, 600, 'P2'));
Promise.race([p1, p2]).then(result => console.log(result)); // 'P1'

七、async函数

async / await 让异步代码写起来像同步代码,可读性大大提高。

7.1 基本用法

async function get(url) {
    let resp = await fetch(url);
    let result = await resp.json();
    return result;
}
  • async function 定义一个异步函数,等价于返回Promise。
  • await 只能在 async function 内部使用,它自动实现异步调用并等待结果。

错误处理:用传统的 try...catch

async function get(url) {
    try {
        let resp = await fetch(url);
        let result = await resp.json();
        return result;
    } catch (e) {
        // 处理错误
    }
}

7.2 在普通函数中调用async函数

普通函数不能直接使用 await,但可以获取返回的Promise对象,再调用 .then()

async function get(url) {
    let resp = await fetch(url);
    return resp.text();
}

function doGet() {
    let promise = get('/content.html');
    promise.then(data => {
        document.getElementById('test-response-text').value = data;
    });
}
doGet();

结论:定义异步任务用 async function 更简单;调用异步任务用 await 更简单;捕获错误用 try...catch。只要浏览器支持,完全可以用async简洁实现异步操作。

八、Canvas绘图

Canvas是HTML5新增组件,可用JS绘制图形、动画等,替代Flash。

8.1 创建与检测

<canvas id="test-canvas" width="300" height="200">
    <p>你的浏览器不支持Canvas</p>
</canvas>
let canvas = document.getElementById('test-canvas');
if (canvas.getContext) {
    console.log('你的浏览器支持Canvas!');
    let ctx = canvas.getContext('2d'); // 获取2D上下文
    // 如需3D: canvas.getContext("webgl");
} else {
    console.log('你的浏览器不支持Canvas!');
}

8.2 坐标系统

Canvas坐标以左上角为原点,水平向右为X轴正方向,垂直向下为Y轴正方向,单位是像素。

8.3 绘制形状

let canvas = document.getElementById('test-shape-canvas'),
    ctx = canvas.getContext('2d');

ctx.clearRect(0, 0, 200, 200); // 擦除矩形区域(变透明)
ctx.fillStyle = '#dddddd';
ctx.fillRect(10, 10, 130, 130); // 填充矩形

// 使用Path绘制复杂路径(笑脸)
let path = new Path2D();
path.arc(75, 75, 50, 0, Math.PI * 2, true);
path.moveTo(110, 75);
path.arc(75, 75, 35, 0, Math.PI, false);
path.moveTo(65, 65);
path.arc(60, 65, 5, 0, Math.PI * 2, true);
path.moveTo(95, 65);
path.arc(90, 65, 5, 0, Math.PI * 2, true);
ctx.strokeStyle = '#0000ff';
ctx.stroke(path);

8.4 绘制文本

let canvas = document.getElementById('test-text-canvas'),
    ctx = canvas.getContext('2d');

ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, 300, 100);
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.shadowBlur = 2;
ctx.shadowColor = '#ccc';
ctx.font = '28px Arial';
ctx.fillStyle = '#999';
ctx.fillText('带阴影的文字', 20, 40);

8.5 性能优化建议

  • 通过不可见的Canvas绘图,再将结果复制到可见Canvas
  • 尽量使用整数坐标
  • 创建多个重叠Canvas绘制不同层
  • 不变背景图片直接用 <img> 标签放在最底层

8.6 练习:绘制天气预报图表

给定天气数据数组,在Canvas上绘制未来天气预报(柱状图等)。

let data = [
    { high: 35, low: 22 },
    { high: 37, low: 24 },
    { high: 37, low: 25 },
    { high: 34, low: 24 },
    { high: 33, low: 23 }
];

let canvas = document.getElementById('weather-canvas');
// 在此实现绘图逻辑(400x200)

// 下载图片
let download = document.getElementById('weather-download');
download.href = canvas.toDataURL();

读者可自行实现柱状图绘制,展示每日最高温和最低温。

本篇核心知识点速记

  • 浏览器对象window(窗口尺寸)、navigator(浏览器信息,不可靠)、screen(屏幕信息)、location(URL操作)、document(DOM根节点、Cookie)、history(历史记录,配合pushState使用)
  • DOM操作:获取(getElementById/querySelector)、更新(innerHTML/innerText/style)、插入(appendChild/insertBefore)、删除(removeChild),注意children实时更新
  • 表单操作:用value获取文本框/下拉框值,用checked获取单选/复选框;提交表单用onsubmit事件并return true/false;安全传输用hidden字段
  • 文件上传<input type="file"> + FormData;File API可读取本地文件实现图片预览;异步回调处理
  • AJAX与跨域:同源策略限制;CORS通过响应头授权跨域;简单请求与预检请求;Fetch API推荐使用
  • Promise:解决回调地狱,支持链式调用(then/catch),提供all/race并行控制
  • async/await:异步函数同步写,错误用try...catch,必须在async函数内使用await
  • Canvas:2D绘图上下文,坐标左上角原点,可绘制形状、文本,支持性能优化