JS高级学习

一、集合引用类型

1.1 选择 Object 还是 Map

  1. 内存占用
    Map大约可以比多存储50%得键值对

  2. 插入性能
    Map 插入性能更加

  3. 查找速度
    Object 查找速度更快。在把 Object 当成数组使用得情况下(比如使用连续整数作为属性),浏览器引擎可以进行优化,在内存中使用更高效得布局。

  4. 删除性能
    delete 删除得是 Object 的引用,不会真正释放内存;任何情况下都会返回 true;在性能上一直饱受诟病。对于大多数浏览器引擎来说,Map 的 delete() 操作都比插入和查找更快。

1.2 使用弱映射

1.2.1 私有变量

const User = (() => {
  const wm = new WeakMap()

  class User {
    constructor(id) {
      this.idProperty = Symbol('id')
      this.setId(id)
    }

    setPrivate(property, value) {
      const privateMembers = wm.get(this) || {}
      privateMembers[property] = value
      wm.set(this, privateMembers)
    }

    getPrivate(property) {
      return wm.get(this)[property]
    }

    setId(id) {
      this.setPrivate(this.idProperty, id)
    }

    getId(id) {
      return this.getPrivate(this.idProperty)
    }
  }
  return User
})()

const user = new User(123)
alert(user.getId()) // 123
user.setId(456)
alert(user.getId()) // 456
alert(wm.get(user)[user.idProperty]) // ReferenceError: wm is not defined

这样,拿不到弱映射中的健,也就无法取得弱映射中对应的值。

1.2.2 DOM 节点元数据

因为 WeakMap 实例不会妨碍垃圾回收,所以非常适合保存关联元数据。

const map = new Map()
const loginButton = docement.querySelector('#login')

map.set(loginButton, { disabled: true })

上面的代码,假设原来的登录按钮从 DOM 树中被删掉了,但由于映射中还保存着按钮的引用,所以对应的 DOM 节点任然会逗留在内存中。

如果使用的是弱映射,如下代码,那么从 DOM 树中被删除后,垃圾回收程序就可以立即释放其内存(假设没有其他地方引用这个对象):

const wm = new WeakMap()
const loginButton = docement.querySelector('#login')

wm.set(loginButton, { disabled: true })

1.3 使用 Set 实现并集(Union)、交集(Intersect)和差集(Difference)

let a = new Set([1, 2, 3])
let b = new Set([4, 3, 2])

// 并集
let union = new Set([...a, ...b])
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)))
// set {2, 3}

// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x)))
// Set {1}

二、继承

2.1 组合继承

也叫伪经典继承,综合了原型链和盗用构造函数,基本思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。

原型链的问题

  1. 包含的引用值会在所有实例间共享
  2. 子类型在实例化时不能传参
function SuperType() {
  this.colors = ['red', 'blue', 'green']
}

function SubType() {}

// 继承 SuperType
SubType.prototype = new SuperType()

const instance1 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // ['red', 'blue', 'green', 'black']

const instance2 = new SubType()
console.log(instance2.colors) // ['red', 'blue', 'green', 'black']

盗用构造函数

为了解决包含引用值的问题,基本思路就是在子类构造函数中调用父类构造函数

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}

function SubType() {
  // 继承 SuperType, 传递参数
  SuperType.call(this, 'Bob')
}

const instance1 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // ['red', 'blue', 'green', 'black']
console.log(instance1.name) // 'Bob'

const instance2 = new SubType()
console.log(instance2.colors) // ['red', 'blue', 'green']

缺点:必须在构造函数中定义方法,因此函数不能重用。此外,子类不能访问父类在原型上定义的方法。

综合例子

function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}

SuperType.prototype.sayName = function() {
  console.log(this.name)
}

function SubType(name, age) {
  // 继承属性
  SuperType.call(this, name)
  // 自身实例属性
  this.age = age
}

// 继承方法
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType

2.2 原型式继承

即使不定义类型也可以通过原型实现对象之间的信息共享,适用于这种情况:有一个对象,想在它的基础上再创建一个新对象。

// 定义创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型
function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}

const person = {
  name: 'Bob',
  friends: ['Jay', 'Lida']
}

const anotherPerson = object(person)
anotherPerson.name = 'Greg'
anotherPerson.friends.push('Rob')

const another = object(person)
anotherPerson.name = 'Alun'
anotherPerson.friends.push('Court')

console.log(person.name) // Bob
console.log(person.friends) // ['Jay', 'Lida', 'Rob', 'Court']

2.3 寄生式继承

创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象

// 定义创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型
function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}

function creatAnother(original) {
  const clone = object(original) // 通过调用函数创建一个新对象
  clone.sayHi = function() { // 以某种方式增强这个对象
    console.log('Hi')
  }
  return clone // 返回这个对象
}

注意:通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数类似。

2.4 寄生式组合继承

组合继承其实也存在效率问题。主要问题就是父类构造函数始终会被调用两次。寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。

组合继承

function SuperType(name) {
  this.name = name
}

SuperType.prototype.sayName = function() {
  console.log(this.name)
}

function SubType(name) {
  // 继承属性
  SuperType.call(this, name) // 第二次调用 SuperType
}

// 继承方法
SubType.prototype = new SuperType() // 第一次调用 SuperType
SubType.prototype.constructor = SubType

寄生式组合继承

