JavaScript语言学习

JAVASCRIPT 简介

javascript 最初被创建的原因是作在浏览器环境中使得浏览器的交互效果更加生动

javascript 这种编程语言写出来的程序称之为脚本 ,即是可以被直接写在网页的 HTML 中,在页面加载的时候自动执行。脚本被以纯文本的形式提供和执行。它们不需要特殊的准备或编译即可运行

发展到如今 JavaScript 不仅能在浏览器中执行,也可以在服务端执行,甚至可以在任意存在 Javascript 引擎的设置中执行

浏览器中嵌入了 JavaScript 引擎,有时也称作“JavaScript 虚拟机”。

比如:V8(javascript engine)、SpiderMonkey

引擎是如何工作的?

引擎很复杂,但是基本原理很简单。

  • 引擎(如果是浏览器,则引擎被嵌入在其中)读取(“解析”)脚本。
  • 然后,引擎将脚本转化(“编译”)为机器语言。
  • 然后,机器代码快速地执行。

引擎会对流程中的每个阶段都进行优化。它甚至可以在编译的脚本运行时监视它,分析流经该脚本的数据,并根据获得的信息进一步优化机器代码。

浏览器中的 JavaScript 能做什么?

现代的 JavaScript 是一种“安全的”编程语言。它不提供对内存或 CPU 的底层访问,因为它最初是为浏览器创建的,不需要这些功能。

JavaScript 的能力很大程度上取决于它运行的环境。例如,Node.js 支持允许 JavaScript 读取/写入任意文件,执行网络请求等的函数。

JavaScript 的上层语言

不同的人想要不同的功能。JavaScript 的语法也不能满足所有人的需求。

这是正常的,因为每个人的项目和需求都不一样。

因此,最近出现了许多新语言,这些语言在浏览器中执行之前,都会被 编译(转化)成 JavaScript。

现代化的工具使得编译速度非常快且透明,实际上允许开发者使用另一种语言编写代码并会将其“自动转换”为 JavaScript。

此类语言的示例有:

  • CoffeeScript 是 JavaScript 的一种语法糖。它引入了更加简短的语法,使我们可以编写更清晰简洁的代码。
  • TypeScript 专注于添加“严格的数据类型”以简化开发,以更好地支持复杂系统的开发。由微软开发。
  • Flow 也添加了数据类型,但是以一种不同的方式。由 Facebook 开发。
  • Dart 是一门独立的语言。它拥有自己的引擎,该引擎可以在非浏览器环境中运行(例如手机应用),它也可以被编译成 JavaScript。由 Google 开发。
  • Brython 是一个 Python 到 JavaScript 的转译器,让我们可以在不使用 JavaScript 的情况下,以纯 Python 编写应用程序。
  • Kotlin 是一个现代、简洁且安全的编程语言,编写出的应用程序可以在浏览器和 Node 环境中运行。

这样的语言还有很多。当然,即使我们在使用此类编译语言,我们也需要了解 JavaScript。因为了解 JavaScript 才能让我们真正明白我们在做什么。

基础知识

JavaScript 中的数据类型

原始数据类型:

Number

number 类型代表整数和浮点数
除了常规的数字,还包括所谓的“特殊数值(“special numeric values”)”也属于这种类型:Infinity-InfinityNaN

  • Infinity表示数学概念上的无穷大 ∞ console.log(1/0) || console.log(Infinity)

  • NaN代表一个计算错误它是一个不正确的或者一个未定义的数学操作所得到的结果

    1
    console.log('not a number' / 2) //NaN

    NaN 是粘性的。任何对 NaN 的进一步数学运算都会返回 NaN

    1
    2
    3
    alert(NaN + 1) // NaN
    alert(3 * NaN) // NaN
    alert('not a number' / 2 - 1) // NaN

    数学运算是安全的,脚本永远不会因为一个致命的错误(“死亡”)而停止。最坏的情况下,我们会得到 NaN 的结果。

编写数字的更多方法

1
2
3
4
5
6
7
8
let billion = 1000000000
// 可以使用 _ 这种语法糖
billio = 1_000_000_000
// 使用 e 来缩短
billio = 1e9
console.log(7.3e9)
let msc = 0.000001
msc = 1e-6

十进制、二进制、八进制

十六进制 数字在 JavaScript 中被广泛用于表示颜色,编码字符以及其他许多东西。所以自然地,有一种较短的写方法:0x,然后是数字。

1
2
3
4
console.log(0xff) // 255
console.log(0xff) // 255(一样,大小写没影响)
let a = 0b11111111 // 二进制形式的 255
let b = 0o377 // 八进制形式的 255

toString(base)

方法 num.toString(base) 返回在给定 base 进制数字系统中 num 的字符串表示形式。

1
2
3
4
let num = 255

alert(num.toString(16)) // ff
alert(num.toString(2)) // 11111111

base 的范围可以从 236。默认情况下是 10

舍入

  • Math.floor: 向下舍入

  • Math.ceil: 向上舍入

  • Math.round: 最近舍入

  • Math.trunc: 移除小数点后的所有内容

  • toFixed(n): 将数字舍入到小数点后 n 位,并以字符串形式返回结果。

    1
    2
    3
    4
    5
    6
    let num = 12.34
    alert(num.toFixed(1)) // "12.3"
    num = 12.36
    alert(num.toFixed(1)) // "12.4"
    let num = 12.34
    alert(num.toFixed(5)) // "12.34000",在结尾添加了 0,以达到小数点后五位

    我们可以使用一元加号或 Number() 调用,将其转换为数字,例如 + num.toFixed(5)

不精确的计算:
在内部,数字是以 64 位格式IEEE-754,如果一个如果一个数字真的很大,则可能会溢出 64 位存储,变成一个特殊的数值 Infinity

1
2
console.log(0.1 + 0.2 == 0.3) // false
console.log(0.1 + 0.2) // 0.30000000000000004

一个数字以其二进制的形式存储在内存中,一个 1 和 0 的序列。但是在十进制数字系统中看起来很简单的 0.10.2 这样的小数,实际上在二进制形式中是无限循环小数。

使用二进制数字系统无法 精确 存储 0.1 或 _0.2_,就像没有办法将三分之一存储为十进制小数一样。
IEEE-754 数字格式通过将数字舍入到最接近的可能数字来解决此问题。这些舍入规则通常不允许我们看到“极小的精度损失”,但是它确实存在。

最可靠的方法是借助方法 toFixed(n) 对结果进行舍入:

1
2
let sum = 0.1 + 0.2
alert(sum.toFixed(2)) // "0.30"

isNaN 和 isFinite

  • isNaN(value) 将其参数转换为数字,然后测试它是否为 NaN

    1
    2
    3
    4
    alert(isNaN(NaN)) // true
    alert(isNaN('str')) // true
    console.log(isNaN('123')) // false
    alert(NaN === NaN) // false
  • isFinite(value) 将其参数转换为数字,如果是常规数字而不是 NaN/Infinity/-Infinity,则返回 true

    1
    2
    3
    alert(isFinite('15')) // true
    alert(isFinite('str')) // false,因为是一个特殊的值:NaN
    alert(isFinite(Infinity)) // false,因为是一个特殊的值:Infinity

parseInt 和 pareseFloat:

使用加号 +Number() 的数字转换是严格的。如果一个值不完全是一个数字,就会失败:

1
alert(+'100px') // NaN

但在现实生活中,我们经常会有带有单位的值,例如 CSS 中的 "100px""12pt"

它们可以从字符串中“读取”数字,直到无法读取为止。如果发生 error,则返回收集到的数字。函数 parseInt 返回一个整数,而 parseFloat 返回一个浮点数:

1
2
3
4
5
alert(parseInt('100px')) // 100
alert(parseFloat('12.5em')) // 12.5

alert(parseInt('12.3')) // 12,只有整数部分被返回了
alert(parseFloat('12.3.4')) // 12.3,在第二个点出停止了读取

某些情况下,parseInt/parseFloat 会返回 NaN。当没有数字可读时会发生这种情况:

1
alert(parseInt('a123')) // NaN,第一个符号停止了读取

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

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

1
2
3
alert(parseInt('0xff', 16)) // 255
alert(parseInt('ff', 16)) // 255,没有 0x 仍然有效
alert(parseInt('2n9c', 36)) // 123456

其他数学函数:

Math.random()返回一个从 0 到 1 的随机数(不包括 1

Math.max(a, b, c...)和Math.min(a, b, c...) 从任意数量的参数中返回最大值和最小值。

Math.pow(n, power)返回 n 的给定(power)次幂。

Math.sqrt(100) 取根号

String

  • 单引号: let str = 'hello'
  • 双引号:let str = "hello"
  • 反引号: let str = `hello`

字符串中的方法

常用
  • toUpperCase() || toLowerCase():改变大小写

  • substring(start||0,end?length):获得子串

    MDN 已经不推荐使用 substr 方法了 属于遗留特性 建议使用slice

  • slice(start,end):参数可以为负数,不破坏原来的串返回新的串

  • replace(pattern:(string|regex,replacement:(string|function))):替换(pattern 是如果是 string,则只会替换第一项) –> replaceAll

  • split(separator:(undefined||string||regex),limit?):分割字符形成数组,如果separatorundefined则会形成['str']

  • includes(searchString,position?):boolean: 查找是否包含

  • indexof(serchValue,position?):index||-1:查找的字符串 searchValue 的第一次出现的索引,如果没有找到,则返回 -1

  • lastIndexOf(serchValue,position?)

不常用
  • at()
  • charAt
  • charCodeAt()
  • match(regexp)
  • startsWith(searchString,position?)
  • endsWith(...)
  • trim()

Boolean(逻辑类型)

布尔转换时

  • 值:即 0、” “、undefined、NaN、null 转换为false
  • 其余为 true

注意 “0” 是 true 噢

逻辑运算

或运算(||)

传统的比如 if中使用 就不提了

1
2
3
4
5
6
let res = value1 || value2 || value3
// 处理每一个操作数时,都将其转化为布尔值。如果结果是 true,就停止计算,返回这个操作数的初始值。

// 调用处理逻辑
flag || function()
false || alert("printed");
与运算(&&)
1
2
3
console.log(1 && 0) // 0
console.log(1 && 5) // 5
// 在处理每一个操作数时,都将其转化为布尔值。如果结果是 false,就停止计算,并返回这个操作数的初始值。
非运算(!)

两个非运算 !! 有时候用来将某个值转化为布尔类型:

1
2
alert(!!'non-empty string') // true
alert(!!null) // false

值比较

严格相等

普通的相等性检查 == 存在一个问题,会先转换类型才会进行比较

1
2
console.log(0 == false) //true
console.log('' == false) // true

严格相等运算符 === 在进行比较时不会做任何的类型转换。

1
alert(0 === false) // false,因为被比较值的数据类型不同
nullundefined进行比较
1
2
3
4
5
6
7
8
alert(null === undefined) // false
alert(null == undefined) // true
alert(null > 0) // (1) false
alert(null == 0) // (2) false
alert(null >= 0) // (3) true
alert(undefined > 0) // false (1)
alert(undefined < 0) // false (2)
alert(undefined == 0) // false (3)
???运算符

?运算符

1
2
3
4
let result = condition ? value1 : value2
// 计算条件结果,如果结果为真,则返回 value1,否则返回 value2。
let accessAllowed = age > 18 ? true : false
// 上面写法有点多余 因为 accessAllowed 如果写成 age > 18 本来就会返回一个boolean

使用一系列问号 ? 运算符可以返回一个取决于多个条件的值

1
2
3
4
5
6
7
8
9
10
11
12
let age = prompt('age?', 18)

let message =
age < 3
? 'Hi, baby!'
: age < 18
? 'Hello!'
: age < 100
? 'Greetings!'
: 'What an unusual age!'

alert(message)

? 有时候能代替if但是可读性并不强 赋值的时可以考虑使用 ? 做逻辑判断的时候 if 更佳

空值合并运算符??

对待 nullundefined 的方式类似,所以当一个值既不是 null 也不是 undefined 时,我们将其称为“已定义的(defined)否则为未定义

a ?? b 的结果是:

  • 如果 a 是已定义的,则结果为 a
  • 如果 a 不是已定义的,则结果为 b
1
2
3
4
5
let result = a !== null && a !== undefined ? a : b
let user
alert(user ?? '匿名') // 匿名(user 未定义)
let user = 'John'
alert(user ?? '匿名') // John(user 已定义)
1
2
3
4
5
let firstName = null
let lastName = null
let nickName = 'Supercoder'
// 显示第一个已定义的值:
alert(firstName ?? lastName ?? nickName ?? '匿名') // Supercoder

与||比较

它们之间重要的区别是:

  • || 返回第一个 值。
  • ?? 返回第一个 已定义的 值。

换句话说,|| 无法区分 false0、空字符串 ""null/undefined。它们都一样 —— 假值(falsy values)。如果其中任何一个是 || 的第一个参数,那么我们将得到第二个参数作为结果。

不过在实际中,我们可能只想在变量的值为 null/undefined 时使用默认值。也就是说,当该值确实未知或未被设置时。

1
2
3
4
let height = 0

alert(height || 100) // 100
alert(height ?? 100) // 0

Null

特殊的null值不属于任何一种类型构成了独立类型,仅代表无、空、值未知等状态

1
2
let age = null
// 表示age是未知的

Undefined

特殊值 undefinednull 一样自成类型。
如果一个变量已被声明,但未被赋值,那么它的值就是 undefined

1
2
let age
alert(age) // 弹出 "undefined"

通常,使用 null 将一个“空”或者“未知”的值写入变量中,而 undefined 则保留作为未进行初始化的事物的默认初始值。

Symbol

“symbol” 值表示唯一的标识符,可以使用 Symbol() 来创建这种类型的值

1
let id = Symbol('id')

隐藏属性

symbol 允许我们创建对象的“隐藏”属性,代码的任何其他部分都不能意外访问或重写这些属性。

例如,如果我们使用的是属于第三方代码的 user 对象,我们想要给它们添加一些标识符。

我们可以给它们使用 symbol 键:

1
2
3
4
5
6
7
let user = {
// 属于另一个代码
name: 'John'
}
let id = Symbol('id')
user[id] = 1
console.log(user[id]) // 我们可以使用 symbol 作为键来访问数据

我们的标识符和它们的标识符之间不会有冲突,因为 symbol 总是不同的,即使它们有相同的名字。

对象字面量中的 symbol

如果我们要在对象字面量 {...} 中使用 symbol,则需要使用方括号把它括起来。

1
2
3
4
5
let id = Symbol('id')
let user = {
name: 'John',
[id]: 123 // 而不是 "id":123
}

Symbol 会在 for in 中跳过

symbol 属性不参与 for..in 循环。

1
2
3
4
5
6
7
8
9
let id = Symbol('id')
let user = {
name: 'John',
age: 30,
[id]: 123
}
for (let key in user) alert(key) // name, age(没有 symbol)
// 使用 symbol 任务直接访问
alert('Direct: ' + user[id]) // Direct: 123

Object.keys(user) 也会忽略它们。这是一般“隐藏符号属性”原则的一部分。如果另一个脚本或库遍历我们的对象,它不会意外地访问到符号属性。

相反,Object.assign 会同时复制字符串和 symbol 属性:

1
2
3
4
5
6
let id = Symbol('id')
let user = {
[id]: 123
}
let clone = Object.assign({}, user)
alert(clone[id]) // 123

全局 Symbol

要从注册表中读取(不存在则创建)symbol,请使用 Symbol.for(key)
该调用会检查全局注册表,如果有一个描述为 key 的 symbol,则返回该 symbol,否则将创建一个新 symbol(Symbol(key)),并通过给定的 key 将其存储在注册表中。

1
2
3
4
5
6
// 从全局注册表中读取
let id = Symbol.for('id') // 如果该 symbol 不存在,则创建它
// 再次读取(可能是在代码中的另一个位置)
let idAgain = Symbol.for('id')
// 相同的 symbol
alert(id === idAgain) // true

Symbol.keyFor

1
2
3
4
5
6
// 通过 name 获取 symbol
let sym = Symbol.for('name')
let sym2 = Symbol.for('id')
// 通过 symbol 获取 name
alert(Symbol.keyFor(sym)) // name
alert(Symbol.keyFor(sym2)) // id

BigInt

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

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

1
2
3
const bigint = 1234567890123456789012345678901234567890n
const sameBigint = BigInt('1234567890123456789012345678901234567890')
const bigintFromNumber = BigInt(10) // 与 10n 相同
基本类型和引用类型
  1. Javascript中栈和堆

    • 栈(stack):自动分配固定大小的内存空间,并由系统自动释放,栈数据结构遵从先进后出的原则
    • 堆(heap):堆内存,动态分配内存,内存大小不固定,也不会自动释放,堆数据结构是一种无序的树状结构,满足key-value键值对我们只用知道 key 名,就能通过 key 查找到对应的 value。比较经典的就是书架存书的例子,我们知道书名,就可以找到对应的书籍

  2. 引用类型的引用和复制

    当一个对象变量被复制 —— 引用被复制,而该对象自身并没有被复制。

    1
    2
    3
    let a = { name: '月晕', age: 18 }
    let b = a
    // 这里在堆内存中并没有新new 一份 {name: '月晕', age: 18},而只是把b的内容地址指向a的地址 指向堆内存中的同一份

非原始数据类型:

Object

Object 表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let user = new Object(); // “构造函数” 的语法
let user = {}; // “字面量” 的语法
let user = {
name: "John",
age: 30,
}
// 这将提示有语法错误
user.likes birds = true
// 下面这样不会报错
let user = {};
// 设置
user["likes birds"] = true;
// 读取
alert(user["likes birds"]); // true
// 删除
delete user["likes birds"];

object 中 key 的计算属性

1
2
3
4
5
6
let fruit = prompt('Which fruit to buy?', 'apple')
let bag = {
[fruit]: 5, // 属性名是从 fruit 变量中得到的
[fruit + 'Computers']: 5
}
alert(bag.apple) // 5 如果 fruit="apple"

Object 的引用和复制

对象与原始类型的根本区别之一是,对象是“通过引用”存储和复制的,而原始类型:字符串、数字、布尔值等 —— 总是“作为一个整体”复制。

当一个对象变量被复制 —— 引用被复制,而该对象自身并没有被复制。

1
2
let user = { name: 'John' }
let admin = user // 复制引用

克隆和合并,Object.assign

拷贝一个对象变量会又创建一个对相同对象的引用,复制一个对象,那该怎么做呢?
最先想到的就是遍历一份

1
2
3
4
5
6
7
8
9
10
11
let user = {
name: '月晕',
age: 20
}
let clone = {}
for (let key in user) {
clone[key] = user[key]
}

// 使用 es6 对象展开符
let clone1 = { ...user }

Object.assign来实现

1
2
3
4
5
6
7
8
// Object.assign(dest, src1, src2, src3,...)
let let user = { name: "yueyun" };
let permissions1 = { canView: true };
let permissions2 = { canEdit: true };
// 将 permissions1 和 permissions2 中的所有属性都拷贝到 user 中
Object.assign(user, permissions1, permissions2);
// 现在 user = { name: "John", canView: true, canEdit: true }
// 如果被拷贝的属性的属性名已经存在,那么它会被覆盖

深层克隆:

到现在为止,我们都假设 user 的所有属性均为原始类型。但属性可以是对其他对象的引用。
当数组中存在对象抑或是对象中存在对象就要使用深拷贝

深拷贝可以使用 JSON 序列化(有优缺点)来做或者是自己手写一个深拷贝函数

lodash库中的.cloneDeep(obj)

使用 structuredClone() 去拷贝

Object 中的 this

this 即是函数的上下文,this 出现的值取决于它出现的上下文:函数、类或全局

函数写在对象中称之为对象的方法

方法中的this

通常, 对象方法需要访问对象中存储的信息才能完成其工作。
this 会指向一个对象:

  • 以函数形式调用时、this 指向的是 widow(浏览器环境)/globalThis(nodejs 环境)
  • 以方法的形式调用、this 指向的是调用方法的对象
1
2
3
4
5
6
7
8
let user = {
name: 'John',
age: 30,
sayHi() {
// this 指的是当前对象 John
console.log(this.name)
}
}

this 的值是在代码运行时计算出来的,它取决于代码上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let user = { name: 'John' }
let admin = { name: 'Admin' }

function sayHi() {
alert(this.name)
}

// 在两个对象中使用相同的函数
user.f = sayHi
admin.f = sayHi

// 这两个调用有不同的 this 值
// 函数内部的 "this" 是“点符号前面”的那个对象
user.f() // John(this == user)
admin.f() // Admin(this == admin)
admin['f']() // Admin(使用点符号或方括号语法来访问这个方法,都没有关系。)
箭头函数没有自己的this

箭头函数有些特别:它们没有自己的 this。如果我们在这样的函数中引用 thisthis 值取决于外部“正常的”函数。

1
2
3
4
5
6
7
8
let user = {
firstName: 'Ilya',
sayHi() {
let arrow = () => alert(this.firstName)
arrow()
}
}
user.sayHi() // Ilya

可选链?

可选链?. 是一种访问嵌套对象属性的安全的方式,即使中间属性不存在也不会出现错误

不存在属性问题:

如果我们有很多个 user 对象其中存储了我们的用户数据,我们大多数用户的地址都存储在 user.address 中,街道地址存储在 user.address.street 中,但有些用户没有提供这些信息。在这种情况下,当我们尝试获取 user.address.street,而该用户恰好没提供地址信息,我们则会收到一个错误:

1
2
let user = {} // 一个没有 "address" 属性的 user 对象
console.log(user.address.street) // Error!

javascritp 会把 user.address 识别为 undefined 尝试读取user.address.street 即是undefined.street自然是会失败并返回一个错误

在 Web 开发中,我们可以使用特殊的方法调用(例如 document.querySelector('.elem'))以对象的形式获取一个网页元素,如果没有这种对象,则返回 null

1
2
let html = document.querySelector('.elem').innerHTML
// 如果 document.querySelector('.elem') 的结果为 null,则会出现错误

同样,如果该元素不存在,则访问 null.innerHTML 属性时会报错。在某些情况下,当元素的缺失是没问题的时候,我们希望避免出现这种错误,而是接受 html = null 作为结果。

首先我们想到的肯定是可以用if条件语句判断或者?运算符来解决

1
2
let user = {}
console.log(user.address ? user.address.street : undefined)

当层级多了之后显示会很臃肿而且不优雅 即引入了可选链?

1
2
3
4
let user = {} // user 没有 address 属性
alert(user?.address?.street) // undefined(不报错)
let html = document.querySelector('.elem')?.innerHTML
// 如果没有符合的元素,则为 undefined

如果未声明变量 user,那么 user?.anything 会触发一个错误
?. 前的变量必须已声明(例如 let/const/var user 或作为一个函数参数)。可选链仅适用于已声明的变量。

当然也存在*?.()?.[]

Javascript 中的方法

原始类型的方法

string number bigInt boolean symbol null undefined

比如下面的这样

1
2
let str = 'Hello'
console.log(str.toUpperCase())

str.toUpperCase()中实际发生的情况

  • 字符串str是一个原始值。因此,在访问其属性时,会创建一个包含字符串字面值的特殊对象,并且具有可用的方法,例如 toUpperCase()
  • 该方法运行并返回一个新的字符串(由 console.log 显示)。
  • 特殊对象被销毁,只留下原始值 str

所以原始类型可以提供方法,但它们依然是轻量级的。

JavaScript 引擎高度优化了这个过程。它甚至可能跳过创建额外的对象。但是它仍然必须遵守规范,并且表现得好像它创建了一样。

数字有其自己的方法,例如,toFixed(n) 将数字舍入到给定的精度

数组中的方法

但很多时候我们发现还需要 有序集合,里面的元素都是按顺序排列的。例如,我们可能需要存储一些列表,比如用户、商品以及 HTML 元素等,这时一个特殊的数据结构数组(Array)就派上用场了,它能存储有序的集合。

从 JS 的数据类型本质上面来说 数组属于是一种特殊的对象

  • 添加/删除元素
    • push(...items) —— 向尾端添加元素,
    • pop() —— 从尾端提取一个元素,
    • shift() —— 从首端提取一个元素,
    • unshift(...items) —— 向首端添加元素,
    • splice(pos, deleteCount, ...items) —— 从 pos 开始删除 deleteCount 个元素,并插入 items
    • slice(start, end) —— 创建一个新数组,将从索引 start 到索引 end(但不包括 end)的元素复制进去。
    • concat(...items) —— 返回一个新数组:复制当前数组的所有元素,并向其中添加 items。如果 items 中的任意一项是一个数组,那么就取其元素。
  • 搜索元素
    • indexOf/lastIndexOf(item, pos) —— 从索引 pos 开始搜索 item,搜索到则返回该项的索引,否则返回 -1
    • includes(value) —— 如果数组有 value,则返回 true,否则返回 false
    • find/filter(func) —— 通过 func 过滤元素,返回使 func 返回 true 的第一个值/所有值。
    • findIndexfind 类似,但返回索引而不是值。
  • 遍历元素
    • forEach(func) —— 对每个元素都调用 func,不返回任何内容。
  • 转换数组
    • map(func) —— 根据对每个元素调用 func 的结果创建一个新数组。
    • sort(func) —— 对数组进行原位(in-place)排序,然后返回它。
    • reverse() —— 原位(in-place)反转数组,然后返回它。
    • split/join —— 将字符串转换为数组并返回。
    • reduce/reduceRight(func, initial) —— 通过对每个元素调用 func 计算数组上的单个值,并在调用之间传递中间结果。
  • 其他方法
    • Array.isArray(value) 检查 value 是否是一个数组,如果是则返回 true,否则返回 false

请注意,sortreversesplice 方法修改的是数组本身。

Iterable object(可迭代对象)

可迭代(Iterable) 对像是数组的泛化,即是对象可以在for of循环中使用

数组是可迭代的。但不仅仅是数组。很多其他内建对象也都是可迭代的。例如字符串也是可迭代的。

如果从技术上讲,对象不是数组,而是表示某物的集合(列表,集合),for..of 是一个能够遍历它的很好的语法,因此,让我们来看看如何使其发挥作用。

Symbol.iterator

比如现在有一个range对象代表了一个数字区间

1
2
3
4
5
6
let range = {
from: 1,
to: 5
}
// 我们希望
// for(let num of range) {console.log(num) ...1 2 3 4 5}

为了让range对象可以迭代我们需要手动为其添加一个Symbol.iterator方法

  1. for..of 循环启动时,它会调用这个方法(如果没找到,就会报错)。这个方法必须返回一个 迭代器(iterator) —— 一个有 next 方法的对象。
  2. 从此开始,for..of 仅适用于这个被返回的对象
  3. for..of 循环希望取得下一个数值,它就调用这个对象的 next() 方法。
  4. next() 方法返回的结果的格式必须是 {done: Boolean, value: any},当 done=true 时,表示循环结束,否则 value 是下一个值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
const range = {
from: 1,
to: 5,
[Symbol.iterator]: function () {
return {
current: this.from,
last: this.to,
next() {
if (this.current <= this.last) {
console.log('explosion')
return { done: false, value: this.current++ }
} else {
return { done: true }
}
}
}
}
}

for (let i of range) {
console.log(i)
}

const personInfo = {
name: '月晕',
age: 18,
hobby: ['code', 'play Game', 'sleep'],
[Symbol.iterator]: function () {
let that = this
return {
keys: Object.keys(this),
index: 0,
next() {
if (this.index < this.keys.length) {
return { done: false, value: that[this.keys[this.index++]] }
} else {
return { done: true }
}
}
}
}
}
for (let i of personInfo) {
console.log(i)
}

const personInfo = {
name: '月晕',
age: 18,
hobby: ['code', 'play Game', 'sleep'],
[Symbol.iterator]: function () {
let keys = Object.keys(this)
let index = 0
return {
next: () => {
if (index < keys.length) {
return { done: false, value: this[keys[index++]] }
} else {
return { done: true }
}
}
}
}
}
for (let i of personInfo) {
console.log(i)
}

无穷迭代器(iterator)

无穷迭代器也是可能的。例如,将 range 设置为 range.to = Infinity,这时 range 则成为了无穷迭代器。或者我们可以创建一个可迭代对象,它生成一个无穷伪随机数序列。也是可能的。

next 没有什么限制,它可以返回越来越多的值,这是正常的。

当然,迭代这种对象的 for..of 循环将不会停止。但是我们可以通过使用 break 来停止它。

字符串迭代

1
2
3
4
const chars = 'abcdef'
for (let char of chars) {
console.log(char)
}

显示调用迭代器

为了更深层地了解底层知识,让我们来看看如何显式地使用迭代器。

我们将会采用与 for..of 完全相同的方式遍历字符串,但使用的是直接调用。这段代码创建了一个字符串迭代器,并“手动”从中获取值。

1
2
3
4
5
6
7
8
9
10
11
12
let str = 'yueyun'

// 和 for..of 做相同的事
// for (let char of str) alert(char);

let iterator = str[Symbol.iterator]()

while (true) {
let result = iterator.next()
if (result.done) break
console.log(result.value) // 一个接一个地输出字符
}

很少需要我们这样做,但是比 for..of 给了我们更多的控制权。例如,我们可以拆分迭代过程:迭代一部分,然后停止,做一些其他处理,然后再恢复迭代。

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

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

Array.from

有一个全局方法 Array.from 可以接受一个可迭代或类数组的值,并从中获取一个“真正的”数组。然后我们就可以对其调用数组方法了。

1
2
3
4
5
6
7
8
9
let arrayLike = {
0: 'yueyun',
1: 'suki',
length: 2
}

const arr = Array.from(arrayLike)
console.log(arr)
console.log(arr.pop())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 接受上面的range 生成数组
const range = {
from: 1,
to: 5,
[Symbol.iterator]: function () {
return {
current: this.from,
last: this.to,
next() {
if (this.current <= this.last) {
return { done: false, value: this.current++ }
} else {
return { done: true }
}
}
}
}
}
const rangeArr = Array.from(range)
console.log(rangeArr) // [1 2 3 4 5]

可选的第二个参数 mapFn 可以是一个函数,该函数会在对象中的元素被添加到数组前,被应用于每个元素,此外 thisArg 允许我们为该函数设置 this

1
2
3
4
// 求每个数的平方
let arr = Array.from(range, (num) => num * num)

alert(arr) // 1,4,9,16,25

Map 和 Set(映射和集合)

Map

Map是一个带键的数据项集合.就跟Object一样,区别就是Mapkey允许是任意类型
Map 的方法和属性如下

  • new Map() —— 创建 map。
  • map.set(key, value) —— 根据键存储值。
  • map.get(key) —— 根据键来返回值,如果 map 中不存在对应的 key,则返回 undefined
  • map.has(key) —— 如果 key 存在则返回 true,否则返回 false
  • map.delete(key) —— 删除指定键的值。
  • map.clear() —— 清空 map。
  • map.size —— 返回当前元素个数。

Map 可以使用对象来做键

1
2
3
4
5
6
let john = { name: 'John' }
// 存储每个用户的来访次数
let visitsCountMap = new Map()
// john 是 Map 中的键
visitsCountMap.set(john, 123)
console.log(visitsCountMap.get(john)) // 123

使用对象作为键是 Map 最值得注意和重要的功能之一。在 Object 中,我们则无法使用对象作为键。在 Object 中使用字符串作为键是可以的,但我们无法使用另一个 Object 作为 Object 中的键

map.set调用都会返回 map 本身 即我们可以进行链式调用

Map 迭代

如果要在Map里使用循环 可以使用下面的方法

  • map.keys() 遍历并返回一个包含所有键的可迭代对象
  • map.values() —— 遍历并返回一个包含所有值的可迭代对象,
  • map.entries() —— 遍历并返回一个包含所有实体 [key, value] 的可迭代对象,for..of 在默认情况下使用的就是这个。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let recipeMap = new Map([
['cucumber', 500],
['tomatoes', 300],
['onion', 50]
])
// 遍历所有的键(vegetables)
for (let vegetable of recipeMap.keys()) {
console.log(vegetable) // cucumber, tomatoes, onion
}

// 遍历所有的值(amounts)
for (let amount of recipeMap.values()) {
console.log(amount) // 500, 350, 50
}

// 遍历所有的实体 [key, value]
for (let entry of recipeMap) {
// 与 recipeMap.entries() 相同
console.log(entry) // cucumber,500 (and so on)
}

迭代的顺序与插入值的顺序相同。与普通的 Object 不同,Map 保留了此顺序。

Map中有内建的forEach

1
2
3
4
// 对每个键值对 (key, value) 运行 forEach 函数
recipeMap.forEach((value, key, map) => {
console.log(`${key}: ${value}`) // cucumber: 500 etc
})

Map 和对象的转换

Object.entries从对象创建 Map

1
2
3
4
5
6
let obj = {
name: 'yueyun',
age: 18
}
let map = new Map(Object.entries(obj))
console.log(map.get('name')) // yueyun

Object.fromEntries从 Map 创建对象

1
2
3
4
5
6
7
8
9
10
11
12
13
let prices = Object.fromEntries([
['banana', 1],
['orange', 2],
['meat', 4]
])

// 现在 prices = { banana: 1, orange: 2, meat: 4 }
console.log(prices.orange) // 2
let map = new Map()
map.set('banana', 1)
map.set('orange', 2)
map.set('meat', 4)
let obj = Object.fromEntries(map.entries())

当 Map 中含有对象作为 key 时 专成对象时的 key 会变成'[object object]'

Set

Set 是一个特殊的类型集合 —— “值的集合”(没有键),它的每一个值只能出现一次。 它的主要方法如下:

  • new Set(iterable) —— 创建一个 set,如果提供了一个 iterable 对象(通常是数组),将会从数组里面复制值到 set 中。
  • set.add(value) —— 添加一个值,返回 set 本身
  • set.delete(value) —— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false
  • set.has(value) —— 如果 value 在 set 中,返回 true,否则返回 false
  • set.clear() —— 清空 set。
  • set.size —— 返回元素个数。

它的主要特点是,重复使用同一个值调用 set.add(value) 并不会发生什么改变。这就是 Set 里面的每一个值只出现一次的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let set = new Set()

let john = { name: 'John' }
let pete = { name: 'Pete' }
let mary = { name: 'Mary' }
// visits,一些访客来访好几次
set.add(john)
set.add(pete)
set.add(mary)
set.add(john)
set.add(mary)
// set 只保留不重复的值
alert(set.size) // 3
for (let user of set) {
alert(user.name) // John(然后 Pete 和 Mary)
}

Set 迭代(iteration)

我们可以使用 for..offorEach 来遍历 Set:

1
2
3
4
5
6
7
8
let set = new Set(['oranges', 'apples', 'bananas'])

for (let value of set) alert(value)

// 与 forEach 相同:
set.forEach((value, valueAgain, set) => {
alert(value)
})

WeakMap 和 WeakSet

在垃圾回收中 Javascript 引擎在值“可达”和“可使用”时会将其保存在内存中

1
2
3
4
5
let yueyun = { name: 'yueyun', age: 18 }
// 该对象能访问 yueyun是它的引用
// 覆盖
yueyun = null
// 该对象将会被从内存中清除

通常 当对象,数组之类的数据结构在内存中时,它们的子元素 如对象的属性,数组的元素都是认为可达的 例如,如果把一个对象放入到数组中,那么只要这个数组存在,那么这个对象也就存在,即使没有其他对该对象的引用。

1
2
3
4
5
6
7
let yueyun = { name: 'yueyun', age: 18 }
let arr = [yueyun]
// 覆盖
yueyun = null
// 前面由 yueyun 所引用的那个对象被存储在了 array 中
// 所以它不会被垃圾回收机制回收
// 我们可以通过 array[0] 获取到它

所以当我们使用对象作为 Map 的键的时 如果 Map 存在 那么对象就会一直存在占用内存不会被垃圾回收

WeakMap 在这方面有着根本上的不同。它不会阻止垃圾回收机制对作为键的对象(key object)的回收。

WeakMap

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

1
2
3
4
5
let weakMap = new WeakMap()
let obj = {}
weakMap.set(obj, 'ok') // 正常工作(以对象作为键)
// 不能使用字符串作为键
weakMap.set('test', 'Whoops') // Error,因为 "test" 不是一个对象

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

1
2
3
4
5
let yueyun = { name: 'yueyun' }
let weakMap = new WeakMap()
weakMap.set(yueyun, 'yueyun')
yueyun = null
console.log('weakMap', weakMap.get(yueyun)) // undefined

与常规的map相比 如果yueyun仅仅是作为 WeakMap 的键而存在 —— 它将会被从 map(和内存)中自动删除。

WeakMap 不支持迭代以及 keys()values()entries() 方法。所以没有办法获取 WeakMap 的所有键或值。
WeakMap 只有以下的方法:

  • weakMap.get(key)
  • weakMap.set(key, value)
  • weakMap.delete(key)
  • weakMap.has(key)

WeakSet

WeakSet 的表现类似:

  • Set 类似,但是我们只能向 WeakSet 添加对象(而不能是原始值)。
  • 对象只有在其它某个(些)地方能被访问的时候,才能留在 WeakSet 中。
  • Set 一样,WeakSet 支持 addhasdelete 方法,但不支持 sizekeys(),并且不可迭代。

变“弱(weak)”的同时,它也可以作为额外的存储空间。但并非针对任意数据,而是针对“是/否”的事实。WeakSet 的元素可能代表着有关该对象的某些信息。

例如,我们可以将用户添加到 WeakSet 中,以追踪访问过我们网站的用户:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let visitedSet = new WeakSet()

let john = { name: 'John' }
let pete = { name: 'Pete' }
let mary = { name: 'Mary' }

visitedSet.add(john) // John 访问了我们
visitedSet.add(pete) // 然后是 Pete
visitedSet.add(john) // John 再次访问

// visitedSet 现在有两个用户了

// 检查 John 是否来访过?
alert(visitedSet.has(john)) // true

// 检查 Mary 是否来访过?
alert(visitedSet.has(mary)) // false

john = null

// visitedSet 将被自动清理(即自动清除其中已失效的值 john)

解构赋值

JavaScript 中最常用的数据结构是ObjectArray 解构赋值是一种特殊的语法 将数组或对象拆包到一系列的变量中

数组解构

1
2
3
const [firstName, lastName] = 'yue yun'.split(' ')
console.log(firstName) // yue
console.log(lastName) // yun

解构并没有破坏 只是方便简单的赋值

有想忽略的元素

1
2
const [firstName, , title] = ['yueyun', 'megumi', 'korumi']
console.log(title) // korumi

等号的右侧可以是任何可迭代的对象

1
2
let [a, b, c] = 'abc' // ["a", "b", "c"]
let [one, two, three] = new Set([1, 2, 3])

交换变量值的技巧

1
2
3
4
5
6
7
let guest = 'yue'
let admin = 'yun'

// 让我们来交换变量的值:使得 guest = yue,admin = yun
;[guest, admin] = [admin, guest]

console.log(`${guest} ${admin}`) // yun yue(成功交换!)

其余的 ...

通常,如果数组比左边的列表长,那么“其余”的数组项会被省略。如果我们还想收集其余的数组项 —— 我们可以使用三个点 "..." 来再加一个参数以获取其余数组项:

1
2
3
4
5
6
7
const [name1, name2, ...rest] = [
'Julius',
'Caesar',
'Consul',
'of the Roman Republic'
]
// rest 就是剩下元素的数组集合

我们也能使用...去快速浅拷贝或者赋值

1
2
const oldArr = ['yueyun', 'meigumi', 'kurumi', 'explosion']
const newArr = [...oldArr]

默认值

1
2
3
const [name = 'yueyun', age = 18] = ['yueyun2']
console.log(name) // yueyun
console.log(age) // 18 而不是undefined

对象解构

解构赋值同样适用于对象

1
2
3
4
5
6
7
8
9
// 基本情况是
const { v1, v2 } = { v1: '...', v2: '...' }
let options = {
Dom: 'Nav',
Height: 200,
width: 100
}
let { Dom, Height, Width } = options
console.log(Dom, Height, Width)

属性 options.titleoptions.widthoptions.height 值被赋给了对应的变量。变量的顺序并不重要

可以取别名映射 也可以默认赋值 也可以使用 … 去解构

1
2
3
4
5
6
7
8
9
let options = {
title: 'Menu'
}

let { width: w = 100, height: h = 200, title } = options

console.log(title) // Menu
console.log(w) // 100
console.log(h) // 200

注意使用声明 (javascript 代码块)

嵌套解构

建议不要使用捏 会让简单的变得很烦

智能函数参数

有这样的场景 一个函数需要接受很多参数 而且大部分参数都是可选的
下面是很糟糕的写法

1
2
3
function showMenu(title = 'Untitled', width = 200, height = 100, items = []) {
// ...
}

在实际开发中,记忆如此多的参数的位置是一个很大的负担。通常集成开发环境(IDE)会尽力帮助我们,特别是当代码有良好的文档注释的时候,但是…… 另一个问题就是,在大部分的参数只需采用默认值的情况下,调用这个函数时会需要写大量的 undefined。

1
2
// 在采用默认值就可以的位置设置 undefined
showMenu('My Menu', undefined, undefined, ['Item1', 'Item2'])

这太难看了。而且,当我们处理更多参数的时候可读性会变得很差。

解构赋值可以解决这些问题。我们可以用一个对象来传递所有参数,而函数负责把这个对象解构成各个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 我们传递一个对象给函数
let options = {
title: 'My menu',
items: ['Item1', 'Item2']
}

// ……然后函数马上把对象解构成变量
function showMenu({
title = 'Untitled',
width = 200,
height = 100,
items = []
}) {
// title, items – 提取于 options,
// width, height – 使用默认值
alert(`${title} ${width} ${height}`) // My Menu 200 100
alert(items) // Item1, Item2
}

showMenu(options)

我们也可以使用带有嵌套对象和冒号映射的更加复杂的解构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let options = {
title: 'My menu',
items: ['Item1', 'Item2']
}

function showMenu({
title = 'Untitled',
width: w = 100, // width goes to w
height: h = 200, // height goes to h
items: [item1, item2] // items first element goes to item1, second to item2
}) {
alert(`${title} ${w} ${h}`) // My Menu 100 200
alert(item1) // Item1
alert(item2) // Item2
}

showMenu(options)

完整语法和解构赋值是一样的:

1
2
3
4
function({
incomingProperty: varName = defaultValue
...
})

我们可以通过指定空对象 {} 为整个参数对象的默认值来解决这个问题:

1
2
3
4
5
function showMenu({ title = 'Menu', width = 100, height = 200 } = {}) {
alert(`${title} ${width} ${height}`)
}

showMenu() // Menu 100 200

JSON 方法 toJSON

javascript 的一些数据结构是属于独有的 当传输网络数据或者在日志输出的时候需要传输数据

JSON.stringify

JSON(JavaScript Object Notation)是表示值和对象的通用格式。在 RFC 4627 标准中有对其的描述。最初它是为 JavaScript 而创建的,但许多其他编程语言也有用于处理它的库。因此,当客户端使用 JavaScript 而服务器端是使用 Ruby/PHP/Java 等语言编写的时,使用 JSON 可以很容易地进行数据交换。

JavaScript 提供了如下方法:

  • JSON.stringify 将对象转换成JSON
  • JSON.parse 将 JSON 转换成对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let student = {
name: 'John',
age: 30,
isAdmin: false,
courses: ['html', 'css', 'js'],
spouse: null
}

let json = JSON.stringify(student)

alert(typeof json) // we've got a string!

alert(json)
/* JSON 编码的对象:
{
"name": "John",
"age": 30,
"isAdmin": false,
"courses": ["html", "css", "js"],
"spouse": null
}
*/

方法JSON.stingify(stduent)接受对象并将其转换成字符串

得到的JSON字符串是一个被称之为JSON 编码(JSON-encoded)或 序列化 或 字符串化 或 编组化的对象
JSON.stringify 也可以应用于原始(primitive)数据类型。

JSON 支持的数据类型:

  • Objects { ... }
  • Arrays [ ... ]
  • Primitives:
    • strings,
    • numbers,
    • boolean values true/false
    • null

JSON 是语言无关的纯数据规范,因此一些特定于 JavaScript 的对象属性会被 JSON.stringify 跳过。即:

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

支持嵌套对象转换 但是不能循环引用
JSON.stringify的完整语法是
let json = JSON.stringify(value,replacer, space)
value:要编码的值、replacer:要编码属性数组活映射函数、space:用于美化输出的空格数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const room = {
number: 23
}
let meetup = {
title: 'Conference',
participants: [{ name: 'John' }, { name: 'Alice' }],
place: room // meetup 引用了 room
}
room.occupiedBy = meetup // room 引用了 meetup
// console.log(meetup)
console.log(
JSON.stringify(meetup, function replacer(key, value) {
console.log(`${key}: ${value}, type: ${typeof value}`)
// return key != '' && value == meetup ? undefined : value
return key == 'occupiedBy' ? undefined : value
})
)
/**
0: [object Object], type: object
name: John, type: string
1: [object Object], type: object
name: Alice, type: string
place: [object Object], type: object
number: 23, type: number
occupiedBy: [object Object], type: object
{"title":"Conference","participants":[{"name":"John"},{"name":"Alice"}],"place":{"number":23}}
*/

JSON.parse

要解码 JSON 字符串 需要使用JSON.parse方法
let value = JSON.parse(str,reviver)
str:要解析的 JSON 字符串、reviver:可选的函数,将为每个(键,值)对调用此函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 字符串化数组
let numbers = '[0, 1, 2, 3]'
numbers = JSON.parse(numbers)
console.log(numbers[1]) // 1
let userData =
'{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }'
let user = JSON.parse(userData)
console.log(user.friends[1]) // 1
// 反序列化的时候如果遇到特殊对象会调用reviver函数
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
})
console.log(meetup.date.getDate()) // 现在正常运行了!
总结
  • JSON 是一种数据格式,具有自己的独立标准和大多数编程语言的库。
  • JSON 支持 object,array,string,number,boolean 和 null。
  • JavaScript 提供序列化(serialize)成 JSON 的方法 JSON.stringify 和解析 JSON 的方法 JSON.parse。
  • 这两种方法都支持用于智能读/写的转换函数。
  • 如果一个对象具有 toJSON,那么它会被 JSON.stringify 调用。

