Javascript 红宝书(上)¶
一、什么是 Javascript¶
核心 ECMAScript¶
- Web 浏览器只是 ECMAScript 实现可能存在的一种宿主环境。(还有 Node.js/Flash 等)
- 宿主环境提供 ECMAScript 的基准实现和与环境自身交互必需的扩展。
DOM¶
- DOM 是一个API,用于在 HTML 中使用扩展的XML。
- 将页面抽象成一组分层的节点。
- 提供与网页内容交互的方法和接口。
BOM¶
- BOM API,提供访问/操作浏览器的方法和接口。
- HTML 5 规范了 BOM 的特性
二、HTML 与 Javascript¶
<script>
元素¶
-
脚本属性
-
crossorigin
- crossorigin=“anonymous”: 文件请求无需凭据
- crossorigin=“use-credentials”: 出站请求包含凭据
-
async 或 defer
-
-
行内脚本与外部脚本
-
行内脚本
- 从上到下解释,阻塞页面解析
- 不能出现
</script>
字符串,需要转义
-
外部脚本
- 同样阻塞页面解析,阻塞时间包含文件加载时间
-
推荐使用外部文件
- 可维护性
- 缓存
- 适应未来
-
-
标签位置
- 推荐放在页面尾部,空白页面时间较短
-
推迟执行 defer
- 设置 defer 属性,相当于告诉浏览器立即下载,但延迟执行。
- 浏览器解析到结束标签
</html>
之后,才执行。 - 第一个 defer 脚本会在第二个 defer 之前执行;两者都会在
DOMContentLoaded
事件之前执行。
-
异步执行 async
- 告诉浏览器,不必等脚本加载和执行完后再加载页面
- 异步脚本不应该在加载期间修改 DOM
- 第二个脚本可能先于第一个脚本执行
-
动态加载
document.createElement('script')
- 为避免影响性能,需要在文档头部显示声明,让浏览器预加载器知道。
<link ref="preload" href="gibberish.js">
<noscript>
标签¶
- 在不支持JavaScript的浏览器中显示替代内容。
三、语言基础¶
语法¶
-
严格模式
- ECMAScript 5 增加严格模式(strict mode)
- ECMAScript 3 中不规范的写法/不安全的活动会抛出异常。
"use strict";
变量¶
-
var
- 声明 函数作用域
- var 声明提升(hoist)
- 不推荐改变变量保存值的类型
-
let
- 声明 块级作用域
- 暂时性死区(temporal dead zone):声明前执行,抛出 ReferenceError
- 全局声明:let 在全局作用域中声明的变量不会成为 window 对象的属性
- for 循环中的 let 声明:let 出现之前,for 循环定义的迭代变量会渗透到循环体外部。
-
const
- const 声明可以让浏览器运行时强制保持不变,也可以让静态代码分析工具提前发现不合法的赋值操作。
-
声明风格及最佳实践
- 不使用 var
- const 优先,let 次之
数据类型¶
-
简单数据类型
- Undefined
- Null
- Boolean
-
Number
- 八进制
077; // 八进制的63 078; // 无效的八进制,当成78处理 0o77; // 严格模式下的八进制表示
- 十六进制 (比如 0x1f)
-
浮点值
- 浮点值的精确度可高达17位小数。
- 0.1 + 0.2 = 0.30000000000000004
- 舍入错误:计算机的二进制实现和位数限制有些数无法有限表示。因为 JS 遵循 IEEE 754 数值规范,除了 javascript,其他相同格式语言也会有此问题。
-
NaN
console.log(0/0); // NaN console.log(1/0); // Infinity isNaN(): 表示是否不能转换为数值
- 数值转换:Number() 、parseInt() 和 parseFloat()
-
String
- 模版字面量标签函数(tag function)
function simpleTag(strings, ...expressions) { console.log(strings); console.log(expressions); } simpleTag`${a} + ${b} = ${a+b}`; // strings: ["", " + ", " = ", "", raw: Array(4)] // expressions: [6, 9, 15]
- 原始字符串(String.raw)
/* 表示不希望被转义 * / console.log(`\u03a3`); // "Σ" console.log(String.raw`\u03a3`); // "\u03a3"
-
Symbol
- 不可变唯一实例
Symbol('foo') === Symbol('foo'); // false Symbol.for('foo') === Symbol.for('foo'); // true
-
Symbol.asyncIterator
- 方法,返回值为对象的异步迭代器 AsyncIterator
- 一个异步可迭代对象必须要有
Symbol.asyncIterator
属性 - 可用于
for await...of
循环 - 示例
const myAsyncIterable = new Object(); myAsyncIterable[Symbol.asyncIterator] = async function*() { yield "hello"; yield new Promise(r => r("async")); yield "iteration!"; }; // 执行 (async () => { for await (const x of myAsyncIterable) { console.log(x); // expected output: // "hello" // "async" // "iteration!" } })();
-
Symbol.iterator
- 方法,返回值为对象的迭代器
- 可用于
for...of
循环 - 示例
var myIterable = {} myIterable[Symbol.iterator] = function* () { yield 1; yield 2; yield 3; }; [...myIterable] // [1, 2, 3]
-
Symbol.hasInstance
- 决定一个构造器对象是否认可一个对象是它的实例。
- 由 instanceof 操作符使用
- 示例
class A { static [Symbol.hasInstance](instance) { return false; } } let a = new A(); a instanceof A; // false
-
Symbol.isConcatSpreadable
- 决定一个数组的元素是否可以被打平。
- 由
Array.prototype.concat
使用 - 示例
let a = [2]; [1].concat(a); // [1, 2] a[Symbol.isConcatSpreadable] = false [1].concat(a); // [1, Array(1)]
-
Symbol.match
String.prototype.match()
方法会调用此函数
-
Symbol.replace
- 当一个字符串替换所匹配字符串时所调用的方法
String.prototype.replace()
方法会调用此方法。
-
Symbol.search
- 接受用户输入的正则表达式,返回该正则表达式在字符串中匹配到的下标
String.prototype.search()
-
Symbol.split
- 通过
String.prototype.split()
调用
- 通过
-
Symbol.toStringTag
- 作为对象的属性键使用,对应的属性值(字符串类型)用来表示该对象的自定义类型标签。
- Object.prototype.toString() 方法会去读取这个标签并把它包含在自己的返回值里。
- 代码
class ValidatorClass { get [Symbol.toStringTag]() { return "Validator"; } } Object.prototype.toString.call(new ValidatorClass()); // "[object Validator]"
-
Symbol.toPrimitive
- 当一个对象转换为对应的原始值时,会调用此函数。
-
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
-
复杂数据类型(Object)
- Object类型即一种无序名对值的集合。
- constructor
- hasOwnProperty: 判断非原型属性
- isPrototypeof
- propertyIsEnumerable: 属性是否可使用
- toLocaleString
- toString: 返回对象的字符串表示
- valueOf: 返回对象的字符串、数值或布尔值表示。
-
typeof 操作符
- "undefined"
- "boolean"
- "number"
- "string"
- "symbol"
- "object":非函数对象 或 null。
- "function":函数对象
流控制语句¶
-
标签语句
- label: statement
- 可通过 break 或 continue 引用
-
break 和 continue 语句
- break 强制执行循环之后下一条语句
- continue 从循环顶部开始执行
-
with 语句
- 将代码 作用域 设置为一个 对象
理解函数¶
四、变量、作用域与内存¶
原始值与引用值¶
执行上下文与作用域¶
-
执行上下文(作用域)
- 任何变量都存在于某个执行上下文中(即作用域)。这个上下文决定了变量的生命周期,以及它们可以访问代码的哪些部分。
- 执行上下文分 全局上下文,函数上下文 和 块级上下文。
- 每个上下文都有一个关联的 变量对象(Variable Object),上下文中定义的所有变量和函数都存在这个对象上。
- 如果上下文是函数,则其 活动对象(Activation Object) 作为变量对象;活动对象最初只有一个定义变量:
arguments
。 - 如果是全局上下文,其活动对象叫 全局对象(Global Object)。
-
执行流
- 代码执行流进入函数时,函数的上下文推入 上下文栈。函数执行完后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。
-
作用域链
- 代码执行流每进入一个新上下文,都会创建 变量对象 的一个 作用域链,用于搜索变量和函数。
- 代码正在执行的上下文的变量对象 始终位于作用域链的最前端。
- 代码执行时在 标识符(即变量名)解析 时,通过沿作用域链,从前往后逐级搜索标识符合名称完成的。直到找到标识符(若找不到通常会报错:ReferenceError: x is not defined)。
- 内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东西。 上下文之间的连接是线性的、有序的。
-
作用域链增强
- 某些语句会导致在作用域链前端,临时添加一个上下文,执行完后被删除。
- try/catch 语句的 catch 块
- with 语句
-
变量声明
- var 声明:与变量提升
- const 声明:暗示变量值类型单一且不可变,v8 引擎对此优化,在 javascript 运行时编译器将其替换成实际值
- 标识符查找:访问局部变量比访问全局变量快,因为不必切换作用域。
垃圾回收¶
-
标记清理(mark-and-sweep)
- 当变量进入上下文时,加上存在于上下文的标记;离开时,也会被加上离开上下文的标记。
- 回收时,将上下文中或被上下文引用的变量的标记去掉,剩下带标记的都待删除。垃圾回收程序做一次内存清理,销毁带标记的所有值,收回内存。
-
引用计数(reference counting)
- 对每个值都记录他被引用的次数。
- 声明变量并赋引用值时,这个值引用数为 1; 值被赋给另一个变量,计数加 1; 该引用值的变量被其他值覆盖,计数减 1。
-
循环引用的问题
- 两个变量相互引用,它们的引用数永远不会变成 0;
- 变量设置为null,切断变量与其引用值之间联系,便于回收。
-
性能考量
- 内存有限时,垃圾回收时可能明显拖慢渲染速度和帧速率。
- 写代码:无论什么时候开始收集垃圾,都能让它尽快结束工作。
-
内存优化
-
通过 const/let 提升性能
- 以块为作用域,可以更早的让垃圾回收程序介入。
-
解除引用
- 优化内存的原则是保证在执行代码时只保存必要的数据。
- 如果数据不再需要,设置为 null,从而释放其引用。
-
隐藏类和删除操作
- 避免「先创建再补充(ready-fire-aim)」式的动态属性赋值,而应在构造函数内一次性声明所有属性。
- 避免动态删除属性,最好把不用的属性设置为 null。
- 以上都会导致同一个构造函数的实例,不再共享一个隐藏类。
-
内存泄漏
- 意外声明全局变量
- 定时器回调函数引用的外部变量不释放
- 闭包使用不当
-
静态分配与对象池
-
五、基本引用类型¶
Date¶
RegExp¶
let expression = /pattern/flags;
-
模式:pattern
- 字符类
- 限定符
- 分组
- 向前查找
- 反向引用
-
选项:flags
- g: 全局模式
- i: 不区分大小写
- m: 多行模式
- y: 粘附模式
- u: Unicode 模式
- s: dotAll 模式
-
实例属性
- 返回布尔类型:global/ignoreCase/unicode/sticky/multiline
- source/flags
-
实例方法
-
exec()
- 若匹配,返回数组;否则,返回 null。
- 数组中第一个元素是匹配整个模式的字符串,其他元素是与表达式中捕获组匹配的字符串。
- 如果模式设置全局标记g,则每次调用 exec() 返回匹配信息,并且指针后移;否则不设置全局,只返回第一个匹配信息。
- 如果模式设置全局标记y,则每次调用 exec()只会在 lastIndex 的位置上寻找你匹配项。
-
test()
- 用法与 exec() 区别仅在于返回值为bool值。
-
-
构造函数属性
- input: 最后搜索的字符串($_)
- lastMatch: 最后匹配的文本($&)
- lastParen:最后匹配的捕获组($+)
- leftContext:input 字符串出现在 lastMatch 前面的文本($`)
- rightContext:input 字符串出现在 lastMatch 后面的文本($')
原始包装类型¶
- 概念:Boolean/Number/String 类型,既有其他 引用类型 的特点,也有各自 原始类型 对应的特殊行为。
-
Boolean
- 实例会重写
valueOf()
方法,返回原始值 true 或 false。 new Boolean(false) && true
; // true
- 实例会重写
-
Number
- 重写
valueOf()
方法,返回原始数值;重写toString()
方法,接受基数参数,返回基数形式的字符串; - toFixed/toPrecision/toExponential
- Number.isInteger(1.0)
- 重写
-
String
-
Javascript 字符
- str.charCodeAt()
- String.fromCharCode(65, 666)
- String.fromCodePoint(0x1F60A):码点是 Unicode 中一个字符完整的标识。16 位或 32位。
-
normalize() 方法
-
字符串操作方法
- concat/slice/substr
-
字符串位置方法
- indexOf/lastIndexOf
-
字符串包含方法
- startsWith/endsWith/includes
-
trim/repeat/padStart/padEnd
-
模式匹配
- str.match() 与 reg.exec() 相同
- str.search() 返回第一个匹配索引位置
- str.replace()
var text = 'cat, rat'; text.replace(/(.at)/g, 'word($1)'); // "word(cat), word(rat)"
-
localCompare: 比较字符串,并返回 1/0/-1。
-
单例内置对象¶
- Global:浏览器将 window 对象实现为 Global 对象的代理。
- Math
六、集合引用类型¶
Object¶
数组(Array)¶
-
Array.from
- 将 类数组结构 转换为数组实例
-
Array.of
- 将 一组参数 转换为数组实例
-
检测数组
- value instanceof Array
- Array.isArray(value)
-
迭代器方法
- Array.prototype.keys(): 返回索引迭代器
- Array.prototype.values(): 返回元素迭代器
- Array.prototype.entries(): 返回索引/值对迭代器
- 示例:for(let [idx, elment] of arr.entries()) {...}
-
填充数组
- Array.prototype.fill()
- Array.prototype.copyWithin()
-
转换方法
- valueOf():
arr.valueOf() === arr;
// true - toString():
[1, {}, 'a', [2, [3]]].toString();
// "1,[object Object],a,2,3" - join():
[1, {}, 'a', [2, [3, 4]]].join('#');
// "1#[object Object]#a#2,3,4"
- valueOf():
-
栈和队列
- push/pop
- shift/unshift
-
排序(sort/reverse)
- sort 默认将元素 String() 后再排序
- 注意:返回调用它们数组的引用(原数组)。
-
操作
- concat():返回新数组
- slice():返回新数组
- splice():删除、插入、替换,返回新数组
-
搜索
- 严格相等:indexOf/lastIndexOf/includes
- 断言函数:find/findIndex
-
迭代和归并
- every/filter/forEach/map/sort
- reduce/reduceRight
定型数组(Typed Array)¶
-
ArrayBuffer
- ArrayBuffer 对象代表存储二进制数据的一段内存,是一个字节数组。
- ArrayBuffer 是所有 定型数组 及 视图 引用的 基本单位。
- ArrayBuffer 也是一个构造函数,连续的内存区域,参数是内存大小(单位字节)。
const buf = new ArrayBuffer(32);
- ArrayBuffer 一经创建就不能再调整大小。
- 要读取或写入 ArrayBuffer,必须通过 “视图”。
-
Typed Array
- ArrayBuffer 的一种视图。
const buf = new ArrayBuffer(12); const ins = new Int32Array(buf); ins.buffer.byteLength; // 12 ins.length; // 3
- 定型数组行为:类似普通数组,可使用 every/find/reduce/map/slice/values 等。
-
合并、复制和修改
- 无法调整大小。
- 不支持 pop/push/splice等
- 向内复制 set()
- 向外复制()
-
上溢和下溢
-
DataView
- 另一种允许读写 ArrayBuffer 的视图。
new DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);
-
支持设置字节序。
- 什么是字节序?字节序是数值在内存中的存储方式。分为小端字节序(little-endian)和大端字节序(big-endian)两种:比如个人电脑基本都是小端字节序,而互联网标准通常要求数据使用 big-endian 存储。
- TypedArray 中,字节序会跟随系统的字节序,于是基本都是小端字节序,是不支持自己设置的,于是就会带来一个问题:如果从网络请求来的数据是大端字节序,会导致数据无法解析。
- 相比之下,DataView 可以支持设置字节序。
Map(顺序与迭代)¶
- 与 Object 类型最主要的区别:
m[Symbol.iterator] === m.entries; // true
WeakMap¶
-
API
- 弱映射,键只能是 Object 类型;否则,抛出 TypeError。
-
弱键
- 键,不属于正式引用,不会阻止垃圾回收;
- 值,只要键存在,就不会被垃圾回收。
-
不可迭代键
-
用于存储:私有变量
- 以实例对象为键,以私有成员的字典为值。
-
用于存储:DOM 节点元数据
- WeakMap 实例不会妨碍垃圾回收,适合保存关联元数据。
m.set(dom,{ a: 1 });
// 若从 DOM 树删除,dom 仍在内存中。wm.set(dom,{ a: 1 });
// 若删除,垃圾回收程序立即释放其内存
Set¶
-
顺序与迭代
s[Symbol.iterator] === s.entries; // true
-
保留插入顺序
-
定义集合操作
- union/intersection/difference
WeakSet¶
-
API
- 弱集合,值只能是 Object 类型
-
不可迭代值
-
用于存储:DOM 节点
- Set() 存储:若从 DOM 树删除,dom 仍在内存中。
- WeakSet() 存储:若删除,垃圾回收程序立即释放其内存。
迭代与扩展¶
七、迭代器与生成器¶
迭代器¶
- 可迭代协议:即实现 Iterable 接口的能力,支持迭代的自我识别能力和创建实现 Iterator 接口的对象的能力。
- 内置迭代类型:字符串/数组/Map/Set/arguments对象/NodeList
-
原生语言的迭代器特性
- for-of 循环
- 数组解构
- 扩展符操作
- Arrary.from()
- 创建集合
- 创建映射
- Promise.all() 接收
- Promise.race() 接收
- yield* 操作符
-
自定义迭代器
提前终止迭代器¶
- 迭代器实例的 return() 方法,可选。
生成器¶
function * generatorFn() {}
- 只要可定义函数的地方,就可以定义生产器。
- 调用生成器函数会返回一个 生成器对象,实现了 Iterator 接口。
- 生成器对象 一开始处于 suspened 状态。
- 生成器函数只有在初次调用 next() 后才开始执行
g === g[Symbol.iterator](); // true
yield¶
-
yield 中断执行
- 生成器函数在遇到
yield
关键字后,执行会停止,函数作用域的状态会被保留。 - 停止执行的生成器函数,只能通过生产器对象调用
next()
方法恢复。
- 生成器函数在遇到
-
实现输入和输出
- 上一次让生成器函数暂停的 yield 关键字会接收给 next() 方法的第一个值。
- 第一次 next() 传参不会被使用,因为这次调用只为开始执行生成器。
- 代码
function* generatorFn () { return yield 'x'; } let it = generatorFn(); it.next(); // {value: "x", done: false} it.next('y'); // {value: "y", done: true}
-
产生可迭代对象
- 使用星号增强 yield 行为,让它能迭代一个可迭代对象。
- 代码
function* generatorFn () { yield* [1,2,3]; } for(let x of generatorFn()) {console.log(x);} // 1 // 2 // 3
yield *
实际上只是 将一个可迭代对象,序列化为一连串可单独产出的值。
-
使用 yield * 实现递归算法
- 案例一
function * nTimes(n) { if (n > 0) { yield * nTimes(n-1); yield n - 1; } } for(let x of nTimes(3)) {console.log(x);} // 0 // 1 // 2
- 案例二:图连通算法
class Node { constructor(id) { this.id = id; this.neighbors = new Set(); } connect(node) { if (node !== this) { this.neighbors.add(node); node.neighbors.add(this); } } } class RandomGraph { constructor(size) { this.nodes = new Set(); // 创建节点 for(let i = 0; i < size; i++) { this.nodes.add(new Node(i)); } // 随机连接节点 const threshhold = 1 / size; for(const x of this.nodes) { for(const y of this.nodes) { if (Math.random() < threshhold) { x.connect(y); } } } } isConnected() { const visitedNodes = new Set(); // 深度优先遍历 function * traverse(nodes) { for(const node of nodes) { if (!visitedNodes.has(node)) { console.log('visiting ', node.id); yield node; yield * traverse(node.neighbors); } } } // 取得集合中的第一个节点 const firstNode = this.nodes[Symbol.iterator]().next().value; // 使用递归生成器迭代每一个节点 for(const node of traverse([firstNode])) { visitedNodes.add(node); } debugger; return visitedNodes.size === this.nodes.size; } print() { for(const node of this.nodes) { const ids = [...node.neighbors] .map(n => n.id) .join(','); console.log(`${node.id}: ${ids}`); } } } // 示例 const g = new RandomGraph(6); g.print(); g.isConnected();
生成器作为默认迭代器¶
class Foo {
constructor() {
this.values = [1, 2, 3];
}
*[Symbol.iterator]() {
yield * this.values;
}
}
for(let x of new Foo()) {console.log(x);}
// 1
// 2
// 3
提前终止生成器¶
- 一个实现 Iterator 接口的对象一定有 next() 方法,还有可选的 return() 方法用于提前终止迭代器。
- 生成器另有一个 throw() 方法。
- 与迭代器不同,生成器的 return() 方法关闭后,无法恢复。
throw()
:会在暂停时注入错误,若未被处理,则生成器关闭;若处理,则可恢复执行。
八、对象、类与面向对象编程¶
理解对象¶
- 定义:一组属性的无序集合
-
属性的类型
-
数据属性:数据属性 包含一个保存数值的位置。4个特性:
- [[Configurable]]
- [[Enumberable]]
- [[Writable]]
- [[Value]]
-
访问器属性:访问器属性 不包含数据值,且必须使用
Object.defineProperty()
定义。4个特性:- [[Configurable]]
- [[Enumberable]]
- [[Get]]
- [[Set]]
-
-
定义多个属性
- Object.defineProperties()
-
读取属性的特性
- Object.getOwnPropertyDescriptor()
- 新增 Object.getOwnPropertyDescriptors()
-
对象合并
- 浅拷贝 Object.assign()
-
相等判定
+0 === -0; // true
NaN === NaN; // false
Object.is(+0, -0); // false
Object.is(NaN, NaN); // true
-
对象解构
let personCopy = {}; ({ name: personCopy.name, age: personCopy.age } = person);
理解创建对象¶
- 工厂模式:createPerson()
-
构造函数模式:new Person()
new
操作符的过程:- 1、在内存中创建一个对象
- 2、这个新对象内部的 [[Prototype]] 属性(即
__proto__
)被赋值为构造函数的 prototype 属性 - 3、构造函数内部的 this 被赋值为这个新对象
- 4、执行构造函数内部代码
- 5、构造函数返回某个非空对象;或者,返回刚创建的新对象。
-
原型模式
- 每个函数都会创建一个 prototype 属性,即原型对象。原型对象的 constructor 属性,指回与之关联的构造函数。
- 实例对象内部 [[Prototype]] 指针(即
__proto__
属性)被赋值为构造函数的原型对象。 - Object.create() 创建新对象,并指定其原型。
Object.prototype.__proto__ === null; // true
-
原型层级
person.hasOwnProperty('name');
// 实例属性 返回 true‘name’ in person;
// 不论是实例属性,还是原型属性,都返回 true。for-in
: 访问对象中可被枚举的 实例属性和原型属性。
-
对象迭代
- Object.entries()
- Object.values()
理解继承¶
-
继承的概念
- 面向对象语言支持两种继承:接口继承和实现继承。
- ECMAScript 只支持实现继承,通过原型链。
-
原型链继承
SubType.prototype = new SuperType();
-
盗用构造函数(constructor stealing) 继承
- Why: 又称 “对象伪装” 或 “经典继承”,解决原型包含引用导致的继承问题。
- How: 在子类构造函数中调用父类构造函数。
-
组合继承
- 也称“伪经典继承”,综合了 原型链 和 盗用构造函数。
- 通过原型链继承原型上的属性和方法;通过盗用构造函数继承实例属性。
-
原型式继承
function object(o) { function F() {} F.prototype = o; return new F(); }
- Object.create() 将原型式继承的概念规范化。
-
寄生式继承
- 思想:寄生构造函数和工厂模式。
- How: 创建一个实现继承的函数,以某种方式增强对象,返回这个对象。
-
寄生式组合继承
- Why: 组合继承存在效率问题,父类构造函数始终会被调用两次。
- 寄生式组合继承通过 盗用构造函数 继承 属性,通过 混合式原型链 继承 方法。
- 核心逻辑: inheritPrototype
function inheritPrototype(subType, superType) { let prototype = object(superType.prototype); // 创建对象 subType.prototype = prototype; // 赋值对象 prototype.constructor = subType; // 增强对象 } // 或 // 实现继承的核心函数 function inheritPrototype(subType,superType) { function F() {}; // F()的原型指向的是superType F.prototype = superType.prototype; // subType的原型指向的是F() subType.prototype = new F(); // 重新将构造函数指向自己,修正构造函数 subType.prototype.constructor = subType; }
理解类¶
- 构造函数
-
实例,原型和类成员
- 支持迭代器与生成器方法
-
用于继承
-
继承基础
- 静态类方法和原型方法都会带到派生类上。
-
构造函数,HomeObject 和 super()
- super 只能在派生类构造函数和静态方法中使用。
-
抽象基类
- 可供继承,但本身不会被实例化。
- 通过
new.target
实现
-
继承内置类型
-
类混入
- 混入模式可以在通过在一个表达式中连缀多个元素来实现,此表达式最终会被解析为一个可以继承的类。
- 定义一组 “可嵌套“ 的函数,函数接收超类为参数,混入类为该参数的子类,返回混入类。
- 很多JS 框架抛弃混入模式,转向组合模式。符合设计原则:“复合胜过继承”。
-
九、代理与反射¶
代理基础¶
- 代理是目标对象 (target) 的抽象。
-
创建空代理
- 默认情况下,在代理对象的执行的所有操作都会无障碍的传播到目标对象。
-
定义捕获器 (trap)
- 捕获器是处理程序对象(handler) 中定义的 “基本操作的拦截器”。
-
捕获器参数和 Reflect API
- const proxy = new Proxy(target, Reflect);
-
可撤销代理
- const { proxy, revoke } = Proxy.revocable(target, Reflect);
- 撤销代理之后再调用代理会抛出 TypeError。
-
实用反射 API
- Reflect API 与 Object API
-
状态标记
- Reflect 方法返回的布尔值称为 “状态标记”。
-
用一等函数代替操作符
- Reflect.get(): 可替代对象属性访问操作符。
- Reflect.set(): 可替代赋值操作符。
- Reflect.has(): 可替代赋值 in 操作符。
- Reflect.deleteProperty(): 可替代 delete 操作符。
- Reflect.construct(): 可替代 new 操作符。
-
安全地 apply 函数
- Function.prototype.apply.call(myFunc, thisVal, argList);
- Reflect.apply(myFunc, thisVal, argList);
-
代理的问题与不足
- 代理中的 this
- 代理与内部槽位
代理捕获器与反射方法¶
- get()
- set()
- has()
- defineProperty()
- getOwnPropertyDescriptor()
- deleteProperty()
- ownKeys()
- getPrototypeOf()
- setPrototypeOf()
- isExtensible()
- preventExtensions()
- apply()
- construct()
代理模式¶
- 跟踪属性访问
- 隐藏属性
- 属性验证
- 函数与构造函数参数验证
- 数据绑定与可观察对象
十、函数¶
箭头函数¶
- 箭头函数不能使用 arguments, super 和 new.target。
- 此外,箭头函数没有 prototype 属性。
函数名¶
- 如果是 get 函数,set 函数,或使用bind() 实例化,函数名前面会加上一个前缀。
函数声明与函数表达式¶
- Javascript 引擎在任何代码执行前,会先读取函数声明,并在执行上下文中生成函数定义。
- 函数表达式必须等代码执行到它那一行,才生成函数定义
函数内部¶
-
arguments
- 类数组对象
- arguments.callee 是一个指向 arguments 对象所在函数的指针。
-
this
- 标准函数中,this 引用的是把函数当成方法调用的上下文对象。
- 箭头函数中,this 引用的是定义箭头函数的上下文。
-
new.target
- 引用被调用的构造函数。
-
caller
- arguments.callee.caller: 引用的是调用当前函数的函数,在全局作用域中为null。
函数属性与方法¶
-
length 属性
- 函数定义的命名参数的个数。
-
prototype 属性
-
call()/apply() 方法
- 以指定的 this 值来调用函数。
-
bind() 方法
- 创建一个新的函数实例,其 this 值被绑定到指定对象。
尾调用机制¶
- ES6 新增的一项内存管理优化机制。
-
条件
- 严格模式下执行
- 外部函数的返回值是对尾调用函数的调用
- 尾调用函数返回后不需要执行额外的逻辑
- 尾调用函数不是引用外部函数作用域变量的闭包
-
此优化在递归场景下效果明显。
闭包¶
-
作用域链
- 在定义函数时,就会为它创建作用域链,预装载全局变量对象,并保存在内部的 [[Scope]] 中。
- 在调用函数时,会创建相应的执行上下文。然后通过复制函数的 [[Scope]] 来创建其作用域链
- 接着创建函数的活动对象,推入作用域链首。
-
闭包生命周期
- 外部函数执行完,其执行上下文的作用域链会销毁,但它的 活动对象仍存在于内存;直到内部函数被销毁后才被销毁。
-
this 对象
- 如果内部函数没使用箭头函数,则 this 对象会在运行时绑定到 执行函数的上下文。
- 如果在全局函数中调用,则 this 在非严格模式下等于 window;严格模式下 undefinde。
- 如果作为对象方法,this 等于这个对象。
-
内存泄漏
- 注意切断循环引用
立即调用的函数表达式 (IIFE)¶
- 模拟块级作用域
- 锁定参数,防止变量定义外泄。
十一、期约与异步函数¶
异步编程¶
- 同步行为和异步行为的对立与统一是计算机科学的一个基本概念。
期约 (Promise)¶
- Promises/A+ 规范
-
期约的基础
-
期约状态机
- pending/fulfilled/rejected
- 状态不可逆
- 状态是私有的
-
解决值 (value)、拒绝理由 (reason) 及期约用例
-
通过执行函数 (executor) 控制期约状态
- 执行函数的指责:初始化期约的异步行为和控制状态的最终转换。
-
Promise.resolve()
- 幂等性
- let p = Promise.resolve(1); p === Promise.resolve(p); // true
-
Promise.reject()
- 没有幂等性
-
同步/异步执行的二原型
- try { Promise.reject(new Error('foo')); } catch(e) { console.log(e); } // 未捕获期约抛出的错误
- 原因: 拒绝期约的错误,没有跑到同步代码中,而是通过浏览器异步消息队列来处理的。因此,try/catch 不能捕获该错误。
-
-
期约的实例方法
- 实现 Thenable 接口
-
Promise.protype.then()
- 为期约实例添加处理程序的主要方法。参数:onResolved 和 onRejected。
- onResolved 处理程序的返回值,会通过 Promise.resolve() 包装来形成新的期约。
Promise.resolve('foo').then().then(); // Promise {<fulfilled>: "foo"}
Promise.resolve(new Error('qux')).then(); // Promise {<fulfilled>: Error: qux
Promise.reject(1).then(null, () => 2).then(); // Promise {<fulfilled>: 2}
-
Promise.protype.catch()
- 为期约实添加拒绝处理程序。参数:onRejected。
- 等价于:Promise.protype.then(null, onRejected)
Promise.reject(1).catch(() => 2).then(); // Promise {<fulfilled>: 2}
-
Promise.protype.finally()
- 期约转换为解决或拒绝都会执行。参数:onFinally。
- onFinally 是一个与状态无关的方法。原样后传父期约。
Promise.resolve(1).finally(() => 2).then(); // Promise {<fulfilled>: 1}
Promise.reject(1).finally(() => 2).then(); // Promise {<rejected>: 1}
-
非重入期约方法
- 当期约进入落定状态 (settled)时,与该状态相关的处理程序仅仅会被 排期,而非立即执行。
- Javascript 运行时,保证 “非重入特性”:即,同步代码一定会在处理程序之前先执行。
-
邻近处理程序的执行顺序
- 传递解决值和拒绝理由
-
拒绝期约与拒绝错误处理
- 错误实际上是从消息队列异步抛出,所以不会阻止继续执行同步指令。
-
期约连锁与期约合成
-
期约连锁
- 一个期约接一个期约地拼接。
- 串行化异步任务,解决回调地狱问题。
-
期约图
- 期约连锁可以构建有向非循环图的结构。
- 每个期约为图中的节点,使用实例方法添加的处理程序是有向顶点。
- 由于期约的处理程序是先添加都消息队列,然后才逐个执行,因此都成了 层序遍历。
-
Promise.all() & Promise.race()
-
期约合成
- 将多个期约组合为一个期约。
- 类似函数合成,把多个函数左右处理程序合成一个连续传值的期约连锁。
- Case 1
function addTen(x) { return Promise.resove(x) .then(addTwo) .then(addThree) .then(addFive); }
- Case 2
function compose(...fns) { return x => fns.reduce( (promise, fn) => promise.then(fn), Promise.resolve(x) ); } let addTen = compose(addTwo, addThree, addFive);
-
-
期约扩展
-
期约取消
- 期约在处理过程中,程序却不再需要其结果。
- Kevin Smith 的取消令牌 CancelToken
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 cancelableDelayedResolve(delay) { setTimeout(console.log, 0, 'start 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', () => cancelableDelayedResolve(2000));
-
期约进度追踪
- TrackablePromise
class TrackablePromise extends Promise { constructor(executor) { const notifyHandlers = []; super((resolve, reject) => { return executor(resolve, reject, status => { notifyHandlers.map(handler => handler(status)); }); }); this.notifyHandlers = notifyHandlers; } notify(handler) { this.notifyHandlers.push(handler); return this; } } /* 测试用例 */ let p = new TrackablePromise((resolve, reject, notify) => { function countdown(x) { if(x > 0) { notify('x = ' + x); setTimeout(() => countdown(x - 1), 1000); } else { resolve(); } } countdown(5); }); p.notify(x => setTimeout(console.log, 0, x));
-
异步函数 (async/await)¶
-
异步函数
- async
-
await
- await 关键字会暂停异步函数后面的代码,让出 Javascript 运行时的执行线程,等待期约解决。
- 此行为与生成器的函数中的 yield 关键字一样。
-
await 关键字期待一个实现 thenable 接口的对象,但常规的值也可以。
-
等待一个常规值
await 'foo';
-
等待一个实现 thenable 接口的非期约对象
const thenable = { then(callback) { callback('bar'); } }; await thenable;
-
等待一个期约
- await Promise.resolve('baz');
-
-
await 的限制
-
停止和恢复执行
- async/await 中真正起作用的是 await。async 只是一个标志符。
- Javascript runtime 在遇到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,Javascript runtime 会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。
- 运行时工作 Case
async function foo() { console.log(2); await null; console.log(4); } console.log(1); foo(); console.log(3); // 1 // 2 // 3 // 4
-
运行时工作 Case 分析
- 1) 打印 1;
- 2) 调用异步函数 foo();
- 3) (在 foo() 中) 打印 2;
- 4) (在 foo() 中) await 关键字暂停执行,为立即可用的值 null 向消息队列中添加一个任务;
- 5) foo() 退出
- 6) 打印 3;
- 7) 同步线程代码执行完毕;
- 8) Javascript 从消息队列中取出任务,恢复异步函数执行;
- 9) (在 foo() 中) 恢复执行,await 取出 null 值;
- 10) (在 foo() 中) 打印 4;
- 11) 在 foo() 返回。
-
异步函数策略
- 实现 sleep()
-
利用平行执行
- 期约之间没有依赖关系时,先一次性初始化所有期约,然后再分别等待它们的结果。
-
串行执行期约
- 使用 async/await 期约连锁会变得简单。
-
栈追踪与内存管理
- 期约与异步函数功能大多重叠,但在内存中的表示则差别很大。
- 异步函数的栈追踪信息能更准确的反映当前的调用栈。在抛错生成栈追踪信息时,性能更优。