function inheritPrototype(subType, superType) {
  const prototype = Object.create(superType.prototype) // 创建对象
  prototype.constructor = subType // 增强对象
  subType.prototype = prototype // 赋值对象
}

function SuperType(name) {
  this.name = name
}

SuperType.prototype.sayName = function() {
  console.log(this.name)
}

function SubType(name) {
  SuperType.call(this, name)
}

inheritPrototype(SuperType, SuperType)

2.5 类继承

ES6 继承方式,使用 extends 关键字,就可以继承任何拥有[[ Constructor ]] 和原型的对象。既可以继承一个类,也可以继承普通的构造函数。

注意: super 关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。在类构造函数中使用 super 可以调用父类构造函数。

class Vehicle {
  constructor() {
    this.hasEngine = true
  }

  identify(name) {
    console.log(name)
  }
}

class Bus extends Vehicle {
  constructor() {
    // 不要在调用 super() 之前引用 this,否则会抛出 ReferenceError
    super() // 相当于 super.constructor()
    console.log(this instanceof Vehicle) // true
    console.log(this) // Bus { hasEngine: true }
  }
  
  identify() {
    super.identify('bus')
  }
}

const bus = new Bus()
bus.identify() // 'bus'

三、BOM

3.1 window 对象

在浏览器中有两重身份,一个是 ECMAScript 中的 Global 对象,另一个就是浏览器窗口的 JavaScript 接口

3.1.1 窗口关系

  1. window.top: 始终指向最上层(最外层)窗口,即浏览器窗口本身。
  2. window.parent: 始终指向当前窗口的父窗口。
  3. window.self: 始终指向 window(self 和 window 就是同一个对象)。

如果当前窗口是最上层窗口,则 parent 等于 top(都等于 window)。

3.1.2 窗口位置

相对位置:

  1. window.screenLeft: 窗口相对于屏幕左侧的位置,返回值是 CSS 像素。
  2. window.screenTop: 窗口相对于屏幕顶部的位置,返回值是 CSS 像素。

移动窗口:

可以使用 moveTo() 和 moveBy() 方法移动窗口。这两个方法都接收两个参数,其中 moveTo() 接收要移动的新位置的绝对坐标 x 和 y;而 moveBy() 则接收相对当前位置在两个方向上移动的像素数。

// 把窗口移动到左上角
window.moveTo(0, 0)
// 把窗口向下移动 100 像素
window.moveBy(0, 100)

注意:依浏览器而定,以上方法可能会被部分或全部禁用。

3.1.3 窗口大小

所有现代浏览器都支持 4 个属性:

  1. window.innerWidth: 返回浏览器可视区宽度(不包括浏览器边框和工具栏)。
  2. window.innerHeight: 返回浏览器可视区高度(不包括浏览器边框和工具栏)。
  3. window.outerWidth: 返回浏览器窗口自身宽度。
  4. window.outerHeight: 返回浏览器窗口自身高度。

页面视口大小还可以通过 document.documentElement.clientWidth、document.documentElement.clientHeight、document.body.clientWidth、document.body.clientHeight 等获取

在部分移动浏览器中,document.documentElement.clientWidth、document.documentElement.clientHeight 返回布局视口的大小,即渲染页面的实际大小。布局视口是相对于可见视口的概念,可见视口只能显示整个页面的一小部分。

3.1.4 视口位置

  1. window.pageXOffset/window.scrollX: 滚动条到相对视口的左侧距离。
  2. window.pageYOffset/window.scrollY: 滚动条到相对视口的顶部距离。

可以使用 scroll()、scrollTo()、scrollBy() 方法滚动页面。都接收表示相对视口距离的 x 和 y 坐标,这两个参数在前两个方法中表示要滚动到的坐标,在最后一个方法中表示滚动的距离。

// 相对于当前视口向下滚动 100 像素
window.scrollBy(0, 100)

// 相对于当前视口向右滚动 100 像素
window.scrollBy(100, 0)

// 滚动到页面左上角
window.scrollTo(0, 0)

// 滚动到距离屏幕左边及顶边各 100 像素的位置
window.scrollTo(100, 100)

这几个方法也都接收一个 ScrollToOptions 字典,除了提供偏移值,还可以通过 behavior 属性设置是否平滑滚动

// 正常滚动
window.scrollTo({
  left: 100,
  top: 100,
  behavior: 'auto'
})

// 平滑滚动
window.scrollTo({
  left: 100,
  top: 100,
  behavior: 'smooth'
})

3.2 history 对象

表示当前窗口首次使用以来用户的导航历史记录。

3.2.1 导航

  1. go()

    // 后退一页
    history.go(-1)
    
    // 前进一页
    history.go(1)
    
  2. back(): 后退一页

  3. forward(): 前进一页

判断是否为窗口中的第一个页面: history.length === 1

3.2.2 历史状态管理

默认情况下,处理 location.hash 之外,只要修改 location 的一个属性,就会导致页面重新加载新 URL。而状态管理 API 则可以让开发者改变浏览器 URL 而不会加载新页面。

history.pushState()

这个方法接收3个参数:一个 state 对象、一个新状态的标题和一个(可选的)相对 URL
history.pushState({ foo: 'bar', 'my title', 'bar.html'})

方法执行后,状态信息就会被推到历史记录中(会触发 window 对象上的 popstate 事件)。为防止滥用,这个状态的对象大小是有限制的,通常在 500KB ~ 1MB 以内

