Loading

现代 JavaScript 教程 — JavaScript 编程语言篇

同步链接: https://www.shanejix.com/posts/现代 JavaScript 教程 — JavaScript 编程语言篇/

摘自 现代 JavaScript 教程;总结自己觉得重要/疏忽/未知的部分,闲来无事时看看,抓耳挠腮时看看。长篇预警!


ECMA-262 规范


最权威的信息来源(语言细节),每年都会发布一个新版本的规范

🚩 最新的规范草案请见 https://tc39.es/ecma262/

🚩 最新最前沿的功能,包括“即将纳入规范的”(所谓的 “stage 3”),请看这里的提案 https://github.com/tc39/proposals

现代模式,"use strict"


- 新的特性被加入,旧的功能也没有改变 这么做有利于兼容旧代码,

- 但缺点是 JavaScript 创造者的任何错误或不完善的决定也将永远被保留在 JavaScript 语言中

- 这种情况一直持续到 2009 年 ECMAScript 5 (ES5) 的出现

- ES5 规范增加了新的语言特性并且修改了一些已经存在的特性

- 为了保证旧的功能能够使用,大部分的修改是默认不生效的

- 【需要一个特殊的指令 —— "use strict" 来明确地激活这些特性】

🚩 位置和时机


- 脚本文件的顶部

- 函数体的开头

- “classes” 和 “modules”自动启用`'use strict'`

---

- 没有办法取消 `'use strict'`

- console控制台`'use strict'; <Shift+Enter 换行>`

大写形式的常数


一个普遍的做法是将常量用作别名,以便记住那些在执行之前就已知的难以记住的值

🚩使用大写字母和下划线来命名这些常量

例如,让我们以所谓的“web”(十六进制)格式为颜色声明常量:

const COLOR_RED = "#F00";
const COLOR_GREEN = "#0F0";
const COLOR_BLUE = "#00F";
const COLOR_ORANGE = "#FF7F00";

// ……当我们需要选择一个颜色
let color = COLOR_ORANGE;
alert(color); // #FF7F00

好处:


-  `COLOR_ORANGE` 比 `"#FF7F00"` 更容易记忆

- 比起 `COLOR_ORANGE` 而言,`"#FF7F00"` 更容易输错

- 阅读代码时,`COLOR_ORANGE` 比 `#FF7F00` 更易懂

🚩 什么时候该为常量使用大写命名,什么时候进行常规命名?让我们弄清楚一点


- 作为一个“常数”,意味着值永远不变

- **但是有些常量在执行之前就已知了(比如红色的十六进制值),还有些在执行期间被“计算”出来,但初始赋值之后就不会改变**

例如:

const pageLoadTime = /* 网页加载所需的时间 */;


// **`pageLoadTime` 的值在页面加载之前是未知的,所以采用常规命名,但是它仍然是个常量,因为赋值之后不会改变**

// 换句话说,**大写命名的常量仅用作“硬编码(hard-coded)”值的别名**。**当值在执行之前或在被写入代码的时候,就知道值是什么了**。

数据类型

在 JavaScript 中有 8 种基本的数据类型(译注:**7 种原始类型(基本数据类型)和 1 种引用类型(复杂数据类型)**)

- `number`

- `bigint`

- `string`

- `boolean`

- `null`

- `undefined`

- `symbol`

- `object`

🚩 可以通过 typeof 运算符查看存储在变量中的数据类型


- 两种形式:`typeof x` 或者 `typeof(x)`

- 以字符串的形式返回类型名称,例如 `"string"`

- `typeof null` 会返回 `"object"` —— 这是 JavaScript 编程语言的一个错误,实际上它并不是一个 object

- `typeof alert` 的结果是 `"function"`。在 JavaScript 语言中没有一个特别的 `“function”` 类型。函数隶属于 object 类型。但是 `typeof` 会对函数区分对待,并返回 `"function"`。这也是来自于 JavaScript 语言早期的问题。*从技术上讲,这种行为是不正确的,但在实际编程中却非常方便。*

Number 类型


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

- `alert( 1 / 0 ); // Infinity`

- `alert( "not a number" / 2 + 5 ); // NaN` **NaN 是粘性的。任何对 NaN 的进一步操作都会返回 NaN**

常用的类型转换:转换为 string 类型、转换为 number 类型和转换为 boolean 类型

🚩 字符串转换

- 转换发生在输出内容的时候

- 或通过 String(value) 进行显式转换

🚩 数字型转换


- 转换发生在进行算术操作时

- 或通过 Number(value) 进行显式转换

- 规则:

 - Number(undefined);// NaN

 - Number(null);// 0

 - Number(true);// 1

 - Number(false);// 0

 - Number(str);// 原样读取str字符串,忽略两端空白,空字符串转换为0,出错则为NaN

🚩 布尔型转换


- 转换发生在进行逻辑操作时

- 可以通过 Boolean(value) 进行显式转换

    - Boolean(0);//false

    - Boolean(null);//false

    - Boolean(undefined);//false

    - Boolean(NaN);//false

    - Boolean("");//false

    - Boolean(" ");//true

    - Boolean("0");//true

自增/自减


**所有的运算符都有返回值**,自增/自减也不例外


- 前置形式返回一个新的值

- 后置返回原来的值(做加法/减法之前的值)

赋值 = 返回一个值


在 JavaScript 中,大多数运算符都会返回一个值

- 这对于 + 和 - 来说是显而易见的

- 但对于 = 来说也是如此

🚩语句 x = value 将值 value 写入 x 然后返回 x。

let a = 1;
let b = 2;

let c = 3 - (a = b + 1);

alert(a); // 3
alert(c); // 0

// 上面这个例子,(a = b + 1) 的结果是赋给 a 的值(也就是 3)。然后该值被用于进一步的运算。

// 有时会在 JavaScript 库中看到它。不过,请不要写这样的代码。这样的技巧绝对不会使代码变得更清晰或可读。

在比较字符串的大小时,JavaScript 会使用“字典(dictionary)”或“词典(lexicographical)”顺序进行判定


换言之,字符串是按字符(母)逐个进行比较的。

例如:

alert("Z" > "A"); // true
alert("Glow" > "Glee"); // true
alert("Bee" > "Be"); // true

🚩 字符串的比较算法非常简单



- 首先比较两个字符串的首位字符大小

- 如果一方字符较大(或较小),则该字符串大于(或小于)另一个字符串。算法结束。

- 否则,如果两个字符串的首位字符相等,则继续取出两个字符串各自的后一位字符进行比较

- 重复上述步骤进行比较,直到比较完成某字符串的所有字符为止

- 如果两个字符串的字符同时用完,那么则判定它们相等,否则未结束(还有未比较的字符)的字符串更大

🚩非真正的字典顺序,而是 Unicode 编码顺序


这是因为在 JavaScript 使用的内部编码表中(Unicode),小写字母的字符索引值更大

值的比较


避免问题:

- 除了严格相等 === 外,其他但凡是有 undefined/null 参与的比较,我们都需要格外小心

- 除非你非常清楚自己在做什么,否则永远不要使用 >= > < <= 去比较一个可能为 null/undefined 的变量

- 对于取值可能是 null/undefined 的变量,请按需要分别检查它的取值情况

逻辑 或 运算符


''一个或运算 || 的链,将返回第一个真值,如果不存在真值,就返回该链的最后一个值''

返回的值是操作数的初始形式,不会做布尔转换

🚩与“纯粹的、传统的、仅仅处理布尔值的或运算”相比,这个规则就引起了一些很有趣的用法

一,''获取变量列表或者表达式中的第一个真值''

// 例如,有变量 firstName、lastName 和 nickName,都是可选的(即可以是 undefined,也可以是假值)。

// 用或运算 || 来选择有数据的那一个,并显示出来(如果没有设置,则用 "Anonymous"):

let firstName = "";
let lastName = "";
let nickName = "SuperCoder";

alert(firstName || lastName || nickName || "Anonymous"); // SuperCoder

// 如果所有变量的值都为假,结果就是 "Anonymous"。

二,''短路求值(Short-circuit evaluation)''

// 或运算符 || 的另一个用途是所谓的“短路求值”。

// 这指的是,|| 对其参数进行处理,直到达到第一个真值,然后立即返回该值,而无需处理其他参数。

//如果操作数不仅仅是一个值,而是一个有副作用的表达式,例如变量赋值或函数调用,那么这一特性的重要性就变得显而易见了。

//在下面这个例子中,只会打印第二条信息:

true || alert("not printed");
false || alert("printed");

// 在第一行中,或运算符 || 在遇到 true 时立即停止运算,所以 alert 没有运行。

// 有时,人们利用这个特性,只在左侧的条件为假时才执行命令。

逻辑 与 运算符


与运算返回第一个假值,如果没有假值就返回最后一个值

返回的值是操作数的初始形式,**不会做布尔转换**

一,''获取变量列表或者表达式中的第一个假值''

alert(1 && 2 && null && 3); // null

二,''短路求值(Short-circuit evaluation):如果所有的值都是真值,最后一个值将会被返回''

alert(1 && 2 && 3); // 3,最后一个值

与运算 && 的优先级比或运算 || 要高

逻辑 非 运算符


逻辑非运算符接受一个参数,并按如下运作:

- 将操作数转化为布尔类型:true/false。

- **返回相反的值**

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

空值合并运算符(nullish coalescing operator) ??


- 将值既不是 null 也不是 undefined 的表达式**定义为**“已定义的(defined)


a ?? b 的结果是:

- 如果 a 是已定义的,则结果为 a

- 如果 a 不是已定义的,则结果为 b


- 换句话说,如果第一个参数不是 null/undefined,则 ?? 返回第一个参数。否则,返回第二个参数
result = a ?? b;

// 等价于

result = a !== null && a !== undefined ? a : b;

🚩 场景:

// 1. 为可能是未定义的变量提供一个默认值

let user;

alert(user ?? "Anonymous"); // Anonymous

// 2. 可以使用 ?? 序列从一系列的值中选择出第一个非 null/undefined 的值

let firstName = null;
let lastName = null;
let nickName = "Supercoder";

// 显示第一个已定义的值:
alert(firstName ?? lastName ?? nickName ?? "Anonymous"); // Supercoder

🚩** || 和 ?? 之间重要的区别是**:


- || 返回第一个 真 值

- ?? 返回第一个 已定义的 值

switch 类型很关键

🚩 严格相等

// 被比较的值必须是相同的类型才能进行匹配

let arg = prompt("Enter a value?");
switch (arg) {
  case "0":
  case "1":
    alert("One or zero");
    break;

  case "2":
    alert("Two");
    break;

  case 3:
    alert("Never executes!");
    break;
  default:
    alert("An unknown value");
}

// 输入 3,因为 prompt 的结果是字符串类型的 "3",不严格相等 === 于数字类型的 3,所以 case 3 不会执行!因此 case 3 部分是一段无效代码。所以会执行 default 分支。

函数 return 返回值


1.**空值的 return 或没有 return 的函数返回值为 undefined**

2.**不要在 return 与返回值之间添加新行**

对于 return 的长表达式,可能你会很想将其放在单独一行

如下所示:

return;
some + long + expression + or + whatever * f(a) + f(b);

但这不行,因为 JavaScript 默认会在 return 之后加上分号。上面这段代码和下面这段代码运行流程相同:

return;
some + long + expression + or + whatever * f(a) + f(b);

因此,实际上它的返回值变成了空值

函数表达式末尾会有个分号?

🚩 为什么函数表达式结尾有一个分号 ; 而函数声明没有?

function sayHi() {
  // ...
}

let sayHi = function () {
  // ...
};

答案很简单


- 在代码块的结尾不需要加分号 ;

    - if { ... }

    - for { }

    - function f { }

    - 等语法结构后面都不用加


- 函数表达式是在语句内部的:

    - `let sayHi = ...;`

    - 作为一个值,它不是代码块而是一个赋值语句

    - 不管值是什么,都建议在语句末尾添加分号 ;

    - 所以这里的分号与函数表达式本身没有任何关系,它只是用于终止语句

Babel :Transpiler And Polyfill


- 当使用语言的一些现代特性时,一些引擎可能无法支持这样的代码


- 正如上所述,并不是所有功能在任何地方都有实现


- 这就是 Babel 来拯救的东西

🚩Babel 是一个 transpiler,它将现代的 JavaScript 代码转化为以前的标准形式。

实际上,Babel 包含了两部分


1.第一,用于重写代码的 transpiler 程序

- 开发者在自己的电脑上运行它,它以之前的语言标准对代码进行重写

- 然后将代码传到面向用户的网站

