Javascript基本认识
入门
立即调用的函数表达式
IIFE = Immediately-Invoked Function Expression
可避免污染全局变量。
数据类型
数组中空位(hole)
length会算上空位。
foreach,for…in, Object.keys方法遍历时会跳过空位。
运算符
指数 运算符是**,是右结合的运算符。
2**4= 16 amazing
2** 3** 2 =512 === 2** (3**2 )
对象object的转化机制
除了data对象这种特殊的对象,会先执行toString方法,其他一律先执行valueOf 方法,然后在执行toString。
在执行比较时object会先进行转化
===严格相等运算在比较复合类型值时,比较的是地址。
undefined 与 null 进行对比
undefined == null
结果为true,与其他对比时均为false
运算符
布尔运算
执行短路策略,如果第一个数判断不能确认最终布尔运算的结果,那么就返回符合后面的值(是值,而不是布尔值)
0 || 8888 || 9999
第一个数为0不能判断,那么计算机这一步返回 8888 || 9999 , 这一步ok,返回true。
0 || false|| “” ==> 返回 “”
text = text || “” 或运算可以用来设置默认值。
位运算
一个数与自身的取反值相加,等于-1。因为负数是用补码表示的:
补码=反码+1。
那么一个数自身的相反与之相加,就一定是全1,全1 是一个负数,就是一个补码,这个补码对应的原码=~(补码-1)=1,
补码=-原码,因此也就是说全1对应的二进制数就是-1。上诉结论没毛病。
~~2.9 等于 2 ,双取反相当于取整,舍弃小数部分。
语法专题
null转化为数值时为0,undefined转为数值时为NaN。
null+1=1 ; undefined + 1 = NaN
标准库
属性描述对象
obj = {p:‘jewinh’}
p,是对象的属性,同时,他也是一个对象,也叫作属性描述对象:
{
value: 123,
writable: false,//值是否可写
enumerable: true,//是否可被遍历
configurable: false,//定义这个属性的属性描述对象是否可配置
get: undefined,//
set: undefined
}
enumerable 可以设置隐藏属性。以下3种情况不会把隐藏属性搞出来。
- for … in …循环
- Object.keys方法
- JSON.stringify方法
configurable 与 writable 只要有一个为true,value就可以被改写。configurable 为false 时,writeable可置为false,不能置为true。
属性描述对象可以被控制为不能拓展。preventExtension
seal(密封,海豹) ,he sealed the envelope and put on a stamp.
Object.seal 方法, 相当于configurable : false 。 锁住谁也不许走。
freeze,连值也不让改,相当于seal 之后,writable:false
Array对象
Array
Array.isArray(arr) 可以判断变量arr是否为数组,而typeof arr 返回的是object
push ,pop , shift ,unshift , 在数组头或尾添加或者删除一个元素。
join (把数组中所有的元素,用某个特定的字符串链接起来。)
concat , 合并数组,返回数组对象的引用,属于浅拷贝,如果
reverse 颠倒 , slice 切片,[slais],可以把类数组对象转化为数组
Array.prototype.slice.call({ 0 : ‘a’ , 1 : ‘b’ , length : 2 })
splice : he taught me to edit and splice film 剪接。
arr.splice(start splice , how long do you want to splice)
sort
[10111, 1101, 111].sort(function (a, b) {
return a - b;
})
// [111, 1101, 10111]
//从小到大排列
//sort后面跟的函数的运行原理,如果a-b小于0,则a一定在b之前。
map
arr.map(func(elem , index ,arr )) , 可以接受元素,索引,数组本身,这三个参数。
注:map会跳过空位(empty)
1.不修改arr本身
2.执行原理:相当于把arr中所有的值当做输入参数,给func,把返回值对应的放在一个新的数组。
arr.map(func(e,i,a),newArr),newArr 绑定func中的this变量。
foreach
与map相似,但是不会有返回值,map返回一个操作后的新的数组。foreach 无法break,会跳过空位
fliter使用方法与map类似
把过滤原则写到函数中即可,某元素运算完成后,return true的元素留下来。
some(func),every(func)
some ,有一个元素使得func返回true,就返回true
every,所有元素使得func都返回true,才返回true。
特别的,空数组,[],some返回false,every返回true。
reduce, reduceRight
reduce 从左往右,+个right,就变成从右往左。
传入的函数arr.reduce( func(累计值,当前值) , 初始值 )
每次运行函数return的值都会存在累计值中。
- 初始值有时,初始值给累计值,当前值为数组第一个元素
- 初始值没时,累计值等于第一个元素,当前值等于第二个元素。
indexOf() , lastIndexOf()
索引,找到第一个出现或者最后一个出现这个元素的
NaN无法被识别,因为内部使用的是“===”,因为NaN === NaN 会返回false。
包装对象
String,Number,Boolean 三个函数。
- +new , var str = new String(‘abc’) ,str 就是一个对象
- 不加new时,String函数就是可以把其他数据类型转化为字符串。
用方法valueOf可以让上诉三个函数生成的对象变回原来的值。
基本类型转换为对象类型时,这个对象是暂时的,调用完毕后该对象自动销毁,下一次调用时,他又重新生成新的对象。
如果需要添加,则需要使用他的原型对象,prototype。
对象进行逻辑运算时,自动转化为true
Boolean
- +new,返回的是对象,在if中时,就一定判为
true
- 不加new ,就等效于双重否定:!!
Number
- Number有一些静态属性,类似于宏定义,就是定义了正无穷,负无穷等一系列的值。
- (数字).toString(进制数),直接toString会默认转化为10进制。
- 可以支持自定义方法,prototype
String
- 定义在对象本身,不是定义在对象实例中的方法,称之为静态方法。例如:String.fromCharCode(104,101,108,108,110)–>hello
trim(),用来去除字符串两端的 换行,制表,回车,空格符。
substring,substr , slice , indexOf , match , search , replace , split ,
Math
- 静态属性:有一些特殊值可以直接调用。
- 静态方法: abs , ceil(天花板的意思) , floor(底板的意思) , random ,round
- 三角函数:sin ,cos ,tan ,asin ,acos ,atan
面向对象
new
使用new+构造函数,会新建一个空对象,构造函数中的this,就指向这个新的空对象。同new会返回这个新建的对象
this
应避免循环套用,可使用that把this指向的东西给that,确保指针是你所期望指向的地方。
call
func.call(thisvalue, arg1,arg2)
函数的call方法,就是可以指定函数内部this要指定的对象,以及需要输入的参数。
在call中使用this,那么this就指向func,后面跟参数
当对象的原生方法被继承使用后,正常调用将无法使用原来的函数,这是可以采用call方法实现调用:
Object.prototype.hasOwnProperty.call(obj, 'toString')
hasOwnProperty是对象数据类型的原型函数,.call方法就可以让强行让obj成为Object中的this,而这个hasOwnProperty就是没修改过的,原版函数,这样调用就不受继承后覆盖影响啦。
apply
与call类似,都是改变this的指向,不过apply第二个参数是传入数组。这个机制可以做出一些事情:
//数组求最大值
var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15
//数组空元素转化为undefine
Array.apply(null, ['a', ,'b'])
// [ 'a', undefined, 'b' ]
//对象转化为数组, apply参数为对象,返回的为数组
Array.prototype.slice.apply({
0: 1, length: 1}) // [1]
Array.prototype.slice.apply({
0: 1}) // []
Array.prototype.slice.apply({
0: 1, length: 2}) // [1, undefined]
Array.prototype.slice.apply({
length: 1}) // [undefined]
bind
当新建一个指针时,若赋值的函数中的含有this,那么就容易出现指向错误的问题,那么就可以用bind,把函数中的this,绑定给一个你想让他绑定的对象,这样就可以避免错误。
除了可以绑定函数外,还可以绑定参数。
element.addEventListener('click', o.m.bind(o));
//bind生成的匿名函数没有东西存,后面如果想取消绑定,将无法取消。
//当下次点击的时候,就不能重新绑定,就导致下面函数失效。
element.removeEventListener('click', o.m.bind(o));
//正确的做法是用一个变量存起来。
var listener = o.m.bind(o);
element.addEventListener('click', listener);
// ...
element.removeEventListener('click', listener);
bind 方法结合 call 方法使用:
call方法第一个参数是一个对象,
[1, 2, 3].slice(0, 1) // [1]
// 等同于
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]
//改写成下面这样:
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]
//这段的意思是,Function有一个call函数.
//现在将Function绑定Array.prototype.slice.
//新的slice函数指向这个call函数
/*
var Function={
...
prototype:{
call:function(){
this...
//call函数的操作。这里的this也是call的第一个参数
//当使用.bind(Array.prototype.slice)方法时,
//这里的this本来是指向Function.prototype的
//现在把这里的this绑定到Array.prototype.slice
//所以上下两者完全等价。
}
}
}
*/
对象的继承
prototype
当构造对象时,构造函数中的prototype属性会自动被实例对象继承,相当于实例对象中有一组指针指向构造函数中的prototype属性。
访问的顺序是,先访问实例对象定义的属性,如果实例中有,读实例中的,如果实例中没有,则读原型对象中的。
原型链
原型是一路继承过来的,所有函数都可以被当做构造函数,最后的尽头是null,Object.prototype的对象原型是null。这就是尽头。
constructor()
这是一个实例对象指向构造函数的指针,有了这个方法,实例对象就可以调用自身构造函数。
instanceof
实例函数 instanceof 构造函数
检查的是整个原型链,就是实例函数的原型链中,是否包含构造函数
构造函数.prototype.isPrototypeOf(实例函数)
检查的是
构造函数继承
function Sub(value) {
Super.call(this);
//把Sub的父类Super中的this指向Sub。相当于把父类的属性整个搬过来。
this.prop = value;
}
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
多重继承小技巧
function M1() {
this.hello = 'hello';
}
function M2() {
this.world = 'world';
}
function S() {
M1.call(this);
M2.call(this);
}
// 继承 M1
S.prototype = Object.create(M1.prototype);
// 继承链上加入 M2
Object.assign(S.prototype, M2.prototype);
// 指定构造函数
S.prototype.constructor = S;
var s = new S();
s.hello // 'hello'
s.world // 'world'
异步操作概述
主线程会去执行同步任务,同步任务完成后,看异步任务是否已经满足执行条件,满足就进入主线程中执行,这时就变成同步任务了,然后一个个执行异步任务,当任务队列清空,程序就运行结束。
异步任务的写法通常是采用回调函数,一旦异步任务重新进入主线程,就会执行相应的回调函数,如果没有回调函数,就不会进入任务队列。
Event Loop ,事件循环,事件循环机制不断在扫码异步任务是否已完成,并判断是否进入主线程。
异步操作模式
回调函数
function f1(callback) {
// ...
callback();
}
function f2() {
// ...
}
f1(f2);
//显然,当函数f1执行完之后,才会调用f2。
//这种写法的缺陷很明显,f1函数里面有一句调用f2,会导致代码的耦合程度很高,不利于维护。
事件监听
f1.on('done', f2);
function f1() {
setTimeout(function () {
// ...
f1.trigger('done');
}, 1000);
}
//这种做法有点像Linux中的信号灯,通过trigger给引擎说,我这边已经完成,可以执行我绑定的函数了。这种去耦合有利于实现模块化。但也使得代码阅读时比较难看出谁是主流程。
发布/订阅
publish - subscribe pattern 发布/订阅模式,也可以叫做观察者模式observe pattern
//这个模式有多种实现,下面采用的是 Ben Alman 的 Tiny Pub/Sub,这是 jQuery 的一个插件。
jQuery.subscribe('done', f2);
function f1() {
setTimeout(function () {
// ...
jQuery.publish('done');
}, 1000);
}
jQuery.unsubscribe('done', f2);
//这种做法的好处是可以监控程序的运行。可以通过查看“消息中心”,了解有多少信号,每个信号有多少订阅者。
异步操作流程控制
串行执行:
function final(value) {
console.log('完成: ', value);
}
async(1, function (value) {
async(2, function (value) {
async(3, function (value) {
async(4, function (value) {
async(5, function (value) {
async(6, final);
});
});
});
});
});
// 参数为 1 , 1秒后返回结果
// 参数为 2 , 1秒后返回结果
// 参数为 3 , 1秒后返回结果
// 参数为 4 , 1秒后返回结果
// 参数为 5 , 1秒后返回结果
// 参数为 6 , 1秒后返回结果
// 完成: 12
上面代码中,六个回调函数的嵌套,不仅写起来麻烦,容易出错,而且难以维护。
串行执行
我们可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行。
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
function async(arg, callback) {
console.log('参数为 ' + arg +' , 1秒后返回结果');
setTimeout(function () {
callback(arg * 2); }, 1000);
}
function final(value) {
console.log('完成: ', value);
}
function series(item) {
if(item) {
async( item, function(result) {
results.push(result);
return series(items.shift());
});
} else {
return final(results[results.length - 1]);
}
}
series(items.shift());
并行执行:
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
function async(arg, callback) {
console.log('参数为 ' + arg +' , 1秒后返回结果');
setTimeout(function () {
callback(arg * 2); }, 1000);
}
function final(value) {
console.log('完成: ', value);
}
items.forEach(function(item) {
async(item, function(result){
results.push(result);
if(results.length === items.length) {
final(results[results.length - 1]);
}
})
});
并行与串行结合:
原因是,如果单用串行,太慢,单用并行运行速度快,但会占用大量系统资源,因此两者结合使用,可以调节到一个想要的速度。
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
var running = 0;
var limit = 2;
function async(arg, callback) {
console.log('参数为 ' + arg +' , 1秒后返回结果');
setTimeout(function () {
callback(arg * 2); }, 1000);
}
function final(value) {
console.log('完成: ', value);
}
function launcher() {
while(running < limit && items.length > 0) {
var item = items.shift();
async(item, function(result) {
results.push(result);
running--;
if(items.length > 0) {
launcher();
} else if(running == 0) {
final(results);
}
});
running++;
}
}
launcher();
定时器
setTimeout()
var timerId = setTimeout(func|code, delay);
//最后两个参数将在1000ms之后,作为回调函数的参数,赋值给回调函数。
setTimeout(function (a,b) {
console.log(a + b);
}, 1000, 1, 1);
//对于对象方法,如果直接使用会使得对象中直接指向this。
//可以通过bind或者放入一个函数中避免。
var x = 1;
var obj = {
x: 2,
y: function () {
console.log(this.x);
}
};
//直接引用
setTimeout(obj.y, 1000) // 1
//放入一个函数中
setTimeout(function () {
obj.y();
}, 1000);
// 2
//使用bind绑定一下。
setTimeout(obj.y.bind(obj), 1000) //2
setInterval()
interval 间隔的意思,每隔一段时间执行一下,是个死循环
//每过1000ms就执行一次。
var i = 1
var timer = setInterval(function() {
console.log(2);
}, 1000)
//setInterval 有一个问题,它是每次开始就会开始计时,而不是每次结束后,这样就会导致如果执行太慢,两次执行的时间就很短,很不确定,因此可以使用setTimeout代替。
var i = 1;
var timer = setTimeout(function f() {
// ...
timer = setTimeout(f, 2000);
}, 2000);
clearTimeout & clearInterval
setTimeout & setInterval 两个函数执行后会返回计数器编号,将该编号传给clearTimeout & clearInterval 可以取消对应的定时器。
var id1 = setTimeout(f, 1000);
var id2 = setInterval(f, 1000);
clearTimeout(id1);
clearInterval(id2);
debounce(防抖动)
防抖动设计代码:
$('textarea').on('keydown', debounce(ajaxAction, 2500));
function debounce(fn, delay){
var timer = null; // 声明计时器
return function() {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}
运行机制
//setTimeout , setInterval 两个函数,如果后面跟了一个需要运行很久的任务(超过100ms),那么就相当于阻塞了。到了100ms之后,会等待,直到verylongTask执行完毕后,someTask会立刻执行。
setTimeout(someTask, 100);
veryLongTask();
//如果设置的延时为0,并不会立刻执行,因为setTimeout是异步任务,引擎首先执行同步任务。实际上,设置为0并不会是为0,就是说这个延时设置有一个下限,Edge浏览器为4ms,如果电脑使用电池则为16毫秒...
//总的原则是,尽量节约系统资源,因为它是异步任务,对及时性要求不需要太高。
setTimeout(function () {
console.log(1);
}, 0);
console.log(2);
// 2
// 1
//setTimeout(f, 0)的几个非常重要的用途:
//1.调整事件的发生顺序
//点击之后,先执行A,但是A里面是一个异步任务,因此会等到下一轮事件循环执行。此时C应先于B执行。
// HTML 代码如下
// <input type="button" id="myButton" value="click">
var input = document.getElementById('myButton');
input.onclick = function A() {
setTimeout(function B() {
input.value +=' input';
}, 0)
};
document.body.onclick = function C() {
input.value += ' body'
};
//2.延后某些任务的触发
// HTML 代码如下
// <input type="text" id="input-box">
document.getElementById('input-box').onkeypress = function (event) {
this.value = this.value.toUpperCase();
}
//用户输入字符时,只会把当前输入字符之前的字符转化为大写,因为最后一个输入的字符没有发送给浏览器,浏览器不知道。所以应该延迟一点点,等输入之后,‘过一会’ 再发过去,就ok
//改写成下面这样就可以,每次输入完了,才执行,起到“等一等”的作用。
document.getElementById('input-box').onkeypress = function() {
var self = this;
setTimeout(function() {
self.value = self.value.toUpperCase();
}, 0);
}
//3.避免DOM造作堆积,JavaScript 执行速度远高于 DOM
var div = document.getElementsByTagName('div')[0];
// 写法一
for (var i = 0xA00000; i < 0xFFFFFF; i++) {
div.style.backgroundColor = '#' + i.toString(16);
}
// 写法二
var timer;
var i=0x100000;
function func() {
timer = setTimeout(func, 0);
div.style.backgroundColor = '#' + i.toString(16);
if (i++ == 0xFFFFFF) clearTimeout(timer);
}
timer = setTimeout(func, 0);
//写法一会造成浏览器“堵塞”,写法二则避免了这一点。这就是setTimeout(f, 0)的好处。
promise对象
没有promise时,异步任务会横向发展而不是向下发展,就会变得很混乱。promise对象解决了这一问题,
promise有三种状态,未完成(pending), 成功(fulfilled),失败(rejected)
执行的结果只有两种:
- 异步操作成功,Promise 实例传回一个值(value),状态变为
fulfilled
。 - 异步操作失败,Promise 实例抛出一个错误(error),状态变为
rejected
。
JS提供原生的Promise构造函数,用来生成Promise实例:
var promise = new Promise(function (resolve, reject) {
// ...
if (/* 异步操作成功 */){
resolve(value);
} else {
/* 异步操作失败 */
reject(new Error());
}
});
//上面代码中,Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。
//resolve函数的作用是,将Promise实例的状态从“未完成”变为“成功”(即从pending变为fulfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。reject函数的作用是,将Promise实例的状态从“未完成”变为“失败”(即从pending变为rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
//下面是一个例子。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100)
//then方法使用,接收两个回调函数,第一个为成功时的操作,第二个为失败时的操作。
var p1 = new Promise(function (resolve, reject) {
resolve('成功');
});
p1.then(console.log, console.error);
// "成功"
var p2 = new Promise(function (resolve, reject) {
reject(new Error('失败'));
});
p2.then(console.log, console.error);
// Error: 失败
//then方法链式使用
p1
.then(step1)
.then(step2)
.then(step3)
.then(
console.log,
console.error
);
//原则是错误会传递,成功不会往下传。
微任务
//then属于微任务,是本轮事件循环执行的,而setTimeout是下一轮事件开始时执行。
//执行顺序:同步任务>微任务(then)>setTimeout
setTimeout(function() {
console.log(1);
}, 0);
new Promise(function (resolve, reject) {
resolve(2);
}).then(console.log);
console.log(3);
// 3
// 2
// 1
DOM
DOM的最小组成单位为节点node,
节点的类型有七种。
Document
:整个文档树的顶层节点DocumentType
:doctype
标签(比如<!DOCTYPE html>
)Element
:网页的各种HTML标签(比如<body>
、<a>
等)Attribute
:网页元素的属性(比如class="right"
)Text
:标签之间或标签包含的文本Comment
:注释DocumentFragment
:文档的片段
节点树
浏览器提供document节点,代表整个文档。
文档的第一层只有一个节点<html>
,这个节点是根节点root node,除根节点外,其他节点都有三种层级关系。
- 父节点关系(parentNode):直接的那个上级节点
- 子节点关系(childNodes):直接的下级节点
- 同级节点关系(sibling):拥有同一个父节点的节点
DOM提供操作接口用来获取三种关系的节点。
子节点接口:firstChild , lastChild 等属性
同级节点:nextSibling , previousSibling
node接口
属性
nodeType
返回一个整数值,表示节点类型
document.nodeType // 9
不同节点的nodeType
属性值和对应的常量如下。
- 文档节点(document):9,对应常量
Node.DOCUMENT_NODE
- 元素节点(element):1,对应常量
Node.ELEMENT_NODE
- 属性节点(attr):2,对应常量
Node.ATTRIBUTE_NODE
- 文本节点(text):3,对应常量
Node.TEXT_NODE
- 文档片断节点(DocumentFragment):11,对应常量
Node.DOCUMENT_FRAGMENT_NODE
- 文档类型节点(DocumentType):10,对应常量
Node.DOCUMENT_TYPE_NODE
- 注释节点(Comment):8,对应常量
Node.COMMENT_NODE
确定节点类型时,使用nodeType
属性是常用方法。
var node = document.documentElement.firstChild;
if (node.nodeType === Node.ELEMENT_NODE) {
console.log('该节点是元素节点');
}
nodeName
属性返回节点的名称。
// HTML 代码如下
// <div id="d1">hello world</div>
var div = document.getElementById('d1');
div.nodeName // "DIV"
上面代码中,元素节点<div>
的nodeName
属性就是大写的标签名DIV
。
不同节点的nodeName
属性值如下。
- 文档节点(document):
#document
- 元素节点(element):大写的标签名
- 属性节点(attr):属性的名称
- 文本节点(text):
#text
- 文档片断节点(DocumentFragment):
#document-fragment
- 文档类型节点(DocumentType):文档的类型
- 注释节点(Comment):
#comment
nodeValue
属性返回一个字符串,表示当前节点本身的文本值,该属性可读写。
只有文本节点(text)、注释节点(comment)和属性节点(attr)有文本值,因此这三类节点的nodeValue
可以返回结果,其他类型的节点一律返回null
。同样的,也只有这三类节点可以设置nodeValue
属性的值,其他类型的节点设置无效。
// HTML 代码如下
// <div id="d1">hello world</div>
var div = document.getElementById('d1');
div.nodeValue // null
div.firstChild.nodeValue // "hello world"
上面代码中,div
是元素节点,nodeValue
属性返回null
。div.firstChild
是文本节点,所以可以返回文本值。
testContent
返回当前节点和它的所有后代节点的文本内容。
// HTML 代码为
// <div id="divA">This is <span>some</span> text</div>
document.getElementById('divA').textContent
// This is some text
…还有其他属性,不一一列举。
方法
var p = document.createElement('p');
document.body.appendChild(p);
//上面代码新建一个<p>节点,将其插入document.body的尾部。
节点树跟文件目录很像,它定义了一些方法,无非就是对这些节点增删改查,没有什么特别的。非常像文件io。
mutation observer API(变动观察者)
DOM发生变动,就会触发这个API。可以类比成操作系统监听文件系统发生的事情。
var article = document.querySelector('article');
var options = {
'childList': true,
'attributes':true
} ;
observer.observe(article, options);
- childList:子节点的变动(指新增,删除或者更改)。
- attributes:属性的变动。
- characterData:节点内容或节点文本的变动。
mutation observer 处理的就是一个个 MutationRecord 实例所组成的数组。
MutationRecord
对象包含了DOM的相关信息,有如下属性:
type
:观察的变动类型(attributes
、characterData
或者childList
)。target
:发生变动的DOM节点。addedNodes
:新增的DOM节点。removedNodes
:删除的DOM节点。previousSibling
:前一个同级节点,如果没有则返回null
。nextSibling
:下一个同级节点,如果没有则返回null
。attributeName
:发生变动的属性。如果设置了attributeFilter
,则只返回预先指定的属性。oldValue
:变动前的值。这个属性只对attribute
和characterData
变动有效,如果发生childList
变动,则返回null
。
有了这一章节的内容,我们就好像拥有了操作系统一样,可以合理的管理好网页文本资源,非常的棒。因此初学者应该学DOM。
事件
EventTarget
DOM的时间操作(监听+触发)都是定义在EventTarget这个借口中。所有节点对象都部署了这个接口,其他一些需要事件通信的浏览器内置对象(XMLhttpRequest , AudioNode , AudioContext )也部署了这个接口。
EventTarget 主要提供了三个实例方法:
addEventListener :绑定事件的监听函数
removeEventListener:移除事件的监听函数
dispatchEvent:触发事件 (dispatch 调度的意思)
addEventListener
//范式
target.addEventListener(type, listener[, useCapture]);
//例子
function hello() {
console.log('Hello world');
}
var button = document.getElementById('btn');
button.addEventListener('click', hello, false);
该方法接受三个参数。
type
:事件名称,大小写敏感。listener
:监听函数。事件发生时,会调用该监听函数。useCapture
:布尔值,表示监听函数是否在捕获阶段(capture)触发(参见后文《事件的传播》部分),默认为false
(监听函数只在冒泡阶段被触发)。该参数可选。
关于这些参数更详细的论述,用到再查。
removeEventListener
匿名函数无法移除,输入参数与addEventListener不同无法移除。
dispatchEvent
//这个函数是判断一件事是否被调度?没太理解,先往后看。相当于信号灯之类的东西?
para.addEventListener('click', hello, false);
var event = new Event('click');
para.dispatchEvent(event);
//dispatchEvent函数可以判断对象是否被取消。
var canceled = !cb.dispatchEvent(event);
if (canceled) {
console.log('事件取消');
} else {
console.log('事件未取消');
}
事件模型
监听函数listener,这是事件驱动编程模式的主要编程方法
绑定监听函数
javascript有三种方法,可以为事件绑定监听函数
-
HTML的on - 属性
<!-- 直接在html中定义某些事件的监听代码。 --> <body onload="doSomething()"> <div onclick="console.log('触发事件')"> <!-- 这种方法的事件,是把on-xx后面写的事件,载入到js引擎中执行,因此要加(),执行时,是冒泡式执行,先执行子元素,再执行父元素。 当然,因为是载入到js引擎中执行,我们也可以直接用js来替代这种做法,只是用DOM去定位的时候稍微麻烦一些。 --> <div onClick="console.log(2)"> <button onClick="console.log(1)">点击</button> </div> el.setAttribute('onclick', 'doSomething()'); // 等同于 // <Element onclick="doSomething()">
-
元素节点的事件属性
//元素节点对象有事件属性,同样可以指定监听函数,这里不用加(),同样的,也只在冒泡阶段触发。 window.onload = doSomething; div.onclick = function (event) { console.log('触发事件'); };
-
addEventListener()
DOM节点实例,都有addEventListener方法,
1,2各有缺点,都不推荐使用,主推3。原因:
都有这个接口,可以绑定多个,可以指定什么阶段触发。
this的指向
可以认为绑定监听事件,就是bind了一下,这时this必定指向绑定的对象。
事件的传播propagation
什么是冒泡阶段,什么是捕获阶段???这里给出解释
一个事件被触发后,会在子元素和父元素之间传播,传播分三个阶段:
-
第一阶段,从window对象传导到目标节点(上层传到底层),称为"捕获阶段capture phase"。
类比思考:相当于我要打开一个文件,要找到这个文件的路径,捕获到路径了,才能打开,行业术语,也就是捕获。正在找
-
第二阶段,在目标节点上触发,称为"目标阶段target phase"
类比思考:找到。
-
第三阶段,从目标节点传导回windows对象(从底层传递回上层),称为"冒泡阶段bubbling phase"
举个例子:
<div>
<p>点击</p>
</div>
var phases = {
1: 'capture',
2: 'target',
3: 'bubble'
};
var div = document.querySelector('div');
var p = document.querySelector('p');
div.addEventListener('click', callback, true);
p.addEventListener('click', callback, true);
div.addEventListener('click', callback, false);
p.addEventListener('click', callback, false);
function callback(event) {
var tag = event.currentTarget.tagName;
var phase = phases[event.eventPhase];
console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
}
// 点击以后的结果
// Tag: 'DIV'. EventPhase: 'capture'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'DIV'. EventPhase: 'bubble'
注意,浏览器总是假定click
事件的目标节点,就是点击位置嵌套最深的那个节点(本例是<div>
节点里面的<p>
节点)。所以,<p>
节点的捕获阶段和冒泡阶段,都会显示为target
阶段。
事件传播的最上层对象是window
,接着依次是document
,html
(document.documentElement
)和body
(document.body
)。也就是说,上例的事件传播顺序,在捕获阶段依次为window
、document
、html
、body
、div
、p
,在冒泡阶段依次为p
、div
、body
、html
、document
、window
。
事件代理
把子节点的监听函数定义在父节点上,由父节点统一监听处理多个子元素的事件,叫做事件代理(delegation)。
显然:事件代理的触发只能在冒泡阶段,因为要确认父子关系,在capture phase 是不可以的。
典型的就是处理列表啦:
var ul = document.querySelector('ul');
ul.addEventListener('click', function (event) {
if (event.target.tagName.toLowerCase() === 'li') {
// some code
}
});
有时,我们也希望在某个事件发生后,不再往下传播,我们可以使用stopPropagation。但是stopPropagation不会影响click事件的客观存在,只是停止了传播,如果click事件还绑定了其他的事件监听器,则会正常执行。如果想其他也不执行,应该用stopImmediatePropagation,相当于直接取消了click事件。
// 事件传播到 p 元素后,就不再向下传播了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, true);
// 事件冒泡到 p 元素后,就不再向上冒泡了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, false);
//stopPropagation的应用。
p.addEventListener('click', function (event) {
event.stopPropagation();
console.log(1);
});
p.addEventListener('click', function(event) {
// 会触发
console.log(2);
});
//stopImmediatePropagation的应用
p.addEventListener('click', function (event) {
event.stopImmediatePropagation();
console.log(1);
});
p.addEventListener('click', function(event) {
// 不会被触发
console.log(2);
});
Event对象
事件和监听函数直接是怎么通讯的?通过Event对象,
event = new Event(type, options);
//options是给对象进行配置用的
//一般主要有bubbles,cancelable两个
//bubbles为true,就是事件冒泡(默认为false,事件默认不冒泡)。
var ev = new Event(
'look',
{
'bubbles': true,
'cancelable': false
}
);
//dispatchEvent函数可以触发这个定义好的事件look。
document.dispatchEvent(ev);
// HTML 代码为
// <div><p>Hello</p></div>
var div = document.querySelector('div');
var p = document.querySelector('p');
function callback(event) {
var tag = event.currentTarget.tagName;
console.log('Tag: ' + tag); // 没有任何输出
}
//这里定义了div的监听函数,最后的参数为false,说明在冒泡时监听,而click事件,不会冒泡。
//因此当p触发click的时候,事件向下传播,一直到叶,然后停止。不会冒泡,给到div,因此定义在div上click监听函数,不会被触发。
div.addEventListener('click', callback, false);
var click = new Event('click');
p.dispatchEvent(click);
//如果这个事件是在div元素上触发。这个事件都会触发。因为这个跟传播没有任何卵关系,点击即触发,当然,是不会送屠龙宝剑的。
div.dispatchEvent(click);
Event实例属性,bubbles,eventPhase
Event.bubbles 属性返回一个布尔值,新建event时写进去的,读一下而已
eventPhase 返回 ,0:无事发生,1:捕获阶段,2:目标节点event.target指向的节点 , 3:冒泡阶段
cancelable , cancelBubble , defaultPrevented
cancelable 默认false , 就是说一个事件发生了,Event.preventDefault()方法就不会起作用。最好用之前判断一下。
其实这种做法也相当于执行Event.stopPropagation()
function preventEvent(event) {
if (event.cancelable) {
event.preventDefault();
} else {
console.warn('This event couldn\'t be canceled.');
console.dir(event);
}
}
//defaultPrevented属性是表示该事件是否被调用过。
if (event.defaultPrevented) {
console.log('该事件已经取消了');
}
//没太想到使用场景,见到实际例子再回来看。
currentTarget , target
currentTarget 返回的是当前被触发的监听函数所绑定的节点。
target 返回的是目标节点。
比较绕,看一个例子。
// HTML代码为
// <p id="para">Hello <em>World</em></p>
function hide(e) {
console.log(this === e.currentTarget); // 总是 true
console.log(this === e.target); // 有可能不是 true
e.target.style.visibility = 'hidden';
}
para.addEventListener('click', hide, false);
//如果点的是hello,target指向的是P节点,currentTarget指向的也是P节点。
//如果点的是world,target指向的是p的子节点<em>,currentTarget指向的还是p节点。因为currentTarget是触发函数的时候才确定的,target是点击的时候确定的。
event.type
var evt = new Event('foo');
evt.type // "foo"
//click,mousemove等 都是一种事件类型,当然你可以定义任意事件类型。像上面的例子一样。
event.timeStamp
返回一个毫秒时间戳,这个时间是相对于网页加载成功开始算的。
var evt = new Event('foo');
evt.timeStamp // 3683.6999999995896
event.isTrusted
确认事件是否由用户产生为true,脚本产生为false
event.detail
只有浏览器UI事件才具有detail属性,可以返回鼠标点击几次,滚轮滚的距离等信息。
实例方法
上面介绍完属性,这里讲一下方法。
event.preventDefault()
取消浏览器对当前事件的默认行为,但不阻止事件的传播。
如果要阻止可以使用stopPropagation()
或stopImmediatePropagation()
方法。
//下面这个例子就可以保证,当键盘输入非小写字母的时候,就会输入无效。用这个特性可以限制用户一些行为。
// HTML 代码为
// <input type="text" id="my-input" />
var input = document.getElementById('my-input');
input.addEventListener('keypress', checkName, false);
function checkName(e) {
if (e.charCode < 97 || e.charCode > 122) {
e.preventDefault();
}
}
event.stopPropagation()
这个方法可以阻止事件的传播
function stopEvent(e) {
e.stopPropagation();
}
//这样做之后,el的父节点收不到click事件。
el.addEventListener('click', stopEvent, false);
event.stopImmediatePropagation()
这个方法阻止同一事件的其他监听函数被调用。
function l1(e){
e.stopImmediatePropagation();
//这里调用了这个方法后,click事件立马消失。
//如果只是禁止传播,那么绑定在这个节点上的函数还是能被调用的。但这个就直接消灭了click事件。
}
function l2(e){
console.log('hello world');
}
el.addEventListener('click', l1, false);
el.addEventListener('click', l2, false);
event.composedPath
composed组成的意思。返回一个数组,成员是事件的最底层节点和依次冒泡经过的所有上层节点
// HTML 代码如下
// <div>
// <p>Hello</p>
// </div>
var div = document.querySelector('div');
var p = document.querySelector('p');
div.addEventListener('click', function (e) {
console.log(e.composedPath());
}, false);
// [p, div, body, html, document, Window]
由这个函数,我们可以知道,当我们点击时,先往下传播,直到叶节点。例如上面隐藏的例子,点击隐藏,会把该节点下所有子节点都隐藏起来,所以应该是会先向下到底,再向上。只有这样,才能满足父子节点的信息传递,这样的"关系"才是完整的。
各种各样的事件
鼠标,键盘,进度条,表单,触摸,拖拉…用到再看看。
浏览器模型
浏览器内置了JavaScript引擎,并且提供了各种接口,让js可以控制浏览器的各种功能。网页中一旦有js脚本,浏览器加载网页时,就会去执行脚本。
代码嵌入网页
-
script元素中嵌入代码
<script> var x = 1 + 5; console.log(x); </script> //有一个type属性来指定脚本类型: text/javascript,是一个默认值,对于老式浏览器,这个值比较好 application/javascript,新浏览器建议用这个值。 <script type="application/javascript"> console.log('Hello World'); </script> //如果是type属性的值,浏览器不认识,那么就不会执行其中的代码。但是在DOM中依然会存在。 <script id="mydata" type="x-custom-data"> console.log('Hello World'); </script> document.getElementById('mydata').text // console.log('Hello World');
-
script元素加载外部脚本
<script src="/assets/application.js" integrity="sha256-TvVUHzSfftWg1rcfL6TIJ0XKEGrgLyEq6lEpcmrG9qs="> </script> //1.用来src加载之后,<script>里面的js就无效 //2.src里面的东西必须是纯的js //3.integrity(廉正,健全,诚实的意思)属性可以指定外部脚本的hash签名,用来保证脚本的一致性。如果有人修改了脚本浏览器就会拒绝加载。
-
事件属性
<button id="myBtn" onclick="console.log(this.id)">点击</button> //当事件发生时会直接调用这里的js代码
-
url协议
url支持
javascript:
协议,在url位置写入js代码,使用这个url的时候就会执行js代码。<a href="javascript:console.log('Hello')">点击</a> //如果返回字符串,browser会新建一个文档,展示这个返回的字符串,原有的文档会消失 //如果返回的不上字符串,那么网页不会跳转,只会执行js代码。可以通过+void或者在后面+void 0 避免跳转。 <a href="javascript: void new Date().toLocaleTimeString();">点击</a> <a href="javascript: new Date().toLocaleTimeString();void 0;">点击</a>
script元素
- 浏览器一边下载 HTML 网页,一边开始解析。也就是说,不等到下载完,就开始解析。
- 解析过程中,浏览器发现
<script>
元素,就暂停解析,把网页渲染的控制权转交给 JavaScript 引擎。 - 如果
<script>
元素引用了外部脚本,就下载该脚本再执行,否则就直接执行代码。 - JavaScript 引擎执行完毕,控制权交还渲染引擎,恢复往下解析 HTML 网页
注意信息:
- 如果部分脚本文件不太重要,应该放到html文件的底部。这样就算在网络不太好的时候,网页的主体部分也能被展示,因为边下载边加载。
- 放在
由于边加载边执行的机制,还会出现当你访问一个DOM元素时,它还没加载好,那么遇到这个问题,可以监听DOMContentLoaded事件,这个事件只有在DOM结构生成之后才会触发。
<head>
<script>
document.addEventListener(
'DOMContentLoaded',
function (event) {
console.log(document.body.innerHTML);
}
);
</script>
</head>
//这里提供第二个解决方案,当script标签指定的外部脚本文件下载和解析完成,会触发一个load事件,可以把所需执行的代码,放在这个事件的回调函数里面。
//其实没明白为什么可以...因为你下载的时候不是要等你下载完成吗?那下面的DOM依然没有加载。除非说html先自己去加载了,比外部的js要快。
<script src="jquery.min.js" onload="console.log(document.body.innerHTML)">
</script>
浏览器的资源下载:
如果是来自同一个域名的静态资源,浏览器最多同时下载6~20个资源,不同域名就没有这个限制。条件允许,对于大型的项目,可以把静态资源放在不同的域名之下, 可以加快下载速度。
defer属性:
加入defer之后,脚本会延迟执行,等到DOM生成之后,再执行脚本。内置的和动态生成的script不起作用。
defer加载的外部脚本不应该使用document.write方法
async属性
async异步的意思,这个属性是使用另外一个进程去下载脚本,下载时不会阻塞渲染。与defer不同,async是下载完后立刻执行,html要给async的js让路。如果同时有defer和async那么浏览器的行为由async决定。
当然async也不应该使用document.write方法,至于为什么之后再查资料。
脚本动态加载
//动态加载a,b
['a.js', 'b.js'].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
});
//有顺序的动态加载a,b。a先加载,b后加载。
['a.js', 'b.js'].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
//当然还可以指定一些回调函数。
function loadScript(src, done) {
var js = document.createElement('script');
js.src = src;
js.onload = function() {
done();
};
js.onerror = function() {
done(new Error('Failed to load script ' + src));
};
document.head.appendChild(js);
}
加载协议
//默认用http
<script src="example.js"></script>
//指定用https
<script src="https://example.js"></script>
//页面用什么,我们就用什么加载。
<script src="//example.js"></script>
渲染引擎
渲染引擎的作用是,将网页代码渲染为用户视觉可以感知的平面文档。
不同浏览器用不同的引擎:
- Firefox:Gecko 引擎
- Safari:WebKit 引擎
- Chrome:Blink 引擎
- IE: Trident 引擎
- Edge: EdgeHTML 引擎
渲染的四个阶段
- 解析代码:HTML 代码解析为 DOM,CSS 代码解析为 CSSOM(CSS Object Model)。
- 对象合成:将 DOM 和 CSSOM 合成一棵渲染树(render tree)。
- 布局:计算出渲染树的布局(layout)。
- 绘制:将渲染树绘制到屏幕。
并不是严格按照顺序执行,有时html还没下载完,浏览器就已经可以显示出内容了。
重流与重绘
渲染树转为网页布局,称为“布局流”(flow),布局显示到页面的过程,称为“绘制”paint。这两个过程都有阻塞效应,而且会耗费很多时间和计算资源。
页面生成后,脚本与样式表操作会触发重流reflow和重绘repaint。
重流一定重绘,重绘不一定重流,改变颜色之后重绘不会重流
由于这个特性,所以尽量不要动高层的DOM,而是去触动底层的DOM,这样系统开销相对小一些。
另外重绘table , flex 布局,开销都会比较大。
浏览器的一个特性是会累积DOM变动,然后一次性执行。
以下是一些优化技巧:
- 读取 DOM 或者写入 DOM,尽量写在一起,不要混杂。不要读取一个 DOM 节点,然后立刻写入,接着再读取一个 DOM 节点。
- 缓存 DOM 信息。
- 不要一项一项地改变样式,而是使用 CSS class 一次性改变样式。
- 使用
documentFragment
操作 DOM - 动画使用
absolute
定位或fixed
定位,这样可以减少对其他元素的影响。 - 只在必要时才显示隐藏元素。
- 使用
window.requestAnimationFrame()
,因为它可以把代码推迟到下一次重流时执行,而不是立即要求页面重流。 - 使用虚拟 DOM(virtual DOM)库。
// 重绘代价高
function doubleHeight(element) {
var currentHeight = element.clientHeight;
element.style.height = (currentHeight * 2) + 'px';
}
all_my_elements.forEach(doubleHeight);
// 重绘代价低
function doubleHeight(element) {
var currentHeight = element.clientHeight;
window.requestAnimationFrame(function () {
element.style.height = (currentHeight * 2) + 'px';
});
}
all_my_elements.forEach(doubleHeight);
//上面的第一段代码,每读一次 DOM,就写入新的值,会造成不停的重排和重流。第二段代码把所有的写操作,都累积在一起,从而 DOM 代码变动的代价就最小化了。
javascript引擎(虚拟机)
- Chakra (Microsoft Internet Explorer)
- Nitro/JavaScript Core (Safari)
- Carakan (Opera)
- SpiderMonkey (Firefox)
- V8 (Chrome, Chromium)
windows对象
指当前浏览器窗口,是当前页面的顶层对象,就是最高一层对象。一个变量如果未声明,那么就是顶层对象的属性
a = 1;
window.a // 1
//window.name,这个属性是定义当前浏览器的窗口的名字,在当前窗口跳转,这个对象的属性会保留下来,如果这个窗口关闭了,这个值就消失。可以认为,如果你打开了很多个网页窗口,就会有很多个window对象。
window.name = 'Hello World!';
console.log(window.name)
// "Hello World!"
var popup = window.open();
//open方法可以打开一个新的窗口,
if ((popup !== null) && !popup.closed) {
// 窗口仍然打开着
}
window.open().opener === window // true
//opener属性表达打开当前窗口的父窗口。
//这里说明浏览器内部的window其实也是可以互相调用,但是要定义一下,否则没有其他办法可以操作其他窗口。
//一般我们建议子窗口和父窗口不要联系比较好。我们可以把opener属性设置为null。
//<a>元素中添加属性rel="noopener"也可以防止新打开的窗口获取父窗口。
frames , length , self , window , frameElement,top , parent
//这两属性只读,其实就是window本身
window.self === window // true
window.window === window // true
//length,frames
frames === window // true
//frames就是window,如果使用frame,iframe。那么window.frames就返回一个数组,也就是说window变成了一个数组,数组中的元素就是各个frame。
//window.frameElement属性主要用于当前窗口嵌在另一个网页的情况(嵌入<object>、<iframe>或<embed>元素),返回当前窗口所在的那个元素节点。如果当前窗口是顶层窗口,或者所嵌入的那个网页不是同源的,该属性返回null。
// HTML 代码如下
// <iframe src="about.html"></iframe>
// 下面的脚本在 about.html 里面,这样就可以指回自己。否则直接用window对象可能就不知道指向哪了。
var frameEl = window.frameElement;
if (frameEl) {
frameEl.src = 'other.html';
}
//window.top属性指向最顶层窗口,主要用于在框架窗口(frame)里面获取顶层窗口。
//window.parent属性指向父窗口。如果当前窗口没有父窗口,window.parent指向自身。
if (window.parent !== window.top) {
// 表明当前窗口嵌入不止一层
}
devicePixelRatio
css像素与物理像素的比率
位置大小属性
screenX,screenY , innerHeight , innerWidth , outerHeight, outerWidth , scrollX,scrollY ,pageXOffset , pageYOffset ,isSecureContext
一些方法
alert,prompt,confirm,open ,close ,stop , moveTo , moveBy, resizeTo , resizeBy , scrollTo , scroll , scrollBy , print , focus , blur , getSelection , getComputedStyle ,matchMedia , requestAnimationFrame , cancelAnimationFrame , requestIdleCallback ,
一些事件
load( 发生在文档在浏览器窗口加载完毕时 ),onload属性可以指定这个事件的回调函数。
error( 发生在脚本发生错误时 ) , onerror…
onafterprint , onbeforeprint , onbeforeunload , onhashchange , onlanguagechange , onmessage , onmessageerror , onoffline , ononline , onpagehide , onpageshow , onpopstate , onstorage , onunhandledrejection , onunload .
多窗口操作
iframe元素使得网页可以互相嵌套使用,会形成多窗口,如果子窗口又嵌入其他网页就会形成多级窗口。
top , 顶层窗口
parent , 父窗口
self , 自身
与这些变量对应,浏览器还提供一些特殊窗口名,供window.open()方法,,标签,等引用。
_top
:顶层窗口_parent
:父窗口_blank
:新窗口
//下面代码就表示在顶层窗口打开网页。
<a href="somepage.html" target="_top">Link</a>
iframe 嵌入窗口的获取办法
var frame = document.getElementById('theFrame');
var frameWindow = frame.contentWindow;
//上面代码中,frame.contentWindow可以拿到子窗口的window对象。然后,在满足同源限制的情况下,可以读取子窗口内部的属性。
// 获取子窗口的标题
frameWindow.document.title
//也可以通过 var frameDocument = frame.contentDocument;
//直接获取document对象。
//<iframe>元素遵守同源政策,只有当父窗口与子窗口在同一个域时,两者之间才可以用脚本通信,否则只有使用window.postMessage方法。
navigator对象的属性
这个对象其实是去识别浏览器到底运行在哪个设备,哪个引擎,有哪些插件…等。
userAgent , plugins , platform , onLine , language , languages , geolocation , cookieEnable , javaEnable() , sendBeacon() ,beacon 烽火
screen对象
属性:height , width , availHeight , availWidth , pixelDepth , colorDepth , orientation ,
cookie
cookie是服务器保持在浏览器的一小段文本信息,每一段cookie大小一般不能超过4KB , 每次浏览器发出请求都会自动附带上这段信息。
cookie主要用来分辨请求是否来自同一个浏览器,以及用来保存一些状态信息,常用场合如下
- 对话(session)管理:保存登录,购物车等需要记录的信息。
- 个性化:保存用户偏好,例如网页字体,背景颜色…
- 追踪:记录和分析用户行为
有些开发者使用 Cookie 作为客户端储存。这样做虽然可行,但是并不推荐,因为 Cookie 的设计目标并不是这个,它的容量很小(4KB),缺乏数据操作接口,而且会影响性能。客户端储存应该使用 Web storage API 和 IndexedDB。
Cookie 包含以下几方面的信息。
- Cookie 的名字
- Cookie 的值(真正的数据写在这里面)
- 到期时间
- 所属域名(默认是当前域名)
- 生效的路径(默认是当前网址)
cookie对同一域名的根路径和所有子路径都有效!
cookie由http生成,也由它使用
如果希望浏览器保存cookie,需要在头信息中放置Set-Cookie字段
Set-Cookie:foo=bar
//可以生成多个cookie
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
[page content]
//可以添加cookie的属性。
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<non-zero-digit>
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
Set-Cookie: <cookie-name>=<cookie-value>; Secure
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly
//Set-cookie字段里面,可以同时包括多个属性,没有次序的要求
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly
//例如:
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
//如果服务器想改变一个早先设置的 Cookie,必须同时满足四个条件:Cookie 的key、domain、path和secure都匹配。举例来说,如果原始的 Cookie 是用如下的Set-Cookie设置的。
Set-Cookie: key1=value1; domain=example.com; path=/blog
//改变上面这个 Cookie 的值,就必须使用同样的Set-Cookie。
Set-Cookie: key1=value2; domain=example.com; path=/blog
//只要有一个属性不同,就会生成一个全新的 Cookie,而不是替换掉原来那个 Cookie。
//上面的命令设置了一个全新的同名Cookie,但是属性不同,下次访问网站/blog时,会同时发两个cookie,匹配越精确的,越排在前面
/blog要先发, /后发
http请求:cookie的发送
浏览器发送http请求时,每个请求都会带上相应的cookie,就是把之前服务器发给浏览器的那段信息发会给服务器,这是要使用HTTP头信息的Cookie字段
Cookie: foo=bar
//上面代码会发送名为foo的cookie,值为bar,可以包含多个cookie
Cookie: name=value; name2=value2; name3=value3
//例如:
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
//cookie的属性没发?,谁设置的没说?上面的那四个值没有说。
Expries , Max-Age
Max-Age 比 Expries优先 , 不设置这个属性,或者设为null,cookie只在当前session有效,关闭浏览器窗口就会被删除。
Domain , Path
domain , path 两个属性是用来保证同一域名,同一路径下的网页可以用同样的Cookie
Secure , HttpOnly
secure → https , HttpOnly → javascript (设置了,脚本就拿不到了)
document.cookie
document.cookie // "foo=bar;baz=bar"
//读cookie
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
console.log(cookies[i]);
}
// foo=bar
// baz=bar
//cookie可写
document.cookie = 'fontSize=14';
//写入的时候,Cookie 的值必须写成key=value的形式。注意,等号两边不能有空格。另外,写入 Cookie 的时候,必须对分号、逗号和空格进行转义(它们都不允许作为 Cookie 的值),这可以用encodeURIComponent方法达到。
//但是,document.cookie一次只能写入一个 Cookie,而且写入并不是覆盖,而是添加。
document.cookie = 'test1=hello';
document.cookie = 'test2=world';
document.cookie
// test1=hello;test2=world
//document.cookie读写行为的差异(一次可以读出全部 Cookie,但是只能写入一个 Cookie),与 HTTP 协议的 Cookie 通信格式有关。浏览器向服务器发送 Cookie 的时候,Cookie字段是使用一行将所有 Cookie 全部发送;服务器向浏览器设置 Cookie 的时候,Set-Cookie字段是一行设置一个 Cookie。
//写入 Cookie 的时候,可以一起写入 Cookie 的属性。
document.cookie = "foo=bar; expires=Fri, 31 Dec 2020 23:59:59 GMT";
各个属性的写入注意点如下。
path
属性必须为绝对路径,默认为当前路径。domain
属性值必须是当前发送 Cookie 的域名的一部分。比如,当前域名是example.com
,就不能将其设为foo.com
。该属性默认为当前的一级域名(不含二级域名)。max-age
属性的值为秒数。expires
属性的值为 UTC 格式,可以使用Date.prototype.toUTCString()
进行日期格式转换。
document.cookie = 'fontSize=14; '
+ 'expires=' + someDate.toGMTString() + '; '
+ 'path=/subdirectory; '
+ 'domain=*.example.com';
//Cookie 的属性一旦设置完成,就没有办法读取这些属性的值。
//删除一个现存 Cookie 的唯一方法,是设置它的expires属性为一个过去的日期。
document.cookie = 'fontSize=;expires=Thu, 01-Jan-1970 00:00:01 GMT';
XMLHttpRequest
简介
Asynchronous JavaScript and XML ,
ajax == JavaScript 脚本发起 HTTP 通信的代名词
具体来说,AJAX 包括以下几个步骤。
- 创建 XMLHttpRequest 实例
- 发出 HTTP 请求
- 接收服务器传回的数据
- 更新网页数据
如今已经不用xml了,改用json。
这个对象可以发送任何格式的数据,
XMLHttpRequest 是一个构造函数,使用new生成实例
var xhr = new XMLHttpRequest();
//建立好后,即可用来发送请求。
xhr.open('GET', 'http://www.example.com/page.php', true);
//指定回调函数。
xhr.onreadystatechange = handleStateChange;
function handleStateChange() {
// ...
}
//AJAX 只能向同源网址(协议、域名、端口都相同)发出 HTTP 请求
//完整实例;
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
// 通信成功时,状态值为4
if (xhr.readyState === 4){
if (xhr.status === 200){
console.log(xhr.responseText);
} else {
console.error(xhr.statusText);
}
}
};
xhr.onerror = function (e) {
console.error(xhr.statusText);
};
xhr.open('GET', '/endpoint', true);
xhr.send(null);
XMLHttpRequest的实例属性
readyState
XMLHttpRequest.readyState
返回一个整数,表示实例对象的当前状态。该属性只读。它可能返回以下值。
- 0,表示 XMLHttpRequest 实例已经生成,但是实例的
open()
方法还没有被调用。 - 1,表示
open()
方法已经调用,但是实例的send()
方法还没有调用,仍然可以使用实例的setRequestHeader()
方法,设定 HTTP 请求的头信息。 - 2,表示实例的
send()
方法已经调用,并且服务器返回的头信息和状态码已经收到。 - 3,表示正在接收服务器传来的数据体(body 部分)。这时,如果实例的
responseType
属性等于text
或者空字符串,responseText
属性就会包含已经收到的部分信息。 - 4,表示服务器返回的数据已经完全接收,或者本次接收已经失败。
XMLHttpRequest.onreadystatechange
属性指向一个监听函数。readystatechange
事件发生时(实例的readyState
属性变化),就会执行这个属性。
abort()终止请求时,也会造成readyState属性变化。
response
XMLHttpRequest.response
属性表示服务器返回的数据体(即 HTTP 回应的 body 部分)。它可能是任何数据类型,比如字符串、对象、二进制对象等等,具体的类型由XMLHttpRequest.responseType
属性决定。该属性只读。
如果本次请求没有成功或者数据不完整,该属性等于null
。但是,如果responseType
属性等于text
或空字符串,在请求没有结束之前(readyState
等于3的阶段),response
属性包含服务器已经返回的部分数据。
responseType
XMLHttpRequest.responseType
属性是一个字符串,表示服务器返回数据的类型。这个属性是可写的,可以在调用open()
方法之后、调用send()
方法之前,设置这个属性的值,告诉服务器返回指定类型的数据。如果responseType
设为空字符串,就等同于默认值text
。
XMLHttpRequest.responseType
属性可以等于以下值。
- “”(空字符串):等同于
text
,表示服务器返回文本数据。 - “arraybuffer”:ArrayBuffer 对象,表示服务器返回二进制数组。
- “blob”:Blob 对象,表示服务器返回二进制对象。
- “document”:Document 对象,表示服务器返回一个文档对象。
- “json”:JSON 对象。
- “text”:字符串。
XMLHttpRequest.responseText
属性返回从服务器接收到的字符串,该属性为只读。只有 HTTP 请求完成接收以后,该属性才会包含完整的数据。
XMLHttpRequest.responseXML
属性返回从服务器接收到的 HTML 或 XML 文档对象,该属性为只读。如果本次请求没有成功,或者收到的数据不能被解析为 XML 或 HTML,该属性等于null
。
该属性得到的数据,是直接解析后的文档 DOM 树。
status
XMLHttpRequest.status
属性返回一个整数,表示服务器回应的 HTTP 状态码。一般来说,如果通信成功的话,这个状态码是200;如果服务器没有返回状态码,那么这个属性默认是200。请求发出之前,该属性为0
。该属性只读。
- 200, OK,访问正常
- 301, Moved Permanently,永久移动
- 302, Moved temporarily,暂时移动
- 304, Not Modified,未修改
- 307, Temporary Redirect,暂时重定向
- 401, Unauthorized,未授权
- 403, Forbidden,禁止访问
- 404, Not Found,未发现指定网址
- 500, Internal Server Error,服务器发生错误
XMLHttpRequest.timeout
属性返回一个整数,表示多少毫秒后,如果请求仍然没有得到结果,就会自动终止。如果该属性等于0,就表示没有时间限制。
XMLHttpRequestEventTarget.ontimeout
属性用于设置一个监听函数,如果发生 timeout 事件,就会执行这个监听函数。
var xhr = new XMLHttpRequest();
var url = '/server';
xhr.ontimeout = function () {
console.error('The request for ' + url + ' timed out.');
};
xhr.onload = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// 处理服务器返回的数据
} else {
console.error(xhr.statusText);
}
}
};
xhr.open('GET', url, true);
// 指定 10 秒钟超时
xhr.timeout = 10 * 1000;
xhr.send(null);
监听属性
XMLHttpRequest 对象可以对以下事件指定监听函数。
- XMLHttpRequest.onloadstart:loadstart 事件(HTTP 请求发出)的监听函数
- XMLHttpRequest.onprogress:progress事件(正在发送和加载数据)的监听函数
- XMLHttpRequest.onabort:abort 事件(请求中止,比如用户调用了
abort()
方法)的监听函数 - XMLHttpRequest.onerror:error 事件(请求失败)的监听函数
- XMLHttpRequest.onload:load 事件(请求成功完成)的监听函数
- XMLHttpRequest.ontimeout:timeout 事件(用户指定的时限超过了,请求还未完成)的监听函数
- XMLHttpRequest.onloadend:loadend 事件(请求完成,不管成功或失败)的监听函数
xhr.onload = function() {
var responseText = xhr.responseText;
console.log(responseText);
// process the response.
};
xhr.onabort = function () {
console.log('The request was aborted');
};
xhr.onprogress = function (event) {
console.log(event.loaded);
console.log(event.total);
};
xhr.onerror = function() {
console.log('There was an error!');
};
XMLHttpRequest.withCredentials
属性是一个布尔值,表示跨域请求时,用户信息(比如 Cookie 和认证的 HTTP 头信息)是否会包含在请求之中,默认为false
,即向example.com
发出跨域请求时,不会发送example.com
设置在本机上的 Cookie(如果有的话)。
如果需要跨域 AJAX 请求发送 Cookie,需要withCredentials
属性设为true
。注意,同源的请求不需要设置这个属性。
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true);
xhr.withCredentials = true;
xhr.send(null);
为了让这个属性生效,服务器必须显式返回Access-Control-Allow-Credentials
这个头信息。
Access-Control-Allow-Credentials: true
XMLHttpRequest 不仅可以发送请求,还可以发送文件,这就是 AJAX 文件上传。发送文件以后,通过XMLHttpRequest.upload
属性可以得到一个对象,通过观察这个对象,可以得知上传的进展。主要方法就是监听这个对象的各种事件:loadstart、loadend、load、abort、error、progress、timeout。
XMLHttpRequest.open()
方法用于指定 HTTP 请求的参数,或者说初始化 XMLHttpRequest 实例对象。它一共可以接受五个参数。
void open(
string method,
string url,
optional boolean async,
optional string user,
optional string password
);
method
:表示 HTTP 动词方法,比如GET
、POST
、PUT
、DELETE
、HEAD
等。url
: 表示请求发送目标 URL。async
: 布尔值,表示请求是否为异步,默认为true
。如果设为false
,则send()
方法只有等到收到服务器返回了结果,才会进行下一步操作。该参数可选。由于同步 AJAX 请求会造成浏览器失去响应,许多浏览器已经禁止在主线程使用,只允许 Worker 里面使用。所以,这个参数轻易不应该设为false
。user
:表示用于认证的用户名,默认为空字符串。该参数可选。password
:表示用于认证的密码,默认为空字符串。该参数可选。
注意,如果对使用过open()
方法的 AJAX 请求,再次使用这个方法,等同于调用abort()
,即终止请求。
下面发送 POST 请求的例子。
var xhr = new XMLHttpRequest();
xhr.open('POST', encodeURI('someURL'));
XMLHttpRequest实例方法
send()
下面是发送 POST 请求的例子。
var xhr = new XMLHttpRequest();
var data = 'email='
+ encodeURIComponent(email)
+ '&password='
+ encodeURIComponent(password);
xhr.open('POST', 'http://www.example.com', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send(data);
//可以用send去人工制作提交表单,进行异步提交。
setRequestHeader()
XMLHttpRequest.setRequestHeader()
方法用于设置浏览器发送的 HTTP 请求的头信息。该方法必须在open()
之后、send()
之前调用。如果该方法多次调用,设定同一个字段,则每一次调用的值会被合并成一个单一的值发送。
overrideMimeType()
重载MIME类型,当浏览器解析不成功,拿不到数据,为了拿到数据,可以把MIME类型修改了,把原始数据拿过来。
xhr.overrideMimeType('text/plain')
一些其他方法
getResponseHeader() , getAllResponseHeaders , abort() ,
XMLHttpRequest实例事件
readyStateChange
readystate属性发生变化时,就会触发,这个经常用 ,下面是以前项目用到的一段标准用法代码。
var xmlhttp;
xmlhttp=new XMLHttpRequest();
var name=document.getElementById("name").value;
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200){
var txt = xmlhttp.responseText.toString();
var obj = JSON.parse(txt);
var show="该成员不属于网络技术组";
for (var i = 0; i < obj.contact.length; i++) {
if(obj.contact[i].name==name){
show=obj.contact[i].name+"的电话号码为:"+obj.contact[i].num;
break;
}
}
alert(show);
}
};
xmlhttp.open("GET", "http://222.111.111.223:8888/contacts.txt", true);//3000000002283171
xmlhttp.send();
progress事件
//可以查看上传的进度
var xhr = new XMLHttpRequest();
function updateProgress (oEvent) {
if (oEvent.lengthComputable) {
var percentComplete = oEvent.loaded / oEvent.total;
} else {
console.log('无法计算进展');
}
}
xhr.addEventListener('progress', updateProgress);
xhr.open();
load,error,abort,loadend
load 事件表示服务器传来的数据接收完毕,error 事件表示请求出错,abort 事件表示请求被中断(比如用户取消请求)。
上面三个事件会伴随一个loadend事件,表示请求结束,但不知道是否成功。
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', transferComplete);
xhr.addEventListener('error', transferFailed);
xhr.addEventListener('abort', transferCanceled);
xhr.addEventListener('loadend', loadEnd);
xhr.open();
function transferComplete() {
console.log('数据接收完毕');
}
function transferFailed() {
console.log('数据接收出错');
}
function transferCanceled() {
console.log('用户取消接收');
}
function loadEnd(e) {
console.log('请求结束,状态未知');
}
timeout
timeout属性一节已经讲过。
navigator.sendBeacon()
用户卸载网页时,有时需要向服务器发一些数据。由于网页关闭太快,会导致没发完,网页就关闭了。这时浏览器引入sendBeacon()
方法,这时一个浏览器任务,页面关闭后继续运行。保证网页能发出去!
window.addEventListener('unload', logData, false);
//Navigator.sendBeacon方法接受两个参数,第一个参数是目标服务器的 URL,第二个参数是所要发送的数据(可选),可以是任意类型(字符串、表单对象、二进制对象等等)。
function logData() {
navigator.sendBeacon('/log', analyticsData);
}
//该方法发送数据的 HTTP 方法是 POST,可以跨域,类似于表单提交数据。它不能指定回调函数。
同源限制
同源 = 协议,域名, 端口 都相同
目的:为了安全,防止cookie被利用。
限制范围:不同源的两个网页几乎不能进行通讯。
规避限制
cookie共享,可以通过设置domain属性使得两者同源。
iframe通讯,如果一级域名相同,可以通过domain属性解决,如果是完全不同网站拼接起来的,目前可以用两种办法解决:
- 片段识别符(fragment identifier)
- 跨文档通信API(Cross-document messaging)
fragment identifier 是指URL的#号后面的部分。
http://example.com/x.html#fragment
的#fragment
,只改变片段识别符,页面不会重新刷新
具体用法如下:(还没想明白为什么,先记住)
父窗口可以把信息,写入子窗口的片段标识符。
var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;
上面代码中,父窗口把所要传递的信息,写入 iframe 窗口的片段标识符。
子窗口通过监听hashchange
事件得到通知。
window.onhashchange = checkMessage;
function checkMessage() {
var message = window.location.hash;
// ...
}
同样的,子窗口也可以改变父窗口的片段标识符。
parent.location.href = target + '#' + hash;
window.postMessage()
HTML5为了解决这个问题,引入了一个API,cross-document messaging
// 父窗口打开一个子窗口
var popup = window.open('http://bbb.com', 'title');
// 父窗口向子窗口发消息
popup.postMessage('Hello World!', 'http://bbb.com');
// 子窗口向父窗口发消息
window.opener.postMessage('Nice to see you', 'http://aaa.com');
// 父窗口和子窗口都可以用下面的代码,
// 监听 message 消息
window.addEventListener('message', function (e) {
console.log(e.data);
},false);
message
事件的参数是事件对象event
,提供以下三个属性。
event.source
:发送消息的窗口event.origin
: 消息发向的网址event.data
: 消息内容
//下面的例子是,子窗口通过event.source属性引用父窗口,然后发送消息
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
event.source.postMessage('Nice to see you!', '*');
}
//上面做法不安全,应该采用下面做法,更安全一些。
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
if (event.origin !== 'http://aaa.com') return;
if (event.data === 'Hello World') {
event.source.postMessage('Hello', event.origin);
} else {
console.log(event.data);
}
}
LocalStorage
通过window.postMessage
,读写其他窗口的 LocalStorage 也成为了可能。
下面是一个例子,主窗口写入 iframe 子窗口的localStorage
。
window.onmessage = function(e) {
if (e.origin !== 'http://bbb.com') {
return;
}
var payload = JSON.parse(e.data);
localStorage.setItem(payload.key, JSON.stringify(payload.data));
};
上面代码中,子窗口将父窗口发来的消息,写入自己的 LocalStorage。
父窗口发送消息的代码如下。
var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = { name: 'Jack' };
win.postMessage(
JSON.stringify({key: 'storage', data: obj}),
'http://bbb.com'
);
加强版的子窗口接收消息的代码如下。
window.onmessage = function(e) {
if (e.origin !== 'http://bbb.com') return;
var payload = JSON.parse(e.data);
switch (payload.method) {
case 'set':
localStorage.setItem(payload.key, JSON.stringify(payload.data));
break;
case 'get':
var parent = window.parent;
var data = localStorage.getItem(payload.key);
parent.postMessage(data, 'http://aaa.com');
break;
case 'remove':
localStorage.removeItem(payload.key);
break;
}
};
加强版的父窗口发送消息代码如下。
var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = {
name: 'Jack' };
// 存入对象
win.postMessage(
JSON.stringify({
key: 'storage', method: 'set', data: obj}),
'http://bbb.com'
);
// 读取对象
win.postMessage(
JSON.stringify({
key: 'storage', method: "get"}),
"*"
);
window.onmessage = function(e) {
if (e.origin != 'http://aaa.com') return;
console.log(JSON.parse(e.data).name);
};
AJAX
同源政策规定,AJAX 请求只能发给同源的网址,否则就报错。
除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制。
- JSONP
- WebSocket
- CORS
JSONP
JSONP 是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务端改造非常小。
它的基本思想是,网页通过添加一个<script>
元素,向服务器请求 JSON 数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。
首先,网页动态插入<script>
元素,由它向跨源网址发出请求。
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}
window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');
}
function foo(data) {
console.log('Your public IP address is: ' + data.ip);
};
上面代码通过动态添加<script>
元素,向服务器example.com
发出请求。注意,该请求的查询字符串有一个callback
参数,用来指定回调函数的名字,这对于 JSONP 是必需的。
服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。
foo({
"ip": "8.8.8.8"
});
由于<script>
元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo
函数,该函数就会立即调用。作为参数的 JSON 数据被视为 JavaScript 对象,而不是字符串,因此避免了使用JSON.parse
的步骤。
WebSocket
WebSocket 是一种通信协议,使用ws://
(非加密)和wss://
(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。
下面是一个例子,浏览器发出的 WebSocket 请求的头信息(摘自维基百科)。
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
上面代码中,有一个字段是Origin
,表示该请求的请求源(origin),即发自哪个域名。
正是因为有了Origin
这个字段,所以 WebSocket 才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通信。如果该域名在白名单内,服务器就会做出如下回应。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
CORS
CORS 是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,属于跨源 AJAX 请求的根本解决方法。相比 JSONP 只能发GET
请求,CORS 允许任何类型的请求。
CORS通讯
CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能。
整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与普通的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感知。因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨域通信。
与JSONP的区别。
CORS 与 JSONP 的使用目的相同,但是比 JSONP 更强大。JSONP 只支持GET
请求,CORS 支持所有类型的 HTTP 请求。JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。
Storage接口
Storage 接口用于脚本在浏览器保存数据。两个对象部署了这个接口:window.sessionStorage
和window.localStorage
。
sessionStorage
保存的数据用于浏览器的一次会话(session),当会话结束(通常是窗口关闭),数据被清空;localStorage
保存的数据长期存在,下一次访问该网站的时候,网页可以直接读取以前保存的数据。
这个接口很像 Cookie 的强化版,能够使用大得多的存储空间。目前,每个域名的存储上限视浏览器而定,Chrome 是 2.5MB,Firefox 和 Opera 是 5MB,IE 是 10MB。其中,Firefox 的存储空间由一级域名决定,而其他浏览器没有这个限制。也就是说,Firefox 中,a.example.com
和b.example.com
共享 5MB 的存储空间。另外,与 Cookie 一样,它们也受同域限制。某个网页存入的数据,只有同域下的网页才能读取,如果跨域操作会报错。
storage的属性
Storage 接口只有一个属性。
Storage.length
:返回保存的数据项个数。
storage的方法
Storage.setItem()
方法用于存入数据。它接受两个参数,第一个是键名,第二个是保存的数据。如果键名已经存在,该方法会更新已有的键值。该方法没有返回值
写入不一定要用这个方法,直接赋值也是可以的。
// 下面三种写法等价
window.localStorage.foo = '123';
window.localStorage['foo'] = '123';
window.localStorage.setItem('foo', '123');
Storage.getItem()
方法用于读取数据。它只有一个参数,就是键名。如果键名不存在,该方法返回null
。
window.sessionStorage.getItem('key')
window.localStorage.getItem('key')
键名应该是一个字符串,否则会被自动转为字符串。
Storage.removeItem()
方法用于清除某个键名对应的键值。它接受键名作为参数,如果键名不存在,该方法不会做任何事情。
sessionStorage.removeItem('key');
localStorage.removeItem('key');
Storage.clear()
方法用于清除所有保存的数据。该方法的返回值是undefined
。
window.sessionStorage.clear()
window.localStorage.clear()
Storage.key()
接受一个整数作为参数(从零开始),返回该位置对应的键值。
window.sessionStorage.setItem('key', 'value');
window.sessionStorage.key(0) // "key"
结合使用Storage.length
属性和Storage.key()
方法,可以遍历所有的键。
for (var i = 0; i < window.localStorage.length; i++) {
console.log(localStorage.key(i));
}
storage事件
Storage 接口储存的数据发生变化时,会触发 storage 事件,可以指定这个事件的监听函数。
window.addEventListener('storage', onStorageChange);
监听函数接受一个event
实例对象作为参数。这个实例对象继承了 StorageEvent 接口,有几个特有的属性,都是只读属性。
StorageEvent.key
:字符串,表示发生变动的键名。如果 storage 事件是由clear()
方法引起,该属性返回null
。StorageEvent.newValue
:字符串,表示新的键值。如果 storage 事件是由clear()
方法或删除该键值对引发的,该属性返回null
。StorageEvent.oldValue
:字符串,表示旧的键值。如果该键值对是新增的,该属性返回null
。StorageEvent.storageArea
:对象,返回键值对所在的整个对象。也说是说,可以从这个属性上面拿到当前域名储存的所有键值对。StorageEvent.url
:字符串,表示原始触发 storage 事件的那个网页的网址。
下面是StorageEvent.key
属性的例子。
function onStorageChange(e) {
console.log(e.key);
}
window.addEventListener('storage', onStorageChange);
注意,该事件有一个很特别的地方,就是它不在导致数据变化的当前页面触发,而是在同一个域名的其他窗口触发。也就是说,如果浏览器只打开一个窗口,可能观察不到这个事件。比如同时打开多个窗口,当其中的一个窗口导致储存的数据发生改变时,只有在其他窗口才能观察到监听函数的执行。可以通过这种机制,实现多个窗口之间的通信。
history对象
window.history
属性指向 History 对象,它表示当前窗口的浏览历史。
History 对象保存了当前窗口访问过的所有页面网址。下面代码表示当前窗口一共访问过3个网址。
window.history.length // 3
由于安全原因,浏览器不允许脚本读取这些地址,但是允许在地址之间导航。
// 后退到前一个网址
history.back()
// 等同于
history.go(-1)
浏览器工具栏的“前进”和“后退”按钮,其实就是对 History 对象进行操作。
history的属性
History 对象主要有两个属性。
History.length
:当前窗口访问过的网址数量(包括当前网页)History.state
:History 堆栈最上层的状态值(详见下文)
// 当前窗口访问过多少个网页
window.history.length // 1
// History 对象的当前状态
// 通常是 undefined,即未设置
window.history.state // undefined
history的方法
History.back(),History.forward(),History.go()
注意,移动到以前访问过的页面时,页面通常是从浏览器缓存之中加载,而不是重新要求服务器发送新的网页。
pushState()
作用是添加一条历史记录,相当于在history这个数组中添加一个元素,但不能实现跨域。
replaceState()
History.replaceState()
方法用来修改 History 对象的当前记录,其他都与pushState()
方法一模一样。
上诉两个函数的作用都是把history中的存放数组修改,push就是字面意思,replace就是改 arr[0] 的值
history事件
popstate事件,当前窗口页面变化时会触发(包括刷新),另外,该事件只针对同一文档,如果加载不同的文档,事件也不会触发。
window.onpopstate = function (event) {
console.log('location: ' + document.location);
console.log('state: ' + JSON.stringify(event.state));
};
// 或者
window.addEventListener('popstate', function(event) {
console.log('location: ' + document.location);
console.log('state: ' + JSON.stringify(event.state));
});
回调函数的参数是一个event
事件对象,它的state
属性指向pushState
和replaceState
方法为当前 URL 所提供的状态对象(即这两个方法的第一个参数)。上面代码中的event.state
,就是通过pushState
和replaceState
方法,为当前 URL 绑定的state
对象。
这个state
对象也可以直接通过history
对象读取。
var currentState = history.state;
注意,页面第一次加载的时候,浏览器不会触发popstate
事件
location对象
Location
对象是浏览器提供的原生对象,提供 URL 相关的信息和操作方法。通过window.location
和document.location
属性,可以拿到这个对象。
location属性
Location
对象提供以下属性。
Location.href
:整个 URL。Location.protocol
:当前 URL 的协议,包括冒号(:
)。Location.host
:主机,包括冒号(:
)和端口(默认的80端口和443端口会省略)。Location.hostname
:主机名,不包括端口。Location.port
:端口号。Location.pathname
:URL 的路径部分,从根路径/
开始。Location.search
:查询字符串部分,从问号?
开始。Location.hash
:片段字符串部分,从#
开始。Location.username
:域名前面的用户名。Location.password
:域名前面的密码。Location.origin
:URL 的协议、主机名和端口。
// 当前网址为
// http://user:[email protected]:4097/path/a.html?x=111#part1
document.location.href
// "http://user:[email protected]:4097/path/a.html?x=111#part1"
document.location.protocol
// "http:"
document.location.host
// "www.example.com:4097"
document.location.hostname
// "www.example.com"
document.location.port
// "4097"
document.location.pathname
// "/path/a.html"
document.location.search
// "?x=111"
document.location.hash
// "#part1"
document.location.username
// "user"
document.location.password
// "passwd"
document.location.origin
// "http://user:[email protected]:4097"
这些属性里面,只有origin
属性是只读的,其他属性都可写。
注意,如果对Location.href
写入新的 URL 地址,浏览器会立刻跳转到这个新地址。
// 跳转到新网址
document.location.href = 'http://www.example.com';
这个特性常常用于让网页自动滚动到新的锚点。
document.location.href = '#top';
// 等同于
document.location.hash = '#top';
直接改写location
,相当于写入href
属性。
document.location = 'http://www.example.com';
// 等同于
document.location.href = 'http://www.example.com';
另外,Location.href
属性是浏览器唯一允许跨域写入的属性,即非同源的窗口可以改写另一个窗口(比如子窗口与父窗口)的Location.href
属性,导致后者的网址跳转。Location
的其他属性都不允许跨域写入。
location方法
(1)Location.assign()
assign
方法接受一个 URL 字符串作为参数,使得浏览器立刻跳转到新的 URL。如果参数不是有效的 URL 字符串,则会报错。
// 跳转到新的网址
document.location.assign('http://www.example.com')
(2)Location.replace()
replace
方法接受一个 URL 字符串作为参数,使得浏览器立刻跳转到新的 URL。如果参数不是有效的 URL 字符串,则会报错。
它与assign
方法的差异在于,replace
会在浏览器的浏览历史History
里面删除当前网址,也就是说,一旦使用了该方法,后退按钮就无法回到当前网页了,相当于在浏览历史里面,使用新的 URL 替换了老的 URL。它的一个应用是,当脚本发现当前是移动设备时,就立刻跳转到移动版网页。
// 跳转到新的网址
document.location.replace('http://www.example.com')
(3)Location.reload()
reload
方法使得浏览器重新加载当前网址,相当于按下浏览器的刷新按钮。
它接受一个布尔值作为参数。如果参数为true
,浏览器将向服务器重新请求这个网页,并且重新加载后,网页将滚动到头部(即scrollTop === 0
)。如果参数是false
或为空,浏览器将从本地缓存重新加载该网页,并且重新加载后,网页的视口位置是重新加载前的位置。
// 向服务器重新请求当前网址
window.location.reload(true);
(4)Location.toString()
toString
方法返回整个 URL 字符串,相当于读取Location.href
属性。
URL编码与解码
网页的 URL 只能包含合法的字符。合法字符分成两类。
- URL 元字符:分号(
;
),逗号(,
),斜杠(/
),问号(?
),冒号(:
),at(@
),&
,等号(=
),加号(+
),美元符号($
),井号(#
) - 语义字符:
a-z
,A-Z
,0-9
,连词号(-
),下划线(_
),点(.
),感叹号(!
),波浪线(~
),星号(*
),单引号('
),圆括号(()
)
除了以上字符,其他字符出现在 URL 之中都必须转义,规则是根据操作系统的默认编码,将每个字节转为百分号(%
)加上两个大写的十六进制字母。
JavaScript 提供四个 URL 的编码/解码方法。
encodeURI()
encodeURIComponent()
decodeURI()
decodeURIComponent()
encodeURI()
方法用于转码整个 URL。它的参数是一个字符串,代表整个 URL。它会将元字符和语义字符之外的字符,都进行转义。
encodeURI('http://www.example.com/q=春节')
// "http://www.example.com/q=%E6%98%A5%E8%8A%82"
encodeURIComponent()
方法用于转码 URL 的组成部分,会转码除了语义字符之外的所有字符,即元字符也会被转码。所以,它不能用于转码整个 URL。它接受一个参数,就是 URL 的片段。
encodeURIComponent('春节')
// "%E6%98%A5%E8%8A%82"
encodeURIComponent('http://www.example.com/q=春节')
// "http%3A%2F%2Fwww.example.com%2Fq%3D%E6%98%A5%E8%8A%82"
decodeURI()
方法用于整个 URL 的解码。它是encodeURI()
方法的逆运算。它接受一个参数,就是转码后的 URL。
decodeURI('http://www.example.com/q=%E6%98%A5%E8%8A%82')
// "http://www.example.com/q=春节"
decodeURIComponent()
用于URL 片段的解码。它是encodeURIComponent()
方法的逆运算。它接受一个参数,就是转码后的 URL 片段。
decodeURIComponent('%E6%98%A5%E8%8A%82')
// "春节"
url对象
URL
对象是浏览器的原生对象,可以用来构造、解析和编码 URL。一般情况下,通过window.URL
可以拿到这个对象。
元素和元素都部署了这个接口。这就是说,它们的 DOM 节点对象可以使用 URL 的实例属性和方法。
var a = document.createElement('a');
a.href = 'http://example.com/?foo=1';
a.hostname // "example.com"
a.search // "?foo=1"
上面代码中,a
是<a>
元素的 DOM 节点对象。可以在这个对象上使用 URL 的实例属性,比如hostname
和search
。
构造函数
URL
对象本身是一个构造函数,可以生成 URL 实例。
它接受一个表示 URL 的字符串作为参数。如果参数不是合法的 URL,会报错。
var url = new URL('http://www.example.com/index.html');
url.href
// "http://www.example.com/index.html"
如果参数是另一个 URL 实例,构造函数会自动读取该实例的href
属性,作为实际参数。
如果 URL 字符串是一个相对路径,那么需要表示绝对路径的第二个参数,作为计算基准。
var url1 = new URL('index.html', 'http://example.com');
url1.href
// "http://example.com/index.html"
var url2 = new URL('page2.html', 'http://example.com/page1.html');
url2.href
// "http://example.com/page2.html"
var url3 = new URL('..', 'http://example.com/a/b.html')
url3.href
// "http://example.com/"
上面代码中,返回的 URL 实例的路径都是在第二个参数的基础上,切换到第一个参数得到的。最后一个例子里面,第一个参数是..
,表示上层路径。
URL属性
URL 实例的属性与Location
对象的属性基本一致,返回当前 URL 的信息。
- URL.href:返回整个 URL
- URL.protocol:返回协议,以冒号
:
结尾 - URL.hostname:返回域名
- URL.host:返回域名与端口,包含
:
号,默认的80和443端口会省略 - URL.port:返回端口
- URL.origin:返回协议、域名和端口
- URL.pathname:返回路径,以斜杠
/
开头 - URL.search:返回查询字符串,以问号
?
开头 - URL.searchParams:返回一个
URLSearchParams
实例,该属性是Location
对象没有的 - URL.hash:返回片段识别符,以井号
#
开头 - URL.password:返回域名前面的密码
- URL.username:返回域名前面的用户名
var url = new URL('http://user:[email protected]:4097/path/a.html?x=111#part1');
url.href
// "http://user:[email protected]:4097/path/a.html?x=111#part1"
url.protocol
// "http:"
url.hostname
// "www.example.com"
url.host
// "www.example.com:4097"
url.port
// "4097"
url.origin
// "http://www.example.com:4097"
url.pathname
// "/path/a.html"
url.search
// "?x=111"
url.searchParams
// URLSearchParams {}
url.hash
// "#part1"
url.password
// "passwd"
url.username
// "user"
这些属性里面,只有origin
属性是只读的,其他属性都可写。
var url = new URL('http://example.com/index.html#part1');
url.pathname = 'index2.html';
url.href // "http://example.com/index2.html#part1"
url.hash = '#part2';
url.href // "http://example.com/index2.html#part2"
上面代码中,改变 URL 实例的pathname
属性和hash
属性,都会实时反映在 URL 实例当中。
URL方法
(1)URL.createObjectURL()
URL.createObjectURL
方法用来为上传/下载的文件、流媒体文件生成一个 URL 字符串。这个字符串代表了File
对象或Blob
对象的 URL。
// HTML 代码如下
// <div id="display"/>
// <input
// type="file"
// id="fileElem"
// multiple
// accept="image/*"
// onchange="handleFiles(this.files)"
// >
var div = document.getElementById('display');
function handleFiles(files) {
for (var i = 0; i < files.length; i++) {
var img = document.createElement('img');
img.src = window.URL.createObjectURL(files[i]);
div.appendChild(img);
}
}
上面代码中,URL.createObjectURL
方法用来为上传的文件生成一个 URL 字符串,作为<img>
元素的图片来源。
该方法生成的 URL 就像下面的样子。
blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1
注意,每次使用URL.createObjectURL
方法,都会在内存里面生成一个 URL 实例。如果不再需要该方法生成的 URL 字符串,为了节省内存,可以使用URL.revokeObjectURL()
方法释放这个实例。
(2)URL.revokeObjectURL()
URL.revokeObjectURL
方法用来释放URL.createObjectURL
方法生成的 URL 实例。它的参数就是URL.createObjectURL
方法返回的 URL 字符串。
下面为上一段的示例加上URL.revokeObjectURL()
。
var div = document.getElementById('display');
function handleFiles(files) {
for (var i = 0; i < files.length; i++) {
var img = document.createElement('img');
img.src = window.URL.createObjectURL(files[i]);
div.appendChild(img);
img.onload = function() {
window.URL.revokeObjectURL(this.src);
}
}
}
上面代码中,一旦图片加载成功以后,为本地文件生成的 URL 字符串就没用了,于是可以在img.onload
回调函数里面,通过URL.revokeObjectURL
方法卸载这个 URL 实例。
URLSearchParams对象
URLSearchParams
对象是浏览器的原生对象,用来构造、解析和处理 URL 的查询字符串(即 URL 问号后面的部分)。
它本身也是一个构造函数,可以生成实例。参数可以为查询字符串,起首的问号?
有没有都行,也可以是对应查询字符串的数组或对象。
// 方法一:传入字符串
var params = new URLSearchParams('?foo=1&bar=2');
// 等同于
var params = new URLSearchParams(document.location.search);
// 方法二:传入数组
var params = new URLSearchParams([['foo', 1], ['bar', 2]]);
// 方法三:传入对象
var params = new URLSearchParams({'foo' : 1 , 'bar' : 2});
URLSearchParams
会对查询字符串自动编码。
var params = new URLSearchParams({'foo': '你好'});
params.toString() // "foo=%E4%BD%A0%E5%A5%BD"
上面代码中,foo
的值是汉字,URLSearchParams
对其自动进行 URL 编码。
浏览器向服务器发送表单数据时,可以直接使用URLSearchParams
实例作为表单数据。
const params = new URLSearchParams({foo: 1, bar: 2});
fetch('https://example.com/api', {
method: 'POST',
body: params
}).then(...)
上面代码中,fetch
命令向服务器发送命令时,可以直接使用URLSearchParams
实例。
URLSearchParams
可以与URL
接口结合使用。
var url = new URL(window.location);
var foo = url.searchParams.get('foo') || 'somedefault';
上面代码中,URL 实例的searchParams
属性就是一个URLSearchParams
实例,所以可以使用URLSearchParams
接口的get
方法。
DOM 的a
元素节点的searchParams
属性,就是一个URLSearchParams
实例。
var a = document.createElement('a');
a.href = 'https://example.com?filter=api';
a.searchParams.get('filter') // "api"
URLSearchParams
实例有遍历器接口,可以用for...of
循环遍历(详见《ES6 标准入门》的《Iterator》一章)。
var params = new URLSearchParams({'foo': 1 , 'bar': 2});
for (var p of params) {
console.log(p[0] + ': ' + p[1]);
}
// foo: 1
// bar: 2
URLSearchParams
没有实例属性,只有实例方法。
toString()
toString
方法返回实例的字符串形式。
var url = new URL('https://example.com?foo=1&bar=2');
var params = new URLSearchParams(url.search);
params.toString() // "foo=1&bar=2'
那么需要字符串的场合,会自动调用toString
方法。
var params = new URLSearchParams({version: 2.0});
window.location.href = location.pathname + '?' + params;
上面代码中,location.href
赋值时,可以直接使用params
对象。这时就会自动调用toString
方法。
append()
append
方法用来追加一个查询参数。它接受两个参数,第一个为键名,第二个为键值,没有返回值。
var params = new URLSearchParams({'foo': 1 , 'bar': 2});
params.append('baz', 3);
params.toString() // "foo=1&bar=2&baz=3"
append
方法不会识别是否键名已经存在。
var params = new URLSearchParams({'foo': 1 , 'bar': 2});
params.append('foo', 3);
params.toString() // "foo=1&bar=2&foo=3"
上面代码中,查询字符串里面foo
已经存在了,但是append
依然会追加一个同名键。
delete()
delete
方法用来删除指定的查询参数。它接受键名作为参数。
var params = new URLSearchParams({
'foo': 1 , 'bar': 2});
params.delete('bar');
params.toString() // "foo=1"
has()
has
方法返回一个布尔值,表示查询字符串是否包含指定的键名。
var params = new URLSearchParams({
'foo': 1 , 'bar': 2});
params.has('bar') // true
params.has('baz') // false
set()
set
方法用来设置查询字符串的键值。
它接受两个参数,第一个是键名,第二个是键值。如果是已经存在的键,键值会被改写,否则会被追加。
var params = new URLSearchParams('?foo=1');
params.set('foo', 2);
params.toString() // "foo=2"
params.set('bar', 3);
params.toString() // "foo=2&bar=3"
上面代码中,foo
是已经存在的键,bar
是还不存在的键。
如果有多个的同名键,set
会移除现存所有的键。
var params = new URLSearchParams('?foo=1&foo=2');
params.set('foo', 3);
params.toString() // "foo=3"
下面是一个替换当前 URL 的例子。
// URL: https://example.com?version=1.0
var params = new URLSearchParams(location.search.slice(1));
params.set('version', 2.0);
window.history.replaceState({
}, '', location.pathname + `?` + params);
// URL: https://example.com?version=2.0
get(),getAll()
get
方法用来读取查询字符串里面的指定键。它接受键名作为参数。
var params = new URLSearchParams('?foo=1');
params.get('foo') // "1"
params.get('bar') // null
两个地方需要注意。第一,它返回的是字符串,如果原始值是数值,需要转一下类型;第二,如果指定的键名不存在,返回值是null
。
如果有多个的同名键,get
返回位置最前面的那个键值。
var params = new URLSearchParams('?foo=3&foo=2&foo=1');
params.get('foo') // "3"
上面代码中,查询字符串有三个foo
键,get
方法返回最前面的键值3
。
getAll
方法返回一个数组,成员是指定键的所有键值。它接受键名作为参数。
var params = new URLSearchParams('?foo=1&foo=2');
params.getAll('foo') // ["1", "2"]
上面代码中,查询字符串有两个foo
键,getAll
返回的数组就有两个成员。
sort()
sort
方法对查询字符串里面的键进行排序,规则是按照 Unicode 码点从小到大排列。
该方法没有返回值,或者说返回值是undefined
。
var params = new URLSearchParams('c=4&a=2&b=3&a=1');
params.sort();
params.toString() // "a=2&a=1&b=3&c=4"
上面代码中,如果有两个同名的键a
,它们之间不会排序,而是保留原始的顺序。
keys(),values(),entries()
这三个方法都返回一个遍历器对象,供for...of
循环消费。它们的区别在于,keys
方法返回的是键名的遍历器,values
方法返回的是键值的遍历器,entries
返回的是键值对的遍历器。
var params = new URLSearchParams('a=1&b=2');
for(var p of params.keys()) {
console.log(p);
}
// a
// b
for(var p of params.values()) {
console.log(p);
}
// 1
// 2
for(var p of params.entries()) {
console.log(p);
}
// ["a", "1"]
// ["b", "2"]
如果直接对URLSearchParams
进行遍历,其实内部调用的就是entries
接口。
for (var p of params) {
}
// 等同于
for (var p of params.entries()) {
}
ArrayBuffer对象,Blob对象
Blob:binary large object
ArrayBuffer对象
ArrayBuffer 对象表示一段二进制数据,用来模拟内存里面的数据。通过这个对象,JavaScript 可以读写二进制数据。这个对象可以看作内存数据的表达。
这个对象是 ES6 才写入标准的,普通的网页编程用不到它,为了教程体系的完整,下面只提供一个简略的介绍,详细介绍请看《ES6 标准入门》里面的章节。
浏览器原生提供ArrayBuffer()
构造函数,用来生成实例。它接受一个整数作为参数,表示这段二进制数据占用多少个字节。
var buffer = new ArrayBuffer(8);
上面代码中,实例对象buffer
占用8个字节。
ArrayBuffer 对象有实例属性byteLength
,表示当前实例占用的内存长度(单位字节)。
var buffer = new ArrayBuffer(8);
buffer.byteLength // 8
ArrayBuffer 对象有实例方法slice()
,用来复制一部分内存。它接受两个整数参数,分别表示复制的开始位置(从0开始)和结束位置(复制时不包括结束位置),如果省略第二个参数,则表示一直复制到结束。
var buf1 = new ArrayBuffer(8);
var buf2 = buf1.slice(0);
上面代码表示复制原来的实例
Blob对象
Blob 对象表示一个二进制文件的数据内容,比如一个图片文件的内容就可以通过 Blob 对象读写。它通常用来读写文件,它的名字是 Binary Large Object (二进制大型对象)的缩写。它与 ArrayBuffer 的区别在于,它用于操作二进制文件,而 ArrayBuffer 用于操作内存。
浏览器原生提供Blob()
构造函数,用来生成实例对象。
new Blob(array [, options])
Blob
构造函数接受两个参数。第一个参数是数组,成员是字符串或二进制对象,表示新生成的Blob
实例对象的内容;第二个参数是可选的,是一个配置对象,目前只有一个属性type
,它的值是一个字符串,表示数据的 MIME 类型,默认是空字符串。
var htmlFragment = ['<a id="a"><b id="b">hey!</b></a>'];
var myBlob = new Blob(htmlFragment, {type : 'text/html'});
上面代码中,实例对象myBlob
包含的是字符串。生成实例的时候,数据类型指定为text/html
。
下面是另一个例子,Blob 保存 JSON 数据。
var obj = { hello: 'world' };
var blob = new Blob([ JSON.stringify(obj) ], {type : 'application/json'});
属性与方法
Blob
具有两个实例属性size
和type
,分别返回数据的大小和类型。
var htmlFragment = ['<a id="a"><b id="b">hey!</b></a>'];
var myBlob = new Blob(htmlFragment, {type : 'text/html'});
myBlob.size // 32
myBlob.type // "text/html"
Blob
具有一个实例方法slice
,用来拷贝原来的数据,返回的也是一个Blob
实例。
myBlob.slice(start,end, contentType)
slice
方法有三个参数,都是可选的。它们依次是起始的字节位置(默认为0)、结束的字节位置(默认为size
属性的值,该位置本身将不包含在拷贝的数据之中)、新实例的数据类型(默认为空字符串)。
获取文件信息
文件选择器<input type="file">
用来让用户选取文件。出于安全考虑,浏览器不允许脚本自行设置这个控件的value
属性,即文件必须是用户手动选取的,不能是脚本指定的。一旦用户选好了文件,脚本就可以读取这个文件。
文件选择器返回一个 FileList 对象,该对象是一个类似数组的成员,每个成员都是一个 File 实例对象。File 实例对象是一个特殊的 Blob 实例,增加了name
和lastModifiedDate
属性。
// HTML 代码如下
// <input type="file" accept="image/*" multiple οnchange="fileinfo(this.files)"/>
function fileinfo(files) {
for (var i = 0; i < files.length; i++) {
var f = files[i];
console.log(
f.name, // 文件名,不含路径
f.size, // 文件大小,Blob 实例属性
f.type, // 文件类型,Blob 实例属性
f.lastModifiedDate // 文件的最后修改时间
);
}
}
除了文件选择器,拖放 API 的dataTransfer.files
返回的也是一个FileList 对象,它的成员因此也是 File 实例对象。
下载文件
AJAX 请求时,如果指定responseType
属性为blob
,下载下来的就是一个 Blob 对象。
function getBlob(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'blob';
xhr.onload = function () {
callback(xhr.response);
}
xhr.send(null);
}
上面代码中,xhr.response
拿到的就是一个 Blob 对象。
生成URL
浏览器允许使用URL.createObjectURL()
方法,针对 Blob 对象生成一个临时 URL,以便于某些 API 使用。这个 URL 以blob://
开头,表明对应一个 Blob 对象,协议头后面是一个识别符,用来唯一对应内存里面的 Blob 对象。这一点与data://URL
(URL 包含实际数据)和file://URL
(本地文件系统里面的文件)都不一样。
var droptarget = document.getElementById('droptarget');
droptarget.ondrop = function (e) {
var files = e.dataTransfer.files;
for (var i = 0; i < files.length; i++) {
var type = files[i].type;
if (type.substring(0,6) !== 'image/')
continue;
var img = document.createElement('img');
img.src = URL.createObjectURL(files[i]);
img.onload = function () {
this.width = 100;
document.body.appendChild(this);
URL.revokeObjectURL(this.src);
}
}
}
上面代码通过为拖放的图片文件生成一个 URL,产生它们的缩略图,从而使得用户可以预览选择的文件。
浏览器处理 Blob URL 就跟普通的 URL 一样,如果 Blob 对象不存在,返回404状态码;如果跨域请求,返回403状态码。Blob URL 只对 GET 请求有效,如果请求成功,返回200状态码。由于 Blob URL 就是普通 URL,因此可以下载。
读取文件
取得 Blob 对象以后,可以通过FileReader
对象,读取 Blob 对象的内容,即文件内容。
FileReader 对象提供四个方法,处理 Blob 对象。Blob 对象作为参数传入这些方法,然后以指定的格式返回。
FileReader.readAsText()
:返回文本,需要指定文本编码,默认为 UTF-8。FileReader.readAsArrayBuffer()
:返回 ArrayBuffer 对象。FileReader.readAsDataURL()
:返回 Data URL。FileReader.readAsBinaryString()
:返回原始的二进制字符串。
下面是FileReader.readAsText()
方法的例子,用来读取文本文件。
// HTML 代码如下
// <input type=’file' onchange='readfile(this.files[0])'></input>
// <pre id='output'></pre>
function readfile(f) {
var reader = new FileReader();
reader.readAsText(f);
reader.onload = function () {
var text = reader.result;
var out = document.getElementById('output');
out.innerHTML = '';
out.appendChild(document.createTextNode(text));
}
reader.onerror = function(e) {
console.log('Error', e);
};
}
上面代码中,通过指定 FileReader 实例对象的onload
监听函数,在实例的result
属性上拿到文件内容。
下面是FileReader.readAsArrayBuffer()
方法的例子,用于读取二进制文件。
// HTML 代码如下
// <input type="file" οnchange="typefile(this.files[0])"></input>
function typefile(file) {
// 文件开头的四个字节,生成一个 Blob 对象
var slice = file.slice(0, 4);
var reader = new FileReader();
// 读取这四个字节
reader.readAsArrayBuffer(slice);
reader.onload = function (e) {
var buffer = reader.result;
// 将这四个字节的内容,视作一个32位整数
var view = new DataView(buffer);
var magic = view.getUint32(0, false);
// 根据文件的前四个字节,判断它的类型
switch(magic) {
case 0x89504E47: file.verified_type = 'image/png'; break;
case 0x47494638: file.verified_type = 'image/gif'; break;
case 0x25504446: file.verified_type = 'application/pdf'; break;
case 0x504b0304: file.verified_type = 'application/zip'; break;
}
console.log(file.name, file.verified_type);
};
}
File对象,FileList对象,FileReader对象
File对象
File 对象代表一个文件,用来读写文件信息。它继承了 Blob 对象,或者说是一种特殊的 Blob 对象,所有可以使用 Blob 对象的场合都可以使用它。
最常见的使用场合是表单的文件上传控件(<input type="file">
),用户选中文件以后,浏览器就会生成一个数组,里面是每一个用户选中的文件,它们都是 File 实例对象。
// HTML 代码如下
// <input id="fileItem" type="file">
var file = document.getElementById('fileItem').files[0];
file instanceof File // true
上面代码中,file
是用户选中的第一个文件,它是 File 的实例。
构造函数
浏览器原生提供一个File()
构造函数,用来生成 File 实例对象。
new File(array, name [, options])
File()
构造函数接受三个参数。
- array:一个数组,成员可以是二进制对象或字符串,表示文件的内容。
- name:字符串,表示文件名或文件路径。
- options:配置对象,设置实例的属性。该参数可选。
第三个参数配置对象,可以设置两个属性。
- type:字符串,表示实例对象的 MIME 类型,默认值为空字符串。
- lastModified:时间戳,表示上次修改的时间,默认为
Date.now()
。
下面是一个例子。
var file = new File(
['foo'],
'foo.txt',
{
type: 'text/plain',
}
);
实例属性与方法
File 对象有以下实例属性。
- File.lastModified:最后修改时间
- File.name:文件名或文件路径
- File.size:文件大小(单位字节)
- File.type:文件的 MIME 类型
var myFile = new File([], 'file.bin', {
lastModified: new Date(2018, 1, 1),
});
myFile.lastModified // 1517414400000
myFile.name // "file.bin"
myFile.size // 0
myFile.type // ""
上面代码中,由于myFile
的内容为空,也没有设置 MIME 类型,所以size
属性等于0,type
属性等于空字符串。
File 对象没有自己的实例方法,由于继承了 Blob 对象,因此可以使用 Blob 的实例方法slice()
。
FileLisr对象
FileList
对象是一个类似数组的对象,代表一组选中的文件,每个成员都是一个 File 实例。它主要出现在两个场合。
- 文件控件节点(
<input type="file">
)的files
属性,返回一个 FileList 实例。 - 拖拉一组文件时,目标区的
DataTransfer.files
属性,返回一个 FileList 实例。
// HTML 代码如下
// <input id="fileItem" type="file">
var files = document.getElementById('fileItem').files;
files instanceof FileList // true
上面代码中,文件控件的files
属性是一个 FileList 实例。
FileList 的实例属性主要是length
,表示包含多少个文件。
FileList 的实例方法主要是item()
,用来返回指定位置的实例。它接受一个整数作为参数,表示位置的序号(从零开始)。但是,由于 FileList 的实例是一个类似数组的对象,可以直接用方括号运算符,即myFileList[0]
等同于myFileList.item(0)
,所以一般用不到item()
方法。
FileReader对象
FileReader 对象用于读取 File 对象或 Blob 对象所包含的文件内容。
浏览器原生提供一个FileReader
构造函数,用来生成 FileReader 实例。
var reader = new FileReader();
FileReader 有以下的实例属性。
- FileReader.error:读取文件时产生的错误对象
- FileReader.readyState:整数,表示读取文件时的当前状态。一共有三种可能的状态,
0
表示尚未加载任何数据,1
表示数据正在加载,2
表示加载完成。 - FileReader.result:读取完成后的文件内容,有可能是字符串,也可能是一个 ArrayBuffer 实例。
- FileReader.onabort:
abort
事件(用户终止读取操作)的监听函数。 - FileReader.onerror:
error
事件(读取错误)的监听函数。 - FileReader.onload:
load
事件(读取操作完成)的监听函数,通常在这个函数里面使用result
属性,拿到文件内容。 - FileReader.onloadstart:
loadstart
事件(读取操作开始)的监听函数。 - FileReader.onloadend:
loadend
事件(读取操作结束)的监听函数。 - FileReader.onprogress:
progress
事件(读取操作进行中)的监听函数。
下面是监听load
事件的一个例子。
// HTML 代码如下
// <input type="file" onchange="onChange(event)">
function onChange(event) {
var file = event.target.files[0];
var reader = new FileReader();
reader.onload = function (event) {
console.log(event.target.result)
};
reader.readAsText(file);
}
上面代码中,每当文件控件发生变化,就尝试读取第一个文件。如果读取成功(load
事件发生),就打印出文件内容。
FileReader 有以下实例方法。
- FileReader.abort():终止读取操作,
readyState
属性将变成2
。 - FileReader.readAsArrayBuffer():以 ArrayBuffer 的格式读取文件,读取完成后
result
属性将返回一个 ArrayBuffer 实例。 - FileReader.readAsBinaryString():读取完成后,
result
属性将返回原始的二进制字符串。 - FileReader.readAsDataURL():读取完成后,
result
属性将返回一个 Data URL 格式(Base64 编码)的字符串,代表文件内容。对于图片文件,这个字符串可以用于<img>
元素的src
属性。注意,这个字符串不能直接进行 Base64 解码,必须把前缀data:*/*;base64,
从字符串里删除以后,再进行解码。 - FileReader.readAsText():读取完成后,
result
属性将返回文件内容的文本字符串。该方法的第一个参数是代表文件的 Blob 实例,第二个参数是可选的,表示文本编码,默认为 UTF-8。
下面是一个例子。
/* HTML 代码如下
<input type="file" onchange="previewFile()">
<img src="" height="200">
*/
function previewFile() {
var preview = document.querySelector('img');
var file = document.querySelector('input[type=file]').files[0];
var reader = new FileReader();
reader.addEventListener('load', function () {
preview.src = reader.result;
}, false);
if (file) {
reader.readAsDataURL(file);
}
}
上面代码中,用户选中图片文件以后,脚本会自动读取文件内容,然后作为一个 Data URL 赋值给<img>
元素的src
属性,从而把图片展示出来。
表单,FormData对象
表单概述
表单(<form>
)用来收集用户提交的数据,发送到服务器。比如,用户提交用户名和密码,让服务器验证,就要通过表单。表单提供多种控件,让开发者使用,具体的控件种类和用法请参考 HTML 语言的教程。本章主要介绍 JavaScript 与表单的交互。
<form action="/handling-page" method="post">
<div>
<label for="name">用户名:</label>
<input type="text" id="name" name="user_name" />
</div>
<div>
<label for="passwd">密码:</label>
<input type="password" id="passwd" name="user_passwd" />
</div>
<div>
<input type="submit" id="submit" name="submit_button" value="提交" />
</div>
</form>
上面代码就是一个简单的表单,包含三个控件:用户名输入框、密码输入框和提交按钮。
用户点击“提交”按钮,每一个控件都会生成一个键值对,键名是控件的name
属性,键值是控件的value
属性,键名和键值之间由等号连接。比如,用户名输入框的name
属性是user_name
,value
属性是用户输入的值,假定是“张三”,提交到服务器的时候,就会生成一个键值对user_name=张三
。
所有的键值对都会提交到服务器。但是,提交的数据格式跟<form>
元素的method
属性有关。该属性指定了提交数据的 HTTP 方法。如果是 GET 方法,所有键值对会以 URL 的查询字符串形式,提交到服务器,比如/handling-page?user_name=张三&user_passwd=123&submit_button=提交
。下面就是 GET 请求的 HTTP 头信息。
GET /handling-page?user_name=张三&user_passwd=123&submit_button=提交
Host: example.com
如果是 POST 方法,所有键值对会连接成一行,作为 HTTP 请求的数据体发送到服务器,比如user_name=张三&user_passwd=123&submit_button=提交
。下面就是 POST 请求的头信息。
POST /handling-page HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 74
user_name=张三&user_passwd=123&submit_button=提交
注意,实际提交的时候,只要键值不是 URL 的合法字符(比如汉字“张三”和“确定”),浏览器会自动对其进行编码。
点击submit
控件,就可以提交表单。
<form>
<input type="submit" value="提交">
</form>
上面表单就包含一个submit
控件,点击这个控件,浏览器就会把表单数据向服务器提交。
注意,表单里面的<button>
元素如果没有用type
属性指定类型,那么默认就是submit
控件。
<form>
<button>提交</button>
</form>
上面表单的<button>
元素,点击以后也会提交表单。
除了点击submit
控件提交表单,还可以用表单元素的submit()
方法,通过脚本提交表单。
formElement.submit();
表单元素的reset()
方法可以重置所有控件的值(重置为默认值)。
formElement.reset()
FormData 对象
概述
表单数据以键值对的形式向服务器发送,这个过程是浏览器自动完成的。但是有时候,我们希望通过脚本完成过程,构造和编辑表单键值对,然后通过XMLHttpRequest.send()
方法发送。浏览器原生提供了 FormData 对象来完成这项工作。
FormData 首先是一个构造函数,用来生成实例。
var formdata = new FormData(form);
FormData()
构造函数的参数是一个表单元素,这个参数是可选的。如果省略参数,就表示一个空的表单,否则就会处理表单元素里面的键值对。
下面是一个表单。
<form id="myForm" name="myForm">
<div>
<label for="username">用户名:</label>
<input type="text" id="username" name="username">
</div>
<div>
<label for="useracc">账号:</label>
<input type="text" id="useracc" name="useracc">
</div>
<div>
<label for="userfile">上传文件:</label>
<input type="file" id="userfile" name="userfile">
</div>
<input type="submit" value="Submit!">
</form>
我们用 FormData 对象处理上面这个表单。
var myForm = document.getElementById('myForm');
var formData = new FormData(myForm);
// 获取某个控件的值
formData.get('username') // ""
// 设置某个控件的值
formData.set('username', '张三');
formData.get('username') // "张三"
实例方法
FormData 提供以下实例方法。
FormData.get(key)
:获取指定键名对应的键值,参数为键名。如果有多个同名的键值对,则返回第一个键值对的键值。FormData.getAll(key)
:返回一个数组,表示指定键名对应的所有键值。如果有多个同名的键值对,数组会包含所有的键值。FormData.set(key, value)
:设置指定键名的键值,参数为键名。如果键名不存在,会添加这个键值对,否则会更新指定键名的键值。如果第二个参数是文件,还可以使用第三个参数,表示文件名。FormData.delete(key)
:删除一个键值对,参数为键名。FormData.append(key, value)
:添加一个键值对。如果键名重复,则会生成两个相同键名的键值对。如果第二个参数是文件,还可以使用第三个参数,表示文件名。FormData.has(key)
:返回一个布尔值,表示是否具有该键名的键值对。FormData.keys()
:返回一个遍历器对象,用于for...of
循环遍历所有的键名。FormData.values()
:返回一个遍历器对象,用于for...of
循环遍历所有的键值。FormData.entries()
:返回一个遍历器对象,用于for...of
循环遍历所有的键值对。如果直接用for...of
循环遍历 FormData 实例,默认就会调用这个方法。
下面是get()
、getAll()
、set()
、append()
方法的例子。
var formData = new FormData();
formData.set('username', '张三');
formData.append('username', '李四');
formData.get('username') // "张三"
formData.getAll('username') // ["张三", "李四"]
formData.append('userpic[]', myFileInput.files[0], 'user1.jpg');
formData.append('userpic[]', myFileInput.files[1], 'user2.jpg');
下面是遍历器的例子。
var formData = new FormData();
formData.append('key1', 'value1');
formData.append('key2', 'value2');
for (var key of formData.keys()) {
console.log(key);
}
// "key1"
// "key2"
for (var value of formData.values()) {
console.log(value);
}
// "value1"
// "value2"
for (var pair of formData.entries()) {
console.log(pair[0] + ': ' + pair[1]);
}
// key1: value1
// key2: value2
// 等同于遍历 formData.entries()
for (var pair of formData) {
console.log(pair[0] + ': ' + pair[1]);
}
// key1: value1
// key2: value2
表单的内置验证
自动校验
表单提交的时候,浏览器允许开发者指定一些条件,它会自动验证各个表单控件的值是否符合条件。
<!-- 必填 -->
<input required>
<!-- 必须符合正则表达式 -->
<input pattern="banana|cherry">
<!-- 字符串长度必须为6个字符 -->
<input minlength="6" maxlength="6">
<!-- 数值必须在1到10之间 -->
<input type="number" min="1" max="10">
<!-- 必须填入 Email 地址 -->
<input type="email">
<!-- 必须填入 URL -->
<input type="URL">
如果一个控件通过验证,它就会匹配:valid
的 CSS 伪类,浏览器会继续进行表单提交的流程。如果没有通过验证,该控件就会匹配:invalid
的 CSS 伪类,浏览器会终止表单提交,并显示一个错误信息。
checkValidity()
除了提交表单的时候,浏览器自动校验表单,还可以手动触发表单的校验。表单元素和表单控件都有checkValidity()
方法,用于手动触发校验。
// 触发整个表单的校验
form.checkValidity()
// 触发单个表单控件的校验
formControl.checkValidity()
checkValidity()
方法返回一个布尔值,true
表示通过校验,false
表示没有通过校验。因此,提交表单可以封装为下面的函数。
function submitForm(action) {
var form = document.getElementById('form');
form.action = action;
if (form.checkValidity()) {
form.submit();
}
}
willValidate 属性
控件元素的willValidate
属性是一个布尔值,表示该控件是否会在提交时进行校验。
// HTML 代码如下
// <form novalidate>
// <input id="name" name="name" required />
// </form>
var input = document.querySelector('#name');
input.willValidate // true
validationMessage 属性
控件元素的validationMessage
属性返回一个字符串,表示控件不满足校验条件时,浏览器显示的提示文本。以下两种情况,该属性返回空字符串。
- 该控件不会在提交时自动校验
- 该控件满足校验条件
// HTML 代码如下
// <form><input type="text" required></form>
document.querySelector('form input').validationMessage
// "请填写此字段。"
下面是另一个例子。
var myInput = document.getElementById('myinput');
if (!myInput.checkValidity()) {
document.getElementById('prompt').innerHTML = myInput.validationMessage;
}
setCustomValidity()
控件元素的setCustomValidity()
方法用来定制校验失败时的报错信息。它接受一个字符串作为参数,该字符串就是定制的报错信息。如果参数为空字符串,则上次设置的报错信息被清除。
如果调用这个方法,并且参数不为空字符串,浏览器就会认为控件没有通过校验,就会立刻显示该方法设置的报错信息。
/* HTML 代码如下
<form>
<p><input type="file" id="fs"></p>
<p><input type="submit"></p>
</form>
*/
document.getElementById('fs').onchange = checkFileSize;
function checkFileSize() {
var fs = document.getElementById('fs');
var files = fs.files;
if (files.length > 0) {
if (files[0].size > 75 * 1024) {
fs.setCustomValidity('文件不能大于 75KB');
return;
}
}
fs.setCustomValidity('');
}
上面代码一旦发现文件大于 75KB,就会设置校验失败,同时给出自定义的报错信息。然后,点击提交按钮时,就会显示报错信息。这种校验失败是不会自动消除的,所以如果所有文件都符合条件,要将报错信息设为空字符串,手动消除校验失败的状态。
validity 属性
控件元素的属性validity
属性返回一个ValidityState
对象,包含当前校验状态的信息。
该对象有以下属性,全部为只读属性。
ValidityState.badInput
:布尔值,表示浏览器是否不能将用户的输入转换成正确的类型,比如用户在数值框里面输入字符串。ValidityState.customError
:布尔值,表示是否已经调用setCustomValidity()
方法,将校验信息设置为一个非空字符串。ValidityState.patternMismatch
:布尔值,表示用户输入的值是否不满足模式的要求。ValidityState.rangeOverflow
:布尔值,表示用户输入的值是否大于最大范围。ValidityState.rangeUnderflow
:布尔值,表示用户输入的值是否小于最小范围。ValidityState.stepMismatch
:布尔值,表示用户输入的值不符合步长的设置(即不能被步长值整除)。ValidityState.tooLong
:布尔值,表示用户输入的字数超出了最长字数。ValidityState.tooShort
:布尔值,表示用户输入的字符少于最短字数。ValidityState.typeMismatch
:布尔值,表示用户填入的值不符合类型要求(主要是类型为 Email 或 URL 的情况)。ValidityState.valid
:布尔值,表示用户是否满足所有校验条件。ValidityState.valueMissing
:布尔值,表示用户没有填入必填的值。
下面是一个例子。
var input = document.getElementById('myinput');
if (input.validity.valid) {
console.log('通过校验');
} else {
console.log('校验失败');
}
下面是另外一个例子。
var txt = '';
if (document.getElementById('myInput').validity.rangeOverflow) {
txt = '数值超过上限';
}
document.getElementById('prompt').innerHTML = txt;
表单的 novalidate 属性
表单元素的 HTML 属性novalidate
,可以关闭浏览器的自动校验。
<form novalidate>
</form>
这个属性也可以在脚本里设置。
form.noValidate = true;
如果表单元素没有设置novalidate
属性,那么提交按钮(<button>
或<input>
元素)的formnovalidate
属性也有同样的作用。
<form>
<input type="submit" value="submit" formnovalidate>
</form>
enctype 属性
表单能够用四种编码,向服务器发送数据。编码格式由表单的enctype
属性决定。
假定表单有两个字段,分别是foo
和baz
,其中foo
字段的值等于bar
,baz
字段的值是一个分为两行的字符串。
The first line.
The second line.
下面四种格式,都可以将这个表单发送到服务器。
(1)GET 方法
如果表单使用GET
方法发送数据,enctype
属性无效。
<form
action="register.php"
method="get"
onsubmit="AJAXSubmit(this); return false;"
>
</form>
数据将以 URL 的查询字符串发出。
?foo=bar&baz=The%20first%20line.%0AThe%20second%20line.
(2)application/x-www-form-urlencoded
如果表单用POST
方法发送数据,并省略enctype
属性,那么数据以application/x-www-form-urlencoded
格式发送(因为这是默认值)。
<form
action="register.php"
method="post"
onsubmit="AJAXSubmit(this); return false;"
>
</form>
发送的 HTTP 请求如下。
Content-Type: application/x-www-form-urlencoded
foo=bar&baz=The+first+line.%0D%0AThe+second+line.%0D%0A
上面代码中,数据体里面的%0D%0A
代表换行符(\r\n
)。
(3)text/plain
如果表单使用POST
方法发送数据,enctype
属性为text/plain
,那么数据将以纯文本格式发送。
<form
action="register.php"
method="post"
enctype="text/plain"
onsubmit="AJAXSubmit(this); return false;"
>
</form>
发送的 HTTP 请求如下。
Content-Type: text/plain
foo=bar
baz=The first line.
The second line.
(4)multipart/form-data
如果表单使用POST
方法,enctype
属性为multipart/form-data
,那么数据将以混合的格式发送。
<form
action="register.php"
method="post"
enctype="multipart/form-data"
onsubmit="AJAXSubmit(this); return false;"
>
</form>
发送的 HTTP 请求如下。
Content-Type: multipart/form-data; boundary=---------------------------314911788813839
-----------------------------314911788813839
Content-Disposition: form-data; name="foo"
bar
-----------------------------314911788813839
Content-Disposition: form-data; name="baz"
The first line.
The second line.
-----------------------------314911788813839--
这种格式也是文件上传的格式。
文件上传
用户上传文件,也是通过表单。具体来说,就是通过文件输入框选择本地文件,提交表单的时候,浏览器就会把这个文件发送到服务器。
<input type="file" id="file" name="myFile">
此外,还需要将表单<form>
元素的method
属性设为POST
,enctype
属性设为multipart/form-data
。其中,enctype
属性决定了 HTTP 头信息的Content-Type
字段的值,默认情况下这个字段的值是application/x-www-form-urlencoded
,但是文件上传的时候要改成multipart/form-data
。
<form method="post" enctype="multipart/form-data">
<div>
<label for="file">选择一个文件</label>
<input type="file" id="file" name="myFile" multiple>
</div>
<div>
<input type="submit" id="submit" name="submit_button" value="上传" />
</div>
</form>
上面的 HTML 代码中,file 控件的multiple
属性,指定可以一次选择多个文件;如果没有这个属性,则一次只能选择一个文件。
var fileSelect = document.getElementById('file');
var files = fileSelect.files;
然后,新建一个 FormData 实例对象,模拟发送到服务器的表单数据,把选中的文件添加到这个对象上面。
var formData = new FormData();
for (var i = 0; i < files.length; i++) {
var file = files[i];
// 只上传图片文件
if (!file.type.match('image.*')) {
continue;
}
formData.append('photos[]', file, file.name);
}
最后,使用 Ajax 向服务器上传文件。
var xhr = new XMLHttpRequest();
xhr.open('POST', 'handler.php', true);
xhr.onload = function () {
if (xhr.status !== 200) {
console.log('An error occurred!');
}
};
xhr.send(formData);
除了发送 FormData 实例,也可以直接 AJAX 发送文件。
var file = document.getElementById('test-input').files[0];
var xhr = new XMLHttpRequest();
xhr.open('POST', 'myserver/uploads');
xhr.setRequestHeader('Content-Type', file.type);
xhr.send(file);
IndexedDB API
可以在用户本地建立一个数据库,类似nosql,然后就是增删改查的操作…
Web Worker
基本用法:
主线程采用new
命令,调用Worker()
构造函数,新建一个 Worker 线程。
var worker = new Worker('work.js');
Worker()
构造函数的参数是一个脚本文件,该文件就是 Worker 线程所要执行的任务。由于 Worker 不能读取本地文件,所以这个脚本必须来自网络。如果下载没有成功(比如404错误),Worker 就会默默地失败。
然后,主线程调用worker.postMessage()
方法,向 Worker 发消息。
worker.postMessage('Hello World');
worker.postMessage({method: 'echo', args: ['Work']});
worker.postMessage()
方法的参数,就是主线程传给 Worker 的数据。它可以是各种数据类型,包括二进制数据。
接着,主线程通过worker.onmessage
指定监听函数,接收子线程发回来的消息。
worker.onmessage = function (event) {
doSomething(event.data);
}
function doSomething() {
// 执行任务
worker.postMessage('Work done!');
}
上面代码中,事件对象的data
属性可以获取 Worker 发来的数据。
Worker 完成任务以后,主线程就可以把它关掉。
worker.terminate();
worker线程
Worker 线程内部需要有一个监听函数,监听message
事件。
self.addEventListener('message', function (e) {
self.postMessage('You said: ' + e.data);
}, false);
上面代码中,self
代表子线程自身,即子线程的全局对象。因此,等同于下面两种写法。
// 写法一
this.addEventListener('message', function (e) {
this.postMessage('You said: ' + e.data);
}, false);
// 写法二
addEventListener('message', function (e) {
postMessage('You said: ' + e.data);
}, false);
除了使用self.addEventListener()
指定监听函数,也可以使用self.onmessage
指定。监听函数的参数是一个事件对象,它的data
属性包含主线程发来的数据。self.postMessage()
方法用来向主线程发送消息。
根据主线程发来的数据,Worker 线程可以调用不同的方法,下面是一个例子。
self.addEventListener('message', function (e) {
var data = e.data;
switch (data.cmd) {
case 'start':
self.postMessage('WORKER STARTED: ' + data.msg);
break;
case 'stop':
self.postMessage('WORKER STOPPED: ' + data.msg);
self.close(); // Terminates the worker.
break;
default:
self.postMessage('Unknown command: ' + data.msg);
};
}, false);
上面代码中,self.close()
用于在 Worker 内部关闭自身。
work加载脚本
Worker 内部如果要加载其他脚本,有一个专门的方法importScripts()
。
importScripts('script1.js');
该方法可以同时加载多个脚本。
importScripts('script1.js', 'script2.js');
错误处理
主线程可以监听 Worker 是否发生错误。如果发生错误,Worker 会触发主线程的error
事件。
worker.onerror(function (event) {
console.log([
'ERROR: Line ', event.lineno, ' in ', event.filename, ': ', event.message
].join(''));
});
// 或者
worker.addEventListener('error', function (event) {
// ...
});
Worker 内部也可以监听error
事件。
关闭worker
使用完毕,为了节省系统资源,必须关闭 Worker。
// 主线程
worker.terminate();
// Worker 线程
self.close();
worker与主线程之间的数据通信
前面说过,主线程与 Worker 之间的通信内容,可以是文本,也可以是对象。需要注意的是,这种通信是拷贝关系,即是传值而不是传址,Worker 对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给 Worker,后者再将它还原。
主线程与 Worker 之间也可以交换二进制数据,比如 File、Blob、ArrayBuffer 等类型,也可以在线程之间发送。下面是一个例子。
// 主线程
var uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (var i = 0; i < uInt8Array.length; ++i) {
uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);
// Worker 线程
self.onmessage = function (e) {
var uInt8Array = e.data;
postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);
};
但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向 Worker 发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects。这使得主线程可以快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担。
如果要直接转移数据的控制权,就要使用下面的写法。
// Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);
// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
API
浏览器原生提供Worker()
构造函数,用来供主线程生成 Worker 线程。
var myWorker = new Worker(jsUrl, options);
Worker()
构造函数,可以接受两个参数。第一个参数是脚本的网址(必须遵守同源政策),该参数是必需的,且只能加载 JS 脚本,否则会报错。第二个参数是配置对象,该对象可选。它的一个作用就是指定 Worker 的名称,用来区分多个 Worker 线程。
// 主线程
var myWorker = new Worker('worker.js', { name : 'myWorker' });
// Worker 线程
self.name // myWorker
Worker()
构造函数返回一个 Worker 线程对象,用来供主线程操作 Worker。Worker 线程对象的属性和方法如下。
- Worker.onerror:指定 error 事件的监听函数。
- Worker.onmessage:指定 message 事件的监听函数,发送过来的数据在
Event.data
属性中。 - Worker.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
- Worker.postMessage():向 Worker 线程发送消息。
- Worker.terminate():立即终止 Worker 线程。
Web Worker 有自己的全局对象,不是主线程的window
,而是一个专门为 Worker 定制的全局对象。因此定义在window
上面的对象和方法不是全部都可以使用。
Worker 线程有一些自己的全局属性和方法。
- self.name: Worker 的名字。该属性只读,由构造函数指定。
- self.onmessage:指定
message
事件的监听函数。 - self.onmessageerror:指定 messageerror 事件的监听函数。发送的数据无法序列化成字符串时,会触发这个事件。
- self.close():关闭 Worker 线程。
- self.postMessage():向产生这个 Worker 线程发送消息。
- self.importScripts():加载 JS 脚本。