// popstate 事件的事件对象有一个 state 属性,其中包含通过 pushState() 第一个参数传入的 state 对象
window.addEventListener('popstate', e => {
  const state = e.state
  if (state) // 第一个页面加载时状态是 null
})

history.replaceState()

传入与 pushState 同样的前两个参数来覆盖当前状态。

四、DOM

4.1 Text 类型

纯文本(包含空格)。

4.1.1 基础介绍

  • nodeType 等于 3
  • nodeName 值为 “#text”
  • nodeValue 值为节点中包含的文本
  • parentNode 值为 Element 对象
  • 不支持子节点

Text 节点中包含的文本可以通过 nodeValue 属性访问,也可以通过 data 属性访问,这两个属性包含相同的值。修改 nodeValue 或 data 的值,也会在另一个属性反映出来。

操作文本的方法:

  • 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 的文本
<!-- 没有内容,因此没有文本节点 -->
<div></div>
<!-- 有空格,因此有一个文本节点 -->
<div> </div>
<!-- 有内容,因此有一个文本节点 -->
<div>hello world!</div>

4.1.2 规范化文本节点

合并相邻的文本节点。

const element = document.createElement('div')
const textNode = document.createTextNode('Hello')
element.appendChild(textNode)

const anotherTextNode = document.createTextNode(' World!')
element.appendChild(anotherTextNode )

alert(element.childNodes.length) // 2
element.normalize()
alert(element.childNodes.length) // 1

4.2 焦点管理

document.activeElement,始终包含当前拥有焦点的DOM元素。页面加载时,可以通过用户输入(按 Tab 键或代码中使用 focus()方法)让某个元素自动获得焦点。

const button = document.getElementById('myButton')
button.focus()

console.log(document.activeElement === button) // true

默认情况下,document.activeElement在页面刚加载完之后会设置为document.body。而在页面完全加载之前,document.activeElement的值为 null。

document.hasFocus(),该方法返回布尔值,表示文档是否拥有焦点。

4.3 HTMLDocument 扩展

4.3.1 readyState 属性

document.readyState 有两个可能的值:

  1. loading,表示文档正在加载
  2. complete,表示文档加载完成
if (document.readyState === 'complete') {
  // 文档加载完成执行操作
}

4.3.2 compatMode 属性

表示浏览器处于什么渲染模式

  1. CSS1Compat,表示标准模式
  2. BackCompat,表示混杂模式

4.3.3 head 属性

document.head,取得<head>元素

4.4 插入标记

4.4.1 innerHTML 属性

将 HTML 字符串解析为相应的 DOM 树。

4.4.2 outerHTML 属性

返回调用它的元素(及所有后代元素)的 HTML 字符串。

如果使用 outerHTML 设置 HTML,比如:

div.outerHTML = '<p>This is a paragraph.</p>'

// 相当于
const p = document.createElement('p')
p.appendChild(document.createTextNode('This is a paragraph.'))
div.parentNode.replaceChild(p, div)

4.4.3 insertAdjacentHTML() 与 insertAdjacentText()

接收两个参数:要插入标记的位置和要插入的 HTML 或文本。第一个参数值必须是下面其中一个:

  • beforebegin,插入当前元素前面,作为一个前同胞节点
  • afterbegin,插入当前元素内部,作为新的子节点或放在第一个字节点前面
  • beforeend,插入当前元素内部、作为新的子节点或放在最后一个子节点后面
  • afterend,插入当前元素后面,作为下一个同胞节点

4.5 srollIntoView()

滚动浏览器窗口或容器元素以便包含元素进入视口。参数如下:

  • alignToTop 是一个布尔值

    • true,窗口滚动后元素的顶部与视口顶部对齐
    • false,窗口滚动后元素的底部与视口底部对齐
  • srollIntoViewOptions 是一个选项对象

    • behavior,定义过渡动画,可取值为smoothauto,默认为auto
    • block,定义垂直方向的对齐,可取值为startendcenternearest,默认为start
    • inline,定义水平方向的对齐,可取值为startendcenternearest,默认为nearest
  • 不传参数等同于 alignToTop 为 true

// 确保元素可见
document.forms[0].srollIntoView()

// 将元素平滑地滚入视口
document.forms[0].srollIntoView({
  behavior: 'smooth',
  block: 'start'
})

4.6 元素尺寸

4.6.1 偏移尺寸

  • offsetHeight,元素的高度(包括水平滚动条高度(如果可见)),height + padding(上下值) + border(上下值)
  • offsetWidth,元素的宽度(包括垂直滚动条宽度(如果可见)),width+ padding(左右值) + border(左右值)
  • offsetLeft,元素左边框外侧距离最近的带有position属性(relative,absolute,fixed)的父元素的左边框内侧像素数
  • offsetTop,元素上边框外侧距离最近的带有position属性(relative,absolute,fixed)的父元素的上边框内侧像素数
  • offsetParent,返回最近的带有position属性(relative,absolute,fixed)的父元素

4.6.2 客户端尺寸

  • clientHeight,元素可视内容区加上、下内边距高度,height + padding-top + padding-bottom
  • clientWidth,元素可视内容区加左、右内边距宽度,width + padding-left + padding-right
  • clientTop,元素上边框的像素数,border-top
  • clientLeft,元素左边框的像素数,border-left

