JS高级程序设计(四)读书小记

74 阅读40分钟

参考: 《js高程》(四)

第二章 HTML中的JavaScript

推迟执行脚本 defer

HTML 4.01 为

异步执行脚本 async

也都只适用于外部脚本,都会告诉浏览器立即开始下载。不过,与 defer 不同的是,标记为 async 的脚本并不保证能按照它们出现的次序执行。

因此,重点在于它们之间没有依赖关系。给脚本添加 async 属性的目的是告诉浏览器,不必等脚本下载和执行完后再加载页面,同样也不必等到 该异步脚本下载和执行后再加载其他脚本。正因为如此,异步脚本不应该在加载期间修改 DOM。

异步脚本保证会在页面的 load 事件前执行,但可能会在 DOMContentLoaded之前或之后。

动态加载脚本

所以通过 向 DOM 中动态添加 script 元素同样可以加载指定的脚本。只要创建一个 script 元素并将其添加到 DOM 即可。

let script = document.createElement('script'); 
script.src = 'gibberish.js'; 
document.head.appendChild(script);

在把 HTMLElement 元素添加到 DOM 且执行到这段代码之前不会发送请求。默认情况下,以这种方式创建的

let script = document.createElement('script');
script.src = 'gibberish.js';
script.async = false;
document.head.appendChild(script); 

以这种方式获取的资源对浏览器预加载器是不可见的。这会严重影响它们在资源获取队列中的优先 级。这种方式可能会严重影响性能。要想让预加载器知道这些 动态请求文件的存在,可以在文档头部显式声明它们:

<link rel="preload" href="gibberish.js"> 

第三章 语言基础

语法

语句

ECMAScript 中的语句以分号结尾。省略分号意味着由解析器确定语句在哪里结尾,加分号有助于防止省略造成的问题,比如可以避免输入内容不完整。此外,加分号也便于开发者通过删除空行来压缩代码(如果没有结尾的分号,只删除空行,则会导致语法错误)。加分号也有助于在某些情况下提升性能,因为解析器会尝试在合适的位置补上分号以纠正语法错误。

变量

const声明变量时必须同时初始化变量,且 尝试修改 const 声明的变量会导致运行时错误。

const 声明的限制只适用于它指向的变量的引用。换句话说,如果 const 变量引用的是一个对象, 那么修改这个对象内部的属性并不违反 const 的限制。

JavaScript 引擎会为 for 循环中的 let 声明分别创建独立的变量实例,虽然 const 变量跟 let 变 量很相似,但是不能用 const 来声明迭代变量(因为迭代变量会自增)。

数据类型

undefined

一般来说,永远不用显式地给某个变量设置 undefined 值。字面值 undefined 主要用于比较,而且在 ECMA-262 第 3 版之前是不存在的。增加这个特殊值的目的就是为 了正式明确空对象指针(null)和未初始化变量的区别。

无论是声明还是未声明,typeof 返回的都是字符串"undefined"。逻辑上讲这是对的,因为虽然 严格来讲这两个变量存在根本性差异,但它们都无法执行实际操作。

即使未初始化的变量会被自动赋予 undefined 值,但我们仍然建议在声明变量的 同时进行初始化。这样,当 typeof 返回"undefined"时,你就会知道那是因为给定的变 量尚未声明,而不是声明了但未初始化。

Null

Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回"object"的原因。

在定义将来要保存对象值的变量时,建议使用 null 来初始化,不要使用其他值。这样,只要检查 这个变量的值是不是 null 就可以知道这个变量是否在后来被重新赋予了一个对象的引用。

即使 null 和 undefined 有关系,它们的用途也是完全不一样的。如前所述,永远不必显式地将 变量值设置为 undefined。但 null 不是这样的。任何时候,只要变量要保存对象,而当时又没有那个 对象可保存,就要用 null 来填充该变量。这样就可以保持 null 是空对象指针的语义,并进一步将其 与 undefined 区分开来。

Object

Boolean

String

Number

Number()函数基于如下规则执行转换(一元加操作符与 Number()函数遵循相同的转换规则)

    • 布尔值,true 转换为 1,false 转换为 0。
    • 数值,直接返回。
    • null,返回 0。
    • undefined,返回 NaN。
    • 字符串,应用以下规则。
      • 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。因此,Number("1")返回 1,Number("123")返回 123,Number("011")返回 11(忽略前面的零)。
      • 如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
      • 如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整数值。
      • 如果是空字符串(不包含字符),则返回 0。
      • 如果字符串包含除上述情况之外的其他字符,则返回 NaN。
      • 对象,调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用toString()方法,再按照转换字符串的规则转换。

parseInt()

    • parseInt()函数更专注于字符串是否包含数值模式。字符串最前面的空格会被 忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt()立即 返回 NaN。这意味着空字符串也会返回 NaN(这一点跟 Number()不一样,它返回 0)。如果第一个字符 是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。
    • parseInt()也接收第二个参数,用于指定进制数

Symbol

Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。 符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

符号就是用来创建唯一记号,进而用作非 字符串形式的对象属性。

使用全局符号注册表

运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册 表中创建并重用符号。

let fooGlobalSymbol = Symbol.for('foo');
console.log(typeof fooGlobalSymbol); // symbol

Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运 行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同 字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。

let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号
let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号
console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true

即使采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol()定义的符号也并不等同:

let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false 

全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for()的任何值都会被转换为字符串。此外,注册表中使用的键同时也会被用作符号描述。

let emptyGlobalSymbol = Symbol.for();
console.log(emptyGlobalSymbol); // Symbol(undefined)

还可以使用 Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字 符串键。如果查询的不是全局符号,则返回 undefined。

// 创建全局符号
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo

// 创建普通符号
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined

//如果传给 Symbol.keyFor()的不是符号,则该方法抛出 TypeError:
Symbol.keyFor(123); // TypeError: 123 is not a symbol 

使用符号作为属性

凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性和Object.defineProperty()/Object.defineProperties()定义的属性。对象字面量只能在计算属 性语法中使用符号作为属性。