规范和调试

高级内容

函数

递归

在函数解决任务时 调用了自身就是所谓的递归

比如想在要完成一个函数pow(x, n)可以计算xn次方 有两种解法

  1. 迭代思路 使用for循环

    1
    2
    3
    4
    5
    6
    7
    function pow(x, n) {
    let result = 1
    for (let i = 0; i < n; i++) {
    result *= x
    }
    return result
    }
  2. 递归思路:简化任务 调用自身

    1
    2
    3
    4
    5
    6
    7
    function pow(x, n) {
    if (n === 1) {
    return x
    } else {
    return x * pow(x, n - 1)
    }
    }

pow(x, n)被调用时 执行分为下面两个分支:

1
2
3
4
5
              if n==1  = x
/
pow(x, n) =
\
else = x * pow(x, n - 1)
  • n===1时 叫做基础递归 因为会产生明显的结果
  • 可以使用 x * pow(x, n - 1)表示pow(x, n) 这就递归步骤将人物转化为更简单的行为和更简单的同类任务调用 (带有更小的 npow 运算)。接下来的步骤将其进一步简化,直到 n 达到 1

递归将函数调用简化成为一个更简单的函数调用 然后在将其简化为一个更加简单的函数 以此类推 直到结果变得显而易见

最大的嵌套调用次数(包括首次)被称为递归深度 在上面这个例子正好为 n

执行上下文和堆栈

递归调用的工作 函数底层的工作原理

有关正在运行的函数的执行过程的相关信息被存储在其执行上下文中

执行上下文是一个内部据结构 他包含有关函数执行时的详细细节:

  • 当前控制流所在的位置 (作用域链):每个执行上下文都有一个与之相关联的作用域链。作用域链是一个对象列表,它定义了变量和函数的查找规则,决定了代码在哪些区域是有效的。当代码在一个执行上下文中查找变量时,如果在当前上下文的变量对象中找不到,它会沿着作用域链向上查找。
  • 当前的变量:包含函数的参数、局部变量、函数声明、变量声明 在函数执行的初始阶段 函数所有的参数值、函数内部的函数声明以及变量声明都会被添加到变量对象中。
  • this的值:表示调用上下文,依赖于函数的调用方式 全局执行上下文 函数执行上下文(如何被调用)
  • 及内部的一些细节

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

当一个函数进行嵌套调用的时候 将发生

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

比如现在来分析上面 pow(2,3) 这个例子 使用抽象的来表示一下执行流程

  1. 在调用pow(2, 3)而开始,执行上下文(context)会储存变量:x = 2, n = 3 执行流程在函数而第一行我们将其定义为
    Context: { x:2, n:3, at line 1 } call pow(2, 3)
    当函数开始执行的时 进入第二条分支 变量相同但是位置改变了
    **Context: { x:2, n:3, at line 5 } call pow(2, 3) **

    执行到计算 x * pow(x, n - 1) 需要带入新的参数新的pow子调用pow(2,2)

  2. 为了执行嵌套调用,JavaScript 会在 执行上下文堆栈 中记住当前的执行上下文。

    这里我们调用相同的函数 pow,但这绝对没问题。所有函数的处理都是一样的:

    1. 当前上下文被“记录”在堆栈的顶部。
    2. 为子调用创建新的上下文。
    3. 当子调用结束后 —— 前一个上下文被从堆栈中弹出,并继续执行。

    下面是进入子调用pow(2, 2)时的上下文堆栈:

    **Context: { x:2, n:2, at line 5 } call pow(2, 2) **
    **Context: { x:2, n:3, at line 5 } call pow(2, 3) **
    当我们完成了子调用后 很容易恢复一个上下文 因为它既保留了变量 也保留了当时代码的确切位置

  3. 执行pow(2, 1) 重复过程 现在的调用堆栈
    Context: { x:2, n:1, at line 5 } call pow(2, 1)

    **Context: { x:2, n:2, at line 5 } call pow(2, 2) **
    **Context: { x:2, n:3, at line 5 } call pow(2, 3) **

  4. 出口 即使调用堆栈 出栈口

递归可以更加简单明了优雅的描述出一段代码的逻辑 虽然性能上可能不如循环但是在一些复杂的数据结构下面使用递归往往更好 (比如 树 链表等)

Rest 参数和 Spread 语法

简单来说就是

function sum (...args)

1
2
cosnt arr2 = [1,2,3,4,5]
const arr1 = [...arr2,6,7,8,9]

变量作用域和闭包

JavaScript是非常面向对象和函数的语言 会有很大的自由度和写法 我们可以随时创建函数可以将函数作为参数传递 在任意不同的代码位置调用 可以访问外部的环境

代码块

如果在{ ... }内声明变量 那么这个变量并不会向外传递 只能在内部访问该代码块内可见

1
2
3
4
5
6
7
8
let a = 10
{
let a = 20
console.log('2a', a)
}
console.log('a', a)
// 2a 20
// a 10

if, for, while{...}中声明的变量也仅在内部可见

嵌套函数

如果一个函数在另外一个函数中创建的 被称为高级函数或者嵌套函数

1
2
3
4
5
6
7
8
9
function sayHiBye(firstName, lastName) {
// 辅助嵌套函数使用如下
function getFullName() {
return firstName + ' ' + lastName
}

console.log('Hello, ' + getFullName())
console.log('Bye, ' + getFullName())
}

词法环境

变量:
在 Javascript 中每个 运行的函数 代码块 { ... } 以及整个脚本都有一个被称为词法**环境(Lexical Enviroment)**的内部的关联对象

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

  • 环境记录(Enviroment Record) 一个存储所有局部变量作为其属性 (包括一些其他的信息 例如this的值)的对象
  • 外部词法环境的引用 与外部代码相关联

一个变量只是**”环境记录”**这个特殊的内部对象的一个属性 获取或修改变量一味着获取或修改词法环境的一个属性 “获取或修改变量” 意味着 获取或修改词法环境的一个属性

比如下面的一个最简单的例子

1
2
let phrase = 'hello'
console.log(phrase)

这个就是所谓的与整个脚本相关联的全局词法环境

在上面的过程中 矩形区域表示环境记录(变量存储) 箭头表示外部引用 全局词法环境没有外部引用 所以箭头指向了null

随着代码的开始继续的执行 词法环境发生了变化

上面的图片中右侧演示了执行过程中词法环境的变化:

  1. 当脚本开始运行 词法环境先填充了所有声明的变量
    在最初 它们处于未初始化的状态这是一种特殊的内部状态 这意味着引擎知道这个变量存在但是在 let 声明之前 不能引用它 几乎就跟不存在一样
  2. let phrase 定义出现 尚未被赋值 因此值是undefined
  3. phrase被赋予了一个值
  4. phrase被修改

实际上执行的过程是

  • 变量是特殊内部对象的属性 与当前正在执行的**(代码)块/函数/脚本** 有关
  • 操作变量实际上是操作该对象的属性

词法环境是一个规范的对象 是存在于语言规范的理论层面 用于描述是如何工作的 我们无法在代码块中获取该对象并直接进行操作

函数声明:

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

不同就在于 如果是函数声明的初始化会被立刻完成

当创建了一个词法环境时,函数会立即变成即用型函数( 并不像 let 那样到声明处才可以去使用)
例如 下面是添加一个函数时全局词法环境的初始状态

这种行为仅适用于函数声明 而不适用于匿名函数的声明 比如let sayHello = function () {...} 或者 let sayhello = () => {...}

内部和外部的词法环境
当一个函数运行时 在调用刚开始 会自动创建一个新的词法环境以存储这个调用的局部变量和参数 例如对于say("yueyun") 的执行流程如下

1
2
3
4
5
let phrase = ' Hello '
function say(name) {
cosole.log(`${phrase}, ${name}`)
}
say('yueyun')

在函数调用期间我们拥有两个词法环境 内部一个(用于函数调用) 和外部一个(全局):

  • 内部词法环境与say的当前执行相对应 它具有单独的属性:name 函数的参数 调用的是say("yueyun")所以name的值为yueyun
  • 外部词法环境是全局词法环境 它具有phrase变量和函数本身

内部词法环境引用了outer

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

返回函数:

比如下面的例子

1
2
3
4
5
6
7
function makeCounter() {
let count = 0
return function () {
return count++
}
}
let counter = makeCounter()

在每次makeCounter()调用的开始,都会创建一个新的词法环境对象,以存储该makeCounter运行时的变量

因此,我们有两层嵌套的词法环境

不一样的是 在执行 makeCounter()的过程中创建了一个仅占一行的嵌套函数 return count++ 我们并没有运行它 只是创建了这么一个函数

所有的函数在创建时都会记住它的词法环境 从技术上来说 所有的函数都有名为[[Environment]]的隐藏属性 该属性保存了对创建对象该函数的词法环境的应引用

因此 counter.[[Env]] 有对 {count: 0}词法环境的引用 这就是函数记住它创建于何处的方式与调用无关 [[Environment]] 引用在函数创建时被设置并永久保存。

稍后调用counter()时,会自动创建一个新的词法环境 并且其外部词法环境引用获取于counter.[[Environment]]

现在,当 counter() 中的代码查找 count 变量时,它首先搜索自己的词法环境(为空,因为那里没有局部变量),然后是外部 makeCounter() 的词法环境,并且在哪里找到就在哪里修改。

在变量所在的词法环境中更新变量

如果我们调用 counter() 多次,count 变量将在同一位置增加到 23 等。

闭包是一个编程术语 是指一个函数可以记住其他外部变量并可以访问这些变量 在某些编程语言中 会有不同的差异 但在Javascript中 所有的函数天生都是闭包的 即JavaScirpt中的函数会自动通过隐藏[[Environment]]属性记住创建它们的位置 所以它们都可以访问外部变量

垃圾回收

通常,函数调用完成后,会将词法环境和其中的所有变量从内存中删除。因为现在没有任何对它们的引用了。与 JavaScript 中的任何其他对象一样,词法环境仅在可达时才会被保留在内存中。

但是 如果有一个嵌套函数在函数结束后的语句任然可达 则它将具有引用词法环境的 [[Environment]] 属性。

1
2
3
4
5
6
7
8
9
function f() {
let value = 123

return function () {
console.log(value)
}
}

let g = f() // g.[[Environment]] 存储了对相应 f() 调用的词法环境的引用

如果多次调用 f(),并且返回的函数被保存,那么所有相应的词法环境对象也会保留在内存中。下面代码中有三个这样的函数:

1
2
3
4
5
6
7
8
9
10
function f() {
let value = Math.random()

return function () {
alert(value)
}
}

// 数组中的 3 个函数,每个都与来自对应的 f() 的词法环境相关联
let arr = [f(), f(), f()]

当词法环境对象变得不可达时,它就会死去(就像其他任何对象一样)。换句话说,它仅在至少有一个嵌套函数引用它时才存在。

在下面的代码中,嵌套函数被删除后,其封闭的词法环境(以及其中的 value)也会被从内存中删除:

1
2
3
4
5
6
7
8
9
10
11
function f() {
let value = 123

return function () {
alert(value)
}
}

let g = f() // 当 g 函数存在时,该值会被保留在内存中

g = null // ……现在内存被清理了

全局对象和函数对象

全局对象

全局对象提供可以在任何地方都使用的变量和函数 默认的情况下这些全局变量内建于语言或环境中

在浏览器环境中 全局对象是window 对于nodejs运行时环境 全局对象是global

在最新的规定中globalThis 被作为全局对象的标准名称加入到了 JavaScript 中,所有环境都应该支持该名称。所有主流浏览器都支持它。

全局对象的所有属性都可以直接被访问

1
2
3
alert('hello')
// 等同于
window.alert('Hello')

在浏览器中 使用var声明的全局函数和变量都会成为全局属性

1
2
var gVar = 10
console.log(window.gVar)

请不要这样去使用!这种行为是出于兼容性而存在的。现代脚本使用 JavaScript modules 所以不会发生这种事情。

如果我们使用 let,就不会发生这种情况

如果一个值非常重要你想让它在全局的范围中

1
2
3
4
5
6
7
8
9
10
11
// 将当前用户信息全局化,以允许所有脚本访问它
window.currentUser = {
name: 'yueyun'
}

// 代码中的另一个位置
alert(currentUser.name) // yueyun

// 或者,如果我们有一个名为 "currentUser" 的局部变量
// 从 window 显式地获取它(这是安全的!)
alert(window.currentUser.name) // yueyun

函数对象

在 JavaScript 中函数也是一个值,函数值的类型是 object

可以把函数理解成为一个可调用的行为对象(action object) 同样可以传递属性和引用传递

比如属性 name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function sayHi() {
console.log('Hi')
}
console.log(sayHi, sayHi.name) //[Function: sayHi] sayHi

//***********************************************************
let sayHi = function () {
console.log('Hi')
}

console.log(sayHi.name) // sayHi(有名字!)

//***********************************************************
function f(sayHi = function () {}) {
alert(sayHi.name) // sayHi(生效了!)
}

f()

规范中把这种特性叫做「上下文命名」。如果函数自己没有提供,那么在赋值中,会根据上下文来推测一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let user = {
sayHi() {
// ...
},

sayBye: function () {
// ...
}
}

alert(user.sayHi.name) // sayHi
alert(user.sayBye.name) // sayBye

// 函数是在数组中创建的
let arr = [function () {}]

alert(arr[0].name) // <空字符串>
// 引擎无法设置正确的名字,所以没有值

属性 length

1
2
3
4
5
6
7
function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}

alert(f1.length) // 1
alert(f2.length) // 2
alert(many.length) // 2

属性 length 有时在操作其它函数的函数中用于做 内省/运行时检查(introspection)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function ask(question, ...handlers) {
let isYes = confirm(question)
for (let handler of handlers) {
if (handler.length === 0) {
if (isYes) handler()
} else {
handler(isYes)
}
}
}
// 对于肯定的回答,两个 handler 都会被调用
// 对于否定的回答,只有第二个 handler 被调用
ask(
'Question?',
() => alert('You said yes'),
(result) => alert(result)
)

自定义属性

我们可以在函数中添加counter属性记录被调用了多少次

1
2
3
4
5
6
7
8
9
function sayHi() {
console.log('Hi')
sayHi.counter++
}
sayHi.counter = 0 // 初始值
sayHi() // Hi
sayHi() // Hi

alert(`Called ${sayHi.counter} times`) // Called 2 times

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

函数属性有时会用来替代闭包 如下面修改之前写过的闭包

1
2
3
4
5
6
7
8
9
10
function makeCounter() {
function counter() {
return counter.count++
}
counter.count = 0
return counter
}
let counter = makeCounter()
alert(counter()) // 0
alert(counter()) // 1

count被直接存储在函数里,而不是它外部的词法环境

这种写法一般不太常见 因为可以在外部去修改它的属性 从而导致代码很混乱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function makeCounter() {
function counter() {
return counter.count++
}

counter.count = 0

return counter
}

let counter = makeCounter()
// 可以任意的修改
counter.count = 10
console.log(counter()) // 10

命名函数的表达式

命名函数表达式(NFE, Named Function Expression) 指带有名字的函数表达式术语

1
2
3
let sayHi = function func(who) {
console.log(`Hello, ${who}`)
}

它仍然是一个函数表达式。在 function 后面加一个名字 "func" 没有使它成为一个函数声明,因为它仍然是作为赋值表达式中的一部分被创建的。

添加这个名字当然也没有打破任何东西。函数依然可以通过 sayHi() 来调用:

关于添加func的两个特殊的地方

  • 允许函数在内部引用自己
  • 它在函数外是引用不到的
1
2
3
4
5
6
7
8
9
10
11
let sayHi = function func(who) {
if (who) {
alert(`Hello, ${who}`)
} else {
func('Guest') // 使用 func 再次调用函数自身
}
}

sayHi() // Hello, Guest
// 但这不工作:
func() // Error, func is not defined(在函数外不可见)

同样不适用sayHi()去写递归 因为 sayHi()很容易就被外部污染

1
2
3
4
5
6
7
8
9
10
11
12
let sayHi = function (who) {
if (who) {
console.log(`Hello, ${who}`)
} else {
sayHi('Guest') // Error: sayHi is not a function
}
}

let welcome = sayHi
sayHi = null

welcome() // Error,嵌套调用 sayHi 不再有效!

当我们需要一个可靠的内部名时,这就成为了你把函数声明重写成函数表达式的理由了。

调度:setTimeout 和 setInterval

当我们并不想立刻执行一个函数,而是等待特定一段时间之后再执行。这就是所谓的“计划调用(scheduling a call)”。

目前的实现方式有下面两种方式实现

  • setTimeout: 允许我们将函数推迟到一段时间间隔之后再执行
  • setInterval: 允许我们重复运行一个函数,从一段时间间隔之后开始运行,之后以该时间间隔连续重复运行该函数。

setTimeout

1
2
3
4
5
6
7
8
9
let sayHi = function (who) {
let count = 0
return function () {
count++
console.log(`Hello, ${who}! ${count}`)
}
}

setTimeout(sayHi('yueyun'), 1000)

setTimeout 期望得到一个对函数的引用

clearTimeout 来取消调度

setTimeout 在调用时会返回一个“定时器标识符(timer identifier)”,在我们的例子中是 timerId,我们可以使用它来取消执行。

1
2
let timerId = setTimeout(...);
clearTimeout(timerId);

setInterval

setInterval 方法和 setTimeout 的语法相同:不过与 setTimeout 只执行一次不同,setInterval 是每间隔给定的时间周期性执行。

装饰器模式和转发:Call/apply

JavaScript在处理函数时提供了很高的灵活性,它们可以被传递 用作对象 下面将介绍它们之间的转发(forward)装饰(decorate)

透明缓存

假设现在我们有一个 CPU 重负载的函数slow(x) 但是他纯函数 给定相同的参数总是会返回相同的结果 如果这个函数使用频繁 我们希望能记住这个缓存能记住 因此避免花费额外的时间 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function slow(x) {
// big Clc
return x
}

function cachingDecorator(func) {
const cache = new Map()
return function (x) {
if (cache.has(x)) {
return cache.get(x) // 从缓存中读取结果
}
let result = func(x)
cache.set(x, result) // 将结果记录下来
return result
}
}
slow = cachingDecorator(slow)
consle.log(slow(1)) // 被缓存下 并返回结果
console.log('Again', slow(1)) //返回缓存记录的结果

在上面的例子中 cachingDecorator是一个装饰器(decorator)

这样我们可以为任何函数调用cachingDecorator 它将返回缓存包装器 这样别的函数需要这种特性就可以直接复用 还可以将缓存与主代码分开变得更加简单

cachingDecorator(func) 的结果是一个“包装器”:function(x)func(x) 的调用“包装”到缓存逻辑中 从外部代码来看,包装的 slow 函数执行的仍然是与之前相同的操作。它只是在其行为上添加了缓存功能。

使用分离的 cachingDecorator 而不是改变 slow 本身的代码有几个好处

  • cachingDecorator 是可重用的。我们可以将它应用于另一个函数。
  • 缓存逻辑是独立的,它没有增加 slow 本身的复杂性(如果有的话)。
  • 如果需要,我们可以组合多个装饰器(其他装饰器将遵循同样的逻辑)。
function.call 设定上下文

但是如果我们在对象中这样使用的话呢 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let worker = {
someMethod() {
return 1
},
slow(x) {
// calc
return x * this.someMethod()
}
}
function cachingDecorator(func) {
let cache = new Map()
return function (x) {
if (cache.has(x)) {
return cache.get(x)
}
let result = func(x) // (*step*)
cache.set(x, result)
return result
}
}

alert(worker.slow(1)) // 原始方法有效

worker.slow = cachingDecorator(worker.slow) // 现在对其进行缓存

alert(worker.slow(2)) // 报错!Error: Cannot read property 'someMethod' of undefined

错误在于试图访问this.someMethod失败了 原因是包装器将原始函数调用为 (*step*) 行中的 func(x)。但是这样调用得到的this=undefined 这是因为包装器将调用传递给原始方法 但是并没有上下文的this

使用内建的函数方法function.call(context,...args)允许调用一个显示设置的this函数

例如,在下面的代码中,我们在不同对象的上下文中调用 sayHisayHi.call(user) 运行 sayHi 并提供了 this=user,然后下一行设置 this=admin

1
2
3
4
5
6
7
8
9
10
function sayHi() {
console.log(this.name)
}

let user = { name: 'John' }
let admin = { name: 'Admin' }

// 使用 call 将不同的对象传递为 "this"
sayHi.call(user) // John
sayHi.call(admin) // Admin

在我们的例子中,我们可以在包装器中使用 call 将上下文传递给原始函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let worker = {
someMethod() {
return 1
},
slow(x) {
// calc
return x * this.someMethod()
}
}
function cachingDecorator(func) {
let cache = new Map()
return function (x) {
if (cache.has(x)) {
return cache.get(x)
}
let result = func.call(this, x)
cache.set(x, result)
return result
}
}

alert(worker.slow(1))

worker.slow = cachingDecorator(worker.slow)

alert(worker.slow(1))

现在一切工作正常 this的传递过程

  • 在经过装饰之后,worker.slow 现在是包装器 function (x) { ... }
  • 因此,当 worker.slow(2) 执行时,包装器将 2 作为参数,并且 this=worker(它是点符号 . 之前的对象)。
  • 在包装器内部,假设结果尚未缓存,func.call(this, x) 将当前的 this=worker)和当前的参数(=2)传递给原始方法。
传递多个参数

记住参数组合(min,max)的结果

  • 实现一个新的类似于 map 的更通用的并且允许多个键的数据结构
  • 使用嵌套的 map 去实现比如map.get(min).get(max)来获取 result
  • 将两个值合并成一个 多为装饰器添加一个函数

现在以第三种方法写出带多个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let worker = {
slow(min, max) {
return min + max
}
}
function cachingDecorator(func) {
const cache = new Map()
return function (...args) {
// **
let key = hash(args)
if (cache.has(key)) {
return cache.get(key)
}
let result = func.call(this, ...args) //***
cache.set(key, result)
return result
}
}

function hash(args) {
return args.join(',')
}

现在这个包装器可以处理任意数量的参数了

  • (**) 行中它调用 hash 来从 arguments 创建一个单独的键。这里我们使用一个简单的“连接”函数,将参数 (3, 5) 转换为键 "3,5"。更复杂的情况可能需要其他哈希函数。
  • 然后 (***) 行使用 func.call(this, ...arguments) 将包装器获得的上下文和所有参数(不仅仅是第一个参数)传递给原始函数。
function.apply

applycall的用法类似 区别就是 apply 希望接受的是一个参数列表而不是多个参数

1
function.call(this,...args) === function.apply(this,args)
装饰器和属性函数

通常,用装饰的函数替换一个函数或一个方法是安全的,除了一件小东西。如果原始函数有属性,例如 func.calledCount 或其他,则装饰后的函数将不再提供这些属性。因为这是装饰器。因此,如果有人使用它们,那么就需要小心。

例如,在上面的示例中,如果 slow 函数具有任何属性,而 cachingDecorator(slow) 则是一个没有这些属性的包装器。

一些包装器可能会提供自己的属性。例如,装饰器会计算一个函数被调用了多少次以及花费了多少时间,并通过包装器属性公开(expose)这些信息。

存在一种创建装饰器的方法,该装饰器可保留对函数属性的访问权限,但这需要使用特殊的 Proxy 对象来包装函数。

函数绑定

在将对象的方法作为回调进行传递 例如传递给setTimeout的时候 会存在一个常见的问题即是丢失this

比如下面的情况

1
2
3
4
5
6
7
8
let user = {
name: 'yueyun',
sayHi() {
console.log(`hello ${this.name}`)
}
}

setTimeout(user.sayHi, 1000) // 输出 Hello,undefined

这是因为setTimeout获取到了函数user.sayHi 但是他和对象分开了 this丢失了

解决办法 1 用函数包括执行

1
2
3
4
5
6
7
8
9
10
let user = {
name: 'yueyun',
sayHi() {
console.log(`hello ${this.name}`)
}
}

setTimeout(() => {
user.sayHi()
}, 1000)

这样即可以成功

但是这样又会存在 可能在定时器还在计时的过程中如果 sayHi() 函数发生变化 那么又会调用到错误的对象this

解决办法 2 bind
func.bind(context)的结果是一个特殊的类似于函数的“外来对象”,它可以像函数一样被调用,并且透明地将调用传递给 func 并设定 this=context

如下

1
2
3
4
5
6
7
8
let user = {
name: 'yueyun'
}
function func() {
console.log(`hello ${this.name}`)
}
let funcUser = func.bind(user)
funcUser() // hello yueyun

上面例子的解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let user = {
firstName: 'yueyun',
sayHi() {
console.log(`Hello, ${this.firstName}!`)
}
}

user.sayHi = user.sayHi.bind(user)

setTimeout(user.sayHi, 1000)

user = {
sayHi() {
console.log('Another user in setTimeout!')
}
}

箭头函数

箭头函数不仅仅是编写简介代码的”捷径” 还具有非常特殊有用的特性

JavaScript 充满了我们需要编写在其他地方执行的小函数的情况

例如:

  • arr.forEach(func) 每个元素都执行func
  • setTimeout(func) 由内建调度器执行
  • ….

JavaScript 的精髓在于创建一个函数并将其传递到某个地方。

在这样的函数中,我们通常不想离开当前上下文。这就是箭头函数的应用场景

箭头函数没有 this

1
2
3
4
5
6
7
8
9
10
let group = {
title: 'Our Group',
students: ['John', 'Pete', 'Alice'],

showList() {
this.students.forEach((student) => console.log(this.title + ': ' + student))
}
}

group.showList()

这里forEach中使用了箭头函数 其中的this.title 其实和外部方法showList完全一样

如果我们使用正常的函数 则会出现错误

1
2
3
4
5
6
7
8
9
10
11
12
13
let group = {
title: 'Our Group',
students: ['John', 'Pete', 'Alice'],

showList() {
this.students.forEach(function (student) {
// Error: Cannot read property 'title' of undefined
console.log(this.title + ': ' + student)
})
}
}

group.showList()

报错是因为 forEach 运行它里面的这个函数,但是这个函数的 this 为默认值 this=undefined,因此就出现了尝试访问 undefined.title 的情况。但箭头函数就没事,因为它们没有 this