4.6.3 滚动尺寸

  • scrollHeight,没有滚动条时,元素内容区总高度,height + padding-top + padding-bottom
  • scrollWidth,没有滚动条时,元素内容区总宽度,width + padding-left + padding-right
  • scrollTop,内容区顶部隐藏的像素数,可设置以改变元素的滚动位置
  • scrollLeft,内容区左侧隐藏的像素数,可设置以改变元素的滚动位置

4.6.4 确定元素尺寸

getBoundingClientRect(),包含6个属性:left、top、right、bottom、height、width。这些属性给出了元素在页面中相对于视口的位置。

  • left,元素左边框外侧距离视口左侧的像素数
  • top,元素上边框外侧距离视口顶部的像素数
  • right,left + width
  • bottom,top + height
  • width,元素自身总宽度,width+ padding(左右值) + border(左右值)
  • height,元素自身总高度,height + padding(上下值) + border(上下值)

五、事件

JavaScript 与 HTML 的交互是通过事件实现的,事件代表文档或浏览器窗口中某个有意义的时刻。

5.1 事件处理程序

事件意味着用户或浏览器执行的某种动作。比如,单机(click)、加载(load)、鼠标悬停(mouseover)。为响应事件而调用的函数被称为事件处理程序(或事件监听器)。

5.1.1 DOM0 事件处理程序

每个元素(包括 window 和 document)都有通常小写的事件处理程序属性,比如 onclick。把这个属性赋值为一个函数即可:

const btn = document.getElementById('btn')
btn.onclick = function() {}

5.1.2 DOM2 事件处理程序

为事件处理程序的赋值和移除定义了两个方法:addEventListener()removeEventListener()。这两个方法暴露在所有 DOM 节点上,它们接收3个参数:事件名、事件处理函数和一个布尔值,true 表示在捕获阶段调用事件处理程序,false(默认值)表示在冒泡阶段调用事件处理程序。

const btn = document.getElementById('btn')
const handler = function() {}
btn.addEventListener('click', handler, false)
btn.removeEventListener('click', handler, false)

用 addEventListener 添加的事件可以以添加顺序依次触发,removeEventListener 必须传入与添加时同样的参数来移除。

5.1.3 IE 事件处理程序

IE 实现了 DOM 类似的方法,即attachEvent()detachEvent()。这两个方法接收两个同样的参数:事件处理程序的名字和事件处理函数。因为 IE8 及更早版本只支持事件冒泡,所以使用attachEvent()添加的事件处理程序会添加到冒泡阶段。

const btn = document.getElementById('btn')
const handler = function() {}
btn.attachEvent('onclick', handler)
btn.detachEvent('onclick', handler)

注意:attachEvent() 的第一个参数是“onclick”,而且给同一个元素添加多个事件处理程序时会反向触发。

5.2 事件类型

5.2.1 用户界面事件

  • load:在 window 上当页面加载完成后触发(包括所有图像、javaScript文件、css文件等外部资源),在<img>元素上当图片加载完成后触发,在<object>元素上当相应对象加载完成后触发
  • unload:在 window 上当页面完全卸载后触发,在<object>元素上当相应对象卸载完成后触发
  • beforeunload:在 window 上触发,给用户阻止页面被卸载的机会
  • DOMContentLoaded:在 window 上触发,DOM 树构建完成后立即触发,不用等待外部资源下载
  • abort:在<object>元素上当相应对象加载完成前被用户提前终止下载时触发
  • error:在 window 上当 JavaScript 报错时触发,在<img>元素上当无法加载指定图片时触发,在<object>元素上当无法加载相应对象时触发
  • select:在文本框(<input>或<textarea>)上当用户选择了一个或多个字符时触发
  • resize:在 window 上当窗口被缩放时触发
  • scroll:当用户滚动包含滚动条的元素时在元素上触发
// 当在页面输入后,刷新或关闭标签页时就会弹出一个是否确认关闭的提示框
window.addEventListener('beforeunload', function(event) {
  event.returnValue = 'You have unsaved changes.'
})

5.2.2 焦点事件

  • blur:当元素失去焦点时触发。这个事件不冒泡,所有浏览器都支持
  • focus:当元素获得焦点时触发。这个事件不冒泡,所有浏览器都支持
  • focusin:当元素获得焦点时触发。这个事件是 focus 的冒泡版
  • focusout:当元素失去焦点时触发。这个事件是 blur 的通用版

5.2.3 鼠标和滚轮事件

  • click:单击鼠标左键触发
  • contextmenu: 单击鼠标右键触发
  • dblclick:双击鼠标左键触发(如果注册了 )
  • mousedown:用户按下鼠标任意键时触发。这个事件不能通过键盘触发
  • mouseup:用户释放鼠标键时触发。这个事件不能通过键盘触发
  • mouseenter:鼠标从元素外部移到元素内部时触发。这个事件不冒泡
  • mouseleave:鼠标从元素内部移到元素外部时触发。这个事件不冒泡
  • mousemove:鼠标在元素上移动时触发。这个事件不能通过键盘触发
  • mouseover:鼠标从元素外部移到元素内部时触发
  • mouseout:鼠标从元素内部移到元素外部时触发

5.2.4 键盘事件

  • keydown:按下键盘上某个键时触发,持续按住会重复触发(文本框出现变化之前)
  • keypress:按下键盘上某个键并产生字符时触发(esc 或 backspace 键不会触发 ),持续按住会重复触发。DOM3已废弃
  • keyup:释放键盘上某个键触发(文本框出现变化之后)
  • textInput:字符被输入到可编辑区域时触发(作为对 keypress 的替代)

