Skip to main content

JavaScript API Notes

BOM

Window

const selfWindow = window.self;
const topWindow = window.top;
const parentWindow = window.parent;
const grandParentWindow = window.parent.parent;
// eslint-disable-next-line no-restricted-globals
if (confirm('Are you sure?')) {
alert("I'm so glad you're sure!");
} else {
alert("I'm sorry to hear you're not sure.");
}

const result = prompt('What is your name? ', 'James');
if (result !== null) {
alert(`Welcome, ${result}`);
}

// 显示打印对话框
window.print();

// 显示查找对话框
window.find();

// 显式打印机
window.print();

弹窗有非常多的安全限制:

  • 禁止隐藏状态栏与地址栏.
  • 弹窗默认不能移动或缩放.
  • 只允许用户操作下 (鼠标/键盘) 创建弹窗.
  • 屏蔽弹窗.
const newWin = window.open(
'https://www.new.com/',
'newWindow',
'height=400,width=400,top=10,left=10,resizable=yes'
);
newWin.resizeTo(500, 500);
newWin.moveTo(100, 100);
alert(newWin.opener === window); // true
newWin.close();
alert(newWin.closed); // true

let blocked = false;
try {
const newWin = window.open('https://www.new.com/', '_blank');
if (newWin === null) {
blocked = true;
}
} catch (ex) {
blocked = true;
}
if (blocked) {
alert('The popup was blocked!');
}

Location