warning 不能对箭头函数进行new操作 不具有this自然就意味着箭头函数不能作为构造(constructor)器

箭头函数没有 arguments

箭头函数也没有arguments变量

对象

属性标志和属性描述符

我们知道 对象可以存储属性到目前为止,属性对我们来说只是一个简单的“键值”对。但对象属性实际上是更灵活且更强大的东西。

属性标志

对象属性(properties) 是除了value 还有三个特殊的特性(attributes) 即标志

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let user = {
name: 'yueyun'
}

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

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

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

使用的语法是Object.defineProperty(obj,propertyName,descriptor)

obj,propertyName 要应用描述符的对象及其属性 descriptor要应用的属性描述符对象

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let user = {}

Object.defineProperty(user, 'name', {
value: 'yueyun'
})

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

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

将它与上面的“以常用方式创建的” user.name 进行比较:现在所有标志都为 false。如果这不是我们想要的,那么我们最好在 descriptor 中将它们设置为 true

可以设置属性为: 只读 不可枚举 不可配置

Object.defineProperties

1
2
3
4
5
Object.defineProperties(user, {
name: { value: 'John', writable: false },
surname: { value: 'Smith', writable: false }
// ...
})

设定一个全局的密封对象

还有针对它们的测试:

  • Object.isExtensible(obj)

    如果添加属性被禁止,则返回 false,否则返回 true

  • Object.isSealed(obj)

    如果添加/删除属性被禁止,并且所有现有的属性都具有 configurable: false则返回 true

  • Object.isFrozen(obj)

    如果添加/删除/更改属性被禁止,并且所有当前属性都是 configurable: false, writable: false,则返回 true

对象属性配置

getter 和 setter

有两种类型的对象属性。

第一种是 数据属性。我们已经知道如何使用它们了。到目前为止,我们使用过的所有属性都是数据属性。

第二种类型的属性是新东西。它是 访问器属性(accessor property)。它们本质上是用于获取和设置值的函数,但从外部代码来看就像常规属性。

访问器属性由 “getter” 和 “setter” 方法表示。在对象字面量中,它们用 getset 表示:

1
2
3
4
5
6
7
8
9
let obj = {
get propName() {
// 当读取 obj.propName 时,getter 起作用
},

set propName(value) {
// 当执行 obj.propName = value 操作时,setter 起作用
}
}

当读取 obj.propName 时,getter 起作用,当 obj.propName 被赋值时,setter 起作用。

例如,我们有一个具有 namesurname 属性的对象 user

1
2
3
4
5
6
7
8
9
10
let user = {
name: 'yue',
surname: 'yun',

get fullName() {
return `${this.name} ${this.surname}`
}
}

console.log(user.fullName) // yue yun

从外表看,访问器属性看起来就像一个普通属性。这就是访问器属性的设计思想。我们不以函数的方式 调用 user.fullName,我们正常 读取 它:getter 在幕后运行。

让我们通过为 user.fullName 添加一个 setter 来修改它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let user = {
name: 'yue',
surname: 'yun',

get fullName() {
return `${this.name} ${this.surname}`
},

set fullName(value) {
;[this.name, this.surname] = value.split(' ')
}
}

// set fullName 将以给定值执行
user.fullName = 'me gumi'

alert(user.name) // me
alert(user.surname) // gumi

访问器描述符

访问器属性的描述符与数据属性的不同。

对于访问器属性,没有 valuewritable,但是有 getset 函数。

所以访问器描述符可能有:

  • get —— 一个没有参数的函数,在读取属性时工作,
  • set —— 带有一个参数的函数,当属性被设置时调用,
  • enumerable —— 与数据属性的相同,
  • configurable —— 与数据属性的相同。

例如上面的例子使用defineProperty创建一个fullName访问器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let user = {
name: 'yue',
surname: 'yun'
}

Object.defineProperty(user, 'fullName', {
get() {
return `${this.name} ${this.surname}`
},

set(value) {
;[this.name, this.surname] = value.split(' ')
}
})

alert(user.fullName) // yue yun

for (let key in user) alert(key) // yue yun

请注意,一个属性要么是访问器(具有 get/set 方法),要么是数据属性(具有 value),但不能两者都是。

如果我们试图在同一个描述符中同时提供 getvalue,则会出现错误:

1
2
3
4
5
6
7
8
// Error: Invalid property descriptor.
Object.defineProperty({}, 'prop', {
get() {
return 1
},

value: 2
})

原型

在编程中我们经常会想获取并扩展一些东西

比如我们有一个user对象及其属性和方法,并希望将 adminguest 作为基于 user 稍加修改的变体。我们想重用 user 中的内容,而不是复制/重新实现它的方法,而只是在其之上构建一个新的对象。

原型继承(Prototypal inheritance) 这个语言特性能够帮助我们实现这一需求。

Prototype

在 JavaScript 中 对象有特殊的隐藏属性[[Prototype]] 他们要么是null 要么就是在另一个对象中的引用 该对象称之为原型

我们会从Object逐步的向上寻找 即原型继承属性 [[Prototype]] 是内部的而且是隐藏的,但是这儿有很多设置它的方式。其中之一就是使用特殊的名字 __proto__,就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
let animal = {
eats: true
}
let rabbit = {
jumps: true
}

rabbit.__proto__ = animal

// 现在这两个属性我们都能在 rabbit 中找到:
console.log(rabbit.eats) // true
console.log(rabbit.jumps) // true

原型链可能会很长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let animal = {
eats: true,
walk() {
console.log('Animal walk')
}
}
let rabbit = {
jumps: true,
__proto__: animal
}
let longEar = {
earLength: 10,
__proto__: rabbit
}
// walk 是通过原型链获得的
longEar.walk() // Animal walk
alert(longEar.jumps) // true(从 rabbit)

现在,如果我们从 longEar 中读取一些它不存在的内容,JavaScript 会先在 rabbit 中查找,然后在 animal 中查找。

这里只有两个限制:

  1. 引用不能形成闭环。如果我们试图给 __proto__ 赋值但会导致引用形成闭环时,JavaScript 会抛出错误。
  2. __proto__ 的值可以是对象,也可以是 null。而其他的类型都会被忽略。

当然,这可能很显而易见,但是仍然要强调:只能有一个 [[Prototype]]。一个对象不能从其他两个对象获得继承。

注意: __proto__[[Prototype]] 的因历史原因而留下来的 getter/setter
__proto__与内部的[[Prototype]]不一样 __proto__[[Prototype]] 的 getter/setter。__proto__ 属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数 Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__ 去 get/set 原型

原型仅用于读取属性上,赋值操作是由setter函数去处理而 因此写入类属性实际上就是与调用函数相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let user = {
name: 'John',
surname: 'Smith',

set fullName(value) {
;[this.name, this.surname] = value.split(' ')
},

get fullName() {
return `${this.name} ${this.surname}`
}
}

let admin = {
__proto__: user,
isAdmin: true
}

console.log(admin.fullName) // John Smith (*)

// setter triggers!
admin.fullName = 'yue yun' // (**)

console.log(admin.fullName) // yue yun 的内容被修改了
console.log(user.fullName) // John Smith,user 的内容被保护了

Object.key(obj)只会遍历当前的属性的值

for in 会遍历当前的和继承的值 如果要判断是否是自己的可以使用obj.hasOwnPropetry

总结
  • 在 JavaScript 中,所有的对象都有一个隐藏的 [[Prototype]] 属性,它要么是另一个对象,要么就是 null
  • 我们可以使用 obj.__proto__ 访问它(历史遗留下来的 getter/setter,这儿还有其他方法,很快我们就会讲到)。
  • 通过 [[Prototype]] 引用的对象被称为“原型”。
  • 如果我们想要读取 obj 的一个属性或者调用一个方法,并且它不存在,那么 JavaScript 就会尝试在原型中查找它。
  • 写/删除操作直接在对象上进行,它们不使用原型(假设它是数据属性,不是 setter)。
  • 如果我们调用 obj.method(),而且 method 是从原型中获取的,this 仍然会引用 obj。因此,方法始终与当前对象一起使用,即使方法是继承的。
  • for..in 循环在其自身和继承的属性上进行迭代。所有其他的键/值获取方法仅对对象本身起作用。

F.prototype

我们可以通过new F()这样的构造函数来创建一个新对象

如果F.prototype是一个对象, new操作符会使用它作为新对象设置[[Prototype]]

这里的F.prototype指的是F的一个prototype的普通(常规)属性 如下

1
2
3
4
5
6
7
8
9
10
11
let animal = {
eats: true
}
function Rabbit(name) {
this.name = name
}

Rabbit.prototype = animal

let rabbit = new Rabbit('white Rabbit')
console.log(rabbit.eats)

设置 Rabbit.prototype = animal 的字面意思是:“当创建了一个 new Rabbit 时,把它的 [[Prototype]] 赋值为 animal”。

在上图中,"prototype" 是一个水平箭头,表示一个常规属性,[[Prototype]] 是垂直的,表示 rabbit 继承自 animal

F.prototype仅用在new F被调用时使用 它为新对象的[[Prototype]]赋值

默认的 F.prototype 构造器属性

每个函数都有 "prototype" 属性,即使我们没有提供它。默认的 "prototype" 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Rabbit() {}

/* 默认的 prototype
Rabbit.prototype = { constructor: Rabbit };
*/
function Rabbit() {}
// 默认:
// Rabbit.prototype = { constructor: Rabbit }

console.log(Rabbit.prototype.constructor == Rabbit) // true

function Rabbit() {}
// 默认:
// Rabbit.prototype = { constructor: Rabbit }

let rabbit = new Rabbit() // 继承自 {constructor: Rabbit}

console.log(rabbit.constructor == Rabbit) // true (from prototype)
  • F.prototype 属性(不要把它与 [[Prototype]] 弄混了)在 new F 被调用时为新对象的 [[Prototype]] 赋值。
  • F.prototype 的值要么是一个对象,要么就是 null:其他值都不起作用。
  • "prototype" 属性仅当设置在一个构造函数上,并通过 new 调用时,才具有这种特殊的影响。

原生的原型

prototype属性在 JavaScript 中广泛的使用 所有而内建构造函数都使用到了它

Object.prototype

1
2
let obj = {}
alert(obj) // "[object Object]"?

内建的toString生成了字符串[object object] obj = {}obj = new Object() 是一个意思,其中 Object 就是一个内建的对象构造函数,其自身的 prototype 指向一个带有 toString 和其他方法的一个巨大的对象。

1
2
3
4
5
let obj = {}

console.log(obj.__proto__ === Object.prototype) // true
console.log(obj.toString === obj.__proto__.toString) //true
console.log(obj.toString === Object.prototype.toString) //true

其他内建原型

例如: Array Date Function及其他,都在prototype上挂载了方法

当我们创建一个数组 [1, 2, 3],在内部会默认使用 new Array() 构造器。因此 Array.prototype 变成了这个数组的 prototype,并为这个数组提供数组的操作方法。这样内存的存储效率是很高的。所有的内建原型顶端都是 Object.prototype。这就是为什么有人说“一切都从对象继承而来”。

经过下面验证

1
2
3
4
5
6
7
8
9
10
let arr = [1, 2, 3]

// 它继承自 Array.prototype?
alert(arr.__proto__ === Array.prototype) // true

// 接下来继承自 Object.prototype?
alert(arr.__proto__.__proto__ === Object.prototype) // true

// 原型链的顶端为 null。
alert(arr.__proto__.__proto__.__proto__) // null

一些方法在原型上可能会发生重叠,例如,Array.prototype 有自己的 toString 方法来列举出来数组的所有元素并用逗号分隔每一个元素。

基本数据类型

最复杂的事情发生在字符串、数字和布尔值上。
正如我们记忆中的那样,它们并不是对象。但是如果我们试图访问它们的属性,那么临时包装器对象将会通过内建的构造器 StringNumberBoolean 被创建。它们提供给我们操作字符串、数字和布尔值的方法然后消失。
这些对象对我们来说是无形地创建出来的。大多数引擎都会对其进行优化,但是规范中描述的就是通过这种方式。这些对象的方法也驻留在它们的 prototype 中,可以通过 String.prototypeNumber.prototypeBoolean.prototype 进行获取。

nullundefined没有对象包装器.并且它们也没有相应的原型。

更改原生原型

在开发的过程中,我们可能会想要一些新的内建方法,并且想把它们添加到原生原型中。但这通常是一个很不好的想法。

原型是全局的,所以很容易造成冲突。如果有两个库都添加了 String.prototype.show 方法,那么其中的一个方法将被另一个覆盖。

所以,通常来说,修改原生原型被认为是一个很不好的想法。

class 语法

1
2
3
4
5
6
7
class MyClass {
constructor() { ... }
method1() { ... }
method2() { ... }
method3() { ... }
...
}

通过new MyClass()来创建具有上述方法的新对象

new 会自动调用 constructor() 方法,因此我们可以在 constructor() 中初始化对象。

1
2
3
4
5
6
7
8
9
class MyClass {
constructor(name, age) {
this.name = name
this.age = age
}
sayHello() {
console.log(`Hello, my name is ${this.name}`)
}
}

JavaScript中 类其实是一种函数 在ES6中增加了类而关键字是一种新的语法糖放其更加方便直观的创建想要的类

1
2
3
4
5
6
7
8
9
10
class User {
constructor(name) {
this.name = name
}
sayHi() {
console.log(`hello my name is ${this.name}`)
}
}

console.log(typeof User) // function

class User{...}构造实际是完成了下面的事

  • 创建一个名为User的函数 该函数为类声明的结果 该函数的代码来自于constructor方法
  • 存储类中的方法 例如User.prototype中的sayHi

如下面的代码解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class User {
constructor(name) {
this.name = name
}
sayHi() {
alert(this.name)
}
}

// class 是一个函数
console.log(typeof User) // function

// ...或者,更确切地说,是 constructor 方法
console.log(User === User.prototype.constructor) // true

// 方法在 User.prototype 中,例如:
console.log(User.prototype.sayHi) // sayHi 方法的代码

// 在原型中实际上有两个方法
consoe.log(Object.getOwnPropertyNames(User.prototype)) // constructor, sayHi

错误处理

try catch

通常我们在编写脚本的时候总是会遇到很多非预期的错误 导致脚本停止执行,有一种语法结构 try...catch,它使我们可以“捕获(catch)”错误,因此脚本可以执行更合理的操作,而不是死掉。

1
2
3
4
5
try {
// ...try to execute the code...
} catch (err) {
// ...handle errors...
}
  1. 首先执行try {...}中的代码
  2. 如果没有错误,那么就跳过catch(err)中的代码,继续执行,try中的代码执行完毕
  3. 如果出现错误,那么try中剩下的代码停止执行,控制台执行catch(err)中的代码,catch中的代码将包含一个error 的对象执行完毕

Error对象

发生错误时,JavaScript 会生成一个包含有关此 error 详细信息的对象。然后将该对象作为参数传递给 catch

1
2
3
4
5
try {
// ...
} catch (err) { // <-- “error 对象”,也可以用其他参数名代替 err
// ...
}
  • name: Error的名称 例如一个未定义的变量则报错是ReferenceError
  • message: 关于error的详细文字描述
  • stack: 当前的单调栈 用于调试

抛出自定义的Error

throw操作符

throw操作符会生成一个error对象

1
throw <error object>

技术上讲,我们可以将任何东西用作 error 对象。甚至可以是一个原始类型数据,例如数字或字符串,但最好使用对象,最好使用具有 namemessage 属性的对象(某种程度上保持与内建 error 的兼容性)。JavaScript 中有很多内建的标准 error 的构造器:ErrorSyntaxErrorReferenceErrorTypeError 等。我们也可以使用它们来创建 error 对象。

1
2
3
4
5
let error = new Error(message);
// 或
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...

再次抛出(Rethrowing)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let json = '{ "age": 30 }'; // 不完整的数据
try {
let user = JSON.parse(json);
if (!user.name) {
throw new SyntaxError("数据不全:没有 name");
}
blabla(); // 预料之外的 error
console.log( user.name );
} catch (err) {
if (err instanceof SyntaxError) {
console.log( "JSON Error: " + err.message );
} else {
throw err; // 再次抛出 (*)
}
}

try…catch…finally

try...catch 结构可能还有一个代码子句(clause):finally

1
2
3
4
5
6
7
try {
... 尝试执行的代码 ...
} catch (err) {
... 处理 error ...
} finally {
... 总是会执行的代码 ...
}

自定义Error,扩展Error

当我们开发项目时,经常需要我们自己定义error类来反映任务中可能出错的特定任务, 对于网络操作中的error 我们需要HttpError 对于数据库操作中的error 我们需要DbError,对于搜索操作的error 我们需要NotFoundError

我们自定义的 error 应该支持基本的 error 的属性,例如 messagename,并且最好还有 stack。但是它们也可能会有其他属于它们自己的属性,例如,HttpError 对象可能会有一个 statusCode 属性,属性值可能为 404403500 等。

扩展Error

如果我们需要使用json去检查是否存在某个数据我们现在规定成ValidationError

Error类是内建的 结构类似如下

1
2
3
4
5
6
7
8
// JavaScript 自身定义的内建的 Error 类的“伪代码”
class Error {
constructor (message) {
this.message = message
this.name = "Error" // (不同的内建error类拥有不同的名字)
this.stack = <call stack>
}
}

现在让我们从其中继承 ValidationError,并尝试进行运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ValidationError extends Error {
constructor (message) {
super(message)
this.name = "ValidationError"
}
}
function test() {
throw new ValidationError("Explosion!");
}

try {
test();
} catch(err) {
console.log(err.message); // Explosion!
console.log(err.name); // ValidationError
console.log(err.stack); // 一个嵌套调用的列表,每个调用都有对应的行号
}

深入继承

ValidationError类是非常通用的 很多东西都可能出错 对象的属性可能缺失或者属性可能有格式错误,让我们针对缺少属性的错误来制作一个更具体的 PropertyRequiredError 类。它将携带有关缺少的属性的相关信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class ValidationError extends Error {
constructor(message) {
super(message)
this.name = "ValidationError"
}
}

class PropertyRequiredError extends ValidationError {
constructor(property) {
super("No property:" + property)
this.name = "PropertyRequiredError"
this.property = property
}
}