textInput 与 keypress的区别:keypress 会在任何可以获得焦点的元素上触发,而 textInput 只在可编辑区域上触发

5.2.5 HTML5 事件

  • pageshow:在页面显示时触发(无论是否来自往返缓存),新加载的页面上,pageshow 会在 load 事件之后触发
    • pageshow 的 event 对象中还包含一个 persisted 属性。是个布尔值,如果页面是在缓存中就是 true,否则是 false
  • pagehide:页面从浏览器卸载后,unload 事件之前触发。同样包含一个 persisted 属性,为 true 表示页面在卸载之后会被保存在缓存中

第一次触发 pageshow 事件时 persisted 始终是 false,而第一次触发 pagehide 事件时 persisted 始终是 true(除非页面不符合缓存条件)

  • hashchange:URL # 后面的部分发生变化时触发

5.2.6 设备事件

  • orientationchange:判断用户的设备是处于垂直模式还是水平模式,所有 iOS 设备都支持
    • 移动 Safari 在 window 上暴露了 window.orientation 属性,三种值:0 表示垂直模式,90 表示左转水平模式(主屏幕键在右侧),-90表示右转水平模式(主屏幕键在左侧)

5.2.7 触摸事件

  • touchstart: 手指放到屏幕上是触发(即使屏幕上已经有手指)
  • touchmove:手指在屏幕上滑动时连续触发。调用 preventDafault() 可以阻止滚动
  • toucend:手指从屏幕上移开时触发

5.3 模拟事件

使用document.createEvent()方法创建一个 event 对象。接受一个参数,此参数是一个表示要创建事件类型的字符串。可用的字符串值是以下值之一:

  • UIEvents(DOM3 中是 UIEvent):通用用户界面事件(鼠标和键盘事件都继承自这个事件)
  • MouseEvents(DOM3 中是 MouseEvent):通用鼠标事件
  • HTMLEvents(DOM3 中没有):通用HTML事件

触发事件用dispatchEvent(),接受一个参数,即表示要触发事件的 event 对象

自定义DOM事件:

需要调用document.createEvent('CustomEvent')。返回的对象包含initCustomEvent()方法,该方法接收4个参数:

  • type:string,要触发的事件类型,如‘myevent’
  • bubbles:boolean,表示事件是否冒泡
  • cancelable:boolean,表示事件是否可以取消
  • detail:object,任意值。作为 event 对象的 detail 属性
window.addEventListener('myevent', (event) => {
  console.log(event.detail)
})

const customEvent = document.createEvent('CustomEvent')
customEvent.initCustomEvent('myevent', true, false, 'Hello World!')
customEvent.dispatchEvent('myevent')

六、JavaScript API

6.1 File API

6.1.1 File 类型

以表单中的文件输入字段为基础,增加了直接访问文件信息的能力。HTML5 在 DOM 上为文件输入文素添加了 files 集合,这个集合包含一组 FIle 对象,每个 file 对象都有一些只读属性:

  • name: 本地系统中的文件名
  • size: 以字节计的文件大小
  • type: 包含文件 MIME 类型的字符串
  • lastModifiedDate: 表示文件最后修改时间的字符串。这个属性只有 Chome 实现了。
// 监听 change 事件,遍历 files 集合可以取得每个选中文件的信息
const fileList = document.getElementById('file-list')
fileList.addEventListener('change', e => {
  const files = e.target.files
  const len = files.length
  let i = 0

  while (i < len) {
    const f = fileList[i]
    console.log(`${f.name} ${f.size} ${f.type} bytes`)
    i++
  }
})

6.1.2 FileReader 类型

异步文件读取机制。FileReader 类型提供了几个读取文件数据的方法:

  • readAsText(file, encoding): 从文件中读取纯文本内容并保存在 result 属性中。第二个参数表示编码,是可选的
  • readAsDataURL(file): 读取文件并将内容的数据 URI 保存在 result 属性中
  • readAsBinaryString(file): 读取文件并将每个字符的二进制数据保存在 result 属性中
  • readAsArrayBuffer(file): 读取文件并将内容以 ArrayBuffer 保存在 result 属性中

因为这些读取方法是异步的,所以每个 FileReader 会发布几个事件,其中 3 个最有用的事件是 progress(还有更多数据)、error(发生错误) 和 load(读取完成)

progress 事件每 50 毫秒就会触发一次;
error 事件会由于某种原因无法读取文件时触发。FileReader 的 error 属性会包含错误信息。这个属性是一个对象,只包含一个属性:code。值可能为1(未找到)、2(安全错误)、3(读取被中断)、4(文件不可读)、5(编码错误)

load 事件会在文件成功加载后触发。如果 error 事件被触发,则不会再触发 load 事件

const reader = new FileReader()

// 读取文件属性
if (/image/.test(files[0].type)) {
  reader.readAsDataURL(files[0])
} else {
  reader.readAsText(files[0])
}

reader.onerror = function() {
  console.log(reader.error.code)
}

reader.onprogress = function(e) {
  if (e.lengthComputable) {
    console.log(${e.loaded} / ${e.total})
  }
}

reader.onload = function() {
  console.log(reader.result)
}

6.1.3 FileReaderSync 类型

FileReader 的同步版本

6.2 Blob API