Object.getOwnPropertyNames()返回对象实例的常规属性数组;

Object.getOwnPropertySymbols()返回对象实例的符号属性数组;

Object.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象;

Reflect.ownKeys()会返回两种类型 的键:

let s1 = Symbol('foo'),
 		s2 = Symbol('bar');
let o = {
 [s1]: 'foo val',
 [s2]: 'bar val',
 baz: 'baz val',
 qux: 'qux val'
};
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(foo), Symbol(bar)]
console.log(Object.getOwnPropertyNames(o));
// ["baz", "qux"]
console.log(Object.getOwnPropertyDescriptors(o));
// {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}}
console.log(Reflect.ownKeys(o));
// ["baz", "qux", Symbol(foo), Symbol(bar)]

Symbol.asyncIterator

这个符号作为一个属性表示“一个方法,该方法返回对象默认的 AsyncIterator。 由 for-await-of 语句使用”。换句话说,这个符号表示实现异步迭代器 API 的函数。

for-await-of 循环会利用这个函数执行异步迭代操作。循环时,它们会调用以 Symbol.asyncIterator 为键的函数,并期望这个函数会返回一个实现迭代器 API 的对象。很多时候,返回的对象是实现该 API 的 AsyncGenerator:

class Foo {
 async *[Symbol.asyncIterator]() {}
}
let f = new Foo();
console.log(f[Symbol.asyncIterator]());
// AsyncGenerator {<suspended>} 

Symbol.hasInstance

这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由 instanceof 操作符使用”。

在 ES6 中,instanceof 操作符会使用 Symbol.hasInstance 函数来确定关系。以 Symbol. hasInstance 为键的函数会执行同样的操作,只是操作数对调了一下:

function Foo() {}
let f = new Foo();
console.log(f instanceof Foo); // true
class Bar {}
let b = new Bar();
console.log(b instanceof Bar); // true

// 使用Symbol.hasInstance:
function Foo() {}
let f = new Foo();
console.log(Foo[Symbol.hasInstance](f)); // true
class Bar {}
let b = new Bar();
console.log(Bar[Symbol.hasInstance](b)); // true

这个属性定义在 Function 的原型上,因此默认在所有函数和类上都可以调用。由于 instanceof 操作符会在原型链上寻找这个属性定义,就跟在原型链上寻找其他属性一样,因此可以在继承的类上通 过静态方法重新定义这个函数

class Bar {}
class Baz extends Bar {
 static [Symbol.hasInstance]() {
 return false;
 }
}
let b = new Baz();
console.log(Bar[Symbol.hasInstance](b)); // true
console.log(b instanceof Bar); // true
console.log(Baz[Symbol.hasInstance](b)); // false
console.log(b instanceof Baz); // false

Symbol.isConcatSpreadable

这个符号作为一个属性表示“一个布尔值,如果是 true,则意味着对象应 该用 Array.prototype.concat()打平其数组元素。

Array.prototype.concat()方法会 根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。覆盖 Symbol.isConcatSpreadable 的值可以修改这个行为。

数组对象默认情况下会被打平到已有的数组,false 或假值会导致整个对象被追加到数组末尾;

类数组对象默认情况下会被追加到数组末尾,true 或真值会导致这个类数组对象被打平到数组实例;

其 他不是类数组对象的对象在 Symbol.isConcatSpreadable 被设置为 true 的情况下将被忽略。

// 数组
let initial = ['foo'];
let array = ['bar'];
console.log(array[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(array)); // ['foo', 'bar']
array[Symbol.isConcatSpreadable] = false;
console.log(initial.concat(array)); // ['foo', Array(1)] 

// 类数组
let arrayLikeObject = { length: 1, 0: 'baz' };
console.log(arrayLikeObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(arrayLikeObject)); // ['foo', {...}]
arrayLikeObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(arrayLikeObject)); // ['foo', 'baz']

// 其他不是类数组对象的对象
let otherObject = new Set().add('qux');
console.log(otherObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(otherObject)); // ['foo', Set(1)]
otherObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(otherObject)); // ['foo']

Symbol.iterator

这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。 由 for-of 语句使用”。换句话说,这个符号表示实现迭代器 API 的函数。

for-of 循环这样的语言结构会利用这个函数执行迭代操作。循环时,它们会调用以 Symbol.iterator 为键的函数,并默认这个函数会返回一个实现迭代器 API 的对象。很多时候,返回的对象是实现该 API 的 Generator:

class Emitter {
 constructor(max) {
 this.max = max;
 this.idx = 0;
 }
 *[Symbol.iterator]() {
   while(this.idx < this.max) {
   yield this.idx++;
   }
 }
}
function count() {
 let emitter = new Emitter(5);
 for (const x of emitter) {
 console.log(x);
 }
}
count(); // 0 1 2 3 4 

Symbol.match、Symbol.replace、Symbol.search、Symbol.split

改符号表示正则表达式的方法,正则表达式的原型上默认有这个函数的定义, 因此所有正则表达式实例默认是这个 String 方法的有效参数。给这些方法传入非正则表达式值会导致该值被转换为 RegExp 对象。如果想改变这种行为,让方法 直接使用参数,则可以重新定义 Symbol.match/ Symbol.replace/Symbol.search/ Symbol.split函数以取代默认对正则表达式求值的行为

class FooMatcher {
 static [Symbol.match](target) {
 return target.includes('foo');
 }
}
console.log('foobar'.match(FooMatcher)); // true
console.log('barbaz'.match(FooMatcher)); // false
class StringMatcher {
 constructor(str) {
 this.str = str;
 }
 [Symbol.match](target) {
 return target.includes(this.str);
 }
}
console.log('foobar'.match(new StringMatcher('foo'))); // true
console.log('barbaz'.match(new StringMatcher('qux'))); // false 

Symbol.species

允许子类覆盖对象的默认构造函数。

class MyArray extends Array {
  // 覆盖 species 到父级的 Array 构造函数上
  static get [Symbol.species]() { return Array; }
}
var a = new MyArray(1,2,3);
var mapped = a.map(x => x * x);