- 像 [webpack](http://webpack.github.io/) 这样的现代项目构建系统,提供了在每次代码改变时自动运行 transpiler 的方法,因此很容易集成在开发过程中

2.第二,polyfill

- 新的语言特性可能不仅包括语法结构,还包括新的内建函数

- Transpiler 会重写代码,将语法结构转换为旧的结构

- 但是对于新的内建函数,需要我们去实现

- JavaScript 是一个高度动态化的语言,脚本可以添加/修改任何函数,从而使它们的行为符合现代标准

- 更新/添加新函数的脚本称为 “polyfill”,它“填补”了缺口,并添加了缺少的实现

🚩 两个有意思的 polyfills


- [core js](https://github.com/zloirock/core-js) 支持很多,允许只包含需要的功能

- [polyfill.io](http://polyfill.io/) 根据功能和用户的浏览器,为脚本提供 polyfill 的服务

🚩transpiler 和 polyfill 是必要的


如果要使用现代语言功能,transpiler 和 polyfill 是必要的

尾随(trailing)或悬挂(hanging)逗号

列表中的最后一个属性应以逗号结尾:

let user = {
  name: "John",
  age: 30,
};

- 列表中的最后一个属性应以逗号结尾,叫做尾随(trailing)或悬挂(hanging)逗号

- 这样便于添加、删除和移动属性,因为所有的行都是相似的

方括号访问属性的灵活性

🚩 对于多词属性,点操作就不能用了:


// 这将提示有语法错误
user.likes birds = true

// JavaScript 理解不了。它认为我们在处理 user.likes,然后在遇到意外的 birds 时给出了语法错误。

- 点符号要求 key 是''有效的变量标识符''。这意味着:''不包含空格,不以数字开头,也不包含特殊字符(允许使用 $ 和 _)''。


- 另一种方法,就是使用方括号,可用于任何字符串
let user = {};

// 设置
user["likes birds"] = true;

// 读取
alert(user["likes birds"]); // true

// 删除
delete user["likes birds"];

// 请注意方括号中的字符串要放在引号中,单引号或双引号都可以

🚩 方括号同样''提供了一种可以通过任意表达式来获取属性名的方法'' —— 跟语义上的字符串不同 —— 比如像类似于下面的变量:

let key = "likes birds";

// 跟 user["likes birds"] = true; 一样
user[key] = true;

🚩''变量 key 可以是程序运行时计算得到的,也可以是根据用户的输入得到的。然后可以用它来访问属性。这给了我们很大的灵活性。''

例如:

let user = {
  name: "John",
  age: 30,
};

let key = prompt("What do you want to know about the user?", "name");

// 访问变量
alert(user[key]); // John(如果输入 "name")

点符号不能以类似的方式使用:

let user = {
  name: "John",
  age: 30,
};

let key = "name";
alert(user.key); // undefined

计算属性 && 方括号访问属性

🚩 在对象字面量中,使用方括号

let fruit = prompt("Which fruit to buy?", "apple");

let bag = {
  [fruit]: 5, // 属性名是从 fruit 变量中得到的
};

alert(bag.apple); // 5 如果 fruit="apple"

🚩 本质上,这跟下面的语法效果相同:

let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};

// 从 fruit 变量中获取值
bag[fruit] = 5;

属性命名没有限制


变量名不能是编程语言的某个保留字,如 “for”、“let”、“return” 等……

🚩 但''对象的属性名并不受此限制'':

// 这些属性都没问题
let obj = {
  for: 1,
  let: 2,
  return: 3,
};

alert(obj.for + obj.let + obj.return); // 6

🚩 简而言之,属性命名没有限制。


- ''属性名可以是任何字符串或者 symbol(一种特殊的标志符类型,将在后面介绍)''


- ''其他类型会被自动地转换为字符串''

🚩 陷阱:


名为 `__proto__ `的属性。不能将它设置为一个''非对象''的值
let obj = {};
obj.__proto__ = 5; // 分配一个数字
alert(obj.__proto__); // [object Object] — 值为对象,与预期结果不同

对象有顺序吗?


对象有顺序吗?换句话说,如果我们遍历一个对象,我们获取属性的顺序是和属性添加时的顺序相同吗?这靠谱吗?

- 简短的回答是:“有特别的顺序”:''整数属性会被进行排序,其他属性则按照创建的顺序显示''

> 这里的“整数属性”指的是一个可以在不做任何更改的情况下与一个整数进行相互转换的字符串

🚩 整数属性:

let codes = {
  49: "Germany",
  41: "Switzerland",
  44: "Great Britain",
  // ..,
  1: "USA",
};

for (let code in codes) {
  alert(code); // 1, 41, 44, 49
}

🚩 非整数属性,按照创建时的顺序来排序:

let user = {
  name: "John",
  surname: "Smith",
};
user.age = 25; // 增加一个

// 非整数属性是按照创建的顺序来排列的
for (let prop in user) {
  alert(prop); // name, surname, age
}

🚩 利用非整数属性名来欺骗程序:

let codes = {
  "+49": "Germany",
  "+41": "Switzerland",
  "+44": "Great Britain",
  // ..,
  "+1": "USA",
};

for (let code in codes) {
  alert(+code); // 49, 41, 44, 1
}

垃圾回收

JavaScript内存管理得重要概念-可达性(Reachability)

- ''可达值:以某种方式可访问或可用的值''

- ''根(roots)'':固有的可达值的基本集合(这些值明显不能被释放):

    * 当前函数的局部变量和参数

    * 嵌套调用时,当前调用链上所有函数的变量与参数

    * 全局变量

    * 还有一些内部的

''被引用与可访问(从一个根)不同'':一组相互连接的对象可能整体都不可达

https://zh.javascript.info/garbage-collection#ke-da-xing-reachability

this

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



* 如果你经常使用其他的编程语言,那么你可能已经习惯了“绑定 this”的概念,即在对象中定义的方法总是有指向该对象的 this


* 在 JavaScript 中,this 是“自由”的,它的值是在调用时计算出来的,它的值并不取决于方法声明的位置,而是取决于在“点符号前”的是什么对象


* 在运行时对 this 求值的这个概念既有优点也有缺点

    - 一方面,函数可以被重用于不同的对象

    - 另一方面,更大的灵活性造成了更大的出错的可能

如果 `obj.f()` 被调用了,则 `this` 在 `f` 函数调用期间是 `obj`

new 操作符

🚩new


当一个函数被使用 new 操作符执行时,它按照以下步骤:

- 一个新的空对象被创建并分配给 this

- 函数体执行.通常它会修改 this,为其添加新的属性

- 返回 this 的值
// `new User(...)` 做的就是类似的事情
function User(name) {
  // this = {};(隐式创建)

  // 添加属性到 this
  this.name = name;
  this.isAdmin = false;

  // return this;(隐式返回)
}

🚩 return


- 通常,构造器没有 return 语句,任务是将所有必要的东西写入 this,并自动转换为结果

- 但是,如果这有一个 return 语句,那么规则就简单了:

    * ''如果 return 返回的是一个对象,则返回这个对象,而不是 this''

    * ''如果 return 返回的是一个原始类型,则忽略''

🚩 思考题:是否可以创建像 new A() == new B() 这样的函数 A 和 B?

function A() { ... }
function B() { ... }

let a = new A;
let b = new B;

alert( a == b ); // true

这是构造器的主要目的 —— 实现''可重用的对象创建''代码

new.target


在一个函数内部,可以使用 new.target 属性来检查/判断该函数是被

- 通过 new 调用的“构造器模式”

- 还是没被通过 new 调用的“常规模式”
function User() {
  alert(new.target);
}

// 不带 "new":
User(); // undefined

// 带 "new":
new User(); // function User { ... }
function User(name) {
  if (!new.target) {
    // 如果你没有通过 new 运行我
    return new User(name); // ……我会给你添加 new
  }

  this.name = name;
}

let john = User("John"); // 将调用重定向到新用户
alert(john.name); // John

?.可选链

🚩不存在的属性”的问题

// 获取 user.address.street,而该用户恰好没提供地址信息,会收到一个错误:

let user = {}; // 一个没有 "address" 属性的 user 对象

alert(user.address.street); // Error!

这是预期的结果

- JavaScript 的工作原理就是这样的,但是在很多实际场景中,我们''更希望得到的是 undefined 而不是一个错误''

可能最先想到的方案是在访问该值的属性之前,使用 if 或条件运算符 ? 对该值进行检查,像这样:

let user = {};

alert(user.address ? user.address.street : undefined);

……但是不够优雅,💡''对于嵌套层次更深的属性就会出现更多次这样的重复,这就是问题了''

// 例如,让我们尝试获取 user.address.street.name。既需要检查 user.address,又需要检查 user.address.street:

let user = {}; // user 没有 address 属性

alert(
  user.address ? (user.address.street ? user.address.street.name : null) : null
);

这样就''太扯淡了'',并且这可能导致写出来的代码很难让别人理解

更好的实现方式,就是 💡 使用 && 运算符:

let user = {}; // user 没有 address 属性

alert(user.address && user.address.street && user.address.street.name); // undefined(不报错

但仍然不够优雅

🚩可选链


''如果可选链 ?. 前面的部分是 undefined 或者 null,它会停止运算并返回该部分''

🚩不要过度使用可选链


''应该只将 ?. 使用在一些东西`可以不存在(null/undefined)`的地方''

- 例如,如果根据的代码逻辑,user 对象必须存在,但 address 是可选的,那么我们应该这样写 user.address?.street,而不是这样 user?.address?.street

- 所以,如果 user 恰巧因为失误变为 undefined,我们会看到一个编程错误并修复它。否则,代码中的错误在不恰当的地方被消除了,这会导致调试更加困难

🚩短路效应


如果 ?. 左边部分不存在,就会立即停止运算(“短路效应”)

🚩 其它变体:?.(),?.[]


- 可选链 ?. 不是一个运算符,而是一个特殊的语法结构

- 它还可以与函数和方括号一起使用

Symbol

🚩Symbol 值表示唯一的标识符

// id1 id2 是 symbol 的一个实例化对象, 描述都为"id"
let id1 = Symbol("id");
let id2 = Symbol("id");

// 描述相同的 Symbol —— 它们不相等
alert(id1 == id2); // false

alert(id1); // 类型错误:无法将 Symbol 值自动转换为字符串。

alert(id1.toString()); // 通过 toString 显示转化,现在它有效了

alert(id.description); // 或者获取 symbol.description 属性,只显示描述(description)

🚩“隐藏”属性


- ''Symbol 允许创建对象的“隐藏”属性

- 代码的任何其他部分都不能意外访问或重写这些属性''

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

// 属于另一个代码
let user = {
  name: "John",
};

// 使用 Symbol("id") 作为键
let id = Symbol("id");

user[id] = 1;

// 使用 Symbol 作为键来访问数据
alert(user[id]);

📌使用 Symbol("id") 作为键,比起用字符串 "id" 来有什么好处呢


- 因为 user 对象属于其他的代码,那些代码也会使用这个对象,所以不应该在它上面直接添加任何字段,这样很不安全

- 但是添加的 Symbol 属性不会被意外访问到,''第三方代码根本不会看到它'',所以使用 Symbol 基本上不会有问题

- 另外,假设另一个脚本希望在 user 中有自己的标识符,以实现自己的目的

- 这可能是另一个 JavaScript 库,因此脚本之间完全不了解彼此

- 然后该脚本可以创建自己的 Symbol("id")

像这样:

// ...
let id = Symbol("id");

user[id] = "Their id value";

- 我们的标识符和它们的标识符之间不会有冲突

- 因为 Symbol 总是不同的,即使它们有相同的名字

- ……但如果我们处于同样的目的,使用字符串 "id" 而不是用 symbol,那么 就会 ''出现冲突''

例如

let user = { name: "John" };

// 我们的脚本使用了 "id" 属性。
user.id = "Our id value";

// ……另一个脚本也想将 "id" 用于它的目的……
user.id = "Their id value";

// 砰!无意中被另一个脚本重写了 id!

🚩 跳过


''Symbol 属性不参与 for..in 循环''
let id = Symbol("id");
let user = {
  name: "John",
  age: 30,
  [id]: 123,
};

for (let key in user) alert(key); // name, age (no symbols)

// 使用 Symbol 任务直接访问
alert("Direct: " + user[id]);

- ''Object.keys(xxx)也会忽略'',“隐藏符号属性”原则的一部分

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

这里并不矛盾,就是这样设计的

- 这里的想法是当克隆或者合并一个 object 时,通常希望'' 所有 ''属性被复制(包括像 id 这样的 Symbol)

🚩全局 symbol


`有时想要名字相同的 Symbol 具有相同的实体`

例如,应用程序的不同部分想要访问的 Symbol "id" 指的是完全相同的属性。为了实现这一点,可以创建一个 ''全局 Symbol 注册表''。

要从注册表中读取(不存在则创建)Symbol,请使用 Symbol.for(key):

// 从全局注册表中读取
let id = Symbol.for("id"); // 如果该 Symbol 不存在,则创建它

// 再次读取(可能是在代码中的另一个位置)
let idAgain = Symbol.for("id");

// 相同的 Symbol
alert(id === idAgain); // true


Symbol 不是 100% 隐藏的

- 内置方法 Object.getOwnPropertySymbols(obj) 允许获取所有的 Symbol

- 还有一个名为 Reflect.ownKeys(obj) 的方法可以返回一个对象的 所有 键,包括 Symbol

所以它们并不是真正的隐藏

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

alert((123456).toString(36)); // 2n9c

🚩 如果想直接在一个数字上调用一个方法,比如上面例子中的 toString,那么需要在它后面放置两个点 ..


如果放置一个点:`123456.toString(36)`,那么就会出现一个 error,因为 JavaScript 语法隐含了第一个点之后的部分为小数部分

如果再放一个点,那么 JavaScript 就知道小数部分为空,现在使用该方法

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

为什么 0.1 + 0.2 不等于 0.3?

alert(0.1 + 0.2 == 0.3); // false

alert(0.1 + 0.2); // 0.30000000000000004

- 在十进制数字系统中,可以保证以 10 的整数次幂作为除数能够正常工作,但是以 3 作为除数则不能(1/3 * 3 = 1 1/3 = 0.3333... 无限循环)

- 也是同样的原因,在二进制数字系统中,可以保证以 2 的整数次幂作为除数时能够正常工作,但 1/10 就变成了一个无限循环的二进制小数

- 使用二进制数字系统无法 精确 存储 0.1 或 0.2,就像没有办法将三分之一存储为十进制小数一样

- IEEE-754 数字格式通过将数字舍入到最接近的可能数字来解决此问题,这些舍入规则通常不允许看到的“极小的精度损失”,但是它确实存在。
- 不仅仅是 JavaScript
许多其他编程语言也存在同样的问题

- PHP,Java,C,Perl,Ruby 给出的也是完全相同的结果,因为它们基于的是相同的数字格式

- 有时候我们可以尝试完全避免小数

- 例如,正在创建一个电子购物网站,那么可以用角而不是元来存储价格。但是,如果要打 30% 的折扣呢?

- ''实际上,完全避免小数处理几乎是不可能的。只需要在必要时剪掉其“尾巴”来对其进行舍入即可''

两个零

数字内部表示的另一个有趣结果是存在两个零:

- 0

- -0

这是因为在存储时,使用一位来存储符号,因此对于包括零在内的任何数字,可以设置这一位或者不设置

在大多数情况下,这种区别并不明显,因为运算符将它们视为相同的值

isFinite 和 isNaN


两个特殊的数值

- Infinity(和 -Infinity)是一个特殊的数值,比任何数值都大(小)

- NaN 代表一个 error

🚩 isNaN


isNaN(value) 将其参数转换为数字,然后测试它是否为 NaN
alert(isNaN(NaN)); // true
alert(isNaN("str")); // true

但是需要这个函数吗?不能只使用 `=== NaN` 比较吗?

- 不好意思,这不行

- `值 “NaN” 是独一无二的,它不等于任何东西,包括它自身`
alert(NaN === NaN); // false

🚩 isFinite:


isFinite(value) 将其参数转换为数字,如果是常规数字,则返回 true,而不是 NaN/Infinity/-Infinity
alert(isFinite("15")); // true
alert(isFinite("str")); // false,因为是一个特殊的值:NaN
alert(isFinite(Infinity)); // false,因为是一个特殊的值:Infinity

有时 isFinite 被用于验证字符串值是否为常规数字
let num = +prompt("Enter a number", "");

// 结果会是 true,除非你输入的是 Infinity、-Infinity 或不是数字
alert(isFinite(num));

Object.is


有一个特殊的内建方法 Object.is,它类似于 === 一样对值进行比较,但它对于`两种边缘情况`更可靠:

* 它适用于 `NaN`:`Object.is(NaN,NaN)=== true`,这是件好事。

* 值 `0` 和 `-0` 是不同的:`Object.is(0,-0)=== false`,从技术上讲这是对的,因为在内部,数字的符号位可能会不同,即使其他所有位均为零。

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

- 这种比较方式经常被用在 JavaScript 规范中

- 当内部算法需要比较两个值是否完全相同时,它使用 Object.is(内部称为 SameValue)

- https://tc39.es/ecma262/#sec-samevalue

在所有数字函数中,空字符串或仅有空格的字符串均被视为 0

isFinite(""); // true
isFinite("    "); // true

Number(""); // 0
Number("    "); //0

+""; //0
+"    "; //0

随机数

🚩 从 min 到 max 的随机数

// 将区间 0…1 中的所有值“映射”为范围在 min 到 max 中的值
// 1.将 0…1 的随机数乘以 max-min,则随机数的范围将从 0…1 增加到 0..max-min
// 2.将随机数与 min 相加,则随机数的范围将为 min 到 max

function random(min, max) {
  return min + Math.random() * (max - min);
}

alert(random(1, 5));
alert(random(1, 5));
alert(random(1, 5));

🚩 从 min 到 max 的随机整数

👉''错误的方案''

function randomInteger(min, max) {
  let rand = min + Math.random() * (max - min);
  return Math.round(rand);
}

alert(randomInteger(1, 3));

获得边缘值 min 和 max 的概率比其他值''低两倍'';💡 因为 Math.round() 从范围 1..3 中获得随机数,并按如下所示进行四舍五入:

values from 1    ... to 1.4999999999  become 1
values from 1.5  ... to 2.4999999999  become 2
values from 2.5  ... to 2.9999999999  become 3

👉''正确的解决方案''


方法一:调整取值范围的边界
//为了确保相同的取值范围,我们可以生成从 0.5 到 3.5 的值,从而将所需的概率添加到取值范围的边界

function randomInteger(min, max) {
  // 现在范围是从  (min-0.5) 到 (max+0.5)
  let rand = min - 0.5 + Math.random() * (max - min + 1);
  return Math.round(rand);
}

alert(randomInteger(1, 3));

方法二:使用 `Math.floor`
function randomInteger(min, max) {
  // here rand is from min to (max+1)
  let rand = min + Math.random() * (max + 1 - min);
  return Math.floor(rand);
}

alert(randomInteger(1, 3));

间隔都以这种方式映射
values from 1  ... to 1.9999999999  become 1
values from 2  ... to 2.9999999999  become 2
values from 3  ... to 3.9999999999  become 3

Unicode 规范化形式

http://www.unicode.org/reports/tr15/

http://www.unicode.org/

代理对


所有常用的字符都是一个 2 字节的代码

大多数欧洲语言,数字甚至大多数象形文字中的字母都有 2 字节的表示形式

但 2 字节只允许 65536 个组合,这对于表示每个可能的符号是不够的

所以稀有的符号被称为“''代理对''”的一对 2 字节的符号编码

这些符号的''长度是 2'':

alert("𝒳".length); // 2,大写数学符号 X
alert("😂".length); // 2,笑哭表情
alert("𩷶".length); // 2,罕见的中国象形文字

`String.fromCharCode` 和 `str.charCodeAt`

与

`String.fromCodePoint` 和 `str.codePointAt`,差不多


但是不适用于''代理对''

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String

JavaScript-Array


''JavaScript 中的数组

- 既可以用作队列,

- 也可以用作栈''

''允许从首端/末端来添加/删除元素''

- 这在计算机科学中,允许这样的操作的数据结构被称为 双端队列(deque)

- https://en.wikipedia.org/wiki/Double-ended_queue

JavaScript-数组误用的几种方式

🚩 数组误用的几种方式


// 1. 添加一个非数字的属性,比如 :
arr.test = 5。


// 2. 制造空洞,比如:添加 arr[0],然后添加 arr[1000] (它们中间什么都没有)。

arr[0] = 'first';
arr[1000] = 'last';


// 3. 以倒序填充数组,比如 arr[1000],arr[999] 等等。

arr[1000] = 1000;
arr[999] = 999;

JavaScript中数组是一种特殊的【对象】

- 请将数组视为作用于 ''有序数据'' 的特殊结构

- ''数组在 JavaScript 引擎内部是经过特殊调整的,使得更好地作用于连续的有序数据,所以请以正确的方式使用数组''

- 如果需要任意键值,那很有可能实际上需要的是常规对象 {}

JavaScript - 关于 Array 的 “length”


一,当修改数组的时候,length 属性会自动更新
let fruits = [];
fruits[123] = "Apple";

alert(fruits.length); // 124

二,数组的length 属性是可写的
// 如果手动增加它,则不会发生任何有趣的事儿。但是如果减少它,数组就会被截断。该过程是不可逆的,下面是例子:

let arr = [1, 2, 3, 4, 5];

arr.length = 2; // 截断到只剩 2 个元素
alert(arr); // [1, 2]

arr.length = 5; // 又把 length 加回来
alert(arr[3]); // undefined:被截断的那些数值并没有回来

📌 所以,''清空数组最简单的方法就是:arr.length = 0''

thisArg

🚩users.filter(user => army.canJoin(user)) 替换为users.filter(army.canJoin, army)
的区别?


用 users.filter(user => army.canJoin(user)) 替换对 users.filter(army.canJoin, army) 的调用

- 前者的使用频率更高

- 因为对于大多数人来说,它更容易理解

显式调用迭代器

let str = "Hello";

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

let iterator = str[Symbol.iterator]();

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

Symbol.iterator

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

希望 for..of 这样运行:

for(let num of range) ... num=1,2,3,4,5

注释的 range 的完整实现

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

// 1. for..of 调用首先会调用这个:
range[Symbol.iterator] = function () {
  // ……它返回迭代器对象(iterator object):
  // 2. 接下来,for..of 仅与此迭代器一起工作,要求它提供下一个值
  return {
    current: this.from,
    last: this.to,

    // 3. next() 在 for..of 的每一轮循环迭代中被调用
    next() {
      // 4. 它将会返回 {done:.., value :...} 格式的对象
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    },
  };
};

// 现在它可以运行了!
for (let num of range) {
  alert(num); // 1, 然后是 2, 3, 4, 5
}

🚩''注意可迭代对象的核心功能:关注点分离''


- range 自身没有 next() 方法

- 相反,是通过调用 range[Symbol.iterator]() 创建了另一个对象,即所谓的“迭代器”对象,并且它的 next 会为迭代生成值。

''迭代器对象和与其进行迭代的对象是分开的''

🚩 可迭代对象必须实现 Symbol.iterator 方法


* `obj[Symbol.iterator]()` 的结果被称为 `迭代器(iterator)`。由它处理进一步的迭代过程。

*一个迭代器必须有 `next()` 方法,它返回一个 `{done: Boolean, value: any}` 对象,这里 `done:true` 表明迭代结束,否则 value 就是下一个值

`Symbol.iterator `方法会被 `for..of `自动调用

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

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

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


Iterable 是实现了 Symbol.iterator 方法的对象

Array-like 是有索引和 length 属性的对象(所以它们看起来很像数组)

一个可迭代对象也许不是类数组对象。反之亦然,类数组对象可能不可迭代

🚩 如果有一个这样的对象,并想像数组那样操作它?


有一个全局方法 `Array.from` 可以接受一个`可迭代或类数组的值`,并从中获取一个“真正的”数组

然后就可以对其调用数组方法了

`Array.from(obj[, mapFn, thisArg])` 将`可迭代对象`或`类数组对象` obj 转化为`真正的数组 Array`

Map 可以使用对象作为键

let john = { name: "John" };

// 存储每个用户的来访次数
let visitsCountMap = new Map();

// john 是 Map 中的键
visitsCountMap.set(john, 123);

alert(visitsCountMap.get(john)); // 123

- ''使用对象作为键是 Map 最值得注意和重要的功能之一''

- 对于字符串键,Object(普通对象)也能正常使用,但对于对象键则不行
let john = { name: "John" };

let visitsCountObj = {}; // 尝试使用对象

visitsCountObj[john] = 123; // 尝试将 john 对象作为键

// 是写成了这样!
alert(visitsCountObj["[object Object]"]); // 123

// 因为 `visitsCountObj` 是一个对象,它会将所有的键如 john 转换为字符串,所以得到字符串键 `"[object Object]"`

Map 是怎么比较键的?


* Map 使用 SameValueZero 算法来比较键是否相等

* 它和严格等于 === 差不多,但区别是

- NaN 被看成是等于 NaN所以 NaN 也可以被用作键

- 0 不等于 -0

这个算法不能被改变或者自定义

https://tc39.github.io/ecma262/#sec-samevaluezero

Map 链式调用

每一次 map.set 调用都会返回 map 本身,所以可以进行“链式”调用:

map.set("1", "str1").set(1, "num1").set(true, "bool1");

Map 迭代

可以使用以下三个方法:


一,遍历所有的键
map.keys() —— 遍历并返回所有的键(returns an iterable for keys)

二,遍历所有的值
map.values() —— 遍历并返回所有的值(returns an iterable for values)

三,遍历所有的实体
map.entries() —— 遍历并返回所有的实体(returns an iterable for entries)

[key, value],for..of 在默认情况下使用的就是这个

Map 使用插入顺序


* 迭代的顺序与插入值的顺序相同

* 与普通的 Object 不同,Map 保留了此顺序'

Map 有内置的 forEach 方法

与 Array 类似

let recipeMap = new Map([
  ["cucumber", 500],
  ["tomatoes", 350],
  ["onion", 50],
]);

// 对每个键值对 (key, value) 运行 forEach 函数
recipeMap.forEach((value, key, map) => {
  alert(`${key}: ${value}`); // cucumber: 500 etc
});

Object.entries:从对象创建 Map


如果想从一个已有的普通对象(plain object)来创建一个 Map

那么可以使用内建方法 `Object.entries(obj)`

''该方法返回对象的键/值对数组,该数组格式完全按照 Map 所需的格式''
let obj = {
  name: "John",
  age: 30,
};

let map = new Map(Object.entries(obj));

alert(map.get("name")); // John


* `Object.entries` 返回`键/值对数组:[ ["name","John"], ["age", 30] ]`

* 这就是 Map 所需要的格式

Object.fromEntries:从 Map 创建对象


`Object.fromEntries` 方法的作用和`Object.entries(obj)`的使用是相反的

给定一个具有 [key, value] 键值对的数组,它会根据给定数组创建一个对象
let prices = Object.fromEntries([
  ["banana", 1],
  ["orange", 2],
  ["meat", 4],
]);

// 现在 prices = { banana: 1, orange: 2, meat: 4 }

alert(prices.orange); // 2

Set 迭代(iteration)


可以使用

- `for..of`

- 或 `forEach`

来遍历 `Set`

一,使用  `for..of`
let set = new Set(["oranges", "apples", "bananas"]);

for (let value of set) {
  alert(value);
}
二,使用 `forEach` 来遍历
let set = new Set(["oranges", "apples", "bananas"]);

// 于数组 forEach 类似 ,与  Map forEach 相同:
set.forEach((value, valueAgain, set) => {
  alert(value);
});

forEach 的回调函数有三个参数:

- 一个 value

- 然后是 同一个值 valueAgain

- 最后是目标对象

没错,同一个值在参数里出现了两次

* 👍forEach 的回调函数有三个参数,是为了与 Map 兼容 - 底层实现是一致的

🚩Map 中用于迭代的方法在 Set 中也同样支持:


一,`set.keys()`

set.keys() —— 遍历并返回所有的值(returns an iterable object for values)

二,`set.values()`

set.values() —— 与 set.keys() 作用相同,这是为了兼容 Map
三,`set.entries()`

set.entries() —— 遍历并返回所有的实体(returns an iterable object for entries)[value, value],它的存在也是为了兼容 Map

`''Set 和 Map 是兼容的''`

WeakMap 和 Map 的区别


一,WeakMap 的键必须是对象,不能是原始值
let weakMap = new WeakMap();

let obj = {};

weakMap.set(obj, "ok"); // 正常工作(以对象作为键)

// 不能使用字符串作为键
weakMap.set("test", "Whoops"); // Error,因为 "test" 不是一个对象

二,如果在 WeakMap 中使用一个对象作为键,并且没有其他对这个对象的引用 —— 该对象将会被从内存(和Map)中''自动清除''
let john = { name: "John" };

let weakMap = new WeakMap();
weakMap.set(john, "...");

john = null; // 覆盖引用

// john 被从内存中删除了!

🚩JavaScript 引擎在值可访问(并可能被使用)时将其存储在内存中

let john = { name: "John" };

// 该对象能被访问,john 是它的引用

// 覆盖引用
john = null;

// 该对象将会被从内存中清除

通常,当对象、数组这类数据结构在内存中时,它们的子元素,如对象的属性、数组的元素都是可以访问的

例如,如果把一个对象放入到数组中,那么只要这个数组存在,那么这个对象也就存在,即使没有其他对该对象的引用

就像这样:

let john = { name: "John" };

let array = [john];

john = null; // 覆盖引用

// 前面由 john 所引用的那个对象被存储在了 array 中

// 所以它不会被垃圾回收机制回收

💡类似的,如果使用对象作为常规 Map 的键,那么当 Map 存在时,该对象也将存在;它会占用内存,并且应该不会被(垃圾回收机制)回收

例如:

let john = { name: "John" };

let map = new Map();
map.set(john, "...");

john = null; // 覆盖引用

// john 被存储在了 map 中,
// 我们可以使用 map.keys() 来获取它

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

三,WeakMap 不支持''迭代''以及 `keys()`,`values()` 和 `entries()` 方法;所以没有办法获取 WeakMap 的所有键或值

WeakMap 只有以下的方法:

- weakMap.get(key)

- weakMap.set(key, value)

- weakMap.delete(key)

- weakMap.has(key)

从技术上讲,WeakMap 的当前元素的数量是[未知的]

- JavaScript 引擎可能清理了其中的垃圾

- 可能没清理

- 也可能清理了一部分

因此,暂不支持访问 WeakMap 的所有键/值的方法

WeakMap 使用场景:额外的数据


假如正在处理一个“属于”另一个代码的一个对象,也可能是第三方库,并想存储一些与之相关的数据,那么这些数据就应该与这个对象【共存亡】

—— 这时候 WeakMap 正是我们所需要的利器👍


- 将这些数据放到 WeakMap 中,并使用该对象作为这些数据的键,那么当该对象被垃圾回收机制回收后,这些数据也会被自动清除
weakMap.set(john, "secret documents");
// 如果 john 消失,secret documents 将会被自动清除

🚩 例如,有用于处理用户访问计数的代码


收集到的信息被存储在 map 中:

- 一个用户对象作为键,其访问次数为值

- 当一个用户离开时(该用户对象将被垃圾回收机制回收),这时我们就不再需要他的访问次数了

使用 Map 的计数函数的例子:

// 📁 visitsCount.js
let visitsCountMap = new Map(); // map: user => visits count

// 递增用户来访次数
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

/** 其他部分的代码,可能是使用它的其它代码 **/

// 📁 main.js
let john = { name: "John" };

countUser(john); // count his visits

// 不久之后,john 离开了
john = null;

/**

现在 john 这个对象应该被垃圾回收,但他仍在内存中,因为它是 visitsCountMap 中的一个键

当移除用户时,需要清理 visitsCountMap,否则它将在内存中无限增大。在复杂的架构中,这种清理会成为一项繁重的任务

**/

📌可以通过使用 WeakMap 来避免这样的问题:

// 📁 visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: user => visits count

// 递增用户来访次数
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

/** 
现在不需要去清理 visitsCountMap 了。当 john 对象变成不可访问时,即便它是 WeakMap 里的一个键,它也会连同它作为 WeakMap 里的键所对应的信息一同被从内存中删除
**/

WeakMap 使用场景:缓存


当一个函数的结果需要被记住(“缓存”)

这样在后续的对同一个对象的调用时,就可以重用这个被缓存的结果

🚩 使用 Map 来存储结果


/ 📁 cache.js
let cache = new Map();

// 计算并记住结果
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* calculations of the result for */ obj;

    cache.set(obj, result);
  }

  return cache.get(obj);
}

