回顾经典-读《JavaScript高级程序设计》


highlight: a11y-dark
theme: smartblue

Matt Frisbie 马特.弗里斯比(美)

0.jpg

一句话总结

本书分为三大部分:ES、BOM、DOM,本文是对 ES 部分的详细梳理。
基础知识永不过时,立足于基础、完备的知识体系才能走到更稳。

脑图

1.jpg

详情

第1章:什么是 JavaScript

本章主要讲了 JavaScript 的一些历史
JavaScript包含三个部分:ECMAScript(核心)、DOM(文档对象模型)、BOM(浏览器对象模型)

  • ECMAScript:即 ECMA-262的定义,他描述了这本语言的基础(语法、类型、语句、关键字、保留字、操作符、全局对象)
  • 注意:JavaScript 只是实现了 ECMAScript 的定义,同样实现的还有 Adobe ActionScript等

第2章:HTML 中的 JavaScript

2.1 script 元素

  • 将 JS 插入到 HTML 中的主要方式是使用 script 元素,这个元素是由网景公司创造出来的,这个元素有以下 8 个属性
    • async: 立即下载,延后执行(不会阻塞 Dom)
    • charset: 很少使用,大部分浏览器不会在意他的值,表示 src 资源的字符集
    • crossorigin:配置相关的 CORS(跨源资源共享) 设置
    • defer:脚本延迟到文档完全解析并显示后执行
    • intergity:完整性验证
    • language:废弃
    • src:外部文件地址
    • type:代替 language 代表脚本中的语言类型,如果这个值是 model 那么 js 代码中才能出现 import export 等关键字
  • 顺序执行:不管包含什么代码,如果 script 中没有设置 async 或者 defer,浏览器都会按照顺序去执行他们
  • 标签位置:现代 Web 应用程序通常将所有的 JavaScript 引用放在 body 元素中的页面内容后面。因为浏览器解析到 body 的起始标签的时候才会开始渲染,如果放在 中就意味着必须把所有的 JS 代码都下载、解析、解释完成后才开始渲染
  • 推迟执行脚本:在 script 元素上设置 defer 属性
  • 动态加载脚本:可以通过 DOM Api 向 Dom 中动态添加 script 元素
// 通过这种方式添加的代码默认是以异步方式进行加载的,相当于添加了 async 属性
let script = document.createElement('script');
script.src = 'url';
document.head.appendChild(script)

第3章:语言基础

3.1 语法

  1. 区分大小写:不论变量、函数名还是操作符都区分大小写
  2. 标识符:就是变量、函数、属性、函数参数的名称。
    1. 标识命名规则:第一个字符必须是字母、下划线或者$
    2. 剩下的字符可以是字母、下划线、$、或者数字。推荐使用驼峰命名
    3. 关键字、保留字、true、false、null不能作为标志符
  3. 注解:单行注解 // ;多行注解 /**/
  4. 严格模式:ES5新增的概念,对于一些不安全的写法会报错
// 针对整个脚本启用严格模式
"use strict";

// 针对函数体启用严格模式
function doSth(){
    "use strict"
    // 严格模式下不允许读取 arguments 对象,所以这里会报错 
    // TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
    console.log(arguments.callee); 
}

doSth();
  1. 语句:语句后面写分号、代码块使用 {} 进行包裹

3.2 保留字与关键字

保留字与关键字不能作为标识符或者属性名
2.jpg

3.3 变量

1. var 关键字

var 的作用域是函数作用域并且存在变量提升(就是把变量的声明拉到函数的顶部)

  1. var 的作用域
function test(){
    var msg = 'message'; // 函数的局部变量,函数运行完毕后会被销毁
    g_msg = 'g_msg'; // 没有使用 var 定义,会变成全局变量即 
}
test();
console.log(g_msg)
  1. 声明提升
function test(){
    console.log(msg); // undefined 但不会报错。存在变量提升因此这里有 msg 的定义
    var msg = 'message'; 

    // 上述代码相当于
    // var msg;
    // console.log(msg);
    // msg = 'message'
}
test();
2. let 关键字

let 的作用域是块作用域(简单理解就是 {} 包裹的)

function test(){
    if(true){
        var msg = 'msg';
    }
    console.log(msg); // output: msg 由于 var 作用域是函数作用域,所以存在变量提升

    if(true){
        let msg2 = 'msg2'
    }
    console.log(msg2); // ReferenceError: msg2 is not defined
}
test();
  1. 暂时性死区:在 let 声明之前的执行瞬间叫“暂时性死区”
  2. 全局声明:在全局作用域中使用 var 定义的变量会变成 window 对象的属性,但是使用 let 的就不会(经过测试:似乎只有浏览器中才会出现这个结论,Node 中不成立)
    1. 3.jpg
    2. 4.jpg
  3. 条件声明:js 引擎会将重复的 var 变量定义在作用域顶部合并成一个,因此重复定义 var 的同名变量是没问题的
  4. for 循环中使用 let 声明:由于 var 的作用域是函数级别的,因此 var 在循环体中使用的时候会溢出到循环外面,但是 let 就不会。注意 for 循环(包括 for in、for of等方式)每次迭代的时候都会声明一个独立的(但不是新的,会复用原来的变量)变量实例。
// 下面代码没问题,因为 js 引擎在最顶部的作用域将命名合并了
var msg = 'msg';
var msg = 'msg';

// 下面代码会报错,因为有重复定义
let msg2 = '';
let msg2 = '';

// var 定义的 i 会溢出到外部
for(var i=0;i < 10; i++){}
console.log(i) //10

// let 定义的 j 不会溢出到外部
for(let j=0;j < 10; j++){}
console.log(j) // ReferenceError: j is not defined

3. const 声明
  1. 不允许重复定义
  2. 作用域也是代码块
  3. 只限制它指向的变量的引用(如果 const 变量引用的是一个对象,那对象的改动是没有问题的)
  4. 不能用在 for 中,因为迭代变量会自增
let obj = {}
const ref = obj;
obj.name = 'name';
ref = '' //TypeError: Assignment to constant variable.
4. 声明风格及最佳实践

不用 var,优先使用 const 次之是 let

3.4 数据类型

ES6 有 6 中原始类型:Undefined Null Boolean Number String Symbol;1 中引用类型 Object

1. typeof 操作符

5.jpg

let obj = {};
function name(){};
let str = '';
let bool = true;
let sym = Symbol();

// object function string boolean symbol
console.log(`${typeof obj}   ${typeof name} ${typeof str } ${typeof bool} ${typeof sym}`);
2. Undefined 类型

这个类型只有一个值就是 undefined。增加这个值的目的就是区分空指针对象(null)与未初始化变量的区别。注意:未声明的变量与未初始化的变量调用 typeof 都会返回 undefined,关键逻辑要判断清楚

let ref;
console.log(typeof ref) //undefined
console.log(typeof a) //undefined
3. Null 类型

只有一个值就是 null ,这玩意表示一个**空对象指针,**所以 typeof null === 'object'

4. Boolean 类型

true、false。可以通过调用 Boolean 函数将其他类型转换成布尔值
6.jpg

