DOM 和 BOM 研究

443 阅读10分钟

DOM

文档对象模型 (DOM) 将 web 页面与到脚本或编程语言连接起来。通常是指 JavaScript,但将 HTML、SVG 或 XML 文档建模为对象并不是 JavaScript 语言的一部分。DOM模型用一个逻辑树来表示一个文档,树的每个分支的终点都是一个节点(node),每个节点都包含着对象(objects)。DOM的方法(methods)让你可以用特定方式操作这个树,用这些方法你可以改变文档的结构、样式或者内容。节点可以关联上事件处理器,一旦某一事件被触发了,那些事件处理器就会被执行

Document

documentDocument 的一个实例, document.constructorHTMLDocument, 继承了 Document

同时 Document 还继承了 NodeParentNode, 因此有一些处理节点的方法

  • 基本属性

    interface Document extends Node, ParentNode, ... {
        readonly URL: string;
        readonly documentURI: string;
        domain: string;
        cookie: string;
        
        readonly head: HTMLHeadElement;
        title: string;
        readonly scripts: HTMLCollectionOf<HTMLScriptElement>;
        body: HTMLElement;
        readonly documentElement: HTMLElement;
        
        readonly characterSet: string;
        readonly charset: string;
        readonly contentType: string;
        readonly doctype: DocumentType | null;
        
        designMode: string; // 复制辣鸡百度文档神器
        
        readonly fullscreenEnabled: boolean;
        
        location: Location
        
        readonly origin: string;
        readonly readyState: DocumentReadyState;
        readonly referrer: string;
        
        // ...
    }
    
  • 基本方法

    interface Document extends Node, ParentNode, ... {
        // 1. 查询元素, querySelector 在 ParentNode 中介绍
        getElementById(elementId: string): HTMLElement | null;
        
        getElementsByClassName(classNames: string): HTMLCollectionOf<Element>;
        
        getElementsByName(elementName: string): NodeListOf<HTMLElement>;
        
        getElementsByTagName(qualifiedName: string): HTMLCollectionOf<Element>;
        
        // 2. 创建元素
        createAttribute(localName: string): Attr;
        createComment(data: string): Comment;
        createDocumentFragment(): DocumentFragment; // 创建新的文档
        createElement(tagName: string, options?: ElementCreationOptions): HTMLElement;
        createTextNode(data: string): Text;
        
        // 3. 事件
        addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
        
        removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
        
        createEvent(eventInterface: 'xxx'): xxx;
        // 例如 createEvent(eventInterface: 'FocusEvent'): FocusEvent
        
        // ...
    }
    

Node

代表一个 DOM 节点

  • 属性

    interface Node extends EventTarget {
        // 节点信息
        readonly nodeName: string;
        readonly nodeType: number;
        nodeValue: string | null;
        textContent: string | null;
        readonly ownerDocument: Document | null;
        readonly isConnected: boolean; // 节点是否连接在 DOM 上
        readonly baseURI: string;
        
        // 子
        readonly childNodes: NodeListOf<ChildNode>;
        readonly firstChild: ChildNode | null;
        readonly lastChild: ChildNode | null;
        
        // 兄弟
        readonly nextSibling: ChildNode | null; // 下一个兄弟节点
        readonly previousSibling: ChildNode | null; // 上一个兄弟节点
        
        // 父
        readonly parentElement: HTMLElement | null;
        readonly parentNode: Node & ParentNode | null;
    }
    
  • 方法

    interface Node extends EventTarget {
        // 增 
        appendChild<T extends Node>(newChild: T): T;
        insertBefore<T extends Node>(newChild: T, refChild: Node | null): T;
        
        // 查
        contains(other: Node | null): boolean;
        getRootNode(options?: GetRootNodeOptions): Node;
        hasChildNodes(): boolean;
        isEqualNode(otherNode: Node | null): boolean;
        isSameNode(otherNode: Node | null): boolean;
        
        // 更新
        replaceChild<T extends Node>(newChild: Node, oldChild: T): T;
        
        // 删
        removeChild<T extends Node>(oldChild: T): T;
        
        // 克隆
        cloneNode(deep?: boolean): Node;
        
        // ...
    }
    

ParentNode

Document 同时继承了 NodeParentNode

