JavaScript 读书笔记

269 阅读1小时+

现代的JavaScript

严格模式下,当一个函数声明在一个代码块内时,它在该代码块内的任何位置都是可见的。但在代码块外不可见。

在函数声明被定义之前,它就可以被调用。

函数表达式是在代码执行到达时被创建,并且仅从那一刻起可用。

当 JavaScript 准备 运行脚本时,首先会在脚本中寻找全局函数声明,并创建这些函数。我们可以将其视为“初始化阶段”。

  • 函数是值。它们可以在代码的任何地方被分配,复制或声明。
  • 如果函数在主代码流中被声明为单独的语句,则称为“函数声明”。
  • 如果该函数是作为表达式的一部分创建的,则称其“函数表达式”。
  • 在执行代码块之前,内部算法会先处理函数声明。所以函数声明在其被声明的代码块内的任何位置都是可见的。
  • 函数表达式在执行流程到达时创建。

Object

如果调用者不是对象,则this在严格模式为undefined。

  1. 任何函数(除了箭头函数,它没有自己的 this)都可以用作构造器。即可以通过 new 来运行(首字母大写)
  2. 可以使用 new.target 属性来检查它是否被使用 new 进行调用了。

可选链 ?.

中间的属性不存在,也不会报错。

如果可选链 ?. 前面的值为 undefined 或者 null,它会停止运算并返回 undefined,而不是报错。

请注意:?. 语法使其前面的值成为可选值,但不会对其后面的起作用。

变体:.?() .?[]

函数名?.() 用于调用一个可能不存在的函数。

如果函数存在则调用,否则什么都不会发生。

总结:可选链 ?. 语法有三种形式:

  1. obj?.prop —— 如果 obj 存在则返回 obj.prop,否则返回 undefined
  2. obj?.[prop] —— 如果 obj 存在则返回 obj[prop],否则返回 undefined
  3. obj.method?.() —— 如果 obj.method 存在则调用 obj.method(),否则返回 undefined

symbol 类型

只有两种原始类型可以用作对象属性键

  • 字符串类型
  • symbol 类型

总结

symbol 是唯一标识符的基本类型

symbol 是使用带有可选描述(name)的 Symbol() 调用创建的。

Object.keys(user) for in,遍历也会忽略它们。

Object.assign会同时复制字符串和 symbol 属性.

symbol 总是不同的值,即使它们有相同的名字。如果我们希望同名的 symbol 相等,那么我们应该使用全局注册表:Symbol.for(key) 返回(如果需要的话则创建)一个以 key 作为名字的全局 symbol。使用 Symbol.for 多次调用 key 相同的 symbol 时,返回的就是同一个 symbol。

symbol 有两个主要的使用场景:

  1. “隐藏” 对象属性。

    如果我们想要向“属于”另一个脚本或者库的对象添加一个属性,我们可以创建一个 symbol 并使用它作为属性的键。symbol 属性不会出现在 for..in 中,因此它不会意外地被与其他属性一起处理。并且,它不会被直接访问,因为另一个脚本没有我们的 symbol。因此,该属性将受到保护,防止被意外使用或重写。

    因此我们可以使用 symbol 属性“秘密地”将一些东西隐藏到我们需要的对象中,但其他地方看不到它。

  2. JavaScript 使用了许多系统 symbol,这些 symbol 可以作为 Symbol.* 访问。我们可以使用它们来改变一些内建行为。例如,在本教程的后面部分,我们将使用 Symbol.iterator 来进行 迭代 操作,使用 Symbol.toPrimitive 来设置 对象原始值转换 等等。

从技术上说,symbol 不是 100% 隐藏的。有一个内建方法 Object.getOwnPropertySymbols(obj) 允许我们获取所有的 symbol。还有一个名为 Reflect.ownKeys(obj) 的方法可以返回一个对象的 所有 键,包括 symbol。

缩略图

数字类型

数学 - JavaScript |多核 (mozilla.org)

num.toString(base) base为进制

使用两个点来调用一个方法

请注意 123456..toString(36) 中的两个点不是打错了。如果我们想直接在一个数字上调用一个方法,比如上面例子中的 toString,那么我们需要在它后面放置两个点 ..

如果我们放置一个点:123456.toString(36),那么就会出现一个 error,因为 JavaScript 语法隐含了第一个点之后的部分为小数部分。如果我们再放一个点,那么 JavaScript 就知道小数部分为空,现在使用该方法。

也可以写成 (123456).toString(36)

Object.is 进行比较

Object.is,它类似于 === 一样对值进行比较,但它对于两种边缘情况更可靠:

  1. 它适用于 NaNObject.is(NaN, NaN) === true,这是件好事。
  2. 0-0 是不同的:Object.is(0, -0) === false,从技术上讲这是对的,因为在内部,数字的符号位可能会不同,即使其他所有位均为零。

在所有其他情况下,Object.is(a, b)a === b 相同。

parseInt(str, radix) 的第二个参数

parseInt() 函数具有可选的第二个参数。它指定了数字系统的基数,因此 parseInt 还可以解析十六进制数字、二进制数字等的字符串:

alert( parseInt('0xff', 16) ); // 255
alert( parseInt('ff', 16) ); // 255,没有 0x 仍然有效

alert( parseInt('2n9c', 36) ); // 123456

总结

要写有很多零的数字:

  • "e" 和 0 的数量附加到数字后。就像:123e6123 后面接 6 个 0 相同。
  • "e" 后面的负数将使数字除以 1 后面接着给定数量的零的数字。例如 123e-6 表示 0.000123123 的百万分之一)。

对于不同的数字系统:

  • 可以直接在十六进制(0x),八进制(0o)和二进制(0b)系统中写入数字。
  • parseInt(str, base) 将字符串 str 解析为在给定的 base 数字系统中的整数,2 ≤ base ≤ 36
  • num.toString(base) 将数字转换为在给定的 base 数字系统中的字符串。

对于常规数字检测:

  • isNaN(value) 将其参数转换为数字,然后检测它是否为 NaN
  • isFinite(value) 将其参数转换为数字,如果它是常规数字,则返回 true,而不是 NaN/Infinity/-Infinity

要将 12pt100px 之类的值转换为数字:

  • 使用 parseInt/parseFloat 进行“软”转换,它从字符串中读取数字,然后返回在发生 error 前可以读取到的值。

小数:

  • 使用 Math.floorMath.ceilMath.truncMath.roundnum.toFixed(precision) 进行舍入。
  • 请确保记住使用小数时会损失精度。

字符串

