廖雪峰javascrip学习笔记<高级>
2015-08-09 16:50
537 查看
来自:http://www.liaoxuefeng.com/
JavaScript可以获取浏览器提供的很多对象,并进行操作。
对应的,还有一个
navigator.appName:浏览器名称;
navigator.appVersion:浏览器版本;
navigator.language:浏览器设置的语言;
navigator.platform:操作系统类型;
navigator.userAgent:浏览器设定的
但这样既可能判断不准确,也很难维护代码。正确的方法是充分利用JavaScript对不存在属性返回
screen.width:屏幕宽度,以像素为单位;
screen.height:屏幕高度,以像素为单位;
screen.colorDepth:返回颜色位数,如8、16、24。
可以用
要加载一个新页面,可以调用
阅读: 1680
JavaScript可以获取浏览器提供的很多对象,并进行操作。
兼容性:IE<=8不支持。
对应的,还有一个
navigator.appName:浏览器名称;
navigator.appVersion:浏览器版本;
navigator.language:浏览器设置的语言;
navigator.platform:操作系统类型;
navigator.userAgent:浏览器设定的
请注意,
但这样既可能判断不准确,也很难维护代码。正确的方法是充分利用JavaScript对不存在属性返回
screen.width:屏幕宽度,以像素为单位;
screen.height:屏幕高度,以像素为单位;
screen.colorDepth:返回颜色位数,如8、16、24。
可以用
要加载一个新页面,可以调用
请观察浏览器窗口标题的变化。
要查找DOM数的某个节点,需要从
我们先准备HTML数据:
摩卡热摩卡咖啡酸奶北京老酸奶果汁鲜榨苹果汁
用
Cookie是由服务器发送的key-value标示符。因为HTTP协议是无状态的,但是服务器要区分到底是哪个用户发过来的请求,就可以用Cookie来区分。当一个用户成功登录后,服务器发送一个Cookie给浏览器,例如
Cookie还可以存储网站的一些设置,例如,页面显示的语言等等。
JavaScript可以通过
由于JavaScript能读取到页面的Cookie,而用户的登录信息通常也存在Cookie中,这就造成了巨大的安全隐患,这是因为在HTML页面中引入第三方的JavaScript代码是允许的:
如果引入的第三方的JavaScript中存在恶意代码,则
为了解决这个问题,服务器在设置Cookie时可以使用
SP1开始支持。
为了确保安全,服务器端在设置Cookie时,应该始终坚持使用
由于HTML文档被浏览器解析后就是一棵DOM树,要改变HTML的结构,就需要通过JavaScript来操作DOM。
始终记住DOM是一个树形结构。操作一个DOM节点实际上就是这么几个操作:
更新:更新该DOM节点的内容,相当于更新了该DOM节点表示的HTML的内容;
遍历:遍历该DOM节点下的子节点,以便进行进一步操作;
添加:在该DOM节点下新增一个子节点,相当于动态增加了一个HTML节点;
删除:将该节点从HTML中删除,相当于删掉了该DOM节点的内容以及它包含的所有子节点。
在操作一个DOM节点前,我们需要通过各种方式先拿到这个DOM节点。最常用的方法是
用JavaScript操作表单和操作DOM是类似的,因为表单本身也是DOM树。
不过表单的输入框、下拉框等可以接收用户输入,所以用JavaScript来操作表单,可以获得用户输入的内容,或者对一个输入框设置新的内容。
HTML表单的输入控件主要有以下几种:
文本框,对应的
口令框,对应的
单选框,对应的
复选框,对应的
下拉框,对应的
隐藏文本,对应的
如果我们获得了一个
这种方式可以应用于
设置值和获取值类似,对于
对于单选框和复选框,设置
HTML5新增了大量标准控件,常用的包括
<input type="date">:
<input type="datetime-local">:
<input type="color">:
不支持HTML5的浏览器无法识别新的控件,会把它们当做
最后,JavaScript可以以两种方式来处理表单的提交(AJAX方式在后面章节介绍)。
方式一是通过
这种方式的缺点是扰乱了浏览器对form的正常提交。浏览器默认点击
注意要
在检查和修改
例如,很多登录表单希望用户输入用户名和口令,但是,安全考虑,提交表单时不传输明文口令,而是口令的MD5。普通JavaScript开发人员会直接修改
这个做法看上去没啥问题,但用户输入了口令提交时,口令框的显示会突然从几个
要想不改变用户的输入,可以利用
注意到
在HTML表单中,可以上传文件的唯一控件就是
注意:当一个表单包含
出于安全考虑,浏览器只允许用户点击
待上传文件:
通常,上传的文件都由后台服务器处理,JavaScript可以在提交表单时对文件扩展名做检查,以便防止用户上传无效格式的文件:
由于JavaScript对用户上传的文件操作非常有限,尤其是无法读取文件内容,使得很多需要操作文件的网页不得不用Flash这样的第三方插件来实现。
随着HTML5的普及,新增的File API允许JavaScript读取文件内容,获得更多的文件信息。
HTML5的File API提供了
AJAX不是JavaScript的规范,它只是一个哥们“发明”的缩写:Asynchronous JavaScript and XML,意思就是用JavaScript执行异步网络请求。
如果仔细观察一个Form的提交,你就会发现,一旦用户点击“Submit”按钮,表单开始提交,浏览器就会刷新页面,然后在新页面里告诉你操作是成功了还是失败了。如果不幸由于网络太慢或者其他原因,就会得到一个404页面。
这就是Web的运作原理:一次HTTP请求对应一个页面。
如果要让用户留在当前页面中,同时发出新的HTTP请求,就必须用JavaScript发送这个新请求,接收到数据后,再用JavaScript更新页面,这样一来,用户就感觉自己仍然停留在当前页面,但是数据却可以不断地更新。
最早大规模使用AJAX的就是Gmail,Gmail的页面在首次加载后,剩下的所有数据都依赖于AJAX来更新。
用JavaScript写一个完整的AJAX代码并不复杂,但是需要注意:AJAX请求是异步执行的,也就是说,要通过回调函数获得响应。
在现代浏览器上写AJAX主要依靠
当创建了
注意,千万不要把第三个参数指定为
最后调用
JavaScript的世界中,所有代码都是单线程执行的。
由于这个“缺陷”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。异步执行可以用回调函数实现:
观察上述代码执行,在Chrome的控制台输出可以看到:
可见,异步操作会在将来的某个时间点触发一个函数调用。
AJAX就是典型的异步操作。以上一节的代码为例:
把回调函数
有没有更好的写法?比如写成这样:
这种链式写法的好处在于,先统一执行AJAX逻辑,不关心如何处理结果,然后,根据结果是成功还是失败,在将来的某个时候调用
古人云:“君子一诺千金”,这种“承诺将来会执行”的对象在JavaScript中称为Promise对象。
Promise有各种开源实现,在ES6中被统一规范,由浏览器直接支持。
我们先看一个最简单的Promise例子:生成一个0-2之间的随机数,如果小于1,则等待一段时间后返回成功,否则返回失败:
这个
有了执行函数,我们就可以用一个Promise对象来执行它,并在将来某个时刻获得成功或失败的结果:
变量
当
Promise对象可以串联起来,所以上述代码可以简化为:
实际测试一下,看看Promise是如何异步执行的:
new Promise(function (resolve, reject) {
log('start new Promise...');
var timeOut = Math.random() * 2;
log('set timeout to: ' + timeOut + ' seconds.');
setTimeout(function () {
if (timeOut < 1) {
log('call resolve()...');
resolve('200 OK');
}
else {
log('call reject()...');
reject('timeout in ' + timeOut + ' seconds.');
}
}, timeOut * 1000);
}).then(function (r) {
log('Done: ' + r);
}).catch(function (reason) {
log('Failed: ' + reason);
});
可见Promise最大的好处是在异步执行的流程中,把执行代码和处理结果的代码清晰地分离了:
Promise还可以做更多的事情,比如,有若干个异步任务,需要先做任务1,如果成功后再做任务2,任何任务失败则不再继续并执行错误处理函数。
要串行执行这样的异步任务,不用Promise需要写一层一层的嵌套代码。有了Promise,我们只需要简单地写:
其中,
下面的例子演示了如何串行执行一系列需要异步计算获得结果的任务:
// 0.5秒后返回input*input的计算结果:
function multiply(input) {
return new Promise(function (resolve, reject) {
log('calculating ' + input + ' x ' + input + '...');
setTimeout(resolve, 500, input * input);
});
}
// 0.5秒后返回input+input的计算结果:
function add(input) {
return new Promise(function (resolve, reject) {
log('calculating ' + input + ' + ' + input + '...');
setTimeout(resolve, 500, input + input);
});
}
var p = new Promise(function (resolve, reject) {
log('start new Promise...');
resolve(123);
});
p.then(multiply)
.then(add)
.then(multiply)
.then(add)
.then(function (result) {
log('Got value: ' + result);
});
var log = document.getElementById('test-promise-ajax-result');
var p = ajax('GET', '/api/categories');
p.then(function (text) { // 如果AJAX成功,获得响应内容
log.innerText = text;
}).catch(function (status) { // 如果AJAX失败,获得响应代码
log.innerText = 'ERROR: ' + status;
});
除了串行执行若干异步任务外,Promise还可以并行执行异步任务。
试想一个页面聊天系统,我们需要从两个不同的URL分别获得用户的个人信息和好友列表,这两个任务是可以并行执行的,用
有些时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用
由于
如果我们组合使用Promise,就可以把很多异步任务以并行和串行的方式组合起来执行。
Canvas是HTML5新增的组件,它就像一块幕布,可以用JavaScript在上面绘制各种图表、动画等。
没有Canvas的年代,绘图只能借助Flash插件实现,页面不得不用JavaScript和Flash进行交互。有了Canvas,我们就再也不需要Flash了,直接使用JavaScript完成绘制。
一个Canvas定义了一个指定尺寸的矩形框,在这个范围内我们可以随意绘制:
由于浏览器对HTML5标准支持不一致,所以,通常在
如果需要绘制3D怎么办?HTML5还有一个WebGL规范,允许在Canvas中绘制3D图形:
本节我们只专注于绘制2D图形。
我们可以在Canvas上绘制各种形状。在绘制前,我们需要先了解一下Canvas的坐标系统:
Canvas的坐标以左上角为原点,水平向右为X轴,垂直向下为Y轴,以像素为单位,所以每个点都是非负整数。
ctx.clearRect(0, 0, 200, 200); // 擦除(0,0)位置大小为200x200的矩形,擦除的意思是把该区域变为透明
ctx.fillStyle = '#dddddd'; // 设置颜色
ctx.fillRect(10, 10, 130, 130); // 把(10,10)位置大小为130x130的矩形涂色
// 利用Path绘制复杂路径:
var 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);
绘制文本就是在指定的位置输出文本,可以设置文本的字体、样式、阴影等,与CSS完全一致:
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.shadowBlur = 2;
ctx.shadowColor = '#666666';
ctx.font = '24px Arial';
ctx.fillStyle = '#333333';
ctx.fillText('带阴影的文字', 20, 40);
Canvas除了能绘制基本的形状和文本,还可以实现动画、缩放、各种滤镜和像素转换等高级操作。如果要实现非常复杂的操作,考虑以下优化方案:
通过创建一个不可见的Canvas来绘图,然后将最终绘制结果复制到页面的可见Canvas中;
尽量使用整数坐标而不是浮点数;
可以创建多个重叠的Canvas绘制不同的层,而不是在一个Canvas中绘制非常复杂的图;
背景图片如果不变可以直接用
浏览器对象
JavaScript可以获取浏览器提供的很多对象,并进行操作。
window
window对象不但充当全局作用域,而且表示浏览器窗口。
window对象有
innerWidth和
innerHeight属性,可以获取浏览器窗口的内部宽度和高度。内部宽高是指除去菜单栏、工具栏、边框等占位元素后,用于显示网页的净宽高。
对应的,还有一个
outerWidth和
outerHeight属性,可以获取浏览器窗口的整个宽高。
navigator
navigator对象表示浏览器的信息,最常用的属性包括:
navigator.appName:浏览器名称;
navigator.appVersion:浏览器版本;
navigator.language:浏览器设置的语言;
navigator.platform:操作系统类型;
navigator.userAgent:浏览器设定的
User-Agent字符串。
navigator的信息可以很容易地被用户修改,所以JavaScript读取的值不一定是正确的。很多初学者为了针对不同浏览器编写不同的代码,喜欢用
if判断浏览器版本,例如:
var width; if (getIEVersion(navigator.userAgent) < 9) { width = document.body.clientWidth; } else { width = window.innerWidth; }
但这样既可能判断不准确,也很难维护代码。正确的方法是充分利用JavaScript对不存在属性返回
undefined的特性,直接用短路运算符
||计算:
var width = window.innerWidth || document.body.clientWidth;
screen
screen对象表示屏幕的信息,常用的属性有:
screen.width:屏幕宽度,以像素为单位;
screen.height:屏幕高度,以像素为单位;
screen.colorDepth:返回颜色位数,如8、16、24。
ocation
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()方法非常方便。
document
document对象表示当前页面。由于HTML在浏览器中以DOM形式表示为树形结构,
document对象就是整个DOM数的根节点。
document的
title属性是从HTML文档中的
<title>xxx</title>读取的,但是可以动态改变:
浏览器对象
阅读: 1680JavaScript可以获取浏览器提供的很多对象,并进行操作。
window
window对象不但充当全局作用域,而且表示浏览器窗口。
window对象有
innerWidth和
innerHeight属性,可以获取浏览器窗口的内部宽度和高度。内部宽高是指除去菜单栏、工具栏、边框等占位元素后,用于显示网页的净宽高。
兼容性:IE<=8不支持。
对应的,还有一个
outerWidth和
outerHeight属性,可以获取浏览器窗口的整个宽高。
navigator
navigator对象表示浏览器的信息,最常用的属性包括:
navigator.appName:浏览器名称;
navigator.appVersion:浏览器版本;
navigator.language:浏览器设置的语言;
navigator.platform:操作系统类型;
navigator.userAgent:浏览器设定的
User-Agent字符串。
请注意,
navigator的信息可以很容易地被用户修改,所以JavaScript读取的值不一定是正确的。很多初学者为了针对不同浏览器编写不同的代码,喜欢用
if判断浏览器版本,例如:
var width; if (getIEVersion(navigator.userAgent) < 9) { width = document.body.clientWidth; } else { width = window.innerWidth; }
但这样既可能判断不准确,也很难维护代码。正确的方法是充分利用JavaScript对不存在属性返回
undefined的特性,直接用短路运算符
||计算:
var width = window.innerWidth || document.body.clientWidth;
screen
screen对象表示屏幕的信息,常用的属性有:
screen.width:屏幕宽度,以像素为单位;
screen.height:屏幕高度,以像素为单位;
screen.colorDepth:返回颜色位数,如8、16、24。
'use strict';
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()方法非常方便。
document
document对象表示当前页面。由于HTML在浏览器中以DOM形式表示为树形结构,
document对象就是整个DOM数的根节点。
document的
title属性是从HTML文档中的
<title>xxx</title>读取的,但是可以动态改变:
请观察浏览器窗口标题的变化。
要查找DOM数的某个节点,需要从
document对象开始查找。最常用的查找是根据ID和Tag Name。
我们先准备HTML数据:
<dl id="drink-menu" style="border:solid 1px #ccc;padding:6px;"> <dt>摩卡</dt> <dd>热摩卡咖啡</dd> <dt>酸奶</dt> <dd>北京老酸奶</dd> <dt>果汁</dt> <dd>鲜榨苹果汁</dd> </dl>
摩卡热摩卡咖啡酸奶北京老酸奶果汁鲜榨苹果汁
用
document对象提供的
getElementById()和
getElementsByTagName()可以按ID获得一个DOM节点和按Tag名称获得一组DOM节点:
document对象还有一个
cookie属性,可以获取当前页面的Cookie。
Cookie是由服务器发送的key-value标示符。因为HTTP协议是无状态的,但是服务器要区分到底是哪个用户发过来的请求,就可以用Cookie来区分。当一个用户成功登录后,服务器发送一个Cookie给浏览器,例如
user=ABC123XYZ(加密的字符串)...,此后,浏览器访问该网站时,会在请求头附上这个Cookie,服务器根据Cookie即可区分出用户。
Cookie还可以存储网站的一些设置,例如,页面显示的语言等等。
JavaScript可以通过
document.cookie读取到当前页面的Cookie:
document.cookie; // 'v=123; remember=true; prefer=zh'
由于JavaScript能读取到页面的Cookie,而用户的登录信息通常也存在Cookie中,这就造成了巨大的安全隐患,这是因为在HTML页面中引入第三方的JavaScript代码是允许的:
<!-- 当前页面在wwwexample.com --> <html> <head> <script src="http://www.foo.com/jquery.js"></script> </head> ... </html>
如果引入的第三方的JavaScript中存在恶意代码,则
www.foo.com网站将直接获取到
www.example.com网站的用户登录信息。
为了解决这个问题,服务器在设置Cookie时可以使用
httpOnly,设定了
httpOnly的Cookie将不能被JavaScript读取。这个行为由浏览器实现,主流浏览器均支持
httpOnly选项,IE从IE6
SP1开始支持。
为了确保安全,服务器端在设置Cookie时,应该始终坚持使用
httpOnly。
操作DOM
由于HTML文档被浏览器解析后就是一棵DOM树,要改变HTML的结构,就需要通过JavaScript来操作DOM。始终记住DOM是一个树形结构。操作一个DOM节点实际上就是这么几个操作:
更新:更新该DOM节点的内容,相当于更新了该DOM节点表示的HTML的内容;
遍历:遍历该DOM节点下的子节点,以便进行进一步操作;
添加:在该DOM节点下新增一个子节点,相当于动态增加了一个HTML节点;
删除:将该节点从HTML中删除,相当于删掉了该DOM节点的内容以及它包含的所有子节点。
在操作一个DOM节点前,我们需要通过各种方式先拿到这个DOM节点。最常用的方法是
document.getElementById()和
document.getElementsByTagName(),以及CSS选择器
document.getElementsByClassName()。
操作表单
用JavaScript操作表单和操作DOM是类似的,因为表单本身也是DOM树。不过表单的输入框、下拉框等可以接收用户输入,所以用JavaScript来操作表单,可以获得用户输入的内容,或者对一个输入框设置新的内容。
HTML表单的输入控件主要有以下几种:
文本框,对应的
<input type="text">,用于输入文本;
口令框,对应的
<input type="password">,用于输入口令;
单选框,对应的
<input type="radio">,用于选择一项;
复选框,对应的
<input type="checkbox">,用于选择多项;
下拉框,对应的
<select>,用于选择一项;
隐藏文本,对应的
<input type="hidden">,用户不可见,但表单提交时会把隐藏文本发送到服务器。
获取值
如果我们获得了一个<input>节点的引用,就可以直接调用
value获得对应的用户输入值:
// <input type="text" id="email"> var input = document.getElementById('email'); input.value; // '用户输入的值'
这种方式可以应用于
text、
password、
hidden以及
select。但是,对于单选框和复选框,
value属性返回的永远是HTML预设的值,而我们需要获得的实际是用户是否“勾上了”选项,所以应该用
checked判断:
// <label><input type="radio" name="weekday" id="monday" value="1"> Monday</label> // <label><input type="radio" name="weekday" id="tuesday" value="2"> Thurthday</label> var mon = document.getElementById('monday'); var tue = document.getElementById('tuesday'); mon.value; // '1' tue.value; // '2' mon.checked; // true或者false tue.checked; // true或者false
设置值
设置值和获取值类似,对于text、
password、
hidden以及
select,直接设置
value就可以:
// <input type="text" id="email"> var input = document.getElementById('email'); input.value = 'test@example.com'; // 文本框的内容已更新
对于单选框和复选框,设置
checked为
true或
false即可。
HTML5控件
HTML5新增了大量标准控件,常用的包括date、
datetime、
datetime-local、
color等,它们都使用
<input>标签:
<input type="date">:
<input type="datetime-local">:
<input type="color">:
不支持HTML5的浏览器无法识别新的控件,会把它们当做
type="text"来显示。支持HTML5的浏览器将获得格式化的字符串。例如,
type="date"类型的
input的
value将保证是一个有效的
YYYY-MM-DD格式的日期,或者空字符串。
提交表单
最后,JavaScript可以以两种方式来处理表单的提交(AJAX方式在后面章节介绍)。方式一是通过
<form>元素的
submit()方法提交一个表单,例如,响应一个
<button>的
click事件,在JavaScript代码中提交表单:
<!-- HTML --> <form id="test-form"> <input type="text" name="test"> <button type="button" onclick="doSubmitForm()">Submit</button> </form> <script> function doSubmitForm() { var form = document.getElementById('test-form'); // 可以在此修改form的input... // 提交form: form.submit(); } </script>
这种方式的缺点是扰乱了浏览器对form的正常提交。浏览器默认点击
<button type="submit">时提交表单,或者用户在最后一个输入框按回车键。因此,第二种方式是响应
<form>本身的
onsubmit事件,在提交form时作修改:
<!-- HTML --> <form id="test-form" onsubmit="return checkForm()"> <input type="text" name="test"> <button type="submit">Submit</button> </form> <script> function checkForm() { var form = document.getElementById('test-form'); // 可以在此修改form的input... // 继续下一步: return true; } </script>
注意要
return true来告诉浏览器继续提交,如果
return false,浏览器将不会继续提交form,这种情况通常对应用户输入有误,提示用户错误信息后终止提交form。
在检查和修改
<input>时,要充分利用
<input type="hidden">来传递数据。
例如,很多登录表单希望用户输入用户名和口令,但是,安全考虑,提交表单时不传输明文口令,而是口令的MD5。普通JavaScript开发人员会直接修改
<input>:
<!-- HTML --> <form id="login-form" method="post" onsubmit="return checkForm()"> <input type="text" id="username" name="username"> <input type="password" id="password" name="password"> <button type="submit">Submit</button> </form> <script> function checkForm() { var pwd = document.getElementById('password'); // 把用户输入的明文变为MD5: pwd.value = toMD5(pwd.value); // 继续下一步: return true; } </script>
这个做法看上去没啥问题,但用户输入了口令提交时,口令框的显示会突然从几个
*变成32个
*(因为MD5有32个字符)。
要想不改变用户的输入,可以利用
<input type="hidden">实现:
<!-- HTML --> <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() { var input_pwd = document.getElementById('input-password'); var md5_pwd = document.getElementById('md5-password'); // 把用户输入的明文变为MD5: md5_pwd.value = toMD5(input_pwd.value); // 继续下一步: return true; } </script>
注意到
id为
md5-password的
<input>标记了
name="password",而用户输入的
id为
input-password的
<input>没有
name属性。没有
name属性的
<input>的数据不会被提交。
操作文件
在HTML表单中,可以上传文件的唯一控件就是<input type="file">。
注意:当一个表单包含
<input type="file">时,表单的
enctype必须指定为
multipart/form-data,
method必须指定为
post,浏览器才能正确编码并以
multipart/form-data格式发送表单的数据。
出于安全考虑,浏览器只允许用户点击
<input type="file">来选择本地文件,用JavaScript对
<input type="file">的
value赋值是没有任何效果的。当用户选择了上传某个文件后,JavaScript也无法获得该文件的真实路径:
待上传文件:
通常,上传的文件都由后台服务器处理,JavaScript可以在提交表单时对文件扩展名做检查,以便防止用户上传无效格式的文件:
var f = document.getElementById('test-file-upload'); var filename = f.value; // 'C:\fakepath\test.png' if (!filename || !(filename.endsWith('.jpg') || filename.endsWith('.png') || filename.endsWith('.gif'))) { alert('Can only upload image file.'); return false; }
File API
由于JavaScript对用户上传的文件操作非常有限,尤其是无法读取文件内容,使得很多需要操作文件的网页不得不用Flash这样的第三方插件来实现。随着HTML5的普及,新增的File API允许JavaScript读取文件内容,获得更多的文件信息。
HTML5的File API提供了
File和
FileReader两个主要对象,可以获得文件信息并读取文件。
AJAX
AJAX不是JavaScript的规范,它只是一个哥们“发明”的缩写:Asynchronous JavaScript and XML,意思就是用JavaScript执行异步网络请求。如果仔细观察一个Form的提交,你就会发现,一旦用户点击“Submit”按钮,表单开始提交,浏览器就会刷新页面,然后在新页面里告诉你操作是成功了还是失败了。如果不幸由于网络太慢或者其他原因,就会得到一个404页面。
这就是Web的运作原理:一次HTTP请求对应一个页面。
如果要让用户留在当前页面中,同时发出新的HTTP请求,就必须用JavaScript发送这个新请求,接收到数据后,再用JavaScript更新页面,这样一来,用户就感觉自己仍然停留在当前页面,但是数据却可以不断地更新。
最早大规模使用AJAX的就是Gmail,Gmail的页面在首次加载后,剩下的所有数据都依赖于AJAX来更新。
用JavaScript写一个完整的AJAX代码并不复杂,但是需要注意:AJAX请求是异步执行的,也就是说,要通过回调函数获得响应。
在现代浏览器上写AJAX主要依靠
XMLHttpRequest对象:
function success(text) { var textarea = document.getElementById('test-response-text'); textarea.value = "成功了"; } function fail(code) { var textarea = document.getElementById('test-response-text'); textarea.value = 'Error code: ' + code; } var request = new XMLHttpRequest(); // 新建XMLHttpRequest对象 request.onreadystatechange = function () { // 状态发生变化时,函数被回调 if (request.readyState === 4) { // 成功完成 // 判断响应结果: if (request.status === 200) { // 成功,通过responseText拿到响应的文本: return success(request.responseText); } else { // 失败,根据响应码判断失败原因: return fail(request.status); } } else { // HTTP请求还在继续... } } // 发送请求: request.open('GET', '/api/categories'); request.send(); alert('请求已发送,请等待响应...');
当创建了
XMLHttpRequest对象后,要先设置
onreadystatechange的回调函数。在回调函数中,通常我们只需通过
readyState === 4判断请求是否完成,如果已完成,再根据
status === 200判断是否是一个成功的响应。
XMLHttpRequest对象的
open()方法有3个参数,第一个参数指定是
GET还是
POST,第二个参数指定URL地址,第三个参数指定是否使用异步,默认是
true,所以不用写。
注意,千万不要把第三个参数指定为
false,否则浏览器将停止响应,直到AJAX请求完成。如果这个请求耗时10秒,那么10秒内你会发现浏览器处于“假死”状态。
最后调用
send()方法才真正发送请求。
GET请求不需要参数,
POST请求需要把body部分以字符串或者
FormData对象传进去。
Promise
JavaScript的世界中,所有代码都是单线程执行的。由于这个“缺陷”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。异步执行可以用回调函数实现:
function callback() { console.log('Done'); } console.log('before setTimeout()'); setTimeout(callback, 1000); // 1秒钟后调用callback函数 console.log('after setTimeout()');
观察上述代码执行,在Chrome的控制台输出可以看到:
before setTimeout() after setTimeout() (等待1秒后) Done
可见,异步操作会在将来的某个时间点触发一个函数调用。
AJAX就是典型的异步操作。以上一节的代码为例:
request.onreadystatechange = function () { if (request.readyState === 4) { if (request.status === 200) { return success(request.responseText); } else { return fail(request.status); } } }
把回调函数
success(request.responseText)和
fail(request.status)写到一个AJAX操作里很正常,但是不好看,而且不利于代码复用。
有没有更好的写法?比如写成这样:
var ajax = ajaxGet('http://...'); ajax.ifSuccess(success) .ifFail(fail);
这种链式写法的好处在于,先统一执行AJAX逻辑,不关心如何处理结果,然后,根据结果是成功还是失败,在将来的某个时候调用
success函数或
fail函数。
古人云:“君子一诺千金”,这种“承诺将来会执行”的对象在JavaScript中称为Promise对象。
Promise有各种开源实现,在ES6中被统一规范,由浏览器直接支持。
我们先看一个最简单的Promise例子:生成一个0-2之间的随机数,如果小于1,则等待一段时间后返回成功,否则返回失败:
function test(resolve, reject) { var timeOut = Math.random() * 2; log('set timeout to: ' + timeOut + ' seconds.'); setTimeout(function () { if (timeOut < 1) { log('call resolve()...'); resolve('200 OK'); } else { log('call reject()...'); reject('timeout in ' + timeOut + ' seconds.'); } }, timeOut * 1000); }
这个
test()函数有两个参数,这两个参数都是函数,如果执行成功,我们将调用
resolve('200 OK'),如果执行失败,我们将调用
reject('timeout in ' + timeOut + ' seconds.')。可以看出,
test()函数只关心自身的逻辑,并不关心具体的
resolve和
reject将如何处理结果。
有了执行函数,我们就可以用一个Promise对象来执行它,并在将来某个时刻获得成功或失败的结果:
var p1 = new Promise(test); var p2 = p1.then(function (result) { console.log('成功:' + result); }); var p3 = p2.catch(function (reason) { console.log('失败:' + reason); });
变量
p1是一个Promise对象,它负责执行
test函数。由于
test函数在内部是异步执行的,当
test函数执行成功时,我们告诉Promise对象:
// 如果成功,执行这个函数: p1.then(function (result) { console.log('成功:' + result); });
当
test函数执行失败时,我们告诉Promise对象:
p2.catch(function (reason) { console.log('失败:' + reason); });
Promise对象可以串联起来,所以上述代码可以简化为:
new Promise(test).then(function (result) { console.log('成功:' + result); }).catch(function (reason) { console.log('失败:' + reason); });
实际测试一下,看看Promise是如何异步执行的:
'use strict'; // 清除log: var logging = document.getElementById('test-promise-log'); while (logging.children.length > 1) { logging.removeChild(logging.children[logging.children.length - 1]); } // 输出log到页面: function log(s) { var p = document.createElement('p'); p.innerHTML = s; logging.appendChild(p); }
new Promise(function (resolve, reject) {
log('start new Promise...');
var timeOut = Math.random() * 2;
log('set timeout to: ' + timeOut + ' seconds.');
setTimeout(function () {
if (timeOut < 1) {
log('call resolve()...');
resolve('200 OK');
}
else {
log('call reject()...');
reject('timeout in ' + timeOut + ' seconds.');
}
}, timeOut * 1000);
}).then(function (r) {
log('Done: ' + r);
}).catch(function (reason) {
log('Failed: ' + reason);
});
可见Promise最大的好处是在异步执行的流程中,把执行代码和处理结果的代码清晰地分离了:
Promise还可以做更多的事情,比如,有若干个异步任务,需要先做任务1,如果成功后再做任务2,任何任务失败则不再继续并执行错误处理函数。
要串行执行这样的异步任务,不用Promise需要写一层一层的嵌套代码。有了Promise,我们只需要简单地写:
job1.then(job2).then(job3).catch(handleError);
其中,
job1、
job2和
job3都是Promise对象。
下面的例子演示了如何串行执行一系列需要异步计算获得结果的任务:
'use strict'; var logging = document.getElementById('test-promise2-log'); while (logging.children.length > 1) { logging.removeChild(logging.children[logging.children.length - 1]); } function log(s) { var p = document.createElement('p'); p.innerHTML = s; logging.appendChild(p); }
// 0.5秒后返回input*input的计算结果:
function multiply(input) {
return new Promise(function (resolve, reject) {
log('calculating ' + input + ' x ' + input + '...');
setTimeout(resolve, 500, input * input);
});
}
// 0.5秒后返回input+input的计算结果:
function add(input) {
return new Promise(function (resolve, reject) {
log('calculating ' + input + ' + ' + input + '...');
setTimeout(resolve, 500, input + input);
});
}
var p = new Promise(function (resolve, reject) {
log('start new Promise...');
resolve(123);
});
p.then(multiply)
.then(add)
.then(multiply)
.then(add)
.then(function (result) {
log('Got value: ' + result);
});
setTimeout可以看成一个模拟网络等异步执行的函数。现在,我们把上一节的AJAX异步执行函数转换为Promise对象,看看用Promise如何简化异步处理:
'use strict'; // ajax函数将返回Promise对象: function ajax(method, url, data) { var request = new XMLHttpRequest(); return new Promise(function (resolve, reject) { request.onreadystatechange = function () { if (request.readyState === 4) { if (request.status === 200) { resolve(request.responseText); } else { reject(request.status); } } }; request.open(method, url); request.send(data); }); }
var log = document.getElementById('test-promise-ajax-result');
var p = ajax('GET', '/api/categories');
p.then(function (text) { // 如果AJAX成功,获得响应内容
log.innerText = text;
}).catch(function (status) { // 如果AJAX失败,获得响应代码
log.innerText = 'ERROR: ' + status;
});
除了串行执行若干异步任务外,Promise还可以并行执行异步任务。
试想一个页面聊天系统,我们需要从两个不同的URL分别获得用户的个人信息和好友列表,这两个任务是可以并行执行的,用
Promise.all()实现如下:
var p1 = new Promise(function (resolve, reject) { setTimeout(resolve, 500, 'P1'); }); var p2 = new Promise(function (resolve, reject) { setTimeout(resolve, 600, 'P2'); }); // 同时执行p1和p2,并在它们都完成后执行then: Promise.all([p1, p2]).then(function (results) { console.log(results); // 获得一个Array: ['P1', 'P2'] });
有些时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用
Promise.race()实现:
var p1 = new Promise(function (resolve, reject) { setTimeout(resolve, 500, 'P1'); }); var p2 = new Promise(function (resolve, reject) { setTimeout(resolve, 600, 'P2'); }); Promise.race([p1, p2]).then(function (result) { console.log(result); // 'P1' });
由于
p1执行较快,Promise的
then()将获得结果
'P1'。
p2仍在继续执行,但执行结果将被丢弃。
如果我们组合使用Promise,就可以把很多异步任务以并行和串行的方式组合起来执行。
Canvas
Canvas是HTML5新增的组件,它就像一块幕布,可以用JavaScript在上面绘制各种图表、动画等。没有Canvas的年代,绘图只能借助Flash插件实现,页面不得不用JavaScript和Flash进行交互。有了Canvas,我们就再也不需要Flash了,直接使用JavaScript完成绘制。
一个Canvas定义了一个指定尺寸的矩形框,在这个范围内我们可以随意绘制:
<canvas id="test-canvas" width="300" height="200"></canvas>
由于浏览器对HTML5标准支持不一致,所以,通常在
<canvas>内部添加一些说明性HTML代码,如果浏览器支持Canvas,它将忽略
<canvas>内部的HTML,如果浏览器不支持Canvas,它将显示
<canvas>内部的HTML:
<canvas id="test-stock" width="300" height="200"> <p>Current Price: 25.51</p> </canvas>
getContext('2d')方法让我们拿到一个
CanvasRenderingContext2D对象,所有的绘图操作都需要通过这个对象完成。
var ctx = canvas.getContext('2d');
如果需要绘制3D怎么办?HTML5还有一个WebGL规范,允许在Canvas中绘制3D图形:
gl = canvas.getContext("webgl");
本节我们只专注于绘制2D图形。
绘制形状
我们可以在Canvas上绘制各种形状。在绘制前,我们需要先了解一下Canvas的坐标系统:Canvas的坐标以左上角为原点,水平向右为X轴,垂直向下为Y轴,以像素为单位,所以每个点都是非负整数。
CanvasRenderingContext2D对象有若干方法来绘制图形:
'use strict'; var canvas = document.getElementById('test-shape-canvas'), ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, 200, 200); // 擦除(0,0)位置大小为200x200的矩形,擦除的意思是把该区域变为透明
ctx.fillStyle = '#dddddd'; // 设置颜色
ctx.fillRect(10, 10, 130, 130); // 把(10,10)位置大小为130x130的矩形涂色
// 利用Path绘制复杂路径:
var 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);
绘制文本
绘制文本就是在指定的位置输出文本,可以设置文本的字体、样式、阴影等,与CSS完全一致:'use strict'; var canvas = document.getElementById('test-text-canvas'), ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.shadowBlur = 2;
ctx.shadowColor = '#666666';
ctx.font = '24px Arial';
ctx.fillStyle = '#333333';
ctx.fillText('带阴影的文字', 20, 40);
Canvas除了能绘制基本的形状和文本,还可以实现动画、缩放、各种滤镜和像素转换等高级操作。如果要实现非常复杂的操作,考虑以下优化方案:
通过创建一个不可见的Canvas来绘图,然后将最终绘制结果复制到页面的可见Canvas中;
尽量使用整数坐标而不是浮点数;
可以创建多个重叠的Canvas绘制不同的层,而不是在一个Canvas中绘制非常复杂的图;
背景图片如果不变可以直接用
<img>标签并放到最底层。
相关文章推荐
- [笔记][Java7并发编程实战手册]2.5使用Lock实现同步一
- Java构造方法
- corejava知识---【一】
- 属性名前两个字母要么都是大写的要么都是小写的(当然数字与_、$算小写)
- java synchronized静态同步方法与非静态同步方法,同步语句块
- Java回顾之I/O
- Java io文件操作(按字符、字节、行、随机读取,追加,文件操作)
- java中classpath的详解
- JAVA字节流(读写文件)
- 【Java多线程】之十:Timer and TimerTask
- 华为上机 循环报数 java
- 浅析Java中的反射机制原理
- JAVA的整型与字符串相互转换
- struts注解(新旧对比)
- 黑马程序员——其他8:eclipse使用技巧总结
- java多线程编程
- Spring JdbcTemplate笔记
- 华为机试——两个超长正整数的加法 java
- 华为机试题 最小长方形 java 转载
- java多线程零碎知识点