ParetnNode 代表一个父节点, 提供了一些节点作为父节点特有的属性和方法

  • 属性

    interface ParentNode {
        readonly childElementCount: number;
        readonly children: HTMLCollection;
        readonly firstElementChild: Element | null; // 不算 Text, Comment ... 的第一个元素类型的儿子
        readonly lastElementChild: Element | null;
    }
    
  • 方法

    interface ParentNode {
        /**
         * Inserts nodes after the last child of node, while replacing strings in nodes with equivalent Text nodes.
         */
        append(...nodes: (Node | string)[]): void;
        
        /**
         * Inserts nodes before the first child of node, while replacing strings in nodes with equivalent Text nodes.
         */
        prepend(...nodes: (Node | string)[]): void;
        
        // 返回第一个匹配选择器的元素
        querySelector<E extends Element = Element>(selectors: string): E | null;
    
    	// 返回所有匹配
        querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;
    }
    

ChildNode

Element 接口 (下一章讲到) 继承了 NodeChildNodeParentNode

ChildNode 代表一个子节点, 提供了一些子节点特有方法

interface ChildNode extends Node {
    // 添加节点到当前节点之后
    after(...nodes: (Node | string)[]): void;
    
    // 添加节点到当前节点之前
    before(...nodes: (Node | string)[]): void;
   
    // 移除当前节点
    remove(): void;
    
    // 用 nodes 替换当前节点
    replaceWith(...nodes: (Node | string)[]): void;
}

Element

Element 接口继承了 Node, ChildNode, ParentNode, 和 InnerHTML

先从最简单的 InnerHTML 开始, 它定义了一个 innerHTML: string 属性, 用于返回元素内部的 html