console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array);   // true

Symbol.toPrimitive

该符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始 值。由 ToPrimitive 抽象操作使用”。很多内置操作都会尝试强制将对象转换为原始值,包括字符串、 数值和未指定的原始类型。对于一个自定义对象实例,通过在这个实例的 Symbol.toPrimitive 属性 上定义一个函数可以改变默认行为。

Symbol.toStringTag

表示“一个字符串,该字符串用于创建对象的默认 字符串描述。由内置方法Object.prototype.toString()使用。

通过 toString()方法获取对象标识时,会检索由 Symbol.toStringTag 指定的实例标识符,默 认为"Object"。内置类型已经指定了这个值,但自定义类实例还需要明确定义

let s = new Set();
console.log(s.toString()); // [object Set]
console.log(s[Symbol.toStringTag]); // Set 
// 自定义对象
class Bar {
 constructor() {
 this[Symbol.toStringTag] = 'Bar';
 }
}
let bar = new Bar();
console.log(bar.toString()); // [object Bar]
console.log(bar[Symbol.toStringTag]); // Bar 

第四章 变量、作用域与内存

  • 传递参数

ECMAScript 中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数 中,就像从一个变量复制到另一个变量一样。如果是原始值,那么就跟原始值变量的复制一样,如果是 引用值,那么就跟引用值变量的复制一样。变量有按 值和按引用访问,而传参则只有按值传递。

ECMAScript 中函数的参数就是局部变量。

执行上下文与作用域

变量或函数的上下文决定 了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object), 而这个上下文中定义的所有变量和函数都存在于这个对象上。

上下文在其所有代码都执行完毕后会被销毁,包括定义 在它上面的所有变量和函数。

每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。 在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。

上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定 了各级上下文中的代码在访问变量和函数时的顺序。

第五章 基本引用类型

Data

Date.parse()、Date.UTC()、Date.now()

RegExp

  • 匹配模式的标记
    • g:全局模式,表示查找字符串的全部内容,而不是找到第一个匹配的内容就结束。
    • i:不区分大小写,表示在查找匹配时忽略 pattern 和字符串的大小写。
    • m:多行模式,表示查找到一行文本末尾时会继续查找。
    • y:粘附模式,表示只查找从 lastIndex 开始及之后的字符串。
    • u:Unicode 模式,启用 Unicode 匹配。
    • s:dotAll 模式,表示元字符.匹配任何字符(包括\n 或\r)。
  • RegExp 实例属性
    • global:布尔值,表示是否设置了 g 标记
    • ignoreCase:布尔值,表示是否设置了 i 标记
    • unicode:布尔值,表示是否设置了 u 标记
    • sticky:布尔值,表示是否设置了 y 标记
    • lastIndex:整数,表示在源字符串中下一次搜索的开始位置,始终从 0 开始
    • multiline:布尔值,表示是否设置了 m 标记
    • dotAll:布尔值,表示是否设置了 s 标记
    • source:正则表达式的字面量字符串
    • flags:正则表达式的标记字符串
  • RegExp 实例方法
    • exec()
    • test()

原始值包装类型

引用类型与原始值包装类型的主要区别在于对象的生命周期。在通过 new 实例化引用类型后,得到 的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期 间。这意味着不能在运行时给原始值添加属性和方法。

单例内置对象

Global Math

第七章 迭代器与生成器

迭代器是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。任何实现 Iterable 接口的对象都有一个 Symbol.iterator 属性,这个属性引用默认迭代器。默认迭代器就像一个迭代器 工厂,也就是一个函数,调用之后会产生一个实现 Iterator 接口的对象。

迭代器必须通过连续调用 next()方法才能连续取得值,这个方法返回一个 IteratorObject。这 个对象包含一个 done 属性和一个 value 属性。前者是一个布尔值,表示是否还有更多值可以访问;后 者包含迭代器返回的当前值。这个接口可以通过手动反复调用 next()方法来消费,也可以通过原生消 费者,比如 for-of 循环来自动消费。

生成器是一种特殊的函数,调用之后会返回一个生成器对象。生成器对象实现了 Iterable 接口, 因此可用在任何消费可迭代对象的地方。生成器的独特之处在于支持 yield 关键字,这个关键字能够 暂停执行生成器函数。使用 yield 关键字还可以通过 next()方法接收输入和产生输出。在加上星号之 后,yield 关键字可以将跟在它后面的可迭代对象序列化为一连串值。

第8章 对象、类与面向对象编程

  • 与函数定义不同的是,虽然函数声明可以提升,但类定义不能;
  • 另一个跟函数声明不同的地方是,函数受函数作用域限制,而类受块作用域限制。

类构造函数

  • 实例化

使用 new 操作符实例化 Person 的操作等于使用 new 调用其构造函数。唯一可感知的不同之处就 是,JavaScript 解释器知道使用 new 和类意味着应该使用 constructor 函数进行实例化。 使用 new 调用类的构造函数会执行如下操作。

(1) 在内存中创建一个新对象。

(2) 这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性。