function readUser(json) {
const user = JSON.parse(json)
if(!user.age) {
throw new PropertyRequiredError("age")
}
if(!user.name) {
throw new PropertyRequiredError("name")
}
return user
}

try {
let user = readUser('{"age": 25}')
} catch (err) {
if (err instanceof ValidationError) {
console.log("Invalid data" + err.message)
console.log(err.name)
consoe.log(err.property)
} else if(err instanceof SyntaxError) {
console.log("JSON Syntax Error:" + err.message)
} else {
throw err
}
}

这个新的类 PropertyRequiredError 使用起来很简单:我们只需要传递属性名:new PropertyRequiredError(property)。人类可读的 message 是由 constructor 生成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}

class ValidationError extends MyError { }

class PropertyRequiredError extends ValidationError {
constructor(property) {
super("No property: " + property);
this.property = property;
}
}

// name 是对的
console.log( new PropertyRequiredError("field").name ); // PropertyRequiredError

迭代器

generator

常规函数只会返回一个单一值 (或者不返回任何值)

而 generator 可以按需一个接一个地返回yield多个值。它们可与iterable完美配合使用,从而可以轻松地创建数据流。

generator函数

要创建一个generator 我们需要一个特殊语法结构: function * 即所谓的generator function

1
2
3
4
5
function* generateSequence () {
yield 1
yield 2
return 3
}

generator函数与常规函数的行为不同 在此类函数被调用时 他不会运行其代码.而是返回一个被称为 “generator object” 的特殊对象,来管理执行流程。

1
2
3
4
5
6
7
8
function* generateSequence(){
yield 1
yield 2
return 3
}
// "generator function" 创建了一个 "generator object"
let generator = generateSequence()
console.log(generator) //Object [Generator] {}

到现在为止 上段代码中的函数体代码并没有开始执行
一个 generator 的主要方法就是 next()。当被调用时,它会恢复运行,执行直到最近的 yield <value> 语句(value 可以被省略,默认为 undefined)。然后函数执行暂停,并将产出的(yielded)值返回到外部代码。

next()的结果始终是一个具有两个属性的对象:

  • value: 产出的(yielded)的值
  • done: 如果generator函数已执行完成则为true否则为false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function* generateSequence() {
yield 1
yield 2
return 3
}
// "generator function" 创建了一个 "generator object"
let generator = generateSequence()
console.log(generator.next())
console.log(generator.next())
console.log(generator.next())
console.log(generator.next())function* generateSequence() {
yield 1
yield 2
return 3
}
// "generator function" 创建了一个 "generator object"
let generator = generateSequence()
console.log(generator.next()) //{ value: 1, done: false }
console.log(generator.next()) //{ value: 2, done: false }
console.log(generator.next()) //{ value: 3, done: true }
console.log(generator.next()) //{ value: undefined, done: true }

模块导入导出

异步

回调

简介: 回调

JavaScript环境提供了许多内置的函数 这些函数允许执行异步行为,我们现在开始执行的行为,但它们会在稍后完成。

比如经常用的setTimeout函数就是这样的函数 比如函数loadScript(src)

1
2
3
4
5
function loadScript (src) {
let script = document.createElement('script')
script.src = src
document.head.append(script)
}

它将一个新的、带有给定 src 的、动态创建的标签 <script src="…"> 插入到文档中。浏览器将自动开始加载它,并在加载完成后执行它。

脚本是异步调用的 因为它从现在开始加载 在加载函数执行完成后才运行
如果在 loadScript(…) 下面有任何其他代码,它们不会等到脚本加载完成才执行。

如果现在我们有一个需求是脚本加载后立即使用它 它声明了一个新函数 我们想运行它 如果在loadSrript(...) 后立刻运行则会报错

1
2
loadScript('/my/script.js'); // 这个脚本有 "function newFunction() {…}"
newFunction(); // 没有这个函数!

自然情况下 浏览器没有时间加载脚本为了解决这个办法我们可以添加一个callback 函数作为loadScript的第二个参数 该函数应用在脚本加载完成时执行

1
2
3
4
5
6
function loadScript(src,callback){
let script = document.createElement('script')
script.src = src
script.onload = () => callback(script)
document.head.append(srcipt)
}

比如下面的案例

1
2
3
4
5
6
7
8
9
10
11
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}

loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
alert(`脚本 ${script.src} 加载完成`);
alert( _ ); // _ 是所加载的脚本中声明的一个函数
});

这被称为“基于回调”的异步编程风格。异步执行某项功能的函数应该提供一个 callback 参数用于在相应事件完成时调用。

回调中的回调

我们如何依次加载两个脚本:第一个,然后是第二个?

自然的解决方案是将第二个 loadScript 调用放入回调中,如下所示:

1
2
3
4
5
6
loadScript('/my/script.js', function(script) {
alert(`脚本 ${script.src} 加载完成,让我们继续加载另一个吧`);
loadScript('/my/script2.js', function(script) {
alert(`第二个脚本加载完成`);
});
});

因此,每一个新行为(action)都在回调内部。这对于几个行为来说还好,但对于许多行为来说就不好了,所以我们很快就会看到其他变体。

处理Error

我们并没有考虑出现 error 的情况。如果脚本加载失败怎么办?我们的回调应该能够对此作出反应。这是 loadScript 的改进版本,可以跟踪加载错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;

script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));

document.head.append(script);
}

loadScript('/my/script.js', function(error, script) {
if (error) {
// 处理 error
} else {
// 脚本加载成功
}
})

loadScript 中所使用的方案其实很普遍。它被称为“Error 优先回调(error-first callback)”风格。

  • callback 的第一个参数是为 error 而保留的。一旦出现 error,callback(err) 就会被调用。
  • 第二个参数(和下一个参数,如果需要的话)用于成功的结果。此时 callback(null, result1, result2…) 就会被调用。

单一的 callback 函数可以同时具有报告 error 和传递返回结果的作用。

回调地狱

当有很复杂的逻辑的时候一个接一个的异步行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
loadScript('1.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...加载完所有脚本后继续 (*)
}
});

}
});
}
});

在上面这段代码中:

  1. 我们加载 1.js,如果没有发生错误。
  2. 我们加载 2.js,如果没有发生错误……
  3. 我们加载 3.js,如果没有发生错误 —— 做其他操作 (*)

随着调用嵌套的增加,代码层次变得更深,维护难度也随之增加,尤其是我们使用的是可能包含了很多循环和条件语句的真实代码,而不是例子中的 ...

避免金字塔 最好的方法之一就是promise

Promise

生产者: executor

Promise 对象的构造器(constructor) 语法如下

1
2
3
let promise	= new Promise(function (resolve,reject) {
// executor (生产者代码)
})

传递给new Promise的函数被称之为executornew Promise被创建 executor 会自动运行它包含最终应产出结果的生产者代码

它的参数 resolvereject 是由 JavaScript 自身提供的回调。我们的代码仅在 executor 的内部

当 executor 获得了结果,无论是早还是晚都没关系,它应该调用以下回调之一:

  • resolve(value) - 如果任务成功完成并带有结果value
  • reject(error) - 如果出现了error error即为error 对象

executor会自动运行并尝试执行一项工作 尝试结束后 如果成功则调用resolve 如果出现error则调用reject

new Promise构造器返回的promise对象具有以下内部属性

  • state – 最初是pending 然后在resolve被调用时变为fulfilled 或者在reject被调用时变成rejected
  • result 最初是undefined 然后在resovle(value)被调用时变为value 或者在reject(error)被调用时变成error

所以,executor 最终将 promise 移至以下状态之一

下面是一个 promise 构造器和一个简单的 executor 函数,该 executor 函数具有包含时间(即 setTimeout)的“生产者代码”:

1
2
3
4
5
let promise = new Promise(function(resolve,reject) {
// 当 promise 被构造完成时,自动执行此函数
// 1 秒后发出工作已经被完成的信号,并带有结果 "done"
setTimeout(()=>resolve("done"),1000)
})
  • executor 被自动且立即调用 (通过new Promise)
  • executor 接受两个参数: resolvereject这些函数由JavaScript引擎预先定义的函数

经过 1 秒的“处理”后,executor 调用 resolve("done") 来产生结果。这将改变 promise 对象的状态:

下面则是一个 executor 以 error 拒绝 promise 的示例:

1
2
3
4
let promise = new Promise(function(resolve, reject) {
// 1 秒后发出工作已经被完成的信号,并带有 error
setTimeout(() => reject(new Error("Whoops!")), 1000);
})

reject(...) 的调用将 promise 对象的状态移至 "rejected"

总而言之,executor 应该执行一项工作(通常是需要花费一些时间的事儿),然后调用 resolvereject 来改变对应的 promise 对象的状态。

与最初的 “pending” promise 相反,一个 resolved 或 rejected 的 promise 都会被称为 “settled”。

executor 只能调用一个resolve或一个reject任何状态的更改都是最终的 所有其他的再对 resolvereject 的调用都会被忽略

1
2
3
4
5
6
7
let promise = new Promise(function(resolve, reject) {
resolve("done");

reject(new Error("…")); // 被忽略
setTimeout(() => resolve("…")); // 被忽略
});

这的宗旨是,一个被 executor 完成的工作只能有一个结果或一个 error。

并且,resolve/reject 只需要一个参数(或不包含任何参数),并且将忽略额外的参数

消费者: then catch

then

语法如下

1
2
3
4
promise.then(
function(result) { /* handle a successful result */ },
function(error) { /* handle an error */ }
);

.then 的第一个参数是一个函数,该函数将在 promise resolved 且接收到结果后执行。

.then 的第二个参数也是一个函数,该函数将在 promise rejected 且接收到 error 信息后执行。

如果我们只对成功完成的情况感兴趣,那么我们可以只为 .then 提供一个函数参数:

1
2
3
4
let promise = new Promise(resolve => {
setTimeout(() => resolve("done!"), 1000);
});
promise.then(alert); // 1 秒后显示 "done!"

catche

如果我们只对 error 感兴趣,那么我们可以使用 null 作为第一个参数:.then(null, errorHandlingFunction)。或者我们也可以使用 .catch(errorHandlingFunction),其实是一样的

1
2
3
4
5
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
// .catch(f) 与 promise.then(null, f) 一样
promise.catch(alert); // 1 秒后显示 "Error: Whoops!"

.catch(f) 调用是 .then(null, f) 的完全的模拟,它只是一个简写形式。

清理: finally

就像常规 try {...} catch {...} 中的 finally 子句一样,promise 中也有 finally

调用 .finally(f) 类似于 .then(f, f),因为当 promise settled 时 f 就会执行:无论 promise 被 resolve 还是 reject。

finally 的功能是设置一个处理程序在前面的操作完成后,执行清理/终结。

例如,停止加载指示器,关闭不再需要的连接等。

1
2
3
4
5
6
7
new Promise((resolve, reject) => {
/* 做一些需要时间的事,之后调用可能会 resolve 也可能会 reject */
})
// 在 promise 为 settled 时运行,无论成功与否
.finally(() => stop loading indicator)
// 所以,加载指示器(loading indicator)始终会在我们继续之前停止
.then(result => show result, err => show error)

示例: loadScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function loadScript(src) {
return new Promise((resolve,reject)=>{
let script = document.createElement('script')
script.src = src
script.onload=()=>resolve(script)
script.onerror=()=>reject(new Error(`script load error`))
document.head.append(script)
})
}
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js");
promise.then(
script => alert(`${script.src} is loaded!`),
error => alert(`Error: ${error.message}`)
);
promise.then(script => alert('Another handler...'));

Promise链

promise链如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
new Promise(function(resolve, reject) {

setTimeout(() => resolve(1), 1000); // (*)

}).then(function(result) { // (**)

alert(result); // 1
return result * 2;

}).then(function(result) { // (***)

alert(result); // 2
return result * 2;

}).then(function(result) {
alert(result); // 4
return result * 2;
});

通过.then处理程序(handler)链进行传递result

运行流程如下:

  1. 初始 promise 在 1 秒后 resolve (*)
  2. 然后 .then 处理程序被调用 (**),它又创建了一个新的 promise(以 2 作为值 resolve)。
  3. 下一个 then (***) 得到了前一个 then 的值,对该值进行处理(*2)并将其传递给下一个处理程序。
  4. ……依此类推。

随着 result 在处理程序链中传递,我们可以看到一系列的 alert 调用:124

这样之所以是可行的,是因为每个对 .then 的调用都会返回了一个新的 promise,因此我们可以在其之上调用下一个 .then

返回Promise

.then(handler) 中所使用的处理程序(handler)可以创建并返回一个 promise。

在这种情况下,其他的处理程序将等待它 settled 后再获得其结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
loadScript("/article/promise-chaining/one.js")
.then(function(script) {
return loadScript("/article/promise-chaining/two.js");
})
.then(function(script) {
return loadScript("/article/promise-chaining/three.js");
})
.then(function(script) {
// 使用在脚本中声明的函数
// 以证明脚本确实被加载完成了
one();
two();
three();
});

示例: fetch

在前端编程中,promise 通常被用于网络请求 使用fetch方法

1
2
3
4
5
6
7
8
9
10
11
fetch('/article/promise-chaining/user.json')
// 当远程服务器响应时,下面的 .then 开始执行
.then(function(response) {
// 当 user.json 加载完成时,response.text() 会返回一个新的 promise
// 该 promise 以加载的 user.json 为 result 进行 resolve
return response.text();
})
.then(function(text) {
// ……这是远程文件的内容
alert(text); // {"name": "iliakan", "isAdmin": true}
});

例如,我们可以再向 GitHub 发送一个请求,加载用户个人资料并显示头像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function loadJson(url) {
return fetch(url)
.then(response => response.json());
}

function loadGithubUser(name) {
return loadJson(`https://api.github.com/users/${name}`);
}

function showAvatar(githubUser) {
return new Promise(function(resolve, reject) {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);

setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
});
}

// 使用它们:
loadJson('/article/promise-chaining/user.json')
.then(user => loadGithubUser(user.name))
.then(showAvatar)
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
// ...

如果 .then(或 catch/finally 都可以)处理程序返回一个 promise,那么链的其余部分将会等待,直到它状态变为 settled。当它被 settled 后,其 result(或 error)将被进一步传递下去。

Promise API

Promise.all

我们希望并行执行多个 promise, 并等待所有 promise 都准备就绪

例如,并行下载几个 URL,并等到所有内容都下载完毕后再对它们进行处理。

1
2
3
4
5
Promise.all([
new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
]).then(alert); // 1,2,3 当上面这些 promise 准备好时:每个 promise 都贡献了数组中的一个元素

数组中的元素顺序与源promise中的顺序相同 即使第一个 promise 花费了最长的时间才 resolve,但它仍是结果数组中的第一个。

如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let names = ['iliakan', 'remy', 'jeresig'];

let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));

Promise.all(requests)
.then(responses => {
// 所有响应都被成功 resolved
for(let response of responses) {
alert(`${response.url}: ${response.status}`); // 对应每个 url 都显示 200
}

return responses;
})
// 将响应数组映射(map)到 response.json() 数组中以读取它们的内容
.then(responses => Promise.all(responses.map(r => r.json())))
// 所有 JSON 结果都被解析:"users" 是它们的数组
.then(users => users.forEach(user => alert(user.name)));

如果任意一个 promise 被 reject,由 Promise.all 返回的 promise 就会立即 reject,并且带有的就是这个 error。

如果出现 error,其他 promise 将被忽略

Promise.all(iterable) 允许在 iterable 中使用非 promise 的“常规”值

Promise.allSettled

这是一个最近添加到 JavaScript 的特性。 旧式浏览器可能需要 polyfills.

如果任意的 promise reject,则 Promise.all 整个将会 reject。当我们需要 所有 结果都成功时,它对这种“全有或全无”的情况很有用:

1
2
3
4
5
Promise.all([
fetch('/template.html'),
fetch('/style.css'),
fetch('/data.json')
]).then(render); // render 方法需要所有 fetch 的数据

Promise.allSettled 等待所有的 promise 都被 settle,无论结果如何。结果数组具有:

  • {status:"fulfilled", value:result} 对于成功的响应,
  • {status:"rejected", reason:error} 对于 error。

例如,我们想要获取(fetch)多个用户的信息。即使其中一个请求失败,我们仍然对其他的感兴趣。

让我们使用 Promise.allSettled

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://no-such-url'
];

Promise.allSettled(urls.map(url => fetch(url)))
.then(results => { // (*)
results.forEach((result, num) => {
if (result.status == "fulfilled") {
alert(`${urls[num]}: ${result.value.status}`);
}
if (result.status == "rejected") {
alert(`${urls[num]}: ${result.reason}`);
}
});
});

/*[
{status: 'fulfilled', value: ...response...},
{status: 'fulfilled', value: ...response...},
{status: 'rejected', reason: ...error object...}
]*/

Promise.race

Promise.all 类似,但只等待第一个 settled 的 promise 并获取其结果(或 error)。

1
2
3
4
5
Promise.race([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1

这里第一个 promise 最快,所以它变成了结果。第一个 settled 的 promise “赢得了比赛”之后,所有进一步的 result/error 都会被忽略。

Promise.any

Promise.race 类似,区别在于 Promise.any 只等待第一个 fulfilled 的 promise,并将这个 fulfilled 的 promise 返回。如果给出的 promise 都 rejected,那么返回的 promise 会带有 AggregateError —— 一个特殊的 error 对象,在其 errors 属性中存储着所有 promise error。

Promise.resolve/reject

  • Promise.resolve(value) —— 使用给定 value 创建一个 resolved 的 promise。
  • Promise.reject(error) —— 使用给定 error 创建一个 rejected 的 promise。

微任务(Microtask) – jobs

单线程JavaScript 基于事件循环 非阻塞IO型

事件循环中使用一个事件队列,在每个时间点上,系统只会处理一个事件,即使电脑有多个CPU核心,也无法同时并行的处理多个事件。因此,node.js在I/O型的应用中,给每一个输入输出定义一个回调函数,node.js会自动将其加入到事件轮询的处理队列里,当I/O操作完成后,这个回调函数会被触发,系统会继续处理其他的请求。

异步任务的回调会依次进入micro task queue,等待后续被调用,

  • process.nextTick (Node独有)
  • Promise
  • Object.observe
  • MutationObserver

宏任务(Macrotask) – tasks

异步任务的回调会依次进入macro task queue 等待后续被调用

  • setTimeout
  • setInterval
  • setImmediate (Node)
  • requestAnimationFrame (浏览器)
  • I/O
  • UI rendering

async/await

async 和 await 实际上就是语法糖 对于 Promise.resolve() 的语法糖 比如下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async function async1() {
console.log("async1 start")
await async2()
console.log('async1 end');
}

// 实际等于的是
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}
// 如果用promise就是
async function async1() {
console.log('async1 start');
new Promise((resolve,reject)=>{
resolve(async2())
}).then((res)=>{
console.log('async1 end');
})
}

输出顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function fn(){
console.log(1);

setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});