slice(start, end)startend(不含 end允许
substring(start, end)startend(不含 end负值被视为 0
substr(start, length)start 开始获取长为 length 的字符串允许 start 为负数

找出所有项的和最大的 arr 数组的连续子数组。

例,arr = [1, -2, 3, 4, -9, 6].

任务是:找出所有项的和最大的 arr 数组的连续子数组。

写出函数 getMaxSubSum(arr),用其找出并返回最大和。

方法1.外部循环遍历数组所有元素,内部循环计算从当前元素开始的所有子数组各自的和。

function getMaxSubSum(arr) {
  let maxSum = 0; // 如果没有取到任何元素,就返回 0

  for (let i = 0; i < arr.length; i++) {
    let sumFixedStart = 0;
    for (let j = i; j < arr.length; j++) {
      sumFixedStart += arr[j];
      maxSum = Math.max(maxSum, sumFixedStart);
    }
  }

  return maxSum;
}

方法2.快

让我们遍历数组,将当前局部元素的和保存在变量 s 中。如果 s 在某一点变成负数了,就重新分配 s=0。所有 s 中的最大值就是答案。

function getMaxSubSum(arr) {
  let maxSum = 0;
  let partialSum = 0;

  for (let item of arr) { // arr 中的每个 item
    partialSum += item; // 将其加到 partialSum
    maxSum = Math.max(maxSum, partialSum); // 记住最大值
    if (partialSum < 0) partialSum = 0; // 如果是负数就置为 0
  }

  return maxSum;
}

内置的**Symbol.isConcatSpreadable**符号用于配置某对象作为Array.prototype.concat()方法的参数时是否展开其数组元素。

Iterable object(可迭代对象)

可迭代(iterable)和类数组(array-like)

这两个官方术语看起来差不多,但其实大不相同。请确保你能够充分理解它们的含义,以免造成混淆。

  • Iterable 如上所述,是实现了 Symbol.iterator 方法的对象。
  • Array-like 是有索引和 length 属性的对象,所以它们看起来很像数组。

字符串既是可迭代的(for..of 对它们有效),又是类数组的(它们有数值索引和 length 属性)。

总结

可以应用 for..of 的对象被称为 可迭代的

  • 技术上来说,可迭代对象必须实现

    Symbol.iterator
    

    方法。

    • obj[Symbol.iterator]() 的结果被称为 迭代器(iterator)。由它处理进一步的迭代过程。
    • 一个迭代器必须有 next() 方法,它返回一个 {done: Boolean, value: any} 对象,这里 done:true 表明迭代结束,否则 value 就是下一个值。
  • Symbol.iterator 方法会被 for..of 自动调用,但我们也可以直接调用它。

  • 内建的可迭代对象例如字符串和数组,都实现了 Symbol.iterator

  • 字符串迭代器能够识别代理对(surrogate pair)。(译注:代理对也就是 UTF-16 扩展字符。)

索引属性length 属性的对象被称为 类数组对象。这种对象可能还具有其他属性和方法,但是没有数组的内建方法。类数组对象可以通过索引访问元素

如果我们仔细研究一下规范 —— 就会发现大多数内建方法都假设它们需要处理的是可迭代对象或者类数组对象,而不是“真正的”数组,因为这样抽象度更高。

Array.from(obj[, mapFn, thisArg]) 将可迭代对象或类数组对象 obj 转化为真正的数组 Array,然后我们就可以对它应用数组的方法。可选参数 mapFnthisArg 允许我们将函数应用到每个元素。

mapFn 可以是一个函数,该函数会在对象中的元素被添加到数组前,作前置处理,此外 thisArg 允许我们为该函数设置 this

使用展开运算符“...”也可以将可迭代对象转换为真正的数组,而且更加简洁

Map,Set

Map —— 是一个带键的数据项的集合。

方法和属性如下:

  • new Map([iterable]) —— 创建 map,可选择带有 [key,value] 对的 iterable(例如数组)来进行初始化。
  • map.set(key, value) —— 根据键存储值,返回 map 自身。
  • map.get(key) —— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined
  • map.has(key) —— 如果 key 存在则返回 true,否则返回 false
  • map.delete(key) —— 删除指定键对应的值,如果在调用时 key 存在,则返回 true,否则返回 false
  • map.clear() —— 清空 map 。
  • map.size —— 返回当前元素个数。

与普通对象 Object 的不同点:

  • 任何键、对象都可以作为键。
  • 有其他的便捷方法,如 size 属性。

Set —— 是一组唯一值的集合。

方法和属性:

  • new Set([iterable]) —— 创建 set,可选择带有 iterable(例如数组)来进行初始化。
  • set.add(value) —— 添加一个值(如果 value 存在则不做任何修改),返回 set 本身。
  • set.delete(value) —— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false
  • set.has(value) —— 如果 value 在 set 中,返回 true,否则返回 false
  • set.clear() —— 清空 set。
  • set.size —— 元素的个数。

MapSet 中迭代总是按照值插入的顺序进行的,所以我们不能说这些集合是无序的,但是我们不能对元素进行重新排序,也不能直接按其编号来获取元素。

Object.entries()用于将object转为二维数组, Object.fromEntries()用于将二维数组或者Iterable转为对象

WeakMap and WeakSet(弱映射和弱集合)

WeakMap

WeakMapMap 的第一个不同点就是,WeakMap 的键必须是对象,不能是原始值.

​ 在 weakMap 中使用一个对象作为键,并且没有其他对这个对象的引用 —— 该对象将会被从内存(和map)中自动清除。

WeakMap 不支持迭代以及 keys()values()entries() 方法。所以没有办法获取 WeakMap 的所有键或值。为什么JavaScript 引擎可能会选择立即执行内存清理,如果现在正在发生很多删除操作,那么 JavaScript 引擎可能就会选择等一等,稍后再进行内存清理。因此,从技术上讲,`WeakMap` 的当前元素的数量是未知的。JavaScript 引擎可能清理了其中的垃圾,可能没清理,也可能清理了一部分。因此,暂不支持访问 `WeakMap` 的所有键/值的方法。

WeakMap 主要应用场景: 额外数据的存储

例如,我们有用于处理用户访问计数的代码。收集到的信息被存储在 map 中:一个用户对象作为键,其访问次数为值。当一个用户离开时(该用户对象将被垃圾回收机制回收),这时我们就不再需要他的访问次数了。此时就可以用Weakmap

另外一个常见的例子是缓存。我们可以存储(“缓存”)函数的结果,以便将来对同一个对象的调用可以重用这个结果。

WeakSet

JSON

总结

  • JSON 是一种数据格式,具有自己的独立标准和大多数编程语言的库。
  • JSON 支持 object,array,string,number,boolean 和 null
  • JavaScript 提供序列化(serialize)成 JSON 的方法 JSON.stringify 和解析 JSON 的方法 JSON.parse
  • 这两种方法都支持用于智能读/写的转换函数。
  • 如果一个对象具有 toJSON,那么它会被 JSON.stringify 调用。

JSON.stringify()

一些特定于 JavaScript 的对象属性会被 JSON.stringify 跳过。

即:

  • 函数属性(方法)。
  • Symbol 类型的键和值。
  • 存储 undefined 的属性。

JSON.parse(str, [reviver]):

str

要解析的 JSON 字符串。

reviver

可选的函数 function(key,value),该函数将为每个 (key, value) 对调用,并可以对值进行转换。

例如:如果JSON中含有日期,parse直接解析,会被解析成字符串,而我们想要的是Date对象。所以通过可选函数内部进行加工

let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

let meetup = JSON.parse(str, function(key, value) {
  if (key == 'date') return new Date(value);
  return value;
});

img

“arguments” 变量

有一个名为 arguments 的特殊类数组对象可以在函数中被访问,该对象以参数在参数列表中的索引作为键,存储所有参数。

箭头函数没有 "arguments"

如果我们在箭头函数中访问 arguments,访问到的 arguments 并不属于箭头函数,而是属于箭头函数外部的“普通”函数。

Rest参数:...args

把参数列表中剩余的参数收集到一个数组中

展开运算符(Spread 语法): ...arr

Spread 语法内部使用了迭代器来收集元素,与 for..of 的方式相同。

还可以使用 Array.from 来实现,因为该方法会将一个可迭代对象(如字符串)转换为数组。

不过 Array.from(obj)[...obj] 存在一个细微的差别:

  • Array.from 适用于类数组对象也适用于可迭代对象。
  • Spread 语法只适用于可迭代对象。

展开运算符同时可以与Object.assign()功能类似,实现浅拷贝

let arrCopy = [...arr]; // 将数组/对象 spread 到参数列表中
                        // 然后将结果放到一个新数组/对象

一个是获取剩余参数,一个是把可迭代对象展开到参数列表

执行上下文(Context)

执行上下文 是一个内部数据结构,它包含有关函数执行时的详细细节:当前控制流所在的位置,当前的变量,this 的值(此处我们不使用它),以及其它的一些内部细节。

一个函数调用仅具有一个与其相关联的执行上下文。

当一个函数进行嵌套调用时,将发生以下的事儿:

  • 当前函数被暂停;
  • 与它关联的执行上下文被一个叫做 执行上下文堆栈 的特殊数据结构保存;
  • 执行嵌套调用;
  • 嵌套调用结束后,从堆栈中恢复之前的执行上下文,并从停止的位置恢复外部函数。

闭包,作用域

词法环境

词法环境对象由两部分组成:

  1. 环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如 this 的值)的对象{}。
    • 变量是特殊内部对象的属性,与当前正在执行的(代码)块/函数/脚本有关。
    • 操作变量实际上是操作该对象的属性。
  2. 外部词法环境 的引用,与外部代码相关联。

1.变量

脚本开始运行时,词法环境刚创建,局部变量为**未初始化(Uninitialized)**状态,执行到let声明时,变量的undefined,然后被赋值

2。函数

一个函数其实也是一个值,就像变量一样。

变量与函数不同之处在于,函数声明的初始化会被立即完成,也就是说,词法环境刚创建时,函数就已经完成初始化,可以直接调用了。(只适用于,function aaa(){}声明的函数,不适用于let aaa=function(){}

当创建了一个词法环境(Lexical Environment)时,函数声明会立即变为即用型函数(不像 let 那样直到声明处才可用)。

这就是为什么调用函数可以在声明函数之前

在一个函数运行时,在调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数。

3.内部和外部的词法环境

每调用一次函数,都会创建一个词法环境,来存储当前函数的局部变量和参数

同时,该词法环境还会包含一个对父词法环境的引用

当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。

4.返回函数

内嵌函数被返回时,其 [[Environment]] 属性,会记住外部函数的语法环境,

当返回的内嵌函数被调用时,会创建一个新的语法环境,并包含外部环境的引用,

如果在自己的语法环境里寻找变量,找到就好,找不到则到外部语法环境里去找。

闭包

闭包 是指一个函数可以记住其外部变量并可以访问这些变量

也就是说JavaScript 中的函数会自动通过隐藏的 [[Environment]] 属性记住创建它们的位置,所以它们都可以访问外部变量。

但是,如果我们使用 new Function 创建一个函数,那么该函数的 [[Environment]] 并不指向当前的词法环境,而是指向全局环境。

因此,此类函数无法访问外部(outer)变量,只能访问全局变量。

闭包的垃圾回收GC

如果函数返回的内嵌函数不可用时,这个函数才会被垃圾回收,只要有一个内嵌函数还可用,则就不会被垃圾回收。

暂时性死区:

暂时性死区是指在 ES6(ECMAScript 2015)引入的 let 和 const 声明变量的块作用域中,变量被声明但是还未被初始化时,访问该变量会导致一个引用错误

具体来说,当在一个块作用域中使用 let 或 const 声明一个变量时,该变量被创建时就进入了暂时性死区,直到变量被赋值为止。在这个过程中,如果访问该变量,就会抛出一个引用错误。

例如:

javascriptCopy codefunction example() {
  console.log(x); // ReferenceError: x is not defined
  let x = 1;
}

在上面的代码中,变量 x 在声明前被访问,因此会抛出一个引用错误。这是因为在声明 let 变量 x 后,在该声明之前任何对变量 x 的引用都是不合法的。

暂时性死区的存在可以帮助开发人员捕获潜在的错误,同时也强制执行变量的初始化。

let,var

varlet/const 有两个主要的区别:

  1. var 声明的变量没有块级作用域,它们仅在当前函数内可见,或者全局可见(如果变量是在函数外声明的)。
  2. var 变量声明在函数开头就会被处理(脚本启动对应全局变量)。

涉及全局对象时,还有一个非常小的差异,我们将在下一章中介绍。

这些差异使 var 在大多数情况下都比 let 更糟糕。块级作用域是这么好的一个东西。这就是 let 在几年前就被写入到标准中的原因,并且现在(与 const 一起)已经成为了声明变量的主要方式。

函数有内建属性 length它返回函数入参的个数,rest 参数不参与计数。

函数的属性不是变量:被赋值给函数的属性,比如 sayHi.counter = 0不会 在函数内定义一个局部变量 counter。换句话说,属性 counter 和变量 let counter 是毫不相关的两个东西。

IIFE立即执行函数

早期js没有块级作用域,所以用函数来模拟块级作用域,

(function() {

  var message = "Hello";

  alert(message); // Hello

})();

声明一个函数,并立即执行

全局对象

  • 全局对象包含应该在任何位置都可见的变量。

    其中包括 JavaScript 的内建方法,例如 “Array” 和环境特定(environment-specific)的值,例如 window.innerHeight — 浏览器中的窗口高度。

  • 全局对象有一个通用名称 globalThis

    ……但是更常见的是使用“老式”的环境特定(environment-specific)的名字,例如 window(浏览器)和 global(Node.js)。

  • 仅当值对于我们的项目而言确实是全局的时候,才应将其存储在全局对象中。并保持其数量最少。

  • 在浏览器中,除非我们使用 modules,否则使用 var 声明的全局函数和变量会成为全局对象的属性。

  • 为了使我们的代码面向未来并更易于理解,我们应该使用直接的方式访问全局对象的属性,如 window.x

函数属性有时会用来替代闭包。(函数的属性不是内部变量

任意数量的括号求和

function sum(a) {

  let currentSum = a;

  function f(b) {
    currentSum += b;
    return f;
  }

  f.toString = function() {
    return currentSum;
  };

  return f;
}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1)(2) ); // 6
alert( sum(6)(-1)(-2)(-3) ); // 0
alert( sum(0)(1)(2)(3)(4)(5) ); // 15

SetTimeout/SetInterval

  • setTimeout(func, delay, ...args)setInterval(func, delay, ...args) 方法允许我们在 delay 毫秒之后运行 func 一次或以 delay 毫秒为时间间隔周期性运行 func
  • 要取消函数的执行,我们应该调用 clearInterval/clearTimeout,并将 setInterval/setTimeout 返回的值作为入参传入。
  • 嵌套的 setTimeoutsetInterval 用起来更加灵活,允许我们更精确地设置两次执行之间的时间。
  • 零延时调度 setTimeout(func, 0)(与 setTimeout(func) 相同)用来调度需要尽快执行的调用,但是会在当前脚本执行完成后进行调用
  • 浏览器会将 setTimeoutsetInterval 的五层或更多层嵌套调用(调用五次之后)的最小延时限制在 4ms。这是历史遗留问题。

请注意,所有的调度方法都不能 保证 确切的延时。

例如,浏览器内的计时器可能由于许多原因而变慢:

  • CPU 过载。
  • 浏览器页签处于后台模式。
  • 笔记本电脑用的是省电模式。

所有这些因素,可能会将定时器的最小计时器分辨率(最小延迟)增加到 300ms 甚至 1000ms,具体以浏览器及其设置为准。

任何 0延时的setTimeout 都只会在当前代码执行完毕之后才会执行。

周期性调度(定时器)可以用**setInterval**或者**嵌套的 setTimeout**实现

/** instead of:
let timerId = setInterval(() => alert('tick'), 2000);
*/2秒执行一次
let timerId = setTimeout(function tick() {
  alert('tick');
  timerId = setTimeout(tick, 2000); // (*)
}, 2000);

嵌套的 setTimeout 相较于 setInterval 能够更精确地设置两次执行之间的延时。

setInterval 而言,内部的调度程序会每间隔 100 毫秒执行一次 func(i++)

使用 setInterval 时,func 函数的实际调用间隔要比代码中设定的时间间隔要短!

这也是正常的,因为 func 的执行所花费的时间“消耗”了一部分间隔时间。

uTools_1678886296068

嵌套的 setTimeout 就能确保延时的固定(这里是 100 毫秒)。

这是因为setTimeout 都只会在当前代码执行完毕之后才会执行。,下一次调用是在前一次调用完成时再开始调度(开始计时)的

setTimeout:

uTools_1678886359942

装饰器模式和转发,call/apply

丢失This

浏览器中的 setTimeout 方法有些特殊:它为函数调用设定了 this=window(对于 Node.js,this 则会变为计时器(timer)对象,但在这儿并不重要)。所以对于 this.firstName,它其实试图获取的是 window.firstName,这个变量并不存在。在其他类似的情况下,通常 this 会变为 undefined

属性标志

对象属性(properties),除 value 外,还有三个特殊的特性(attributes),也就是所谓的“标志”:

  • writable — 如果为 true,则值可以被修改,否则它是只可读的。
  • enumerable — 如果为 true,则会被在循环中列出,否则不会被列出。
  • configurable — 如果为 true,则此属性可以被删除,这些特性也可以被修改,否则不可以。

我们到现在还没看到它们,是因为它们通常不会出现。当我们用“常用的方式”创建一个属性时,它们都为 true。但我们也可以随时更改它们。

Object.getOwnPropertyDescriptor 方法允许查询有关属性的 完整 信息。

let user = {
  name: "John"
};

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

alert( JSON.stringify(descriptor, null, 2 ) );
/* 属性描述符:
{
  "value": "John",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
*/

为了修改标志,我们可以使用 Object.defineProperty

如果该属性存在,defineProperty 会更新其标志。否则,它会使用给定的值和标志创建属性;在这种情况下,如果没有提供标志,则会假定它是 false

let user = {};

Object.defineProperty(user, "name", {
  value: "John"
});

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
  "value": "John",
  "writable": false,
  "enumerable": false,
  "configurable": false
}
 */

方法 Object.defineProperties(obj, descriptors),允许一次定义多个属性

要一次获取所有属性描述符,我们可以使用 Object.getOwnPropertyDescriptors(obj) 方法。

注意PropertyProperties的区别

访问器属性:Getter/Setter

原型继承

JavaScript深入之从原型到原型链 · Issue #2 · mqyqingfeng/Blog (github.com)

在 JavaScript 中,对象有一个特殊的隐藏属性 [[Prototype]](如规范中所命名的),它要么为 null,要么就是对另一个对象的引用。该对象被称为“原型”

当我们从 对象 中读取一个缺失的属性时,JavaScript 会自动从原型中获取该属性。在编程中,这被称为“原型继承”。

属性 [[Prototype]] 是内部的而且是隐藏的,但是这儿有很多设置它的方式。

其中之一就是使用特殊的名字 __proto__

__proto__ 其实是 [[Prototype]] 的因历史原因而留下来的 getter/setter

__proto__ 属性有点过时了。现在建议我们使用函数 Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__ 去 get/set 原型。

for..in 循环也会迭代继承的属性。几乎所有其他键/值获取方法,例如 Object.keysObject.values 等,都会忽略继承的属性

如果我们想排除继承的属性,那么这儿有一个内建方法 obj.hasOwnProperty(key):如果 obj 具有自己的(非继承的)名为 key 的属性,则返回 true

总结

  • 在 JavaScript 中,所有的对象都有一个隐藏的 [[Prototype]] 属性,它要么是另一个对象,要么就是 null

  • 我们可以使用 obj.__proto__ 访问它(历史遗留下来的 getter/setter,这儿还有其他方法,很快我们就会讲到)。

  • 通过 [[Prototype]] 引用的对象被称为“原型”。

  • 如果我们想要读取 obj 的一个属性或者调用一个方法,并且它不存在,那么 JavaScript 就会尝试在原型中查找它。

  • 写/删除操作直接在对象上进行,它们不使用原型(假设它是数据属性,不是 setter)。

  • 如果我们调用 obj.method(),而且 method 是从原型中获取的,this 仍然会引用 obj。因此,方法始终与当前对象一起使用,即使方法是继承的。

  • for..in 循环在其自身和继承的属性上进行迭代。所有其他的键/值获取方法仅对对象本身起作用。

  • 内建方法 obj.hasOwnProperty(key):如果 obj 具有自己的(非继承的)名为 key 的属性,则返回 true

  • 要使用给定的原型创建对象,使用:

    • 字面量语法:{ __proto__: ... },允许指定多个属性
    • 或 [Object.create(proto, descriptors]),允许指定属性描述符。

    Object.create 提供了一种简单的方式来浅拷贝对象及其所有属性描述符(descriptors)。

    let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
    
  • 设置和访问原型的现代方法有:

  • 不推荐使用内建的的 __proto__ getter/setter 获取/设置原型,它现在在 ECMA 规范的附录 B 中。

  • 我们还介绍了使用 Object.create(null){__proto__: null} 创建的无原型的对象。

    这些对象被用作字典,以存储任意(可能是用户生成的)键。

    通常,对象会从 Object.prototype 继承内建的方法和 __proto__ getter/setter,会占用相应的键,且可能会导致副作用。原型为 null 时,对象才真正是空的。