(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。

(4) 执行构造函数内部的代码(给新对象添加属性)。

(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

  • 类构造函数与构造函数的主要区别是,调用类构造函数必须使用 new 操作符。而普通构造函数如果 不使用 new 调用,那么就会以全局的 this(通常是 window)作为内部对象。

实例、原型和类成员

  • 原型方法与访问器
    • 添加到 this 的所有内容都会存在于不同的实例上
    • 在类块中定义的所有内容都会定义在类的原型上
  • 类方法等同于对象属性,因此可以使用字符串、符号或计算的值作为键

继承

  • es6的继承不仅可以继承一个类,也可以继承普通的构造函数
  • 构造函数、HomeObject 和 super()
    • 派生类的方法可以通过 super 关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅 限于类构造函数、实例方法和静态方法内部
  • 在使用 super 时要注意几个问题
    • super 只能在派生类构造函数和静态方法中使用。
class Vehicle {
 constructor() {
 super();
 // SyntaxError: 'super' keyword unexpected
 }
} 
    • 不能单独引用 super 关键字,要么用它调用构造函数,要么用它引用静态方法。
class Vehicle {}
class Bus extends Vehicle {
 constructor() {
 console.log(super);
 // SyntaxError: 'super' keyword unexpected here
 }
} 
    • 调用 super()会调用父类构造函数,并将返回的实例赋值给 this
class Vehicle {}
class Bus extends Vehicle {
 constructor() {
 super();
 console.log(this instanceof Vehicle);
 }
}
new Bus(); // true
    • super()的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入
class Vehicle {
 constructor(licensePlate) {
 this.licensePlate = licensePlate;
 }
}
class Bus extends Vehicle {
 constructor(licensePlate) {
 super(licensePlate);
 }
}
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' } 
    • 如果没有定义类构造函数,在实例化派生类时会调用 super(),而且会传入所有传给派生类的 参数。
class Vehicle {
 constructor(licensePlate) {
 this.licensePlate = licensePlate;
 }
}
class Bus extends Vehicle {}
console.log(new Bus('1337H4X')); // Bus { licensePlate: '1337H4X' } 
    • 如果在派生类中显式定义了构造函数,则要么必须在其中调用 super(),要么必须在其中返回 一个对象。
class Vehicle {}
class Car extends Vehicle {}
class Bus extends Vehicle {
 constructor() {
 super();
 }
}
class Van extends Vehicle {
 constructor() {
 return {};
 }
}
console.log(new Car()); // Car {}
console.log(new Bus()); // Bus {}
console.log(new Van()); // {}

第9章 代理与反射

代理是目标对象的抽象。具体地 说,可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在对 目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。

定义捕获器

捕获器就是在处理程序对象中定义的“基本操作的 拦截器”。每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作,可以直接 或间接在代理对象上调用。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对 象之前先调用捕获器函数,从而拦截并修改相应的行为;在目标对象上执行这些操作仍然会产生正 常的行为。

可撤销代理

Proxy 也暴露了 revocable()方法,这个方法支持撤销代理对象与目标对象的关联。撤销代理的 操作是不可逆的。而且,撤销函数(revoke())是幂等的,调用多少次的结果都一样。撤销代理之后 再调用代理会抛出 TypeError。

const target = {
 foo: 'bar'
};
const handler = {
 get() {
 return 'intercepted';
 }
};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.foo); // intercepted
console.log(target.foo); // bar
revoke();
console.log(proxy.foo); // TypeError

实用反射 API

1. 反射 API 与对象 API

(1) 反射 API 并不限于捕获处理程序;

(2) 大多数反射 API 方法在 Object 类型上有对应的方法。 通常,Object 上的方法适用于通用程序,而反射方法适用于细粒度的对象控制与操作。

2. 状态标记

很多反射方法返回称作“状态标记”的布尔值,表示意图执行的操作是否成功。有时候,状态标记 比那些返回修改后的对象或者抛出错误(取决于方法)的反射 API 方法更有用。

// 初始代码
const o = {};
try {
 Object.defineProperty(o, 'foo', 'bar');
 console.log('success');
} catch(e) {
 console.log('failure');
}
// 重构后的代码
const o = {};
if(Reflect.defineProperty(o, 'foo', {value: 'bar'})) {
 console.log('success');
} else {
 console.log('failure');
} 

3. 用一等函数替代操作符

以下反射方法提供只有通过操作符才能完成的操作。

 Reflect.get():可以替代对象属性访问操作符。

 Reflect.set():可以替代=赋值操作符。

 Reflect.has():可以替代 in 操作符或 with()。

 Reflect.deleteProperty():可以替代 delete 操作符。

 Reflect.construct():可以替代 new 操作符。

4. 安全地应用函数

Function.prototype.apply.call(myFunc, thisVal, argumentList);
// 可以使用 Reflect.apply 来避免
Reflect.apply(myFunc, thisVal, argumentsList); 

代理的问题与不足

  • 代理中的this
  • 代理与内部槽位

代理捕获器与反射方法

参考:developer.mozilla.org/zh-CN/docs/…

代理模式

跟踪属性访问

通过捕获 get、set 和 has 等操作,可以知道对象属性什么时候被访问、被查询。把实现相应捕获 器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过:

const user = {
 name: 'Jake'
};
const proxy = new Proxy(user, {
 get(target, property, receiver) {
   console.log(`Getting ${property}`);
   return Reflect.get(...arguments);
 },
 set(target, property, value, receiver) {
   console.log(`Setting ${property}=${value}`);
   return Reflect.set(...arguments);
 }
});
proxy.name; // Getting name
proxy.age = 27; // Setting age=27

隐藏属性

代理的内部实现对外部代码是不可见的,因此也可以隐藏目标对象上的属性。

const hiddenProperties = ['foo', 'bar'];
const targetObject = {
 foo: 1,
 bar: 2,
 baz: 3
};
const proxy = new Proxy(targetObject, {
 get(target, property) {
   if (hiddenProperties.includes(property)) {
    return undefined;
   } else {
    return Reflect.get(...arguments);
   }
 },
 has(target, property) { 
   if (hiddenProperties.includes(property)) {
 		return false;
   } else {
   	return Reflect.has(...arguments);
   }
 }
});
// get()
console.log(proxy.foo); // undefined
console.log(proxy.bar); // undefined
console.log(proxy.baz); // 3
// has()
console.log('foo' in proxy); // false
console.log('bar' in proxy); // false
console.log('baz' in proxy); // true 

属性验证

因为所有赋值操作都会触发 set()捕获器,所以可以根据所赋的值决定是允许还是拒绝赋值

const target = {
 onlyNumbersGoHere: 0
};
const proxy = new Proxy(target, {
 set(target, property, value) {
   if (typeof value !== 'number') {
   	return false;
   } else {
   	return Reflect.set(...arguments);
   }
 }
});
proxy.onlyNumbersGoHere = 1;
console.log(proxy.onlyNumbersGoHere); // 1
proxy.onlyNumbersGoHere = '2';
console.log(proxy.onlyNumbersGoHere); // 1

函数与构造函数参数验证

跟保护和验证对象属性类似,也可对函数和构造函数参数进行审查。比如,可以让函数只接收某种 类型的值

function median(...nums) {
 return nums.sort()[Math.floor(nums.length / 2)];
}
const proxy = new Proxy(median, {
 apply(target, thisArg, argumentsList) {
   for (const arg of argumentsList) {
   if (typeof arg !== 'number') {
   	throw 'Non-number argument provided';
   }
 } 
 	return Reflect.apply(...arguments);
 }
});
console.log(proxy(4, 7, 1)); // 4
console.log(proxy(4, '7', 1));
// Error: Non-number argument provided
// 类似地,可以要求实例化时必须给构造函数传参:
class User {
 constructor(id) {
 	this.id_ = id;
 }
}
const proxy = new Proxy(User, {
 construct(target, argumentsList, newTarget) {
   if (argumentsList[0] === undefined) {
    throw 'User cannot be instantiated without id';
   } else {
    return Reflect.construct(...arguments);
   }
 }
});
new proxy(1);
new proxy();
// Error: User cannot be instantiated without id

数据绑定与可观察对象

const userList = [];
class User {
 constructor(name) {
 	this.name_ = name;
 }
}
const proxy = new Proxy(User, {
 construct() {
 	const newUser = Reflect.construct(...arguments);
 	userList.push(newUser);
 	return newUser;
 }
});
new proxy('John');
new proxy('Jacob');
new proxy('Jingleheimerschmidt');
console.log(userList); // [User {}, User {}, User{}]
//--------------------------------------------------
const userList = [];
function emit(newValue) {
 console.log(newValue);
}
const proxy = new Proxy(userList, {
 set(target, property, value, receiver) {
   const result = Reflect.set(...arguments);
   if (result) {
    emit(Reflect.get(target, property, receiver));
   }
 	return result;
 }
});
proxy.push('John');
// John
proxy.push('Jacob');
// Jacob 

函数

理解参数

第四章:理解传递参数

ECMAScript 中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数 中,就像从一个变量复制到另一个变量一样。如果是原始值,那么就跟原始值变量的复制一样,如果是 引用值,那么就跟引用值变量的复制一样。变量有按 值和按引用访问,而传参则只有按值传递。

ECMAScript 中函数的参数就是局部变量。

ECMAScript 中的所有参数都按值传递的。如果把对象作 为参数传递,那么传递的值就是这个对象的引用。

没有重载

  • 没有函数签名(接收参数的类型和数量),自然也就没有重载;
  • 函数名是指针,定义两个同 名的函数会导致后定义的重写先定义的。

参数默认值

在使用默认参数时,arguments 对象的值不反映参数的默认值,只反映传给函数的参数。当然, 跟 ES5 严格模式一样,修改命名参数也不会影响 arguments 对象,它始终以调用函数时传入的值为准

function makeKing(name = 'Henry') {
 name = 'Louis';
 return `King ${arguments[0]}`;
}
console.log(makeKing()); // 'King undefined'
console.log(makeKing('Louis')); // 'King Louis'

默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值:函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。而且,计算默认值的函数 只有在调用函数但未传相应参数时才会被调用。

函数内部

  • arguments
  • this
  • 函数的caller、arguments.caller、arguments.callee
  • new.target: ECMAScript 6 新增, 是否使用 new 关键字调用的 new.target 属性.如果函数是正常调用的, 则 new.target 的值是 undefined;如果是使用 new 关键字调用的,则 new.target 将引用被调用的 构造函数。

函数表达式

  • 理解函数声明与函数表达式之间的区别,关键是理解提升
  • 任何时候, 只要函数被当作值来使用,它就是一个函数表达式

递归

  • 命名函数表达式
// 可以确保无论通过什么变量调用这个函数都不会出问题;
// 在严格模式下运行的代码是不能访问 arguments.callee 的,因为访问会出错
function factorial(num) {
 if (num <= 1) {
 	return 1;
 } else {
 	return num * arguments.callee(num - 1);
 }
} 
// 任何模式下都可以使用
const factorial = (function f(num) {
 if (num <= 1) {
 	return 1;
 } else {
 	return num * f(num - 1);
 }
});

尾调用优化

尾调用:即外部函数的返回值是一个内部函数的返回值。ES6新增的一项内存管理优化机制,让 JavaScript 引擎在满足条件时可以重用栈帧。

尾调用无论有少次嵌套函数,都只有一个栈帧。这就是 ES6 尾调用优化的关键:如果函数的逻辑允许基于尾调用将其 销毁,则引擎就会那么做。

条件

  • 代码在严格模式下执行;
  • 外部函数的返回值是对尾调用函数的调用;
  • 尾调用函数返回后不需要执行额外的逻辑;
  • 尾调用函数不是引用外部函数作用域中自由变量的闭包。

闭包

闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

函数执行时,每个执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫变量对象,它 会在代码执行期间始终存在。而函数局部上下文中的叫活动对象,只在函数执行期间存在。

作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象。

函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量。函数执行完毕后,局 部活动对象会被销毁,内存中就只剩下全局作用域。

this

如果内部函数没有使用箭头函数定义,则 this 对象会在运 行时绑定到执行函数的上下文。

如果在全局函数中调用,则 this 在非严格模式下等于 window,在严 格模式下等于 undefined。

如果作为某个对象的方法调用,则 this 等于这个对象。

匿名函数在这种情 况下不会绑定到某个对象,这就意味着 this 会指向 window,除非在严格模式下 this 是 undefined。

内存泄漏

/* 
* 由于 IE 在 IE9 之前对 JScript 对象和 COM 对象使用了不同的垃圾回收机制(第 4 章讨论过),所以
* 闭包在这些旧版本 IE 中可能会导致问题。在这些版本的 IE 中,把 HTML 元素保存在某个闭包的作用域
* 中,就相当于宣布该元素不能被销毁。
*/
// 优化前
function assignHandler() {
 let element = document.getElementById('someElement');
 element.onclick = () => console.log(element.id);
}

// 优化后
function assignHandler() {
 let element = document.getElementById('someElement');
 let id = element.id;
 element.onclick = () => console.log(id);
 element = null;
}

期约与异步函数

异步编程

同步行为和异步行为的对立统一是计算机科学的一个基本概念。特别是在 JavaScript 这种单线程事 件循环模型中,同步操作与异步操作更是代码所要依赖的核心机制。异步行为是为了优化因计算量大而 时间长的操作。如果在等待其他操作完成的同时,即使运行其他指令,系统也能保持稳定。

同步异步

同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行,而每 条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。这样的执行流程容易分析 程序在执行到代码任意位置时的状态(比如变量的值)。

异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。异步操作经常是必 要的,因为强制进程等待一个长时间的操作通常是不可行的(同步操作则必须要等)。如果代码要访问 一些高延迟的资源,比如向远程服务器发送请求并等待响应,那么就会出现长时间的等待。

期约基础

期约状态机

待定(pending) 是期约的最初始状态。在待定状态下,期约可以落定(settled)为代表成功的兑现 (fulfilled) 状态,或者代表失败的拒绝 (rejected) 状态。无论落定为哪种状态都是不可逆的。只要从待 定转换为兑现或拒绝,期约的状态就不再改变。而且,也不能保证期约必然会脱离待定状态。因此,组 织合理的代码无论期约解决(resolve)还是拒绝(reject),甚至永远处于待定(pending)状态,都应该 具有恰当的行为。

重要的是,期约的状态是私有的,不能直接通过 JavaScript 检测到。这主要是为了避免根据读取到 的期约状态,以同步方式处理期约对象。 另外,期约的状态也不能被外部 JavaScript 代码修改。这与不 能读取该状态的原因是一样的:期约故意将异步行为封装起来,从而隔离外部的同步代码。

try {
 throw new Error('foo');
} catch(e) {
 console.log(e); // Error: foo
}
// ===================================
try {
 Promise.reject(new Error('bar'));
} catch(e) {
 console.log(e);
}
// Uncaught (in promise) Error: bar

这里的同步代码之所以没有捕获promise抛出的错误,是因为它没有通过异步模式捕获错误。在前面的例子中,拒绝promise抛出的错误并没有抛到执行同步代码的线程里,而是通过浏览器异步消息队 列来处理的。因此,try/catch 块并不能捕获该错误。代码一旦开始以异步模式执行,则唯一与之交互 的方式就是使用异步结构——更具体地说,就是promise的方法。

拒绝期约与拒绝错误处理

then()和 catch()的 onRejected 处理程序在语义上相当于 try/catch。出发点都是捕获错误之 后将其隔离,同时不影响正常逻辑执行。为此,onRejected 处理程序的任务应该是在捕获异步错误之 后返回一个解决的期约

期约连锁与期约合成

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>取消promise</title>
</head>
<body>
    <button id="start">Start</button>
    <button id="cancel">Cancel</button> 
<script>
    class CancelToken {
        constructor(cancelFn) {
            this.promise = new Promise((resolve, reject) => {
                cancelFn(() => {
                    setTimeout(console.log, 0, "delay cancelled");
                    resolve();
                });
            });
        }
    }

    const startButton = document.querySelector('#start');
    const cancelButton = document.querySelector('#cancel');
    
    function cancellableDelayedResolve(delay) {
        setTimeout(console.log, 0, "set delay");

        return new Promise((resolve, reject) => {
            const id = setTimeout((() => {
                setTimeout(console.log, 0, "delayed resolve");
                resolve();
            }), delay);
            const cancelToken = new CancelToken((cancelCallback) => cancelButton.addEventListener("click", cancelCallback));
            cancelToken.promise.then(() => clearTimeout(id));
        });
    }
    startButton.addEventListener("click", () => cancellableDelayedResolve(1000));
</script> 
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>进度通知</title>
</head>
<body>
    
</body>
<script>
class TrackablePromise extends Promise {
    constructor(executor) {
        const notifyHandlers = [];
        super((resolve, reject) => {
            return executor(resolve, reject, (status) => {
                notifyHandlers.map((handler) => {
                    return handler(status)
                });
            });
        });
        this.notifyHandlers = notifyHandlers;
    }
    notify(notifyHandler) {
        this.notifyHandlers.push(notifyHandler);
        return this;
    }
} 

let p = new TrackablePromise((resolve, reject, notify) => {
 function countdown(x) {
    if (x > 0) {
        notify(`${20 * x}% remaining`);
        setTimeout(() => countdown(x - 1), 1000);
    } else {
        resolve();
    }
 }
 countdown(5);
});

p.notify((x) => setTimeout(console.log, 0, 'progress:', x));
p.then(() => setTimeout(console.log, 0, 'completed')); 

// p.notify((x) => setTimeout(console.log, 0, 'a:', x))
// .notify((x) => setTimeout(console.log, 0, 'b:', x));
// p.then(() => setTimeout(console.log, 0, 'completed'));
</script>
</html>

异步函数

异步函数,也称为“async/await”,是 ES6 期约模式在 ECMAScript 函数中的应用。async/await 是 ES8 规范新增的。这个特性从行为和语法上都增强了 JavaScript,以同步方式写的代码能够异步执行。

ES8 的 async/await 旨在解决利用异步结构组织代码的问题。为此,ECMAScript 对函数进行了扩展, 为其增加了两个新关键字:async 和 await。

解决的痛点:任何需要访问某个期约所产生值的代码,都需要以处理程序的形式 来接收这个值。例:

let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));
p.then((x) => console.log(x)); // 3