属性描述
hash设置或返回从井号 (#) 开始的 URL (锚)
host设置或返回主机名和当前 URL 的端口号
hostname设置或返回当前 URL 的主机名
href设置或返回完整的 URL
pathname设置或返回当前 URL 的路径部分
port设置或返回当前 URL 的端口号
protocol设置或返回当前 URL 的协议
search设置或返回从问号 (?) 开始的 URL (查询部分)
username设置或返回域名前指定的用户名
password设置或返回域名前指定的密码
origin返回 URL 的源地址
function getQueryStringArgs(location) {
// 取得没有开头问号的查询字符串
const qs = location.search.length > 0 ? location.search.substring(1) : '';
// 保存数据的对象
const args = {};

// 把每个参数添加到 args 对象
for (const item of qs.split('&').map(kv => kv.split('='))) {
const name = decodeURIComponent(item[0]);
const value = decodeURIComponent(item[1]);

if (name.length) {
args[name] = value;
}
}

return args;
}
window.location.assign('https://www.new.com');
window.location = 'https://www.new.com';
window.location.href = 'https://www.new.com';
window.location.replace('https://www.new.com'); // No new history
window.location.reload(); // 重新加载, 可能是从缓存加载
window.location.reload(true); // 重新加载, 从服务器加载
window.addEventListener(
'hashchange',
event => {
// event.oldURL
// event.nweURL
if (window.location.hash === '#someCoolFeature') {
someCoolFeature();
}
},
false
);

navigator 对象包含以下接口定义的属性和方法:

  • NavigatorID.
  • NavigatorLanguage.
  • NavigatorOnLine.
  • NavigatorContentUtils.
  • NavigatorStorage.
  • NavigatorStorageUtils.
  • NavigatorConcurrentHardware.
  • NavigatorPlugins.
  • NavigatorUserMedia.
Property/Method
batteryBatteryManager (Battery Status API)
clipboardClipboard API
connectionNetworkInformation (Network Information API)
cookieEnabledBoolean, 是否启用了 cookie
credentialsCredentialsContainer (Credentials Management API)
deviceMemory单位为 GB 的设备内存容量
doNotTrack用户的不跟踪 (do-not-track) 设置
geolocationGeolocation (Geolocation API)
hardwareConcurrency设备的处理器核心数量
language浏览器的主语言
languages浏览器偏好的语言数组
locksLockManager (Web Locks API)
mediaCapabilitiesMediaCapabilities (Media Capabilities API)
mediaDevices可用的媒体设备
maxTouchPoints设备触摸屏支持的最大触点数
onLineBoolean, 表示浏览器是否联网
pdfViewerEnabledBoolean, 是否启用了 PDF 功能
permissionsPermissions (Permissions API)
serviceWorkerServiceWorkerContainer
storageStorageManager (Storage API)
userAgent浏览器的用户代理字符串 (默认只读)
vendor浏览器的厂商名称
webdriver浏览器当前是否被自动化程序控制
xrXRSystem (WebXR Device API)
registerProtocolHandler()将一个网站注册为特定协议的处理程序
sendBeacon()异步传输一些小数据
share()当前平台的原生共享机制
vibrate()触发设备振动

Web Online API

const connectionStateChange = () => console.log(navigator.onLine);
window.addEventListener('online', connectionStateChange);
window.addEventListener('offline', connectionStateChange);
// 设备联网时:
// true
// 设备断网时:
// false

Web Connection API

const downlink = navigator.connection.downlink;
const downlinkMax = navigator.connection.downlinkMax;
const rtt = navigator.connection.rtt;
const type = navigator.connection.type; // wifi/bluetooth/cellular/ethernet/mixed/unknown/none.
const networkType = navigator.connection.effectiveType; // 2G - 5G.
const saveData = navigator.connection.saveData; // Boolean: Reduced data mode.

navigator.connection.addEventListener('change', changeHandler);

Web Protocol Handler API

navigator.registerProtocolHandler(
'mailto',
'http://www.somemailclient.com?cmd=%s',
'Some Mail Client'
);

Web Battery Status API

navigator.getBattery().then(battery => {
// 添加充电状态变化时的处理程序
const chargingChangeHandler = () => console.log(battery.charging);
battery.addEventListener('chargingchange', chargingChangeHandler);
// 添加充电时间变化时的处理程序
const chargingTimeChangeHandler = () => console.log(battery.chargingTime);
battery.addEventListener('chargingtimechange', chargingTimeChangeHandler);
// 添加放电时间变化时的处理程序
const dischargingTimeChangeHandler = () =>
console.log(battery.dischargingTime);
battery.addEventListener(
'dischargingtimechange',
dischargingTimeChangeHandler
);
// 添加电量百分比变化时的处理程序
const levelChangeHandler = () => console.log(battery.level * 100);
battery.addEventListener('levelchange', levelChangeHandler);
});

Web Storage Estimate API

navigator.storage.estimate().then(estimate => {
console.log(((estimate.usage / estimate.quota) * 100).toFixed(2));
});

Web Geolocation API

if (window.navigator.geolocation) {
// getCurrentPosition第三个参数为可选参数
navigator.geolocation.getCurrentPosition(locationSuccess, locationError, {
// 指示浏览器获取高精度的位置, 默认为false
enableHighAccuracy: true,
// 指定获取地理位置的超时时间, 默认不限时, 单位为毫秒
timeout: 5000,
// 最长有效期, 在重复获取地理位置时, 此参数指定多久再次获取位置.
maximumAge: 3000,
});
} else {
alert('Your browser does not support Geolocation!');
}

locationError 为获取位置信息失败的回调函数, 可以根据错误类型提示信息:

function locationError(error) {
switch (error.code) {
case error.TIMEOUT:
showError('A timeout occurred! Please try again!');
break;
case error.POSITION_UNAVAILABLE:
showError("We can't detect your location. Sorry!");
break;
case error.PERMISSION_DENIED:
showError('Please allow geolocation access for this to work.');
break;
case error.UNKNOWN_ERROR:
showError('An unknown error occurred!');
break;
default:
throw new Error('Unsupported error!');
}
}

locationSuccess 为获取位置信息成功的回调函数, 返回的数据中包含经纬度等信息:

  • position.timestamp.
  • position.coords:
    • latitude: 维度.
    • longitude: 经度.
    • accuracy.
    • altitude: 海拔高度.
    • altitudeAccuracy.

结合 Google Map API 即可在地图中显示当前用户的位置信息:

function locationSuccess(position) {
const coords = position.coords;
const latlng = new google.maps.LatLng(
// 维度
coords.latitude,
// 精度
coords.longitude
);
const myOptions = {
// 地图放大倍数
zoom: 12,
// 地图中心设为指定坐标点
center: latlng,
// 地图类型
mapTypeId: google.maps.MapTypeId.ROADMAP,
};

// 创建地图并输出到页面
const myMap = new google.maps.Map(document.getElementById('map'), myOptions);

// 创建标记
const marker = new google.maps.Marker({
// 标注指定的经纬度坐标点
position: latlng,
// 指定用于标注的地图
map: myMap,
});

// 创建标注窗口
const infoWindow = new google.maps.InfoWindow({
content: `您在这里<br/>纬度: ${coords.latitude}<br/>经度: ${coords.longitude}`,
});

// 打开标注窗口
infoWindow.open(myMap, marker);
}
navigator.geolocation.watchPosition(
locationSuccess,
locationError,
positionOption
);

navigator.userAgent 特别复杂:

  • 历史兼容问题: Netscape -> IE -> Firefox -> Safari -> Chrome -> Edge.
  • 每一个新的浏览器厂商必须保证旧网站的检测脚本能正常识别自家浏览器, 从而正常打开网页, 导致 navigator.userAgent 不断变长.
  • UserAgent Data Parser
console.log(navigator.userAgent);
// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)
// Chrome/101.0.4922.0 Safari/537.36 Edg/101.0.1198.0'

Screen

浏览器窗口外面的客户端显示器的信息:

Property
availHeight屏幕像素高度减去系统组件高度 (只读)
availWidth屏幕像素宽度减去系统组件宽度 (只读)
colorDepth表示屏幕颜色的位数: 多数系统是 32 (只读)
height屏幕像素高度
width屏幕像素宽度
pixelDepth屏幕的位深 (只读)
orientationScreen Orientation API 中屏幕的朝向
const screen = window.screen;

console.log(screen.colorDepth); // 24
console.log(screen.pixelDepth); // 24

// 垂直看
console.log(screen.orientation.type); // portrait-primary
console.log(screen.orientation.angle); // 0
// 向左转
console.log(screen.orientation.type); // landscape-primary
console.log(screen.orientation.angle); // 90
// 向右转
console.log(screen.orientation.type); // landscape-secondary
console.log(screen.orientation.angle); // 270

全屏 API:

function toggleFullscreen() {
const elem = document.querySelector('video');

if (document.fullscreenElement) {
document
.exitFullscreen()
.then(() => console.log('Document Exited from Full screen mode'))
.catch(err => console.error(err));
} else {
elem
.requestFullscreen()
.then(() => {})
.catch(err => {
alert(
`Error occurred while switch into fullscreen mode: ${err.message} (${err.name})`
);
});
}
}

document.onclick = function (event) {
if (document.fullscreenElement) {
document
.exitFullscreen()
.then(() => console.log('Document Exited from Full screen mode'))
.catch(err => console.error(err));
} else {
document.documentElement
.requestFullscreen({ navigationUI: 'show' })
.then(() => {})
.catch(err => {
alert(
`Error occurred while switch into fullscreen mode: ${err.message} (${err.name})`
);
});
}
};

History

History Navigation

const history = window.history;

// 后退一页
history.go(-1);
// 前进一页
history.go(1);
// 前进两页
history.go(2);
// 导航到最近的 new.com 页面
history.go('new.com');
// 导航到最近的 example.net 页面
history.go('example.net');
// 后退一页
history.back();
// 前进一页
history.forward();

if (history.length === 1) {
console.log('这是用户窗口中的第一个页面');
}

if (history.scrollRestoration) {
history.scrollRestoration = 'manual';
}

History State Management

const history = window.history;

const stateObject = { foo: 'bar' };
history.pushState(stateObject, 'My title', 'baz.html');

history.replaceState({ newFoo: 'newBar' }, 'New title'); // No new history state.

window.addEventListener('popstate', event => {
const state = event.state;

if (state) {
// 第一个页面加载时状态是 null
processState(state);
}
});

Browser Compatibility

User Agent Detection

class BrowserDetector {
constructor() {
// 测试条件编译
// IE6~10 支持
// eslint-disable-next-line spaced-comment
this.isIE_Gte6Lte10 = /*@cc_on!@*/ false;
// 测试 documentMode
// IE7~11 支持
this.isIE_Gte7Lte11 = !!document.documentMode;
// 测试 StyleMedia 构造函数
// Edge 20 及以上版本支持
this.isEdge_Gte20 = !!window.StyleMedia;
// 测试 Firefox 专有扩展安装 API
// 所有版本的 Firefox 都支持
this.isFirefox_Gte1 = typeof InstallTrigger !== 'undefined';
// 测试 chrome 对象及其 webstore 属性
// Opera 的某些版本有 window.chrome, 但没有 window.chrome.webstore
// 所有版本的 Chrome 都支持
this.isChrome_Gte1 = !!window.chrome && !!window.chrome.webstore;
// Safari 早期版本会给构造函数的标签符追加 "Constructor"字样, 如:
// window.Element.toString(); // [object ElementConstructor]
// Safari 3~9.1 支持
this.isSafari_Gte3Lte9_1 = /constructor/i.test(window.Element);
// 推送通知 API 暴露在 window 对象上
// 使用 IIFE 默认参数值以避免对 undefined 调用 toString()
// Safari 7.1 及以上版本支持
this.isSafari_Gte7_1 = (({ pushNotification = {} } = {}) =>
pushNotification.toString() === '[object SafariRemoteNotification]')(
window.safari
);
// 测试 addons 属性
// Opera 20 及以上版本支持
this.isOpera_Gte20 = !!window.opr && !!window.opr.addons;
}

isIE() {
return this.isIE_Gte6Lte10 || this.isIE_Gte7Lte11;
}

isEdge() {
return this.isEdge_Gte20 && !this.isIE();
}

isFirefox() {
return this.isFirefox_Gte1;
}

isChrome() {
return this.isChrome_Gte1;
}

isSafari() {
return this.isSafari_Gte3Lte9_1 || this.isSafari_Gte7_1;
}

isOpera() {
return this.isOpera_Gte20;
}
}

Browser Feature Detection

不使用特性/浏览器推断, 往往容易推断错误 (且会随着浏览器更新产生新的错误).

// 检测浏览器是否支持 Netscape 式的插件
const hasNSPlugins = !!(navigator.plugins && navigator.plugins.length);
// 检测浏览器是否具有 DOM Level 1 能力
const hasDOM1 = !!(
document.getElementById &&
document.createElement &&
document.getElementsByTagName
);

// 特性检测
if (document.getElementById) {
element = document.getElementById(id);
}

DOM

  • DOM Level 0.
  • DOM Level 1:
    • DOM Core.
    • DOM XML.
    • DOM HTML.
  • DOM Level 2:
    • DOM2 Core.
    • DOM2 XML.
    • DOM2 HTML.
    • DOM2 Views.
    • DOM2 StyleSheets.
    • DOM2 CSS.
    • DOM2 CSS 2.
    • DOM2 Events.
    • DOM2 UIEvents.
    • DOM2 MouseEvents.
    • DOM2 MutationEvents (Deprecated).
    • DOM2 HTMLEvents.
    • DOM2 Range.
    • DOM2 Traversal.
  • DOM Level 3:
    • DOM3 Core.
    • DOM3 XML.
    • DOM3 Events.
    • DOM3 UIEvents.
    • DOM3 MouseEvents.
    • DOM3 MutationEvents (Deprecated).
    • DOM3 MutationNameEvents.
    • DOM3 TextEvents.
    • DOM3 Load and Save.
    • DOM3 Load and Save Async.
    • DOM3 Validation.
    • DOM3 XPath.
const hasXmlDom = document.implementation.hasFeature('XML', '1.0');
const hasHtmlDom = document.implementation.hasFeature('HTML', '1.0');

DOM Core

document.createElement('nodeName');
document.createTextNode('String');

document.getElementById(id);
document.getElementsByName(elementName);
document.getElementsByTagName(tagName);
document.getElementsByClassName(className); // HTML5
document.querySelector(cssSelector); // Selectors API
document.querySelectorAll(cssSelector); // Selectors API

element.getAttribute(attrName); // get default HTML attribute
element.setAttribute(attrName, attrValue);
element.removeAttribute(attrName);

element.compareDocumentPosition(element);
element.contains(element);
element.isSameNode(element); // Same node reference
element.isEqualNode(element); // Same nodeName/nodeValue/attributes/childNodes
element.matches(cssSelector);
element.closest(cssSelector); // Returns closest ancestor matching selector
element.cloneNode();
element.normalize();
element.before(...elements);
element.after(...elements);
element.replaceWith(...elements);
element.remove();

parentElement.hasChildNodes();
parentElement.appendChild(childElement);
parentElement.append(childElements);
parentElement.insertBefore(newChild, targetChild);
parentElement.replaceChild(newChild, targetChild);
parentElement.replaceChildren(children);
parentElement.removeChild(child);
const showAlert = (type, message, duration = 3) => {
const div = document.createElement('div');
div.className = type;
div.appendChild(document.createTextNode(message));
container.insertBefore(div, form);
setTimeout(() => div.remove(), duration * 1000);
};

DOM Node Type

Node 除包括元素结点 (tag) 外, 包括许多其它结点 (甚至空格符视作一个结点), 需借助 nodeType 找出目标结点.

Node TypeNode RepresentationNode NameNode Value
1ELEMENT_NODETag Namenull
2ATTRIBUTE_NODEAttr NameAttr Value
3TEXT_NODE#textText
4CDATA_SECTION_NODE#cdata-sectionCDATA Section
5ENTITY_REFERENCE_NODE
6ENTITY_NODE
8COMMENT_NODE#commentComment
9DOCUMENT_NODE#documentnull
10DOCUMENT_TYPE_NODEhtml/xmlnull
11DOCUMENT_FRAGMENT_NODE#document-fragmentnull
12NOTATION_NODE
const type = node.nodeType;
const name = node.nodeName;
const value = node.nodeValue;

if (someNode.nodeType === Node.ELEMENT_NODE) {
alert('Node is an element.');
}

DOM Attribute Node

const id = element.attributes.getNamedItem('id').nodeValue;
const id = element.attributes.id.nodeValue;
element.attributes.id.nodeValue = 'someOtherId';
const oldAttr = element.attributes.removeNamedItem('id');
element.attributes.setNamedItem(newAttr);
const attr = document.createAttribute('align');
attr.value = 'left';
element.setAttributeNode(attr);

alert(element.attributes.align.value); // "left"
alert(element.getAttributeNode('align').value); // "left"
alert(element.getAttribute('align')); // "left"

DOM Text Node

Text node methods:

  • appendData(text): 向节点末尾添加文本 text.
  • deleteData(offset, count): 从位置 offset 开始删除 count 个字符.
  • insertData(offset, text): 在位置 offset 插入 text.
  • replaceData(offset, count, text): 用 text 替换从位置 offset 到 offset + count 的文本.
  • splitText(offset): 在位置 offset 将当前文本节点拆分为两个文本节点.
  • substringData(offset, count): 提取从位置 offset 到 offset + count 的文本.

Normalize text nodes:

const element = document.createElement('div');
element.className = 'message';

const textNode = document.createTextNode('Hello world!');
const anotherTextNode = document.createTextNode('Yippee!');

element.appendChild(textNode);
element.appendChild(anotherTextNode);
document.body.appendChild(element);
alert(element.childNodes.length); // 2

element.normalize();
alert(element.childNodes.length); // 1
alert(element.firstChild.nodeValue); // "Hello world!Yippee!"

Split text nodes:

const element = document.createElement('div');
element.className = 'message';

const textNode = document.createTextNode('Hello world!');
element.appendChild(textNode);
document.body.appendChild(element);

const newNode = element.firstChild.splitText(5);
alert(element.firstChild.nodeValue); // "Hello"
alert(newNode.nodeValue); // " world!"
alert(element.childNodes.length); // 2
TextContent vs InnerText vs InnerHTML
  • textContent:
    • Security: Doesn’t parse HTML.
    • Performance: Including <script> and <style> text content.
  • innerText:
    • Doesn't parse HTML.
    • Only show human-readable text content
    • innerText care CSS styles, read innerText value will trigger reflow.
  • innerHTML:
    • Do parse HTML.
const textContent = element.textContent;
const innerText = element.innerText;
const innerHTML = element.innerHTML;

DOM Document Node

document node (#document):

alert(document.nodeType); // 9
alert(document.nodeName); // "#document"
alert(document.nodeValue); // null
const html = document.documentElement;
const doctype = document.doctype;
const head = document.head; // HTML5 head.
const body = document.body;

const title = document.title; // 可修改.
const domain = document.domain; // 可设置同源域名.
const url = document.URL;
const referer = document.referer;
const charSet = document.characterSet; // HTML5 characterSet.

const anchors = documents.anchors;
const images = documents.images;
const links = documents.links;
const forms = documents.forms;
const formElements = documents.forms[0].elements; // 第一个表单内的所有字段

// HTML5 compatMode:
if (document.compatMode === 'CSS1Compat') {
console.log('Standards mode');
} else if (document.compatMode === 'BackCompat') {
console.log('Quirks mode');
}
document.getElementById(id);
// eslint-disable-next-line no-restricted-globals
document.getElementsByName(name);
document.getElementsByTagName(tagName);
document.getElementsByClassName(className); // HTML5
document.querySelector(cssSelector); // Selectors API
document.querySelectorAll(cssSelector); // Selectors API
document.write();
document.writeln();

DOM Document Type Node

<!DOCTYPE html PUBLIC "-// W3C// DTD HTML 4.01// EN" "http:// www.w3.org/TR/html4/strict.dtd">
console.log(document.doctype.name); // "html"
console.log(document.nodeType); // 10
console.log(document.doctype.nodeName); // "html"
console.log(document.doctype.nodeValue); // null
console.log(document.doctype.publicId); // "-// W3C// DTD HTML 4.01// EN"
console.log(document.doctype.systemId); // "http://www.w3.org/TR/html4/strict.dtd"

const doctype = document.implementation.createDocumentType(
'html',
'-// W3C// DTD HTML 4.01// EN',
'http://www.w3.org/TR/html4/strict.dtd'
);
const doc = document.implementation.createDocument(
'http://www.w3.org/1999/xhtml',
'html',
doctype
);

DOM Document Fragment Node

减少 DOM 操作次数, 减少页面渲染次数:

const frag = document.createDocumentFragment();

let p;
let t;

p = document.createElement('p');
t = document.createTextNode('first paragraph');
p.appendChild(t);
frag.appendChild(p);

p = document.createElement('p');
t = document.createTextNode('second paragraph');
p.appendChild(t);
frag.appendChild(p);

// 只渲染一次HTML页面
document.body.appendChild(frag);

克隆节点进行处理, 处理完毕后再替换原节点:

const oldNode = document.getElementById('result');
const clone = oldNode.cloneNode(true);
// work with the clone

// when you're done:
oldNode.parentNode.replaceChild(clone, oldNode);

Parse HTML:

const range = document.createRange();
const parse = range.createContextualFragment.bind(range);

parse(`<ol>
<li>a</li>
<li>b</li>
</ol>
<ol>
<li>c</li>
<li>d</li>
</ol>`);

function parseHTML(string) {
const context = document.implementation.createHTMLDocument();

// Set the base href for the created document so any parsed elements with URLs
// are based on the document's URL
const base = context.createElement('base');
base.href = document.location.href;
context.head.appendChild(base);

context.body.innerHTML = string;
return context.body.children;
}

DOM Programming

Append DOM Node

MethodNodeHTMLTextIEEvent ListenersSecure
appendYesNoYesNoPreservesYes
appendChildYesNoNoYesPreservesYes
innerHTMLNoYesYesYesLosesCareful
insertAdjacentHTMLNoYesYesYesPreservesCareful
const testDiv = document.getElementById('testDiv');

const para = document.createElement('p');
testDiv.appendChild(para);

const txt = document.createTextNode('Hello World');
para.appendChild(txt);

innerHTML: non-concrete, including all types of childNodes:

div.innerHTML = '<p>Test<em>test</em>Test.</p>';
// <div>
// <p>Test<em>test</em>Test.</p>
// </div>

innerHTML performance:

// BAD
for (const value of values) {
ul.innerHTML += `<li>${value}</li>`; // 别这样做!
}

// GOOD
let itemsHtml = '';
for (const value of values) {
itemsHtml += `<li>${value}</li>`;
}
ul.innerHTML = itemsHtml;

// BEST
ul.innerHTML = values.map(value => `<li>${value}</li>`).join('');

Insert DOM Node

// Append
el.appendChild(newEl);

// Prepend
el.insertBefore(newEl, el.firstChild);

// InsertBefore
el.parentNode.insertBefore(newEl, el);

// InsertAfter
function insertAfter(newElement, targetElement) {
const parent = targetElement.parentNode;

if (parent.lastChild === targetElement) {
parent.appendChild(newElement);
} else {
parent.insertBefore(newElement, targetElement.nextSibling);
}
}

insertAdjacentHTML/insertAdjacentText:

  • beforebegin: 插入前一个兄弟节点.
  • afterbegin: 插入第一个子节点.
  • beforeend: 插入最后一个子节点.
  • afterend: 插入下一个兄弟节点.
// 4 positions:
//
// <!-- beforebegin -->
// <p>
// <!-- afterbegin -->
// foo
// <!-- beforeend -->
// </p>
// <!-- afterend -->
const p = document.querySelector('p');

p.insertAdjacentHTML('beforebegin', '<a></a>');
p.insertAdjacentText('afterbegin', 'foo');

// simply be moved element, not copied element
p.insertAdjacentElement('beforebegin', link);

Replace DOM Node

node.replaceChild(document.createTextNode(text), node.firstChild);
node.replaceChildren(...nodeList);

Remove DOM Node

// 删除第一个子节点
const formerFirstChild = someNode.removeChild(someNode.firstChild);

// 删除最后一个子节点
const formerLastChild = someNode.removeChild(someNode.lastChild);

while (div.firstChild) {
div.removeChild(div.firstChild);
}

// Remove self
el.parentNode.removeChild(el);
el.remove();

Traverse DOM Node

const parent = node.parentNode;
const children = node.childNodes;
const first = node.firstChild;
const last = node.lastChild;
const previous = node.previousSibling;
const next = node.nextSibling;

node.matches(selector);

Element Traversal API: navigation properties listed above refer to all nodes. For instance, in childNodes can see both text nodes, element nodes, and even comment nodes.

const count = el.childElementCount;
const parent = el.parentElement;
const children = el.children;
const first = el.firstElementChild;
const last = el.lastElementChild;
const previous = el.previousElementSibling;
const next = el.nextElementSibling;

el.matches(selector);

NodeList is iterable:

const elements = document.querySelectorAll('div');

for (const element of elements) {
console.log(element);
}

Node Iterator:

const div = document.getElementById('div1');
const filter = function (node) {
return node.tagName.toLowerCase() === 'li'
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_SKIP;
};
const iterator = document.createNodeIterator(
div,
NodeFilter.SHOW_ELEMENT,
filter,
false
);

for (
let node = iterator.nextNode();
node !== null;
node = iterator.nextNode()
) {
console.log(node.tagName); // 输出标签名
}

Tree Walker:

const div = document.getElementById('div1');
const walker = document.createTreeWalker(
div,
NodeFilter.SHOW_ELEMENT,
null,
false
);

walker.firstChild(); // 前往<p>
walker.nextSibling(); // 前往<ul>

for (
let node = walker.firstChild();
node !== null;
node = walker.nextSibling()
) {
console.log(node.tagName); // 遍历 <li>
}
NodeIterator vs TreeWalker
  • NodeFilter.acceptNode() FILTER_REJECT:
    • For NodeIterator, this flag is synonymous with FILTER_SKIP.
    • For TreeWalker, child nodes are also rejected.
  • TreeWalker has more methods:
    • firstChild.
    • lastChild.
    • previousSibling.
    • nextSibling.

Attributes DOM Node

HTML attributes 设置对应的 DOM properties 初始值.

alert(div.getAttribute('id')); // "myDiv" default div.id
alert(div.getAttribute('class')); // "bd" default div.class
div.setAttribute('id', 'someOtherId');
div.setAttribute('class', 'ft');
div.removeAttribute('id');
div.removeAttribute('class');

// `data-src`
console.log(el.dataset.src);

Select DOM Node

Range API:

  • startContainer: 范围起点所在的节点 (选区中第一个子节点的父节点).
  • startOffset: 范围起点在 startContainer 中的偏移量.
  • endContainer: 范围终点所在的节点 (选区中最后一个子节点的父节点).
  • endOffset: 范围起点在 startContainer 中的偏移量.
  • commonAncestorContainer: 文档中以 startContainerendContainer 为后代的最深的节点.
  • setStartBefore(refNode): 把范围的起点设置到 refNode 之前, 从而让 refNode 成为选区的第一个子节点.
  • setStartAfter(refNode): 把范围的起点设置到 refNode 之后, 从而将 refNode 排除在选区之外, 让其下一个同胞节点成为选区的第一个子节点.
  • setEndBefore(refNode): 把范围的终点设置到 refNode 之前, 从而将 refNode 排除在选区之外, 让其上一个同胞节点成为选区的最后一个子节点.
  • setEndAfter(refNode): 把范围的终点设置到 refNode 之后, 从而让 refNode 成为选区的最后一个子节点.
  • setStart(refNode, offset).
  • setEnd(refNode, offset).
  • deleteContents(): remove.
  • extractContents(): remove and return.
  • cloneContents(): clone.
  • insertNode(node): 在范围选区的开始位置插入一个节点.
  • surroundContents(node): 插入包含范围的内容.
  • collapse(boolean): 范围折叠.
  • compareBoundaryPoints(Range.HOW, sourceRange): 确定范围之间是否存在公共的边界 (起点或终点).
<!doctype html>
<html>
<body>
<p id="p1"><b>Hello</b> world!</p>
</body>
</html>
const p1 = document.getElementById('p1');
const helloNode = p1.firstChild.firstChild;
const worldNode = p1.lastChild;
const range = document.createRange();

range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
const fragment1 = range.cloneContents(); // clone
const fragment2 = range.extractContents(); // remove and return

p1.parentNode.appendChild(fragment1);
p1.parentNode.appendChild(fragment2);
const p1 = document.getElementById('p1');
const helloNode = p1.firstChild.firstChild;
const worldNode = p1.lastChild;
const range = document.createRange();

const span = document.createElement('span');
span.style.color = 'red';
span.appendChild(document.createTextNode('Inserted text'));

range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);
range.insertNode(span);
// <p id="p1"><b>He<span style="color: red">Inserted text</span>llo</b> world</p>
const p1 = document.getElementById('p1');
const helloNode = p1.firstChild.firstChild;
const worldNode = p1.lastChild;
const range = document.createRange();

const span = document.createElement('span');
span.style.backgroundColor = 'yellow';

range.selectNode(helloNode);
range.surroundContents(span);
// <p><b><span style="background-color:yellow">Hello</span></b> world!</p>

Dynamic Scripts Loading

function loadScript(url) {
const script = document.createElement('script');
script.src = url;
script.async = true;
document.body.appendChild(script);
}
function loadScriptString(code) {
const script = document.createElement('script');
script.async = true;
script.type = 'text/javascript';

try {
script.appendChild(document.createTextNode(code));
} catch (ex) {
script.text = code;
}

document.body.appendChild(script);
}
InnerHTML Script

所有现代浏览器中, 通过 innerHTML 属性创建的 <script> 元素永远不会执行.

Dynamic Styles Loading

function loadStyles(url) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = url;

const head = document.getElementsByTagName('head')[0];
head.appendChild(link);
}
function loadStyleString(css) {
const style = document.createElement('style');
style.type = 'text/css';

try {
style.appendChild(document.createTextNode(css));
} catch (ex) {
style.styleSheet.cssText = css;
}

const head = document.getElementsByTagName('head')[0];
head.appendChild(style);
}
StyleSheet CSSText
  • 若重用同一个 <style> 元素并设置该属性超过一次, 则可能导致浏览器崩溃.
  • cssText 设置为空字符串也可能导致浏览器崩溃.