// 现在我们在其它文件中使用 process()

// 📁 main.js
let obj = {/* 假设我们有个对象 */};

let result1 = process(obj); // 计算完成

// ……稍后,来自代码的另外一个地方……
let result2 = process(obj); // 取自缓存的被记忆的结果

// ……稍后,我们不再需要这个对象时:
obj = null;

alert(cache.size); // 1(啊!该对象依然在 cache 中,并占据着内存!)


/**
对于多次调用同一个对象,它只需在第一次调用时计算出结果,之后的调用可以直接从 cache 中获取。这样做的缺点是,当不再需要这个对象的时候需要清理 cache
**/

📌用 WeakMap 替代 Map,这个问题便会消失:当对象被垃圾回收时,对应的缓存的结果也会被自动地从内存中清除

// 📁 cache.js
let cache = new WeakMap();

// 计算并记结果
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* calculate the result for */ obj;

    cache.set(obj, result);
  }

  return cache.get(obj);
}

// 📁 main.js
let obj = {
  /* some object */
};

let result1 = process(obj);
let result2 = process(obj);

// ……稍后,我们不再需要这个对象时:
obj = null;

// 无法获取 cache.size,因为它是一个 WeakMap,
// 要么是 0,或即将变为 0
// 当 obj 被垃圾回收,缓存的数据也会被清除

WeakSet 和 Set 的区别


一,只能向 WeakSet 添加对象(而不能是原始值)

二,对象只有在其它某个(些)地方能被访问的时候,才能留在 set 中

三,跟WeakMap类似 支持 `add`,`has` 和 `delete` 方法,但不支持 `size` 和 `keys()`,并且不可`迭代`

.keys(),.values(),*entries()


 `*.keys(),*.values(),*entries()`  对于 `Array`  `Map` `Set`是通用的,对于普通对象有所不同

| | Array
Map
Set | Object |
| --- | --- | --- |
| 调用语法 | arr.keys()
map.keys()
set.keys() | Object.keys(obj)
,而不是 obj.keys() |
| 返回值 | 可迭代项 | ''“真正的”数组'' |

🚩 两个重要的区别:

一,对于对象使用的调用语法是 `Object.keys(obj)`,而不是 `obj.keys()`

主要原因是''灵活性''