5. Number类型
  1. 浮点值:浮点值的内存空间是整数的两倍,所以 js 引擎为了优化内存使用会将浮点值尽量转化成整形,比如 1.0 会转化成 1
    1. 对于非常大或者非常小以及浮点类型来说,ES 会把他们转化成科学计数法(3.1e1 === 31)的形式来存储
    2. 浮点值的计算不准确:浮点值的最高精度是 17 位小数,并且由于 ES 规范采用了 IEEE 754 数值,会导致 0.1 + 0.2 !== 0.3 (https://zhuanlan.zhihu.com/p/100353781 我理解:计算过程是十进制数转化成二进制数的科学计数法形式——64位,由于 0.1 转化成二进制数的时候出现了循环,导致二进制的科学计数法形式出现截断,进而导致反向计算的时候不准)。所有采用 IEEE754 规范的数值都会出现这个问题
  2. 值的范围:
    1. 最小值 Number.MIN_VALUE;
    2. 最大值 Number.MAX_VALUE;
    3. Infinity(正无穷大) Number.POSITIVE_INFINITY;
    4. -Infinity(负无穷大) Number.NEGATIVE_INFINITY
  3. NaN:not a number ,代表不是一个数值。下面集中涉及到 NaN 的计算。ES 提供了 isNaN() 方法来判断是否是 NaN
1/0, //Infinity 别的语言会报错,这货就报 Infinity
0/0, //NaN 
0/1, 0,
NaN===NaN, //false NaN与NaN不相等
NaN/10 //NaN NaN参与的任何计算都是NaN
  1. 数值转换:Number parseInt parseFloat
    1. Number 的转化规则如图 7.jpg
    2. parseInt:从头开始,如果不是数字则返回 NaN ,后面不是数字的部分会被忽略,小数点也会被忽略。注意,这个函数可以传入第二个参数,代表按照什么进制去转化 parseInt(88.88,10) === 88
    3. parseFloat:类似于 parseInt,首个小数点会被记录,首个小数点后面的不是数字的部分会被忽略(2 个小数点只会有一个生效 比如 22.22.22 会被搞成 22.22)
let arr = [
    parseInt(22.62), // 22
    parseInt('asds'), // 字符串开头不是数字直接返回 NaN
    parseInt('22e2'), //22 
    parseFloat(22.22), // 22.22
    parseFloat('22.2d3'), // 22.2 由于 d 不是数字会忽略 d3
    parseFloat('22.22.22'), // 22.22 第二个小数点会被忽略
    parseFloat('22.22e2') // 2222 会被搞成科学计数法的形式
]
console.log(arr)
6. String 类型
  1. 一些字符字面量。注意涉及到转义的时候可以通过 \ 对字符进行转义 比如 ` 就代表 ` 。字符串的长度可以通过 str.length 来获取~8.jpg9.jpg
  2. 字符串是不可变的
  3. 转换为字符串:
    1. 除了null undefined 外,所有类型都有 toString() 函数
    2. 也可以使用 String()函数进行转化。这个函数会调用对象的 toString() 函数,对于 null undefined 这俩没有的直接返回对应的字面量 ‘null’ ‘undefined’
    3. 使用 + ” 也可以转化成一个字符串
let obj = {}
let arr = [
    String(null), // 'null'
    String(undefined), // 'undefined'
    String(2), //'2'
    2 + '', //'2'
    obj +'' //'[object Object]' 
]
console.log(arr);

  1. 模板字符串:“ 会保留字符串内的回车等符号
  2. 字符串插值: ${表达式}对字符串进行插值,表达式运行完毕后会自动调用 toString() 然后插入到字符串中
  3. 标签函数:没搞懂有啥用,但是知道他的用法吧。注意标签函数时不需要调用()去执行的,他接收的第一个参数是字符串模板将插值干掉后的数组(插值把字符串模板劈开成数组形式),后面的参数是字符串模板插值的值
let name = "will";
let age = 18;
function sayHello(arg1, arg2, arg3) {
  console.log(arg1);
  console.log(arg2);
  console.log(arg3);
}
/**
下面两个函数等价,都会输出:
[ "I'm ", ". I'm ", ' years old.' ]
will
18 */
// 普通函数
sayHello(["I'm ", ". I'm ", " years old."], name, age);
// tag 函数
sayHello`I'm ${name}. I'm ${age} years old.`;
  1. 原始字符串:String.raw,注意这玩意是个标签函数!!!
console.log(`\u00A9`); // 会被转义成 © 
console.log(String.raw`\u00A9`); // 不会被转义,直接输出 \u00A9
7. Symbol 类型

就是用来创建唯一的记号,避免对象的属性值冲突。这玩意不能使用 new 操作符。对应的 String、Number、Boolean 可以直接通过 new 搞出来对象,并且 typeof 这些对象都是 object

  1. 可以传入字符串用作标志,但是并没有啥毛用~ 看下面的代码
  2. 可以从全局符号表中搞到符号实例 Symbol.for,可以找到原来创建出来的符号实例
let sym = Symbol('sys'),sym2 = Symbol('sys');
console.log(sym === sym2); // false 虽然 Symbol('sys')都是同样的字符串,但是这俩玩意不相等


let globalSym = Symbol.for('g_sys');
let globalSym2 = Symbol.for('g_sys');
console.log(globalSym === globalSym2); // true 从全局符号表里可以搞到相同的
console.log(Symbol.keyFor(globalSym2)) //g_sys,找到符号对应的 key
  1. 使用符号作为属性
let s1 = Symbol('s1');
let s2 = Symbol('s2');
let obj = {
    key:'value',
    [s1]: 's1value', // 通过[]把动态的属性搞到字面量的对象中
}
Object.defineProperty(obj,s2,{s2value:'s2value'});
// 下面这俩函数互斥!!!
console.log(Object.getOwnPropertyNames(obj)); // [ 'key' ] 只会返回非 Symbol 的属性
console.log(Object.getOwnPropertySymbols(obj)); //[ Symbol(s1), Symbol(s2) ] 只会返回 Symbol 的属性
// 下面这个函数可以返回对象的所有属性
console.log(Reflect.ownKeys(obj)); //[ 'key', Symbol(s1), Symbol(s2) ]
  1. 常用内置符号:用于暴露语言内部的行为,开发者重写这些函数后可以重新定义一些行为。比如 Symbol.search 就是供 String.prototype.searh() 调用的
let text = "Mr. Blue has a blue house";
class Search{
    constructor(str){
        this.str = str;
    }
    [Symbol.search](target){ // 重新定义这个函数,注意 Symbol.search 是key啊!!!!!
        return target.indexOf(this.str);
    }
}
let position = text.search(new Search('Blue'));
console.log(position) // 4
8. Object 类型

一组数据与功能的集合(这玩意是无序的)

3.5 操作符

1. 一元操作符

递增/递减操作(i++:表达式结束后才+1;++i:先 +1 再计算表达式)

let num = 20;
let num2 = 2;
// console.log(--num + num2); // 21
console.log(num-- + num2); // 22
2. 位操作符

**ES 中所有数值都以 IEEE 754 64 位格式存储。**位操作符符不会直接应用到 64 位,而是先把值转化成32位,操作完后再转成64位
没看懂。。。。。但是学了一招 ~~num就是取绝对值

3. 相等操作符

== 与 === 的区别在于:=== 既对比值又对比类型

3.6 语句

  1. for-in:针对对象
  2. for-of:针对迭代器
  3. switch 语句的判断可以用于任何类型甚至是对象,别的语言只能用数值
let obj = {key: 'value'}
for(let props in obj){console.log(props)}

let arr = ['1',2]
for(let props of arr){console.log(props)}

let obj2 = obj
switch(obj2){ // 这玩意用对象都他么行,太牛逼了
    case obj:
        console.log('aaa')
}

3.7 函数

函数的最佳实践是要么返回,要么不返回,不要在条件语句中有的条件下返回有的条件下不返回,徒增调试成本!!!

第4章:变量、作用域与内存

1. 原始值与引用值

  • 原始值是按值访问的,存储在栈内存里Undefined、Null、Boolean、Number、String、Symbol但是你用的时候不要 new,new 的话就是包装类型了 比如 new String('')这就是 Object 类型了 let str = new String('') //typeof str object
  • 引用值是按引用访问的,存储在堆内存里Object
  1. 引用值可以动态添加属性,比如 new 了一个对象,我们可以动态的添加/删除对象的一些属性,但是原始值不行
  2. 值的复制上也有差异:引用值复制后两个对象会有干扰(因为引用值指向的是同一个对象)

10.jpg
11.jpg

  1. ES6 中的函数参数都是按值传递,如果是原始类型那就复制一份值,如果是引用类型那就复制一份引用!!!这玩意就跟复制变量的机制是一毛一样的,就是创建了一个变量的副本~~~~
let a = 'aaaa'
let b = {}
function fun(a,b){
    a = 'change_a';
    b.name = 'will'
    b = {} // 这句话能说明函数的参数不是按照引用传递,否则下面的输出就应该是个空对象才对
}
fun(a,b)
console.log(a,b) // aaaa { name: 'will' }
  1. 使用 instanceof 可以判断是否是一个对象的子类

2. 执行上下文与作用域

上下文:变量或者函数的上下文决定了他们可以访问哪些数据以及他们的行为。每个上下文都附属一个变量对象,上下文中定义的所有变量与函数都存储在这个变量对象上。上下文中的代码在执行的时候会创建一个作用域链。这个作用域链决定了上下文中的代码访问变量或函数的顺序。当前执行的上下文的变量对象始终在这条作用域链的最前端。
注意:函数的参数被认为是当前上下文中的变量,因此遵循与当前上下文中其他变量相同的访问规则
12.jpg

  1. 作用域链增强:解释了 with 语句的使用,但是这玩意现在已经不推荐使用了
  2. 变量声明
    1. var:函数级作用域。var 变量的声明会被拿到函数的顶部,多个相同名字的 var 变量会被合并,这叫变量提升
    2. let:块级作用域({}内)。声明前使用 let 变量由于暂时性死区的存在,会报错
    3. const:不允许重新赋值(推荐使用这玩意,因为引擎会有优化)。但是如果 const 变量引用的是一个对象,那对象的操作不受 const 影响。如果定义了对象后不想再让别人改动,那就调用 Object.freeze()方法冷冻
let obj = {}
let freezeObj = Object.freeze(obj);
obj.name = 'will'; // 这条赋值语句不会生效
3. 垃圾回收

跟 JVM 一个鸟样~~~

  1. 标记清理
  2. 引用计数(这玩意解决不了循环引用问题)
  3. 如果内存中的变量过多,会导致频繁 GC 进而导致性能下降,大部分浏览器没有开放垃圾回收函数,垃圾回收时机是由引擎决定的(跟 JVM 一样~~~)
  4. 内存管理:就讲了讲不要因为使用闭包导致内存泄漏啥的,个人觉得意义不大

第5章:基本引用类型

1. Date

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date 看文档吧 常用的就是获取时间戳 console.log(new Date().getTime()); //1668479750783

2. RegExp

正则相关的,直接参考之前的正则表达式文档吧

3. 原始值的包装类型

使用原始值的包装类型可以 new 出来(Boolean String Number),但是这时候 typeof 就是 object 了,调用一些基础数据类型的方法时,引擎后台会自动转化成包装类型并调用函数,但是这些玩意在前台是看不见的,运行完后引擎后台就会销毁.

let strObj = new String('will');
console.log(typeof strObj) // object
let str = 'will'
console.log(str.length) // 4 引擎后台会自动转化成 string 的包装类型再调用属性,但是这玩意开发者看不见

几个比较常用的方法

let num = 1.055; 
console.log(num.toFixed(2)) // 1.05  


// 这招好使
num = Math.round(num * 100) / 100; // 保留两位就是小数才是 100
console.log(num) //1.06
  • String 的一些方法
    • 截取字符串:slice substr substring 详细的 api 看文档去吧 [https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/substring](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/substring)
    • trim:去除两端的空格
    • padStart() padEnd() 在字符串前面填充或者在字符串后面填充
    • toUpperCase toLowerCase:转化成大写/小写
    • 字符串包含方法:
      • startWith()是否以xxx开始
      • endWith()是否以xxx结束
      • includes()是否包含

4. 单例内置对象

4.1 对 URI 进行编码有两种方式
  1. encodeURI:不会编码属于URL 组件的特殊符号,比如冒号、斜杠、问号、井号
  2. encodeURIComponent:会编码他发现的所有非标准字符
let url = "http://www.baidu.com#target xxx";
console.log(
    encodeURI(url), // http://www.baidu.com#target%20xxx
    encodeURIComponent(url) // http%3A%2F%2Fwww.baidu.com%23target%20xxx
);
4.2 eval(‘xxx’)

这个方法可以模拟引擎行为,这里面的 xxx 语句会被引擎解析出来插入到对应的位置,这玩意与正常调用代码有相同的作用域链。用这个 api 要防止用户注入错误代码导致系统崩溃~

let msg = 'msg'
eval('console.log(msg)') // 输出 msg
4.3 Math 进行一些数学公式的计算

(注意 Math.round 执行四舍五入,不要用 Number.toFix())

Math.min(1,3,5) // 1 找最小的
Math.max(1,3,5) // 5 找最大的

// 舍入方法
Math.ceil(5.1) // 向上取整
Math.floor(5.5) // 向下取整
Math.round(1.055) // 四舍五入,只会对整数部分四舍五入,如果保留2位小数,那就先100再除100

Math.random() // 返回0-1的随机数,包含0不包含1

第6章:集合引用类型

1. Object

可以通过变量的形式给对象定义值以及取值

let key = 'key';
let obj = {
    [key]: 'value'
}
obj[key] //value

2. Array

2.1 Array.from & Array.of

这玩意有两个静态方法都可以将参数转化成数组实例,都执行的是浅复制

  1. Array.from:将可迭代对象转化成数组实例
  2. Array.of:将参数抓化成数组实例
let set = new Set().add(1).add(2)
Array.from(set) // [ 1, 2 ] 将可迭代对象转化成数组实例
Array.of(1,{},3,4) //[ 1, {}, 3, 4 ] 将参数转化成数组实例
2.2 数组空位

数组空位:不推荐使用。忽略

2.3 数组索引

数组有 length 属性标志数组的长度,这玩意可写,用于数组末尾添加或者删除元素

let arr = Array.from(''.padStart(20,'a'));
arr.length -= 1 // 会把末尾的一个元素干掉
console.log(arr.length) // 19
2.4 Array.isArray

使用 Array.isArray(value)检测是否是数组

2.5 迭代器方法
  1. array.keys(): 返回数组的索引迭代器
  2. array.values():返回数组值的迭代器
  3. array.entries(): 返回数组键值对的迭代器
let arr = ['a','b','c'];
console.log(Array.from(arr.keys())) //[ 0, 1, 2 ]
console.log(Array.from(arr.values())) //[ 'a', 'b', 'c' ]
console.log(Array.from(arr.entries())) //[ [ 0, 'a' ], [ 1, 'b' ], [ 2, 'c' ] ]
2.6 复制和填充方法
  1. 复制方法: copyWithin()
  2. 填充方法 fill()
let arr = ['a','b','c'];
arr.fill(1) // [1,1,1]  进行填充
arr.copyWithin(0,2) //[ 'a', 'b', 'c' ] 浅复制索引为2的元素到索引为0的位置上
2.7 转换方法:

join 方法会挨个调用数组元素的 toString() 方法然后将 join 方法的参数插入到中间,然后返回字符串

let arr = new Array(2)
arr.fill('a') // [1,1,1]  进行填充
console.log(arr.join('-')) //a-a
2.8 栈方法:
  1. push()接受任意数量的参数,加到数组后面,返回数组新长度
  2. pop()方法删除最后一项并返回被删除的项目
2.9 队列方法
  1. push()同上
  2. shift()删除数组的第一个元素并返回他 (跟 pop() 相反)
let arr = [1]
arr.push(5,6)

console.log(arr) //[ 1, 5, 6 ]
console.log(arr.pop())//6
console.log(arr)//[ 1, 5 ]

console.log(arr.shift())//1
console.log(arr)//5
2.10 排序方法

下面两个函数都返回数组的引用~

  1. sort():排序,默认是正向的,可以接受一个函数用于倒叙排序
  2. reverse():倒序排序
let arr = [1,3,2,]
console.log(
    arr.sort(), // 正序排序
    arr.reverse(), // 倒序排序
    arr.sort((a,b) => { // 使用 sort 函数倒序排序
        return b-a
    })    
)
2.11 操作方法
  1. contact():将参数的数组搞到现有数组的后面,返回一个新数组,新数组是对原数组的浅拷贝
  2. slice(a,b):返回数组索引从 a 开始到 b (不包含 b)的数组元素。如果 b 不传则默认是数组最后
  3. splice(a,b,...c) :这个函数比较吊,删除、插入、替换都能搞
    1. a 开始的索引
    2. b 要删除的数量
    3. c 要插入的元素(可以有多个)
// contact() 测试
let obj = {};
let arr = [obj,3,2,]
let b = arr.concat(['']) 
console.log(b) //[ {}, 3, 2, '' ]
console.log(b[0] === arr[0]) //true

//slice() 测试
let sArr = [1,2,3,4,5];
console.log(sArr.slice(2)) // [ 3, 4, 5 ]

//splice() 测试
let sArr1 = [1,2,3,4,5];
sArr1.splice(0,2) // 删除:index = 0 1 的元素
console.log(sArr1); // [ 3, 4, 5 ]

sArr1.splice(0,0,'red') //插入:从index=0开始(第二个0是说要删除的元素数量是0),插入 red
console.log(sArr1); //[ 'red', 3, 4, 5 ] 

sArr1.splice(0,1,'blue') // 替换: 从index=0开始,干掉1个元素,然后插入 blue
console.log(sArr1); //[ 'blue', 3, 4, 5 ]
2.12 搜索与位置方法
  1. 严格相等 indexOf() lastIndexOf() includes()会使用 === 对比参数与数组的每个元素
  2. 断言函数 find() findIndex()接收一个函数用于判断是否相等。注意:这俩函数找到第一个结果后就不找了
let arr = [1,'a','a','b',5,{key: 'value'}]
console.log(
    arr.indexOf('b'), // 3
    arr.lastIndexOf('a'), // 2
    arr.includes('44'), // false
    arr.find((element,index,arr) => { // {key: 'value'} 断言函数
        return element?.key === 'value' 
    }),
    arr.findIndex((element,index,arr) => { // 断言函数
        return element?.key === 'value' // 5
    })
)
2.13 迭代方法
  1. every()
  2. some(func)
  3. filter()
  4. map()
  5. forEach()
let arr = [1,'a','a','b',5,{key: 'value'}]
console.log(
    arr.every((element,index,arr) => {return element.key}), // false 每个都符合才会 true
    arr.some((element,index,arr) => {return element.key}), // true 一个符合就会 true
    arr.filter((element,index,arr) => {return element.key}), // 过滤操作 返回符合的数组
    arr.map((element,index,arr) => {return element.toString()}), // 转化操作 返回转化后的数组
)
arr.forEach((value,index,array) => {console.log(value)}) // 遍历 没有返回值
2.14 归并方法:

下面这俩方法仅仅是方向的差异,别的没啥区别

  1. reduce() 从左向右归并
  2. recudeRight() 从右向左归并

3. 定型数组:ArrayBuffer

我理解就是直接操作二进制以更高效的操作数据,没细看

4. Map

Java 里面有 put 方法,但是 JS 中是 set 。。。。。。 妈的 老记混~~~~

4.1 Map 的构造及基本使用

Map可以使用任何类型作为键值,判断键值的时候使用的是严格相等来判断,相反,对象只能使用数值、字符串、符号作为键

  1. 构造
    1. 直接 new 出来
    2. 通过嵌套数组这种可迭代对象进行初始化
  2. 基本使用
    1. set 设置值
    2. get 取值
    3. has 判断是否有值
    4. delete 删除
    5. clear 清空
    6. map.size 返回元素个数
let map = new Map();
let map2 = new Map([['key1','value1'],['key2','value2']])
console.log(
    map.set('1','2'), //Map(1) { '1' => '2' }
    map.get('1'), // 2
    map2.has('key1'), // true
    map2.delete('key2'), //true
)
map2.clear() // 清空 map2
console.log(map2.size) // 0
4.2 顺序与迭代

Map 会保存键的插入顺序,Object 则不会 注意:for of 是针对迭代器 for in 是针对对象的

let map = new Map([['key1','value1']]);
map.set('key3','value3');


for(let item of map.keys()){
    console.log(item) // key1 key3
}
map.forEach((value,key) =>{
    console.log(`${key} -> ${value}`) //key1 -> value1  key3 -> value3
})


// 注意:如果对象作为 key 或者 value,对象属性的改动并不影响在 Map 中的身份
let keyObj = {}
map.set(keyObj,'keyObjValue');
keyObj.id = '艹' // 作为 key 对象的属性改动并不影响身份识别
console.log(map.get(keyObj)); //keyObjValue
4.3 Map 与 Object 的差异

Map 与 Object 的差异比较小以下两种场景下需要使用 Map

  1. 如果想记住顺序,那就使用 Map
  2. 如果考虑到大量的插入、删除时的性能,那就使用 Map
4.4 WeakMap

键不会阻止垃圾回收,跟 JVM 一样,用的时候再看吧

5. Set

这玩意就像是加强版的 Map,这俩有很多相似的行为。Set 也会保存插入的顺序,并且 add() delete()是幂等的(就是执行多次与执行一次是一样的效果)

5.1 基本 API
  1. 构造:通过 new 关键字新建出来或者通过一维数组实例化出来
  2. 基本方法
    1. add 增加值
    2. has 查询值
    3. size 获取元素数量
    4. delete 删除值
    5. clear 清空值
let set = new Set(['v3',2]);
set.add({}).add(1) // add 方法返回 set 的实例
console.log(set.has(1));  // true
set.delete(1); // true
set.clear(); // 清空
console.log(set) // Set(0) {}
5.2 迭代

如果 set 的值是一个对象,那么改变对象的一些属性后调用 has 这些方法仍然好使(对象的属性改变不会影响 Set 中的身份认证,见下面的代码)

let set = new Set(['v3',2]);
// 两种循环方式
for(let value of set.values()){
    console.log(value); // v3 2
}
set.forEach((value) => {
    console.log(value) // v3 2
})

let value ={};
set.add(value);
value.id = '艹'; // 对象属性的改动不影响 Set 中的身份识别
console.log(set.has(value)) //true
5.3 WeakSet

就是弱引用不影响垃圾回收,跟 JVM 一样

6. 迭代与扩展

Array Map Set 都支持迭代器,因此都可以使用 for of来进行遍历,并且都支持扩展操作符 ...

let set = new Set(['v3',2]);
let map = new Map([['key','value']])
let arr = [1,2,3];


for(let item of set){
    console.log(item)
}
console.log(...arr);

第7章:迭代器与生成器

1. 理解迭代

数组的遍历就是最简单的迭代

2. 迭代器模式

任何实现了 Iterable 接口的数据结构都可以被实现了 Iterator 接口的结构进行消费,双方互不感知到具体的细节。(特别类似于 DDD 中的边界)

2.1 可迭代协议

上面列出来的内置的类型基本都实现了可迭代协议,可迭代协议必须使用 Symbol.iterator 作为键并且返回一个迭代器工厂函数,调用这个函数必须返回一个新的迭代器(就是被迭代对象与迭代对象的接口协议而已)

let arr = [];
console.log(arr[Symbol.iterator]()) //Object [Array Iterator] {}
2.2 迭代器协议

迭代器仅仅是一个游标而已,不会存储被迭对象的镜像。因此被迭代对象的改动会及时表现出来(比如迭代过程中对迭代对象改动了以后迭代器遍历到这个改动了的属性后会及时输出出来)

let arr = [''];
let iter = arr[Symbol.iterator]() //Object [Array Iterator] {}
iter.next() //{ value: '', done: false }
arr.push('value') //迭代器仅仅是一个游标而已,不会存储被迭对象的镜像。因此别迭代对象的改动会及时表现出来
console.log(iter.next()) //{ value: 'value', done: false }
2.3 自定义迭代器
2.4 提前退出迭代器
class Counter{
    constructor(count){
        this.count = count;
    }
    [Symbol.iterator](){
        let currentCount=1,count=this.count;
        return { // 这个就是说的新的迭代器对象,每次迭代都应该是一个新的对象否则两次迭代会互相干扰
            next(){
                if(currentCount < count){
                    return {done: false,value: currentCount++};
                }else{
                    return {done: true,value: undefined}
                }
            },
            return(){
                console.log('你特么提前结束了');
                return {done: true,value: '你特么提前结束了'}
            }
        }
    }
}
let count = new Counter(4);
for(let num of count){  // 正常迭代
    console.log(num); // 1 2 3
}

for(let num of count){ // 提前退出迭代
    if(num === 1){
        break  //你特么提前结束了 continue return throw 等操作符都会提前退出迭代
    }
}

3. 生成器

生成器是 ES6 中非常灵活的一个结构,拥有在函数块中暂停和恢复代码执行的能力。用生成器可以自定义迭代器并且实现协程

3.1 生成器基础

函数前面加上 * 就变成了生成器,**箭头函数无法定义成生成器函数,**生成器也实现了迭代器协议,因此也具备 {done,vlaue}的返回值

3.2 通过 yield 中断执行

注意:多次调用生成器返回的对象互不干扰;可以使用 for of对可迭代对象进行迭代

function * fnc(){
    yield 'will'; // 调用 next 时候的暂停点
    console.log(`do something`);
    yield 'reka';
}
let fncObj = fnc();
let fncObj2 = fnc(); // 跟 fncObj 是两个实例,生成器内部互不干扰
console.log(fncObj.next()) //{ value: 'will', done: false }

for(let value of fncObj2){ // 可迭代对象都可以通过 for of 进行迭代
    console.log(value) // will reka
}
3.3 通过 yield 实现输入输出

注意: 首次调用 * 函数时不会执行任何代码。第一次调用 next() 函数时 yield 不会接收到 next() 的参数,这次调用是为了开始执行生成器函数,后续的 next() 函数中的参数都会被 yield 接收到

function * generatorFn(init){ 
    return yield;
}

let fn = generatorFn() // 调用生成器函数会生成一个 生成器对象,这个对象会处于暂停状态
console.log(fn.next('bar')); // { value: undefined, done: false } 第一次调用 next() 传入的参数不会使用,这次调用是为了开始执行生成器函数
console.log(fn.next('baz')) // { value: 'baz', done: true } 后续的 next() 里面的参数会被 yield 拿到,也可以直接拿到
3.4 使用 * 增强 yield 行为

yield * 的值是关联迭代器返回 done: true 时的 value 属性,对于普通迭代器来说这个值是 undefined ,对于生成器函数来说就是返回值

function * testFun(){
    console.log(yield* [1,2,3]); // undefined 对于普通的迭代器来说这个是 undeinfed
    return 'testFun'
}

function * otherFun(){
    console.log(yield * testFun()); //testFun  生成器函数来说就是返回值
}

for(const x of testFun()){
    console.log(x) // 1 2 3
}

for(const x of otherFun()){
    console.log(x) // 1 2 3
}
3.5 提前终止生成器(两种提前终止生成器的方式)
  1. return()
  2. throw():前提是生成器中没有捕获这个错误,如果生成器内部捕获了这个错误则会跳过一个值
function * testFun(){
    yield* [1,2,3]; // undefined 对于普通的迭代器来说这个是 undeinfed
    return 'testFun'
}
let res = testFun();
// console.log(res.next()); // { value: 1, done: false }
// console.log(res.return()); // { value: undefined, done: true } 使用 return 方法提前终止生成器
// console.log(res.next()); // { value: undefined, done: true }

// try{
//     res.throw('foo'); // 使用 throw 方法终止生成器,前提是函数内部没有自己 catch 错误啊~~~~~
// }catch(e){}
// console.log(res.next()); // { value: undefined, done: true } 

function * testFun2(){
   for(let i of [1,2,3]){
    try{
        yield i;
    }catch(e){
        console.log(`内部捕获到了错误 ${e}`);
    }
   }
}
let res2 = testFun2();
console.log(res2.next()); //{ value: 1, done: false }
res2.throw('注入错误')
console.log(res2.next()); // { value: 3, done: false }

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

ES 的对象就是一张散列表(不记录顺序)

1 理解对象

对象可以通过两种方式来定义:1. new Object() 2. 字面量的方式(这种方式最常用)

1. 1 属性的类型
1.1.1 数据属性
  1. 数据属性又4种方式描述他们的行为
    1. Configurable:是否可以通过 delete 删除 直接定义到对象上的的默认是 true
    2. Enumerable:是否可以通过 for in 循环返回 直接定义到对象上的的默认是 true
    3. Writable:是否可以被修改 直接定义到对象上的的默认是 true
    4. Value:属性实际的值,直接定义到对象上的的默认是 undefined
  2. 如果修改属性的默认特性,**就必须使用 ****Object.defineProperty()****函数,这个函数接收三个参数 「被修改的对象」、「属性名」、「描述符对象」,这个描述符对象就包含 value configurable(默认是 false) enumerable(默认是 false) writable(默认是 false) 四个属性。**注意:通过 Object.defineProperty 定义了 configurable 后如果设置成 false,再次调用同一个对象及同一个属性的 Object.defineProperty 去定义 configurable 是无效的
let person = {};
Object.defineProperty(person,'name',{
    value: 'will', // 值是啥
    configurable: false, // 是否可被 delete 
    writable: false, // name 属性是否 delete 
    enumerable: true // for in 是否会被返回
})

delete person.name; // 上面的 configurable 是 false 因此这里的 delete 没啥用
console.log(person.name) //will
for(let i in person){
    console.log(i) // name
}
1.2 访问器属性
  1. 访问器属性也有4个方式描述他们的行为
    1. Configurable:是否可以通过 delete 删除 直接定义到对象上的的默认是 true
    2. Enumerable:是否可以通过 for in 循环返回 直接定义到对象上的的默认是 true
    3. Get:获取函数,在读取属性时调用 默认是 undefined
    4. Set:设置函数,在写入属性时调用 默认是 undefined
  2. 访问器属性不能直接定义,必须使用 Object.defineProperty 进行定义。获取与设置函数不一定都要定义,只设置获取函数意味着属性是只读的,尝试修改属性会被忽略,这玩意典型的使用场景是在设置或者获取值的时候可以干一些别的事情
let person = {_name: 'will'};
Object.defineProperty(person,'name',{
    configurable: true, // 是否可被 delete 
    enumerable: true, // for in 是否会被返回
    get(){
        return this._name
    },
    set(value){
        this._name = value;
        // TODO 在这里面还可以干别的事情
        console.log('可以干别的事情')
    }
})
person.name = 'reka' 
console.log(person.name) //reka
1.2 定义多个属性

使用 Object.defineProperties 可以设置多个属性

let person = {};
Object.defineProperties(person,{
    _name: { // 数据属性
        value: 'will',
        writable: true
    },
    name: { // 访问器属性
        set(value){
            this._name = value;
        },
        get(){
            return this._name;
        }
    }
})
person.name = 'reka' 
console.log(person.name) //reka
1.3 读取属性的特性

通过 Object.getOwnPropertyDescriptor 获取单个属性的描述,通过 getOwnPropertyDescriptors 获取多个属性的描述

let person = {name: 'will',sex: 'man'};


console.log(Object.getOwnPropertyDescriptor(person,'name'))
//{ value: 'will', writable: true, enumerable: true, configurable: true }

console.log(Object.getOwnPropertyDescriptors(person))
// {
//     name: {
//       value: 'will',
//       writable: true,
//       enumerable: true,
//       configurable: true
//     },
//     sex: {
//       value: 'man',
//       writable: true,
//       enumerable: true,
//       configurable: true
//     }
// }
1.4 合并对象

掌握三个知识点:Object.assign(targetObj,…) 可以完成对象的浅复制 ;复制过程没有回滚,也就是说如果复制了一半报错了,那已经复制的哪些属性仍然生效;后引入的属性会覆盖重复的属性

let targetObj = {};
let paramObj = {a:()=>{}}
let result = Object.assign(targetObj,paramObj);
console.log(result === targetObj) //true // 复制结束的时候返回的事 target 的引用
console.log(result === paramObj) //false
console.log(result.a === paramObj.a) //true 执行的是浅复制,对于函数或者对象赋值的是引用,所以这里是 true
1.5 对象标识及相等判定

很多情况下 === 的一些行为不符合预期,这时候就可以用 Object.is 来解决

console.log(+0 === -0)  // true
console.log(NaN === NaN) // false 如果要判断是否是 NaN 还可以使用 isNaN() 来判断,但是 === 不能用
console.error(Object.is(+0,-0)) //false
console.error(Object.is(NaN,NaN)) //true
1.6 增强对象语法

没啥用,喜闻乐见的东西了(属性值简写、可计算属性、简写方法名)

1.7 对象解构
  1. 嵌套解构
  2. 部分解构:解构没有回滚,类似于 Object.assign(),如果解构了一半出错了,那已经赋值的那部分仍然生效
  3. 解构的赋值并不影响 arguments 对象
let person = {
    home: {
        address: 'beijing'
    },
    name:'will'
}

let fName,errorTest;
try{
    ({name: fName,a:{a}} = person); // 这骚语法我到现在也没弄明白。。。。
}catch(e){
    console.log(e) // 会把 a 报错
}

console.log(fName) //will 不影响 fName 的赋值

2 创建对象

2.1 (了解就好)概述

通过函数虽然可以创建对象,但是还是尽量通过 E6 里面的类去定义

2.2 (了解就好)工厂模式

通过工厂去创建相同的对象,但是创建出来的这些新对象没有啥继承关系

2.3 构造函数模式
  1. 构造函数首字母应该大写,非构造函数首字母应该小写(这是从面向对象编程领域借鉴过来的经验)
  2. 新构造的对象都有一个 constructor 指向构造函数(因为对象都有一个[[Prototype]]属性指向原型对象,原型对象上有个 constructor 属性指回构造函数)
  3. 通过 new 关键字去创建一个新的对象会发生这些事情
    1. 内存中创建一个新的对象
    2. 新对象的 [[Prototype]] 特性被赋值为构造函数的 prototype 属性
    3. 构造函数内部的 this 赋值为这个新的对象
    4. 执行构造函数内部的代码(就是给这个新的对象添加属性)
    5. 如果构造函数返回 非空 对象,则返回这个对象,否则返回刚创建的新对象
function Person (name,sex){
    this.name = name;
    this.sex = sex;
}

let person1 = new Person('will','man');
let person2 = new Person('reka','women');
console.log(JSON.stringify(person1))  //{"name":"will","sex":"man"}
console.log(person1.constructor === person2.constructor); //true 因为这俩货都有同一个原型对象,原型对象上的 constructor 指向构造函数
  1. 构造函数也是函数,在普通的调用方式下(即没有使用 new 关键字的时候),如果没有强制指定 this 的指向(没有适用 call bind 等方式,没有作为对象的调用),那默认是 global 对象,大部分环境下就是 window 对象
  2. ES 中函数是对象
  3. 了解就好:构造函数有一定的问题,比如下面的 sayName 方法每次 new 出来的对象都是有一个自己的 sayName 方法,两个实例的 sayName 方法不是一个实例。如果想定义成同一个方法的实例那就需要再全局定义一个方法然后给每个对象的实例单独赋值,这会让代码到处乱放
let name = 'will';
function Person (){
    this.sayName = function(){
        console.log(this.name)
    }
}
let obj = {
    name: 'reka'
};
Person.call(obj); // call 跟 apply 方法都具备切换 this 的牛逼能力,这里代表把 this 切换到了 obj 作用域上
obj.sayName() // 执行这个函数的时候可以就是在 obj 作用域上进行执行,所有的 this 都指向 obj
2.4 原型模式

特别重要:每个函数都有一个 prototype 属性指向原型对象,这个对象包含了这个函数作为构造函数时所有实例共用的方法与属性。换句话说,这个对象就是调用构造函数创建的对象的原型

function Person(name){
    this.name = name;
}
Person.prototype.sayName = function(){
    console.log(this.name)
}
let person = new Person('will')
let person2 = new Person('reka');
person.sayName() // 输出 will
console.log(person.sayName === person2.sayName); // true 因为 sayName 这个方法是直接定义在函数的原型上的,所以所有实例共享
  1. 原型的几个知识点:
    1. **函数都有自己的 prototype 属性指向原型对象 **
    2. **原型对象有个 constructor 属性指回这个函数 **
    3. **每次调用构造函数创建一个新的实例,这个实例内部的 [[Prototype]] 属性就会指向构造函数的原型对象,当然这个 [[Prototype]] 属性在脚本中读取不到是系统用的,但是 Chrome 等实现中在每个对象上暴露了 ****__proto__****属性用于获取这个 [[Prototype]] **
    4. 可以通过原型对象的 isPrototypeOf() 方法判断是否是一个对象的原型,下面会解释
    5. Object.getPrototypeOf() 方法可以返回类似于**__proto__**的值,这玩意只对new 出来的实例有用!!!!!

13.jpg

function Person(name){
    this.name = name;
}
console.log(Person.prototype.constructor  ===  Person); // true 函数原型的 constructor 指回这个函数,真他么花里胡哨 
let person = new Person()
console.log(person.__proto__ === Person.prototype) //true 实例的 __proto__ 属性指向构造函数的原型对象 
console.log(person.__proto__ === Person.prototype.constructor.prototype) //true 基于上面的花里胡哨,这玩意也是

console.log(Person.prototype.isPrototypeOf(person)) //true 原型对象上的 isPrototypeOf 函数可以判断是否是一个实例的原型

//ES规定可以通过 Object.getPrototypeOf 函数获取对象的原型 即那个 [[Prototype]] 内部属性指向的东西,在 chrome 等实现中就是 __proto__ 指向的值
console.log(Object.getPrototypeOf(person) === person.__proto__) //true 
  1. 原型的层级
    1. 对于引擎来说,属性及方法的寻找是先从实例再找原型对象的。只要在实例上定义过属性即使是 null,也会覆盖原型的同名属性。只有 delete 才会重新恢复与原型对象的关联
    2. hasOwnProperty() 方法用于确定某个属性是在实例上还是在原型上。只有实例上有这个属性才会是 true ,否则返回 false
    3. Object.getOwnPropertyDescriptor() 只对实例属性有效,要获取原型对象上的描述符,则需要拿到原型对象再调用这个方法才行
function Person(){}
Person.prototype.name = 'will';
let person = new Person();

// false hasOwnProperty 判断属性是在实例上还是在原型上,只有在实例上定义出来的才是 true 
console.log(person.hasOwnProperty('name')) // false

// true hasOwnProperty 判断属性是在实例上还是在原型上 
console.log(Object.getOwnPropertyDescriptors(person)) // {} 空对象,因为 getOwnPropertyDescriptors 只返回实例上的描述不返回原型上的
console.log(Object.getOwnPropertyDescriptors(Person.prototype)) // 如果想看原型的描述得直接怼原型对象做调用
  1. 原型和in 操作符
    1. in 操作符有两种用法:直接用或者使用 for in。直接用的时候代表属性是否在这个对象上(不管是实例还是原型对象)
function Person(){}

Person.prototype.name = 'will';
let person = new Person();

console.log('name' in person);  //true 
console.log(person.hasOwnProperty('name')) // false

// 使用 in 与 hasOwnProperty 函数可以判断属性是否只在原型上的
function hasPrototypeProperty(object,name){
    return name in object && !object.hasOwnProperty(name);
}
console.log(hasPrototypeProperty(person,'name')) //true
  1. 关于对象属性的获取
    1. Object.keys 只返回对象上可枚举的属性(不包括原型对象及不可枚举的属性)
    2. Object.keys(Object.getPrototypeOf(对象)):如果要获取原型对象上的可枚举的属性必须直接对原型对象做调用
    3. Object.getOwnPropertyNames(person):只返回对象上所有属性(包括不可枚举的但是不包括原型对象的)。比如对原型对象调用这个函数的时候就会返回 constructor ,这玩意就是不可枚举的
function Person(){}

Person.prototype.name = 'will';
let person = new Person();
person.sex ='man'

Object.keys(person) // [ 'sex' ] // 只返回可枚举的对象上的属性
Object.keys(Object.getPrototypeOf(person)) // [ 'name' ] // 如果想看原型上的可枚举属性那就直接在原型上调用
Object.getOwnPropertyNames(person) // [ 'sex' ] 返回所有对象上的属性
Object.getOwnPropertyNames(Object.getPrototypeOf(person)) //[ 'constructor', 'name' ] 返回了 constructor 这种不可枚举的属性
  1. 属性枚举顺序 有下面迭代方式
    1. 顺序不确定,不同的实现有不同的顺序:for…in…;Object.keys();
    2. 顺序确定:
      1. Objcet.getOwnPropertyNames();
      2. Object.getOwnPropertySymbols();
      3. Object.assign()
2.5 对象迭代

ES7 中新增了两个静态对象用于遍历对象 Object.values() Object.entries()

let obj = {
    a: '1',b: '2'
}
console.log(Object.values(obj)) // 这玩意会返回一维数组 [ '1', '2' ] 
console.log(Object.entries(obj)) // 这玩意会返回二维数组 数组中的元素仍然是数组,子数组 0 是key 1 是 value[ [ 'a', '1' ], [ 'b', '2' ] ]
for(let i of Object.entries(obj)){
    // [ 'a', '1' ]
    // [ 'b', '2' ]
    console.log(i) 
}

**原型的属性改动与重写不是一回事!!!属性改动会实时体现到实例上,但是原型对象的重写不会~~~**实例跟原型之间的链接就是简单的指针而不是保存的副本,实例的 [[Prototype]] 在构造函数调用的时候就会被赋值,如果实例被初始化后原型对象被重新赋值了,那实例的 [[Prototype]] 不会改变,如果原型的属性发生了改动,那实例上会体现出来

function Person (){}
Person.prototype.sayName = function(){
    console.log('prototype sayName')
}
let person =  new Person();
person.sayName();  //prototype sayName 这个没毛病因为只是原型的属性发生了变化

Person.prototype = {
    constructor: Person,
    sayName2:function(){
        console.log('prototype sayName2')
    }
}
// person.sayName2(); // 这个会报错,因为原型被重写了,但是实例是之前被初始化出来的所以原型指针还指向原来的原型对象 
let person2 =  new Person();
person2.sayName2();  //prototype sayName2 重新实例化的对象指针就是新的原型对象了,所以存在 sayName2() 
Object.getPrototypeOf(person2) === Person.prototype //true
Object.getPrototypeOf(person2) === Object.getPrototypeOf(person) // false 由于原型被重写了所以这两个实例的原型对象是不相等的

3 继承

ES 里面的继承就是通过原型链来做的

1. 原型链

原型链就是把一个构造函数的原型对象赋值成另外一个构造函数的实例,这样子构造函数的实例就能引用到父构造函数的一些能力。

function SuperType(){}
SuperType.prototype.say = function(){console.log('parent say')}


function SubType(){}
SubType.prototype = new SuperType(); // 关键点:子构造函数的 prototype 属性被赋值给父类的一个实例
let sub = new SubType();
sub.say(); //parent say

14.jpg

  1. 默认情况下每个引用类型都继承 Object,这个继承关系也是原型链实现的(如上图绿色部分)
  2. 判断继承的两种方式
    1. instanceof:如果一个实例的原型链出现过对应的构造函数则返回 true
    2. 原型上可以调用 isPrototypeOf():只要实例的原型链包含这个原型就返回 true
function SuperType(){}
function SubType(){}
SubType.prototype = new SuperType();
let subType = new SubType();

console.log(subType instanceof SuperType); //true
console.log(SuperType.prototype.isPrototypeOf(subType));
  1. 由于原型链是对所有实例都生效的,所以这玩意不会单独使用

4 类

4.1 类定义
  1. 两种表示类的方式:类声明(class A {}) & 类表达式(const A = class{})
  2. 类受“块作用域”限制,跟 let 类似
4.2 类构造函数

constructor 关键字告诉 JS 解释器要在使用 new 关键字新建对象的时候要调用这个方法

  1. 实例化。在通过 new 关键字进行实例化(调用类构造函数)的时候会执行下面的步骤
    1. 在内存中新建一个对象
    2. 这个新对象内部的[[Prototype]]指针被赋值给构造函数的 prototype 属性
    3. 执行构造函数的代码(这些代码大概率是给这个对象添加属性)
    4. 如果构造函数返回非空对象则返回该对象,否则返回新创建的对象
  2. 类是一种特殊的函数。ES 中没有类这个概念,类跟普通构造函数的一些属性完全一致
    1. 类标志符也有 prototype 属性指向原型对象
    2. 原型对象也有 constructor 属性指回类
    3. typeof 一个类返回的是 function
    4. 类的实例上也有 constructor 属性指向类本身(因为会从原型链上找)
class SuperType{};
let superType = new SuperType();

console.log(SuperType.prototype.constructor === SuperType); //true
console.log(typeof SuperType); //function
console.log(Object.getPrototypeOf(superType).constructor === SuperType) // true
4.3 实例、原型和类成员
  1. 实例成员:绑在 this 上的数据都属于实例成员,各个实例成员之间数据不共享
  2. 原型方法与访问器:类中定义的函数在所有实例中是共享的
  3. 静态函数定义在类本身上,不依赖具体的类实例
class Person{
    say(){} // 所有实例共享
    set name(newName){  
        this._name = newName
    }
    get name(){
        return this._name;
    }
    static staticSay(){ // 静态方法,直接定义在类上
        
    }
};
let person = new Person();
let person2 = new Person();

console.log(person.say === person2.say); //true
person.name = 'will'
console.log(person.name); // will

下面的代码演示了类与函数本质上没啥差异!!!!!!!!!!!!!!

class Person{
     constructor(){
        this.say = function(){console.log('instance say')}
     }
     say(){
        console.log('prototype say');
     }
     static say(){
        console.log('class say')
     }
};
let person = new Person();


person.say(); // instance say
Person.prototype.say(); // prototype say
Person.say(); // class say
4.4 继承
4.4.1 super 关键字
  1. super 只能在子类的构造函数或者静态方法中使用
    1. 构造函数中使用默认调用父类构造函数
    2. 静态方法中使用可以调用父类的静态方法
class Parent{
     constructor(){
        super(); // 这里会报错,因为 Parent 不是一个子类
     }
};
let parent = new Parent();
  1. 不能单独使用 super 关键字,要么他调用构造函数,要么引用静态方法
class Parent{
    constructor(){
    }
    static say(){
        console.log('parent say')
    }
};
class Child extends Parent{
    static sayHello(){
        super.say()
    }
}
Child.sayHello() //parent say
  1. 调用 super() 会调用父类的构造函数并将返回的实例赋值给 this
class Parent{
     constructor(){
        
     }
};
class Child extends Parent{
    constructor(){
        super(); // 调用 super 后才执行父类的构造函数,后面调用 this 才没问题 否则会报错
        console.log(this instanceof Parent); // true
    }
}
let child = new Child();
  1. super() 行为如同调用构造函数,如果想给父类构造函数传参,需要手动输入
class Parent{
    constructor(name){
        console.log(console.log(name));
    }
};
class Child extends Parent{
    constructor(){
        super('will') // 如果子类定义了 constructor 函数,则需要手动传入父构造函数参数才行
    }
}
new Child() //will
  1. 子类如果没有定义构造函数,实例化子类的时候会默认调用 super(),传给子类构造函数的所有方法都会传给父类
class Parent{
    constructor(name){
        console.log(console.log(name));
    }
};
class Child extends Parent{} // 子类没有明确定义构造函数,则传入子类构造函数的参数全部会透给父类构造函数
new Child('test') //test
  1. 不能在 super 前调用 this
  2. 如果子类定义了构造函数,要么调用 super() 要么返回一个对象
4.4.2 抽象基类

ES 没有抽象类(可被继承,不可被实例化)这种定义,但是我们可以通过 new.target 来杜绝父类被实例化(这个东西用来保存 new 关键字调用的类或者函数)

class Parent{
    constructor(){
        if(new.target === Parent){
            throw new Error('老子不能被初始化,只能被继承');
        }
        if(!this.foo){
          throw new Error('必须实现 foo 方法')
        }
    }
};
new Parent('test')  // 报错

第9章:代理与反射

代理与反射是 ES6 新增的新特性,不向前兼容

9.1 代理基础

简单理解:代理可以看成 C++ 中的指针,代理是目标对象的替身,但又完全作用于目标对象(操作 target 与 操作 proxy 效果一样)

9.1.1 创建空代理

代理是通过 Proxy 关键字创建的,创建空代理时可以把处理程序搞成一个空对象。这样代理对象上执行的任何操作都会是实际应用到目标对象

let target = {};
let proxy = new Proxy(target,{})

target.name = 'will'; // 对 target 或者 proxy 的操作会同步到两个对象上!!!!!!
proxy.sex ='man'
console.log(proxy === target) // false
console.log(proxy) //{ name: 'will', sex: 'man' }
console.log(target) //{ name: 'will', sex: 'man' }
9.1.2 定义捕获器(trap)

就简单定义一个 handler 的示意,后面会详细讲解

let handler = {
    get(){
        return '我被代理了'
    }
};
let target = {}
let proxy = new Proxy(target,handler)
target.name = 'will'
console.log(proxy.name) // 我被代理了
console.log(target.name) // will
9.1.3 捕获器参数与反射 API

两个知识点:

  1. 所有的捕获器(就是 handler 对象中的函数)都有自己的参数,基本上通过这些参数都可以拿到 目标对象、属性、代理对象等参数(后面会详细说到)
  2. 捕获了对目标对象的一些操作后,很多时候我们无法模拟一些行为让这次调用继续下去。因此可以通过反射 API : Reflect 对象去完成正常的调用行为。简单理解:比如我代理了目标对象的 get 行为中间加了一个日志,但是代理中打完日志后需要调用目标对象的 get 去返回值,这时候就可以调用 Reflect.get(...arguments)去实现,基本上 Reflect 上具备了所有 Object 的通用行为
let handler = {
    get(){
        console.log('日志输出')
        return Reflect.get(...arguments) // 通过全局的Reflect对象去模拟get行为
    }
};
let target = {}
let proxy = new Proxy(target,handler)
proxy.name = 'will'
console.log(proxy.name) // will
9.1.4 捕获器不变式

为了防止捕获器出现一些另类的行为,系统对捕获器也有一些限制。比如如果一个对象的属性是不可操作的,那如果捕获器中的 get 返回的值不是这个属性值时就会报错

let handler = {
    get(){
        return 'will' // 这玩意会报错,因为下面的定义 name 属性是不可改的并且值是 reka
    }
};
let target = {};
Object.defineProperty(target,'name',{
    configurable: false,
    writable: false,
    value: 'reka'
})
let proxy = new Proxy(target,handler)

//报错
//TypeError: 'get' on proxy: property 'name' is a read-only 
//and non-configurable data property on the proxy target 
//but the proxy did not return its actual value (expected 'reka' but got 'will')
console.log(proxy.name)
9.1.5 可撤销代理

通过new Proxy 出来的代理对象会一直存在于目标对象的代理关系不可中断。如果想中断这种关系可以通过 Proxy 的静态函数 Proxy.revocable() 方法,这玩意返回一个对象,对象内部包含代理对象与 revoke() 函数,调用 revoke() 函数后就可以中断代理关系.

let handler = {
    get(){
        return 'will' 
    }
};
let target = {};
let {proxy,revoke} = Proxy.revocable(target,handler)
console.log(proxy.name) // will
revoke() // 调用了撤销函数后就解除了代理关系,后面再用代理就报错了
console.log(proxy.name) // TypeError: Cannot perform 'get' on a proxy that has been revoked
9.1.6 代理嵌套

但是谁会这么沙雕的使用呢~~~ 净整这些花里胡哨

let handler = {
    get(){
        console.log('handle')
        return Reflect.get(...arguments)  
    }
};
let target = {};
let {proxy,revoke} = Proxy.revocable(target,handler)
let handler2 = {
    get(){
        console.log('handle2')
        return Reflect.get(...arguments) 
    }
};
let {proxy: proxy2} = Proxy.revocable(proxy,handler2)
console.log(proxy2.name) // handle2 handle undefined 会经过两层代理

9.2 代理捕获器与反射方法

一共有 13 种捕获器方法,并且每个捕获器方法都有对应的全局反射方法供捕获器调用去模拟原始调用行为。太多了看文档去吧
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect

9.3 代理的一些用法

  1. 跟踪属性访问,比如加日志啥的
  2. 隐藏属性:在捕获器上加判断,如果不想对外暴露的属性直接在 get() 返回 null
  3. 属性验证:在 set() 上加判断,如果参数不合法直接不设置目标对象的属性
  4. 构造函数参数验证:在捕获器的 construct() 中加判断验证构造函数的合法性

注意:代理不仅可以代理实例,还能代理类!!!

const users = [];
class User{
    constructor(name){
        this.name = name;
    }
}

let UserProxy = new Proxy(User,{ // 直接代理类,就问你屌不屌
    construct(){
        const newUser = Reflect.construct(...arguments);
        users.push(newUser);
        return newUser
    }
})
new UserProxy('will')
new UserProxy('reka')
console.log(JSON.stringify(users)) //[{"name":"will"},{"name":"reka"}]

第10章:函数

ES 中函数实际上就是对象,每个函数都是 Funcion 类型的实例,Function 也有自己的属性与方法,跟其他引用类型一样

10.1 箭头函数

箭头函数的 this 是指定义的上下文,箭头函数相对于普通的函数有以下缺点

  1. 不能使用 arguments(就是在函数中可以取到的所有参数的数组)
  2. 不能使用 super
  3. 没有 new.target
  4. 不能用作构造函数
  5. 没有 prototype 属性

10.2 函数名

函数名其实就是指向函数的指针,ES6 的所有函数都有一个只读的 name 属性

function test(){}
console.log(test.name); //test
console.log(test.bind(null).name) //bound test 会加一个前缀 bound
console.log((new Function()).name) //anonymous 匿名的

10.3 理解参数

ES 中的所有函数参数都是值传递!!!!!!!!!
一个比较有意思的东西:函数的 arguments 对象被改变的时候会同步到函数参数变量中,但是 argument 对象与函数的参数是两个地址

function add(num1,num2){
    arguments[1] = 10; // 改这里下面的 num2 也会生效,但是!!!!这个过程只是一个同步的过程并不代表 arguments[1] 与 num2 是同一个地址
    console.log(num1,num2) //1 10
}
add(1,2)

10.4 没有重载

ES6 函数不关心参数的数量与类型,所以也不存在 Java 中的重载。书上说重复定义的同名函数后面定义的会覆盖前面定义的。不过测试结果是如果出现了重名函数会报错~~~~~

function add(num1,num2){
    console.log('add') 
}
function add(num1){
    console.log('add1') 
}
add(1) // 报错

10.5 默认参数值

注意:arguments 参数不体现默认参数值
函数参数的作用域遵循“暂时性死区”,后定义的参数可以用前定义的参数,反过来不行~~

let def = () => { return 1000;}
function add(num1=def()){ // 默认参数也可以使用函数,函数会先求值,后使用
    console.log([...arguments]) // [] 
    console.log(num1) //1000 
}
add() 

10.6 参数扩展与收集

10.6.1 扩展参数

可以使用扩展操作符解析传参,从而实现数组元素挨个传入函数

function fuc(){
    console.log([...arguments]) //[ 1, 2, 3 ] 
}
fuc(...[1,2,3])
10.6.2 收集参数

通过扩展操作符可以将不同长度的独立参数组合成一个数组(类似于 arguments 参数的合成机制),注意:扩展操作符只能放在最后一个参数位置

function fuc(firstValue, ...others){ // 扩展操作符只能放到最后一个参数位置
    console.log(others) //[ 2, 3 ]
}
function fuc2(...others){
    console.log(others) //[ 1, 2, 3 ]
}

fuc(1,2,3)
fuc2(1,2,3)

10.7 函数声明与函数表达式

JS 引擎对于函数声明与函数表达式两种定义函数的方式是区别对待的。函数声明的定义形式会出现「函数声明定义提升」即引擎会把函数的声明提升到顶部;函数表达式则不会出现这种情况,需要严格遵循先定义再使用的顺序(注意 var 定义的函数表达式也不存在声明定义提升)

fuc()  //aaa 函数声明存在变量提升,因此可以先使用后定义
function fuc(){
    console.log('aaa')
}

fuc2() //Cannot access 'fuc2' before initialization
let fuc2 = function(){} // 函数表达式不存在声明提升,因此在定义前使用会报错
fuc3() // 即使使用 var 定义函数表达式也不行
var fuc3 = function(){}

10.8 函数作为值

函数名在 ES 中就是变量,因此函数可以用在任何可以使用变量的地方

10.9 函数内部

函数内部有两个比较特殊的玩意 arguments 与 this

10.9.1 arguments
  1. arugments 函数内部有个 callee 属性指向 arguments 所在的函数
  2. 箭头函数没有 arguments 参数
  3. 严格模式下,这玩意也获取不到,获取 arguments 会报错
function fuc(){
    const {callee} = arguments; // callee 属性指向 arguments 所在的函数
    console.log(callee === fuc) //true
}

fuc();
10.9.2 this

在标准函数下与箭头函数下 this 有不同的行为:箭头函数中 this 指向的是定义箭头函数的上下文对象;标准函数时指向的是把函数当成方法调用的上下文对象(这个上下文对象在运行时才能确定,是会变的)

function say(){ 
    console.log(this.name)
}
let user = {
    name: 'will',
    run: 'running'
}
user.say = say; // say 这种函数名仅仅是保存函数指针的变量,因此全局的 say 与 user.say 是同一个函数,只是运行时的上下文不同
user.say() // will

let run = () => {console.log(this.run)}; // undefined 箭头函数的 this 指向的是定义箭头函数的上下文,因此与运行时无关
user.run = run;
user.run();
10.9.3 caller

arguments 属性中有个 callee 属性指向 arguments 所在的函数;callee 属性有个 caller 属性指向谁调用的这个函数

function inner(){ 
    // arguments.callee.caller  caller代表谁调用了这个函数;callee表示arguments属性归属于哪个函数
    console.log(arguments.callee.caller.name) //out
}
function out(){
    inner();
}
out();
10.9.4 new.target

如果函数是通过 new 调用的则new.target将引用被调用的构造函数,如果是普通调用则是 undefined

function Inner(){ 
    console.log(new.target)  
}
new Inner();//[Function: Inner] 通过 new 调用则指向构造函数(就是 Inner)
Inner()//undefined 普通调用则不展示 new.target

10.10 函数的属性与方法

10.10.1 length prototype 属性

ES中函数是对象

  1. length:参数的个数
  2. prototype:原型对象参考 8 章
function Inner(a){ }
console.log(Inner.length) //1 参数的个数是 1
10.10.2 apply 与 call 方法指定 this

apply 与 call 都能改变函数的 this(实例可以没有这个函数,就生调~~~),这两者除了传参外没啥区别。

  1. call(this,param1,param2,3):call 只能一个一个传参数
  2. apply(this,arguments):apply 可以传一个数组
color = 'red';
let obj = {
    color: 'blue'
}
function sayColor(){
    console.log(this.color);
}

sayColor(); // red  注意:node里面直接测试不行,需要在浏览器环境中才能生效
sayColor.apply(obj,[]) //blue
10.10.3 bind 绑定 this

bind 方法返回一个新的函数实例,这个新的实例 this 值会被绑定到 bind() 的参数对象中,基于这个新的函数实例进行调用的时候 this 始终都是 bind(obj) 中 obj 的值

let color = 'red';
let obj = {
    color: 'blue'
}
function sayColor(){
    console.log(this.color); // blue
    console.log(this === obj) // true 绑定过后 this 就指向 bind 的参数~~~~~~
}
let afterFunc = sayColor.bind(obj)
afterFunc()

10.11 函数表达式

定义函数的两种方式:函数声明与函数表达式。这俩方式的差异就在于理解函数声明提升,函数声明存在提升;函数表达式(不管是 let 的还是 var 的)都不存在提升,都需要先声明后使用~~~

10.12 递归

递归就是函数在内部重新调用自己,可以通过 arguments.callee 来实现优化(了解就好)

function add(num){
    if(num > 0){
        // return num + add(num-1); // add is not a function 由于下面的 add 被赋值成了 null 因此这里会报错
        return num + arguments.callee(num-1)
    }else{
        return 0;
    }
}
let wrapAdd = add;
add = null; // 这里的赋值语句会让上面报错
console.log(wrapAdd(3))

10.13 尾调用优化

function outFuc(){
    return inFuc() // 尾调用
}

上面的例子举例说明了尾调用,尾调用会形成两个函数栈帧(outFuc 一个,inFuc 一个)。如果 inFuc() 不会引用 outFuc() 的变量,那么引擎就可以在执行 inFuc() 时先把 outFuc 栈帧干掉,这样全局就变成了一个栈帧**(节省栈空间)**。尾调用优化的条件如下:

  1. 代码在严格模式下执行
  2. 外部函数的返回值是对尾调函数的调用
  3. 尾调函数返回后不需要有额外的逻辑
  4. 尾调函数不引用外部函数作用域中的变量的闭包

10.14 闭包

通常是在嵌套函数中实现的,指一个函数引用了另一个函数作用域中的变量。
闭包的理解需要详细理解作用域链的原理:每个执行上下文都附属一个变量对象(上下文中所有的方法、变量都在这个变量对象上),如果上下文是函数,则使用活动对象(arguments)与函数的其他命名参数来初始化变量对象。
调用函数时会为这个函数创建一个执行上下文、创建一个作用域链并且会创建两个特殊变量 this 与 arguments。作用域链的第一个元素是当前执行上下文的变量对象,第二个元素是包含此函数上下文的变量对象

function compare(value1,value2){
    return value1 < value2;
}
let result = compare(1,2);

上面函数的变量对象及作用域示意
15.jpg
正常函数执行完毕后,作用域链及变量对象都会销毁。但是闭包的函数执行完毕后作用域链会被销毁,但是变量对象会被保存(因为闭包函数还在用)。只有闭包函数被赋值为 null 才会让 GC 回收这个变量对象,因此会造成内存泄漏

10.14.1 this 对象

如果函数没有使用箭头函数,那么this对象会在运行时会动态绑定到函数上下文中,每个函数在调用的时候会创建两个特殊的变量 this 与 arguments ,内部函数不能直接访问外部函数的这两个参数,但是如果保存一下是可以访问得到
16.jpg
17.jpg

10.15 立即调用的函数表达式

IEEE只是用来模拟块级作用域而已,由于 ES5 尚未支持块级作用域,因此会使用这个防止变量溢出

10.16 私有变量

可以通过闭包模拟出私有变量

function Obj(){
    let param = 'will' // 这个变量是定义在函数内部的,因此外部访问不了
    this.publicFuc = function(){ // 这个方法可以外部访问
        return param;
    }
}

let obj = new Obj();
console.log(obj.param); //undefined
console.log(obj.publicFuc()) //will

第11章:期约与异步函数

11.1 异步编程

同步与异步的对立统一是计算机科学里的基本概念。

11.1.1 同步与异步

同步:代码按照顺序执行
异步:代码不是按照顺序执行,无法推断出程序的状态

11.1.2 以往的异步编程模式

就是各种回调函数,容易形成“回调地狱”

11.2 期约 Promise

11.2.1 Promise/A+ 规范

Promise/A+是一个组织名,E6增加了对 Promise/A+的规范支持

11.2.2 期约基础

ES6 新增了 Promsie 对象作为期约对象,该对象接受一个执行器函数作为参数
let p = new Promise(() => {})

11.2.2.1 期约状态机

Promise 有三种状态:待定pending、解决resolved、拒绝rejected。这三种状态都是私有的,无法通过 js 检测到~

11.2.2.2 解决值、拒绝理由以及期约用例

Promise 不管是转换成 rejected 状态还是 resolved状态,都可以在回调函数中传入对应的值。异步代码会收到这个值

let p = new Promise((resolve,reject) => {
    setTimeout(() => {resolve(123)},100) // reject('reason') 也一样
});
p.then((result) => {
    console.log(result) //123 
},(reason) => {
    console.log(reason) //123  
})
11.2.2.3 通过执行函数控制期约的状态

执行器函数中的代码是同步执行的!!! 执行器函数有两个重要职责:初始化异步行为以及控制状态的最终转换(通过传入的两个函数参数控制)

11.2.2.4 Promise.resolve()

这个方法实例化一个已解决的 Promise,如果传入非期约的值,那么这个方法会把这个值转化成期约(就是包装一下而已)。如果传入的是期约,那这个方法就是个空包装。这个方法是幂等的。
即使是 Promise.resolve(new Error()),期约也是解决的状态
18.jpg

11.2.2.5 Promise.reject()

这个方法实例化一个拒绝的期约并返回一个异步错误(注意,这些玩意无法通过 try catch 捕获,因为这是异步错误)

try {
  let p = Promise.reject('错误理由');
  console.log(p);
} catch (e) {}
// (node:13234) UnhandledPromiseRejectionWarning: 错误理由
11.2.2.6 同步/异步执行的二元性

Promise 的处理是通过浏览器的异步消息队列执行的,因此使用普通的 try catch 拿不到 Promise 的异常错误

11.2.3 期约的实例方法
11.2.3.1 实现 Thenable 接口

期约实现了 Thenable 接口,因此都有一个 then() 方法

11.2.3.2 Promise.prototype.then()
  • 参数:then 方法接收两个参数 onResolve() 与 onReject(),这两个参数都可选,分别是 Promise 被解决与被拒绝的时候调用(注意 .catch() 方法其实就是 onRejct() 函数的语法糖而已,如果then 接收了 onReject() 函数,那 catch 就不会被触发了~~~)
  • 返回:返回一个新的 Promise 实例。这个新的 Promise 实例基于 onResolve() 或 onReject()函数的返回值进行 Promise.resolve() 包裹(都是使用的 Promise.resolve() 包裹!!!)。当 Promise 是一个 resolve 态时:
    • onResolve() 函数有返回值时,通过 Promise.resolve(返回值) 进行包裹返回
    • onResolve() 函数无返回值/没写return时,通过 Promise.resolve(undefined) 进行包裹返回
    • 如果没有提供 onResolve() 函数,则返回上一个 Promise 解决之后的值
    • 抛出异常会返回拒绝的期约
    • 返回错误值不会返回拒绝的期约
let p = new Promise((resolve,reject) => {
    resolve('success');
});

let p1 = p.then((reason) => {return '我返回了 reason'}) // 有函数,有返回值 
let p2 = p.then((reason) => {}) // 有函数,没有返回值
let p3 = p.then(); // 没有函数
let p4 = p.then(() => {
    throw new Error('我错了')
}); // 有函数,抛出异常了
let p5 = p.then(() => {return new Error('我返回了错误')})

setTimeout(() => {
    p1; // Promise {<fulfilled>: '我返回了 reason'}  结论:期约实例,onResolve() 函数的返回值
    p2; // Promise {<fulfilled>: undefined} 结论:期约实例,undefined
    p3; // Promise {<fulfilled>: 'success'} 结论:期约实例,上个期约的解决状态
    p4; //Promise {<rejected>: Error: 我错了\n
    p5; // Promise {<fulfilled>: Error: 我返回了错误\n
},0)
let p = new Promise((resolve,reject) => {
    reject('error');
});


let p1 = p.then(null,(reason) => {return '我返回了 reason'}) // 有函数,有返回值 
let p2 = p.then(null,(reason) => {}) // 有函数,没有返回值
let p3 = p.then(); // 没有函数
let p4 = p.then(null,() => {throw new Error('我错了')}); // 有函数,抛出异常了
let p5 = p.then(null,() => {return new Error('我返回了错误')})


setTimeout(() => {
    p1; // Promise {<fulfilled>: '我返回了 reason'}  结论:期约实例,onResolve() 函数的返回值
    p2; // Promise {<fulfilled>: undefined} 结论:期约实例,undefined
    p3; // Promise {<fulfilled>: 'success'} 结论:期约实例,上个期约的解决状态
    p4; //Promise {<rejected>: Error: 我错了\n
    p5; // Promise {<fulfilled>: Error: 我返回了错误\n
},0)
11.2.3.3 Promise.prototype.catch()

这玩意就是个语法糖,等于 Promise.prototype.then(null,onReject)

let p = new Promise((resolve, reject) => {
  reject('error');
});
p.catch((e) => {
    console.log('catch ' + e); // 这个也能捕获到值
  });
p.then(
  () => {},
  (e) => {
    console.log(e); // 这里会输出,下面的 catch 不会。如果这个函数没有写,那根据前面的规则,then() 会返回上个期约的状态
  },
).catch((e) => {
  console.log('catch ' + e);
});
11.2.3.4 Promise.prototype.finally()

finally 不感知期约的状态,在这里面只做一些资源回收动作。这货基本上都是上个期约的透传,除非他自己抛了一个异常出来。

let p = new Promise((resolve, reject) => {
  reject('error');
});

p.finally(() => {
    console.log('finally')
    throw new Error('finally error')
}).catch((e) => {
  console.log('catch ' + e); //  catch finally error
});
11.2.3.5 非重入期约方法

期约属于微任务,期约执行器中的代码是同步执行,状态落定的代码是异步执行的。当期约落定状态后,落定状态的处理程序仅仅会被排期,但是不会被立即调度。这个特性是由 JS 运行时保证的。这个特性被称为“非重入”特性。这种特性在 onResolve onReject finally catch 都会生效。

console.log(1) 
let p = new Promise((resolve, reject) => {
    console.log(2)
    resolve(3);
    console.log(3)
});

p.then(() => {
    console.log(4)
})
.finally(() => {
    console.log(5)
});
setTimeout(() => {
    console.log(6)
},0)
// 1 2 3 4 5 6 
11.2.3.6 临近处理程序的执行顺序

不管是宏任务还是微任务,都是队列形式,所以顺序都是先进先出~

let p = Promise.resolve();
p.then(() => {
    setTimeout(() => {
        console.log('a')
    })
    console.log('aaa')
})

p.then(() => {
    setTimeout(() => {
        console.log('b')
    })
    console.log('bbb')
})
// 先执行微任务队列再执行宏任务队列,记住所有的异步都是先进先出
//aaa bbb a b
11.2.3.7 拒绝的期约与拒绝错误处理

在 Promise 的执行函数或者处理程序中抛出异常都会导致期约进入拒绝态。对应的错误对象会被转化成拒绝的理由。对于 Promise 这种异常,不会阻塞代码的执行

let p = new Promise(() => {
    throw new Error(); // 这里的抛出异常不会阻塞下面 aaa 的输出
})
console.log('aaa') 
/**
 * aaa
   (node:16654) UnhandledPromiseRejectionWarning: Error: 
 */
11.2.4 几个合并期约的方法
  1. Promise.all([])
    1. 所有参数期约都 resolve 时返回所有参数期约的解决值数组
    2. 有一个或多个 reject 时,返回第一个 reject 的原因
  2. Promie.race([])
    1. 第一个参数 resolve 就会 resolve
    2. 第一个参数 reject 就会 reject
11.2.5 期约扩展

扩展了两个能力:取消与进度更新。不咋用,用到的时候再看吧~~~

11.3 异步函数

就是 async 与 await 的使用~

11.3.1 异步函数
11.3.1.1 async 关键字
  1. 这玩意能用在函数声明、函数表达式、箭头函数、方法上
  2. 这种函数的返回值会被 Promise.resolve() 进行包裹。如果这种函数有 return 语句,那就把 return 的参数;否则包裹 undefined。外部函数调用这种函数的时候可以得到他的期约
  3. 这种函数中抛出异常会返回 reject 的期约
  4. 拒绝的期约(就是 Promise.reject() )不会被异步函数捕获到。这里说的是下面代码中第四行写法,但是!!! 正常人谁他妈这么写啊
async function func1(){}
async function func2(){return 1}
async function func3(){ throw new Error('我错了')}
async function func4(){Promise.reject('reject')} // 如果是 return Promise.reject() 外面是可以捕获到的 这么写就是沙雕


func1().then((reason) => {console.log(reason)})
func2().then((reason) => {console.log(reason)})
func3().then((reason) => {console.log(reason)}).catch((e) => {console.log('捕获到了' + e)})
func4().then((reason) => {console.log(reason)}).catch((e) => {console.log('捕获到了' + e)})
11.3.1.2 await 关键字
  1. await 会暂停异步函数代码的执行,等待 Promise 解决
  2. 在暂停异步代码的时候会让出 JS 线程。在这一点上 await 跟 yield 行为是一样的
  3. await 会尝试**“解包”**对象的值,然后将这个值传给表达式并恢复代码的执行
    1. 解包原始值 await ‘aaa’ -> aaa
    2. 解包一个没有实现 thenable 的对象 await [”,”] -> [”,”]
    3. 解包 thenable 不常用,先忽略吧
    4. 解包期约:
      1. 如果期约已经解决则能拿到 reason
      2. 如果期约 reject,则会将 async 函数直接返回(不需要在 async 函数中 return await ) reject 的期约
      3. 如果 await 表达式的过程中 throw 了一些错误,则** 直接返回** reject 期约
async function func1(){
    // 下面的逻辑会走到 then 
    // return await Promise.resolve('success') 

    // 下面的逻辑会走到 catch 
    await Promise.reject('error') // 注意!!! 这里可以不用写 return,await 可以直接帮着返回。后面的代码就不走了
    console.log('aaa') // 不会执行,因为上面被默认返回了
}

func1().then((reason) => {
    console.log(reason)
}).catch((error) => {
    console.log(error);
})
11.3.1.3 await 限制

只能在异步函数中使用,不支持嵌套,只能出现在 async 直接定义的函数中

11.3.1.4 一个停止与恢复执行的例子

记住一个点就行了:先执行同步代码,遇到 await 后会往微任务队列压一条任务(并且中断当前函数的执行流程),等到本次宏任务的代码全部干完了,就从**队列(是队列!先进先出!!!)**中拿微任务执行(并且暂停函数的执行)

// 1 2 3 4 5 8 9 6 7
async function foo(){
    console.log(2)
    console.log(await Promise.resolve(8))
    console.log(9)    
}
async function bar(){
    console.log(4)
    console.log(await 6)
    console.log(7)    
}
console.log(1)
foo();
console.log(3)
bar();
console.log(5)

最后

人生苦短,效率至上。

欢迎大家微信扫下面二维码,关注我的公众号【趣code】,一起成长~

扫码_搜索联合传播样式-白色版.png

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容