Prototype函数才会有的属性,为的就是用函数方式实现对象OOP

F.prototype=xxx设置的是它的实例对象(通过new创建的对象)的[[Prototype]](原型)

默认的.prototype属性指向的是一个原型对象:它是一个具有constructor对象,该constructor指向的是F这个对象(注意实例对象与对象的区别):F.prototype={ constructor:F }

prototype指向的原型对象相当于是该对象所有实例化对象共享空间,实例对象可以通过**__proto__访问原型对象**,也就是该实例对象的**[[Prototype]]属性(原型)**。

Instance of

instanceof 操作符用于检查一个对象是否属于某个特定的 class。同时,它还考虑了继承。

obj instanceof Class 算法的执行过程大致如下:

  1. 如果有静态方法 Symbol.hasInstance,那就直接调用这个方法:
  2. 大多数 class 没有 Symbol.hasInstance。在这种情况下,标准的逻辑是:使用 obj instanceOf Class 检查 Class.prototype 是否等于 obj 的原型链中的原型之一。
obj.__proto__ === Class.prototype?
obj.__proto__.__proto__ === Class.prototype?
obj.__proto__.__proto__.__proto__ === Class.prototype?
...
// 如果任意一个的答案为 true,则返回 true
// 否则,如果我们已经检查到了原型链的尾端,则返回 false

​ 上述步骤可以简化: objA.isPrototypeOf(objB),如果 objA 处在 objB 的原型链中,则返回 true。可以将 obj instanceof Class 检查改为 Class.prototype.isPrototypeOf(obj)。但 Class 的 constructor 自身不参与检查!检查过程只和原型链以及 Class.prototype 有关。

​ 如果创建对象后,将其.prototype={}则无法通过instance of判断

Object.prototype.toString 判断类型

xxxx

可以使用特殊的对象属性 Symbol.toStringTag 自定义对象的 toString 方法的行为。

let user = {
  [Symbol.toStringTag]: "User"
};

alert( {}.toString.call(user) ); // [object User]

类型检查方法:

用于返回值
typeof原始数据类型string
{}.toString原始数据类型,内建对象,包含 Symbol.toStringTag 属性的对象string
instanceof对象true/false

正如我们所看到的,从技术上讲,{}.toString 是一种“更高级的” typeof

当我们使用类的层次结构(hierarchy),并想要对该类进行检查,同时还要考虑继承时,这种场景下 instanceof 操作符确实很出色。

Mixin (混入)模式

定义:一个包含其他类的方法的类。

JS不支持多继承,只能继承单个对象,也就是说每个对象只能有一个 [[Prototype]]

使用方法:

​ 定义一个要混入的类,如sayHiMixin,使用拷贝方法 Object.assign(User.prototype, sayHiMixin);

此时,User就可以使用sayHiMixin里的方法了。

错误处理

Promise

了解Promise就要先了解回调,Promise就是为了解决回调地狱而诞生的

回调

Script脚本是异步调用的,因为它从现在开始加载,但是在这个加载函数执行完成后才运行。(注意:加载和运行不是一个概念)

也就是说Script标签下面的代码不会等到脚本加载完成才执行。

如果我们要在脚本加载后立刻调用它内部的函数,那么调用函数会失效

loadScript('/my/script.js'); // 这个脚本内有 "function newFunction() {…}"

newFunction(); // 没有这个函数!

因此:我们希望了解脚本何时加载完成,以使用其中的新函数和变量。

回调就是为此诞生的

回调地狱

回调中嵌套回调,如下图

uTools_1678973999725

Promise

Promise对象内有两个内部属性:

  • state(状态):

    ​ 最初是 "pending",然后在 resolve 被调用时变为 "fulfilled",或者在 reject 被调用时变为 "rejected"

  • result(结果):

uTools_1678974304480

.Then/.Catch

stateresult 属性都是内部的,我们不能直接访问他们,而是通过.then.catch使用

在 new Promise()的时候,Promise的执行器就会立马执行,但是调用resolve()会触发异步操作,传入的then()方法的函数会被添加到任务队列并异步执行

.finally

  • finally 处理程序没有得到前一个处理程序的结果(它没有参数)。而这个结果被传递给了下一个合适的处理程序。
  • 如果 finally 处理程序返回了一些内容,那么这些内容会被忽略。
  • finally 抛出 error 时,执行将转到最近的 error 的处理程序。

PromiseAPI

Promise 类有 6 种静态方法:

  1. Promise.all(promises) —— 等待所有 promise 都 resolve 时,返回存放它们结果的数组。如果给定的任意一个 promise 为 reject,那么它就会变成 Promise.all 的 error,所有其他 promise 的结果都会被忽略。

  2. Promise.allSettled(promises)
    

    (ES2020 新增方法)—— 等待所有 promise 都 settle 时,并以包含以下内容的对象数组的形式返回它们的结果:

    • status: "fulfilled""rejected"
    • value(如果 fulfilled)或 reason(如果 rejected)。
  3. Promise.race(promises) —— 等待第一个 settle 的 promise,并将其 result/error 作为结果返回。

  4. Promise.any(promises)(ES2021 新增方法)—— 等待第一个 fulfilled 的 promise,并将其结果作为结果返回。如果所有 promise 都 rejected,Promise.any 则会抛出 AggregateError 错误类型的 error 实例。

  5. Promise.resolve(value) —— 使用给定 value 创建一个 resolved 的 promise。

  6. Promise.reject(error) —— 使用给定 error 创建一个 rejected 的 promise。

以上所有方法,Promise.all 可能是在实战中使用最多的。

在 Node.js 中,有一个内建的 promise 化函数 util.promisify

Await/Async

await 接受 “thenables”(有 then 方法的对象),尽管他不是Promise

Generator,高级 iteration

Generator

generator函数:

generator 可以按需一个接一个地返回(“yield”)多个值

语法结构:function* xxx

generator函数被调用时,它不会运行其代码。而是返回一个被称为 “generator object” 的特殊对象,来管理执行流程

generator函数不会自动执行流程,而是当next()方法被调用时,他会执行到最近的yield <value>语句。然后暂停执行,并返回<value>

所以说外部调用next()时,它只有两个返回值:

  • value: 产出的(yielded)的值。
  • done: 如果 generator 函数已执行完成则为 true,否则为 false

你可以把generator函数看成:由yield分割的许多块next()调用时,执行下一块逻辑。

yield不仅可以向外返回结果,而且还可以将外部的值传递到 generator 内。

调用 generator.next(arg),我们就能将参数 arg 传递到 generator 内部。这个 arg 参数会变成 yield 的结果。

要向 yield 传递一个 error,我们应该调用 generator.throw(err)。在这种情况下,err 将被抛到对应的 yield 所在的那一行。

generator.return(value) 完成 generator 的所有执行流程并返回给定的 value

generator 是可迭代的

因为其有next()方法,所以可以推测出其可迭代

因此,我们可以使用for of遍历所有value

我们通用可以使用**...展开语法**,展开其所有值。

在前面的 Iterable object(可迭代对象) 一章中,我们创建了一个可迭代的 range 对象,它返回 from..to 的值。

现在,我们回忆一下可迭代对象的代码:

let range = {
  from: 1,
  to: 5,

  // for..of range 在一开始就调用一次这个方法
  [Symbol.iterator]() {
    // ...它返回 iterator object:
    // 后续的操作中,for..of 将只针对这个对象,并使用 next() 向它请求下一个值
    return {
      current: this.from,
      last: this.to,

      // for..of 循环在每次迭代时都会调用 next()
      next() {
        // 它应该以对象 {done:.., value :...} 的形式返回值
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

// 迭代整个 range 对象,返回从 `range.from` 到 `range.to` 范围的所有数字
alert([...range]); // 1,2,3,4,5

我们使用gennrator升级可迭代对象

let range = {
  from: 1,
  to: 5,

  *[Symbol.iterator]() { // [Symbol.iterator]: function*() 的简写形式
    for(let value = this.from; value <= this.to; value++) {
      yield value;
    }
  }
};

alert( [...range] ); // 1,2,3,4,5

异步可迭代对象

要使对象异步迭代:

  1. 使用 Symbol.asyncIterator 取代 Symbol.iterator
  2. next()方法应该返回一个promise(带有下一个值,并且状态为fulfilled)。
    • 关键字 async 可以实现这一点,我们可以简单地使用 async next()
  3. 我们应该使用for await (let item of iterable)循环来迭代这样的对象。
    • 注意关键字 await
let range = {
  from: 1,
  to: 5,

  [Symbol.asyncIterator]() { // (1)
    return {
      current: this.from,
      last: this.to,

      async next() { // (2)

        // 注意:我们可以在 async next 内部使用 "await"
        await new Promise(resolve => setTimeout(resolve, 1000)); // (3)

        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

(async () => {

  for await (let value of range) { // (4)
    alert(value); // 1,2,3,4,5
  }

})()

Iterator异步 iterator 之间的差异:

Iterator异步 iterator
提供 iterator 的对象方法Symbol.iteratorSymbol.asyncIterator
next() 返回的值是任意值Promise
要进行循环,使用for..offor await..of

Spread 语法 ... 无法异步工作

异步Generator

function* 前面加上 async。这即可使 generator 变为异步的。

然后使用 for await (...) 来遍历它

我们使用异步的Gnerator升级异步可迭代对象:

let range = {
  from: 1,
  to: 5,
  // 这一行等价于 [Symbol.asyncIterator]: async function*() {
  async *[Symbol.asyncIterator]() {
    for(let value = this.from; value <= this.to; value++) {
      // 在 value 之间暂停一会儿,等待一些东西
      await new Promise(resolve => setTimeout(resolve, 1000));
      yield value;
    }
  }
};

(async () => {

  for await (let value of range) {
    alert(value); // 1,然后 2,然后 3,然后 4,然后 5
  }

})();
async function* generateSequence(start, end) {

  for (let i = start; i <= end; i++) {
    // 哇,可以使用 await 了!
    await new Promise(resolve => setTimeout(resolve, 1000));
    yield i;
  }
}
(async () => {
  let generator = generateSequence(1, 5);
  for await (let value of generator) {
    alert(value); // 1,然后 2,然后 3,然后 4,然后 5(在每个 alert 之间有延迟)
  }

})();

异步 iterator 与常规 iterator 在语法上的区别:

Iterable异步 Iterable
提供 iterator 的对象方法Symbol.iteratorSymbol.asyncIterator
next() 返回的值是{value:…, done: true/false}resolve 成 {value:…, done: true/false}Promise

异步 generator 与常规 generator 在语法上的区别:

Generator异步 generator
声明方式function*async function*
next() 返回的值是{value:…, done: true/false}resolve 成 {value:…, done: true/false}Promise

模块(Module)

通常声明模块如下:type="module"

<script type="module">
  alert(this); // undefined
</script>

模块只通过 HTTP(s) 工作,而非本地

如果你尝试通过 file:// 协议在本地打开一个网页,你会发现 import/export 指令不起作用。你可以使用本地 Web 服务器,例如 static-server,或者使用编辑器的“实时服务器”功能,例如 VS Code 的 Live Server Extension 来测试模块。

始终使用严格模式("use strict")

模块代码仅在第一次导入被解析

如果同一个模块被导入到多个其他位置,那么它的代码只会执行一次,即在第一次被导入时。然后将其导出(export)的内容提供给进一步的导入(importer)。

如果执行一个模块中的代码会带来副作用(side-effect),例如显示一条消息,那么多次导入它只会触发一次显示 —— 即第一次。

如果我们需要多次调用某些东西 ,我们应该将其以函数的形式导出

假设导出一个admin,那么所有的导入都只获得了一个唯一的 admin 对象,也就是说,如果1.js中修改了admin,2.js中的admin也会改变。

import.meta

import.meta 对象包含关于当前模块的信息。

它的内容取决于其所在的环境。在浏览器环境中,它包含当前脚本的 URL,或者如果它是在 HTML 中的话,则包含当前页面的 URL

在一个模块中,this时undefined

模块脚本默认总是延迟加载的

defer一样

  • 下载外部模块脚本 <script type="module" src="..."> 不会阻塞 HTML 的处理,它们会与其他资源并行加载。
  • 模块脚本会等到 HTML 文档完全准备就绪(即使它们很小并且比 HTML 加载速度更快),然后才会运行
  • 保持脚本的相对顺序:在文档中排在前面的脚本先执行
  • 模块脚本总是会“看到”已完全加载的 HTML 页面,包括在它下方的 HTML 元素。

跨域

模块脚本如果要从另一个源(域/协议/端口加载外部脚本,需要 CORS header。

<!-- another-site.com 必须提供 Access-Control-Allow-Origin -->
<!-- 否则,脚本将无法执行 -->
<script type="module" src="http://another-site.com/their.js"></script>

export/import

静态导入

命名的导出默认的导出
export class User {...}export default class User {...}
import {User} from ...import User from ...

import 命名的导出时需要花括号,而 import 默认的导出不需要花括号。

default 关键词被用于引用默认的导出。

export {sayHi as default};

“重新导出(Re-export)”语法 export ... from ... 允许导入内容,并立即将其导出

export {sayHi} from './say.js'; // 重新导出 sayHi

这是我们在本节和前面章节中介绍的所有 export 类型:

你可以阅读并回忆它们的含义来进行自查:

  • 在声明一个 class/function/… 之前:
    • export [default] class/function/variable ...
  • 独立的导出:
    • export {x [as y], ...}.
  • 重新导出:
    • export {x [as y], ...} from "module"
    • export * from "module"(不会重新导出默认的导出)。
    • export {default [as y]} from "module"(重新导出默认的导出)。

导入:

  • 导入命名的导出:
    • import {x [as y], ...} from "module"
  • 导入默认的导出:
    • import x from "module"
    • import {default as x} from "module"
  • 导入所有:
    • import * as obj from "module"
  • 导入模块(其代码,并运行),但不要将其任何导出赋值给变量:
    • import "module"

我们把 import/export 语句放在脚本的顶部或底部,都没关系。

因此,从技术上讲,下面这样的代码没有问题:

sayHi();

// ...

import {sayHi} from './say.js'; // 在文件底部导入

在实际开发中,导入通常位于文件的开头,但是这只是为了更加方便。

请注意在 {...} 中的 import/export 语句无效。

像这样的有条件的导入是无效的:

if (something) {
  import {sayHi} from "./say.js"; // Error: import must be at top level
}

动态导入

import(module) 表达式加载模块并返回一个 promise,该 promise resolve 为一个包含其所有导出的模块对象。

Proxy和Reflect

Proxy

一个 Proxy 对象包装另一个对象并拦截诸如读取/写入属性和其他操作.

Proxy 是对象的包装器,将代理上的操作转发到对象,并可以选择捕获其中一些操作。

语法:

let proxy = new Proxy(target, handler)
  • target —— 是要包装的对象,可以是任何东西,包括函数。
  • handler —— 代理配置:带有“捕捉器”(“traps”,即拦截操作的方法)的对象。比如 get 捕捉器用于读取 target 的属性,set 捕捉器用于写入 target 的属性,等等。

handler方法中的参数:

  • get(target, property, receiver)
    • target —— 是目标对象,该对象被作为第一个参数传递给 new Proxy
    • property —— 目标属性名,
    • receiver —— 如果目标属性是一个 getter 访问器属性,则 receiver 就是本次读取属性所在的 this 对象。通常,这就是 proxy 对象本身
  • set(target, property, value, receiver)
    • target —— 是目标对象,该对象被作为第一个参数传递给 new Proxy
    • property —— 目标属性名称,
    • value —— 目标属性的值,
    • receiver —— 与 get 捕捉器类似,仅与 setter 访问器属性相关。
  • has(target, property)
    • target —— 是目标对象,被作为第一个参数传递给 new Proxy
    • property —— 属性名称。
  • apply(target, thisArg, args) 捕捉器能使代理以函数的方式被调用:
    • target 是目标对象(在 JavaScript 中,函数就是一个对象),
    • thisArgthis 的值。
    • args 是参数列表。
内部方法Handler 方法何时触发
[[Get]]get(target, property, receiver)读取属性
[[Set]]set写入属性
[[HasProperty]]hasin 运算符
[[Delete]]deletePropertydelete 操作
[[Call]]apply(target, thisArg, args)proxy 对象作为函数被调用
[[Construct]]constructnew 操作
[[GetPrototypeOf]]getPrototypeOfObject.getPrototypeOf
[[SetPrototypeOf]]setPrototypeOfObject.setPrototypeOf
[[IsExtensible]]isExtensibleObject.isExtensible
[[PreventExtensions]]preventExtensionsObject.preventExtensions
[[DefineOwnProperty]]definePropertyObject.defineProperty, Object.defineProperties
[[GetOwnProperty]]getOwnPropertyDescriptorObject.getOwnPropertyDescriptor, for..in, Object.keys/values/entries
[[OwnPropertyKeys]]ownKeysObject.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object/keys/values/entries

对象的严格相等性检查 === 无法被拦截。

Reflect

Reflect 是一个内建对象,可简化 Proxy 的创建。

操作Reflect 调用内部方法
obj[prop]Reflect.get(obj, prop)[[Get]]
obj[prop] = valueReflect.set(obj, prop, value)[[Set]]
delete obj[prop]Reflect.deleteProperty(obj, prop)[[Delete]]
new F(value)Reflect.construct(F, value)[[Construct]]

例如:

let user = {};

Reflect.set(user, 'name', 'John');

alert(user.name); // John

撤销Proxy

Proxy.revocable返回一个带有 proxyrevoke 函数的对象以将其禁用

let object = {
  data: "Valuable data"
};

let {proxy, revoke} = Proxy.revocable(object, {});

// 将 proxy 传递到其他某处,而不是对象...
alert(proxy.data); // Valuable data

// 稍后,在我们的代码中
revoke();

// proxy 不再工作(revoked)
alert(proxy.data); // Error

revoke() 的调用会从代理中删除对目标对象的所有内部引用,因此它们之间再无连接。

柯里化

它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)

function curry(f) { // curry(f) 执行柯里化转换
  return function(a) {
    return function(b) {
      return f(a, b);
    };
  };
}

// 用法
function sum(a, b) {
  return a + b;
}

let curriedSum = curry(sum);

alert( curriedSum(1)(2) ); // 3

高级柯里化

  1. 如果传入的 args 长度与原始函数所定义的(func.length)相同或者更长,那么只需要使用 func.apply 将调用传递给它即可。

  2. 否则,获取一个部分应用函数:我们目前还没调用 func。取而代之的是,返回另一个包装器 pass,它将重新应用 curried,将之前传入的参数与新的参数一起传入。

function curry(func) {

  return function curried(...args) {
    if (args.length >= func.length) {
      return func.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  };

}

用例:

function sum(a, b, c) {
  return a + b + c;
}

let curriedSum = curry(sum);

alert( curriedSum(1, 2, 3) ); // 6,仍然可以被正常调用
alert( curriedSum(1)(2,3) ); // 6,对第一个参数的柯里化
alert( curriedSum(1)(2)(3) ); // 6,全柯里化

Refernce Type

Reference Type 是 ECMA 中的一个“规范类型”。我们不能直接使用它,但它被用在 JavaScript 语言内部。

Reference Type 的值是一个三个值的组合 (base, name, strict),其中:

  • base 是对象。
  • name 是属性名。
  • strictuse strict 模式下为 true。

一个动态执行的方法调用可能会丢失 this

例如:

let user = {
  name: "John",
  hi() { alert(this.name); },
  bye() { alert("Bye"); }
};

user.hi(); // 正常运行

// 现在让我们基于 name 来选择调用 user.hi 或 user.bye
(user.name == "John" ? user.hi : user.bye)(); // Error!

在最后一行有个在 user.hiuser.bye 中做选择的条件(三元)运算符。当前情形下的结果是 user.hi。接着该方法被通过 () 立刻调用。但是并不能正常工作!

因为在该调用中 "this" 的值变成了 undefined

obj.method() 语句中的两个操作:

  1. 首先,点 '.' 取了属性 obj.method 的值。
  2. 接着 () 执行了它。

对属性 user.hi 访问的结果不是一个函数,而是一个 Reference Type 的值。对于 user.hi,在严格模式下是:

// Reference Type 的值
(user, "hi", true)

() 被在 Reference Type 上调用时,它们会接收到关于对象和对象的方法的完整信息,然后可以设置正确的 this(在此处 =user)。

Reference Type 是一个特殊的“中间人”内部类型,目的是从 . 传递信息给 () 调用。

任何例如赋值 hi = user.hi 等其他的操作,都会将 Reference Type 作为一个整体丢弃掉,而会取 user.hi(一个函数)的值并继续传递。所以任何后续操作都“丢失”了 this

Reference Type 是语言内部的一个类型。

读取一个属性,例如在 obj.method() 中,. 返回的准确来说不是属性的值,而是一个特殊的 “Reference Type” 值,其中储存着属性的值和它的来源对象。

这是为了随后的方法调用 () 获取来源对象,然后将 this 设为它。

对于所有其它操作,Reference Type 会自动变成属性的值(在我们这个情况下是一个函数)。

这整个机制对我们是不可见的。它仅在一些微妙的情况下才重要,例如使用表达式从对象动态地获取一个方法时。

BigInt

BigInt 是一种特殊的数字类型,它提供了对任意长度整数的支持。

创建 bigint 的方式有两种:在一个整数字面量后面加 n 或者调用 BigInt 函数,该函数从字符串、数字等中生成 bigint。

const bigint = 1234567890123456789012345678901234567890n;

const sameBigint = BigInt("1234567890123456789012345678901234567890");

const bigintFromNumber = BigInt(10); // 与 10n 相同

Polyfilling Bigint 比较棘手。,建议我们在写代码时使用 JSBI 替代原生的 bigint

对于那些支持 bigint 的浏览器,可以使用 polyfill(Babel 插件)将 JSBI 调用转换为原生的 bigint。

DOM

给定一个 DOM 节点,我们可以使用导航(navigation)属性访问其直接的邻居。

这些属性主要分为两组:

  • 对于所有节点:parentNodechildNodesfirstChildlastChildpreviousSiblingnextSibling
  • 仅对于元素节点:parentElementchildrenfirstElementChildlastElementChildpreviousElementSiblingnextElementSibling

Table

元素支持 (除了上面给出的,之外) 以下属性:
  • table.rows —— <tr> 元素的集合。
  • table.caption/tHead/tFoot —— 引用元素 <caption><thead><tfoot>
  • table.tBodies —— <tbody> 元素的集合(根据标准还有很多元素,但是这里至少会有一个 —— 即使没有被写在 HTML 源文件中,浏览器也会将其放入 DOM 中)。

<thead><tfoot><tbody> 元素提供了 rows 属性:

  • tbody.rows —— 表格内部 <tr> 元素的集合。

<tr>

  • tr.cells —— 在给定 <tr> 中的 <td><th> 单元格的集合。
  • tr.sectionRowIndex —— 给定的 <tr> 在封闭的 <thead>/<tbody>/<tfoot> 中的位置(索引)。
  • tr.rowIndex —— 在整个表格中 <tr> 的编号(包括表格的所有行)。

<td><th>

  • td.cellIndex —— 在封闭的 <tr> 中单元格的编号

有 6 种主要的方法,可以在 DOM 中搜索元素节点

方法名搜索方式可以在元素上调用?实时的?
querySelectorCSS-selector-
querySelectorAllCSS-selector-
getElementByIdid--
getElementsByNamename-
getElementsByTagNametag or '*'
getElementsByClassNameclass

目前为止,最常用的是 querySelectorquerySelectorAll,但是 getElement(s)By* 可能会偶尔有用,或者可以在旧脚本中找到。

此外:

  • elem.matches(css) 用于检查 elem 与给定的 CSS 选择器是否匹配。
  • elem.closest(css) 用于查找与给定 CSS 选择器相匹配的最近的祖先。elem 本身也算。

让我们在这里提一下另一种用来检查子级与父级之间关系的方法,因为它有时很有用:

  • 如果 elemBelemA 内(elemA 的后代)或者 elemA==elemBelemA.contains(elemB) 将返回 true。

实时的集合

所有的 "getElementsBy*" 方法都会返回一个 实时的(live) 集合。这样的集合始终反映的是文档的当前状态,并且在文档发生更改时会“自动更新”。

相反,querySelectorAll 返回的是一个 静态的 集合。就像元素的固定数组。

在文档中出现新的 div 后,静态集合并没有增加。

节点属性

不同的 DOM 节点可能有不同的属性

注意:"innerHTML+="会完全重写该节点

其所做的不是追加内容,而是完全重写该节点:

跟下面一样:

移除了旧内容,再将 新内容+旧内容 赋值给innerHTML

elem.innerHTML = elem.innerHTML + "..."

如果 chatDiv 有许多其他文本和图片,那么就很容易看到重新加载,并且还会有其他副作用。

例如,如果现有的文本被用鼠标选中了,那么大多数浏览器都会在重写 innerHTML删除选定状态。如果这里有一个带有用户输入的文本的 <input>,那么这个被输入的文本将会被移除。

OuterHTML

outerHTML 属性包含了元素的完整 HTML。就像 innerHTML 加上元素本身一样。

alert(elem.outerHTML); // <div id="elem">Hello <b>World</b></div>

**与 innerHTML 不同,写入 outerHTML 不会改变元素。而是在 DOM 中替换它。**在 div.outerHTML=... 中发生的事情是:

  • div 被从文档(document)中移除。
  • 另一个 HTML 片段 <p>A new element</p> 被插入到其位置上。
  • div 仍拥有其旧的值。新的 HTML 没有被赋值给任何变量。

JS也可以读取注释

注释节点的data属性,获取注释的内容

textContent

假设我们有一个用户输入的任意字符串,我们希望将其显示出来。

  • 使用 innerHTML我们将其“作为 HTML”插入,带有所有 HTML 标签
  • 使用 textContent,我们将其“作为文本”插入,所有符号(symbol)均按字面意义处理。

总结

每个 DOM 节点都属于一个特定的类。这些类形成层次结构(hierarchy)。完整的属性和方法集是继承的结果。

主要的 DOM 节点属性有:

  • nodeType

    我们可以使用它来查看节点是文本节点还是元素节点。它具有一个数值型值(numeric value):1 表示元素,3 表示文本节点,其他一些则代表其他节点类型。只读。

  • nodeName/tagName

    用于元素名,标签名(除了 XML 模式,都要大写)。对于非元素节点,nodeName 描述了它是什么。只读。

  • innerHTML

    元素的 HTML 内容。可以被修改。

  • outerHTML

    元素的完整 HTML。对 elem.outerHTML 的写入操作不会触及 elem 本身。而是在外部上下文中将其替换为新的 HTML。

  • nodeValue/data

    非元素节点(文本、注释)的内容。两者几乎一样,我们通常使用 data。可以被修改。

  • textContent

    元素内的文本:HTML 减去所有 <tags>。写入文本会将文本放入元素内,所有特殊字符和标签均被视为文本。可以安全地插入用户生成的文本,并防止不必要的 HTML 插入。

  • hidden

    当被设置为 true 时,执行与 CSS display:none 相同的事。

DOM 节点还具有其他属性,具体有哪些属性则取决于它们的类。例如,<input> 元素(HTMLInputElement)支持 valuetype,而 <a> 元素(HTMLAnchorElement)则支持 href 等。大多数标准 HTML 特性(attribute)都具有相应的 DOM 属性。

属性和特性

对于元素节点,大多数标准的 HTML 特性(attributes)会自动变成 DOM 对象的属性(properties)

例如,如果标签是 <body id="page">,那么 DOM 对象就会有 body.id="page"

当一个元素有 id 或其他 标准的 特性,那么就会生成对应的 DOM 属性。但是非 标准的 特性则不会。通过ele.xxx就不能访问到你自定义的属性。

如果一个特性不是官方自带的,而是自定义的,那么就没有相对应的 DOM 属性

怎么访问自定义特性呢?:

所有特性都可以通过使用以下方法进行访问:

  • elem.hasAttribute(name) —— 检查特性是否存在。
  • elem.getAttribute(name) —— 获取这个特性值。
  • elem.setAttribute(name, value) —— 设置这个特性值。
  • elem.removeAttribute(name) —— 移除这个特性。
  • elem.attributes ——读取所有特性:属于内建 Attr 类的对象的集合,具有 namevalue 属性。

HTML 特性有以下几个特征:

  • 它们的名字是大小写不敏感的(idID 相同)。
  • 它们的总是字符串类型的。

如果标准的特性被更改,则对应的属性也会自动更新(有几个例外: input.value 只能从特性同步到属性,用处:用户行为可能会导致 input.value 的更改,然后在这些操作之后,如果我们想从 HTML 中恢复“原始”值,那么该值就在特性中)

所有以 “data-” 开头的特性均被保留供程序员使用。它们可在 dataset 属性中使用。

例如,如果一个 elem 有一个名为 "data-about" 的特性,那么可以通过 elem.dataset.about 取到它。像 data-order-state 这样的多词特性可以以驼峰式进行调用:dataset.orderState

联想Vue的data-xxxx

总结

  • 特性(attribute)—— 写在 HTML 中的内容。
  • 属性(property)—— DOM 对象中的内容。

简略的对比:

属性特性
类型任何值,标准的属性具有规范中描述的类型字符串
名字名字(name)是大小写敏感的名字(name)是大小写不敏感的

操作特性的方法:

  • elem.hasAttribute(name) —— 检查是否存在这个特性。
  • elem.getAttribute(name) —— 获取这个特性值。
  • elem.setAttribute(name, value) —— 设置这个特性值。
  • elem.removeAttribute(name) —— 移除这个特性。
  • elem.attributes —— 所有特性的集合。

在大多数情况下,最好使用 DOM 属性。仅当 DOM 属性无法满足开发需求,并且我们真的需要特性时,才使用特性,例如:

  • 我们需要一个非标准的特性。但是如果它以 data- 开头,那么我们应该使用 dataset
  • 我们想要读取 HTML 中“所写的”值。对应的 DOM 属性可能不同,例如 href 属性一直是一个 完整的 URL,但是我们想要的是“原始的”值。

修改文档

  • 创建新节点的方法:

    • document.createElement(tag) —— 用给定的标签创建一个元素节点,
    • document.createTextNode(value) —— 创建一个文本节点(很少使用),
    • elem.cloneNode(deep) —— 克隆元素,如果 deep==true 则与其后代一起克隆。
  • 插入和移除节点的方法:

    • node.append(...nodes or strings) —— 在 node 末尾插入,
    • node.prepend(...nodes or strings) —— 在 node 开头插入,
    • node.before(...nodes or strings) —— 在 node 之前插入,
    • node.after(...nodes or strings) —— 在 node 之后插入,
    • node.replaceWith(...nodes or strings) —— 替换 node
    • node.remove() —— 移除 node

    文本字符串被“作为文本”插入。

  • 这里还有“旧式”的方法:

    • parent.appendChild(node)
    • parent.insertBefore(node, nextSibling)
    • parent.removeChild(node)
    • parent.replaceChild(newElem, node)

    这些方法都返回 node

  • html 中给定一些 HTML,elem.insertAdjacentHTML(where, html) 会根据 where 的值来插入它:

    • "beforebegin" —— 将 html 插入到 elem 前面,
    • "afterbegin" —— 将 html 插入到 elem 的开头,
    • "beforeend" —— 将 html 插入到 elem 的末尾,
    • "afterend" —— 将 html 插入到 elem 后面。

另外,还有类似的方法,elem.insertAdjacentTextelem.insertAdjacentElement,它们会插入文本字符串和元素,但很少使用。

  • 要在页面加载完成之前将 HTML 附加到页面:

    • document.write(html)

    页面加载完成后,**调用write()**将会擦除现有文档。多见于旧脚本。

清除元素

建一个函数 clear(elem) 用来移除元素里的内容。

<ol id="elem">
  <li>Hello</li>
  <li>World</li>
</ol>

<script>
  function clear(elem) { /* 你的代码 */ }

  clear(elem); // 清除列表
</script>

解决方案

首先,让我们看看 错误 的做法:

function clear(elem) {
  for (let i=0; i < elem.childNodes.length; i++) {
      elem.childNodes[i].remove();
  }
}

这是行不通的,因为调用 remove() 会从首端开始移除 elem.childNodes 集合中的元素,因此,元素每次都从索引 0 开始。但是 i 在增加,所以元素就被跳过了。

for..of 循环的结果也跟上面一样。

正确的做法是:

function clear(elem) {
  while (elem.firstChild) {
    elem.firstChild.remove();
  }
}

还有一种更简单的方法,也可以达到我们所要的效果:

function clear(elem) {
  elem.innerHTML = '';
}

样式和类

要管理 class,有两个 DOM 属性:

  • className —— 字符串值,可以很好地管理整个类的集合。如果我们对 elem.className 进行赋值,它将替换类中的整个字符串。

  • classList —— 具有 add/remove/toggle/contains 方法的对象,可以很好地支持单个类。

    classList 的方法:

    • elem.classList.add/remove(class) —— 添加/移除类。
    • elem.classList.toggle(class) —— 如果类不存在就添加类,存在就移除它。
    • elem.classList.contains(class) —— 检查给定类,返回 true/false

要改变样式:

  • style 属性是具有驼峰(camelCased)样式的对象。对其进行读取和修改与修改 "style" 特性(attribute)中的各个属性具有相同的效果。要了解如何应用 important 和其他特殊内容 —— 在 MDN 中有一个方法列表。

  • style.cssText 属性对应于整个 "style" 特性(attribute),即完整的样式字符串。对style完全重写。他是替换整个样式,而不是追加。

      // 我们可以在这里设置特殊的样式标记,例如 "important"
      div.style.cssText=`color: red !important;
        background-color: yellow;
        width: 100px;
        text-align: center;
      `;
    
    

读取已解析(最终应用于元素的样式值)的样式(对于所有类,在应用所有 CSS 并计算最终值之后):

  • getComputedStyle(elem, [pseudo]) 返回与 style 对象类似的,且包含了所有类的对象。只读。

    • element

      需要被读取样式值的元素。

      pseudo

      伪元素(如果需要),例如 ::before。空字符串或无参数则意味着元素本身。

想要移除 样式属性,就像它没有被设置一样。这里不应该使用 delete elem.style.xxx,而应该使用 elem.style.xxxx= "" 将其赋值为空

还有一个特殊的方法 elem.style.removeProperty('color'),可以删除样式属性。

元素大小和滚动

offsetParent :是最接近的祖先(ancestor),在浏览器渲染期间,它被用于计算坐标。

最近的祖先为下列之一:

  1. CSS 定位的(positionabsoluterelativefixedsticky),
  2. <td><th><table>
  3. <body>

属性 offsetLeft/offsetTop 提供相对于 offsetParent 左上角的 x/y 坐标。

offsetWidth/Height

offsetWidth=padding*2+border*2+scrollbar(16px)

未显示的元素(display:none),offsetParentnull,并且 offsetWidthoffsetHeight0

clientTop/Left

是内侧相对于外侧的坐标。

上边框/左边框宽度相等

clientWidth/Height

包括了 “content width” 和 “padding”,但不包括滚动条宽度(scrollbar)

clientHeight = height+padding*2

clientWidth = content width+padding*2+scroll (无border)

scrollWidth/Height

scrollLeft/scrollTop

元素的隐藏、滚动部分的 width/height。

scrollTop 就是“已经滚动了多少”。

但是 scrollLeft/scrollTop 是可修改的,并且浏览器会滚动该元素

总结

元素具有以下几何属性:

  • offsetParent —— 是最接近的 CSS 定位的祖先,或者是 tdthtablebody
  • offsetLeft/offsetTop —— 是相对于 offsetParent 的左上角边缘的坐标。
  • offsetWidth/offsetHeight —— 元素的“外部” width/height,边框(border)尺寸计算在内。
  • clientLeft/clientTop —— 从元素左上角外角到左上角内角的距离。对于从左到右显示内容的操作系统来说,它们始终是左侧/顶部 border 的宽度。而对于从右到左显示内容的操作系统来说,垂直滚动条在左边,所以 clientLeft 也包括滚动条的宽度。
  • clientWidth/clientHeight —— 内容的 width/height,包括 padding,但不包括滚动条(scrollbar)。
  • scrollWidth/scrollHeight —— 内容的 width/height,就像 clientWidth/clientHeight 一样,但还包括元素的滚动出的不可见的部分。
  • scrollLeft/scrollTop —— 从元素的左上角开始,滚动出元素的上半部分的 width/height。

除了 scrollLeft/scrollTop 外,所有属性都是只读的。如果我们修改 scrollLeft/scrollTop,浏览器会滚动对应的元素。

window.pageXOffset/pageYOffset 中获取页面当前滚动信息:

  • window.pageXOffsetwindow.scrollX 的别名。
  • window.pageYOffsetwindow.scrollY 的别名。

总结

几何:

  • 文档可见部分的 width/height(内容区域的 width/height):document.documentElement.clientWidth/clientHeight

  • 整个文档的 width/height,其中包括滚动出去的部分:

    let scrollHeight = Math.max(
      document.body.scrollHeight, document.documentElement.scrollHeight,
      document.body.offsetHeight, document.documentElement.offsetHeight,
      document.body.clientHeight, document.documentElement.clientHeight
    );
    

滚动:

  • 读取当前的滚动:window.pageYOffset/pageXOffset
  • 更改当前的滚动:
    • window.scrollTo(pageX,pageY) —— 绝对坐标,
    • window.scrollBy(x,y) —— 相对当前位置进行滚动,
    • elem.scrollIntoView(top) —— 滚动以使 elem 可见(elem 与窗口的顶部/底部对齐)。

为了获取窗口(window)的宽度和高度,我们可以使用 document.documentElementclientWidth/clientHeight

window.innerWidth/innerHeight 包括了滚动条,的宽度。document.documentElement.clientWidth/clinetHeight 是减去了滚动条宽度后的窗口宽度

页面上的任何点都有坐标:

  1. 相对于窗口的坐标 —— elem.getBoundingClientRect()
  2. 相对于文档的坐标 —— elem.getBoundingClientRect() 加上当前页面滚动。

窗口坐标非常适合和 position:fixed 一起使用,文档坐标非常适合和 position:absolute 一起使用。

这两个坐标系统各有利弊。有时我们需要其中一个或另一个,就像 CSS positionabsolutefixed 一样。

浏览器事件

鼠标事件:

  • click —— 当鼠标点击一个元素时(触摸屏设备会在点击时生成)。
  • contextmenu —— 当鼠标右键点击一个元素时。
  • mouseover / mouseout —— 当鼠标指针移入/离开一个元素时。
  • mousedown / mouseup —— 当在元素上按下/释放鼠标按钮时。
  • mousemove —— 当鼠标移动时。

键盘事件

  • keydownkeyup —— 当按下和松开一个按键时。

表单(form)元素事件

  • submit —— 当访问者提交了一个 <form> 时。
  • focus —— 当访问者聚焦于一个元素时,例如聚焦于一个 <input>

Document 事件

  • DOMContentLoaded —— 当 HTML 的加载和处理均完成,DOM 被完全构建完成时。

CSS 事件

  • transitionend —— 当一个 CSS 动画完成时。

addEbentListener(envent,handler[,option])

event

事件名,例如:"click"

handler

处理程序。

options

具有以下属性的附加可选对象:

  • once:如果为 true,那么会在被触发后自动删除监听器。
  • capture:事件处理的阶段,我们稍后将在 冒泡和捕获 一章中介绍。由于历史原因,options 也可以是 false/true,它与 {capture: false/true} 相同。
  • passive:如果为 true,那么处理程序将不会调用 preventDefault(),我们稍后将在 浏览器默认行为 一章中介绍。

removeEventListener(),移除事件

当事件发生时,浏览器会创建一个 event 对象,作为参数传入handler:

event 对象的一些属性:

  • event.type

    事件类型,这里是 "click"

  • event.currentTarget

    处理事件的元素。这与 this 相同,除非处理程序是一个箭头函数,或者它的 this 被绑定到了其他东西上,之后我们就可以从 event.currentTarget 获取元素了。

  • event.clientX / event.clientY

    指针事件(pointer event)的指针的窗口相对坐标。

冒泡和捕获

冒泡(Bubble)

当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。

几乎所有事件都会冒泡。focus 事件不会冒泡

event.target

父元素上的处理程序始终可以获取事件实际发生位置的详细信息。

引发事件的那个嵌套层级最深的元素被称为目标元素,可以通过 event.target 访问。

this(=event.currentTarget)之间的区别:

  • event.target —— 是引发事件的“目标”元素,它在冒泡过程中不会发生变化。
  • this —— 是“当前”元素,其中有一个当前正在运行的处理程序。

停止冒泡

停止冒泡的方法是 event.stopPropagation()

如果一个元素一个事件上有多个handler函数,即使其中一个停止冒泡,其他handler函数仍会执行。

换句话说,event.stopPropagation() 停止向上移动,但是当前元素上的其他handler函数都会继续运行。

有一个 event.stopImmediatePropagation() 方法,可以用于停止冒泡,并阻止当前元素上的handler函数运行。使用该方法之后,其他handler函数就不会被执行

捕获(capture)

事件处理的另一个阶段被称为“捕获(capturing)”

DOM 事件标准描述了事件传播的 3 个阶段:

  1. 捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素。
  2. 目标阶段(Target phase)—— 事件到达目标元素。
  3. 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡。

点击 某个元素,事件首先通过祖先链向下到达元素(捕获阶段),然后到达目标(目标阶段),最后上升(冒泡阶段),在途中调用处理程序。

为了在捕获阶段捕获事件,我们需要将处理程序的 capture 选项设置为 true

ele.addEventListener(..., {capture: true})

总结

当一个事件发生时 —— 发生该事件的嵌套最深的元素被标记为“目标元素”(event.target)。

  • 然后,事件从文档根节点向下移动到 event.target,并在途中调用分配了 addEventListener(..., true) 的处理程序(true{capture: true} 的一个简写形式)。
  • 然后,在目标元素自身上调用处理程序。
  • 然后,事件从 event.target 冒泡到根,调用使用 on<event>、HTML 特性(attribute)和没有第三个参数的,或者第三个参数为 false/{capture:false}addEventListener 分配的处理程序。

每个处理程序都可以访问 event 对象的属性:

  • event.target —— 引发事件的层级最深的元素。
  • event.currentTarget(=this)—— 处理事件的当前元素(具有处理程序的元素)
  • event.eventPhase —— 当前阶段(capturing=1,target=2,bubbling=3)。

任何事件处理程序都可以通过调用 event.stopPropagation() 来停止事件,但不建议这样做,因为我们不确定是否确实不需要冒泡上来的事件,也许是用于完全不同的事情。

捕获阶段很少使用,通常我们会在冒泡时处理事件。这背后有一个逻辑。

事件委托

捕获和冒泡允许我们实现最强大的事件处理模式之一,即 事件委托 模式。

这个想法是,如果我们有许多以类似方式处理的元素,那么就不必为每个元素分配一个处理程序 —— 而是将单个处理程序放在它们的共同祖先上。

事件委托 (javascript.info)

建议好好看事件委托和行为模式的示例。

浏览器默认行为

许多事件会自动触发浏览器执行某些行为。

例如:

  • 点击一个链接 —— 触发导航(navigation)到该 URL。
  • 点击表单的提交按钮 —— 触发提交到服务器的行为。
  • 在文本上按下鼠标按钮并移动 —— 选中文本。

有两种方式来告诉浏览器我们不希望它执行默认行为:

  • 主流的方式是使用 event 对象。有一个 event.preventDefault() 方法。
  • 如果处理程序是使用 on<event>(而不是 addEventListener)分配的,那返回 false 也同样有效。

某些事件会相互转化。如果我们阻止了第一个事件,那就没有第二个事件了。

例如,在 <input> 字段上的 mousedown 会导致在其中获得焦点,以及 focus 事件。如果我们阻止 mousedown 事件,在这就没有焦点了。

addEventListener 的可选项 passive: true 向浏览器发出信号,表明处理程序将不会调用 preventDefault()

自定义事件

事件构造器

像这样创建 Event 对象:

let event = new Event(type[, options]);

参数:

  • type —— 事件类型,可以是像这样 "click" 的字符串,或者我们自己的像这样 "my-event" 的参数。

  • options —— 具有两个可选属性的对象:

    • bubbles: true/false —— 如果为 true,那么事件会冒泡。
    • cancelable: true/false —— 如果为 true,那么“默认行为”就会被阻止。稍后我们会看到对于自定义事件,它意味着什么。

    默认情况下,以上两者都为 false:{bubbles: false, cancelable: false}

规范中提供了不同 UI 事件的属性的完整列表UI Events (w3.org)

dispatchEvent

事件对象被创建后,我们应该使用 elem.dispatchEvent(event) 调用在元素上“运行”它。

自定义事件

我们应该使用 new CustomEvent

new CustomEvent参数一样,不过多了个detail: object参数,用于传递自定义属性或信息。

事件中的事件是同步的

如果:浏览器正在处理 onclick,这时发生了一个新的事件,例如鼠标移动了,那么它的处理程序会被排入队列,相应的 mousemove 处理程序将在 onclick 事件处理完成后被调用

但:

一个事件是在另一个事件中发起的。例如click事件中调用了 dispatchEvent。这类事件将会被立即处理,即在新的事件处理程序被调用之后,恢复到当前的事件处理程序。

总结

要从代码生成一个事件,我们首先需要创建一个事件对象。

通用的 Event(name, options) 构造器接受任意事件名称和具有两个属性的 options 对象:

  • 如果事件应该冒泡,则 bubbles: true
  • 如果 event.preventDefault() 应该有效,则 cancelable: true

其他像 MouseEventKeyboardEvent 这样的原生事件的构造器,都接受特定于该事件类型的属性。例如,鼠标事件的 clientX

对于自定义事件,我们应该使用 CustomEvent 构造器。它有一个名为 detail 的附加选项,我们应该将事件特定的数据分配给它。然后,所有处理程序可以以 event.detail 的形式来访问它。

尽管技术上可以生成像 clickkeydown 这样的浏览器事件,但我们还是应谨慎使用它们。

我们不应该生成浏览器事件,因为这是运行处理程序的一种怪异(hacky)方式。大多数时候,这都是糟糕的架构。

可以生成原生事件:

  • 如果第三方程序库不提供其他交互方式,那么这是使第三方程序库工作所需的一种肮脏手段。
  • 对于自动化测试,要在脚本中“点击按钮”并查看接口是否正确响应。

使用我们自己的名称的自定义事件通常是出于架构的目的而创建的,以指示发生在菜单(menu),滑块(slider),轮播(carousel)等内部发生了什么。

鼠标事件类型

我们已经见过了其中一些事件:

  • mousedown/mouseup

    在元素上点击/释放鼠标按钮。

  • mouseover/mouseout

    鼠标指针从一个元素上移入/移出。

  • mousemove

    鼠标在元素上的每个移动都会触发此事件。

  • click

    如果使用的是鼠标左键,则在同一个元素上的 mousedownmouseup 相继触发后,触发该事件。

  • dblclick

    在短时间内双击同一元素后触发。如今已经很少使用了。

  • contextmenu

    在鼠标右键被按下时触发。还有其他打开上下文菜单的方式,例如使用特殊的键盘按键,在这种情况下它也会被触发,因此它并不完全是鼠标事件。

总结:

鼠标事件有以下属性:

  • 按钮:button
  • 组合键(如果被按下则为 true):altKeyctrlKeyshiftKeymetaKey(Mac)。
    • 如果你想处理 Ctrl,那么不要忘记 Mac 用户,他们通常使用的是 Cmd,所以最好检查 if (e.metaKey || e.ctrlKey)
  • 窗口相对坐标:clientX/clientY
  • 文档相对坐标:pageX/pageY

mousedown 的默认浏览器操作是文本选择,如果它对界面不利,则应避免它。

鼠标拖放事件

基础的拖放算法如下所示:

  1. mousedown 上 —— 根据需要准备要移动的元素(也许创建一个它的副本,向其中添加一个类或其他任何东西)。
  2. 然后在 mousemove 上,通过更改 position:absolute 情况下的 left/top 来移动它。
  3. mouseup 上 —— 执行与完成的拖放相关的所有行为。

总结

我们考虑了一种基础的拖放算法。

关键部分:

  1. 事件流:ball.mousedowndocument.mousemoveball.mouseup(不要忘记取消原生 ondragstart)。
  2. 在拖动开始时 —— 记住鼠标指针相对于元素的初始偏移(shift):shiftX/shiftY,并在拖动过程中保持它不变。
  3. 使用 document.elementFromPoint 检测鼠标指针下的 “droppable” 的元素。

我们可以在此基础上做很多事情。

  • mouseup 上,我们可以智能地完成放置(drop):更改数据,移动元素。
  • 我们可以高亮我们正在“飞过”的元素。
  • 我们可以将拖动限制在特定的区域或者方向。
  • 我们可以对 mousedown/up 使用事件委托。一个大范围的用于检查 event.target 的事件处理程序可以管理数百个元素的拖放。

我们不能通过在 onscroll 监听器中使用 event.preventDefault() 来阻止滚动,因为它会在滚动发生 之后 才触发。

但是我们可以在导致滚动的事件上,例如在 pageUp 和 pageDown 的 keydown 事件上,使用 event.preventDefault() 来阻止滚动。

表单控件

表单属性和方法

文档中的表单是特殊集合 document.forms 的成员。

document.forms.my; // name="my" 的表单
document.forms[0]; // 文档中的第一个表单

任何元素都可以通过命名的集合 form.elements 来获取到。

 // 获取表单
  let form = document.forms.my; // <form name="my"> 元素

  // 获取表单中的元素
  let elem = form.elements.one; // <input name="one"> 元素

如果有多个同name元素,form.elements[name] 将会是一个集合

反向引用

通过 element.form 访问到其form

表单元素

input 和 textarea

我们可以通过 input.value(字符串)或 input.checked(布尔值)来访问复选框(checkbox)和单选按钮(radio button)中的 value

select 和 option

一个 <select> 元素有 3 个重要的属性:

  1. select.options —— <option> 的子元素的集合,
  2. select.value —— 当前所选择的 <option>value
  3. select.selectedIndex —— 当前所选择的 <option>编号

<select> 具有 multiple 特性(attribute):允许多选

<option> 元素具有以下属性:

  • option.selected

    <option> 是否被选择。

  • option.index

    <option> 在其所属的 <select> 中的编号。

  • option.text

    <option> 的文本内容(可以被访问者看到)。

new Option

用于创建一个 <option> 元素

option = new Option(text, value, defaultSelected, selected);
  • text —— <option> 中的文本,
  • value —— <option>value
  • defaultSelected —— 如果为 true,那么 selected HTML-特性(attribute)就会被创建,
  • selected —— 如果为 true,那么这个 <option> 就会被选中。

总结

表单导航:

  • document.forms

    一个表单元素可以通过 document.forms[name/index] 访问到。

  • form.elements

    表单元素可以通过 form.elements[name/index] 的方式访问,或者也可以使用 form[name/index]elements 属性也适用于 <fieldset>

  • element.form

    元素通过 form 属性来引用它们所属的表单。

value 可以被通过 input.valuetextarea.valueselect.value 等来获取到。(对于单选按钮(radio button)和复选框(checkbox),可以使用 input.checked 来确定是否选择了一个值。

对于 <select>,我们可以通过索引 select.selectedIndex 来获取它的 value,也可以通过 <option> 集合 select.options 来获取它的 value

focus/blur

聚焦:focus/blur (javascript.info)

focusblur 事件不会向上冒泡。但会在捕获阶段向下传播

以使用 focusinfocusout 事件 —— 与 focus/blur 事件完全一样,只是它们会冒泡。值得注意的是,必须使用 elem.addEventListener 来分配它们,而不是 on<event>

大多数元素默认不支持聚焦。使用 tabindex 可以使任何元素变成可聚焦的。

可以通过 document.activeElement 来获取当前所聚焦的元素。

事件:change,input,cut,copy,paste

change

当元素更改完成时,将触发 change 事件。

对于文本输入框,当其失去焦点时,就会触发 change 事件。

selectinput type=checkbox/radio,会在选项更改后立即触发 change 事件。

input

每当用户对输入值进行修改后,就会触发 input 事件.

我们无法使用 event.preventDefault() 阻止input事件

cut,copy,paste

它们属于 ClipboardEvent 类,并提供了对剪切/拷贝/粘贴的数据的访问方法。

可以使用 event.preventDefault() 来中止行为,然后什么都不会被复制/粘贴。

使用 document.getSelection() 来得到被选中的文本

我们不仅可以复制/粘贴文本,也可以复制/粘贴其他各种内容。例如,我们可以在操作系统的文件管理器中复制一个文件并进行粘贴。

这是因为 clipboardData 实现了 DataTransfer 接口,通常用于拖放和复制/粘贴。这超出了本文所讨论的范围,但你可以在 DataTransfer 规范 中进行详细了解。

可以访问剪切板的异步 API:navigator.clipboard

总结:

事件描述特点
change值被改变。对于文本输入,当失去焦点时触发。
input文本输入的每次更改。立即触发,与 change 不同。
cut/copy/paste剪贴/拷贝/粘贴行为。行为可以被阻止。event.clipboardData 属性可以用于访问剪贴板。除了火狐(Firefox)之外的浏览器都支持 navigator.clipboard

表单:事件和方法提交

提交表单主要有两种方式:

  1. 第一种 —— 点击 <input type="submit"><input type="image">
  2. 第二种 —— 在 input 字段中按下 Enter 键。

加载文档和其他资源

页面生命周期:DOMContentLoaded,load,beforeunload,unload

HTML 页面的生命周期包含三个重要事件:

  • DOMContentLoaded —— 浏览器已完全加载 HTML,并构建了 DOM 树,但像 <img> 和样式表之类的外部资源可能尚未加载完成。
  • load —— 浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。
  • beforeunload/unload —— 当用户正在离开页面时。

每个事件都是有用的:

  • DOMContentLoaded 事件 —— DOM 已经就绪,因此处理程序可以查找 DOM 节点,并初始化接口。
  • load 事件 —— 外部资源已加载完成,样式已被应用,图片大小也已知了。
  • beforeunload 事件 —— 用户正在离开:我们可以检查用户是否保存了更改,并询问他是否真的要离开。
  • unload 事件 —— 用户几乎已经离开了,但是我们仍然可以启动一些操作,例如发送统计数据。

DOMContentLoaded 和脚本

当浏览器处理一个 HTML 文档,并在文档中遇到 <script> 标签时,就会在继续构建 DOM 之前运行它。这是一种防范措施,因为脚本可能想要修改 DOM,甚至对其执行 document.write 操作,所以 DOMContentLoaded 必须等待脚本执行结束

此规则有两个例外:

  1. 具有 async 特性(attribute)的脚本不会阻塞 DOMContentLoaded
  2. 使用 document.createElement('script') 动态生成并添加到网页的脚本也不会阻塞 DOMContentLoaded

外部样式表不会影响 DOM,因此 DOMContentLoaded 不会等待它们。但当script要获取样式时,DOMContentLoaded也会等样式加载好。

window.onload

当整个页面,包括样式、图片和其他资源被加载完成时,会触发 window 对象上的 load 事件。可以通过 onload 属性获取此事件。

readyState

document.readyState 属性可以为我们提供当前加载状态的信息。

它有 3 个可能值:

  • loading —— 文档正在被加载。
  • interactive —— 文档被全部读取。
  • complete —— 文档被全部读取,并且所有资源(例如图片等)都已加载完成。

总结

页面生命周期事件:

  • 当 DOM 准备就绪时,

    document上的DOMContentLoaded事件就会被触发。在这个阶段,我们可以将 JavaScript 应用于元素。

    • 诸如 <script>...</script><script src="..."></script> 之类的脚本会阻塞 DOMContentLoaded,浏览器将等待它们执行结束。
    • 图片和其他资源仍然可以继续被加载。
  • 当页面和所有资源都加载完成时,window 上的 load 事件就会被触发。我们很少使用它,因为通常无需等待那么长时间。

  • 当用户想要离开页面时,window 上的 beforeunload 事件就会被触发。如果我们取消这个事件,浏览器就会询问我们是否真的要离开(例如,我们有未保存的更改)。

  • 当用户最终离开时,window 上的 unload 事件就会被触发。在处理程序中,我们只能执行不涉及延迟或询问用户的简单操作。正是由于这个限制,它很少被使用。我们可以使用 navigator.sendBeacon 来发送网络请求。

  • document.readyState
    

    是文档的当前状态,可以在

    readystatechange
    

    事件中跟踪状态更改:

    • loading —— 文档正在被加载。
    • interactive —— 文档已被解析完成,与 DOMContentLoaded 几乎同时发生,但是在 DOMContentLoaded 之前发生。
    • complete —— 文档和资源均已加载完成,与 window.onload 几乎同时发生,但是在 window.onload 之前发生。

脚本:async,defer

defer 特性告诉浏览器不要等待脚本。相反,浏览器将继续处理 HTML,构建 DOM。脚本会“在后台”下载,然后等 DOM 构建完成后,脚本才会执行。

  • 具有 defer 特性的脚本不会阻塞页面。
  • 具有 defer 特性的脚本总是要等到 DOM 解析完毕,但在 DOMContentLoaded 事件之前执行。

具有 defer 特性的脚本保持其相对顺序,就像常规脚本一样。

defer 特性仅适用于外部脚本,如果 <script> 脚本没有 src,则会忽略 defer 特性。

async 特性意味着脚本是完全独立的:

  • 浏览器不会因 async 脚本而阻塞(与 defer 类似)。
  • 其他脚本不会等待 async 脚本加载完成,同样,async 脚本也不会等待其他脚本。
  • DOMContentLoaded和异步脚本不会彼此等待:
    • DOMContentLoaded 可能会发生在异步脚本之前(如果异步脚本在页面完成后才加载完成)
    • DOMContentLoaded 也可能发生在异步脚本之后(如果异步脚本很短,或者是从 HTTP 缓存中加载的)

async 脚本会在后台加载,并在加载就绪时运行。DOM 和其他脚本不会等待它们,它们也不会等待其它的东西。async 脚本就是一个会在加载完成时执行的完全独立的脚本。就这么简单

JS动态添加脚本:

let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)

当脚本被附加到文档 (*) 时,脚本就会立即开始加载。

默认情况下,动态脚本的行为是“异步”的。

可以显式地设置了 script.async=false

总结

asyncdefer 有一个共同点:加载这样的脚本都不会阻塞页面的渲染。因此,用户可以立即阅读并了解页面内容。

但是,它们之间也存在一些本质的区别:

顺序DOMContentLoaded
async加载优先顺序。脚本在文档中的顺序不重要 —— 先加载完成的先执行不相关。可能在文档加载完成前加载并执行完毕。如果脚本很小或者来自于缓存,同时文档足够长,就会发生这种情况。
defer文档顺序(它们在文档中的顺序)在文档加载和解析完成之后(如果需要,则会等待),即在 DOMContentLoaded 之前执行。

script.onload

load 事件,会在脚本加载并执行完成时触发。

脚本加载期间error 会被 error 事件跟踪到。

跨域访问

要允许跨源访问,<script> 标签需要具有 crossorigin 特性(attribute),并且远程服务器必须提供特殊的 header。

这里有三个级别的跨源访问:

  1. crossorigin 特性 —— 禁止访问。
  2. crossorigin="anonymous" —— 如果服务器的响应带有包含 * 或我们的源(origin)的 header Access-Control-Allow-Origin,则允许访问。浏览器不会将授权信息和 cookie 发送到远程服务器。
  3. crossorigin="use-credentials" —— 如果服务器发送回带有我们的源的 header Access-Control-Allow-OriginAccess-Control-Allow-Credentials: true,则允许访问。浏览器会将授权信息和 cookie 发送到远程服务器。

DOM 变动观察器(Mutation observer)

MutationObserver 使用简单。

首先,我们创建一个带有回调函数的观察器:

let observer = new MutationObserver(callback);

然后将其附加到一个 DOM 节点:

observer.observe(node, config);

config 是一个具有布尔选项的对象,该布尔选项表示“将对哪些更改做出反应”:

  • childList —— node 的直接子节点的更改,
  • subtree —— node 的所有后代的更改,
  • attributes —— node 的特性(attribute),
  • attributeFilter —— 特性名称数组,只观察选定的特性。
  • characterData —— 是否观察 node.data(文本内容),

其他几个选项:

  • attributeOldValue —— 如果为 true,则将特性的旧值和新值都传递给回调(参见下文),否则只传新值(需要 attributes 选项),
  • characterDataOldValue —— 如果为 true,则将 node.data 的旧值和新值都传递给回调(参见下文),否则只传新值(需要 characterData 选项)。

示例:

<div contentEditable id="elem">Click and <b>edit</b>, please</div>

<script>
let observer = new MutationObserver(mutationRecords => {
  console.log(mutationRecords); // console.log(the changes)
});

// 观察除了特性之外的所有变动
observer.observe(elem, {
  childList: true, // 观察直接子节点
  subtree: true, // 及其更低的后代节点
  characterDataOldValue: true // 将旧的数据传递给回调
});
</script>

MutationRecord 对象具有以下属性:

  • type—— 变动类型,以下类型之一:
    • "attributes":特性被修改了,
    • "characterData":数据被修改了,用于文本节点,
    • "childList":添加/删除了子元素。
  • target —— 更改发生在何处:"attributes" 所在的元素,或 "characterData" 所在的文本节点,或 "childList" 变动所在的元素,
  • addedNodes/removedNodes —— 添加/删除的节点,
  • previousSibling/nextSibling —— 添加/删除的节点的上一个/下一个兄弟节点,
  • attributeName/attributeNamespace —— 被更改的特性的名称/命名空间(用于 XML),
  • oldValue —— 之前的值,仅适用于特性或文本更改,如果设置了相应选项 attributeOldValue/characterDataOldValue

有一个方法可以停止观察节点:

  • observer.disconnect() —— 停止观察。

当我们停止观察时,观察器可能尚未处理某些更改。在种情况下,我们使用:

  • observer.takeRecords() —— 获取尚未处理的变动记录列表,表中记录的是已经发生,但回调暂未处理的变动。

事件循环

每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染,或进行其他任何操作。

微任务:微任务会在执行任何其他事件处理,或渲染,或执行任何其他宏任务之前完成,因为可以把执行当前脚本当成一个宏任务。

  • promise 的处理程序 .then.catch.finally 都是异步的。
  • queueMicrotask(func),它对 func 进行排队,以在微任务队列中执行。

对于不应该阻塞事件循环的耗时长的繁重计算任务,我们可以使用 Web Workers

这是在另一个并行线程中运行代码的方式。

Web Workers 可以与主线程交换消息,但是它们具有自己的变量和事件循环。

Web Workers 没有访问 DOM 的权限,因此,它们对于同时使用多个 CPU 内核的计算非常有用。

iFrame

如果窗口二级域名相同,例如 john.site.competer.site.comsite.com(它们共同的二级域是 site.com),我们可以使浏览器忽略该差异,使得它们可以被作为“同源”的来对待,以便进行跨窗口通信。

为了做到这一点,每个这样的窗口都应该执行下面这行代码:

document.domain = 'site.com';

跨窗口通信

postMessage 接口允许窗口之间相互通信,无论它们来自什么源。

因此,这是解决“同源”策略的方式之一。它允许来自于 john-smith.com 的窗口与来自于 gmail.com 的窗口进行通信,并交换信息,但前提是它们双方必须均同意并调用相应的 JavaScript 函数。这可以保护用户的安全。

这个接口有两个部分。

1.postMessage

想要发送消息的窗口需要调用接收窗口的 postMessage 方法。换句话说,如果我们想把消息发送给 win,我们应该调用 win.postMessage(data, targetOrigin)

参数:

  • data

    要发送的数据。可以是任何对象,数据会被通过使用“结构化序列化算法(structured serialization algorithm)”进行克隆。IE 浏览器只支持字符串,因此我们需要对复杂的对象调用 JSON.stringify 方法进行处理,以支持该浏览器。

  • targetOrigin

    指定目标窗口的源,以便只有来自给定的源的窗口才能获得该消息。

targetOrigin 是一种安全措施。请记住,如果目标窗口是非同源的,我们无法在发送方窗口读取它的 location。因此,我们无法确定当前在预期的窗口中打开的是哪个网站:用户随时可以导航离开,并且发送方窗口对此一无所知。

2.onmessage

为了接收消息,目标窗口应该在 message 事件上有一个处理程序。当 postMessage 被调用时触发该事件(并且 targetOrigin 检查成功)。

event 对象具有特殊属性:

  • data

    postMessage 传递来的数据。

  • origin

    发送方的源,例如 http://javascript.info

  • source

    对发送方窗口的引用。如果我们想,我们可以立即 source.postMessage(...) 回去。

要为 message 事件分配处理程序,我们应该使用 addEventListener,简短的语法 window.onmessage 不起作用。

总结

要调用另一个窗口的方法或者访问另一个窗口的内容,我们应该首先拥有对其的引用。

对于弹窗,我们有两个引用:

  • 从打开窗口的(opener)窗口:window.open —— 打开一个新的窗口,并返回对它的引用,
  • 从弹窗:window.opener —— 是从弹窗中对打开此弹窗的窗口(opener)的引用。

对于 iframe,我们可以使用以下方式访问父/子窗口:

  • window.frames —— 一个嵌套的 window 对象的集合,
  • window.parentwindow.top 是对父窗口和顶级窗口的引用,
  • iframe.contentWindow<iframe> 标签内的 window 对象。

如果几个窗口的源相同(域,端口,协议),那么这几个窗口可以彼此进行所需的操作。

否则,只能进行以下操作:

  • 更改另一个窗口的 location(只能写入)。
  • 向其发送一条消息。

例外情况:

  • 对于二级域相同的窗口:a.site.comb.site.com。通过在这些窗口中均设置 document.domain='site.com',可以使它们处于“同源”状态。
  • 如果一个 iframe 具有 sandbox 特性(attribute),则它会被强制处于“非同源”状态,除非在其特性值中指定了 allow-same-origin。这可用于在同一网站的 iframe 中运行不受信任的代码。

postMessage 接口允许两个具有任何源的窗口之间进行通信:

  1. 发送方调用 targetWin.postMessage(data, targetOrigin)

  2. 如果 targetOrigin 不是 '*',那么浏览器会检查窗口 targetWin 是否具有源 targetOrigin

  3. 如果它具有,targetWin 会触发具有特殊的属性的 message 事件:

    • origin —— 发送方窗口的源(比如 http://my.site.com)。
    • source —— 对发送方窗口的引用。
    • data —— 数据,可以是任何对象。但是 IE 浏览器只支持字符串,因此我们需要对复杂的对象调用 JSON.stringify 方法进行处理,以支持该浏览器。

    我们应该使用 addEventListener 来在目标窗口中设置 message 事件的处理程序。

本地存储

Cookie

Cookie 通常是由 Web 服务器使用响应 Set-Cookie HTTP-header 设置的,然后在每次请求中携带cookie

作用之一:身份验证

  1. 登录后,服务器在响应中使用 Set-Cookie HTTP-header 来设置具有唯一“会话标识符(session identifier)”的 cookie。
  2. 下次当请求被发送到同一个域时,浏览器会使用 Cookie HTTP-header 通过网络发送 cookie。
  3. 所以服务器知道是谁发起了请求。

我们可以使用 document.cookie 属性从浏览器访问 cookie。

  • document.cookie 的值由 name=value 对组成,以 ; 分隔。每一个都是独立的 cookie。

  • encodeURIComponent 编码后的 name=value 对,大小不能超过 4KB。因此,我们不能在一个 cookie 中保存大的东西。

  • 每个域的 cookie 总数不得超过 20+ 左右,具体限制取决于浏览器。

  • 当设置http-only时,无法通过 document.cookie获取cookie

Cookie 有几个选项:

  • path: 绝对路径,使得该路径下的页面可以访问该 cookie。默认为当前路径

    ​ 如:path=/admin下的cookie,在/admin/admin/xxx路径下都可见

  • domain: domain 控制了可访问 cookie 的域名,默认情况下,cookie 不**会共享给子域名,但将domain设置为根域名(baidu.com)时,子域名都可以访问cookie。**但,二级域名的cookie无法访问另一个二级域名的cookie。

  • expires(过期时间),max-age(存活时间) : 如果一个 cookie 没有设置这两个参数中的任何一个,那么在关闭浏览器之后,它就会消失。

    为了让 cookie 在浏览器关闭后仍然存在,我们可以设置 expiresmax-age 中的一个。

    如果我们将 expires 设置为过去的时间,则 cookie 会被删除。如果max-age设置为 0 或负数,则 cookie 会被删除。

  • secure :指明Cookie 应只能被通过 HTTPS 传输。默认情况下,如果我们在 http://site.com 上设置了 cookie,那么该 cookie 也会出现在 https://site.com 上,反之亦然。

  • samesite:防止 XSRF(跨网站请求伪造)攻击。

    • samesite=strict(和没有值的 samesite 一样)

      ​ 如果用户来自同一网站之外,那么设置了 samesite=strict 的 cookie 永远不会被发送。

    • samesite=lax

      ​ 如果以下两个条件均成立,则会发送含 samesite=lax 的 cookie:

      1. HTTP 方法是“安全的”(例如 GET 方法,而不是 POST)。
      2. 该操作执行顶级导航(更改浏览器地址栏中的 URL)。例如,从笔记中打开网站链接就满足这些条件。
  • httpOnly: 使 document.cookie无法获取cookie

getCookie(name)

总结:

document.cookie 提供了对 cookie 的访问

  • 写入操作只会修改其中提到的 cookie。
  • name/value 必须被编码。
  • 一个 cookie 最大不能超过 4KB。每个域下最多允许有 20+ 个左右的 cookie(具体取决于浏览器)。

Cookie 选项:

  • path=/,默认为当前路径,使 cookie 仅在该路径下可见。
  • domain=site.com,默认 cookie 仅在当前域下可见。如果显式地设置了域,可以使 cookie 在子域下也可见。
  • expiresmax-age 设定了 cookie 过期时间。如果没有设置,则当浏览器关闭时 cookie 就会失效。
  • secure 使 cookie 仅在 HTTPS 下有效。
  • samesite,如果请求来自外部网站,禁止浏览器发送 cookie。这有助于防止 XSRF 攻击。

LocalStorage,SessionStorage

5MB

两个存储对象都提供相同的方法和属性:

  • setItem(key, value) —— 存储键/值对。
  • getItem(key) —— 按照键获取值。
  • removeItem(key) —— 删除键及其对应的值。
  • clear() —— 删除所有数据。
  • key(index) —— 获取该索引下的键名。
  • length —— 存储的内容的长度。

同源,localStorage 数据可以共享

sessionStorage 的数据只存在于当前浏览器标签页。

  • 具有相同页面的另一个标签页中将会有不同的存储。
  • 但是,它在同一标签页下的 iframe 之间是共享的(假如它们来自相同的源)。

Storage 事件

localStoragesessionStorage 中的数据更新后,storage 事件就会触发,它具有以下属性:

  • key —— 发生更改的数据的 key(如果调用的是 .clear() 方法,则为 null)。
  • oldValue —— 旧值(如果是新增数据,则为 null)。
  • newValue —— 新值(如果是删除数据,则为 null)。
  • url —— 发生数据更新的文档的 url。
  • storageArea —— 发生数据更新的 localStoragesessionStorage 对象。
// 在其他文档对同一存储进行更新时触发
window.onstorage = event => { // 也可以使用 window.addEventListener('storage', event => {
  if (event.key != 'now') return;
  alert(event.key + ':' + event.newValue + " at " + event.url);
};

localStorage.setItem('now', Date.now());

允许同源的不同窗口交换消息:

Broadcast Channel API 可以实现同 下浏览器不同窗口,Tab 页,frame 或者 iframe 下的 浏览器上下文 (通常是同一个网站下不同的页面) 之间的简单通讯。

总结

Web 存储对象 localStoragesessionStorage 允许我们在浏览器中保存键/值对。

  • keyvalue 都必须为字符串。
  • 存储大小限制为 5MB+,具体取决于浏览器。
  • 它们不会过期。
  • 数据绑定到源(域/端口/协议)。
localStoragesessionStorage
在同源的所有标签页和窗口之间共享数据在当前浏览器标签页中可见,包括同源的 iframe
浏览器重启后数据仍然保留页面刷新后数据仍然保留(但标签页关闭后数据则不再保留)

API:

  • setItem(key, value) —— 存储键/值对。
  • getItem(key) —— 按照键获取值。
  • removeItem(key) —— 删除键及其对应的值。
  • clear() —— 删除所有数据。
  • key(index) —— 获取该索引下的键名。
  • length —— 存储的内容的长度。
  • 使用 Object.keys 来获取所有的键。
  • 我们将键作为对象属性来访问,在这种情况下,不会触发 storage 事件。

Storage 事件:

  • 在调用 setItemremoveItemclear 方法后触发。
  • 包含有关操作的所有数据(key/oldValue/newValue),文档 url 和存储对象 storageArea
  • 在所有可访问到存储对象的 window 对象上触发,导致当前数据改变的 window 对象除外(对于 sessionStorage 是在当前标签页下,对于 localStorage 是在全局,即所有同源的窗口)。

IndexedDB

  1. 打开数据库

    let openRequest = indexedDB.open(name, version);
    
    • name —— 字符串,即数据库名称。

    • version —— 一个正整数版本,默认为 1(下面解释)。

      调用之后会返回 openRequest 对象,我们需要监听该对象上的事件:

      • success:数据库准备就绪,openRequest.result 中有了一个数据库对象“Database Object”,我们应该将其用于进一步的调用。
      • error:打开失败。
      • upgradeneeded:数据库已准备就绪,但其版本已过时(见下文)。
    let openRequest = indexedDB.open("store", 1);
    
    openRequest.onupgradeneeded = function() {
      // 如果客户端没有数据库则触发
      // ...执行初始化...
    };
    
    openRequest.onerror = function() {
      console.error("Error", openRequest.error);
    };
    
    openRequest.onsuccess = function() {
      let db = openRequest.result;
      // 继续使用 db 对象处理数据库
    };
    
  2. 存储(对象库)

​ 要在 IndexedDB 中存储某些内容,我们需要一个 对象库。跟其他数据库的一样。

库中的每个值都必须有唯一的键 key

db.createObjectStore(name[, keyOptions]);
  • name 是存储区名称,例如 "books" 表示书。
  • keyOptions是具有以下两个属性之一的可选对象:
    • keyPath —— 对象属性的路径,IndexedDB 将以此路径作为键,例如 id
    • autoIncrement —— 如果为 true,则自动生成新存储的对象的键,键是一个不断递增的数字。
db.createObjectStore('books', {keyPath: 'id'});

对象库支持两种存储值的方法

  • put(value, [key])value 添加到存储区。仅当对象库没有 keyPathautoIncrement 时,才提供 key。如果已经存在具有相同键的值,则将替换该值。
  • add(value, [key])put 相同,但是如果已经有一个值具有相同的键,则请求失败,并生成一个名为 "ConstraInterror" 的错误。

3.删除数据库:

let deleteRequest = indexedDB.deleteDatabase(name)
// deleteRequest.onsuccess/onerror 追踪(tracks)结果

4.事务

事务是一组操作,要么全部成功,要么全部失败。

例如,当一个人买东西时,我们需要:

  1. 从他们的账户中扣除这笔钱。
  2. 将该项目添加到他们的清单中。

如果完成了第一个操作,但是出了问题,比如停电。这时无法完成第二个操作,这非常糟糕。两件时应该要么都成功(购买完成,好!)或同时失败(这个人保留了钱,可以重新尝试)。

所有数据操作都必须在 IndexedDB 中的事务内进行。

启动事务:

db.transaction(store[, type]);
  • store 是事务要访问的库名称,例如 "books"。如果我们要访问多个库,则是库名称的数组。
  • type– 事务类型,以下类型之一:
    • readonly —— 只读,默认值。
    • readwrite —— 只能读取和写入数据,而不能 创建/删除/更改 对象库。
let transaction = db.transaction("books", "readwrite"); // (1)

// 获取对象库进行操作
let books = transaction.objectStore("books"); // (2)

let book = {
  id: 'js',
  price: 10,
  created: new Date()
};

let request = books.add(book); // (3)

request.onsuccess = function() { // (4)
  console.log("Book added to the store", request.result);
};

request.onerror = function() {
  console.log("Error", request.error);
};

执行事务基本有四个步骤:

  1. 创建一个事务,在(1)表明要访问的所有存储。
  2. 使用 transaction.objectStore(name),在(2)中获取存储对象。
  3. 在(3)执行对对象库 books.add(book) 的请求,返回一个request
  4. ……处理请求 成功/错误(4),还可以根据需要发出其他请求。

当所有事务的请求完成,并且 微任务队列 为空时,它将自动提交。

事务在浏览器开始执行宏任务之前关闭。

手动中止事务,请调用:

transaction.abort();

IndexedDB 事件冒泡:请求 → 事务 → 数据库。

我们可以使用 db.onerror 处理程序捕获所有错误:

搜索值

对象库有两种主要的搜索类型

  1. 通过键值或键值范围搜索。
    • store.get(query) —— 按键或范围搜索第一个值。
    • store.getAll([query], [count]) —— 搜索所有值。如果 count 给定,则按 count 进行限制。
    • store.getKey(query) —— 搜索满足查询的第一个键,通常是一个范围。
    • store.getAllKeys([query], [count]) —— 搜索满足查询的所有键,通常是一个范围。如果 count 给定,则最多为 count。
    • store.count([query]) —— 获取满足查询的键的总数,通常是一个范围。

// 获取一本书
books.get('js')

// 获取 'css' <= id <= 'html' 的书
books.getAll(IDBKeyRange.bound('css', 'html'))

// 获取 id < 'html' 的书
books.getAll(IDBKeyRange.upperBound('html', true))

// 获取所有书
books.getAll()

// 获取所有 id > 'js' 的键
books.getAllKeys(IDBKeyRange.lowerBound('js', true))

IDBKeyRange 对象,指定一个可接受的“键值范围”,IDBKeyRange 对象是通过下列调用创建的:

  • IDBKeyRange.lowerBound(lower, [open]) 表示:≥lower(如果 open 是 true,表示 >lower
  • IDBKeyRange.upperBound(upper, [open]) 表示:≤upper(如果 open 是 true,表示 <upper
  • IDBKeyRange.bound(lower, upper, [lowerOpen], [upperOpen]) 表示: 在 lowerupper 之间。如果 open 为 true,则相应的键不包括在范围中。
  • IDBKeyRange.only(key) —— 仅包含一个键的范围 key,很少使用。
  1. 通过另一个对象字段,例如 book.price。这需要一个额外的数据结构,名为“索引(index)”:

    objectStore.createIndex(name, keyPath, [options]);
    
    • name —— 索引名称。
    • keyPath —— 索引应该跟踪的对象字段的路径(我们将根据该字段进行搜索)。
    • option—— 具有以下属性的可选对象:
      • unique —— 如果为true,则存储中只有一个对象在 keyPath 上具有给定值。如果我们尝试添加重复项,索引将生成错误。
      • multiEntry —— 只有 keypath 上的值是数组时才使用。这时,默认情况下,索引将默认把整个数组视为键。但是如果 multiEntry 为 true,那么索引将为该数组中的每个值保留一个存储对象的列表。所以数组成员成为了索引键。

删除数据

delete方法查找要由查询删除的值,调用格式类似于 getAll

  • delete(query) —— 通过查询删除匹配的值。
// 删除 id='js' 的书
books.delete('js');

删除所有内容:

books.clear(); // 清除存储。

正则表达式

str.match(regExp)

如果 regexp 不带有修饰符 g,则它以数组的形式返回第一个匹配项,其中包含捕获组和属性 index(匹配项的位置)、input(输入字符串,等于 str):

let str = "I love JavaScript";

let result = str.match(/Java(Script)/);

alert( result[0] );     // JavaScript(完全匹配)
alert( result[1] );     // Script(第一个分组)
alert( result.length ); // 2

// 其他信息:
alert( result.index );  // 7(匹配位置)
alert( result.input );  // I love JavaScript(源字符串)

如果 regexp 带有修饰符 g,则它将返回一个包含所有匹配项的数组,但不包含捕获组和其它详细信息。

如果没有匹配项,则无论是否带有修饰符 g,都将返回 null

str.split(regexp|substr,limit)

使用正则表达式(或子字符串)作为分隔符来分割字符串。

alert('12, 34, 56'.split(/,\s*/)) // 数组 ['12', '34', '56']

str.search(regexp)

返回第一个匹配项的位置,如果没找到,则返回 -1

let str = "A drop of ink may make a million think";

alert( str.search( /ink/i ) ); // 10(第一个匹配位置)

search 仅查找第一个匹配项。

str.replace(str|regExp,str|func)

replace 的第一个参数是字符串时,它只替换第一个匹配项。

如果想要替换所有字符串,则应使用g 修饰符的正则表达式 /-/g

第二个参数替换字符串。我们可以在其中使用特殊字符:

符号替换字符串中的行为
$&插入整个匹配项
`$``插入字符串中匹配项之前的字符串部分
$'插入字符串中匹配项之后的字符串部分
$n如果 n 是一个 1-2 位的数字,则插入第 n 个分组的内容,详见 捕获组
$<name>插入带有给定 name 的括号内的内容,详见 捕获组
$$插入字符 $
let str = "John Smith";

// 交换名字和姓氏
alert(str.replace(/(john) (smith)/i, '$2, $1')) // Smith, John

对于需要“智能”替换的场景,第二个参数可以是一个函数。

该函数 func(match, p1, p2, ..., pn, offset, input, groups) 带参数调用:

  1. match —— 匹配项,
  2. p1, p2, ..., pn —— 捕获组的内容(如有),
  3. offset —— 匹配项的位置,
  4. input —— 源字符串,
  5. groups —— 具有命名的捕获组的对象。

如果正则表达式中没有括号,则只有 3 个参数:func(str, offset, input)

str.replaceAll(str|regexp,str|func)

  1. 如果第一个参数是一个字符串,它会替换 所有出现的 和第一个参数相同的字符串,而 replace 只会替换 第一个
  2. 如果第一个参数是一个没有修饰符 g 的正则表达式,则会报错。带有修饰符 g,它的工作方式与 replace 相同。

replaceAll 的主要用途是替换所有出现的字符串。

regexp.exec(str)

返回字符串 str 中的 regexp 匹配项。

regexp.test(str)

查找匹配项,然后返回 true/false 表示是否存在。

如果正则表达式带有修饰符 g,则 regexp.testregexp.lastIndex 属性开始查找并更新此属性,就像 regexp.exec 一样。