某些情况下,可能需要读取部分文件而不是整个文件。为此,File 对象提供了一个名为 slice() 的方法。接收两个参数:起始字节和要读取的字节。返回一个 Blob 的实例。

Blob 构造函数可以接收一个 options 参数,并在其中指定 MIME 类型:

console.log(new Blob(['foo']))
// Blob { size: 3, type: '' }

console.log(new Blob(['{ "a": "b" }'], { type: 'application/json' }))
// Blob { size: 10, type: 'application/json' }

6.3 对象 URL

引用存储在 File 或 Blob 中的数据的 URL。可以使用 window.URL.createObjectURL() 方法传入 File 或 Blob 对象。这个函数返回的值是一个指向内存中地址的字符串。因为这个字符串是 URL,所以可以在 DOM 中直接使用

const url = URL.createObjectURL(files[0])
console.log(url)

使用完数据后,最好能释放与之关联的内存。只要对象 URL 在使用中,就不能释放内存。页面卸载时,所有对象 URL 占用的内存都会被释放

window.URL.revokeObjectURL(url)

6.4 Notifications API

向用户显示通知。默认会开启两项安全措施:

  • 通知只能在运行在安全上下文的代码中被触发
  • 通知必须按照每个源的原则明确得到用户允许
// 向用户请求通知权限
Notification.requestPermission().then(permission => {
  // 用户点击允许 permission 的值是 granted,反之是 denied 
  console.log(permission)
})

一旦拒绝,就无法通过编程方式挽回,因为不可能再触发授权提示

6.4.1 显示和隐藏通知

立即显示通知

new Notification('Title text!')
const n = new Notification('Title text!', {
  body: 'Body text!', // 主体
  image: 'path/to/image.png', // 图片 
  vibrate: true // 振动
})
// 1s 后关闭通知
setTimeout(() => n.close(), 1000)

6.4.2 通知生命周期

  • onshow: 在通知显示时触发
  • onclick: 在通知被点击时触发
  • onclose: 在通知消失或通过 close() 关闭时触发
  • onerror: 在发生错误阻止通知显示时触发
const n = new Notification('foo')

n.onshow = () => console.log('onshow')
n.onclick = () => console.log('onclick')
n.onclose = () => console.log('onclose')
n.onerror = () => console.log('onerror')

6.5 Page Visibility API

  • document.visibilityState
    • hidden,页面在后台或者被最小化了
    • visible,页面在前台
    • prerender,实际页面隐藏了,但对页面的预览是可见的
  • document.hidden,布尔值,表示页面是否隐藏

6.6 时间戳 API

performance.now(),采用相对量度,这个计时器在执行上下文创建时从0开始计时。

const t0 = performance.now()
const t1 = performance.now()

注意:不同上下文没有共同参照点不能直接比较 performance.now()

Date.now() 只有毫秒级精度,如果中间代码块执行足够快,则两个时间戳的值会相等,而且中间执行的时候如果系统时钟被调整,时间差会有误差。performance.now() 返回一个微妙精度的浮点值,时间戳单调增长不可能出现相等的情况

七、IndexedDB

类似于 MySQL 或 Web SQL Database 的数据库。与传统数据库最大的区别在于,IndexedDB 适用对象存储而不是表格存储。

7.1 使用

调用 indexedDB.open() 方法,传入一个要打开的数据库名称

  • 如果给定数据库存在,就会发送一个打开它的请求;如果不存在,则会会送创建并打开这个数据库的请求
  • 这个方法会返回 IDBRequest 的实例
  • IndexedDB 的实际几乎是异步的,可以添加 onerror 和 onsuccess 事件处理程序来确定输出
let db,
  request,
  version = 1

request = indexedDB.open('admin', version) // version 指定版本号
request.onerror = e => {
  console.error(`Failed to open: ${e.target.errorCode}`)
}
request.onsuccess = e => {
  db = e.target.result // 访问数据库(indexDB)实例
  console.log(db) // > IDBDatabase
}
// 创建数据库(也就是数据库还不存在)、升级版本号都会在 onsuccess 之前调用该事件
request.onupgradeneeded = e => {
  // request === e.target
  console.log(request)
}

7.2 对象存储

创建一条用户记录,用户名必须全局唯一,也是大多数情况下访问数据的凭据。

const users = {
  username: '007',
  firstName: 'James',
  lastName: 'Bond',
  password: 'foo'
}

request.onupgradeneeded = e => {
  db = e.target.result

  // 如果存在则删除当前 objectStore
  if (db.objectStoreNames.contains('users')) {
    db.deleteObjectStore('users')
  }

  // keyPath 属性表示用作键的存储对象的属性名
  const objectStore = db.createObjectStore('users', { keyPath: 'username' })
}

7.3 事务

任何时候,想要读取或修改数据,都要通过事务把所有修改操作组织起来。

  • 第一个参数(必传),表示要访问的对象存储的名称
  • 第二个参数(可选),不传说明以只读方式访问数据
    • readonly,只读
    • readwrite,可读写
    • versionchange,数据库版本变化
// 指定一个要访问的对象存储的名称
var transaction = db.transaction('users')

// 访问多个
var transaction = db.transaction(['users', 'anoth'])

有了事务的引用,就可以使用 objectStore() 方法传入对象存储的名称以访问特定的对象存储。事务本身也有事件处理程序: onerror 和 oncomplete。