下面来看 Element 接口

  • 属性

    interface Element extends Node, ChildNode, ParentNode, InnerHTML  /*, ...... */ {
        readonly tagName: string; // 大写 tag name, 如 DIV
        readonly localName: string; // tag 的本地名, 如 div
        id: string;
        readonly attributes: NamedNodeMap;
        readonly classList: DOMTokenList;
        className: string;
        readonly ownerDocument: Document;
        outerHTML: string;
        
        // 对于没有定义 CSS 或 内联布局盒子的元素为0
        // 否则值为元素内部的高度和宽度 (单位为px), 包含内边距
        // 即 content + padding
        // 得到的值会被四舍五入, 得到精确值需要使用
        // getBoundingClientReact()
        readonly clientHeight: number;
        readonly clientWidth: number;
        
        // 表示一个元素的左边和顶部边框宽度(px)
        readonly clientLeft: number;
        readonly clientTop: number;
        
        // 一个元素高度和宽度的度量
        // 包括由于溢出导致的视图中不可见内容
        readonly scrollHeight: number;
        readonly scrollWidth: number;
        
        // 用于读取或设置元素滚动条到元素左边或上边的距离
        scrollLeft: number;
        scrollTop: number;
    
        readonly shadowRoot: ShadowRoot | null;
        readonly assignedSlot: HTMLSlotElement | null;
        slot: string;
    }
    
  • 方法

    interface Element extends Node, ChildNode, ParentNode, InnerHTML  /*, ...... */ {
        // 属性操作
        getAttribute(qualifiedName: string): string | null;
        getAttributeNames(): string[];
        getAttributeNode(name: string): Attr | null;
        hasAttribute(qualifiedName: string): boolean;
        hasAttributes(): boolean;
        setAttribute(qualifiedName: string, value: string): void;
        setAttributeNode(attr: Attr): Attr | null;
        removeAttribute(qualifiedName: string): void;
        removeAttributeNode(attr: Attr): Attr;
        
        // DOMRect
        getBoundingClientRect(): DOMRect;
        getClientRects(): DOMRectList;
        
        // 查询元素
        getElementsByClassName(classNames: string): HTMLCollectionOf<Element>;
        
        getElementsByTagName<K extends keyof HTMLElementTagNameMap>(qualifiedName: K): HTMLCollectionOf<HTMLElementTagNameMap[K]>;
    
        addEventListener<K extends keyof ElementEventMap>(type: K, listener: (this: Element, ev: ElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
    
        removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
    }
    

HTMLElement

interface HTMLElement extends Element, DocumentAndElementEventHandlers, ElementCSSInlineStyle, ElementCSSInlineStyle, ElementContentEditable, GlobalEventHandlers, HTMLOrSVGElement {
    draggable: boolean;
    hidden: boolean;
    innerText: string;
    lang: string;
    
    readonly offsetHeight: number;
    readonly offsetLeft: number;
    readonly offsetParent: Element | null;
    readonly offsetTop: number;
    readonly offsetWidth: number;
    
    spellcheck: boolean;
    title: string;
    translate: boolean;
    
    click(): void;
    
    addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
    
    removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}

Event

interface Event {
    initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void;
    readonly type: string;
    readonly bubbles: boolean;
    cancelBubble: boolean;
    readonly cancelable: boolean;
    
    readonly eventPhase: number; // 返回用数字代表的事件阶段
    
    readonly target: EventTarget | null; // 返回发布事件的对象
    readonly currentTarget: EventTarget | null; // 返回处理事件的当前对象
    
    
    readonly defaultPrevented: boolean;
    preventDefault(): void;
    stopImmediatePropagation(): void; // 阻止事件冒泡并阻止该元素上同类事件监听器被触发
    stopPropagation(): void; // 阻止事件冒泡
    
    readonly AT_TARGET: number;
    readonly BUBBLING_PHASE: number;
    readonly CAPTURING_PHASE: number;
    readonly NONE: number;
}

EventTarget

interface EventTarget {
    addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void;
    
    dispatchEvent(event: Event): boolean;
    
    removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void;
}

事件类型

不同类型的事件接口都继承了 Event, 例如

  • DragEvent

    interface DragEvent extends MouseEvent {
        readonly dataTransfer: DataTransfer | null;
    }
    
    interface DataTransfer {
        dropEffect: 'none' | 'copy' | 'link' | 'move';
        effectAllowed: 'none' | 'copy' | 'copyLink' | 'copyMove' | 'link' | 'linkMove' | 'move' | 'all';
        
        readonly files: FileList;
        readonly items: DataTransferItemList;
        readonly types: ReadonlyArray<string>;
        
        clearData(format?: string): void;
        getData(format: string): string;
        setData(format: string, data: string): void;
        
        setDragImage(image: Element, x: number, y: number): void;
    }
    
  • UIEvent

    interface UIEvent extends Event {
        readonly detail: number;
        readonly view: Window | null;
    }
    
  • FocusEvent

    interface FocusEvent extends UIEvent {
        readonly relatedTarget: EventTarget | null;
    }
    
  • InputEvent

    interface InputEvent extends UIEvent {
        readonly data: string | null;
        readonly inputType: string;
        readonly isComposing: boolean;
    }
    
  • KeyboardEvent

    interface KeyboardEvent extends UIEvent {
        readonly altKey: boolean;
        readonly code: string;
        readonly ctrlKey: boolean;
        readonly isComposing: boolean;
        readonly key: string;
        readonly location: number;
        readonly metaKey: boolean;
        readonly repeat: boolean;
        readonly shiftKey: boolean;
        
        getModifierState(keyArg: string): boolean;
        
        readonly DOM_KEY_LOCATION_LEFT: number;
        readonly DOM_KEY_LOCATION_NUMPAD: number;
        readonly DOM_KEY_LOCATION_RIGHT: number;
        readonly DOM_KEY_LOCATION_STANDARD: number;
    }
    
  • MouseEvent

    interface MouseEvent extends UIEvent {
        readonly altKey: boolean;
        readonly ctrlKey: boolean;
        readonly shiftKey: boolean;
        readonly metaKey: boolean;
        
        // 0 主键(左键), 1 辅键 (中键), 2 次键 (右键)
        // 4 : 第四个按钮, 通常指浏览器后退按键
        // 5 : 第五个按键, 通常指浏览器前进按键
        // 对于配置为左手用的鼠标对应刚好相反
        readonly button: number;
        readonly buttons: number; // 按下按键数字之和
        
        readonly relatedTarget: EventTarget | null;
        
        // 鼠标事件发生时应用客户端区域的 x 和 y
        // 例如, 不管页面是否有水平垂直滚动
        // 点击左上角 clientX 和 clientY 都为 0
        readonly clientX: number;
        readonly clientY: number;
        
        // 提供了当前事件和上一个 "mousemove" 事件之间鼠标在 x 和 y 方向的移动值
        readonly movementX: number;
        readonly movementY: number;
        
        // 这两个属性还在实验阶段
        readonly offsetX: number;
        readonly offsetY: number;
        
        // 返回相对于整个文档的 x 和 y 值
        // 例如, 页面向右滚动 200px, 点击距离窗口左边 100px
        // pageX 为 300px
        readonly pageX: number;
        readonly pageY: number;
        
        // 返回鼠标基于全局 (整个屏幕) 的 x 和 y 坐标
        readonly screenX: number;
        readonly screenY: number;
        
        // 实验中的属性, clientX 和 clientY 的别名
        readonly x: number;
        readonly y: number;
        
        // ...
    }
    

BOM

浏览器对象模型, 描述了与浏览器进行交互的方法和接口, 由

  • window
  • screen
  • history
  • navigator
  • location

这几个关键对象组成

window

全局 window 对象为一个 Window 实例

  • 属性

    /** A window containing a DOM document; the document property points to the DOM document loaded in that window. */
    interface Window extends EventTarget, AnimationFrameProvider, GlobalEventHandlers, WindowEventHandlers, WindowLocalStorage, WindowOrWorkerGlobalScope, WindowSessionStorage {
        name: string; // 获得或设置窗口名
        readonly closed: boolean; // 窗口是否关闭
        readonly document: Document;
        readonly parent: Window;
        readonly top: Window; // 窗口层级最顶层窗口的引用
        readonly window: Window & typeof globalThis;
        readonly self: Window & typeof globalThis;
        
        readonly frameElement: Element; // 嵌入窗口的元素
        readonly frames: Window; // 返回窗口中所有子窗口
        readonly length: number; // 窗口中 frames 数
        
        readonly screen: Screen;
        readonly history: History;
        readonly navigator: Navigator;
        location: Location;
        
        // 获得窗口内容区的高度和宽度, 包含垂直或水平滚动条 (如果有)
        readonly innerHeight: number;
        readonly innerWidth: number;
        
        // 获得整个浏览器窗口的高度和宽度
        readonly outerHeight: number;
        readonly outerWidth: number;
        
        // 页面 x y 方向偏移
        // 例如往下滚动 100px, 那么 pageYOffset 就为 100px
        readonly pageXOffset: number;
        readonly pageYOffset: number;
        
        // 页面垂直或水平方向的滚动值, pageXOffset 和 pageYOffset 的别名
        readonly scrollX: number;
        readonly scrollY: number;
        
        readonly screenLeft: number;
        readonly screenTop: number;
        
        // 浏览器左边界到操作系统桌面左边界的距离
        // 浏览器上边界到操作系统桌面上边界的距离
        readonly screenX: number;
        readonly screenY: number;
        
        // UI
        readonly locationbar: BarProp;
        readonly menubar: BarProp;
        readonly statusbar: BarProp;
        readonly toolbar: BarProp;
        readonly personalbar: BarProp;
        readonly scrollbars: BarProp;
    }
    
  • 方法

    interface Window extends EventTarget, AnimationFrameProvider, GlobalEventHandlers, WindowEventHandlers, WindowLocalStorage, WindowOrWorkerGlobalScope, WindowSessionStorage {
        // 弹窗
        alert(message?: any): void;
        confirm(message?: string): boolean;
        prompt(message?: string, _default?: string): string | null;
        
        console: Console
        
        // 焦点控制
        blur(): void;
        focus(): void;
        
        // 窗口打开/关闭/停止
        open(url?: string, target?: string, features?: string, replace?: boolean): Window | null;
        close(): void; // 关闭窗口, 等同于 ctrl + w
        stop(): void; // 相当于点击浏览器停止按钮, 方法不能阻止已经包含在加载中的文档, 但能够阻止图片, 新窗口, 和一些会延迟加载的对象的加载
        
        print(): void; // 等同于 ctrl + p
        
        // 滚动当前页面到指定位置
        scroll(x: number, y: number): void;
        // 根据当前页面位置滚动
        scrollBy(x: number, y: number): void;
        // 等价于 scroll
        scrollTo(x: number, y: number): void;
        
        addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
    
        removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
        
        [index: number]: Window;
    }
    

其中, 接口 WindowLocalStorageWindowSessionStorage 分别声明了 localStoragesessionStorage

WindowOrWorkerGlobalScope

Window 接口继承了这个接口

interface WindowOrWorkerGlobalScope {
    readonly caches: CacheStorage;
    readonly crypto: Crypto;
    readonly indexedDB: IDBFactory;
    readonly isSecureContext: boolean;
    readonly origin: string;
    readonly performance: Performance;
    
    atob(data: string): string;
    btoa(data: string): string;
    
    clearInterval(handle?: number): void;
    clearTimeout(handle?: number): void;
    
    setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
    setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
    
    fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
    
    queueMicrotask(callback: VoidFunction): void;
}

screen

interface Screen {
    
    readonly availHeight: number;
    readonly availWidth: number;
    
    readonly height: number;
    readonly width: number;
    
    readonly colorDepth: number;
    readonly pixelDepth: number;
    
    readonly orientation: ScreenOrientation;
    
}

location

/** The location (URL) of the object it is linked to. Changes done on it are reflected on the object it relates to. Both the Document and Window interface have such a linked Location, accessible via Document.location and Window.location respectively. */
interface Location {
    href: string; // 获取 url 或设置以导航到给定 url
    
    // 返回 url 的 origin, origin = protocol + hostname + port
    readonly origin: string; 
    
    protocol: string; // 获取或设置协议
    
    host: string; // 获取 host 或设置以导航到相同 URL 但不同的主机和端口
    hostname: string; // 获取主机名或设置以导航到同 URL 但不同主机名
    port: string; // 获取或设置端口
    
    // 获取或设置当前 url 下的路径
    // 不包括 hash 和 query
    pathname: string; 
    
    // 获取 hash 或 设置以导航到 URL 中的 hash 处
    // 注意修改 hash 不会发送请求到服务器
    hash: string; 
    
    // 获取或设置 query, 例如
    // 在 htts://cn.bing.com/search?q=react
    // 设置 location.search = '?q=vue'
    search: string
    
    // 导航到给定 url
    assign(url: string): void;
    reload(): void; // 等价于 f5 快捷键
    // 从会话历史中删除该页面并导航到指定 url
    replace(url: string): void;
}

history

interface History {
    readonly length: number;
    scrollRestoration: 'auto' | 'manual';
    readonly state: any; // 存储传递的 data
    
    back(): void;
    forward(): void;
    go(delta?: number): void;
    
    pushState(data: any, title: string, url?: string | null): void;
    replaceState(data: any, title: string, url?: string | null): void;
}

HTML5 引入了 pushStatereplaceState, 通常配合 window.onpopstate 使用

pushState

pushState 可以改变 referrer, 他在用户发送 XMLHttpRequest 请求时在 HTTP 头部使用, 改变 state 后创建的 XMLHttpRequest 对象的 referrer 都会被改变. 因为 referrer 是标识创建 XMLHttpRequest 对象时 this 所代表的 window 对象中 documentURL

在某种意义上, 调用 pushState 与设置 window.location = '#foo' 类似, 两者都会在当前页面创建并激活新的历史记录, 但 pushState 有以下优点 :

  • 新 URL 可以是与当前 URL 同源的任意URL. 相反, 修改 hash 时, 设置 window.location 才能是同一个 document
  • 不想更改 URL 时可以不用更改 (省略第三个参数). 而设置 window.location = '#foo' 在当前hash 不为 #foo 才能创建新的历史记录项
    • 可以将任意数据通过 data 参数与历史记录项关联. 而基于 hash 的方式需要将所有数据编码为短字符串
  • 以后还可以使用 title (虽然这个值现在被浏览器忽略)

注意 : pushState 即使新 URL 和 旧 URL 仅 hash 不同也不会触发 hashchange 事件

例如在 : www.xxx.com/foo.html

history.pushState({ a: 1, b: 2 }, '', 'bar.html')

地址栏将变成 www.xxx.com/bar.html

并且 history.state{ a: 1, b: 2 }

replaceState

pushState 类似, 但仅修改当前历史记录项而不新建

navigator

// 表示用户代理的状态和表示, 它允许脚本查询它和注册自己进行一些活动
interface Navigator extends NavigatorStorage /*, ... */ {
    readonly maxTouchPoints: number;
    readonly mediaDevices: MediaDevices;
    readonly permissions: Permissions;
    readonly serviceWorker: ServiceWorkerContainer;
    // ...
}