new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);

Promise.resolve().then(() => {
console.log(6)
}).then(() => {
console.log(7)

setTimeout(() => {
console.log(8)
}, 0);
});
})

setTimeout(() => {
console.log(9);
})

console.log(10);
}
fn();


答案
1
4
10
5
6
7
2
3
9
8

手写Promise A+

浏览器中的 JS

NodeJS

二进制数据、文件

ArrayBuffer、二进制数值

在web开发中 处理文件时(upload、download、create)时 经常会遇到二进制数据 处理头像的时候可能也会需要处理二进制数据 但是在JavaScript中有很多二进制数据的格式 比如

  • ArrayBuffer Uint8Array DataView Blob File 等等…

基本的二进制对象是 ArrayBuffer —— 对固定长度的连续内存空间的引用。

1
2
3
4
5
6
7
8
let buffer = new ArrayBuffer(16)
console.log(buffer,typeof buffer)
/*
ArrayBuffer {
[Uint8Contents]: <00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>,
byteLength: 16
} object
*/

它会分配一个 16 字节的连续内存空间,并用 0 进行预填充

ArrayBuffer 并不是数组

  • 长度固定 无法增加和减少它的长度
  • 正好占用了内存中的那么多的空间
  • 要访问单个字节,需要另一个“视图”对象,而不是 buffer[index]

ArrayBuffer 是一个内存区域 他里面储存了什么我们无法判断 只是一个原始的字节序列

如果需要去操作ArrayBuffer 我们需要去使用“试图”对象

试图对象本身并不存储任何东西 只是给我们提供一个操作的媒介去解释储存在ArrayBuffer中的字节 比如

  • Uint8Array : 将ArrayBuffer中的每个字节视为0到255之间的单个数字(每个字节是8位) 即8 位无符号整数
  • Unit16Array:将每 2 个字节视为一个 0 到 65535 之间的整数。这称为 “16 位无符号整数”
  • Uint32Array —— 将每 4 个字节视为一个 0 到 4294967295 之间的整数。这称为 “32 位无符号整数”。
  • Float64Array —— 将每 8 个字节视为一个 5.0x10-3241.8x10308 之间的浮点数。

因此,一个 16 字节 ArrayBuffer 中的二进制数据可以解释为 16 个“小数字”,或 8 个更大的数字(每个数字 2 个字节),或 4 个更大的数字(每个数字 4 个字节),或 2 个高精度的浮点数(每个数字 8 个字节)。

Array是核心对象是所有的基础,是原始的二进制数据。

1
2
3
4
5
6
7
8
9
10
11
let buffer = new ArrayBuffer(16)
let view = new Uint32Array(buffer)
console.log(view, typeof view) // Uint32Array(4) [ 0, 0, 0, 0 ] object
console.log(view.length) // 4位整数
console.log(view.byteLength) // 16 字节
view[0] = 123456

// 遍历值
for (let num of view) {
console.log(num) // 123456,然后 0,0,0(一共 4 个值)
}

TypedArray

所有这些视图(Uint8ArrayUint32Array 等)的通用术语是 TypedArray。它们共享同一方法和属性集。

请注意,没有名为 TypedArray 的构造器,它只是表示 ArrayBuffer 上的视图之一的通用总称术语:Int8ArrayUint8Array 及其他

1
2
3
4
5
new TypedArray(buffer, [byteOffset], [length]);
new TypedArray(object);
new TypedArray(typedArray);
new TypedArray(length);
new TypedArray();
  1. 如果给定的是 ArrayBuffer 参数,则会在其上创建视图。我们已经用过该语法了。

    可选,我们可以给定起始位置 byteOffset(默认为 0)以及 length(默认至 buffer 的末尾),这样视图将仅涵盖 buffer 的一部分。(ArryaBuffer 即是 buffer)

  2. 如果给定的是 Array,或任何类数组对象,则会创建一个相同长度的类型化数组,并复制其内容。

    我们可以使用它来预填充数组的数据:

    1
    2
    3
    let arr = new Uint8Array([0, 1, 2, 3]);
    alert( arr.length ); // 4,创建了相同长度的二进制数组
    alert( arr[1] ); // 1,用给定值填充了 4 个字节(无符号 8 位整数)
  3. 如果给定的是另一个 TypedArray,也是如此:创建一个相同长度的类型化数组,并复制其内容。如果需要的话,数据在此过程中会被转换为新的类型。

    1
    2
    3
    4
    let arr16 = new Uint16Array([1, 1000]);
    let arr8 = new Uint8Array(arr16);
    alert( arr8[0] ); // 1
    alert( arr8[1] ); // 232,试图复制 1000,但无法将 1000 放进 8 位字节中

如要访问底层的 ArrayBuffer,那么在 TypedArray 中有如下的属性:

  • arr.buffer —— 引用 ArrayBuffer
  • arr.byteLength —— ArrayBuffer 的长度。
1
2
3
let arr8 = new Uint8Array([0, 1, 2, 3]);
// 同一数据的另一个视图
let arr16 = new Uint16Array(arr8.buffer);

越界行为

如果我们尝试将越界值写入类型化数组会出现什么情况?不会报错。但是多余的位被切除。

例如,我们尝试将 256 放入 Uint8Array。256 的二进制格式是 100000000(9 位),但 Uint8Array 每个值只有 8 位,因此可用范围为 0 到 255。

对于更大的数字,仅存储最右边的(低位有效)8 位,其余部分被切除:因此结果是 0。

即是 该数字对 28 取模的结果被保存了下来。

1
2
3
4
5
6
7
8
9
10
let uint8array = new Uint8Array(16);

let num = 256;
console.log(num.toString(2)); // 100000000(二进制表示)

uint8array[0] = 256;
uint8array[1] = 257;

console.log(uint8array[0]); // 0
console.log(uint8array[1]); // 1

TypedArray 方法

TypedArray 具有常规的 Array 方法,但有个明显的例外。

我们可以遍历(iterate),mapslicefindreduce 等。

但有几件事我们做不了:

  • 没有 splice —— 我们无法“删除”一个值,因为类型化数组是缓冲区(buffer)上的视图,并且缓冲区(buffer)是固定的、连续的内存区域。我们所能做的就是分配一个零值。
  • concat 方法。

还有两种其他方法:

  • arr.set(fromArr, [offset])offset(默认为 0)开始,将 fromArr 中的所有元素复制到 arr
  • arr.subarray([begin, end]) 创建一个从 beginend(不包括)相同类型的新视图。这类似于 slice 方法(同样也支持),但不复制任何内容 —— 只是创建一个新视图,以对给定片段的数据进行操作。

有了这些方法,我们可以复制、混合类型化数组,从现有数组创建新数组等。

DataView

DataView 是在 ArrayBuffer 上的一种特殊的超灵活“未类型化”视图。它允许以任何格式访问任何偏移量(offset)的数据。

  • 对于类型化的数组,构造器决定了其格式。整个数组应该是统一的。第 i 个数字是 arr[i]
  • 通过 DataView,我们可以使用 .getUint8(i).getUint16(i) 之类的方法访问数据。我们在调用方法时选择格式,而不是在构造的时候。
1
new DataView(buffer, [byteOffset], [byteLength])
  • buffer —— 底层的 ArrayBuffer。与类型化数组不同,DataView 不会自行创建缓冲区(buffer)。我们需要事先准备好。
  • byteOffset —— 视图的起始字节位置(默认为 0)。
  • byteLength —— 视图的字节长度(默认至 buffer 的末尾)。

ArrayBuffer 是核心对象,是对固定长度的连续内存区域的引用。

在大多数情况下,我们直接对类型化数组进行创建和操作,而将 ArrayBuffer 作为“共同之处(common denominator)”隐藏起来。我们可以通过 .buffer 来访问它,并在需要时创建另一个视图。

TextDecoder和TextEncoder

TextDecoder

二进制实际上是一个字符串该怎么表示呢?

内建的 TextDecoder 对象在给定缓冲区(buffer)和编码格式(encoding)的情况下,允许将值读取为实际的 JavaScript 字符串。

1
let decoder = new TextDecoder([label],[options])
  • label —— 编码格式,默认为 utf-8,但同时也支持 big5windows-1251 等许多其他编码格式。
  • options —— 可选对象:**fatal** ignoreBOM

解码

1
let str = decoder.decode([input], [options]);
  • input —— 要被解码的 BufferSource
  • options —— 可选对象:**stream** —— 对于解码流
1
2
3
4
let uint8Array = new Uint8Array([72, 101, 108, 108, 111])
console.log(new TextDecoder().decode(uint8Array)) // hello
uint8Array = new Uint8Array([228, 189, 160, 229, 165, 189])
console.log(new TextDecoder().decode(uint8Array)) // 你好

TextEncoder

TextEncoder 做相反的事情 —— 将字符串转换为字节。

1
let encoder = new TextEncoder();

只支持 utf-8 编码。

它有两种方法:

  • encode(str) —— 从字符串返回 Uint8Array
  • encodeInto(str, destination) —— 将 str 编码到 destination 中,该目标必须为 Uint8Array
1
2
3
let encoder = new TextEncoder();
let uint8Array = encoder.encode("Hello");
alert(uint8Array); // 72,101,108,108,111

Blob

在浏览器中,还有其他更高级的对象,特别是 BlobFile API中有描述

Blob 由一个可选字符串type(通常是MIME类型) 和 blobParts组成 一系列的Blob对象、字符串、BufferSource

>

1
new Blob(blobParts,options)
  • blobParts: 是 Blob/BufferSource/String 类型的值的数组。
  • options: type: Blob 类型 endings 是否转换换行符 默认为 "transparent"(啥也不做),不过也可以是 "native"(转换)。

例如

1
2
3
let blob = new Blob(["<html>…</html>"], {type: 'text/html'});
let hello = new Uint8Array([72, 101, 108, 108, 111]); // 二进制格式的 "hello"
let blob = new Blob([hello, ' ', 'world'], {type: 'text/plain'});