- ''在 JavaScript 中,对象是所有复杂结构的基础''

- 因此,可能有一个自己创建的对象,比如 data,并实现了它自己的 data.values() 方法

- 同时,依然可以对它调用 Object.values(data) 方法

二,`Object.*` 方法返回的是''“真正的”数组''对象,而不只是一个可迭代项
这主要是历史原因;

🚩 会忽略 symbol 属性


 `*.keys(),*.values(),*entries()`会忽略 `symbol` 属性

就像 `for..in` 循环一样,这些方法会忽略使用 `Symbol(...)` 作为键的属性

🚩 但是,如果也想要 Symbol 类型的键,那么这儿有一个单独的方法

Object.getOwnPropertySymbols

- https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols

- 它会返回一个只包含 Symbol 类型的键的数组

另外,还有一种方法 Reflect.ownKeys(obj)

- https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys

- 它会返回 所有 键

数组解构

🚩 解构数组的完整语法:


let [item1 = default, item2, ...rest] = array

数组是一个存储数据的''有序''集合,`因此解构特征和数据顺序相关`

🚩 一,“解构”并不意味着“破坏”

let [firstName, surname] = "Ilya Kantor".split(" ");

// 这种语法叫做“解构赋值”,因为它通过将结构中的各元素复制到变量中来达到“解构”的目的。但数组本身是没有被修改的。

// 这只是下面这些代码的更精简的写法而已:

// let [firstName, surname] = arr;
let firstName = arr[0];
let surname = arr[1];

🚩 二,忽略使用逗号的元素

// 数组中不想要的元素也可以通过添加额外的逗号来把它丢弃

// 不需要第二个元素
let [firstName, , title] = [
  "Julius",
  "Caesar",
  "Consul",
  "of the Roman Republic",
];

alert(title); // Consul

🚩 三,等号右侧可以是任何可迭代对象

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

🚩 四,赋值给等号左侧的任何内容

// 可以在等号左侧使用任何“可以被赋值的”东西。

// 例如,一个对象的属性:

let user = {};
[user.name, user.surname] = "Ilya Kantor".split(" ");

🚩 五,与 .entries() 方法进行循环操作

let user = {
  name: "John",
  age: 30,
};

// 循环遍历键—值对
for (let [key, value] of Object.entries(user)) {
  alert(`${key}:${value}`); // name:John, then age:30
}

对于 map 对象也类似:

let user = new Map();
user.set("name", "John");
user.set("age", "30");

for (let [key, value] of user) {
  alert(`${key}:${value}`); // name:John, then age:30
}

🚩 六,交换变量的典型技巧

let guest = "Jane";
let admin = "Pete";

// 交换值:让 guest=Pete, admin=Jane
[guest, admin] = [admin, guest];

alert(`${guest} ${admin}`); // Pete Jane(成功交换!)

🚩 七,剩余的

let [name1, name2, ...rest] = [
  "Julius",
  "Caesar",
  "Consul",
  "of the Roman Republic",
];

alert(name1); // Julius
alert(name2); // Caesar

// 请注意,`rest` 的类型是数组
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
alert(rest.length); // 2

🚩 八,默认值

let [firstName, surname] = [];

alert(firstName); // undefined
alert(surname); // undefined
// 默认值
let [name = "Guest", surname = "Anonymous"] = ["Julius"];

alert(name); // Julius(来自数组的值)
alert(surname); // Anonymous(默认值被使用了)

对象解构

🚩 解构对象的完整语法:


let {prop : varName = default, ...rest} = object

对象是''通过键来存储数据项的单个实体'',`因此结构特征和键相关`

🚩 一,等号左侧包含被解构对象相应属性的一个“模式(pattern)”

// 在简单的情况下,等号左侧的就是 被解构对象 中的变量名列表

let options = {
  title: "Menu",
  width: 100,
  height: 200,
};

let { title, width, height } = options;

alert(title); // Menu
alert(width); // 100
alert(height); // 200

🚩 二,剩余模式(pattern)

let options = {
  title: "Menu",
  height: 200,
  width: 100,
};

// title = 名为 title 的属性
// rest = 存有剩余属性的对象
let { title, ...rest } = options;

// 现在 title="Menu", rest={height: 200, width: 100}
alert(rest.height); // 200
alert(rest.width); // 100

🚩 三,嵌套解构

可以在等号左侧使用更复杂的模式(pattern)来提取更深层的数据

let options = {
  size: {
    width: 100,
    height: 200,
  },
  items: ["Cake", "Donut"],
  extra: true,
};

// 为了清晰起见,解构赋值语句被写成多行的形式
let {
  size: {
    // 把 size 赋值到这里
    width,
    height,
  },
  items: [item1, item2], // 把 items 赋值到这里
  title = "Menu", // 在对象中不存在(使用默认值)
} = options;

alert(title); // Menu
alert(width); // 100
alert(height); // 200
alert(item1); // Cake
alert(item2); // Donut

🚩''四,智能函数参数''

有时,一个函数可能有很多参数,其中大部分的参数都是可选的

// 实现这种函数的一个很不好的写法

function showMenu(title = "Untitled", width = 200, height = 100, items = []) {
  // ...
}

// 缺点一:参数的顺序

// 缺点二:可读性会变得很差

📌''可以把所有参数当作一个对象来传递,然后函数马上把这个对象解构成多个变量''

// 传递一个对象给函数
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);

如果想让所有的参数都使用默认值,那应该传递一个空对象:

showMenu({}); // 不错,所有值都取默认值

showMenu(); // 这样会导致错误

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

function showMenu({ title = "Menu", width = 100, height = 200 } = {}) {
  alert(`${title} ${width} ${height}`);
}

showMenu(); // Menu 100 200

🚩''五,不使用 let 时的陷阱''

以下代码无法正常运行:


let title, width, height;

// 这一行发生了错误
{title, width, height} = {title: "Menu", width: 200, height: 100};

''问题在于 JavaScript 把主代码流(即不在其他表达式中)的 {...} 当做一个代码块。这样的代码块可以用于对语句分组,如下所示:''

{
  // 一个代码块
  let message = "Hello";
  // ...
  alert(message);
}

''为了告诉 JavaScript 这不是一个代码块,可以把整个赋值表达式用括号 (...) 包起来''

let title, width, height;

// 现在就可以了
({ title, width, height } = { title: "Menu", width: 200, height: 100 });

alert(title); // Menu

日期和时间

https://zh.javascript.info/date

JSON 方法,toJSON


JSON (JavaScript Object Notation) 是一种数据格式(表示值和对象的通用格式),具有自己的独立标准和大多数编程语言的库

[[RFC 4627 标准中有对其的描述|http://tools.ietf.org/html/rfc4627]]


JSON 支持

    - Objects { ... }

    - Arrays [ ... ]

    - Primitives:

        - strings

        - numbers

        - boolean (true/false)

        - null

* JSON 是语言无关的纯数据规范

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

    - 函数属性(方法)

    - Symbol 类型的属性

    - 存储 undefined 的属性

''重要的限制:不得有循环引用''

例如:

let user = {
  sayHi() {
    // 被忽略
    alert("Hello");
  },
  [Symbol("id")]: 123, // 被忽略
  something: undefined, // 被忽略
};

alert(JSON.stringify(user)); // {}(空对象)

🚩 如何解决?(自定义转换)


JavaScript 提供序列化(serialize)成 JSON 的方法 JSON.stringify 和解析 JSON 的方法 JSON.parse

这两种方法都支持用于智能读/写的转换函数

`JSON.stringify(student)` 得到的 json 字符串是一个被称为'' JSON 编码(JSON-encoded'' 或  或 ''字符串化(stringified)'' 或 ''编组化(marshalled)'' 的对象''序列化(serialized)''

🚩JSON.stringify 的完整语法:


let json = JSON.stringify(value[, replacer, space])

🚩JSON.parse的完整语法:


let value = JSON.parse(str, [reviver]);


如果一个对象具有 toJSON,那么它会被 JSON.stringify 调用

Spread 语法


其实 `任何可迭代对象都可以`

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

let str = "Hello";

alert([...str]); // H,e,l,l,o

// 对于一个字符串,for..of 会逐个返回该字符串中的字符,...str 也同理会得到 "H","e","l","l","o" 这样的结果。随后,字符列表被传递给数组初始化器 [...str]

// 还可以使用 Array.from 来实现,运行结果与 [...str] 相同

let str = "Hello";

// Array.from 将可迭代对象转换为数组
alert(Array.from(str)); // H,e,l,l,o

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


- Array.from 适用于类数组对象也适用于可迭代对象


- Spread 语法只适用于可迭代对象

Spread 语法 Array.from(obj) 的差别


Array.from 适用于类数组对象也适用于可迭代对象


Spread 语法只适用于可迭代对象

支持传入任意数量参数的内建函数



- Math.max(arg1, arg2, ..., argN) —— 返回入参中的最大值


- Object.assign(dest, src1, ..., srcN) —— 依次将属性从 src1..N 复制到 dest

closure


闭包是指使用一个''特殊的属性'' ''[[Environment]]'' 来''记录函数自身的创建时的环境''的''函数''



- ''特殊的属性'' ''[[Environment]]''

- ''记录函数自身的创建时的环境''

- ''函数''

https://zh.javascript.info/closure

IIFE(immediately-invoked function expressions,IIFE)

// 创建 IIFE 的方法

(function () {
  alert("Parentheses around the function");
})();

(function () {
  alert("Parentheses around the whole thing");
})();

!(function () {
  alert("Bitwise NOT operator starts the expression");
})();

+(function () {
  alert("Unary plus starts the expression");
})();

void (function () {
  alert("Unary plus starts the expression");
})();

"new Function"

🚩 语法:

let func = new Function([arg1, arg2, ...argN], functionBody);

🚩 场景:


使用 new Function 创建函数的应用场景非常特殊


比如在复杂的 Web 应用程序中,需要从服务器获取代码或者动态地从模板编译函数时才会使用

特殊:


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

这一点区别于[[closure]]
,因此,''此类函数无法访问外部(outer)变量,只能访问全局变量''

function getFunc() {
  let value = "test";

  let func = new Function("alert(value)");

  return func;
}

getFunc()(); // error: value is not defined

常规行为进行比较:

function getFunc() {
  let value = "test";

  let func = function () {
    alert(value);
  };

  return func;
}

getFunc()(); // "test",从 getFunc 的词法环境中获取的

📌''这一点实在实际中却非常实用'':


在将 JavaScript 发布到生产环境之前,需要使用 压缩程序(minifier) 对其进行压缩(删除多余的注释和空格等压缩代码 —— 更重要的是,将局部变量命名为较短的变量)

如果使 new Function 可以访问自身函数以外的变量,它也很有可能无法找到重命名的 userName,这是因为新函数的创建发生在代码压缩以后,变量名已经被替换了

调度:setTimeout 和 setInterval

🚩 语法:


let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)

let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)

🚩 垃圾回收和 setInterval/setTimeout 回调(callback)


// 当一个函数传入 setInterval/setTimeout 时,将为其创建一个内部引用,并保存在调度程序中。这样,即使这个函数没有其他引用,也能防止垃圾回收器(GC)将其回收


// 在调度程序调用这个函数之前,这个函数将一直存在于内存中
setTimeout(function() {...}, 100);


// 一个副作用:

// 如果函数引用了外部变量(译注:闭包),那么只要这个函数还存在,外部变量也会随之存在。它们可能比函数本身占用更多的内存

// 💡因此,当不再需要调度函数时,最好通过''定时器标识符(timer identifier)'取消它,即使这是个(占用内存)很小的函数。

🚩 嵌套的 setTimeout


嵌套的 setTimeout 能够精确地设置两次执行之间的延时,而 setInterval 却不能
// setInterval

let i = 1;
setInterval(function () {
  // 使用 setInterval 时,func 函数的实际调用间隔要比代码中设定的时间间隔要短!
  func(i++); // 这也是正常的,因为 func 的执行所花费的时间“消耗”了一部分间隔时间
}, 100);

//setTimeout

let i = 1;
setTimeout(function run() {
  func(i++);
  setTimeout(run, 100);
}, 100);

🚩 零延时实际上不为零(在浏览器中)

特殊的用法:


setTimeout(func)

setTimeout(func, 0)

在浏览器环境下,嵌套定时器的运行频率是受限制的。根据 HTML5 标准 所讲:“''经过 5 重嵌套定时器之后,时间间隔被强制设定为至少 4 毫秒''”

let start = Date.now();
let times = [];

setTimeout(function run() {
  times.push(Date.now() - start); // 保存前一个调用的延时

  if (start + 100 < Date.now()) alert(times);
  // 100 毫秒之后,显示延时信息
  else setTimeout(run); // 否则重新调度
});

历史遗留:


这个限制来自“远古时代”,并且许多脚本都依赖于此,所以这个机制也就存在至今

对于服务端的 JavaScript,就没有这个限制,并且还有其他调度即时异步任务的方式。例如 Node.js 的 setImmediate。因此,这个提醒只是针对浏览器环境的

属性标志和属性描述符

🚩属性标志 :对象(存储属性(properties), 键值对)还有三个特殊的特性(attributes)(除了value


- writable — 如果为 true,则值可以被修改,否则它是只可读的

- enumerable — 如果为 true,则会被在循环中列出,否则不会被列出

- configurable — 如果为 true,则此特性可以被删除,这些属性也可以被修改,否则不可以

🚩 查询属性描述符对象(属性的完整信息),使用Object.getOwnPropertyDescriptor


- 属性描述符对象:它包含值和所有的属性标志

语法:

let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);

例如:

let user = {
  name: "John",
};

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

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

🚩 修改属性标志,使用 Object.defineProperty

语法:

Object.defineProperty(obj, propertyName, descriptor);

例如:

let user = {};

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

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

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

不可配置性(configurable)对 defineProperty 施加了一些限制:



- 不能修改 configurable 标志

- 不能修改 enumerable 标志

- 不能将 writable: false 修改为 true(反过来则可以)

- 不能修改访问者属性的 get/set(但是如果没有可以分配它们)

''"configurable: false" 的用途是防止更改和删除属性标志,但是允许更改对象的值''

例如:

let user = {
  name: "John",
};

Object.defineProperty(user, "name", {
  configurable: false,
});

user.name = "Pete"; // 正常工作
delete user.name; // Error

🚩 多个属性接口 Object.definePropertiesObject.getOwnPropertyDescriptors

// 一起使用可以用作克隆对象的标志属性

let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));

for..in的区别:


- for..in 会忽略 symbol 类型的属性

- Object.getOwnPropertyDescriptors 返回包含 symbol 类型的属性在内的 所有 属性描述符

🚩 属性描述符在''单个属性''的级别上工作,还有一些限制访问 ''整个对象''的方法


- Object.preventExtensions(obj)

- Object.seal(obj)

- Object.freeze(obj)

...

属性的 getter 和 setter

🚩 两种种类型的对象属性:


- 数据属性

- 访问器属性(accessor properties): 本质上是用于获取和设置值的函数,但从外部代码来看就像常规属性

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


- 从外表看,访问器属性看起来就像一个普通属性

- 这就是访问器属性的设计思想:不以函数的方式调用,obj.xxx正常读取 (getter 在幕后运行)
let obj = {
  get propName() {
    // 当读取 obj.propName 时,getter 起作用
  },

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

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


对于访问器属性

- 没有 value 和 writable

- get 一个没有参数的函数,在读取属性时工作

- set 带有一个参数的函数,在设置属性时工作

- enumerable —— 与数据属性的相同

- configurable —— 与数据属性的相同

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

// 在同一个描述符中同时提供 get 和 value,则会出现错误

// Error: Invalid property descriptor.
Object.defineProperty({}, "prop", {
  get() {
    return 1;
  },

  value: 2,
});

🚩 访问器的一大用途


允许随时通过使用 getter 和 setter 『替换』“正常的”数据属性,来控制和调整这些属性的行为

例如:

// 始使用数据属性 name 和 age 来实现 user 对象

function User(name, age) {
  this.name = name;
  this.age = age;
}

let john = new User("John", 25);

alert(john.age); // 25

// ...

// ……但迟早,情况可能会发生变化,可能会决定存储 birthday,而不是 age,因为它更精确,更方便

function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;
}