Table Manipulation

<table> 元素添加了以下属性和方法:

  • caption: 指向 <caption> 元素的指针 (如果存在).
  • tBodies: 包含 <tbody> 元素的 HTMLCollection.
  • tFoot: 指向 <tfoot> 元素 (如果存在).
  • tHead: 指向 <thead> 元素 (如果存在).
  • rows: 包含表示所有行的 HTMLCollection.
  • createTHead(): 创建 <thead> 元素, 放到表格中, 返回引用.
  • createTFoot(): 创建 <tfoot> 元素, 放到表格中, 返回引用.
  • createCaption(): 创建 <caption> 元素, 放到表格中, 返回引用.
  • deleteTHead(): 删除 <thead> 元素.
  • deleteTFoot(): 删除 <tfoot> 元素.
  • deleteCaption(): 删除 <caption> 元素.
  • deleteRow(pos): 删除给定位置的行.
  • insertRow(pos): 在行集合中给定位置插入一行.

<tbody> 元素添加了以下属性和方法:

  • rows: 包含 <tbody> 元素中所有行的 HTMLCollection.
  • deleteRow(pos): 删除给定位置的行.
  • insertRow(pos): 在行集合中给定位置插入一行, 返回该行的引用.

<tr> 元素添加了以下属性和方法:

  • cells: 包含 <tr> 元素所有表元的 HTMLCollection.
  • deleteCell(pos): 删除给定位置的表元.
  • insertCell(pos): 在表元集合给定位置插入一个表元, 返回该表元的引用.