我们可以用 slice 方法来提取 Blob 片段 (用法同 string.slice

Blob对象是不可改变的 无法从Blob中更改数据但是可以通过slice获取新的blob 就类似于 字符串

Blob–URL

1
2
3
4
5
6
7
8
let link = document.createElement('a');
link.download = 'hello.txt';

let blob = new Blob(['Hello, world!'], {type: 'text/plain'});
link.href = URL.createObjectURL(blob);
link.click();
URL.revokeObjectURL(link.href);
// link.href的值blob:https://javascript.info/1e67e00e-860d-40a5-89ae-6ab0cbee6273

浏览器内部为每个通过 URL.createObjectURL 生成的 URL 存储了一个 URL → Blob 映射。因此,此类 URL 很短,但可以访问 Blob 在内存中不会被消除 除非退出 该映射会被删除

Blob–base64

URL.createObjectURL 的一个替代方法是,将 Blob 转换为 base64-编码的字符串。

这种编码将二进制数据表示为一个由 0 到 64 的 ASCII 码组成的字符串,非常安全且“可读“。更重要的是 —— 我们可以在 “data-url” 中使用此编码。

1
2
3
4
5
6
7
8
9
10
11
12
let link = document.createElement('a');
link.download = 'hello.txt';
let blob = new Blob(['Hello, world!'], {type: 'text/plain'});
let reader = new FileReader();
reader.readAsDataURL(blob); // 将 Blob 转换为 base64 并调用 onload
reader.onload = function() {
link.href = reader.result; // data url
link.click();
};

// <img
// src = "data:image/png;base64,R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7">

Image–blob

我们可以创建一个图像(image)的、图像的一部分、或者甚至创建一个页面截图的 Blob。这样方便将其上传至其他地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 获取任何图像
let img = document.querySelector('img');

// 生成同尺寸的 <canvas>
let canvas = document.createElement('canvas');
canvas.width = img.clientWidth;
canvas.height = img.clientHeight;

let context = canvas.getContext('2d');

// 向其中复制图像(此方法允许剪裁图像)
context.drawImage(img, 0, 0);
// 我们 context.rotate(),并在 canvas 上做很多其他事情

// toBlob 是异步操作,结束后会调用 callback
canvas.toBlob(function(blob) {
// blob 创建完成,下载它
let link = document.createElement('a');
link.download = 'example.png';

link.href = URL.createObjectURL(blob);
link.click();

// 删除内部 blob 引用,这样浏览器可以从内存中将其清除
URL.revokeObjectURL(link.href);
}, 'image/png');

Blob–ArrayBuffer

1
2
3
4
// 从 blob 获取 arrayBuffer
const bufferPromise = await blob.arrayBuffer();
// 或
blob.arrayBuffer().then(buffer => /* 处理 ArrayBuffer */);

Blob–Stream

当我们读取和写入超过 2 GB 的 blob 时,将其转换为 arrayBuffer 的使用对我们来说会更加占用内存。这种情况下,我们可以直接将 blob 转换为 stream 进行处理。stream 是一种特殊的对象,我们可以从它那里逐部分地读取(或写入)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 从 blob 获取可读流(readableStream)
const readableStream = blob.stream();
const stream = readableStream.getReader();

while (true) {
// 对于每次迭代:value 是下一个 blob 数据片段
let { done, value } = await stream.read();
if (done) {
// 读取完毕,stream 里已经没有数据了
console.log('all blob processed.');
break;
}
// 对刚从 blob 中读取的数据片段做一些处理
console.log(value);

File和FileReader

File

File对象继承自Blob 并扩展了与文件系统相关的功能

1
new File(fileParts, fileName, [options])
  • fileParts —— Blob/BufferSource/String 类型值的数组。
  • fileName —— 文件名字符串。
  • options —— 可选对象:

<input type="file">或拖放或其他浏览器接口来获取文件。在这种情况下,file 将从操作系统(OS)获得 this 信息。

这就是我们从 <input type="file"> 中获取 File 对象的方式:

1
2
3
4
5
6
7
8
9
<input type="file" onchange="showFile(this)">

<script>
function showFile(input) {
let file = input.files[0];
console.log(`File name: ${file.name}`); // 例如 my.png
console.log(`Last modified: ${file.lastModified}`); // 例如 1552830408824
}
</script>

输入(input)可以选择多个文件,因此 input.files 是一个类数组对象。这里我们只有一个文件,所以我们只取 input.files[0]

FileReader

FileReader是一个对象 用处是从Blob对象中读取数据

1
let reader = new FileReader(); // 没有参数

主要方法:

  • readAsArrayBuffer(blob) —— 将数据读取为二进制格式的 ArrayBuffer
  • readAsText(blob, [encoding]) —— 将数据读取为给定编码(默认为 utf-8 编码)的文本字符串。
  • readAsDataURL(blob) —— 读取二进制数据,并将其编码为 base64 的 data url。
  • abort() —— 取消操作。

读取过程中,有以下事件:

  • loadstart —— 开始加载。
  • progress —— 在读取过程中出现。
  • load —— 读取完成,没有 error。
  • abort —— 调用了 abort()
  • error —— 出现 error。
  • loadend —— 读取完成,无论成功还是失败。

读取完成后,我们可以通过以下方式访问读取结果:

  • reader.result 是结果(如果成功)
  • reader.error 是 error(如果失败)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<input type="file" onchange="readFile(this)">

<script>
function readFile(input) {
let file = input.files[0];

let reader = new FileReader();

reader.readAsText(file);

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

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

}
</script>

V8垃圾回收机制和内存泄漏分析

V8

语言分类

解释执行

  • 先将源代码通过解析器转化成中间代码,再用解释器执行中间代码,输出结果
  • 启动快 执行慢

编译执行

  • 先将源代码通过解析器转成中间代码 再用编译器把中间代码转成机器码 最后执行
  • 启动慢 执行快

v8执行过程

  • V8采用的是解释和编译两种方式 这种混合方式称为JIT
  • 第一步先由解析器生成抽象语法树和相关的作用域
  • 第二步根据AST和作用域生成字节码 字节码是介于AST和机器码的中间代码
  • 然后解析器直接执行字节码 也可以让编译器把字节码编译成机器码后在执行

抽象语法树

astexplorer可以查看抽象语法树

1
2
3
const a = 1
const b = 2
const c = 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
{
"type": "Program",
"start": 0,
"end": 215,
"body": [
{
"type": "VariableDeclaration",
"start": 179,
"end": 190,
"declarations": [
{
"type": "VariableDeclarator",
"start": 185,
"end": 190,
"id": {
"type": "Identifier",
"start": 185,
"end": 186,
"name": "a"
},
"init": {
"type": "Literal",
"start": 189,
"end": 190,
"value": 1,
"raw": "1"
}
}
],
"kind": "const"
},
{
"type": "VariableDeclaration",
"start": 191,
"end": 202,
"declarations": [
{
"type": "VariableDeclarator",
"start": 197,
"end": 202,
"id": {
"type": "Identifier",
"start": 197,
"end": 198,
"name": "b"
},
"init": {
"type": "Literal",
"start": 201,
"end": 202,
"value": 2,
"raw": "2"
}
}
],
"kind": "const"
},
{
"type": "VariableDeclaration",
"start": 203,
"end": 214,
"declarations": [
{
"type": "VariableDeclarator",
"start": 209,
"end": 214,
"id": {
"type": "Identifier",
"start": 209,
"end": 210,
"name": "c"
},
"init": {
"type": "Literal",
"start": 213,
"end": 214,
"value": 3,
"raw": "3"
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}

作用域

作用域是一个抽象的概念 描述了一个变量的生命周期

1
2
3
4
5
6
7
Global scope
golbal {
TEMPORARY .result
VAR C
VAR B
VAR A
}

字节码

  • 字节码是机器码的抽象表示
  • 源代码直接编译成机器码编译时间太长 体积太大 不适合

字节码就是机器码的抽象表示 类似与虚拟DOM 机器码等于不同宿主环境的具体实现 比如 node环境 DOM react-native 等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[generated bytecode for function: assert (0x0295d247bb41 <SharedFunctionInfo assert>)]
Bytecode length: 21
Parameter count 3
Register count 2
Frame size 16
Bytecode age: 0
204 S> 00000138D3A85646 @ 0 : 0b 03 Ldar a0
00000138D3A85648 @ 2 : 96 11 JumpIfToBooleanTrue [17] (00000138D3A85659 @ 19)
254 S> 00000138D3A8564A @ 4 : 17 04 LdaImmutableCurrentContextSlot [4]
00000138D3A8564C @ 6 : c3 Star1
254 E> 00000138D3A8564D @ 7 : 61 f9 00 CallUndefinedReceiver0 r1, [0]
00000138D3A85650 @ 10 : c4 Star0
272 S> 00000138D3A85651 @ 11 : 0b fa Ldar r0
278 E> 00000138D3A85653 @ 13 : 69 fa 04 01 02 Construct r0, a1-a1, [2]
272 E> 00000138D3A85658 @ 18 : a7 Throw
00000138D3A85659 @ 19 : 0e LdaUndefined
321 S> 00000138D3A8565A @ 20 : a9 Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 19)
0x0138d3a85661

V8内存管理

  • 程序运行需要内存分配
  • V8也会申请内存 这种内存叫常驻内存集合
  • 常驻内存集合又分成堆和栈

  • 栈用于存放JS中的基本类型和引用类型指针
  • 栈的空间是连续的 增加删除只需要移动指针 操作速度非常快
  • 栈的空间是有限的 当栈满了 就会抛出一个错误
  • 栈一般都是执行函数时创建的 在函数执行完毕后 栈会被销毁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person(name) {
this.name = name
}

function one() {
const a = new Person('a')
function two() {
const b = new Person('b')
function three() {
const c = new Person('c')
}
debugger
three()
}
debugger
two()
}
debugger
one()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name) {
this.name = name
}

function create() {
const a = 1
const b = 2
const p = new Person(a)
return function getP() {
return p
}
}
let winP = create()
winP()

  • 如果不需要连续空间 或者申请的内存很大 可以使用堆
  • 堆主要用于存储JS中的引用类型

堆空间分类
新生代(new_space)
  • 新生代内存用于存放一些生命周期比较短的对象数据
老生代 (old_space)
  • 老生代内存用于存放一些生命周期比较长的对象数据
  • new space的对象进行两个周期的垃圾回收后 如果数据还存在new space中 则将他们存放到old_space
  • old space又可以分为两部分 分别是old pointer space和old data space
    • old pointer space 存放GC后surviving的指针对象
    • old data space 存放GC后surviving的数据对象
  • old space 使用标记清除和标记整理的方式进行垃圾回收
运行时代码空间(code_space)
  • 用于存放JIT已编译的代码
  • 唯一拥有执行权限的内存
大对象空间(Large object space)
  • 为了避免大对象的拷贝 使用该空间专门储存大对象
  • GC不会回收这部分内存
隐藏类(Map Space)
  • 存放对象的Map信息 即隐藏类
  • 隐藏类是为了提升对象属性的访问速度的
  • V8会为每个对象创建一个隐藏类 记录了对象的属性布局 包括所有的属性和偏移量

什么是垃圾

  • 在程序运行过程中肯定会用到一些数据 这些数据会放在堆栈中 但是在程序运行结束后 这些数据就不会在被使用了 那么这些不再使用的就是垃圾

    1
    2
    3
    4
    global.a = {name: "a"}
    global.a.b = {name: "b1"}
    global.a.b = {name: "b2"}
    // {name: b1} 不用了就是垃圾

新生代垃圾回收

  • 新生代内存有两个区域 分别是对象区域(from) 和 空闲区域(to)
  • 新生代内存使用Scavenger算法来管理内存
    • 广度优先遍历From-Space中的对象 从root出发 广度优先遍历所有能到达的对象 把存活对象复制到To-Space中
    • 遍历完成后清空From-Space
    • From-Space和To-Space角色互换
  • 复制后的对象在To-Spce中占用的内存空间是连续的 不会出现碎片问题
  • 这种垃圾回收方式快速而又高效 但是会造成空间浪费
  • 新生代的GC比较频繁
  • 新生代的对象转移到老生代称为晋升Promote 情况有
    • 经过一次GC还存活的对象
    • 对象复制到To-Space时 To-Space 的空间达到一定限制
1
2
3
4
5
global.a = {}
global.b = { e: {} }
global.c = { f: {}, g: { h: {} } }
global.d = {}
global.d = null

老生代的垃圾回收

  • 老生代里的对象有些是从新生代晋升的 有些是比较大的对象直接分配到老生代里的 所以老生代的对象空间大 活得久
  • 如果使用Scavenge算法 浪费空间 而且复制大块的内存空间消耗时间会很长 显然不合适
  • V8在老生代中垃圾回收策略采用Mark-Sweep(标记清除)和Mark-Compact(标记整合)相结合
Mark-Sweep(标记清除)
  • 标记清除分为标记和清除两个阶段
  • 在标记阶段需要遍历堆中的所有对象 并标记哪些活着的对象 然后进入清除阶段 在清除阶段 只清除没有被标记的对象
  • V8采取的是黑色和白色来标记数据 垃圾收集之前 会把所有的数据设置成白色 用来标记所有的尚未标记的对象 然后会从GC ROOT出发以深度优先的方式把所有的能访问到的数据都标记为黑色 遍历结束黑的就是活数据 白的就是可以清理的垃圾数据
  • 由于标记清除只清除死亡对象 而死亡对象在老生代中占用比例很小 所以效率很高
  • 标记清除有一个问题就是进行一次标记清除后 内存往往不是连续的 会出现很多内存碎片 如果后续分配一个大的对象 所有的内存碎片都不够用会出现内存溢出问题

Mark-Compact(标记整理)
  • 标记整理正是为了解决标记清除所带来的内存碎片问题
  • 标记整理在标记清除的基础进行修改 将其的清除阶段变成紧缩极端
  • 在整理的过程中 将活着的对象向内存区的一段移动 移动完成后直接清理掉边界外的内存
  • 紧缩过程涉及对象的移动 所以效率并不好 但是能保证不会生成内存碎片 一般10次标记清理会伴随一次标记整理

优化

  • 在执行垃圾回收算法期间 JS脚本需要暂停 这种叫Stop the world(全停顿)
  • 如果回收时间过长 会引起卡顿
  • 性能优化
    • 如果把大任务拆分成小任务 分布执行 类似于fiber
    • 将一些任务放在后台执行 不占用主线程
Parallel(并行执行)
  • 新生代的垃圾回收采取并行策略提升垃圾回收速度 它会开启多个辅助线程来执行新生代的垃圾回收
  • 并行执行需要的时间等于所有的辅助线程时间的总和加上管理的时间
  • 并行执行的时候也是全停顿状态 主线程不能进行任何操作 只能等待辅助线程的完成
  • 主要用于新生代的垃圾回收
增量标记
  • 老生代因为对象又大又多 所以垃圾回收的时间更长 采用增量标记的方式进行优化
  • 增量标记就是把标记工作分成多个阶段 每个阶段都只标记一部分对象 和主线程的执行穿插进行
  • 为了支持增量标记 V8必须可以支持垃圾回收的暂停和恢复 所以采用了黑白灰三色标记法
    • 黑色表示这个节点被GC ROOT 引用到了 而且该节点的子节点都已经标记完成了
    • 灰色表示这个节点被GC ROOT引用到了 但子节点还没被垃圾回收器标记处理 表面正在处理这个节点
    • 白色表示此节点还没未被垃圾回收器发现 如果在本轮遍历结束时还是白色 则这块数据将会很快被收回
  • 引入了灰色标记后 就可以通过判断有没有灰色节点来判断标记是否完成了 如果有灰色节点下次回复应该从灰色开始执行
写屏障
  • 当黑色指向白色节点的时候 就会触发写屏障 这个写屏障会把白色节点设置成灰色
Lazy Sweeping(惰性清理)
  • 当标记完成后 如果内存够用 先不清理 等JS代码执行完慢慢清理
并发回收
  • 其实增量标记和惰性清理并没有减少暂停的总时间
  • 并发回收就是主线程在执行过程中 辅助线程可以在后台完成垃圾回收工作
  • 标记操作全都由辅助线程完 清理操作由主线程和辅助线程配合完成
并发和并行
  • 并发和并行都是同时执行任务
  • 并行的同时是同一时刻可以多个进程在运行
  • 并发的同时是经过上下文快速切换 使得看上去多个进程都在同时运行的现象

内存泄漏

内存泄漏:当不在用到的对象没有及时被回收时 即内存泄漏了

不合理的闭包

JS中的设计模式

设计模式介绍

  • 设计模式是在解决问题的时候针对待定问题给出的简洁而优化的处理方案
  • 在JS设计模式中 最核心的思想是: 封装变化
  • 变与不变分离 变化的部分灵活 不变的部分稳定

构造器模式

复用对象

1
2
3
4
5
6
7
8
9
10
11
function Employee(name, age, salary) {
this.name = name;
this.age = age;
this.salary = salary;
this.say = ()=>{
console.log(this.name,this.age)
}
}

const emp1 = new Employee("John", 30, 40000);
const emp2 = new Employee("David", 25, 30000);

原型模型

1
2
3
4
5
6
7
8
9
10
11
12
function Employee(name, age, salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
Employee.prototype.say = function() {
console.log(`Name: ${this.name}, Age: ${this.age}, Salary: ${this.salary}`);
}

const emp1 = new Employee("John", 30, 40000);
const emp2 = new Employee("David", 25, 30000);
console.log(emp1.say())

ES6中使用类的写法 (结二唯一) 原型模式将函数挂在在原型对象上

1
2
3
4
5
6
7
8
9
10
class Employee {
constructor(name, age, salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
say() {
console.log(`Name: ${this.name}, Age: ${this.age}, Salary: ${this.salary}`);
}
}

工厂模式

由一个工厂对象决定创建某一种产品对象类实例 主要用来创建同一类对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function userFactory(role) {
function User(role, pages) {
this.role = role
this.pages = pages
}
switch (role) {
case 'superAdmin':
return new User('superAdmin', ['home', 'users', 'settings', 'admin'])
break
case 'admin':
return new User('admin', ['home', 'users', 'settings'])
break
case 'user':
return new User('user', ['home', 'settings'])
break
default:
return new User('guest', ['home'])
break
}
}

ES6写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class User {
constructor(role, pages) {
this.role = role
this.pages = pages
}
static userFactory(role) {
switch (role) {
case 'superAdmin':
return new User('superAdmin', ['home', 'users', 'settings', 'admin'])
break
case 'admin':
return new User('admin', ['home', 'users', 'settings'])
break
case 'user':
return new User('user', ['home', 'settings'])
break
default:
return new User('guest', ['home'])
break
}
}
}
const superAdmin = User.userFactory('superAdmin')

简单工厂的优点在于 只需要一个正确的参数 就可以获取到返回相应的对象 而无需知道创建的具体细节 简单工厂只能作于创建的对象数量较少 对象的创建逻辑不复杂的时候使用

抽象工厂模式

抽象工厂并不直接生成实例 而是生成对产品类的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class User {
constructor(name, role, pages) {
this.name = name
this.role = role
this.pages = pages
}
welcome() {
console.log(`Welcome ${this.name}`)
}
dataShow() {
throw new Error('Method not implemented')
}
}

class SuperAdmin extends User {
constructor(name) {
super(name, 'superAdmin', ['home', 'users', 'settings', 'admin'])
}
dataShow() {
console.log('Data show for super admin')
}
addUser() {
console.log('Add user')
}
deleteUser() {
console.log('Delete user')
}
}

class Admin extends User {
constructor(name) {
super(name, 'admin', ['home', 'users', 'settings'])
}
dataShow() {
console.log('Data show for admin')
}
addUser() {
console.log('Add user')
}
}

class Editor extends User {
constructor(name) {
super(name, 'editor', ['home', 'articles', 'settings'])
}
dataShow() {
console.log('Data show for editor')
}
addArticle() {
console.log('Add article')
}
deleteArticle() {
console.log('Delete article')
}
}

function getAbstractUserFactory(role) {
switch (role) {
case 'superAdmin':
return SuperAdmin
break
case 'admin':
return Admin
break
case 'editor':
return Editor
break
default:
return User
break
}
}

建造者模式

建造者模式属于创建型模式的一种 提供一种创建复杂对象的方式 将一个复杂对象的构建与它的表示分离 使得同样的构建过程可以创建不同的表示

构建者是一步一步的创建一个复杂的对象 运行用户只通过指定复杂的对象类型和内容就可以构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class navbar {
init() {
console.log('Navbar initialized')
}
getData() {
console.log('Data show for navbar')
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('navbar Data fetched')
}, 2000)
})
}
render() {
console.log('Navbar rendered')
}
}

class sidebar {
init() {
console.log('Sidebar initialized')
}
getData() {
console.log('Data show for sidebar')
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('sidebar Data fetched')
}, 2000)
})
}
render() {
console.log('Sidebar rendered')
}
}

class Creator {
async startBuiler(builder) {
await builder.init()
await builder.getData()
await builder.render()
}
}

const option = Creator
option.startBuiler(new navbar())
option.startBuiler(new sidebar())

建造者模式将一个复杂对象的构建层与其表示层分离 同样的构建过程可采用不同的表示 工厂模式主要是为了创建对象实例或者类对象(抽象工厂) 关系的是最终的产出是什么 而不关心构建过程 建站者正好相反关注的是整个过程

单例模式

保证一个类只有一个实例 并提供一个访问它的全局访问点

主要解决一个全局使用的类频繁地创建和销毁 占用内存

1
2
3
4
5
6
7
8
9
10
11
12
13
const single = (function () {
let instace = null
function User(name, age) {
this.name = name
this.age = age
}
return function (name, age) {
if (!instace) {
instace = new User(name, age)
}
return instace
}
})()
1
2
3
4
5
6
7
8
9
10
class Single {
constructor(name, age) {
if (!this.instace) {
this.name = name
this.age = age
this.instace = this
}
return Single.instace
}
}

比如状态管理 vuex | Redux等 store 是单例模式

装饰器模式

转饰器模式能够对已有的功能进行拓展, 这样不会更改原有的代码, 对其他的业务产生影响 这方便我们在较少的改动下对软件功能进行拓展

比如现在给一个上传数据操作加上一个前置的PV操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.before = function (beforeFn) {
const that = this
return function (...args) {
beforeFn.apply(this, ...args)
return that.apply(this, ...args)
}
}

const log = () => {
console.log('打印上传前的日志')
}

const upload = () => {
console.log('上传数据')
}

newUpload = upload.before(log)
newUpload()

适配器模式

将一个类的接口转换成客户端希望的另一个接口, 适配器模式让那些接口不兼容的类也可以一起工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class TencentMap {
show() {
console.log('腾讯地图渲染')
}
}

class BaiduMap {
display() {
console.log('百度地图渲染')
}
}

class TencentMapAdapter extends TencentMap {
constructor() {
super()
}
showMap() {
this.show()
}
}

class BaiduMapAdapter extends BaiduMap {
constructor() {
super()
}
showMap() {
this.display()
}
}

function render(map) {
map.showMap()
}

render(new TencentMapAdapter())
render(new BaiduMapAdapter())

适配器不会去改变实现 主要作用是干涉了抽象的过程 外部接口的适配器能够让同一个方法适用于多个系统 比如经典的axios 分成web环境和node环境

策略模式

策略模式定义了一系列算法 并将每个算法封装起来 使它们可以相互替换 且算法的变化不会影响使用算法的客户 策略模式属于对象行为模式 通过对算法的封装 把算法的责任和算法的实现分开 并委派不同的对象对这些算法进行管理

主要解决了多种算法相似的时 使用 if else 带来的复杂和难以维护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let stragery = {
A: (salary) => {
return salary * 4
},
B: (salary) => {
return salary * 3
},
C: (salary) => {
return salary * 2
}
}

function callBouns(level, salary) {
return stragery[level](salary)
}

代理模式

代理模式(Proxy) 为其他对象提供一种代理以控制对这个对象的访问

代理模式使得代理对象控制具体对象的引用 代理几乎可以是任何对象 文件 资源 内存中的对象 或者一些难以复制的东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const stars = {
name: 'yueyun',
workPrice: 10000
}

const startProxy = new Proxy(stars, {
get: (target, key) => {
console.log('访问了', key)
return target[key]
},
set: (target, key, value) => {
if (key === 'workPrice') {
if (value > 20000) {
console.log('explosion!')
target[key] = value
} else {
throw new Error('?')
}
}
}
})

startProxy.workPrice = 1000

观察者模式

观察者模式包含观察目标和观察者两类对象
一个目标可以有任意数目的与之相依赖的观察者
一旦观察目标的状态发生改变 所有观察者将会得到通知

当一个对象的状态发生改变时、所有依赖于其它的对象都得到了通知 解决了主体对象与观察者之间的功能耦合 即一个对象状态改变给其他对象通知的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 观察目标
class SubJect {
constructor() {
this.observes = []
}
add(observe) {
this.observes.push(observe)
}
remove(observe) {
this.observes = this.observes.filter((item) => item !== observe)
}
notify() {
this.observes.forEach((item) => {
item.update()
})
}
}

// 观察者
class Observe {
constructor(name) {
this.name = name
}
update() {
console.log('update', this.name)
}
}

const observe1 = new Observe('observe1')
const observe2 = new Observe('observe2')
const sub = new SubJect()
sub.add(observe1)
sub.add(observe2)
sub.remove(observe1)
sub.notify()

缺点: 观察者模式虽然实现了对象之间依赖关系的低耦合 但却不能对事件通知进行细分管控 如筛选通知指定主题事件通知

发布订阅模式

观察者和目标要相互知道

发布者和订阅者不用相互知道 通过第三方实现调度 属于经过解耦合的观察者模式