let john = new User("John", new Date(1992, 6, 1));

// ...

// 💡现在应该如何处理仍使用 age 属性的旧代码呢?

// 可以尝试找到所有这些地方并修改它们,但这会花费很多时间

// 而且如果其他很多人都在使用该代码,那么可能很难完成所有修改

// ...

// 为 age 添加一个 getter 来解决这个问题

function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;

  // 年龄是根据当前日期和生日计算得出的
  Object.defineProperty(this, "age", {
    get() {
      let todayYear = new Date().getFullYear();
      return todayYear - this.birthday.getFullYear();
    },
  });
}

let john = new User("John", new Date(1992, 6, 1));

alert(john.birthday); // birthday 是可访问的
alert(john.age); // ……age 也是可访问的

原型继承(Prototypal inheritance)


原型继承 是JavaScript语言特性之一  能 实现 【代码重用】

🚩[[Prototype]]


* 在 JavaScript 中,【对象】有一个特殊的隐藏属性 [[Prototype]](如规范中所命名的)

  -  [[Prototype]]要么为 null

  -  [[Prototype]]要么就是对【另一个对象的引用】(该对象被称为“原型”)

* 当从 object 中读取一个缺失的属性时,JavaScript 会自动从原型中获取该属性

- 在编程中,这种行为被称为“原型继承”

🚩__proto__   和  [[Prototype]]


* 属性 [[Prototype]] 是内部的而且是隐藏的,但是有很多设置它的方式(其中之一就是使用特殊的名字 __proto__)


- 引用不能形成闭环。如果试图在一个闭环中分配 __proto__,JavaScript 会抛出错误


- __proto__ 与内部的 [[Prototype]] 不一样:__proto__ 是 [[Prototype]] 的 getter/setter

- 现代编程语言建议使用函数 Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__ 去 get/set 原型


- 根据规范,__proto__ 必须仅受浏览器环境的支持。但实际上,包括服务端在内的所有环境都支持它

🚩for..in循环


* for..in 循环也会迭代继承的属性


* 几乎所有其他键/值获取方法都忽略继承的属性。例如 Object.keys 和 Object.values 等

F.prototype


* JavaScript中可以使用诸如 new F() 这样的构造函数来创建一个新对象

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

注意:这里的 F.prototype 指的是 F 的一个名为 "prototype" 的常规属性

例如:

let animal = {
  eats: true,
};

function Rabbit(name) {
  this.name = name;
}

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

Rabbit.prototype = animal;

let rabbit = new Rabbit("White Rabbit"); //  rabbit.__proto__ == animal

alert(rabbit.eats); // true

🚩F.prototype 仅用在 new F 时


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


- 如果在创建之后,F.prototype 属性有了变化(F.prototype = <another object>),那么通过 new F 创建的新对象也将随之拥有新的对象作为 [[Prototype]],但已经存在的对象将保持旧有的值

🚩 每个【函数】都有 "prototype" 属性,即使没有提供它


- 默认的 "prototype" 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身
function Rabbit() {}
// by default:
// Rabbit.prototype = { constructor: Rabbit }

alert(Rabbit.prototype.constructor == Rabbit); // true

* 可以使用 constructor 属性来创建一个新对象,该对象使用与现有对象相同的构造器

- 当有一个对象,但不知道它使用了哪个构造器(例如它来自第三方库),并且需要创建另一个类似的对象时,用这种方法就很方便

例如

function Rabbit(name) {
  this.name = name;
  alert(name);
}

let rabbit = new Rabbit("White Rabbit");

let rabbit2 = new rabbit.constructor("Black Rabbit");

* F.prototype 的值要么是一个对象,要么就是 null:其他值都不起作用

- "prototype" 属性仅在设置了一个构造函数(constructor function),并通过 new 调用时,才具有这种特殊的影响

例如

// 在常规对象上,prototype 没什么特别的

let user = {
  name: "John",
  prototype: "Bla-bla", // 这里只是普通的属性
};

* 默认情况下,【所有函数】都有 F.prototype = {constructor:F}

- 所以可以通过访问它的 "constructor" 属性来获取一个对象的构造器

原生的原型


* 所有的内建对象都遵循相同的模式(pattern)

  - 方法都存储在 prototype 中(Array.prototype、Object.prototype、Date.prototype 等)

  - 对象本身只存储数据(数组元素、对象属性、日期)

* 原始数据类型也将方法存储在包装器对象的 prototype 中:Number.prototype、String.prototype 和 Boolean.prototype


* 只有 undefined 和 null 没有包装器对象

* 内建原型可以被修改或被用新的方法填充

  - 但是不建议更改它们

  - 唯一允许的情况可能是,当添加一个还没有被 JavaScript 引擎支持,但已经被加入 JavaScript 规范的新标准时,才可能允许这样做

原型简史


- 有多少种处理 [[Prototype]] 的方式,答案是有很多!

- 很多种方法做的都是同一件事儿!

🚩 为什么会出现这种情况?这是历史原因!


* 构造函数的 "prototype" 属性自古以来就起作用


* 之后,在 2012 年,Object.create 出现在标准中

  - 它提供了使用给定原型创建对象的能力

  - 但没有提供 get/set 它的能力

  - 因此,许多浏览器厂商实现了非标准的 __proto__ 访问器,该访问器允许用户随时 get/set 原型


* 之后,在 2015 年,Object.setPrototypeOf 和 Object.getPrototypeOf 被加入到标准中

  - 执行与 __proto__ 相同的功能

  - 由于 __proto__ 实际上已经在所有地方都得到了实现,但它已过时,所以被加入到该标准的附件 B 中,即:在非浏览器环境下,它的支持是可选的

🚩 为什么将 proto 替换成函数 getPrototypeOf/setPrototypeOf?


__proto__ 是 [[Prototype]] 的 getter/setter,就像其他方法一样,【它位于 Object.prototype】

🚩 如果速度很重要,就请不要修改已存在的对象的 [[Prototype]]


- 从技术上来讲,可以在任何时候 get/set [[Prototype]]。但是通常只在创建对象的时候设置它一次,自那之后不再修改

- 并且,JavaScript 引擎对此进行了高度优化。用 Object.setPrototypeOf 或 obj.__proto__= “即时”更改原型是一个非常缓慢的操作,因为它破坏了对象属性访问操作的内部优化

- 因此,除非你知道自己在做什么,或者 JavaScript 的执行速度对你来说完全不重要,否则请避免使用它

🚩Object.create(null)

语法:

Object.create(proto, [descriptors]); // 利用给定的 proto 作为 [[Prototype]](可以是 null)和可选的属性描述来创建一个空对象

通过 Object.create(null) 来创建没有原型的对象。这样的对象被用作 “pure dictionaries” / “very plain” 对象

* 如果要将一个用户生成的键放入一个对象,那么内建的 __proto__ getter/setter 是不安全的

  - 因为用户可能会输入 "__proto__" 作为键,这会导致一个 error,虽然希望这个问题不会造成什么大影响,但通常会造成不可预料的后果

  - 因此,可以使用 Object.create(null) 创建一个没有 __proto__ 的 “very plain” 对象

  - 或者对此类场景坚持使用 Map 对象

- 此外,Object.create 提供了一种简单的方式来浅拷贝一个对象的所有描述符
let clone = Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
);

Class 基本语法

🚩 基本的类语法看起来像这样:



class MyClass {

  prop = value; // 属性; class 字段 prop 会在在每个独立对象中被设好,而不是设在 Myclass.prototype

  prop = () => { // 属性; class 字段 prop 更优雅的绑定方法
    // ...
  }

  constructor(...) { // 构造器
    // ...
  }

  method(...) {} // method

  get something(...) {} // getter 方法
  set something(...) {} // setter 方法

  [Symbol.iterator]() {} // 有计算名称(computed name)的方法(此处为 symbol)
  // ...
}

- MyClass 是一个函数(提供作为 constructor 的那个)

- methods、getters 和 settors 都被写入了 MyClass.prototype

- prop  每个实例都有一份

🚩 什么是 class?在 JavaScript 中,类是一种函数

很好的诠释:

class User {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    alert(this.name);
  }
}

// class 是一个函数
alert(typeof User); // function

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

// 方法在 User.prototype 中,例如:
alert(User.prototype.sayHi); // alert(this.name);

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

🚩class 不仅仅是语法糖!


1. 通过 class 创建的函数具有特殊的内部属性标记 [[IsClassConstructor]]: true;编程语言会在许多地方检查该属性

例如

// class 必须使用 new 来调用

class User {
  constructor() {}
}

alert(typeof User); // function

User(); // Error: Class constructor User cannot be invoked without 'new'

2.大多数 JavaScript 引擎中的类构造器的字符串表示形式都以 “class…” 开头

js 中

class User {
  constructor() {}
}

alert(User); // class User { ... }

3.类方法不可枚举; 类定义将 "prototype" 中的所有方法的 enumerable 标志设置为 false

如果对一个对象调用 for..in 方法,通常不希望 用 class 方法出现


4 类总是使用 use strict。 在类构造中的所有代码都将自动进入严格模式

🚩 类表达式


- 像函数一样,类可以在另外一个表达式中被定义,被传递,被返回,被赋值等

匿名类表达式(类似匿名函数):

let User = class {
  sayHi() {
    alert("Hello");
  }
};

“命名类表达式(Named Class Expression)”(类似于命名函数表达式(Named Function Expressions):

// “命名类表达式(Named Class Expression)”
// (规范中没有这样的术语,但是它和命名函数表达式类似)
let User = class MyClass {
  sayHi() {
    alert(MyClass); // MyClass 这个名字仅在类内部可见
  }
};

new User().sayHi(); // 正常运行,显示 MyClass 中定义的内容

alert(MyClass); // error,MyClass 在外部不可见;名字仅在类内部可见

类继承

🚩 扩展一个类:class Child extends Parent


* 在内部,关键字 extends 使用了很好的旧的原型机制进行工作

- 它将 Child.prototype.[[Prototype]] 设置为 Parent.prototype

在 extends 后允许任意表达式:

function f(phrase) {
  return class {
    sayHi() {
      alert(phrase);
    }
  };
}

class User extends f("Hello") {}

new User().sayHi(); // Hello

// 这对于高级编程模式,例如当根据许多条件使用函数生成类,并继承它们时来说可能很有用

🚩 重写一个方法


* 默认情况下,所有未在 class child 中指定的方法均从 class Parent 中直接获取


- 有时不希望完全替换父类的方法,而是希望在父类方法的基础上进行调整或扩展其功能

Class 为此提供了 "super" 关键字:


- 执行 super.method(...) 来调用一个父类方法

- 执行 super(...) 来调用一个父类 constructor(只能在子类的 constructor 中)

补充:


- 箭头函数没有 super 和 this

🚩 重写一个 constructor


根据 规范,如果一个类扩展了另一个类并且没有 constructor,那么将生成下面这样的 constructor:


class Child extends Parent {
  // 为没有自己的 constructor 的扩展类生成的
  constructor(...args) {
    super(...args);
  }
}

''继承类的 constructor 必须调用 super(...),并且 (!) 一定要在使用 this 之前调用''

💡 为什么呢?



* 在 JavaScript 中,继承类(所谓的“派生构造器”,英文为 “derived constructor”)的构造函数与其他函数之间是有区别的

- 派生构造器具有特殊的内部属性 [[ConstructorKind]]:"derived";这是一个特殊的内部标签


该标签会影【响它的 new 行为】:

  - 当通过 new 执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给 this


  - 但是,当继承的 constructor 执行时,它不会执行此操作;它期望父类的 constructor 来完成这项工作


* 因此,派生的 constructor 必须调用 super 才能执行其父类(base)的 constructor,否则 this 指向的那个对象将不会被创建

😨 重写类字段: 一个棘手的注意要点;可以重写方法,也可以重写字段:

class Animal {
  name = "animal";

  constructor() {
    alert(this.name); // (*)
  }
}

class Rabbit extends Animal {
  name = "rabbit";
}

new Animal(); // animal
new Rabbit(); // animal

// 两种情况下:new Animal() 和 new Rabbit(),在 (*) 行的 alert 都打印了 animal

// 有点懵逼,用方法来进行比较:

class Animal {
  showName() {
    // 而不是 this.name = 'animal'
    alert("animal");
  }

  constructor() {
    this.showName(); // 而不是 alert(this.name);
  }
}

class Rabbit extends Animal {
  showName() {
    alert("rabbit");
  }
}

new Animal(); // animal
new Rabbit(); // rabbit

// 请注意:这时的输出是不同的

// 这才是本来所期待的结果。当父类构造器在派生的类中被调用时,它会使用被重写的方法;……但对于类字段并非如此。正如前文所述,父类构造器总是使用父类的字段

为什么会有这样的区别呢?


原因在于类字段初始化的顺序:

- 对于基类(还未继承任何东西的那种),在构造函数调用前初始化

- 对于派生类,在 super() 后立刻初始化

''这种字段与方法之间微妙的区别只特定于 JavaScript;这种行为仅在一个被重写的字段被父类构造器使用时才会显现出来;可以通过使用方法或者 getter/setter 替代类字段,来修复这个问题''

🚩 深入地研究 super [[HomeObject]]



- 当一个对象方法执行时,它会将当前对象作为 this

- 随后如果调用 super.method(),那么引擎需要从当前对象的原型中获取 method

😨super 怎么做到的?看似容易,其实并不简单!

使用普通对象演示一下:

let animal = {
  name: "Animal",
  eat() {
    alert(`${this.name} eats.`);
  },
};

let rabbit = {
  __proto__: animal,
  name: "Rabbit",
  eat() {
    // 这就是 super.eat() 可以大概工作的方式
    this.__proto__.eat.call(this); // (*)
  },
};

rabbit.eat(); // Rabbit eats.

''this.proto.eat() 将在原型的上下文中执行 eat,而非当前对象''

let animal = {
  name: "Animal",
  eat() {
    alert(`${this.name} eats.`);
  },
};

let rabbit = {
  __proto__: animal,
  eat() {
    // ...bounce around rabbit-style and call parent (animal) method
    this.__proto__.eat.call(this); // (*)
  },
};

let longEar = {
  __proto__: rabbit,
  eat() {
    // ...do something with long ears and call parent (rabbit) method
    this.__proto__.eat.call(this); // (**)
  },
};

longEar.eat(); // Error: Maximum call stack size exceeded

- 在 (*) 和 (**) 这两行中,this 的值都是当前对象(longEar);这是至关重要的一点:所有的对象方法都将当前对象作为 this,而非原型或其他什么东西


- 因此,在 (*) 和 (**) 这两行中,this.__proto__ 的值是完全相同的:都是 rabbit。它们俩都调用的是 rabbit.eat,它们在不停地循环调用自己,而不是在原型链上向上寻找方法
// 1.在 longEar.eat() 中,(**) 这一行调用 rabbit.eat 并为其提供 this=longEar

// 在 longEar.eat() 中我们有 this = longEar
this.__proto__.eat.call(this); // (**)
// 变成了
longEar.__proto__.eat.call(this);
// 也就是
rabbit.eat.call(this);

// 2.之后在 rabbit.eat 的 (*) 行中,希望将函数调用在原型链上向更高层传递,但是 this=longEar,所以 this.__proto__.eat 又是 rabbit.eat!

// 在 rabbit.eat() 中我们依然有 this = longEar
this.__proto__.eat.call(this); // (*)
// 变成了
longEar.__proto__.eat.call(this);
// 或(再一次)
rabbit.eat.call(this);

//3. ……所以 rabbit.eat 在不停地循环调用自己,因此它无法进一步地提升

😭 这个问题没法仅仅通过使用 this 来解决!!!

🚩 为了提供解决方法,JavaScript 为函数添加了一个特殊的内部属性:[[HomeObject]]


- 当一个函数被定义为类或者对象方法时,它的 [[HomeObject]] 属性就成为了该对象

- 然后 super 使用它来解析(resolve)父原型及其方法

看它是怎么工作的(对于普通对象)

let animal = {
  name: "Animal",
  eat() {
    // animal.eat.[[HomeObject]] == animal
    alert(`${this.name} eats.`);
  },
};

let rabbit = {
  __proto__: animal,
  name: "Rabbit",
  eat() {
    // rabbit.eat.[[HomeObject]] == rabbit
    super.eat();
  },
};

let longEar = {
  __proto__: rabbit,
  name: "Long Ear",
  eat() {
    // longEar.eat.[[HomeObject]] == longEar
    super.eat();
  },
};

// 正确执行
longEar.eat(); // Long Ear eats.

🚩 方法并不是“自由”的


* 函数通常都是“自由”的,并没有绑定到 JavaScript 中的对象。正因如此,它们可以在对象之间复制,并用另外一个 this 调用它。

- [[HomeObject]] 的存在违反了上述原则,因为方法记住了它们的对象

- [[HomeObject]] 不能被更改,所以这个绑定是永久的

- 在 JavaScript 语言中 [[HomeObject]] 仅被用于 super;所以,如果一个方法不使用 super,那么仍然可以视它为自由的并且可在对象之间复制;但是用了 super 再这样做可能就会出错

错误示范

let animal = {
  sayHi() {
    alert(`I'm an animal`);
  },
};

// rabbit 继承自 animal
let rabbit = {
  __proto__: animal,
  sayHi() {
    super.sayHi();
  },
};

let plant = {
  sayHi() {
    alert("I'm a plant");
  },
};

// tree 继承自 plant
let tree = {
  __proto__: plant,
  sayHi: rabbit.sayHi, // (*)
};

tree.sayHi(); // I'm an animal (?!?)

🚩 方法,不是函数属性


- [[HomeObject]] 是为类和普通对象中的方法定义的。但是对于对象而言,方法必须确切指定为 method(),而不是 "method: function()"


- 这个差别对开发者来说可能不重要,但是对 JavaScript 来说却非常重要

错误示范

let animal = {
  eat: function () {
    // 这里是故意这样写的,而不是 eat() {...
    // ...
  },
};

let rabbit = {
  __proto__: animal,
  eat: function () {
    super.eat();
  },
};

rabbit.eat(); // 错误调用 super(因为这里没有 [[HomeObject]])

静态属性和静态方法

🚩 静态方法


把一个方法赋值给类的函数本身,而不是赋给它的 "prototype"


这样的方法被称为 静态的(static)
class User {
  static staticMethod() {
    alert(this === User);
  }
}

User.staticMethod(); // true

// 和作为属性赋值的作用相同

class User {}

User.staticMethod = function () {
  alert(this === User);
};

User.staticMethod(); // true

静态方法被用于实现属于整个类的功能;它与具体的类实例无关

🚩 静态属性


静态属性类似静态方法
class Article {
  static publisher = "Levi Ding";
}

alert(Article.publisher); // Levi Ding

// 等同于直接给 Article 赋值:

Article.publisher = "Levi Ding";

静态属性被用于想要存储类级别的数据时,而不是绑定到实例

🚩 继承静态属性和方法


- 静态属性和方法是可被继承的

- 继承对常规方法和静态方法都有效
class Animal {
  static planet = "Earth";

  constructor(name, speed) {
    this.speed = speed;
    this.name = name;
  }

  run(speed = 0) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  static compare(animalA, animalB) {
    return animalA.speed - animalB.speed;
  }
}

// 继承于 Animal
class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }
}