// 创建表格
const table = document.createElement('table');
table.border = 1;
table.width = '100%';

// 创建表体
const tbody = document.createElement('tbody');
table.appendChild(tbody);

// 创建第一行
tbody.insertRow(0);
tbody.rows[0].insertCell(0);
tbody.rows[0].cells[0].appendChild(document.createTextNode('Cell 1, 1'));
tbody.rows[0].insertCell(1);
tbody.rows[0].cells[1].appendChild(document.createTextNode('Cell 2, 1'));

// 创建第二行
tbody.insertRow(1);
tbody.rows[1].insertCell(0);
tbody.rows[1].cells[0].appendChild(document.createTextNode('Cell 1, 2'));
tbody.rows[1].insertCell(1);
tbody.rows[1].cells[1].appendChild(document.createTextNode('Cell 2, 2'));

// 把表格添加到文档主体
document.body.appendChild(table);

Iframe

Attribute
src="https://google.com/"Sets address of the document to embed
srcdoc="<p>Some html</p>"Sets HTML content of the page to show
height="100px"Sets iframe height in pixels
width="100px"Sets iframe width in pixels
name="my-iframe"Sets name of the iframe (used in JavaScript
allow="fullscreen"Sets feature policy for the iframe
referrerpolicy="no-referrer"Sets referrer when fetching iframe content
sandbox="allow-same-origin"Sets restrictions of the iframe
loading="lazy"Lazy loading
<iframe src="https://www.google.com/" height="500px" width="500px"></iframe>
<iframe src="https://platform.twitter.com/widgets/tweet_button.html"></iframe>
<iframe srcdoc="<html><body>App</body></html>"></iframe>
<iframe
sandbox="allow-same-origin allow-top-navigation allow-forms allow-scripts"
src="http://maps.example.com/embedded.html"
></iframe>
const iframeDocument = iframe.contentDocument;
const iframeStyles = iframe.contentDocument.querySelectorAll('.css');
iframe.contentWindow.postMessage('message', '*');

CSSOM

CSS Object Model is a set of APIs allowing the manipulation of CSS from JavaScript. It is much like the DOM, but for the CSS rather than the HTML. It allows users to read and modify CSS style dynamically.

Inline Styles

interface Element {
style: CSSStyleDeclaration;
}

const style = element.style.XX;
const font = element.style.fontFamily;
const mt = element.style.marginTopWidth;

Styles Getter and Setter

  • cssText: 一次生效.
  • length.
  • getPropertyValue(name).
  • getPropertyPriority: return '' or important.
  • item(index).
  • setProperty(name, value, priority).
  • removeProperty(name).
const box = document.querySelector('.box');

box.style.setProperty('color', 'orange');
box.style.setProperty('font-family', 'Georgia, serif');
op.innerHTML = box.style.getPropertyValue('color');
op2.innerHTML = `${box.style.item(0)}, ${box.style.item(1)}`;

box.style.setProperty('font-size', '1.5em');
box.style.item(0); // "font-size"

document.body.style.removeProperty('font-size');
document.body.style.item(0); // ""

myDiv.style.cssText = 'width: 25px; height: 100px; background-color: green';

for (let i = 0, len = myDiv.style.length; i < len; i++) {
console.log(myDiv.style[i]); // 或者用 myDiv.style.item(i)
}

Computed Styles

  • Shorthand style for full property.
  • Longhand style for specific property.
  • getPropertyValue can get css variables.
  • 在所有浏览器中计算样式都是只读的, 不能修改 getComputedStyle() 方法返回的对象.
const background = window.getComputedStyle(document.body).background;

// dot notation, same as above
const backgroundColor = window.getComputedStyle(el).backgroundColor;

// square bracket notation
const backgroundColor = window.getComputedStyle(el)['background-color'];

// using getPropertyValue()
// can get css variables property too
window.getComputedStyle(el).getPropertyValue('background-color');

CSS Class List

element.classList.add('class');
element.classList.remove('class');
element.classList.toggle('class');
element.classList.contains('class');
function addClassPolyfill(element, value) {
if (!element.className) {
element.className = value;
} else {
newClassName = element.className;
newClassName += ' ';
newClassName += value;
element.className = newClassName;
}
}

DOM StyleSheets API

以下是 CSSStyleSheetStyleSheet 继承的属性:

  • disabled: Boolean, 表示样式表是否被禁用了 (设置为 true 会禁用样式表).
  • href: <link> URL/null.
  • media: 样式表支持的媒体类型集合.
  • ownerNode: 指向拥有当前样式表的节点 <link>/<style>/null (@import).
  • title: ownerNode 的 title 属性.
  • parentStyleSheet: @import parent.
  • type: 样式表的类型 ('text/css').
  • cssRules: 当前样式表包含的样式规则的集合.
  • ownerRule: 如果样式表是使用 @import 导入的, 则指向导入规则.
  • deleteRule(index): 在指定位置删除 cssRules 中的规则.
  • insertRule(rule, index): 在指定位置向 cssRules 中插入规则.
CSS Rules Definition

CSSRule:

  • type of CSSRule: STYLE_RULE (1), IMPORT_RULE (3), MEDIA_RULE (4), KEYFRAMES_RULE (7).
  • cssText: 返回整条规则的文本.
  • selectorText: 返回规则的选择符文本.
  • style: 返回 CSSStyleDeclaration 对象, 可以设置和获取当前规则中的样式.
  • parentRule: 如果这条规则被其他规则 (如 @media) 包含, 则指向包含规则.
  • parentStyleSheet: 包含当前规则的样式表.
const myRules = document.styleSheets[0].cssRules;
const p = document.querySelector('p');

for (i of myRules) {
if (i.type === 1) {
p.innerHTML += `<code>${i.selectorText}</code><br>`;
}

if (i.selectorText === 'a:hover') {
i.selectorText = 'a:hover, a:active';
}

const myStyle = i.style;

// Set the bg color on the body
myStyle.setProperty('background-color', 'peachPuff');

// Get the font size of the body
myStyle.getPropertyValue('font-size');

// Get the 5th item in the body's style rule
myStyle.item(5);

// Log the current length of the body style rule (8)
console.log(myStyle.length);

// Remove the line height
myStyle.removeProperty('line-height');

// log the length again (7)
console.log(myStyle.length);

// Check priority of font-family (empty string)
myStyle.getPropertyPriority('font-family');
}
Media Rules
  • conditionText property of media rule.
  • Nested cssRules.
const myRules = document.styleSheets[0].cssRules;
const p = document.querySelector('.output');

for (i of myRules) {
if (i.type === 4) {
p.innerHTML += `<code>${i.conditionText}</code><br>`;

for (j of i.cssRules) {
p.innerHTML += `<code>${j.selectorText}</code><br>`;
}
}
}
Keyframe Rules
  • name property of keyframe rule
  • keyText property of keyframe rule.
  • Nested cssRules.
const myRules = document.styleSheets[0].cssRules;
const p = document.querySelector('.output');

for (i of myRules) {
if (i.type === 7) {
p.innerHTML += `<code>${i.name}</code><br>`;

for (j of i.cssRules) {
p.innerHTML += `<code>${j.keyText}</code><br>`;
}
}
}
Manipulate CSS Rules
const myStylesheet = document.styleSheets[0];
console.log(myStylesheet.cssRules.length); // 8

document.styleSheets[0].insertRule(
'article { line-height: 1.5; font-size: 1.5em; }',
myStylesheet.cssRules.length
);
console.log(document.styleSheets[0].cssRules.length); // 9
const myStylesheet = document.styleSheets[0];
console.log(myStylesheet.cssRules.length); // 8

myStylesheet.deleteRule(3);
console.log(myStylesheet.cssRules.length); // 7

CSS Typed Object Model API

CSS Typed Object Model API simplifies CSS property manipulation by exposing CSS values as typed JavaScript objects rather than strings.

StylePropertyMap:

const styleMap = document.body.computedStyleMap();
const cssValue = styleMap.get('line-height');
const { value, unit } = cssValue;

CSSStyleValue:

const styleMap = document.querySelector('#myElement').attributeStyleMap;
styleMap.set('display', new CSSKeywordValue('initial'));
console.log(myElement.get('display').value); // 'initial'

DOM Events

  • event.preventDefault().
  • event.stopPropagation().
  • By default, event handlers are executed in the bubbling phase (unless set useCapture to true).
  • element.dispatchEvent(event) to trigger events.

Events Object

Property/MethodType
typeString被触发的事件类型
trustedBoolean浏览器生成/JavaScript 创建
ViewAbstractView事件所发生的 window 对象
currentTargetElementEvent handler binding
targetElementEvent trigger
bubblesBoolean事件是否冒泡
cancelableBoolean是否可以取消事件的默认行为
eventPhaseNumber捕获阶段/到达目标/冒泡阶段
defaultPreventedBooleanpreventDefault() called
preventDefault()Function用于取消事件的默认行为
stopPropagation()Function用于取消所有后续事件捕获或冒泡
stopImmediatePropagation()Function用于取消所有后续事件捕获或冒泡

Events Checking

function handleEvent(event) {
node.matches(event.target); // return false or true
node.contains(event.target); // return false or true
}

Global UI Events

DOMContentLoaded event:

  • 当文档中没有脚本时, 浏览器解析完 HTML 文档便能触发 DOMContentLoaded 事件.
  • 如果文档中包含脚本, 则脚本会阻塞文档的解析, 脚本需要等 CSSOM 构建完成才能执行:
    • DOM/CSSOM 构建完毕, async 脚本执行完成之后, DOMContentLoaded 事件触发.
    • HTML 文档构建不受 defer 脚本影响, 不需要等待 defer 脚本执行与样式表加载, HTML 解析完毕后, DOMContentLoaded 立即触发.
  • 在任何情况下, DOMContentLoaded 的触发不需要等待图片等其他资源加载完成.
  • HTML 文档解析完成就会触发 DOMContentLoaded, 所有资源加载完成之后, load 事件才会被触发.
function ready(fn) {
if (document.readyState !== 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}

document.addEventListener('DOMContentLoaded', event => {
console.log('DOM fully loaded and parsed.');
});

readystatechange event:

document.addEventListener('readystatechange', event => {
// HTML5 readyState
if (
document.readyState === 'interactive' ||
document.readyState === 'complete'
) {
console.log('Content loaded');
} else if (document.readyState === 'loading') {
console.log('Loading');
}
});

load event, 加载完成:

window.addEventListener('load', () => {
const image = document.createElement('img');
image.addEventListener('load', event => {
console.log(event.target.src);
});
document.body.appendChild(image);
image.src = 'smile.gif';

const script = document.createElement('script');
script.addEventListener('load', event => {
console.log('Loaded');
});
script.src = 'example.js';
script.async = true;
document.body.appendChild(script);

const link = document.createElement('link');
link.type = 'text/css';
link.rel = 'stylesheet';
link.addEventListener('load', event => {
console.log('css loaded');
});
link.href = 'example.css';
document.getElementsByTagName('head')[0].appendChild(link);
});

visibilitychange event, 切换标签页时改变网页标题/声音/视频:

window.addEventListener('visibilitychange', () => {
switch (document.visibilityState) {
case 'hidden':
console.log('Tab隐藏');
break;
case 'visible':
console.log('Tab被聚焦');
break;
default:
throw new Error('Unsupported visibility!');
}
});
const videoElement = document.getElementById('videoElement');

// AutoPlay the video if application is visible
if (document.visibilityState === 'visible') {
videoElement.play();
}

// Handle page visibility change events
function handleVisibilityChange() {
if (document.visibilityState === 'hidden') {
videoElement.pause();
} else {
videoElement.play();
}
}

document.addEventListener('visibilitychange', handleVisibilityChange, false);
  • beforeunload event.
  • unload event: 卸载完成.
  • abort event: 提前终止.
  • error event.
  • select event: 在文本框 (<input>textarea) 上选择字符.
  • resize event: 缩放.
  • scroll event: 滚动.

Form Events

// <form className='validated-form' noValidate onSubmit={onSubmit}>

const onSubmit = event => {
event.preventDefault();

const form = event.target;
const isValid = form.checkValidity(); // returns true or false
const formData = new FormData(form);

const validationMessages = Array.from(formData.keys()).reduce((acc, key) => {
acc[key] = form.elements[key].validationMessage;
return acc;
}, {});

setErrors(validationMessages);

console.log({
validationMessages,
data,
isValid,
});

if (isValid) {
// here you do what you need to do if is valid
const data = Array.from(formData.keys()).reduce((acc, key) => {
acc[key] = formData.get(key);
return acc;
}, {});
} else {
// apply invalid class
Array.from(form.elements).forEach(i => {
if (i.checkValidity()) {
// field is valid
i.parentElement.classList.remove('invalid');
} else {
// field is invalid
i.parentElement.classList.add('invalid');
console.log(i.validity);
}
});
}
};
document.querySelector('form').addEventListener('submit', event => {
const form = event.target;
const url = new URL(form.action || window.location.href);
const formData = new FormData(form);
const searchParameters = new URLSearchParams(formData);

const options = {
method: form.method,
};

if (options.method === 'post') {
// Modify request body to include form data
options.body =
form.enctype === 'multipart/form-data' ? formData : searchParameters;
} else {
// Modify URL to include form data
url.search = searchParameters;
}

fetch(url, options);
event.preventDefault();
});

Input Events

  • blur/focus/focusin/focusout event.
  • input/change event.
  • select event: 在文本框 (<input>textarea) 上选择字符.
  • composition event: 中文输入事件.
Input Focus Event

HTML5 focus management:

  • 在页面完全加载之前, document.activeElement 为 null.
  • 默认情况下, document.activeElement 在页面刚加载完之后会设置为 document.body.
document.getElementById('myButton').focus();
console.log(document.activeElement === button); // true
console.log(document.hasFocus()); // true
Focus Events

当焦点从页面中的一个元素移到另一个元素上时, 会依次发生如下事件:

  1. focusout: 在失去焦点的元素上触发.
  2. focusin: 在获得焦点的元素上触发
  3. blur: 在失去焦点的元素上触发
  4. DOMFocusOut: 在失去焦点的元素上触发
  5. focus: 在获得焦点的元素上触发
  6. DOMFocusIn: 在获得焦点的元素上触发.
Input Change Event
  • input event:
    • <input type="text" />.
    • <input type="password"/>.
    • <textarea />.
  • change event:
    • <input type="checkbox" />.
    • <input type="radio" />.
    • <input type="file" />.
    • <input type="file" multiple />.
    • <select />.
const input = document.querySelector('input');

input.addEventListener('change', () => {
for (const file of Array.from(input.files)) {
const reader = new FileReader();
reader.addEventListener('load', () => {
console.log('File', file.name, 'starts with', reader.result.slice(0, 20));
});
reader.readAsText(file);
}
});
Input Select Event
const input = document.querySelector('input');

input.addEventListener('select', event => {
const log = document.getElementById('log');
const selection = event.target.value.substring(
event.target.selectionStart,
event.target.selectionEnd
);
log.textContent = `You selected: ${selection}`;
});

Clipboard Events

Clipboard API (modern alternative for document.execCommand(command)):

  • copy event.
  • cut event.
  • paste event.
const source = document.querySelector('div.source');

source.addEventListener('copy', event => {
const selection = document.getSelection();
event.clipboardData.setData(
'text/plain',
selection.toString().concat('copyright information')
);
event.preventDefault();
});

Mouse Events

  • mousedown event.
  • mouseup event.
  • click event:
    • mousedownmouseup 都触发后, 触发此事件.
    • event.clientX/event.clientY.
    • event.pageX/event.pageY.
    • event.screenX/event.screenY.
    • event.shiftKey/event.ctrlKey/event.altKey/event.metaKey.
  • dbclick event: click 两次触发后, 触发此事件.
  • mousemove event.
  • mouseenter event.
  • mouseleave event: pointer has exited the element and all of its descendants.
  • mouseout event: pointer leaves the element or leaves one of the element's descendants.
  • mouseover event.
  • wheel event (replace deprecated mousewheel event).

For click event, no need for X/Y to judge internal/outside state. Use element.contains to check is a better way.

window.addEventListener('click', event => {
if (document.getElementById('main').contains(event.target)) {
process();
}
});

Drag Event:

  • dragstart: start point.
  • dragend
  • dragenter: call event.preventDefault() in drop zone.
  • dragover: call event.preventDefault() in drop zone.
  • dragleave
  • drop: end point.

Key point for implementing DnD widget is DataTransfer:

  • Bindings between Drag Zone and Drop Zone.
  • DataTransfer.dropEffect and DataTransfer.effectAllowed to define DnD UI type.
  • DataTransfer.getData and DataTransfer.setData to transfer data.
  • DataTransfer.files and DataTransfer.items to transfer data.

Context Menu Event:

const noContext = document.getElementById('noContextMenu');

noContext.addEventListener('contextmenu', e => {
e.preventDefault();
});

Keyboard Events

keydown/keypress/keyup event:

const textbox = document.getElementById('myText');

textbox.addEventListener('keyup', event => {
console.log(event.charCode || event.keyCode);
});

event.key (replace deprecated event.keyCode):

'Alt';
'CapsLock';
'Control';
'Fn';
'Numlock';
'Shift';
'Enter';
'Tab';
' '; // space bar

'ArrowDown';
'ArrowLeft';
'ArrowRight';
'ArrowUp';
'Home';
'End';
'PageDOwn';
'PageUp';

'Backspace';
'Delete';
'Redo';
'Undo';

Device Events

  • deviceorientation event.
  • devicemotion event.
  • touchstart event.
  • touchmove event.
  • touchend event.
  • touchcancel event.

Use touch events:

  • Dispatch custom tap/press/swipe/pinch/drag/drop/rotate event.
  • Dispatch standard click/dbclick/mousedown/mouseup/mousemove` event.
interface Pointer {
startTouch: Touch;
startTime: number;
status: string;
element: TouchEventTarget;
lastTouch?: Touch;
lastTime?: number;
deltaX?: number;
deltaY?: number;
duration?: number;
distance?: number;
isVertical?: boolean;
}

type TouchEventTarget = HTMLDivElement | EventTarget;
type TouchEventHandler = (pointer: Pointer, touch: Touch) => void;

class Recognizer {
pointers: Map<Touch['identifier'], Pointer>;

constructor() {
this.pointers = new Map();
}

start(event: TouchEvent, callback?: TouchEventHandler) {
// touches: 当前屏幕上所有触摸点的列表.
// targetTouches: 当前对象上所有触摸点的列表.
// changedTouches: 涉及当前事件的触摸点的列表.
for (let i = 0; i < event.changedTouches.length; i++) {
const touch = event.changedTouches[i];
const pointer: Pointer = {
startTouch: touch,
startTime: Date.now(),
status: 'tapping',
element: event.target,
};
this.pointers.set(touch.identifier, pointer);
if (callback) callback(pointer, touch);
}
}

move(event: TouchEvent, callback?: TouchEventHandler) {
for (let i = 0; i < event.changedTouches.length; i++) {
const touch = event.changedTouches[i];
const pointer = this.pointers.get(touch.identifier);

if (!pointer) {
return;
}

if (!pointer.lastTouch) {
pointer.lastTouch = pointer.startTouch;
pointer.lastTime = pointer.startTime;
pointer.deltaX = 0;
pointer.deltaY = 0;
pointer.duration = 0;
pointer.distance = 0;
}

let time = Date.now() - pointer.lastTime;

if (time > 0) {
const RECORD_DURATION = 70;

if (time > RECORD_DURATION) {
time = RECORD_DURATION;
}

if (pointer.duration + time > RECORD_DURATION) {
pointer.duration = RECORD_DURATION - time;
}

pointer.duration += time;
pointer.lastTouch = touch;
pointer.lastTime = Date.now();
pointer.deltaX = touch.clientX - pointer.startTouch.clientX;
pointer.deltaY = touch.clientY - pointer.startTouch.clientY;
const x = pointer.deltaX * pointer.deltaX;
const y = pointer.deltaY * pointer.deltaY;
pointer.distance = Math.sqrt(x + y);
pointer.isVertical = x < y;

if (callback) callback(pointer, touch);
}
}
}

end(event: TouchEvent, callback?: TouchEventHandler) {
for (let i = 0; i < event.changedTouches.length; i++) {
const touch = event.changedTouches[i];
const id = touch.identifier;
const pointer = this.pointers.get(id);

if (!pointer) continue;
if (callback) callback(pointer, touch);

this.pointers.delete(id);
}
}

cancel(event: TouchEvent, callback?: TouchEventHandler) {
this.end(event, callback);
}

fire(elem: TouchEventTarget, type: string, props: EventInit) {
if (elem) {
const event = new Event(type, {
bubbles: true,
cancelable: true,
...props,
});
elem.dispatchEvent(event);
}
}

static bind(el: TouchEventTarget, recognizer: Recognizer) {
function move(event: TouchEvent) {
recognizer.move(event);
}

function end(event: TouchEvent) {
recognizer.end(event);
document.removeEventListener('touchmove', move);
document.removeEventListener('touchend', end);
document.removeEventListener('touchcancel', cancel);
}

function cancel(event: TouchEvent) {
recognizer.cancel(event);
document.removeEventListener('touchmove', move);
document.removeEventListener('touchend', end);
document.removeEventListener('touchcancel', cancel);
}

el.addEventListener('touchstart', function (event: TouchEvent) {
recognizer.start(event);
document.addEventListener('touchmove', move);
document.addEventListener('touchend', end);
document.addEventListener('touchcancel', cancel);
});
}
}

export default Recognizer;

Dispatch Events

Dispatch MouseEvent:

const btn = document.getElementById('myBtn');

// 创建 event 对象
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: document.defaultView,
});

// 触发事件
btn.dispatchEvent(event);

Dispatch KeyboardEvent:

const textbox = document.getElementById('myTextbox');

// 按照 DOM3 的方式创建 event 对象
if (document.implementation.hasFeature('KeyboardEvents', '3.0')) {
// 初始化 event 对象
const event = new KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
view: document.defaultView,
key: 'a',
location: 0,
shiftKey: true,
});

// 触发事件
textbox.dispatchEvent(event);
}

Dispatch CustomEvent:

const div = document.getElementById('myDiv');
div.addEventListener('myEvent', event => {
console.log(`DIV: ${event.detail}`);
});
document.addEventListener('myEvent', event => {
console.log(`DOCUMENT: ${event.detail}`);
});

if (document.implementation.hasFeature('CustomEvents', '3.0')) {
const event = new CustomEvent('myEvent', {
bubbles: true,
cancelable: true,
detail: 'Hello world!',
});
div.dispatchEvent(event);
}

Events Util

class EventUtil {
static getEvent(event) {
return event || window.event;
}

static getTarget(event) {
return event.target || event.srcElement;
}

static getRelatedTarget(event) {
// For `mouseover` and `mouseout` event:
if (event.relatedTarget) {
return event.relatedTarget;
} else if (event.toElement) {
return event.toElement;
} else if (event.fromElement) {
return event.fromElement;
} else {
return null;
}
}

static preventDefault(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}

static stopPropagation(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}

static addHandler(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent(`on${type}`, handler);
} else {
element[`on${type}`] = handler;
}
}

static removeHandler(element, type, handler) {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false);
} else if (element.detachEvent) {
element.detachEvent(`on${type}`, handler);
} else {
element[`on${type}`] = null;
}
}
}

DOM Rect

DOM Width and Height

  • outerHeight: 整个浏览器窗口的大小, 包括窗口标题/工具栏/状态栏等.
  • innerHeight: DOM 视口的大小, 包括滚动条.
  • offsetHeight: 整个可视区域大小, 包括 border 和 scrollbar 在内 (content + padding + border).
  • clientHeight: 内部可视区域大小 (content + padding).
  • scrollHeight: 元素内容的高度, 包括溢出部分.

Client Size

// const supportInnerWidth = window.innerWidth !== undefined;
// const supportInnerHeight = window.innerHeight !== undefined;
// const isCSS1Compat = (document.compatMode || '') === 'CSS1Compat';
const width =
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth;
const height =
window.innerHeight ||
document.documentElement.clientHeight ||
document.body.clientHeight;
// 缩放到 100×100
window.resizeTo(100, 100);
// 缩放到 200×150
window.resizeBy(100, 50);
// 缩放到 300×300
window.resizeTo(300, 300);
DOM Rect API

In case of transforms, the offsetWidth and offsetHeight returns the layout width and height (all the same), while getBoundingClientRect() returns the rendering width and height.

getBoundingClientRect:

Client Rect

const isElementInViewport = el => {
const { top, height, left, width } = el.getBoundingClientRect();
const w =
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth;
const h =
window.innerHeight ||
document.documentElement.clientHeight ||
document.body.clientHeight;

return top <= h && top + height >= 0 && left <= w && left + width >= 0;
};

DOM Left and Top

  • offsetLeft/offsetTop: 表示该元素的左上角 (边框外边缘) 与已定位的父容器 (offsetParent 对象) 左上角的距离.
  • clientLeft/clientTop: 表示该元素 padding 至 margin 的距离, 始终等于 .getComputedStyle() 返回的 border-left-width/border-top-width.
  • scrollLeft/scrollTop: 元素滚动条位置, 被隐藏的内容区域左侧/上方的像素位置.

Offset Size

function getElementLeft(element) {
let actualLeft = element.offsetLeft;
let current = element.offsetParent;

while (current !== null) {
actualLeft += current.offsetLeft;
current = current.offsetParent;
}

return actualLeft;
}

function getElementTop(element) {
let actualTop = element.offsetTop;
let current = element.offsetParent;

while (current !== null) {
actualTop += current.offsetTop;
current = current.offsetParent;
}

return actualTop;
}
// 把窗口移动到左上角
window.moveTo(0, 0);
// 把窗口向下移动 100 像素
window.moveBy(0, 100);
// 把窗口移动到坐标位置 (200, 300)
window.moveTo(200, 300);
// 把窗口向左移动 50 像素
window.moveBy(-50, 0);

DOM Scroll Size

  • scrollLeft/scrollX/PageXOffset: 元素内容向右滚动了多少像素, 如果没有滚动则为 0.
  • scrollTop/scrollY/pageYOffset: 元素内容向上滚动了多少像素, 如果没有滚动则为 0.

Scroll Size

// const supportPageOffset = window.pageXOffset !== undefined;
// const isCSS1Compat = (document.compatMode || '') === 'CSS1Compat';
const x =
window.pageXOffset ||
document.documentElement.scrollLeft ||
document.body.scrollLeft;
const y =
window.pageYOffset ||
document.documentElement.scrollTop ||
document.body.scrollTop;
if (window.innerHeight + window.pageYOffset === document.body.scrollHeight) {
console.log('Scrolled to Bottom!');
}
// 相对于当前视口向下滚动 100 像素
window.scrollBy(0, 100);
// 相对于当前视口向右滚动 40 像素
window.scrollBy(40, 0);

// 滚动到页面左上角
window.scrollTo(0, 0);
// 滚动到距离屏幕左边及顶边各 100 像素的位置
window.scrollTo(100, 100);
// 正常滚动
window.scrollTo({
left: 100,
top: 100,
behavior: 'auto',
});
// 平滑滚动
window.scrollTo({
left: 100,
top: 100,
behavior: 'smooth',
});

document.forms[0].scrollIntoView(); // 窗口滚动后, 元素底部与视口底部对齐.
document.forms[0].scrollIntoView(true); // 窗口滚动后, 元素顶部与视口顶部对齐.
document.forms[0].scrollIntoView({ block: 'start' });
document.forms[0].scrollIntoView({ behavior: 'smooth', block: 'start' });

DOM Observer

Intersection Observer

// <img class="lzy_img" src="lazy_img.jpg" data-src="real_img.jpg" />
document.addEventListener('DOMContentLoaded', () => {
const imageObserver = new IntersectionObserver((entries, imgObserver) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const lazyImage = entry.target;
console.log('Lazy loading ', lazyImage);
lazyImage.src = lazyImage.dataset.src;

// only load image once
lazyImage.classList.remove('lzy');
imgObserver.unobserve(lazyImage);
}
});
});

const lazyImages = document.querySelectorAll('img.lzy_img');
lazyImages.forEach(lazyImage => imageObserver.observe(lazyImage));
});

Mutation Observer

如果文档中连续插入 1000 个 <li> 元素, 就会连续触发 1000 个插入事件, 执行每个事件的回调函数, 这很可能造成浏览器的卡顿; Mutation Observer 只会在 1000 个段落都插入结束后才会触发, 且只触发一次.

Mutation Observer 有以下特点:

  • 它等待所有脚本任务完成后, 才会运行, 即采用异步方式.
  • 它把 DOM 变动记录封装成一个数组进行处理, 而不是一条条地个别处理 DOM 变动.
  • 记录队列和回调处理的默认行为是耗尽这个队列, 处理每个 MutationRecord, 然后让它们超出作用域并被垃圾回收.
  • MutationObserver 实例拥有被观察目标节点的弱引用, 不会妨碍垃圾回收程序回收目标节点.
  • 它即可以观察发生在 DOM 节点的所有变动, 也可以观察某一类变动.
  • 被观察子树中的节点 ({ subtree: true }) 被移出子树之后仍然能够触发变化事件.
const mutationObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
console.log(mutation);
});
});

// 开始侦听页面的根 HTML 元素中的更改.
mutationObserver.observe(document.documentElement, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true,
});
const target = document.querySelector('#container');
const callback = (mutations, observer) => {
mutations.forEach(mutation => {
switch (mutation.type) {
case 'attributes':
// the name of the changed attribute is in
// mutation.attributeName
// and its old value is in mutation.oldValue
// the current value can be retrieved with
// target.getAttribute(mutation.attributeName)
break;
case 'childList':
// any added nodes are in mutation.addedNodes
// any removed nodes are in mutation.removedNodes
break;
default:
throw new Error('Unsupported mutation!');
}
});
};

const observer = new MutationObserver(callback);
observer.observe(target, {
attributes: true,
attributeFilter: ['foo'], // only observe attribute 'foo'
attributeOldValue: true,
childList: true,
});
const observer = new MutationObserver(mutationRecords =>
console.log(mutationRecords)
);

// 创建两个初始子节点
document.body.appendChild(document.createElement('div'));
document.body.appendChild(document.createElement('span'));

observer.observe(document.body, { childList: true });

// 交换子节点顺序
document.body.insertBefore(document.body.lastChild, document.body.firstChild);
// 发生了两次变化: 第一次是节点被移除, 第二次是节点被添加
// [
// {
// addedNodes: NodeList[],
// attributeName: null,
// attributeNamespace: null,
// oldValue: null,
// nextSibling: null,
// previousSibling: div,
// removedNodes: NodeList[span],
// target: body,
// type: childList,
// },
// {
// addedNodes: NodeList[span],
// attributeName: null,
// attributeNamespace: null,
// oldValue: null,
// nextSibling: div,
// previousSibling: null,
// removedNodes: NodeList[],
// target: body,
// type: "childList",
// }
// ]

XML Namespace

XML 命名空间可以实现在一个格式规范的文档中混用不同的 XML 语言, 避免元素命名冲突 (tagName/localName/namespaceURI):

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Example XHTML page</title>
</head>
<body>
<s:svg
xmlns:s="http://www.w3.org/2000/svg"
version="1.1"
viewBox="0 0 100 100"
style="width:100%; height:100%"
>
<s:rect x="0" y="0" width="100" height="100" style="fill:red" />
</s:svg>
</body>
</html>
console.log(document.body.isDefaultNamespace('http://www.w3.org/1999/xhtml'));
console.log(svg.lookupPrefix('http://www.w3.org/2000/svg')); // "s"
console.log(svg.lookupNamespaceURI('s')); // "http://www.w3.org/2000/svg"

const newSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
const newAttr = document.createAttributeNS(
'http://www.somewhere.com',
'random'
);
const elems = document.getElementsByTagNameNS(
'http://www.w3.org/1999/xhtml',
'*'
);

Network

JSON

JSON (JavaScript Object Notation) methods:

const obj = JSON.parse(json);
const json = JSON.stringify(obj);

JSON.stringify(value, filter, space):

  • Symbol/function/NaN/Infinity/undefined: null/ignored.
  • BitInt: throw TypeError.
  • Circular reference object: throw TypeError.
  • toJSON method:
const obj = {
name: 'zc',
toJSON() {
return 'return toJSON';
},
};

// return toJSON
console.log(JSON.stringify(obj));

// "2022-03-06T08:24:56.138Z"
JSON.stringify(new Date());

AJAX

AJAX Data Format

FormatSize (bytes)Download (ms)Parse (ms)
Verbose XML582,960999.4343.1
Verbose JSON-P487,913598.20.0
Simple XML437,960475.183.1
Verbose JSON487,895527.726.7
Simple JSON392,895498.729.0
Simple JSON-P392,913454.03.1
Array JSON292,895305.418.6
Array JSON-P292,912316.03.4
Custom Format (script insertion)222,91266.311.7
Custom Format (XHR)222,89263.114.5

AJAX Usage

const XHR = (function () {
const standard = {
createXHR() {
return new XMLHttpRequest();
},
};
const newActionXObject = {
createXHR() {
return new ActionXObject('Msxml12.XMLHTTP');
},
};
const oldActionXObject = {
createXHR() {
return new ActionXObject('Microsoft.XMLHTTP');
},
};

// 根据兼容性返回对应的工厂对象
// 此立即函数运行一次即可完成兼容性检查, 防止重复检查
if (standard.createXHR()) {
return standard;
} else {
try {
newActionXObject.createXHR();
return newActionXObject;
} catch (o) {
oldActionXObject.createXHR();
return oldActionXObject;
}
}
})();

const request = XHR.createXHR();

// 3rd argument : async mode
request.open('GET', 'example.txt', true);

request.onreadystatechange = function () {
// do something
/*
switch(request.readyState) {
case 0: initialize
case 1: loading
case 2: loaded
case 3: transaction
case 4: complete
}
*/
if (request.readyState === 4) {
const para = document.createElement('p');
const txt = document.createTextNode(request.responseText);
para.appendChild(txt);
document.getElementById('new').appendChild(para);
}
};

request.send(null);
ajax({
url: './TestXHR.aspx', // 请求地址
type: 'POST', // 请求方式
data: { name: 'super', age: 20 }, // 请求参数
dataType: 'json',
success(response, xml) {
// 此处放成功后执行的代码
},
fail(status) {
// 此处放失败后执行的代码
},
});

function ajax(options) {
options = options || {};
options.type = (options.type || 'GET').toUpperCase();
options.dataType = options.dataType || 'json';
const params = formatParams(options.data);
let xhr;

// 创建 - 非IE6 - 第一步
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else {
// IE6及其以下版本浏览器
xhr = new ActiveXObject('Microsoft.XMLHTTP');
}

// 接收 - 第三步
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
const status = xhr.status;
if (status >= 200 && status < 300) {
options.success && options.success(xhr.responseText, xhr.responseXML);
} else {
options.fail && options.fail(status);
}
}
};

// 连接 和 发送 - 第二步
if (options.type === 'GET') {
xhr.open('GET', `${options.url}?${params}`, true);
xhr.send(null);
} else if (options.type === 'POST') {
xhr.open('POST', options.url, true);
// 设置表单提交时的内容类型
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send(params);
}
}

// 格式化参数
function formatParams(data) {
const arr = [];

for (const name in data) {
arr.push(`${encodeURIComponent(name)}=${encodeURIComponent(data[name])}`);
}

arr.push(`v=${Math.random()}`.replace('.', ''));
return arr.join('&');
}
function getJSON(url) {
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();

request.open('GET', url);

request.onload = function () {
try {
if (this.status === 200) {
resolve(JSON.parse(this.response));
} else {
reject(Error(`${this.status} ${this.statusText}`));
}
} catch (e) {
reject(e.message);
}
};

request.onerror = function () {
reject(Error(`${this.status} ${this.statusText}`));
};

request.send();
});
}

getJSON('data/sample.json')
.then(ninjas => {
assert(ninjas !== null, 'Get data');
})
.catch(e => handleError(`Error: ${e}`));

AJAX Cross Origin Request

<!-- HTML -->
<meta http-equiv="Access-Control-Allow-Origin" content="*" />
Response.Headers.Add('Access-Control-Allow-Origin', '*');
$.ajax({
url: 'http://map.oicqzone.com/gpsApi.php?lat=22.502412986242&lng=113.93832783228',
type: 'GET',
dataType: 'JSONP', // 处理 AJAX 跨域问题.
success(data) {
$('body').append(`Name: ${data}`);
},
});

AJAX Alternatives

  • client.request(config).
  • client.get(url[, config]).
  • client.delete(url[, config]).
  • client.head(url[, config]).
  • client.options(url[, config]).
  • client.post(url[, data[, config]]).
  • client.put(url[, data[, config]]).
  • client.patch(url[, data[, config]]).
  • client.getUri([config]).
const client = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: { 'X-Custom-Header': 'foobar' },
});

// Add a request interceptor
client.interceptors.request.use(
config => {
// Do something before request is sent.
return config;
},
error => {
// Do something with request error.
return Promise.reject(error);
}
);

client.interceptors.response.use(
response => {
// Any status code that lie within the range of 2xx trigger this function.
// Do something with response data.
return response;
},
error => {
// Any status codes that falls outside the range of 2xx trigger this function.
// Do something with response error.
return Promise.reject(error);
}
);

Fetch

  • GET: read resources.
  • POST: create resources.
  • PUT: fully update resources.
  • PATCH: partially update resources.
  • DELETE: delete resources.

Fetch Basis Usage

const response = await fetch('/api/names', {
headers: {
Accept: 'application/json',
},
});

const response = await fetch('/api/names', {
method: 'POST',
body: JSON.stringify(object),
headers: {
'Content-Type': 'application/json',
},
});

Fetch Form Data

const imageFormData = new FormData();
const imageInput = document.querySelector('input[type="file"][multiple]');
const imageFiles = imageInput.files;

for (const file of imageFiles) {
imageFormData.append('image', file);
}

fetch('/img-upload', {
method: 'POST',
body: imageFormData,
});

Fetch Aborting

const abortController = new AbortController();

fetch('wikipedia.zip', { signal: abortController.signal }).catch(() =>
console.log('Aborted!')
);

// 10 毫秒后中断请求
setTimeout(() => abortController.abort(), 10);

Fetch Objects API

Headers object:

const myHeaders = new Headers();
myHeaders.append('Content-Type', 'text/xml');
myHeaders.get('Content-Type'); // should return 'text/xml'

Request object:

const request = new Request('/api/names', {
method: 'POST',
body: JSON.stringify(object),
headers: {
'Content-Type': 'application/json',
},
});

const response = await fetch(request);

Response object:

fetch('//foo.com').then(console.log);
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: true
// redirected: false
// status: 200
// statusText: "OK"
// type: "basic"
// url: "https://foo.com/"
// }

fetch('//foo.com/redirect-me').then(console.log);
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}