const objectStore = db.createObjectStore('users', { keyPath: 'username' })

objectStore.transaction.oncomplete = e => {
  // 事务成功完成
  const store = db.transaction('users', 'readwrite').objectStore('users')
  console.log(store) // > IDBObjectStore
}

操作对象

  • add(): 添加对象
  • put(): 更新对象
  • clear(): 删除所有对象
  • get(): 获取对象
  • delete(): 删除对象
// users 是一个用户数据的数组
for (const user of users) {
  store.add(user)
}

八、工作者线程

使用工作者线程,浏览器可以在原始页面环境之外再分配一个完全独立的二级子环境。这个子环境不能与依赖单线程交互的 API(如DOM)互操作,但可以与父环境并行执行代码。

8.1 工作者线程与线程

  • 工作者线程并行执行。虽然页面和工作者线程都是单线程 JavaScript 环境,但是每个环境中的指令可以并行执行
  • 工作者线程可以共享某些内存。能够使用 SharedArrayBuffer 在多个环境共享内容
  • 工作者线程不共享全部内存。除了 SharedArrayBuffer 外,从工作者线程进出的数据需要复制或转移
  • 工作者线程不一定在同一个进程里
  • 创建工作者线程的开销更大。有自己独立的事件循环、全局对象、事件处理程序和其他 JavaScript 环境必需的特性。创建这些结构的代价不容忽视

8.2 工作者线程的类型

  1. 专用工作者线程
    只能被创建它的页面使用。简称工作者线程Web WorkerWorker,是一种实用的工具,可以单独创建一个 JavaScript 线程,以执行委托的任务。

  2. 共享工作者线程
    可以被多个不同的上下文使用,包括不同的页面。任何与创建共享工作者线程的脚本同源的脚本,都可以向共享工作者线程发送消息或从中接收消息。

  3. 服务工作者线程
    主要用途是拦截、重定向和修改页面发出的请求,充当网络请求的仲裁者的角色。

8.3 专用工作者线程

8.3.1 创建专用工作者线程

最常见的方式是加载 JavaScript 文件。把文件路径提供给 Worker 构造函数,然后构造函数再在后台异步加载脚本并实例化工作者线程。

  • 第一个参数(必传):引入的脚本地址
  • 第二个参数(可选):options 配置对象
    • name: 可以在工作者线程中通过self.name 读取的字符串标识符
    • type: 加载脚本的运行方式,可以是 “classic(常规脚本)” 或 “module(模块脚本)”
    • credentials: 在 type 为 “module”时,指定如何获取与传输凭证数据相关的工作者线程模块脚本。值可以是“omit”、“same-orign”或“include”。与 fetch() 的凭证选项相同。在 type 为“classic”时,默认为“omit”
// worker.js
...

// main.js
console.log(location.href) // https:example.com/
const worker = new Worker(location.href + 'worker.js')
console.log(worker) // Worker {}

8.3.2 安全限制

只能从与父页面相同的源加载。从其他源加载工作者线程的脚本文件会导致错误,如下所示:

// 尝试基于 https://example.com/worker.js 创建工作者线程
const sameOriginWorker = new Worker('./worker.js')

// 尝试基于 https://untrusted.com/worker.js 创建工作者线程
const remoteOriginWorker = new Worker('https://untrusted.com/worker.js')

// Error: Uncaught DOMException: Failed to construct 'Worker':

注意:不能使用非同源脚本创建工作者线程,并不影响执行其他源的脚本。在工作者线程内部,使用 importScripts() 可以加载其他源的脚本

8.3.3 使用 Worker 对象

Worker() 构造函数返回的 Worker 对象是与刚创建的专用工作者线程通信的连接点。它可用于在工作者线程和父上下文间传输信息,以及捕获专用工作者线程发出的事件。

  • onerror: 发生 ErrorEvent 类型的错误事件时会调用指定给该属性的处理程序
    • 该事件会在工作者线程中抛出错误时发生
    • 该事件也可以通过 worker.addEventListener(‘error’, handler) 的形式处理
  • onmessage: 发生 MessageEvent 类型的消息事件时会调用指定给该属性的处理程序
    • 该事件会在工作者线程向父上下文发送消息时发生
    • 该事件也可以通过 worker.addEventListener(‘message’, handler) 的形式处理
  • onmessageerror: 发生 MessageEvent 类型的错误事件时会调用指定给该属性的处理程序
    • 该事件会在工作者线程收到无法序列化的消息时发生
    • 该事件也可以通过 worker.addEventListener(‘messageerror’, handler) 的形式处理
  • postMessage(): 用于通过异步消息事件向工作者线程发送消息
  • terminate(): 用于立即终止工作者线程

8.3.4 DedicatedWorkerGlobalScope

工作者线程可以通过 self 关键字访问该全局作用域。

// worker.js
console.log('inside worker:', self)

// main.js
const worker = new Worker('./worker.js')
console.log('created worker:', worker)

// created worker: Worker {}
// inside worker: DedicatedWorkerGlobalScope {}

因为工作者线程具有不可忽略的启动延迟,所以即使 Worker 对象存在,工作者线程的日志也会在主线程的日志之后打印出来。

注意:浏览器从两个不同的 JavaScript 线程收到消息,并按照自己认为合适的顺序输出这些消息。为此,在多线程应用程序中使用日志确定操作顺序时必须要当心。