let rabbits = [new Rabbit("White Rabbit", 10), new Rabbit("Black Rabbit", 5)];

rabbits.sort(Rabbit.compare);

rabbits[0].run(); // Black Rabbit runs with speed 5.

alert(Rabbit.planet); // Earth

它是如何工作的?再次,使用原型 😱。extends 让 Rabbit 的 [[Prototype]] 指向了 Animal


Rabbit extends Animal 创建了两个 [[Prototype]] 引用:

- 1. Rabbit 函数原型继承自 Animal 函数

- 2. Rabbit.prototype 原型继承自 Animal.prototype

校验

class Animal {}
class Rabbit extends Animal {}

// 对于静态的
alert(Rabbit.__proto__ === Animal); // true

// 对于常规方法
alert(Rabbit.prototype.__proto__ === Animal.prototype); // true

私有的和受保护的属性和方法

🚩 就面向对象编程(OOP)而言,内部接口与外部接口的划分被称为 [[封装|https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)]]


封装具有以下优点:

1. 保护用户,使他们不会误伤自己

如果一个 class 的使用者想要改变那些本不打算被从外部更改的东西 —— 后果是不可预测的


2. 可支持性

如果严格界定内部接口,那么这个 class 的开发人员可以自由地更改其内部属性和方法,甚至无需通知用户

对于用户来说,当新版本问世时,应用的内部可能被进行了全面检修,但如果外部接口相同,则仍然很容易升级


3. 隐藏复杂性

当实施细节被隐藏,并提供了简单且有据可查的外部接口时,总是很方便的

🚩 为了隐藏内部接口,JavaScript 使用受保护的或私有的属性


- 受保护的字段以 _ 开头;这是一个众所周知的约定,不是在语言级别强制执行的;程序员应该只通过它的类和从它继承的类中访问以 _ 开头的字段


- 私有字段以 # 开头;JavaScript 确保我们只能从类的内部访问它们

受保护

class CoffeeMachine {
  _waterAmount = 0;

  set waterAmount(value) {
    if (value < 0) throw new Error("Negative water");
    this._waterAmount = value;
  }

  get waterAmount() {
    return this._waterAmount;
  }

  constructor(power) {
    this._power = power;
  }
}

// 创建咖啡机
let coffeeMachine = new CoffeeMachine(100);

// 加水
coffeeMachine.waterAmount = -10; // Error: Negative water

// 受保护的属性通常以下划线 _ 作为前缀;一个众所周知的约定,即不应该从外部访问此类型的属性和方法

// 也可使用 get.../set... 函数

class CoffeeMachine {
  _waterAmount = 0;

  setWaterAmount(value) {
    if (value < 0) throw new Error("Negative water");
    this._waterAmount = value;
  }

  getWaterAmount() {
    return this._waterAmount;
  }
}

new CoffeeMachine().setWaterAmount(100);

// 函数更灵活(可以接受多个参数); ,get/set 语法更短

只读

class CoffeeMachine {
  // ...

  constructor(power) {
    this._power = power;
  }

  get power() {
    return this._power;
  }
}

// 创建咖啡机
let coffeeMachine = new CoffeeMachine(100);

alert(`Power is: ${coffeeMachine.power}W`); // 功率是:100W

coffeeMachine.power = 25; // Error(没有 setter)

// 只能被在创建时进行设置,之后不再被修改;只需要设置 getter,而不设置 setter

扩展内建类

🚩 内建的类,例如 Array,Map 等也都是可以扩展的(extendable)

// 给 PowerArray 新增了一个方法(可以增加更多)
class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

let filteredArr = arr.filter((item) => item >= 10);
alert(filteredArr); // 10, 50
alert(filteredArr.isEmpty()); // false

// 💡注意一个非常有趣的事儿!

// 内建的方法例如 filter,map 等 — 返回的正是子类 PowerArray 的新对象;它们内部使用了对象的 constructor 属性来实现这一功能

// arr.constructor === PowerArray

🚩 如果希望像 map 或 filter 这样的内建方法返回常规数组,可以在 Symbol.species 中返回 Array

class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }

  // 内建方法将使用这个作为 constructor
  static get [Symbol.species]() {
    return Array;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false

// filter 使用 arr.constructor[Symbol.species] 作为 constructor 创建新数组
let filteredArr = arr.filter((item) => item >= 10);

// filteredArr 不是 PowerArray,而是 Array
alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function

其他集合的工作方式类似;例如 Map 和 Set 的工作方式类似。它们也使用 Symbol.species;

🚩 内建类没有静态方法继承


- 内建对象有它们自己的静态方法,例如 Object.keys,Array.isArray 等

- 原生的类互相扩展,例如,Array 扩展自 Object

- 通常,当一个类扩展另一个类时,静态方法和非静态方法都会被继承

- 但内建类却是一个例外,它们相互间【不继承静态方法】

类型检查方法

🚩 类型检查方法


- typeof	原始数据类型;返回string


- {}.toString	原始数据类型,内建对象,包含 Symbol.toStringTag 属性的对象;返回string


- instanceof	对象;返回true/false

🚩instanceof 操作符

语法:

obj instanceof Class;

- 通常,instanceof 在检查中会将原型链考虑在内

- 此外,还可以在静态方法 Symbol.hasInstance 中设置自定义逻辑

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


// 1.如果这儿有静态方法 Symbol.hasInstance,那就直接调用这个方法

// 设置 instanceOf 检查
// 并假设具有 canEat 属性的都是 animal
class Animal {
  static [Symbol.hasInstance](obj) {
    if (obj.canEat) return true;
  }
}

let obj = { canEat: true };

alert(obj instanceof Animal); // true:Animal[Symbol.hasInstance](obj) 被调用


// 2. 大多数 class 没有 Symbol.hasInstance。在这种情况下,标准的逻辑是:使用 obj instanceOf Class 检查 Class.prototype 是否等于 obj 的原型链中的原型之一

obj.__proto__ === Class.prototype?
obj.__proto__.__proto__ === Class.prototype?
obj.__proto__.__proto__.__proto__ === Class.prototype?
...
// 如果任意一个的答案为 true,则返回 true
// 否则,如果我们已经检查到了原型链的尾端,则返回 false

🚩 objA.isPrototypeOf(objB)


- 如果 objA 处在 objB 的原型链中,则返回 true


- 可以将 obj instanceof Class 检查改为 Class.prototype.isPrototypeOf(obj)

🚩 福利:使用 Object.prototype.toString 方法来揭示类型


可以将Object.prototype.toString 方法作为 typeof 的增强版或者 instanceof 的替代方法来使用

按照 [[规范 |https://tc39.github.io/ecma262/#sec-object.prototype.tostring]]所讲,内建的 toString 方法可以被从对象中提取出来,并在任何其他值的上下文中执行。其结果取决于该值

// 方便起见,将 toString 方法复制到一个变量中
let objectToString = Object.prototype.toString;

// 它是什么类型的?
let arr = [];

alert(objectToString.call(arr)); // [object Array]

// 💡其结果取决于该值

// 对于 number 类型,结果是 [object Number]
// 对于 boolean 类型,结果是 [object Boolean]
// 对于 null:[object Null]
// 对于 undefined:[object Undefined]
// 对于数组:[object Array]
// ……等(可自定义)

// 💡Symbol.toStringTag

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

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

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

Mixin 模式


* Mixin — 是一个通用的面向对象编程术语:一个包含其他类的方法的类

* 一些其它编程语言允许多重继承。JavaScript 不支持多重继承,但是可以通过将方法拷贝到原型中来实现 mixin

🚩EventMixin : 可以使用 mixin 作为一种通过添加多种行为来扩充类的方法 例如:事件处理

let eventMixin = {
  /**
   * 订阅事件,用法:
   *  menu.on('select', function(item) { ... }
   */
  on(eventName, handler) {
    if (!this._eventHandlers) this._eventHandlers = {};
    if (!this._eventHandlers[eventName]) {
      this._eventHandlers[eventName] = [];
    }
    this._eventHandlers[eventName].push(handler);
  },

  /**
   * 取消订阅,用法:
   *  menu.off('select', handler)
   */
  off(eventName, handler) {
    let handlers = this._eventHandlers?.[eventName];
    if (!handlers) return;
    for (let i = 0; i < handlers.length; i++) {
      if (handlers[i] === handler) {
        handlers.splice(i--, 1);
      }
    }
  },

  /**
   * 生成具有给定名称和数据的事件
   *  this.trigger('select', data1, data2);
   */
  trigger(eventName, ...args) {
    if (!this._eventHandlers?.[eventName]) {
      return; // 该事件名称没有对应的事件处理程序(handler)
    }

    // 调用事件处理程序(handler)
    this._eventHandlers[eventName].forEach((handler) =>
      handler.apply(this, args)
    );
  },
};

用法:

// 创建一个 class
class Menu {
  choose(value) {
    this.trigger("select", value);
  }
}
// 添加带有事件相关方法的 mixin
Object.assign(Menu.prototype, eventMixin);

let menu = new Menu();

// 添加一个事件处理程序(handler),在被选择时被调用:
menu.on("select", (value) => alert(`Value selected: ${value}`));

// 触发事件 => 运行上述的事件处理程序(handler)并显示:
// 被选中的值:123
menu.choose("123");

错误处理,"try..catch"


- 通常,如果发生错误,脚本就会“死亡”(立即停止),并在控制台将错误打印出来。

- 但是有一种语法结构 try..catch,它可以“捕获(catch)”错误,因此脚本可以执行更合理的操作,而不是死掉

🚩 语法

try {
  // 执行此处代码
} catch (err) {
  // 如果发生错误,跳转至此处
  // err 是一个 error 对象
} finally {
  // 无论怎样都会在 try/catch 之后执行
}

- 可能会没有 catch 部分或者没有 finally,所以 try..catch 或 try..finally 都是可用的

Error 对象包含下列属性:

- message — 人类可读的 error 信息

- name — 具有 error 名称的字符串(Error 构造器的名称)

- stack(没有标准,但得到了很好的支持)— Error 发生时的调用栈

- 如果不需要 error 对象,可以通过使用 catch { 而不是 catch(err) { 来省略它

- 可以使用 throw 操作符来生成自定义的 error。从技术上讲,throw 的参数可以是任何东西,但通常是继承自内建的 Error 类的 error 对象

🚩try..catch 仅对运行时的 error 有效

// 在“计划的(scheduled)”代码中发生异常,则 try..catch 不会捕获到异常,例如在 setTimeout 中

try {
  setTimeout(function () {
    noSuchVariable; // 脚本将在这里停止运行,函数本身要稍后才执行,这时引擎已经离开了 try..catch 结构
  }, 1000);
} catch (e) {
  alert("won't work");
}

// 为了捕获到计划的(scheduled)函数中的异常,那么 try..catch 必须在这个函数内

setTimeout(function () {
  try {
    noSuchVariable; // try..catch 处理 error 了!
  } catch {
    alert("error is caught here!");
  }
}, 1000);

🚩 变量和 try..catch..finally 中的局部变量


- 如果使用 let 在 try 块中声明变量,那么该变量将只在 try 块中可见

🚩finally 和 return


- finally 子句适用于 try..catch 的 任何 出口,包括显式的 return
function func() {
  try {
    return 1;
  } catch (e) {
    /* ... */
  } finally {
    alert("finally"); // finally 会在控制转向外部代码前被执行
  }
}

alert(func()); // 先执行 finally 中的 alert,然后执行这个 alert

🚩 再次抛出(rethrowing)是一种错误处理的重要模式:catch 块通常期望并知道如何处理特定的 error 类型,因此它应该再次抛出它不知道的 error

// catch 应该只处理它知道的 error,并“抛出”所有其他 error

// “再次抛出(rethrowing)”技术可以被更详细地解释为:

// 1.Catch 捕获所有 error

// 2.在 catch(err) {...} 块中,对 error 对象 err 进行分析

// 3.如果不知道如何处理它,那就 throw err

// ...

// 在下面的代码中,使用“再次抛出”,以达到在 catch 中只处理 SyntaxError 的目的:

let json = '{ "age": 30 }'; // 不完整的数据
try {
  let user = JSON.parse(json);

  if (!user.name) {
    throw new SyntaxError("Incomplete data: no name");
  }

  blabla(); // 预料之外的 error

  alert(user.name);
} catch (e) {
  if (e instanceof SyntaxError) {
    // 可以使用 instanceof 操作符判断错误类型;还可以从 err.name 属性中获取错误的类名,所有原生的错误都有这个属性;另一种方式是读取 err.constructor.name
    alert("JSON Error: " + e.message);
  } else {
    throw e; // 再次抛出 (*)
  }
}

🚩 全局 catch:即使我们没有 try..catch,大多数执行环境也允许我们设置“全局”错误处理程序来捕获“掉出(fall out)”的 error。在浏览器中,就是 window.onerror

// 如果在 try..catch 结构外有一个致命的 error,然后脚本死亡了!有什么办法可以用来应对这种情况吗?可能想要记录这个 error,并向用户显示某些内容(通常用户看不到错误信息)等

// 规范中没有相关内容,但是代码的执行环境一般会提供这种机制,因为它确实很有用。例如,Node.JS 有 process.on("uncaughtException")。在浏览器中,可以将将一个函数赋值给特殊的 window.onerror 属性,该函数将在发生未捕获的 error 时执行

window.onerror = function (message, url, line, col, error) {
  // ...
};

- 全局错误处理程序 window.onerror 的作用通常不是恢复脚本的执行 — 如果发生编程错误,那这几乎是不可能的,它的作用是将错误信息发送给开发者

- 异常监控:有针对这种情况提供错误日志的 Web 服务,例如 https://errorception.com 或 http://www.muscula.com

回调

🚩 异步 行为(action):现在开始执行的行为,但它们会在稍后完成;例如,setTimeout 函数就是一个这样的函数;例如加载脚本和模块

实际中的异步行为的示例:

/**
 * 使用给定的 src 加载脚本
 * @param src
 **/
function loadScript(src) {
  // 创建一个 <script> 标签,并将其附加到页面
  // 这将使得具有给定 src 的脚本开始加载,并在加载完成后运行
  let script = document.createElement("script");
  script.src = src;
  document.head.append(script);
}

可以像这样使用这个函数:

// 在给定路径下加载并执行脚本
loadScript("/my/script.js");

// loadScript 下面的代码
// 不会等到脚本加载完成才执行
// ...

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

假设需要在新脚本加载后立即使用它,这将不会有效:

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

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

😭 到目前为止,loadScript 函数并没有提供跟踪加载完成的方法。脚本加载并最终运行,仅此而已。但是希望了解脚本何时加载完成,以使用其中的新函数和变量

💡 添加一个 callback 函数作为 loadScript 的第二个参数,该函数应在脚本加载完成时执行:

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(`Cool, the script ${script.src} is loaded`);
    alert(_); // 所加载的脚本中声明的函数
  }
);

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

🚩 回调地狱

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

loadScript("/my/script.js", function (script) {
  loadScript("/my/script2.js", function (script) {
    loadScript("/my/script3.js", function (script) {
      // ...加载完所有脚本后继续
    });
  });
});

加入处理 Error:

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 {
            // ...加载完所有脚本后继续 (*)
          }
        });
      }
    });
  }
});