async

async 关键字用于声明异步函数。这个关键字可以用在函数声明、函数表达式、箭头函数和方法上:

async function foo() {}

let bar = async function() {};

let baz = async () => {};

class Qux {
 async qux() {}
} 

使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。而在参数或闭 包方面,异步函数仍然具有普通 JavaScript 函数的正常行为。

async function foo() {
 console.log(1);
}
foo();
console.log(2);

异步函数的返回值期待(但实际上并不要求)一个实现 thenable 接口的对象,但常规的值也可以。 如果返回的是实现 thenable 接口的对象,则这个对象可以由提供给 then()的处理程序“解包”。如果 不是,则返回值就被当作已经解决的期约。

在异步函数中抛出错误会返回拒绝的期约:

async function foo() {
 console.log(1);
 throw 3;
}
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2);

拒绝期约的错误不会被异步函数捕获:

async function foo() {
 console.log(1);
 Promise.reject(3);
}
// Attach a rejected handler to the returned promise
foo().catch(console.log);
console.log(2);

await

因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。使用 await 关键字可以暂停异步函数代码的执行,等待期约解决。

await 关键字会暂停执行异步函数后面的代码,让出 JavaScript 运行时的执行线程。这个行 为与生成器函数中的 yield 关键字是一样的。await 关键字同样是尝试“解包”对象的值,然后将这 个值传给表达式,再异步恢复异步函数的执行。