DedicatedWorkerGlobalScope 在 WorkerGlobalScope 基础上增加了以下属性和方法:

  • name: 可以提供给 Worker 构造函数的一个可选的字符串标识符
  • postMessage(): 与 worker.postMessage() 对应的方法,用于工作者线程内部向父上下文发送消息
  • close(): 与 worker.terminate() 对应的方法,用于立即终止工作者线程
  • importScripts(): 用于向工作者线程中导入任意数量的脚本

8.3.5 生命周期

专用工作者线程可以非正式区分为处于下列三个状态:初始化活动终止

初始化时,虽然专用工作者线程脚本尚未执行,但可以先把要发送给工作者线程的消息加入队列。

// worker.js
self.addEventListener('message', ({ data }) => console.log(data))

// main.js
const worker = new Worker('./worker.js')

// Worker 可能处于初始化状态
// 但 postMessage() 数据可以正常处理
worker.postMessage('foo')

// foo

内部终止
会取消事件循环中的所有任务,并阻止继续添加新任务,但不会停止同步任务

// worker.js
self.postMessage('foo')
self.close()
self.postMessage('bar')
setTimeout(() => self.postMessage('baz'), 0)

// main.js
const worker = new Worker('./worker.js')

worker.addEventListener('message', ({ data }) => console.log(data))

// foo
// bar

外部终止
会立即终止后续任务

// worker.js
self.addEventListener('message', ({ data }) => console.log(data))

// main.js
const worker = new Worker('./worker.js')

// 给 1s 让工作者线程初始化
setTimeout(() => {
  worker.postMessage('foo')
  worker.terminate()
  worker.postMessage('bar')
  setTimeout(() => worker.postMessage('baz'), 0)
}, 1000)

// foo

8.3.6 行内创建工作者线程

通过 Blob 对象 URL 在行内脚本创建,这样可以更快速地初始化工作者线程,因为没有网络延迟。

// 创建需要执行的 JavaScript 代码字符串
const workerScript = 'self.onmessage = ({ data }) => console.log(data)'

// 基于脚本字符串生成 Blob 对象
const workerScriptBlob = new Blob([workerScript])

// 基于 Blob 实例创建对象 URL
const workerScriptBlobURL = URL.createObjectURL(workerScriptBlob)

// 基于对象 URL 创建工作者线程
const worker = new Worker(workerScriptBlobURL)

worker.postMessage('blob worker script')
// blob worker script

8.3.7 importScripts()

在工作者线程中动态执行脚本,加载的脚本按照加载顺序同步执行

// main.js
const worker = new Worker('./worker.js')

// importing scripts
// scriptA
// scriptB
// scripts importing

// scriptA.js
console.log('scriptA')

// scriptB.js
console.log('scriptB')

// worker.js
console.log('importing scripts')

// 也可以这样写 importScripts('./scriptA.js', './scriptB.js')
importScripts('./scriptA.js')
importScripts('./scriptB.js')

console.log('scripts importing')

脚本加载受到常规 CORS 的限制,但在工作者线程内部可以请求来自任何源的脚本

8.4 共享工作者线程

与专用工作者线程的一个重要区别在于,Worker() 构造函数始终会创建新实例,而 SharedWorker() 则只会在相同的标识不存在的情况下才创建新实例。如果存在与标识匹配的共享工作者线程,才会建立新的连接。

// 实例化一个共享工作者线程
// - 全部基于同源调用构造函数
// - 所有脚本解析为相同的 URL
// - 所有线程都有相同的名称
new SharedWorker('./sharedWorker.js')
new SharedWorker('sharedWorker.js')
new SharedWorker('https:example.com/sharedWorker.js')

8.4.1 创建共享工作者线程

// worker.js
...

// main.js
console.log(location.href) // https:example.com/
const sharedWorker = new SharedWorker(location.href + 'worker.js')
console.log(sharedWorker) // SharedWorker {}

8.4.2 使用 SharedWorker 对象

SharedWorker() 构造函数返回的 SharedWorker 对象是与刚创建的共享工作者线程通信的连接点。它可用来通过 MessagePort 在共享工作者线程和父上下文间传输信息,以及捕获共享线程中发出的错误事件。

  • onerror: 在共享线程中发生 ErrorEvent 类型的错误事件时会调用指定给该属性的处理程序
    • 此事件会在共享线程抛出错误时发生
    • 此事件也可以通过使用 sharedWorker.addEventListener(‘error’, handler) 处理
  • port: 专门用来跟共享线程通信的 MessagePort

8.4.3 SharedWorkerGlobalScope

共享工作者线程可以通过 self 关键字访问该全局作用域。

SharedWorkerGlobalScope 通过以下属性和方法扩展了 WorkerGlobalScope

  • name: 可以提供给 SharedWorker 构造函数的一个可选的字符串标识符
  • close(): 用于立即终止工作者线程,SharedWorker 对象上没有 terminate() 方法,只要还有一个端口连接到该线程就不会真的终止线程
  • importScripts(): 用于向工作者线程中导入任意数量的脚本
  • onconnect(): 与共享线程建立新连接时,应将其设置为处理程序。 connect 事件包括 MessagePort 实例的 ports 数组,可用于把消息发送回父上下文
    • 在通过 worker.port.onmessage 或 worker.port.start() 与共享线程建立连接时都会触发 connect 事件
    • connect 事件也可以通过使用 sharedWorker.addEventListener(‘connect’, handler) 处理
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值