这就是著名的“''回调地狱''”或“厄运金字塔”

💡 可以通过使每个行为都成为一个独立的函数来尝试减轻这种问题

loadScript("1.js", step1);

function step1(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript("2.js", step2);
  }
}

function step2(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...
    loadScript("3.js", step3);
  }
}

function step3(error, script) {
  if (error) {
    handleError(error);
  } else {
    // ...加载完所有脚本后继续 (*)
  }
}

优缺点


- 没有深层的嵌套,独立为顶层函数

- 可读性差

- 没有重用

最好的方法之一就是 “''promise''

Promise

🚩 语法

let promise = new Promise(function (resolve, reject) {
  // executor
  // 当 promise 被构造完成时,executor自动执行此函数
  // executor 通常是异步任务
  // ...
})
  // handler
  .then(
    (result) => {
      // ...
    },
    (error) => {
      // ...
    }
  );

1.当 new Promise 被创建,executor 被自动且立即调用

2.由 new Promise 构造器返回的 promise 对象具有以下【内部属性】

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

    - result — 最初是 undefined,然后在 resolve(value) 被调用时变为 value,或者在 reject(error) 被调用时变为 error

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

4.executor 只能调用一个 resolve 或一个 reject;任何状态的更改都是最终的(不可逆)

🚩 立即 resolve/reject 的 Promise

// executor 通常是异步执行某些操作,并在一段时间后调用 resolve/reject,但这不是必须的;还可以立即调用 resolve 或 reject

// 💡当开始做一个任务时,但随后看到一切都已经完成并已被缓存时,可能就会发生这种情况。这挺好😀

let promise = new Promise(function (resolve, reject) {
  // 不花时间去做这项工作
  resolve(123); // 立即给出结果:123
});

🚩 示例:加载脚本的 loadScript 函数

基于回调函数的变体版本:


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('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
  // 在脚本加载完成后,回调函数才会执行
  alert(`${script.src} is loaded!`);
  alert( _ ); // 所加载的脚本中声明的函数
});

基于 Promise 重写的版本:

function loadScript(src) {
  return new Promise(function (resolve, reject) {
    let script = document.createElement("script");
    script.src = src;

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

    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 链:回忆回调中,何依次加载两个脚本:第一个,然后是第二个?第三个?

// 💡Promise 提供了一些方案来做到这一点:Promise 链

// like this

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;
  });

// 📌为什么可以?因为对 promise.then 的调用会返回了一个 promise,所以我们可以在其之上调用下一个 .then

// 当处理程序(handler)返回一个值时,它将成为该 promise 的 result,所以将使用它调用下一个 .then

// 💣''新手常犯的一个经典错误:从技术上讲,我们也可以将多个 .then 添加到一个 promise 上。但这并不是 promise 链(chaining)''

let promise = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(1), 1000);
});

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

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

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

// 💡这里所做的只是一个 promise 的几个处理程序(handler)。它们不会相互传递 result;相反,它们之间彼此独立运行处理任务

🚩 返回 promise


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

- 在这种情况下,其他的处理程序(handler)将【等待它 settled 后再获得其结果(result)】

示例:promise 化的 loadScript

loadScript("/article/promise-chaining/one.js")
  .then((script) => loadScript("/article/promise-chaining/two.js"))
  .then((script) => loadScript("/article/promise-chaining/three.js"))
  .then((script) => {
    // 脚本加载完成,我们可以在这儿使用脚本中声明的函数
    one();
    two();
    three();
  });

// 💡注意:这儿每个 loadScript 调用都返回一个 promise,并且在它 resolve 时下一个 .then 开始运行。然后,它启动下一个脚本的加载。所以,脚本是一个接一个地加载的

// 💡并且代码仍然是“扁平”的 — 它向下增长,而不是向右

// ...

// 从技术上讲,可以向每个 loadScript 直接添加 .then,就像这样:

loadScript("/article/promise-chaining/one.js").then((script1) => {
  loadScript("/article/promise-chaining/two.js").then((script2) => {
    loadScript("/article/promise-chaining/three.js").then((script3) => {
      // 此函数可以访问变量 script1,script2 和 script3
      one();
      two();
      three();
    });
  });
});

// 💡这段代码做了相同的事儿:按顺序加载 3 个脚本。但它是“向右增长”的。所以会有和使用回调函数一样的问题

// 👍刚开始使用 promise 的人可能不知道 promise 链,所以他们就这样写了。通常,链式是首选

🚩Thenables


- 确切地说,处理程序(handler)返回的不完全是一个 promise,而是返回的被称为 “thenable” 对象 — 一个具有方法 .then 的任意对象

- thenable对象会被当做一个 promise 来对待

- 这个想法是,第三方库可以实现自己的“promise 兼容(promise-compatible)”对象;它们可以具有扩展的方法集,但也与原生的 promise 兼容,因为它们实现了 .then 方法


- 这个特性允许将自定义的对象与 promise 链集成在一起,而不必继承自 Promise

示例:

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve); // function() { native code }
    // 1 秒后使用 this.num*2 进行 resolve
    setTimeout(() => resolve(this.num * 2), 1000); // (**)
  }
}

new Promise((resolve) => resolve(1))
  .then((result) => {
    return new Thenable(result); // (*)
  })
  .then(alert); // 1000ms 后显示 2

🚩 作为一个好的做法:异步行为应该始终返回一个 promise


- 这样就可以使得之后计划后续的行为成为可能

- 即使现在不打算对链进行扩展,但之后可能会需要

示例:

function loadJson(url) {
  return fetch(url).then((response) => response.json());
}

function loadGithubUser(name) {
  return fetch(`https://api.github.com/users/${name}`).then((response) =>
    response.json()
  );
}

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}`));
// ...

使用 promise 进行错误处理

🚩Promise 链在错误(error)处理


- 当一个 promise 被 reject 时,控制权将移交至最近的 rejection 处理程序(handler);这在实际开发中非常方便

- .catch 不必是立即的;它可能在一个或多个 .then 之后出现

示例:

fetch("/article/promise-chaining/user.json")
  .then((response) => response.json())
  .then((user) => fetch(`https://api.github.com/users/${user.name}`))
  .then((response) => response.json())
  .then(
    (githubUser) =>
      new Promise((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);
      })
  )
  .catch((error) => alert(error.message));

🚩 隐式 try…catch


- Promise 的执行者(executor)和 promise 的处理程序(handler)周围有一个“隐式的 try..catch”

- 如果发生异常,它(译注:指异常)就会被捕获,并被视为 rejection 进行处理

示例:

// excutor 中

new Promise((resolve, reject) => {
  throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!

// 等同于

new Promise((resolve, reject) => {
  reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!

// ...

// handler 中

new Promise((resolve, reject) => {
  resolve("ok");
})
  .then((result) => {
    throw new Error("Whoops!"); // reject 这个 promise
  })
  .catch(alert); // Error: Whoops!

🚩 再次抛出(Rethrowing)


- 如果在 .catch 中 throw,那么控制权就会被移交到下一个最近的 error 处理程序(handler)。如果处理该 error 并正常完成,那么它将继续到最近的成功的 .then 处理程序(handler)
// 执行流:catch -> then
new Promise((resolve, reject) => {
  throw new Error("Whoops!");
})
  .catch(function (error) {
    alert("The error is handled, continue normally");
  })
  .then(() => alert("Next successful handler runs"));
// 执行流:catch -> catch
new Promise((resolve, reject) => {
  throw new Error("Whoops!");
})
  .catch(function (error) {
    // (*)

    if (error instanceof URIError) {
      // 处理它
    } else {
      alert("Can't handle such error");

      throw error; // 再次抛出此 error 或另外一个 error,执行将跳转至下一个 catch
    }
  })
  .then(function () {
    /* 不在这里运行 */
  })
  .catch((error) => {
    // (**)

    alert(`The unknown error has occurred: ${error}`);
    // 不会返回任何内容 => 执行正常进行
  });

🚩 未处理的 rejection

new Promise(function () {
  noSuchFunction(); // 这里出现 error(没有这个函数)
}).then(() => {
  // 一个或多个成功的 promise 处理程序(handler)
}); // 尾端没有 .catch!

// ...

// 当一个 error 没有被处理会发生什么?

// 💡如果出现 error,promise 的状态将变为 “rejected”,然后执行应该跳转至最近的 rejection 处理程序(handler)。但是上面这个例子中并没有这样的处理程序(handler)。因此 error 会“卡住(stuck)”。没有代码来处理它

// 在实际开发中,就像代码中常规的未处理的 error 一样,这意味着某些东西出了问题

// 当发生一个常规的错误(error)并且未被 try..catch 捕获时会发生什么?脚本死了,并在控制台(console)中留下了一个信息。对于在 promise 中未被处理的 rejection,也会发生类似的事儿

JavaScript 引擎会跟踪此类 rejection,在这种情况下会生成一个全局的 error


- 在浏览器中,可以使用 unhandledrejection 事件来捕获这类 error
window.addEventListener("unhandledrejection", function (event) {
  // 这个事件对象有两个特殊的属性:
  alert(event.promise); // [object Promise] - 生成该全局 error 的 promise
  alert(event.reason); // Error: Whoops! - 未处理的 error 对象
});

new Promise(function () {
  throw new Error("Whoops!");
}); // 没有用来处理 error 的 catch

Promise API


在 Promise 类中,有 5 种静态方法

- Promise.all([iterable])

- Promise.allSettled([iterable])

- Promise.race([iterable])

- Promise.resolve()

- Promise.reject()

🚩Promise.all

语法

// 接受一个 promise 数组(可以是任何可迭代的)作为参数并返回一个新的 promise

let promise = Promise.all([iterable]);

注意


- 并行执行多个 promise,当所有给定的 promise 都被 成功 时,新的 promise 才会 resolve,并且其结果数组将成为新的 promise 的结果

- 结果数组中元素的顺序与其在源 promise 中的顺序相同(即使第一个 promise 花费了最长的时间)

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

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


- 如果其中一个 promise 被 reject,Promise.all 就会立即被 reject,完全忽略列表中其他的 promise。它们的结果也被忽略

- 例如,如果有多个同时进行的 fetch 调用,其中一个失败,其他的 fetch 操作仍然会继续执行,但是 Promise.all 将不会再关心(watch)它们。它们可能会 settle,但是它们的结果将被忽略

- Promise.all 没有采取任何措施来取消它们,因为 promise 中没有“取消”的概念

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

// romise.all(...) 接受含有 promise 项的可迭代对象(大多数情况下是数组)作为参数。但是,如果这些对象中的任何一个不是 promise,那么它将被“按原样”传递给结果数组

Promise.all([
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000);
  }),
  2,
  3,
]).then(alert); // 1, 2, 3

🚩Promise.allSettled


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

- {status:"fulfilled", value:result} 对于成功的响应

- {status:"rejected", reason:error} 对于 error

Polyfill

if (!Promise.allSettled) {
  const rejectHandler = (reason) => ({ status: "rejected", reason });

  const resolveHandler = (value) => ({ status: "fulfilled", value });

  Promise.allSettled = function (promises) {
    const convertedPromises = promises.map((p) =>
      Promise.resolve(p).then(resolveHandler, rejectHandler)
    );
    return Promise.all(convertedPromises);
  };
}

🚩Promise.race


- 只等待第一个 settled 的 promise 并获取其结果(或 error)

示例

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.resolve/reject

语法

// 结果 value 创建一个 resolved 的 promise
Promise.resolve(value);

// 等同于

let promise = new Promise((resolve) => resolve(value));

//...

// Promise.reject() 类似

- 当一个函数被期望返回一个 promise 时,这个方法用于兼容性

- 💡这里的兼容性是指,直接从缓存中获取了当前操作的结果 value,但是期望返回的是一个 promise,所以可以使用 Promise.resolve(value) 将 value “封装”进 promise,以满足期望返回一个 promise 的这个需求

示例:

let cache = new Map();

function loadCached(url) {
  if (cache.has(url)) {
    return Promise.resolve(cache.get(url)); // (*)
  }

  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      cache.set(url, text);
      return text;
    });
}

// 💡可以使用 loadCached(url).then(…),因为该函数保证了会返回一个 promise。可以放心地在 loadCached 后面使用 .then。这就是 (*) 行中 Promise.resolve 的目的

Promisification


- “Promisification” 指将一个接受回调的函数转换为一个返回 promise 的函数

- 由于许多函数和库都是基于回调的,所以将基于回调的函数和库 promisify 是有意义的

示例:

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('path/script.js', (err, script) => {...})

// ...

// promisify

let loadScriptPromise = function (src) {
  return new Promise((resolve, reject) => {
    loadScript(src, (err, script) => {
      if (err) reject(err);
      else resolve(script);
    });
  });
};

// 用法:
// loadScriptPromise('path/script.js').then(...)

新的函数是对原始的 loadScript 函数的包装,在实际开发中,可能需要 promisify 很多函数

🚩promisify


