语言基础
var声明的范围是函数作用域,let声明的范围是块作用域.
与 var 关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var 声 明的变量则会)
在for循环中var和let
在使用 var 的时候,最常见的问题就是对迭代变量的奇特声明和修改:
1 | for (var i = 0; i < 5; ++i) { |
之所以会这样,是因为在退出循环时,迭代变量保存的是导致循环退出的值:5。在之后执行超时 逻辑时,所有的 i 都是同一个变量,因而输出的都是同一个最终值。
而在使用 let 声明迭代变量时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。 每个 setTimeout 引用的都是不同的变量实例,所以 console.log 输出的是我们期望的值,也就是循环执行过程中每个迭代变量的值。
1 | for (let i = 0; i < 5; ++i) { |
const 的行为与 let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且 尝试修改 const 声明的变量会导致运行时错误。
数据类型
undefined
在对未初始化的变量调用 typeof 时,返回的结果是”undefined”,但对未声明的变量调用它时, 返回的结果还是”undefined”
1 | let message; // 这个变量被声明了,只是值为 undefined |
toString
null 和 undefined 值没有 toString()方法。
如果你不确定一个值是不是 null 或 undefined,可以使用 String()转型函数,它始终会返回表示相应类型值的字符串。
如果值有 toString()方法,则调用该方法(不传参数)并返回结果。
如果值是 null,返回”null”。
如果值是 undefined,返回”undefined”。
== 和 ===
== 会先进行强制类型转换
在转换操作数的类型时,相等和不相等操作符遵循如下规则。
如果任一操作数是布尔值,则将其转换为数值再比较是否相等。false 转换为 0,true 转换 为 1。
如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否 相等。
如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()方法取得其原始值,再 根据前面的规则进行比较。
在进行比较时,这两个操作符会遵循如下规则。
null 和 undefined 相等。
null 和 undefined 不能转换为其他类型的值再进行比较。 null或undefined都不等于0
如果有任一操作数是 NaN,则相等操作符返回 false,不相等操作符返回 true。记住:即使两 个操作数都是 NaN,相等操作符也返回 false,因为按照规则,NaN 不等于 NaN。
如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象, 则相等操作符返回 true。否则,两者不相等。
===在比较相等时不转换操作数
字面量定义和new的区别
原始类型的初始化可以只使用原始字面量形式。如果使用的是 new 关键字,则 JavaScript 会 创建一个 Object 类型的实例,如下例子
1 | let name1 = "Nicholas"; |
引用类型与原始值包装类型的主要区别在于对象的生命周期。在通过 new 实例化引用类型后,得到 的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期 间。这意味着不能在运行时给原始值添加属性和方法。
基本数据和引用数据
- 基本数据类型的值在内存中占据固定大小的空间,因此被保存在栈内存中.
- 从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本
- 引用类型的值是对象,保存在堆内存中
- 从一个变量向另一个变量复制引用类型的值,复制的其实是指针,因此两个变量最终都指向同一个对象
传参
ECMAScript 中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数 中,就像从一个变量复制到另一个变量一样。如果是原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值变量的复制一样。
1 | function addTen(num) { |
注意最后一个例子,说明了对象是按值传递的.
当 person 传入 setName()时,其 name 属性被设置为”Nicholas”。然后变量 obj 被设置 为一个新对象且 name 属性被设置为”Greg”。如果 person 是按引用传递的,那么 person 应该自动将 指针改为指向 name 为”Greg”的对象。可是,当我们再次访问 person.name 时,它的值是”Nicholas”, 这表明函数中参数的值改变之后,原始的引用仍然没变。当 obj 在函数内部被重写时,它变成了一个指 向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了。
数值
NaN
非数值.
特点:
(1)任何涉及NaN的操作都会返回NaN
(2)NaN与任何值都不相等,包括NaN本身
isNaN()方法也适用于对象,在基于对象调用这个函数时,会首先调用对象的valueOf()方法,然后确定该方法返回的值是否可以转换为数值,如果不能,则给予这个返回值再调用toString()方法,再测试返回值.
Number(),parseInt()和parseFloat()
执行上下文和作用域
- 每次进入一个新的执行环境,都会创建一个用于搜索变量和函数的作用域链
- 变量的执行环境有助于确定应该何时释放内存
作用域链增强
虽然执行上下文主要有全局上下文和函数上下文两种(eval()调用内部存在第三种上下文),但有 其他方式来增强作用域链。某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时:
1 | try/catch 语句的 catch 块 |
这两种情况下,都会在作用域链前端添加一个变量对象。对 with 语句来说,会向作用域链前端添 加指定的对象;对 catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误 对象的声明。看下面例子:
1 | function buildUrl() { |
这里,with 语句将 location 对象作为上下文,因此 location 会被添加到作用域链前端。 buildUrl()函数中定义了一个变量 qs。当 with 语句中的代码引用变量 href 时,实际上引用的是 location.href,也就是自己变量对象的属性。在引用 qs 时,引用的则是定义在 buildUrl()中的那 个变量,它定义在函数上下文的变量对象上。而在 with 语句中使用 var 声明的变量 url 会成为函数 上下文的一部分,可以作为函数的值被返回;但像这里使用 let 声明的变量 url,因为被限制在块级作 用域(稍后介绍),所以在 with 块之外没有定义。
变量
变量声明
var
在使用 var 声明变量时,变量会被自动添加到最接近的上下文。在函数中,最接近的上下文就是函 数的局部上下文。在 with 语句中,最接近的上下文也是函数上下文。如果变量未经声明就被初始化了, 那么它就会自动被添加到全局上下文,如下面的例子所示:
1 | function add(num1, num2) { |
这一次,变量 sum 被用加法操作的结果初始化时并没有使用 var 声明。在调用 add()之后,sum 被添加到了全局上下文,在函数退出之后依然存在,从而在后面可以访问到。
let
let是块级作用域声明,let 与 var 的另一个不同之处是在同一作用域内不能声明两次。重复的 var 声明会被忽略,而重 复的 let 声明会抛出 SyntaxError。
只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
1 | var tmp = 123; |
上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。
ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
const
ES6 同时还增加了 const 关键字。使用 const 声明的变量必须同时初始化为某个值。 一经声明,在其生命周期的任何时候都不能再重新赋予新值。
const 声明只应用到顶级原语或者对象。换句话说,赋值为对象的 const 变量不能再被重新赋值 为其他引用值,但对象的键则不受限制。
如果想让整个对象都不能修改,可以使用 Object.freeze(),这样再给属性赋值时虽然不会报错, 但会静默失败:
1 | const o3 = Object.freeze({}); |
由于 const 声明暗示变量的值是单一类型且不可修改,JavaScript 运行时编译器可以将其所有实例 都替换成实际的值,而不会通过查询表进行变量查找。谷歌的 V8 引擎就执行这种优化。
传递参数
ECMAScript 中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数 中,就像从一个变量复制到另一个变量一样。如果是原始值,那么就跟原始值变量的复制一样,如果是 引用值,那么就跟引用值变量的复制一样。对很多开发者来说,这一块可能会不好理解,毕竟变量有按 值和按引用访问,而传参则只有按值传递。
下面看传入对象的例子:
1 | function setName(obj) { |
这一次,我们创建了一个对象并把它保存在变量 person 中。然后,这个对象被传给 setName() 方法,并被复制到参数 obj 中。在函数内部,obj 和 person 都指向同一个对象。结果就是,即使对象 是按值传进函数的,obj 也会通过引用访问对象。当函数内部给 obj 设置了 name 属性时,函数外部的 对象也会反映这个变化,因为 obj 指向的对象保存在全局作用域的堆内存上。很多开发者错误地认为, 当在局部作用域中修改对象而变化反映到全局时,就意味着参数是按引用传递的。为证明对象是按值传 递的,我们再来看看下面这个修改后的例子:
1 | function setName(obj) { |
这个例子前后唯一的变化就是 setName()中多了两行代码,将 obj 重新定义为一个有着不同 name 的新对象。当 person 传入 setName()时,其 name 属性被设置为”Nicholas”。然后变量 obj 被设置 为一个新对象且 name 属性被设置为”Greg”。如果 person 是按引用传递的,那么 person 应该自动将 指针改为指向 name 为”Greg”的对象。可是,当我们再次访问 person.name 时,它的值是”Nicholas”, 这表明函数中参数的值改变之后,原始的引用仍然没变.当 obj 在函数内部被重写时,它变成了一个指 向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了。
垃圾回收
标记清除
最常用的垃圾收集方式是标记清除.垃圾收集器在运行的时候会给存储在内存的所有变量都加上标记.然后它会去掉环境中的变量以及被环境中的变量引用的变量的标记.而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了.最后垃圾收集器完成内存清除工作,销毁那些带标记的值并回收他们所占用的空间.
给变量加标记的方式有很多种。比如,当变量进入上下文时,反转某一位;或者可以维护“在上下 文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另一个列表。标记过程的实现 并不重要,关键是策略。
引用计数
其思路是对每个值都记录它被 引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变 量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一 个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序 下次运行的时候就会释放引用数为 0 的值的内存。
引用计数的问题
了严重的问题:循环引用。所谓循环引 用,就是对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A。比如:
1 | function problem() { |
在这个例子中,objectA 和 objectB 通过各自的属性相互引用,意味着它们的引用数都是 2。在 标记清理策略下,这不是问题,因为在函数结束后,这两个对象都不在作用域中。而在引用计数策略下,objectA 和 objectB 在函数结束后还会存在,因为它们的引用数永远不会变成 0。如果函数被多次调 用,则会导致大量内存永远不会被释放。
在 IE8 及更早版本的 IE 中,并非所有对象都是原生 JavaScript 对象。BOM 和 DOM 中的对象是 C++ 实现的组件对象模型(COM,Component Object Model)对象,而 COM 对象使用引用计数实现垃圾回 收。因此,即使这些版本 IE 的 JavaScript 引擎使用标记清理,JavaScript 存取的 COM 对象依旧使用引用 计数。换句话说,只要涉及 COM 对象,就无法避开循环引用问题。下面这个简单的例子展示了涉及 COM 对象的循环引用问题:
1 | let element = document.getElementById("some_element"); |
为避免类似的循环引用问题,应该在确保不使用的情况下切断原生 JavaScript 对象与 DOM 元素之 间的连接。比如,通过以下代码可以清除前面的例子中建立的循环引用:
1 | myObject.element = null; |
为了补救这一点,IE9 把 BOM 和 DOM 对象都改成了 JavaScript 对象,这同时也避免了由于存在两 套垃圾回收算法而导致的问题,还消除了常见的内存泄漏现象。
内存管理
通过let和const声明提升性能
因为 const和 let 都以块(而非函数)为作用域,所以相比于使用 var,使用这两个新关键字可能会更早地让垃圾回 收程序介入,尽早回收应该回收的内存。在块作用域比函数作用域更早终止的情况下,这就有可能发生。
隐藏类和删除操作
运行期间,V8 会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类 的对象性能会更好,V8 会针对这种情况进行优化,但不一定总能够做到。比如下面的代码:
1 | function Article() { |
V8 会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原 型。假设之后又添加了下面这行代码:
1 | a2.author = 'Jake'; |
此时两个 Article 实例就会对应两个不同的隐藏类。根据这种操作的频率和隐藏类的大小,这有 可能对性能产生明显影响。
当然,解决方案就是避免 JavaScript 的“先创建再补充”(ready-fire-aim)式的动态属性赋值,并在 构造函数中一次性声明所有属性,如下所示:
1 | function Article(opt_author) { |
这样,两个实例基本上就一样了(不考虑 hasOwnProperty 的返回值),因此可以共享一个隐藏类, 从而带来潜在的性能提升。
不过要记住,使用 delete 关键字会导致生成相同的隐藏类片段。看一下这 个例子:
1 | function Article() { |
在代码结束后,即使两个实例使用了同一个构造函数,它们也不再共享一个隐藏类。动态删除属性 与动态添加属性导致的后果一样。最佳实践是把不想要的属性设置为 null。这样可以保持隐藏类不变 和继续共享,同时也能达到删除引用值供垃圾回收程序回收的效果。比如:
1 | function Article() { |
基本引用类型
原始值包装类型
ECMAScript 提供了 3 种特殊的引用类型:Boolean、Number 和 String。
1 | let s1 = "some text"; |
在这里,s1 是一个包含字符串的变量,它是一个原始值。第二行紧接着在 s1 上调用了 substring() 方法,并把结果保存在 s2 中。我们知道,原始值本身不是对象,因此逻辑上不应该有方法。而实际上 这个例子又确实按照预期运行了。这是因为后台进行了很多处理,从而实现了上述操作。具体来说,当 第二行访问 s1 时,是以读模式访问的,也就是要从内存中读取变量保存的值。
在以读模式访问字符串 值的任何时候,后台都会执行以下 3 步:
(1) 创建一个 String 类型的实例;
(2) 调用实例上的特定方法;
(3) 销毁实例。
在读的时候相当于进行了如下操作
1 | let s1 = new String("some text"); |
引用类型与原始值包装类型的主要区别在于对象的生命周期。在通过 new 实例化引用类型后,得到 的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期 间。这意味着不能在运行时给原始值添加属性和方法。比如下面的例子:
1 | let s1 = "some text"; |
原因就是第二行代码运行时会临时创建一个 String 对象,而当第三行代码执行时,这个对 象已经被销毁了。实际上,第三行代码在这里创建了自己的 String 对象,但这个对象没有 color 属性。
Boolean
1 | let falseObject = new Boolean(false); |
Number
1 | let numberObject = new Number(10); |
原始数值在调用 typeof 时始终返回”number”,而 Number 对象则返回”object”。类似地,Number 对象是 Number 类型的实例,而原始数值不是。
String
字符串的操作方法
ECMAScript 提供了 3 个从字符串中提取子字符串的方法:**slice()、substr()和 substring()**。
都接收一或两个参数。第一个参数表示子字符串开 始的位置,第二个参数表示子字符串结束的位置。对 slice()和 substring()而言,第二个参数是提取结 束的位置(即该位置之前的字符会被提取出来)。对 substr()而言,第二个参数表示返回的子字符串数量。 任何情况下,省略第二个参数都意味着提取到字符串末尾。
1 | let stringValue = "hello world"; |
当某个参数是负值时,这 3 个方法的行为又有不同。比如,slice()方法将所有负值参数都当成字 符串长度加上负参数值。 而 substr()方法将第一个负参数值当成字符串长度加上该值,将第二个负参数值转换为 0。 substring()方法会将所有负参数值都转换为 0。
字符串位置方法
有两个方法用于在字符串中定位子字符串:indexOf()和 lastIndexOf()。这两个方法从字符 串中搜索传入的字符串,并返回位置(如果没找到,则返回-1)。两者的区别在于,indexOf()方法 从字符串开头开始查找子字符串,而 lastIndexOf()方法从字符串末尾开始查找子字符串。
字符串包含方法
ECMAScript 6 增加了 3 个用于判断字符串中是否包含另一个字符串的方法:startsWith()、 endsWith()和 includes()。
1 | let message = "foobarbaz"; |
trim()方法
ECMAScript 在所有字符串上都提供了 trim()方法。这个方法会创建字符串的一个副本,删除前、 后所有空格符,再返回结果。比如:
集合引用类型
Array
检测数组
使用 instanceof 的问题是假定只有一个全局执行上下文。如果网页里有多个框架,则可能涉及两 个不同的全局执行上下文,因此就会有两个不同版本的 Array 构造函数。如果要把数组从一个框架传 给另一个框架,则这个数组的构造函数将有别于在第二个框架内本地创建的数组。
ECMAScript 提供了 **Array.isArray()**方法。这个方法的目的就是确定一个值是 否为数组,而不用管它是在哪个全局执行上下文中创建的。
转化数组
Array 构造函数还有两个 ES6 新增的用于创建数组的静态方法:from()和 of()。from()用于将 类数组结构转换为数组实例,而 of()用于将一组参数转换为数组实例。
Array.from()的第一个参数是一个类数组对象,即任何可迭代的结构,或者有一个 length 属性 和可索引元素的结构。
1 | // arguments 对象可以被轻松地转换为数组 |
Array.from()还接收第二个可选的映射函数参数。这个函数可以直接增强新数组的值,而无须像 调用 Array.from().map()那样先创建一个中间数组。还可以接收第三个可选参数,用于指定映射函 数中 this 的值。但这个重写的 this 值在箭头函数中不适用。
1 | const a1 = [1, 2, 3, 4]; |
Array.of()可以把一组参数转换为数组。这个方法用于替代在 ES6之前常用的 Array.prototype. slice.call(arguments),一种异常笨拙的将 arguments 对象转换为数组的写法:
1 | console.log(Array.of(1, 2, 3, 4)); // [1, 2, 3, 4] |
复制和填充方法
使用 fill()方法可以向一个已有的数组中插入全部或部分相同的值。开始索引用于指定开始填充 的位置,它是可选的。
1 | // 用 6 填充索引大于等于 3 的元素 |
copyWithin()会按照指定范围浅复制数组中的部分内容,然后将它们插入到指定索引开始的位置。开始索引和结束索引则与 fill()使用同样的计算方法:
1 | let ints, |
转换方法
前面提到过,所有对象都有 toLocaleString()、toString()和 valueOf()方法。其中,valueOf() 返回的还是数组本身。而 toString()返回由数组中每个值的等效字符串拼接而成的一个逗号分隔的 字符串。
toLocaleString()方法也可能返回跟 toString()和 valueOf()相同的结果,但也不一定。在 调用数组的 toLocaleString()方法时,会得到一个逗号分隔的数组值的字符串。它与另外两个方法 唯一的区别是,为了得到最终的字符串,会调用数组每个值的 toLocaleString()方法,而不是 toString()方法。
1 | let person1 = { |
这里定义了两个对象 person1 和 person2,它们都定义了 toString()和 toLocaleString()方 法,而且返回不同的值。然后又创建了一个包含这两个对象的数组 people。在将数组传给 alert()时, 输出的是”Nicholas,Greg”,这是因为会在数组每一项上调用 toString()方法(与下一行显式调用 toString()方法结果一样)。而在调用数组的 toLocaleString()方法时,结果变成了”Nikolaos, Grigorios”,这是因为调用了数组每一项的 toLocaleString()方法。
栈方法
pop和push,以栈的方式操作数组,pop弹出数组中最后的元素,push在数组尾部添加新的元素
push返回新数组的长度
pop返回弹出的元素
队列方法
shift()和 push(),以队列的方式操作数组,shift删除数组中的第一个元素,push在数组尾部添加新的元素.对应有unshift方法,在数组开头添加任意多个值,然后返回新的数组长度。
shift返回删除的元素
unshift返回新数组的长度
排序方法
数组有两个方法可以用来对元素重新排序:reverse()和 sort()。
reverse()方法就 是将数组元素反向排列。reverse()会改变原来的数组,并返回这个数组
默认情况下,sort()会按照升序重新排列数组元素,即最小的值在前面,最大的值在后面。为此, sort()会在每一项上调用 String()转型函数,然后比较字符串来决定顺序。sort()会改变原来的数组,并返回这个数组,即使数组的元素都是数值, 也会先把数组转换为字符串再比较、排序。比如:
1 | let values = [0, 1, 5, 10, 15]; |
开始数组中数值的顺序是正确的,但调用 sort()会按照这些数值的字符串形式重新排序。因此, 即使 5 小于 10,但字符串”10”在字符串”5”的前头,所以 10 还是会排到 5 前面。很明显,这在多数情 况下都不是最合适的。为此,sort()方法可以接收一个比较函数,用于判断哪个值应该排在前面。
比较函数接收两个参数,如果第一个参数应该排在第二个参数前面,就返回负值;如果两个参数相 等,就返回 0;如果第一个参数应该排在第二个参数后面,就返回正值。下面是使用简单比较函数的一 个例子:
1 | function compare(value1, value2) { |
这个比较函数可以适用于大多数数据类型,可以把它当作参数传给 sort()方法,如下所示:
1 | let values = [0, 1, 5, 10, 15]; |
操作方法
1 join() 方法也可将所有数组元素结合为一个字符串。它的行为类似 toString(),但是可以规定分隔符.**返回生成的字符串
2 shift() 方法会删除首个数组元素,并把所有其他元素“位移”到更低的索引,方法返回被“位移出”的字符串
3 unshift() 方法(在开头)向数组添加新元素,并“反向位移”旧元素, 方法返回新数组的长度
4 splice() 方法可用于向数组添加新项
1 | var fruits = ["Banana", "Orange", "Apple", "Mango"]; |
第一个参数(2)定义了应添加新元素的位置(拼接)。
第二个参数(0)定义应删除多少元素。
其余参数(“Lemon”,“Kiwi”)定义要添加的新元素。
splice() 方法改变原数组,返回一个包含已删除项的数组
5 concat() 方法通过合并(连接)现有数组来创建一个新数组.concat()不会改变原数组,返回连接后的新数组
6 slice() 方法用数组的某个片段切出新数组
slice() 可接受两个参数,比如 (1, 3)。
该方法会从开始参数选取元素,直到结束参数(不包括)为止
如果结束参数被省略,则 slice() 会切出数组的剩余部分
slice()不会改变原数组,返回截取的片段数组
搜索和位置方法
严格相等
ECMAScript 提供了 3 个严格相等的搜索方法:**indexOf()、lastIndexOf()和 includes()**。其 中,前两个方法在所有版本中都可用,而第三个方法是 ECMAScript 7 新增的。
这些方法都接收两个参 数:要查找的元素和一个可选的起始搜索位置。indexOf()和 includes()方法从数组前头(第一项) 开始向后搜索,而 lastIndexOf()从数组末尾(最后一项)开始向前搜索。 indexOf()和 lastIndexOf()都返回要查找的元素在数组中的位置,如果没找到则返回-1。 includes()返回布尔值,表示是否至少找到一个与指定元素匹配的项。在比较第一个参数跟数组每一 项时,会使用全等(===)比较,也就是说两项必须严格相等。
断言函数
find()和 findIndex()方法使用了断言函数。这两个方法都从数组的最小索引开始。find()返回 第一个匹配的元素,findIndex()返回第一个匹配元素的索引。
1 | const people = [ |
迭代方法
1 | every():对数组每一项都运行传入的函数,如果对每一项函数都返回 true,则这个方法返回 true。 |
每个方法接收两个参数:以每一项为参数运行的函数, 以及可选的作为函数运行上下文的作用域对象(影响函数中 this 的值)。传给每个方法的函数接收 3 个参数:数组元素、元素索引和数组本身。
1 | let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1]; |
归并方法
ECMAScript 为数组提供了两个归并方法:reduce()和 reduceRight()。这两个方法都会迭代数 组的所有项,并在此基础上构建一个最终返回值。reduce()方法从数组第一项开始遍历到最后一项。 而 reduceRight()从最后一项开始遍历至第一项。
传给 reduce()和 reduceRight()的函数接收 4 个参数:上一个归并值、当前项、当前项的索引和数 组本身。这个函数返回的任何值都会作为下一次调用同一个函数的第一个参数。
1 | let values = [1, 2, 3, 4, 5]; |
第一次执行归并函数时,prev 是 1,cur 是 2。第二次执行时,prev 是 3(1 + 2),cur 是 3(数 组第三项)。如此递进,直到把所有项都遍历一次,最后返回归并结果。
reduceRight()方法与之类似,只是方向相反。来看下面的例子:
1 | let values = [1, 2, 3, 4, 5]; |
Map
键值对
基本用法
1 | const m = new Map(); |
与 Object 只能使用数值、字符串或符号作为键不同,Map 可以使用任何 JavaScript 数据类型作为 键。
1 | const m = new Map(); |
迭代各个键方法
1 | const m = new Map([ |
选择Object还是Map
- 内存占用:存储单个键/值对所占用的内存数量 都会随键的数量线性增加。给定固定大小的内存,Map 大约可以比 Object 多存储 50%的键/值对。
- 插入性能:向 Object 和 Map 中插入新键/值对的消耗大致相当,不过插入 Map 在所有浏览器中一般会稍微快 一点儿。对这两个类型来说,插入速度并不会随着键/值对数量而线性增加。如果代码涉及大量插入操 作,那么显然 Map 的性能更佳。
- 查找速度:对这两个类型而言, 查找速度不会随着键/值对数量增加而线性增加。如果代码涉及大量查找操作,那么某些情况下可能选 择 Object 更好一些。
- 删除性能:Map 的 delete()操作都比插入和查找更快。 如果代码涉及大量删除操作,那么毫无疑问应该选择 Map。
WeakMap
ECMAScript 6 新增的“弱映射”(WeakMap)是一种新的集合类型,为这门语言带来了增强的键/ 值对存储机制。WeakMap 中的“weak”(弱), 描述的是 JavaScript 垃圾回收程序对待“弱映射”中键的方式。
弱映射中的键只能是 Object 或者继承自 Object 的类型,尝试使用非对象设置键会抛出 TypeError。值的类型没有限制。
WeakMap 实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值。如果 允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了。
弱键
WeakMap 中“weak”表示弱映射的键是“弱弱地拿着”的。意思就是,这些键不属于正式的引用, 不会阻止垃圾回收。但要注意的是,弱映射中值的引用可不是“弱弱地拿着”的。只要键存在,键/值 对就会存在于映射中,并被当作对值的引用,因此就不会被当作垃圾回收。
1 | const wm = new WeakMap(); |
set()方法初始化了一个新对象并将它用作一个字符串的键。因为没有指向这个对象的其他引用, 所以当这行代码执行完成后,这个对象键就会被当作垃圾回收。然后,这个键/值对就从弱映射中消失 了,使其成为一个空映射。在这个例子中,因为值也没有被引用,所以这对键/值被破坏以后,值本身 也会成为垃圾回收的目标。再来看另一个例子
1 | const wm = new WeakMap(); |
这一次,container 对象维护着一个对弱映射键的引用,因此这个对象键不会成为垃圾回收的目 标。不过,如果调用了 removeReference(),就会摧毁键对象的最后一个引用,垃圾回收程序就可以 把这个键/值对清理掉。
不可迭代键
因为 WeakMap 中的键/值对任何时候都可能被销毁,所以没必要提供迭代其键/值对的能力。当然, 也用不着像 clear()这样一次性销毁所有键/值的方法.
使用
私有变量
弱映射造就了在 JavaScript 中实现真正私有变量的一种新方式。前提很明确:私有变量会存储在弱 映射中,以对象实例为键,以私有成员的字典为值。
1 | const wm = new WeakMap(); |
迭代器和生成器
使用
1 | // 可迭代对象 |
对象
属性的类型
属性分数据属性和访问器属性.
数据属性
数据属性包含一个保存数据值的位置,值会从这个位置读取,也会写入到这个位置.数据属性有4个特性描述他们的行为.
要修改属性的默认特性,就必须使用 Object.defineProperty()方法。这个方法接收 3 个参数: 要给其添加属性的对象、属性的名称和一个描述符对象。最后一个参数,即描述符对象上的属性可以包 含:configurable、enumerable、writable 和 value,跟相关特性的名称一一对应。根据要修改 的特性,可以设置其中一个或多个值。比如
1 | let person = {}; |
这个例子创建了一个名为 name 的属性并给它赋予了一个只读的值”Nicholas”。这个属性的值就 不能再修改了.
在调用 Object.defineProperty()时,configurable、enumerable 和 writable 的值如果不 指定,则都默认为 false。
访问器属性
访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不 过这两个函数不是必需的。
访问器属性有 4 个特性描述它们的行为。
- [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特 性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性 都是 true。
- [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对 象上的属性的这个特性都是 true。
- [[Get]]:获取函数,在读取属性时调用。默认值为 undefined。
- [[Set]]:设置函数,在写入属性时调用。默认值为 undefined。
1 | // 定义一个对象,包含伪私有成员 year_和公共成员 edition |
year_和 edition。year_中的下划线常用来表示该 属性并不希望在对象方法的外部被访问。另一个属性 year 被定义为一个访问器属性,其中获取函数简 单地返回 year_的值,而设置函数会做一些计算以决定正确的版本(edition)。因此,把 year 属性修改 为 2018 会导致 year_变成 2018,edition 变成 2。这是访问器属性的典型使用场景,即设置一个属性 值会导致一些其他变化发生。
获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽 略。类似地,只有一个设置函数的属性 是不能读取的.
定义多个属性
在一个对象上同时定义多个属性的可能性是非常大的。为此,ECMAScript 提供了 Object.defineProperties()方法。这个方法可以通过多个描述符一次性定义多个属性。它接收两个参数:要为之添 加或修改属性的对象和另一个描述符对象,其属性与要添加或修改的属性一一对应。比如:
1 | let book = {}; |
读取属性特性方法如下,两种方法,第一种获取的是year_属性的特性描述,第二种方法获取的是book对象的全部属性和描述.
1 | let descriptor = Object.getOwnPropertyDescriptor(book, "year_"); |
合并对象
有时也称为”混入,就是把源对象所有的本地属性一起复制到目标对象上.
ECMAScript 6 专门为合并对象提供了 Object.assign()方法。这个方法接收一个目标对象和一个 或多个源对象作为参数,然后将每个源对象中可枚举(Object.propertyIsEnumerable()返回 true) 和自有(Object.hasOwnProperty()返回 true)属性复制到目标对象。以字符串和符号为键的属性 会被复制。对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的值,然后使用目标 对象上的[[Set]]设置属性的值。
Object.assign()实际上对每个源对象执行的是浅复制。
1 | let dest, src, result; |
对象标识及相等判定
在 ECMAScript 6 之前,有些特殊情况即使是===操作符也无能为力.
为改善这类情况,ECMAScript 6 规范新增了 Object.is(),这个方法与===很像,但同时也考虑 到了上述边界情形。这个方法必须接收两个参数:
1 | // 这些情况在不同 JavaScript 引擎中表现不同,但仍被认为相等 |
创建对象
创建对象的方法有多种,使用 Object 构造函数或对象字面量可以方便地创建对象.下面介绍几种其他的创建对象的方法
工厂模式
1 | function createPerson(name, age, job) { |
构造函数模式
1 | function Person(name, age, job){ |
要创建 Person 的实例,应使用 new 操作符。以这种方式调用构造函数会执行如下操作。
(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。
(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
定义自定义构造函数可以确保实例被标识为特定类型,相比于工厂模式,这是一个很大的好处。在 这个例子中,person1 和 person2 之所以也被认为是 Object 的实例,是因为所有自定义对象都继承 自 Object.
构造函数的缺点
构造函数的主要问题在于,其定义的方法会在每个实例上 都创建一遍。因此对前面的例子而言,person1 和 person2 都有名为 sayName()的方法,但这两个方 法不是同一个 Function 实例。我们知道,ECMAScript 中的函数是对象,因此每次定义函数时,都会 初始化一个对象。
但创建新 Function 实例的机制是一样的。因此不同实例上的函数虽然同名却不相等如下.
console.log(person1.sayName == person2.sayName); // false
这个新问题可以通过原型模式来解决
原型模式
1 | function Person() {} |
(1)isPrototypeOf()方法
可以使用 isPrototypeOf()方法确定两个对 象之间的这种关系。本质上,isPrototypeOf()会在传入参数的[[Prototype]]指向调用它的对象时 返回 true,如下所示:
1 | console.log(Person.prototype.isPrototypeOf(person1)); // true |
(2)Object.getPrototypeOf()方法
返回参数的内部特性 [[Prototype]]的值。例如:
1 | console.log(Object.getPrototypeOf(person1) == Person.prototype); // true |
(3)setPrototypeOf()方法
Object 类型还有一个 setPrototypeOf()方法,可以向实例的私有特性[[Prototype]]写入一 个新值。这样就可以重写一个对象的原型继承关系:
1 | let biped = { |
为避免使用 Object.setPrototypeOf()可能造成的性能下降,可以通过 Object.create()来创 建一个新对象,同时为其指定原型:
1 | let biped = { |
(4)hasOwnProperty()方法
hasOwnProperty()方法用于确定某个属性是在实例上还是在原型对象上。这个方法是继承自 Object 的,会在属性存在于调用它的对象实例上时返回 true.
in操作符
有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。
在单独使用时,in 操作符会在可 以通过对象访问指定属性时返回 true,无论该属性是在实例上还是在原型上。来看下面的例子:
1 | function Person() {} |
只要通过对象可以访问,in 操作符就返回 true,而 hasOwnProperty()只有属性存在于实例上 时才返回 true。因此,只要 in 操作符返回 true 且 hasOwnProperty()返回 false,就说明该属性 是一个原型属性。
如果要确定某个属性是否存在于原型上,则可以像下 面这样同时使用 hasOwnProperty()和 in 操作符:
1 | function hasPrototypeProperty(object, name){ |
在 for-in 循环中使用 in 操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例 属性和原型属性。遮蔽原型中不可枚举([[Enumerable]]特性被设置为 false)属性的实例属性也会 在 for-in 循环中返回,因为默认情况下开发者定义的属性都是可枚举的。
获取实例属性
要获得对象上所有可枚举的实例属性,可以使用 Object.keys()方法。这个方法接收一个对象作 为参数,返回包含该对象所有可枚举属性名称的字符串数组。
1 | function Person() {} |
如果想列出所有实例属性,无论是否可以枚举,都可以使用 Object.getOwnPropertyNames():
1 | let keys = Object.getOwnPropertyNames(Person.prototype); |
注意,返回的结果中包含了一个不可枚举的属性 constructor。
原型的动态性
因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对 象所做的修改也会在实例上反映出来。下面是一个例子:
1 | let friend = new Person(); |
虽然随时能给原型添加属性和方法,并能够立即反映在所有对象实例上,但这跟重写整个原型是两 回事。实例的[[Prototype]]指针是在调用构造函数时自动赋值的,这个指针即使把原型修改为不同 的对象也不会变。重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。 记住,实例只有指向原型的指针,没有指向构造函数的指针。来看下面的例子:
1 | function Person() {} |
在这个例子中,Person 的新实例是在重写原型对象之前创建的。在调用 friend.sayName()的时 候,会导致错误。这是因为 firend 指向的原型还是最初的原型,而这个原型上并没有 sayName 属性。
原型的问题
原型模式也不是没有问题。首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默 认都取得相同的属性值。虽然这会带来不便,但还不是原型的最大问题。原型的最主要问题源自它的共 享特性。
1 | function Person() {} |
这里,Person.prototype 有一个名为 friends 的属性,它包含一个字符串数组。然后这里创建 了两个 Person 的实例。person1.friends 通过 push 方法向数组中添加了一个字符串。由于这个 friends 属性存在于 Person.prototype 而非 person1 上,新加的这个字符串也会在(指向同一个 数组的)person2.friends 上反映出来。如果这是有意在多个实例间共享数组,那没什么问题。但一 般来说,不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因。
对象迭代
ECMAScript 2017 新增了两 个静态方法,用于将对象内容转换为序列化的——更重要的是可迭代的——格式。这两个静态方法 Object.values()和 Object.entries()接收一个对象,返回它们内容的数组。Object.values() 返回对象值的数组,Object.entries()返回键/值对的数组。
继承
原型链继承
1 | function SuperType() { |
他们得关系如下图.
原型链继承的问题
主要问题出现在原型中包含引用值的时候。
1 | function SuperType() { |
在这个例子中,SuperType 构造函数定义了一个 colors 属性,其中包含一个数组(引用值)。每 个 SuperType 的实例都会有自己的 colors 属性,包含自己的数组。但是,当 SubType 通过原型继承 SuperType 后,SubType.prototype 变成了 SuperType 的一个实例,因而也获得了自己的 colors 属性。这类似于创建了 SubType.prototype.colors 属性。最终结果是,SubType 的所有实例都会 共享这个 colors 属性。这一点通过 instance1.colors 上的修改也能反映到 instance2.colors 上就可以看出来。
优点:1、实例可继承的属性有:实例的构造函数的属性,父类构造函 数属性,父类原型的属性。(新实例不会继承父类实例的属性!)
缺点:1、新实例无法向父类构造函数传参。
2、继承单一。
3、所有新实例都会共享父类实例的属性。(原型上的属性是共 享的,一个实例修改了原型属性,另一个实例的原型属性 也会被修改!)
盗用构造函数
1 | function SuperType() { |
传参
1 | function SuperType(name){ |
盗用构造参数的问题
盗用构造函数的主要缺点,也是使用构造函数模式自定义类型的问题:必须在构造函数中定义方法,因此函数不能重用。此外,子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。
优点:1、只继承了父类构造函数的属性,没有继承父类原型的属性。
2、解决了原型链继承缺点1、2、3。
3、可以继承多个构造函数属性(call多个)。
4、在子实例中可向父实例传参。
缺点:1、只能继承父类构造函数的属性。
2、无法实现构造函数的复用。(每次用每次都要重新调用)
3、每个新实例都有父类构造函数的副本,臃肿。
组合继承
基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方 法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
1 | function SuperType(name){ |
优点:1、可以继承父类原型上的属性,可以传参,可复用。
2、每个新实例引入的构造函数属性是私有的。
缺点:调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。
原型式继承
基本思路如下
1 | function object(o) { |
下面给出一个例子
1 | let person = { |
ECMAScript 5 通过增加 Object.create()方法将原型式继承的概念规范化了。这个方法接收两个 参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时, Object.create()与这里的 object()方法效果相同:
1 | let person = { |
Object.create()的第二个参数与 Object.defineProperties()的第二个参数一样:每个新增 属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性。比如:
1 | let person = { |
原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住, 属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。
寄生式继承
1 | function createAnother(original){ |
可以像下面这样使用 createAnother()函数:
1 | let person = { |
寄生式组合继承
组合继承其实也存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次:一次在是 创建子类原型时调用,另一次是在子类构造函数中调用。
1 | function SuperType(name) { |
寄生式组合继承的基本模式如下所示:
1 | function inheritPrototype(subType, superType) { |
如下例所示,调用 inheritPrototype()就可以实现前面例 子中的子类型原型赋值:
1 | function SuperType(name) { |
原型链仍然保持不变,因此 instanceof 操作符和 isPrototypeOf()方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。
代理和反射
代理基础
代理是使用 Proxy 构造函数创建的。这个构造函数接收两个参数:目标对象和处理程序对象。缺 少其中任何一个参数都会抛出 TypeError。
1 | const target = { |
定义捕获器
使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的“基本操作的 拦截器”。每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作,可以直接 或间接在代理对象上调用。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对 象之前先调用捕获器函数,从而拦截并修改相应的行为。
例如,可以定义一个 get()捕获器,在 ECMAScript 操作以某种形式调用 get()时触发。下面的例 子定义了一个 get()捕获器:
1 | const target = { |
这个操作在 JavaScript 代码中可以通过多种形式触发并被 get()捕获器拦截到。proxy[property]、proxy.property 或 Object.create(proxy)[property]等操作都会触发基本的 get()操作以获取属性。因此所有这些操作只要发生在代理对象上,就会触发 get()捕获 器。注意,只有在代理对象上执行这些操作才会触发捕获器。在目标对象上执行这些操作仍然会产生正 常的行为。
捕获器参数和反射 API
所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为。比如,get() 捕获器会接收到目标对象、要查询的属性和代理对象三个参数。
1 | const target = { |
开发者并不需要手动重建原始行为,而是可以通过调用全局 Reflect 对象上(封装了原始行为)的同名方法来轻松重建。
1 | const target = { |
捕获器不变式
每个 捕获的方法都知道目标对象上下文、捕获函数签名,而捕获处理程序的行为必须遵循“捕获器不变式” (trap invariant)。捕获器不变式因方法不同而异,但通常都会防止捕获器定义出现过于反常的行为。
比如,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的 值时,会抛出 TypeError:
1 | const target = {}; |
撤销代理
1 | const target = { |
代理的问题和不足
代理捕获器与反射方法
代理可以捕获 13 种不同的基本操作。这些操作有各自不同的反射 API 方法、参数、关联 ECMAScript 操作和不变式。
不过,对于 在代理对象上执行的任何一种操作,只会有一个捕获处理程序被调用。不会存在重复捕获的情况。
代理模式
使用代理可以在代码中实现一些有用的编程模式。
跟踪属性访问
通过捕获 get、set 和 has 等操作,可以知道对象属性什么时候被访问、被查询。把实现相应捕获 器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过:
1 | const user = { |
隐藏属性
客户端存储
cookie
HTTP cookie 通常也叫作 cookie,最初用于在客户端存储会话信息。这个规范要求服务器在响应 HTTP 请求时,通过发送 Set-Cookie HTTP 头部包含会话信息。例如,下面是包含这个头部的一个 HTTP 响应:
1 | HTTP/1.1 200 OK |
这个 HTTP 响应会设置一个名为”name”,值为”value”的 cookie。名和值在发送时都会经过 URL 编码。浏览器会存储这些会话信息,并在之后的每个请求中都会通过 HTTP 头部 cookie 再将它们发回服 务器,比如:
1 | GET /index.jsl HTTP/1.1 |
限制
cookie 是与特定域绑定的。设置 cookie 后,它会与请求一起发送到创建它的域。不管域名下面的某一个地址需不需要这个 Cookie ,请求都会携带上完整的 Cookie,这样随着请求数的增多,其实会造成巨大的性能浪费的,因为请求携带了很多不必要的内容。这个限制能保证 cookie 中存储的信息只对被认可的接收者开放,不被其他域访问。
浏览器也会限制 cookie 的大小。大多数浏览器对 cookie 的限制是不超过 4096 字节,上下可以有一 个字节的误差。为跨浏览器兼容,最好保证 cookie 的大小不超过 4095 字节。这个大小限制适用于一个 域的所有 cookie,而不是单个 cookie。
由于 Cookie 以纯文本的形式在浏览器和服务器中传递,很容易被非法用户截获,然后进行一系列的篡改,在 Cookie 的有效期内重新发送给服务器,这是相当危险的。
有效期
默认情况下, 浏览器会话结束后会删除所有 cookie。不过,也可以设置删除 cookie 的时间,用于指定删除 cookie 的具体时间。这样即使关闭 浏览器 cookie 也会保留在用户机器上。把过期时间设置为过去的时间会立即删除 cookie。
小结
因为所有 cookie 都会作为请求头部由浏览器发送给服务器,所以在 cookie 中保存大量信息可能会影 响特定域浏览器请求的性能。保存的 cookie 越大,请求完成的时间就越长。即使浏览器对 cookie 大小有 限制,最好还是尽可能只通过 cookie 保存必要信息,以避免性能问题。
Cookie数据会带到请求头的cookie字段里面,每次同主域名的请求中,都会传递数据,增加了网络请求的数据量,并且造成主域的污染。同时,cookie在不同浏览器上数量和大小都有限制
注意 不要在 cookie 中存储重要或敏感的信息。cookie 数据不是保存在安全的环境中,因 此任何人都可能获得。应该避免把信用卡号或个人地址等信息保存在 cookie 中。
web storage
Web Storage 的第 2 版定义了两个对象:localStorage 和 sessionStorage。localStorage 是永久存储机制,sessionStorage 是跨会话的存储机制。
Storage 的实例与其他 对象一样,但增加了以下方法。
1 | clear():删除所有值;不在 Firefox 中实现。 |
sessionStorage 对象
sessionStorage 对象只存储会话数据,这意味着数据只会存储到浏览器关闭。这跟浏览器关闭时 会消失的会话 cookie 类似。存储在 sessionStorage 中的数据不受页面刷新影响,可以在浏览器崩溃 并重启后恢复。(取决于浏览器,Firefox 和 WebKit 支持,IE 不支持。)
因为 sessionStorage 对象与服务器会话紧密相关,所以在运行本地文件时不能使用。存储在 sessionStorage 对象中的数据只能由最初存储数据的页面使用,在多页应用程序中的用处有限。
sessionStorage 对象应该主要用于存储只在会话期间有效的小块数据。如果需要跨会话持久存储 数据,可以使用localStorage。
localStorage 对象
要访问同一个 localStorage 对象,页面必须来自同一个域(子域不可以)、在相同的端口上使用相同的协议。
存储事件
每当 Storage 对象发生变化时,都会在文档上触发 storage 事件。使用属性或 setItem()设置 值、使用 delete 或 removeItem()删除值,以及每次调用 clear()时都会触发这个事件。这个事件的 事件对象有如下 4 个属性。
1 | domain:存储变化对应的域。 |
可以使用如下代码监听 storage 事件:
1 | window.addEventListener("storage", |
小结
两种存储方法的区别在于,存储在 localStorage 中的数据会保留到通过 JavaScript 删除或者用户清除浏览器缓存。localStorage 数据不受页面刷新影响,也不会因关闭窗口、标签页或重新启动浏览 器而丢失。
IndexedDB
Indexed Database API 简称 IndexedDB,是浏览器中存储结构化数据的一个方案。
IndexedDB 的设计几乎完全是异步的。为此,大多数操作以请求的形式执行,这些请求会异步执行, 产生成功的结果或错误。绝大多数 IndexedDB 操作要求添加 onerror 和 onsuccess 事件处理程序来确 定输出。
使用
使用 IndexedDB 数据库的第一步是调用 indexedDB.open()方法,并给它传入一个要打开的数据 库名称。如果给定名称的数据库已存在,则会发送一个打开它的请求;如果不存在,则会发送创建并打 开这个数据库的请求。这个方法会返回 IDBRequest 的实例,可以在这个实例上添加 onerror 和 onsuccess 事件处理程序。举例如下:
1 | let db, |
如果 onsuccess 事件处理程序被调用,说明可以通过 event.target.result 访问数据库(IDBDatabase)实例了, 这个实例会保存到 db 变量中。之后,所有与数据库相关的操作都要通过 db 对象本身来进行。如果打 开数据库期间发生错误,event.target.errorCode 中就会存储表示问题的错误码。
对象存储
假设要存储包含用户名、密码等内容的用户记录。可以用如下对象来表示一条记录:
1 | let user = { |
可以很容易看出最适合作为对象存储键的 username 属性。用户名必须全局唯一, 它也是大多数情况下访问数据的凭据。这个键很重要,因为创建对象存储时必须指定一个键。
数据库的版本决定了数据库模式,包括数据库中的对象存储和这些对象存储的结构。如果数据库还 不存在,open()操作会创建一个新数据库,然后触发 upgradeneeded 事件。可以为这个事件设置处 理程序,并在处理程序中创建数据库模式。如果数据库存在,而你指定了一个升级版的版本号,则会立 即触发 upgradeneeded 事件,因而可以在事件处理程序中更新数据库模式。
下面的代码演示了为存储上述用户信息如何创建对象存储:
1 | request.onupgradeneeded = (event) => { |
事务
创建了对象存储之后,剩下的所有操作都是通过事务完成的。事务要通过调用数据库对象的 transaction()方法创建。
想要访问多个对象存储,可以给第 一个参数传入一个字符串数组:
1 | let transaction = db.transaction(["users", "anotherStore"]); |
如前所述,每个事务都以只读方式访问数据。要修改访问模式,可以传入第二个参数。这个参数应 该是下列三个字符串之一:”readonly”、”readwrite”或”versionchange”。比如:
1 | let transaction = db.transaction("users", "readwrite"); |
网络请求和跨域
XMLHttpRequest
1 | let xhr = new XMLHttpRequest(); |
使用 XHR 对象首先要调用 open()方法,这个方法接收 3 个参数:请求类型(”get”、”post”等)、 请求 URL,以及表示请求是否异步的布尔值。send()方法接收一个参数,是作为请求体发送的数据。如果不需要发送请求体,则必须传 null, 因为这个参数在某些浏览器中是必需的。调用 send()之后,请求就会发送到服务器。下面是一个例子:
1 | xhr.open("get", "example.txt", false); |
收到响应后,XHR 对象的以下属性会被填充上数据。
1 | responseText:作为响应体返回的文本。 |
跨源资源共享
跨源资源共享(CORS,Cross-Origin Resource Sharing)定义了浏览器与服务器如何实现跨源通信。 CORS 背后的基本思路就是使用自定义的 HTTP 头部允许浏览器和服务器相互了解,以确实请求或响应 应该成功还是失败。
对于简单的请求,比如 GET 或 POST 请求,没有自定义头部,而且请求体是 text/plain 类型, 这样的请求在发送时会有一个额外的头部叫 Origin。Origin 头部包含发送请求的页面的源(协议、 域名和端口),以便服务器确定是否为其提供响应。下面是 Origin 头部的一个示例:
Origin: http://www.nczonline.net
如果服务器决定响应请求,那么应该发送 Access-Control-Allow-Origin 头部,包含相同的源; 或者如果资源是公开的,那么就包含”*”。比如:
Access-Control-Allow-Origin: http://www.nczonline.net
如果没有这个头部,或者有但源不匹配,则表明不会响应浏览器请求。否则,服务器就会处理这个 请求。注意,无论请求还是响应都不会包含 cookie 信息。
跨域方法
XMLHttpRequest 对象原生支持 CORS
在尝试访问不同源的资源时,这个行为 会被自动触发。要向不同域的源发送请求,可以使用标准 XHR对象并给 open()方法传入一个绝对 URL, 比如:
1 | let xhr = new XMLHttpRequest(); |
跨域 XHR 对象允许访问 status 和 statusText 属性,也允许同步请求。出于安全考虑,跨域 XHR 对象也施加了一些额外限制:
1 | 不能使用 setRequestHeader()设置自定义头部。 |
图片探测
图片探测是利用标签实现跨域通信的最早的一种技术。任何页面都可以跨域加载图片而不 必担心限制,因此这也是在线广告跟踪的主要方式。可以动态创建图片,然后通过它们的 onload 和 onerror 事件处理程序得知何时收到响应。
1 | let img = new Image(); |
图片探测频繁用于跟踪用户在页面上的点击操作或动态显示广告。当然,图片探测的缺点是只能发送 GET 请求和无法获取服务器响应的内容。这也是只能利用图片探测实现浏览器与服务器单向通信的 原因。
JSONP
JSONP 格式包含两个部分:回调和数据。回调是在页面接收到响应之后应该调用的函数,通常回调 函数的名称是通过请求来动态指定的。而数据就是作为参数传给回调函数的 JSON 数据。下面是一个典 型的 JSONP 请求:
http://freegeoip.net/json/?callback=handleResponse
这个 JSONP 请求的 URL 是一个地理位置服务。JSONP 服务通常支持以查询字符串形式指定回调函 数的名称。比如这个例子就把回调函数的名字指定为 handleResponse()。
JSONP 调用是通过动态创建