等待会抛出错误的同步操作,会返回拒绝的期约:

async function foo() {
 console.log(1);
 await (() => { throw 3; })();
}
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2); 
// 1 2 3

对拒绝的期约使用 await 则会释放(unwrap)错误值(将拒绝期约返回):

async function foo() {
 console.log(1);
 await Promise.reject(3);
 console.log(4); // 这行代码不会执行
}
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2);
// 1 2 3

await的限制

await 关键字必须在异步函数中使用,不能在顶级上下文如标签或模块中使用。不过, 定义并立即调用异步函数是没问题的。

停止和恢复执行

async/await 中真正起作用的是 await。async 关键字,无论从哪方面来看,都不过是一个标识符。 毕竟,异步函数如果不包含 await 关键字,其执行基本上跟普通函数没有什么区别。

要完全理解 await 关键字,必须知道它并非只是等待一个值可用那么简单。JavaScript 运行时在碰 到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息 队列中推送一个任务,这个任务会恢复异步函数的执行。

即使 await 后面跟着一个立即可用的值,函数的其余部分也会被异步求值。

async function foo() {
 console.log(2);
 await null;
 console.log(4);
}
console.log(1);
foo();
console.log(3);
// 1
// 2
// 3
// 4 

客户端检测

能力检测

能力检测(又称特性检测)即在 JavaScript 运行时中使用一套简单的检测逻辑,测试浏览器是否支 持某种特性。这种方式不要求事先知道特定浏览器的信息,只需检测自己关心的能力是否存在即可。