function promisify(f) {
  return function (...args) { // 返回一个包装函数(wrapper-function) (*)
    return new Promise((resolve, reject) => {
      function callback(err, result) { // 对 f 的自定义的回调 (**)
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }
      }

      args.push(callback); // 将自定义的回调附加到 f 参数(arguments)的末尾

      f.call(this, ...args); // 调用原始的函数
    });
  };
}

// 用法:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);

🚩promisification 函数的模块(module)


- https://github.com/digitaldesignlabs/es6-promisify

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

🚩Promisification 场景


- Promisification 不是回调的完全替代

- 请记住,一个 promise 可能只有一个结果,但从技术上讲,一个回调可能被调用很多次

- 因此,promisification 仅适用于调用一次回调的函数。进一步的调用将被忽略

微任务(Microtask)


- Promise 处理始终是异步的,因此,.then/catch/finally 处理程序(handler)总是在当前代码完成后才会被调用

- 所有 promise 行为都会通过内部的 “promise jobs” 队列,也被称为“微任务队列”(ES8 术语

- 如果需要确保一段代码在 .then/catch/finally 之后被执行,可以将它添加到链式调用的 .then 中

async/await


- async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用

- async/await两个关键字一起提供了一个很好的用来编写异步代码的框架,这种代码易于阅读也易于编写

- 有了 async/await 之后,就几乎不需要使用 promise.then/catch,但是不要忘了它们是基于 promise 的,因为有些时候(例如在最外层作用域)不得不使用这些方法

🚩 async 有两个作用


1.让这个函数总是返回一个 promise;其他值将自动被包装在一个 resolved 的 promise 中

2.允许在该函数内使用 await

🚩await


Promise 前的关键字 await 使 JavaScript 引擎等待该 promise settle,然后:

- 如果有 error,就会抛出异常 — 就像那里调用了 throw error 一样

- 否则,就返回结果

示例

async function f() {
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("done!"), 1000);
  });

  let result = await promise; // 等待,直到 promise resolve (*)

  alert(result); // "done!"
}

f();

// 💡(*) 那一行:await 实际上会暂停函数的执行,直到 promise 状态变为 settled,然后以 promise 的结果继续执行

🚩await 不能在顶层代码运行

// 用在顶层代码中会报语法错误
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();

// ...

// 但可以将其包裹在一个匿名 async 函数中,如下所示:

(async () => {
  let response = await fetch('/article/promise-chaining/user.json');
  let user = await response.json();
  ...
})();

🚩await 接受 “thenables”

// 💡像 promise.then 那样,await 允许使用 thenable 对象(那些具有可调用的 then 方法的对象)。这里的想法是,第三方对象可能不是一个 promise,但却是 promise 兼容的:如果这些对象支持 .then,那么就可以对它们使用 await

class Thenable {
  constructor(num) {
    this.num = num;
  }
  then(resolve, reject) {
    alert(resolve);
    // 1000ms 后使用 this.num*2 进行 resolve
    setTimeout(() => resolve(this.num * 2), 1000); // (*)
  }
}

async function f() {
  // 等待 1 秒,之后 result 变为 2
  let result = await new Thenable(1);
  alert(result);
}

f();

// 如果 await 接收了一个非 promise 的但是提供了 .then 方法的对象,它就会调用这个 .then 方法,并将内建的函数 resolve 和 reject 作为参数传入(就像它对待一个常规的 Promise executor 时一样)。然后 await 等待直到这两个函数中的某个被调用(在上面这个例子中发生在 (*) 行),然后使用得到的结果继续执行后续任务

🚩Error 处理

// 如果一个 promise 正常 resolve,await promise 返回的就是其结果

// 但是如果 promise 被 reject,它将 throw 这个 error,就像在这一行有一个 throw 语句那样

async function f() {
  await Promise.reject(new Error("Whoops!"));
}

// 等同于

async function f() {
  throw new Error("Whoops!");
}

// 👍因此,可以用 try..catch 来捕获上面提到的那个 error,与常规的 throw 使用的是一样的方式

async function f() {
  try {
    let response = await fetch("http://no-such-url");
  } catch (err) {
    alert(err); // TypeError: failed to fetch
  }
}

f();

// ...

// 👌如果没有 try..catch,那么由异步函数 f() 的调用生成的 promise 将变为 rejected;可以在函数调用后面添加 .catch 来处理这个 error:

async function f() {
  let response = await fetch("http://no-such-url");
}

// f() 变成了一个 rejected 的 promise
f().catch(alert); // TypeError: failed to fetch // (*)

Generator


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

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

- 可与 iterable 完美配合使用,从而可以轻松地创建数据流

🚩Generator 函数


generator 的主要方法就是 next()

- 当被调用时,执行直到最近的 yield <value> 语句(value 可以被省略,默认为 undefined)

- 然后函数执行暂停,并将产出的(yielded)值返回到外部代码

示例:

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

let generator = generateSequence();

let one = generator.next();

alert(JSON.stringify(one)); // {value: 1, done: false}

let two = generator.next();

alert(JSON.stringify(two)); // {value: 2, done: false}

let three = generator.next();

alert(JSON.stringify(three)); // {value: 3, done: true}

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

- value: 产出的(yielded)的值

- done: 如果 generator 函数已执行完成则为 true,否则为 false

🚩Generator 是可迭代的


- generator 具有 next() 方法, 因此generator 是 可迭代(iterable)的

- 因此可以使用 iterator 的所有相关功能,例如:spread 语法 ...

💡next() 是 iterator 的必要方法(可以使用 for..of 循环遍历)

示例:

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

let generator = generateSequence();

for (let value of generator) {
  alert(value); // 1,然后是 2
}

🚩 使用 generator 进行迭代

// 👉非generator 函数实现 Symbol.iterator

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

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

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

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

// ...

// 👉generator 函数实现 Symbol.iterator

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

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

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

🚩Generator 组合(composition)


- 将一个 generator 流插入到另一个 generator 流的自然的方式

示例:成一个更复杂的序列:首先是数字 0..9(字符代码为 48…57),接下来是大写字母 A..Z(字符代码为 65…90),接下来是小写字母 a...z(字符代码为 97…122)

// 👉generator composition

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generatePasswordCodes() {
  // 0..9
  yield* generateSequence(48, 57);

  // A..Z
  yield* generateSequence(65, 90);

  // a..z
  yield* generateSequence(97, 122);
}

let str = "";

for (let code of generatePasswordCodes()) {
  str += String.fromCharCode(code);
}

alert(str); // 0..9A..Za..z

// 👉等同于

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generateAlphaNum() {
  // yield* generateSequence(48, 57);
  for (let i = 48; i <= 57; i++) yield i;

  // yield* generateSequence(65, 90);
  for (let i = 65; i <= 90; i++) yield i;

  // yield* generateSequence(97, 122);
  for (let i = 97; i <= 122; i++) yield i;
}

let str = "";

for (let code of generateAlphaNum()) {
  str += String.fromCharCode(code);
}

alert(str); // 0..9A..Za..z

🚩“yield” 是一条双向路


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

示例:

function* gen() {
  // 向外部代码传递一个问题并等待答案
  let result = yield "2 + 2 = ?"; // (*)

  alert(result);
}

let generator = gen();

let question = generator.next().value; // <-- yield 返回的 value

generator.next(4); // --> 将结果传递到 generator 中

// 1.第一次调用 generator.next() 应该是不带参数的(如果带参数,那么该参数会被忽略)。它开始执行并返回第一个 yield "2 + 2 = ?" 的结果。此时,generator 执行暂停,而停留在 (*) 行上

// 然后,yield 的结果进入调用代码中的 question 变量

// 在 generator.next(4),generator 恢复执行,并获得了 4 作为结果:let result = 4

异步迭代 和 异步 generator

🚩 异步可迭代对象

// 👉可迭代的 range 的一个实现

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

  [Symbol.iterator]() {
    // 在 for..of 循环开始时被调用一次
    return {
      current: this.from,
      last: this.to,

      next() {
        // 每次迭代时都会被调用,来获取下一个值
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      },
    };
  },
};

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

// 👉异步可迭代的 range 的一个实现

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

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

      async next() {
        // (2)

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

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

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

// 💡

// 使一个对象可以异步迭代,它必须具有方法 【Symbol.asyncIterator 】(1)

// 这个方法必须返回一个带有 next() 方法的对象,next() 方法会【返回一个 promise】 (2)

// 这个 next() 方法可以不是 async 的,它可以是一个返回值是一个 promise 的常规的方法,但是使用 async 关键字可以允许在方法内部使用 await,所以会更加方便

// 使用【 for await(let value of range) 】(4) 来进行迭代,也就是在 for 后面添加 await。它会调用一次 range[Symbol.asyncIterator]() 方法一次,然后调用它的 next() 方法获取值

🚩 异步可迭代对象 Spread 语法 ... 无法异步工作


- 这很正常,因为它期望找到 Symbol.iterator,而不是 Symbol.asyncIterator

- for..of 的情况和这个一样:没有 await 关键字时,则期望找到的是 Symbol.iterator

🚩 异步 generator

// 👉可迭代的 range 的 generate 的 一个实现

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

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

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

// 👉可迭代的 range 的 generate异步 的 一个实现

async function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) {
    // 哇,可以使用 await 了!
    await new Promise((resolve) => setTimeout(resolve, 1000));

    yield i;
  }
}

(async () => {
  let generator = generateSequence(1, 5);
  for await (let value of generator) {
    alert(value); // 1,然后 2,然后 3,然后 4,然后 5(在每个 alert 之间有延迟)
  }
})();

// 💡

// 在一个常规的 generator 中,使用 result = generator.next() 来获得值

// 但在一个异步 generator 中,应该添加 await 关键字,像这样:

result = await generator.next(); // result = {value: ..., done: true/false}

// 💡 这就是为什么异步 generator 可以与 for await...of 一起工作

模块 (Module)

🚩 起源


- 随着项目越来越大,需要将其拆成多个文件,即模块(module)(包含特定目的的类或者函数库)

很长一段时间,JavaScript 都没有语言级(language-level)的模块语法,但随着脚本越来越复杂,因此社区发明了许多种方法来将代码组织到模块中,使用特殊的库按需加载模块

- AMD

    - https://en.wikipedia.org/wiki/Asynchronous_module_definition

    - 最初由 require.js 库实现

- CommonJS

    - http://wiki.commonjs.org/wiki/Modules/1.1

    -  为 Node.js 服务器创建的模块系统

- UMD

    - https://github.com/umdjs/umd

    - 与 AMD 和 CommonJS 都兼容



- ''`语言级的模块系统在 2015 年的时候出现在了标准(ES6)中`''

🚩 模块的核心概念


一个模块就是一个文件,览器需要使用 <script type="module">

与常规脚本相比(<script src="xx">),拥有 type="module" 标识的脚本有一些特定于浏览器的差异:

- 默认是延迟解析的(deferred)

- Async 可用于内联脚本(对于非模块脚本,async 特性(attribute)仅适用于外部脚本(异步脚本会在准备好后立即运行,独立于其他脚本或 HTML 文档),对于模块脚本,也适用于内联脚本)

- 从另一个源(域/协议/端口)加载外部脚本,需要 CORS header

- 重复的外部脚本会被忽略

模块具有自己的本地顶级作用域,并可以通过 import/export 交换功能

- “this” 是 undefined

模块始终使用 use strict

模块代码只执行一次。导出仅创建一次,然后会在导入之间共享

在生产环境中,出于性能和其他原因,开发者经常使用诸如 Webpack 之类的打包工具将模块打包到一起

导出和导入


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

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

🚩 export 导出类型


// 💡在声明一个 class/function/… 之前:
export [default] class/function/variable ...

// 💡独立的导出:
export {x [as y], ...}.

// 💡重新导出:
export {x [as y], ...} from "module"
export * from "module"(不会重新导出默认的导出)。
export {default [as y]} from "module"(重新导出默认的导出)

🚩 import 导入类型


// 💡模块中命名的导出:
import {x [as y], ...} from "module"

// 💡默认的导出:
import x from "module"
import {default as x} from "module"

// 💡所有:
import * as obj from "module"

// 💡导入模块(它的代码,并运行),但不要将其赋值给变量:
import "module"

动态导入

🚩 静态导入/导出


// 💡模块路径必须是原始类型字符串,不能是函数调用
import ... from getModuleName(); // Error, only from "string" is allowed

// 💡无法根据条件
if(...) {
  import ...; // Error, not allowed!
}

// 💡无法在运行时导入
{
  import ...; // Error, we can't put import in any block
}

export / import 语法严格且简单:只提供结构主干

- 便于分析代码结构

- 可以收集模块

- 可以使用特殊工具将收集的模块打包到一个文件中

- 可以删除未使用的导出(“tree-shaken”)

🚩import() 表达式

import(module);

// 返回一个 promise,该 promise resolve 为一个包含其所有导出的模块对象

let modulePath = prompt("Which module to load?");

import(modulePath)
  .then(obj => <module object>)
  .catch(err => <loading error, e.g. if no such module>)

// or

let module = await import(modulePath)

let xx = module .default;// 可以使用模块对象的 default 属性

动态导入在常规脚本中工作时,不需要 script type="module".

😱尽管 import() 看起来像一个函数调用,但它只是一种特殊语法,只是恰好使用了括号(类似于 super())

😱因此,不能将 import 复制到一个变量中,或者对其使用 call/apply

😱因为它不是一个函数

Eval:执行代码字符串

let result = eval(code);

内建函数 eval 允许执行一个代码字符串

🚩eval 的结果是最后一条语句的结果

let value = eval("let i = 0; ++i");
alert(value); // 1

🚩eval 内的代码在当前词法环境(lexical environment)中执行,因此它能访问外部变量

let a = 1;

function f() {
  let a = 2;

  eval("alert(a)"); // 2
}

f();

// 严格模式下,eval 有属于自己的词法环境,如果不启用严格模式,eval 没有属于自己的词法环境

eval("let x = 5; function f() {}");

alert(typeof x); // undefined(没有这个变量)
// 函数 f 也不可从外部进行访问

柯里化(Currying)



柯里化是一种函数的转换:

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

- 柯里化不会调用函数,只是对函数进行转换

🚩 创建一个辅助函数 curry(f)

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

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

let curriedSum = curry(sum);

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

// 💡实现非常简单:只有两个包装器(wrapper)

🚩 柯里化?目的是什么?


轻松地生成偏函数

🚩 高级柯里化实现

function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) {
      // 如果传入的 args 长度与原始函数所定义的(func.length)相同或者更长,那么只需要将调用传递给它即可
      return func.apply(this, args);
    } else {
      // 否则,获取一个偏函数,func 还没有被调用;返回另一个包装器 ,它将重新应用 curried,将之前传入的参数与新的参数一起传入;在一个新的调用中,再次,将获得一个新的偏函数(如果参数不足的话),或者最终的结果
      return function (...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

// 使用

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

let curriedSum = curry(sum);

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

柯里化要求函数具有固定数量的参数,f(...args),不能以这种方式进行柯里化

Reference Type


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

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

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

- 对于所有其它操作,Reference Type 会自动变成属性的值

BigInt

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

const bigint = 1234567890123456789012345678901234567890n;

const sameBigint = BigInt("1234567890123456789012345678901234567890");

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

- 不可以把 bigint 和常规数字类型混合使用,应该显式地转换

- 对 bigint 和 number 类型的数字进行比较没有问题

- == 比较时相等,但在进行 ===(严格相等)比较时不相等

- 除法 向下整除

- BigInt 不支持一元加法

- bigint 0n 为假,其他值为 true

🚩Polyfill

https://github.com/GoogleChromeLabs/jsbi

作者:shanejix
出处:https://www.shanejix.com/posts/现代 JavaScript 教程 — JavaScript 编程语言篇/
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
声明:转载请注明出处!

posted @ 2021-09-02 22:27  shanejix  阅读(327)  评论(0编辑  收藏  举报