能力检测的关键是:

应该先检测最常用的方式;

其次是必须检测切实需要的特性。某个能力存在并不代表别的能力也存在。

安全能力检测

能力检测最有效的场景是检测能力是否存在的同时,验证其是否能够展现出预期的行为。

用户代理检测 软/硬件检测

第十六章 DOM2和DOM3

XML命名空间

1.node的变化

  • 在 DOM2 中,Node 类型包含以下特定于命名空间的属性:
    • localName,不包含命名空间前缀的节点名;
    • namespaceURI,节点的命名空间 URL,如果未指定则为 null;
    • prefix,命名空间前缀,如果未指定则为 null。
  • DOM3 进一步增加了如下与命名空间相关的方法
    • isDefaultNamespace(namespaceURI),返回布尔值,表示 namespaceURI 是否为节点的默 认命名空间
    • lookupNamespaceURI(prefix),返回给定 prefix 的命名空间 URI
    • lookupPrefix(namespaceURI),返回给定 namespaceURI 的前缀。

2.Document的变化

  • 这些命名空间特定的方法只在文档中包含两个或两个以上命名空间时才有用
    • createElementNS(namespaceURI, tagName),以给定的标签名 tagName 创建指定命名空 间namespaceURI 的一个新元素
    • createAttributeNS(namespaceURI, attributeName),以给定的属性名 attributeName 创建指定命名空间 namespaceURI 的一个新属性
    • getElementsByTagNameNS(namespaceURI, tagName),返回指定命名空间 namespaceURI 中所有标签名为 tagName 的元素的 NodeList。

3.Element的变化

  • getAttributeNS(namespaceURI, localName),取得指定命名空间 namespaceURI 中名为 localName 的属性
  • getAttributeNodeNS(namespaceURI, localName),取得指定命名空间 namespaceURI 中 名为 localName 的属性节点
  • getElementsByTagNameNS(namespaceURI, tagName),取得指定命名空间 namespaceURI 中标签名为 tagName 的元素的 NodeList
  • hasAttributeNS(namespaceURI, localName),返回布尔值,表示元素中是否有命名空间 namespaceURI 下名为 localName 的属性(注意,DOM2 Core 也添加不带命名空间的 hasAttribute()方法)
  • removeAttributeNS(namespaceURI, localName),删除指定命名空间 namespaceURI 中 名为 localName 的属性
  • setAttributeNS(namespaceURI, qualifiedName, value),设置指定命名空间 namespaceURI 中名为 qualifiedName 的属性为 value
  • setAttributeNodeNS(attNode),为元素设置(添加)包含命名空间信息的属性节点 attNode。 这些方法与 DOM1 中对应的方法行为相同,除 setAttributeNodeNS()之外都只是多了一个命名 空间参数。

其它变化

  • DocumentType 的变化
    • DocumentType 新增了 3 个属性:publicId、systemId 和 internalSubset。
  • Document 的变化
    • importNode() 这个方法的目的是从其他 文档获取一个节点并导入到新文档,以便将其插入新文档
    • defaultView,是一个指向拥有当前文档的窗口(或窗格)的指针。
      • getComputedStyle()
    • 对 document.implementation
      • createDocumentType()
      • createDocument()
      • createHTMLDocument()
  • Node 的变化
    • 比较节点的方法
      • isSameNode() 节点相同,意味着引用同一个对象
      • isEqualNode() 节点相等,意味着节点类型相同,拥有相等的属性
      • setUserData() DOM3 也增加了给 DOM 节点附加额外数据的方法
  • 内嵌窗格的变化
    • contentDocument 这个属性包含代表子内嵌窗格中内容的 document 对象的指针
    • contentWindow 返回相应窗格的 window 对象

样式

  • DOM 样式属性和方法
    • cssText
    • length
    • parentRule
    • getPropertyPriority
    • getPropertyValue
    • item(index)
    • removeProperty(propertyName)
    • setProperty(propertyName, value, priority)
  • 计算样式
    • document.defaultView.getComputedStyle(myDiv, null)

元素尺寸

偏移尺寸

包含元素在屏幕上占用的所有视觉空间。元素在页 面上的视觉空间由其高度和宽度决定,包括所有内边距、滚动条和边框(但不包含外边距)

其中,offsetLeft 和 offsetTop 是相对于包含元素的,包含元素保存在 offsetParent 属性中。 offsetParent 不一定是 parentNode。

  • offsetHeight,元素在垂直方向上占用的像素尺寸,包括它的高度、水平滚动条高度(如果可 见)和上、下边框的高度
  • offsetLeft,元素左边框外侧距离包含元素左边框内侧的像素数。
  • offsetTop,元素上边框外侧距离包含元素上边框内侧的像素数。
  • offsetWidth,元素在水平方向上占用的像素尺寸,包括它的宽度、垂直滚动条宽度(如果可 见)和左、右边框的宽度。

客户端尺寸

元素的客户端尺寸(client dimensions)包含元素内容及其内边距所占用的空间。客户端尺寸只有两 个相关属性:clientWidth 和 clientHeight。其中,clientWidth 是内容区宽度加左、右内边距宽 度,clientHeight 是内容区高度加上、下内边距高度。

客户端尺寸实际上就是元素内部的空间,因此不包含滚动条占用的空间。这两个属性最常用于确定 浏览器视口尺寸,即检测 document.documentElement 的 clientWidth 和 clientHeight。这两个 属性表示视口(或元素)的尺寸。

滚动尺寸

提供了元素内容滚动距离的信息。有些元素,比如无须任何代码就可以自动滚动,而其他元素则需要使用 CSS 的 overflow 属性令其滚动。

scrollWidth 和 scrollHeight 可以用来确定给定元素内容的实际尺寸。

scrollLeft 和 scrollTop 属性可以用于确定当前元素滚动的位置,或者用于设置它们的滚动位 置。

  • scrollHeight,没有滚动条出现时,元素内容的总高度。
  • scrollLeft,内容区左侧隐藏的像素数,设置这个属性可以改变元素的滚动位置。
  • scrollTop,内容区顶部隐藏的像素数,设置这个属性可以改变元素的滚动位置。
  • scrollWidth,没有滚动条出现时,元素内容的总宽度。

注意 所有这些尺寸属性都是只读的,每次访问都会重新计算。因此,应该尽量减少 查询它们的次数。比如把查询的值保存在局量中,就可以避免影响性能。

确定元素尺寸

getBoundingClientRect()

范围

DOM范围

DOM2 在 Document 类型上定义了一个 createRange()方法,暴露在 document 对象上。使用这 个方法可以创建一个 DOM 范围对象,如下所示: let range = document.createRange();

简单选择

通过范围选择文档中某个部分最简单的方式,就是使用 selectNode()selectNodeContents() 方法。这两个方法都接收一个节点作为参数,并将该节点的信息添加到调用它的范围。selectNode()方 法选择整个节点,包括其后代节点,而 selectNodeContents()只选择节点的后代。

复杂选择

要创建复杂的范围,需要使用 setStart()和 setEnd() 方法。这两个方法都接收两个参数:参照节点 和偏移量。对 setStart()来说,参照节点会成为 startContainer,而偏移量会赋值给 startOffset。 对 setEnd()而言,参照节点会成为 endContainer,而偏移量会赋值给 endOffset。

操作范围

  • deleteContents() 这个方法会从文档中删除范围包 含的节点
  • extractContents() 方法返回范围对应的文档片段
  • cloneContents() 返回的文档片段包含范围中节点的副本,而非实际的节点

范围插入

  • insertNode() 可以在范围选区的开始位置插入一个节点
  • surroundContents() 插入包含范围的内容

范围比较

  • compareBoundaryPoints () 确定范围之间是否存在公共的边 界(起点或终点)

复制范围

  • cloneRange()

清理

  • detach()

模块

CommonJS

  • CommonJS 规范概述了同步声明依赖的模块定义,主要用于在服务器端实现模块化代码组 织,但也可用于定义在浏览器中使用的模块依赖。CommonJS 模块语法不能在浏览器中直接运行。
  • 无论请求多少次,模块只会被加载一次
  • 模块第一次加载后会被缓存,后续加载会取得缓存的模块
  • CommonJS 以服务器端为目标环境,能够一次性把所有模块都加载到内存

AMD

  • 异步模块定义:
    • 异步模块定义的模块定义系统则以浏览器为目标执行环境,这需要考虑网络延迟的 问题。AMD 的一般策略是让模块声明自己的依赖,而运行在浏览器中的模块系统会按需获取依赖,并 在依赖加载完成后立即执行依赖它们的模块
  • AMD 模块实现的核心是用函数包装模块定义

UMD

  • 通用模块定义 :
    • UMD 可用于创建这两个系统都可以使用的模块代码。本质上,UMD 定义的模块会在启动时 检测要使用哪个模块系统,然后进行适当配置,并把所有逻辑包装在一个立即调用的函数表达式(IIFE) 中。

ES6模块

ECMAScript 6 模块借用了 CommonJS 和 AMD 的很多优秀特性。

  • 模块代码只在加载后执行。
  • 模块只能加载一次。
  • 模块是单例。
  • 模块可以定义公共接口,其他模块可以基于这个公共接口观察和交互。
  • 模块可以请求加载其他模块。
  • 支持循环依赖。 ES6 模块系统也增加了一些新行为。
  • ES6 模块默认在严格模式下执行。
  • ES6 模块不共享全局命名空间。
  • 模块顶级 this 的值是 undefined(常规脚本中是 window)。
  • 模块中的 var 声明不会添加到 window 对象。
  • ES6 模块是异步加载和执行的。