JaveScript.md 134 KB

JavaScipt语法

JS教程 JS参考 MDN文档

迷惑的知识点:

  1. 作用域

    • 作用域的理解
    • 作用域提升
    • 块级作用域
    • 作用域链
    • AO、GO、VO等概念
  2. 函数、闭包

    • 闭包的访问规则
    • 闭包的内存泄露
    • 函数中的this的指向
  3. 面向对象

    • Javascript面向对象
    • 继承
    • 原型
    • 原型链等
  4. ES的新特性

    • ES6、7、8、9、10、11、12
  5. 其他知识

    • 事件循环
    • 微任务
    • 宏任务
    • 内存管理
    • Promise
    • await
    • async
    • 防抖
    • 节流等等

浏览器的工作原理和V8引擎

浏览器工作原理

从图中可知 js、css文件并不是跟index.html文件一起下载下来的,而是需要时才会下载

浏览器内核来解析下载下来的文件

内存 使用
Gecko 早期被Netscape和Mozilla Firefox浏览器使用
Trident 微软开发,被IE4~IE11浏览器使用,但Edge转向使用Blink
Webkit 苹果给予KHTML开发、开源的,用于Sfari,Google Chrome之前也在用
Blink Webkit的一个分支,Google开发,目前应用于Google Chrome、Edge、Opera等
。。。 。。。

浏览器内核常指浏览器的排版引擎

排版引擎(layout engine),也成为浏览器引擎(browser engine)、页面渲染引擎(rendering engine)或样板引擎

浏览器工作流程

  • 浏览器工作流程
    1. 通过HTML Parser把HTML文件解析成DOM Tree
    2. HTML解析的时候遇到JavaScript标签时,停止解析HTML转而去加载和执行Javascript代码
    3. Javascript代码可以对Dom进行操作从而修改Domt Tree,这执行Javascript代码就是js引擎
    4. CSS文件通过CSS Parse解析成Style Rulescss规则
    5. Style RulesDom Tree结合(Attachment)到一起,生成渲染树(Render Tree)
    6. 绘制(Painting)到界面上显示(Display)出来

JavaScript代码通过JavaScript引擎来执行

认识JavaScript引擎,V8引擎的原理

引擎 使用
SpiderMonkey 第一款JavaScript引擎,有Brendan Eich开发
Chakra 微软开发,用于IE浏览器
JavascriptCore WebKit中的JavaScript引擎,由Apple公司开发
V8 Google开发的强大JavaScript引擎,也帮助Chrome从众多浏览器中脱颖而出
。。。 。。。
  1. V8引擎是用C++编写的Goole开源高性能JavaScript和WebAssembly引擎,他用于Chrome和Node.js等
  2. 它实现ECMAScriptWebAssembly,并在Windows7或更高版本,MacOS 10.12+和使用x64、IA-32、ARM或MIPS处理器的linux系统上运行
  3. V8可以独立运行,也可以嵌入到任何C++应用程序中

Js引擎的处理

抽象语法树在线生成网站

Parse解析Javascript源代码(包括词法分析和语法分析)成抽象语法树(AST)
AST可以通过V8引擎中的Ignition库转换成字节码(bytecode),不直接转换成机器码是为了根据运行环境做代码优化和环境适配等


V8引擎本身的源码非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道他是如何对Javascript执行的

  • Parse模块会将Javascriptdiamagnetic转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码

  • Ignition是一个解释器,会将AST转换成ByteCode字节码

    • 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能真实运算)
    • 如果函数只调用一次,Ignition会执行解释执行ByteCode
    • Ignition的V8官方文档https://v8.dev/blog/ignition-interpreter
  • TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码

    • 如果一个函数被多次调用,那么就会被标记为热点函数,那么会经过TurboFan转换成优化的机器码,提高代码的执行性能(直接执行函数机器码,比将字节码转换成机器码再执行更高效)
    • 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(因为JS是弱类型语言,传入参数可以为number也可以为string),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码
    • TurboFan的V8官方文档https://v8.dev/blog/turbofan-jit

V8引擎的解析图

  1. Blink是Chrome内核,在解析到JavaScript时将JS代码通过流(Stream)的方式传到V8引擎
  2. 引擎中会将编码进行转换,再转换成Scanner(扫描器,做词法分析)
  3. Scanner将代码转换成许多Tokens,再通过Parse解析转化成AST
    • Parser就是直接将Tokens转成AST树结构
    • PreParser称之为与解析
      • 并不是所有的JavaScript代码在一开始的时候就会被执行,所以一开始对所有JS代码进行解析会影响效率
      • V8引擎实现了Lazy Parsing(延迟解析)的方案,他的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用才会进行
      • 比如在函数Outer()内部定义了Inner()函数,那么Inner()函数就会进行预解析

Parse阶段时,V8引擎会自动创建GlobalObject对象,比如Stirng、Date、Number、SetTimeout()、window...等都是GlobalObject的成员属性,所以在JS代码中可以字节调用这些对象

简易编译流程

// 测试代码
var name = "why";
console.log(num1);
var num1 = 10;
var num2 = 20;
var result = num1 + num2;

foo(1)

function foo(num){
    var m = 10;
    var n = 20;
    console.log('foo');
}
  1. 解析代码,v8引擎内部会创建一个GlobalObject对象
var GlobalObject = {
    String : "类",
    Date : "类型",
    setTimeOut : "函数",
    name : undefined,
    num1 : undefined,
    num2 : undefined,
    result : undefined
    foo : 0xa00
}

String、Date、setTimeOut是GlobalObject自带的对象
name、num1、num2、result是解析出来的变量并添加到GlobalObject
因为此时只是解析阶段,所以name、num1、num2、result都是undefined
foo存储的函数地址

  1. 运行代码
  • 为了执行代码,v8引擎内部会有一个执行上下文栈(函数调用栈)(Execution Context Stack)
  • 因为上述例子是全局代码,v8提供全局执行上下文(Global Execution Contenxt)
  • 这些上下文中存在一个VO(variable object)(变量对象),其中VO分为GOAO两种,全局上下文中VO = GO,函数执行上下文中VO = AO

  • 代码从上下往下依次执行,从VO中取出目标对象并为其赋值

V8引擎的解析图

foo存储函数空间中存在一个父级作用域(parent scope),可以获得父级作用域中的数据,当foo作用域中的使用的数据在foo作用域中没找到就往父级作用域(parent scope)中查找,如果一直没有最终会找到全局作用域


var message = "Hello Global";
function foo(){
    console.log(message);
}
function bar(){
    var message = "Hello Bar";
    foo();
}

bar();      // 输出 Hello Global
  1. 解析代码,v8引擎内部会创建一个GlobalObject对象

foo的父级作用域(parent scope)就是GlobalObject bar的父级作用域(parent scope)也是GlobalObject

  1. 执行代码

赋值。。。

V8引擎的解析图



function foo(){
    m = 100;
}
foo();
console.log(m);

function foo1(){
    var a = b = 10;
    /*
    等价于
    var a = 10;
    b = 10;
    */
}

foo1();
console.log(a);
console.log(b);

  1. 首先,对于foo函数来说,如果没有用var或者let定义变量,则m会被直接定义到全局变量(GO)中,所以对于console.log(m)会输出100而不是报错
  2. 对于foo1函数来说,var a = b = 10;会被理解为var a = 10; b = 10;,根据1的解释,b会被定义到全局变量中,而a还在foo1的AO中,所以最后console.log(a)会报错,而console.log(b)会输出10

环境变量和记录

每一个执行上下文会关联到一个环境变量(Variable Environment)中,在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中

对于函数来说,参数也会被作为环境记录添加到变量环境中
所以对于上面的解释来说,将不再是VO(变量环境),而是环境记录(VariableEnvironment),也就是说不一定是O(object),只要是记录都行(map或者其他可以记录的类型)

function foo() {
    console.log(n)  // 输出 undefined
    var n = 200
    console.log(n)  // 输出 200
}
var n = 100;
console.log(n)

针对上面的测试用例,第二个输出为 200 应该很好理解,第一个输出为什么是 undefined 呢?

因为函数 foo 本身也有有一个函数作用域 FEC,与全局作用域变量初始化一样,先给定义每个变量,再根据执行顺序给各个变量赋值,由于函数 foo 内定义的变量 n,所依此时函数作用域内会定义变量 n 并赋值为 undefined (这个 n 与全局变量中的 n 不是同一个)

那么当第一个 console.log(n) 执行的时候 n 并未赋值,所以会输出 undefined

var a = 100
function foo() {
    console.log(a)  // 输出 undefined
    return 
    var a = 100
}

foo()

这个输出 undefined 与上面的案例同理,虽然这次 foo 函数中的变量 a 定义在 return 之后,是永远不会被执行的,但是代码在解析阶段是不会执行的,也就是不知道会被 return 掉,所以在 foo 的作用域内依旧初始化了变量 a 并且赋初值为 undefined

内存管理

不管什么语言,在代码执行的过程中都需要分配内存,不同的是某些语言需要手动管理内存,某些编程语言可以帮助管理内存

  • 一般而言,内存管理存在如下的生命周期

    1. 分配你申请大小的内存
    2. 使用分配的内存
    3. 不需要使用时,释放内存
  • JavaScript会在定义变量时为我们分配内存

    • JS对于基本数据类型内存的分配会在执行时,直接在栈空间进行分配
    • JS对于复杂数据类型内存的分配会在堆内存中开辟空间,并在将这块空间的指针返回值变量引用

因为内存是有限的,所以当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间
再手动管理内存的语言中,我们需要通过一些方式来释放不再需要的内存,比如free函数:

1. 手动管理的方式会**影响编写逻辑的代码的效率**
2. 对开发者**要求较高**,不小心就会产生**内存泄漏**
  1. 引用计数的垃圾回收算法
  2. 标记清除的垃圾回收算法(JS使用)
    • 设置一个跟对象(Root Object),垃圾回收器定期从这个根开始,找所有从根开始有引用到的对象,对于那些没有引用到的对象,就认为是不可用的对象
    • 可以解决引用计数的循环引用问题

JS引用使用标记清除算法,V8引擎为了更好的优化,它在算法的实现细节上也会结合一些其他的算法

闭包的使用

JS中函数是一等公民,函数可以作为参数传递、作为返回值

function foo(func){
    func();
}

function bar(){
    console.log("aaa");
}

function run(){
    function rush(){
        console.log("bbb");
    }
    return rush;
}

foo(bar);       
var fn = run();
fn();

高阶函数:一个函数如果接受另外一个函数作为参数,或者该会将另外一个函数作为返回值,就被称为高阶函数

比如上述 run() 就是高阶函数
当函数属于某个对象时,称该函数为对象的方法

  • 闭包词法闭包函数闭包

    • 是在支持头等函数的编程语言中,实现词法绑定的一种技术
    • 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表)
    • 闭包跟函数最大的区别在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行
  • 闭包解释2

    • 一个函数和对其周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包
    • 闭包能让你可以在一个内层函数中访问到其外层函数的作用域
    • 在JS中,每当创建一个函数,闭包就会在函数创建的同时被创建出来
function run(){
    var name = "run";
    function rush(){
        console.log(name);
    }
    return rush;
}

var fn = run();
fn();

代码的内存解析

JS 函数的 this 指向

在常见的编程语言中,this 通常只出现在类的方法中,但是在 javascript 更加灵活

JS函数式编程(编程范式、规范方式)

函数第一公民 可以作为形式参数和返回值

apply、call、bind

JS纯函数(pure funtion)

  • 纯函数的定义
    • 此函数在同样的输入值时,需产生相同的输出
    • 函数的输出和输入值与以外的其他隐藏信息或状态无关,有和由IO设备产生的外部输出无关
    • 该函数不能有语义上可观察的函数副作用,诸如触发事件使输出设备输出,或更改输出值以外文件的内容

副作用:在执行一个函数时,除了返回函数值以外,对调函数产生了附加的影响,比如修改了全局变量修改参数或者改变外部的存储
副作用往往是产生BUG的温床

var names = ["avc", "cba", "eax", "fas"];
// 纯函数,确定输入确定输出,没有副作用(没有修改外部变量等,原来的数组name没有被修改)
var name2 = names.slice(0, 3);   
// 非纯函数,调用之后原来的数组name被改变了    


function foo1(num1, num2){  // 纯函数
    return num1 + num2;
}

var name = "log";
function foo2(num1, num2) { // 非纯函数 修改了外界的值
    name = "log1";
    return num1 + num2;
}
  • 纯函数的优势
  • 安心的编写和安心的使用
  • 写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关系传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改
  • 在用的时候,你确定的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出

柯里化

  • 维基百科的解释
    • 卡瑞化或加里化
    • 把接受多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的新函数的技术
    • 柯里化生成如果你固定某些参数,你将得到接受余下参数的一个函数
    • 传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数
function foo(m, n, x, y){
    return m + n * 2 + x * 3 + y * y;
}
foo(1, 2, 3, 4);

function bar(m){
    
    return function(n){
        n = n * 2;
        return function(x){
            x = x * 3;
            return function(y){
                y = y * y;
                return m + n + x + y;
            }
        }
    }
}

bar(1)(2)(3)(4);

// 简易柯里化写法
var bar2 = m => n => x => y => m + n * 2 + x * 3 + y * y;
var bar2 = m => n => x => y => {
    return m + n * 2 + x * 3 + y * y;
}

function foo()function bar()的过程 称为柯里化

  • 柯里化的作用
    • 让函数职责单一
    • 在函数式编程中,我们往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理
    • 我们是否就可以将每次传入的参数在单一的函数中进行处理,处理完后在下一个函数中再使用处理后的结果
    • 让代码可以复用
// 柯里化的代码复用

function MakeAddr(num){
    return function Addr(count){
        return count + num;
    }
}

var addr = MakeAddr(5);
addr(1);    // 5 + 1
addr(2);    // 5 + 2
addr(3);    // 5 + 3

// 普通写法

function Add(m , n){
    return m + n;
}
Add(5, 1);  // 5 + 1
Add(5, 2);  // 5 + 2
Add(5, 3);  // 5 + 3

如果需要频繁对一个数进行加减处理,使用柯里化的代码比普通写法的字母数更少(不用每次都写"5,")

// 函数的形参个数
function adddd(x, y, z){

}
console.log(adddd.length);  // 输出3,即表示该函数有三个形参

// 自动柯里化函数
function hyCurring(fn){
    function curried(...args){
        if(args.length >= fn.length){
            return fn.apply(this, args);
            // return fn.call(this, ...args);
            // return fn(...args);
        }
        else {
            function curried2(...args){
                return curried.apply(this, args.concat(args2));
            }
        }
    }
    return curried;
}

组合函数

  • 组合函数是在JS开发过程中一种对函数的使用技巧模式
    • 比如需要对一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的,那么每次都需要进行两个函数的调用,操作上就会显得重复
    • 那么可以将两个函数组合起来,自动依次调用
    • 这个对函数的组合过程称之为组合函数
function double(num){
    return num * 2;
}

function square(num){
    return num ** 2;
}

var count = 10;
var result = square(double(count));
var count1 = 10;
var result1 = square(double(count));
var count2 = 10;
var result2 = square(double(count));

function composeFn(m, n){
    return function(count){
        n(m(count));
    }
}

var newFn = composeFn(double, square);
count3 = 10;
result3 = newFn(count3);    // 组合了 double和square的函数


function hyCompose(...fns){
    val length = fns.length;
    for(let i = 0; i < length; i++){
        if(typeof fn[i] !== 'function'){
            throw new TypeError("");
        }
    }

    function compose(...args){
        var index = 0;
        var result = length ? fn[index].apply(this, args) : args;
        while(index < length){
            result = fns[index].call(this, result);
        }
    }
    return compose;
}

JS其他函数知识

var message = "VO : GO";
var obj = {name : "Y", message = "Obj message"};

function foo() {
    function bar() {
        with(obj){
            console.log(message);
        }
    }
    bar(); 
}

foo();  // 输出 Obj message

with() {}语句用于定义对象查找作用域
不建议使用with语句,存在兼容性问题

var jsString = 'var message = "hello world"; console.log(message);'
eval(jsString;)

通过eval来将字符串翻译成js语句并执行
Google Chrome报错,不推荐在开发中使用eval

可读性差
运行中可能被篡改
不能被js引擎优化,因为是eval去执行的不经过引擎

JS的面向对象

面向对象是现实的抽象方式

对象是JavaScript中一个非常重要的概念,因为对象可以将多个相关联的数据封装到一起,更好的描述一个事物

  • JavaScript支持多种编程范式,包括函数式编程面向对象编程
    • JS对象被设计成一组属性的无序集合,像是一个哈希表,有K/V组成
    • key是一个标识符名称value可以是任意类型,也可以是其他对象或函数类型
    • 如果值是一个函数,我们称之为对象的方法

创建对象

// 创建对象 方式1 使用Object类和new关键字来创建对象
var obj2 = new Object();
obj.name = "y";
obj.age = 15;
obj.height = 180;

// 创建对象 方式2 通过 字面量 的方式
var obj = {
    name : "y",
    age : 15,
    height : 180,
    eat : function() {
        console.log("在吃饭");
    }
};

{}是字面量,可以立即求值,而new Object()本质上是方法(只不过这个方法是内置的)调用,既然是方法调用,就涉及到在proto链中遍历该方法,当找到该方法后,又会生产方法调用必须的堆栈信息,方法调用结束后,还要释放该堆栈

操作对象属性

var obj = {
    name : "y",
    age : 16
};

console.log(obj.name);      // 获取属性
obj.name = "j";             // 修改属性
delete obj.name;            // 删除属性

for (var key in obj){       // 遍历属性
    console.log(key);
}

上述对象的属性都是直接定义在对象内部,或者直接添加到对象内部的,这样做不能对这个属性进行一些限制:比如是否可以delete/被遍历等

为了对属性进行比较精准的操作控制,我们可以使用属性描述符,通过属性描述符可以精准的添加或修改对象的属性,属性描述符需要使用Object.defineProperty来对属性进行添加或修改

// obj : 对象、prop : 属性、descriptor : 属性描述符
Object.defineProperty(obj, prop, descriptor);

Object.defineProperty()会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

该方法会返回被修改的对象,原本的对象也会被修改,所以可以不用接收函数返回值,并且可以发现该函数并非纯函数

  • 属性描述符
    • 数据属性描述符
    • 存取属性描述符
configurable enumerable value writable get set
数据描述符 可以 可以 可以 可以 不可以 不可以
存取描述符 可以 可以 不可以 不可以 可以 可以
  • configurable 属性是否可以通过delete删除属性,是否可以修改其特性或者修改为存取描述符

    • 直接在一个对象上定义某个属性时,默认为true
    • 通过属性描述符定义也给属性时,默认为false
  • enumerable 属性是否可以通过for-in或者Object.keys()返回该属性

    • 直接在一个对象上定义某个属性时,enumerable为true
    • 通过属性描述符定义属性时,enumerable为false
  • writable 是否可以修改属性的值

    • 直接在一个对象上定义某个属性时,writable为true
    • 通过属性描述符Object.defineProperty定义一个属性时,writable为false
  • value 的具体值,读取属性时返回该值,修改属性时修改该值

    • 默认情况下valueundefined
  • get/get,为获得和设置使用的函数

  • 存取属性描述符:只能设置configurable,enumerable,get,set

  • 数据属性描述符,只能设置configurable,enumerable,writable,value

个人理解:
对于不需要进行额外处理的数据可以使用数据属性描述符,比如PI = 3.1415926
对于需要额外处理的数据,比如年龄只能是10~20就需要使用存取属性描述符

var obj = {
    name : "y",
    age : 16
};

// obj对象不存在height属性,则先添加height属性,再设置其属性描述
Object.defineProperty(obj, 'height', {
    value : 180
});  

console.log(obj);

这里obj并不会输出height属性,因为height是通过属性描述符添加的所以enumerable默认为false

// configurable展示
// obj就是前面的obj
Object.defineProperty(obj, 'height', {
    value : 180,
    configurable : false,
});  
delete obj.name
console.log(obj.name);    // undefined

delete obj.height
console.log(obj.height);    // 180 没有被删除

Object.defineProperty(obj, 'height', {
    value : 200,
    configurable : true,
});  
console.log(obj.height);    // 180 没有被修改成200,因为configurable最开始是false
// enumerable展示
Object.defineProperty(obj, 'height', {
    value : 180,
    configurable : false,
    enumerable : true
});  
console.log(obj);       // 此时可以正常打印出height属性

var obj = {
    name : "y",
    age : 16 ,
    _address : "private"    // 个人习惯:前面有个下划线表示私有变量
};

Object.defineProperty(obj, 'address', {
    configurable : true,
    enumerable : true,
    get : function () {
        return this._address;  
    },
    set : function(value){
        this._address = value;
    }
});  

这里使用address属性作为_address属性的代理
因为_address作为私有变量不希望外界随意读取,所以使用address代理的方法

  • 定义多个属性描述符
var obj = {
    _age : 0,
}
Object.defineProperties(obj, {
    name : {
        configurable : true,
        enumerable : true,
        writable : true,
        value : "y"
    },
    age : {
        configurable : true,
        enumerable : true,
        get : function (){
            return this._age;
        },
        set : function (value) {
            this._age = value;
        }
    }
});
console.log(obj);
  • 另一种使用get/set的方法
var obj = {
    _age : 10,
    set age(value){
        this._age = value;
    },
    get age() {
        return this._age;
    }
}
obj.age = 20;
console.log(obj);
  • 获得对应属性的属性描述符
var obj = {
    _age: 0,
}
console.log(Object.getOwnPropertyDescriptor(obj, "_age"));
  • 获得对象的所有属性描述符
var obj = {
    name: "y",
    age: 16,
    _address: "private"
};
console.log(Object.getOwnPropertyDescriptors(obj));

对对象的限制

  • 禁止对象继续添加新的属性
Object.preventExtensions(obj);
  • 禁止对象配置/删除属性
Object.seal(obj);
  • 让对象属性变成不可修改(writable : false)
Object.freeze(obj);

创建多个对象的方案

如果所有对象都使用前面所说的字面量方式来创建,那么会出现特别多的重复代码

  1. 工厂方法创建对象
function createPerson(name, age){
    var person{};
    person.name = name;
    person.age = age;

    return person;
}
var p1 = createPerson("name1", 10);
var p2 = createPerson("name2", 11);
var p3 = createPerson("name3", 12);
  1. 构造函数:创建对象时会调用的函数
function foo(){
    console.log("hello world");    
}

var f = new foo();
var f2 = new foo;   // 如果不需要传递参数,可以不要小括号
console.log(f2);    // 返回了一个foo类型的对象

当使用new关键字之后,foo就从普通函数变成了构造函数

  • 如果一个函数被new操作符调用,那么会执行如下操作
    1. 内存中创建一个新的对象(空对象)
    2. 对象内部的[[prototpe]]属性会被赋值为该构造函数的prototype属性
    3. 构造函数内部的this,会指向创建出来的新对象
    4. 执行函数的内部代码(函数体代码)
    5. 如果构造函数没有返回非空对象,则返回创建出来的新对象
function Person(name, age, height){
    this.name = name;
    this.age = age;
    this.height = height;

    this.eating = function (){
        console.log(this.name + "正在吃饭");
    }
}

var p1 = new Person("张三", 18, 180);
var p2 = new Person("李四", 10, 180);

console.log(p1.eating === p2.eating);   // false

对比方法1的工厂方法,该方法可以明确知道p1这个变量是什么类型
p1和p2的函数并不是相同,可见每个函数都开辟了一个内存空间,存在浪费的问题

原型

JavaScript当中每个对象都有一个特殊的内置属性[[prototype]],这个特殊的对象可以指向另一个对象,一般把[[prototype]]称为隐式原型(一般看不到、不会改、用不到)

prototype是原型的意思,在浏览器中可以使用obj.__proto__来查看[[prototype]](部分浏览器支持)

隐式原型


  • 原型的作用
var obj = {};
obj.__proto__.age = 10;
console.log(obj.age);
  1. 调用[[get]]操作
  2. 在当前对象中去查找对应的属性,如果找到就直接使用
  3. 如果没有找到,那么会沿着原型链去查找[[prototype]](可以用来实现继承等操作)

  • 函数的原型

函数是一个对象,所以也有隐式原型[[prototype]]
函数存在一个显示原型prototype

[[prototype]]prototype不是一个东西,前者是理论名称,后者是实际属性
fun.__proto__中的__proto__并不是标准支持的,而是部分浏览器为了方便程序员debug而增加的,__proto__的作用是为了显式的显示对象的[[prototype]]这种理论上的隐式原型对象

function Person() {

}

// 函数也是一个对象,所以也有隐式原型[[prototype]]
console.log(Person.__proto__);                     // {}
console.log(Person.prototype);                     // {}
console.log(Object.getOwnPropertyDescriptors(Person.prototype));                     // 输出一个constructor属性,指向构造函数本身

var p1 = new Person();
var p2 = new Person();
console.log(p1.__proto__ === Person.prototype);    // true

Person.prototype.name = "y";
console.log(p1.name, " ", p2.name);

上面有对new的调用操作进行解释,其中第二点对象内部的[[prototpe]]属性会被赋值为该构造函数的prototype属性的意思就是将返回对象的__proto__赋值等于函数的prototype,所以console.log(p1.__proto__ === foo.prototype)返回值为true

原型

foo.prototype = {
    name : "y",
    age : 19,
}
Object.defineProperty(foo.prototype, "constructor", {
    enumerable : false,
    configurable : true,
    writable : true,
    value : foo
});

直接修改prototype对象,但是prototype中必须存在一个constructor属性指向本身


  • 原型与构造函数结合
function Person(name, age){
    this.name = name;
    this.age = age;
}

Person.prototype.eating = function() {
    console.log(this.name + "吃东西");
}

var p1 = new Person("x", 10);
var p2 = new Person("y", 10);
p1.eating();
p2.eating();

原型链和继承

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

var p1 = new Person("x", 10);
var p2 = new Person("y", 10);

上述的Person应该称之为构造函数,但是对其他语言来说更像是一个

  • 继承:可以将重复的代码和逻辑抽离到父类中,子类只需要直接继承过来使用即可
  • JS通过原型链实现继承

原型链

var obj = {
    name : "y"
}

console.log(obj.address);

obj对象并没有address属性,所以回去obj.__proto__原型上查找,如果也没有就会在obj.__proto__.__proto__上去查找直到找到或者顶层原型为止,这种类似链表的查找方式就是原型链

原型链

顶层__proto__就是Object.__proto__

var obj = {};
console.log(obj.__proto__ === Object.prototype);    // true

继承原型

function Person(name, age){
    this.name = name;
    this.age = age;
}
Person.prototype.running = function() {
    console.log(this.name + " is running");
}

function Student(sno){
    this.sno = sno;
    this.friends = [];
}
var p = new Person("1", "2");
Student.prototype = p;
Student.prototype.studying = function(){
    console.log(this.name + " is studying");
}

function Teacher(title){
    this.title = title;
}

Teacher.prototype.teaching = function() {
    console.log(this.name + " is teaching");
}

var s1 = new Student(1);
var s2 = new Student(2);
s1.running();
s1.studying();

继承原型

上述代码的内存解释
代码中能明显发现问题:s1、s2是两个对象公用同一个Person对象的引用会互相影响

借用构造函数继承

为了解决原型链继承中存在的问题,开发人员提供了一种新的技术:constructor stealing(借用构造函数、经典继承、伪造对象)

  • 借用继承的做法非常简单:在子类构造函数的内部调用父类型构造函数
    • 因为函数可以在任意的时刻被调用
    • 因此通过apply()call()方法也可以在新创建的对象上执行构造函数
function Person(name, age, friends){
    this.name = name;
    this.age = age;
    this.friends = friends;
}

Person.prototype.eating = function() {
    console.log(this.name + " eating");
}

var p = new Person();
function Student(name, age, friends, sno) {
    Person.call(this, name, age, friends);
    this.sno = sno;
}
Student.prototype = p;

var s1 = new Student("y", 10, ["1", "2"], 1);

本质上是Student借用执行Person的构造函数的执行过程,本质其实是给Student赋值

借用构造函数继承

借用构造函数继承的内存模型

  • 借用构造函数的弊端
    • 至少会调用两次基类的构造函数(Person函数执行了两次)
    • 子类的原型对象上多出了一些属性(从内存图可见Student和Teacher存在部分相同的属性)

原型式继承函数

一种继承方法,不是通过构造函数实现的方法

var obj = {
    name : "y",
    age : 16
};

function createObject(protoObj){
    var newObj = {};
    Object.setPrototypeOf(newObj, protoObj);    // 设置newObj的原型为protoObj
    return newObj;
}

// 不适用Object函数库实现设置原型的方法
function createObject2(protoObj){
    function Fn() {}
    Fn.prototype = protoObj;
    var new Obj = new Fn();
    return newObj;
    // newObj.__proto__ = protoObj; // 不可这么写,因为__proto__不是所有js引擎都支持
}

// 创建info对象的原型指向obj对象
var info = {};
console.log(info);
console.log(info.__proto__);

info = Object.create(obj);  // 功能等价于 createObject 和 createObject2

Object.setPrototypeOf(newObj, protoObj);设置newObj的原型为protoObj

寄生式继承

寄生式继承的思路是结合原型类继承工厂模式的一种方式
即创建一个封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再将这个对象返回

var personObj = {
    running = function() {
        console.log("running");
    }
}

function createStudent(person, name){       // 工厂函数
    var stu = Object.create(person);        // 原型式继承
    stu.name = name;                        
    stu.studying = function() {
        console.log("studying");
    }
}

var stu1 = createStudent(person, "x");
var stu2 = createStudent(person, "y");

每个对象的studying()方法都是新建的
stu1stu2没有明确的类型(consolo.log一下就知道)

寄生组合式继承(最终方案)

function CreateObject(o){
    function Fn (){}
    Fn.prototype = o;
    return new Fn;
}

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

Person.prototype.running = function() {
    console.log(this.name + " running");
}

function Student(name, age, friends, sno, score) {
    Person.call(this, name, age, friends);
    this.sno = sno;
    this.score = score;
}

Student.prototype = CreateObject(Person.prototype);
Student.prototype.studying = function() {
    console.log(this.name + " studying " + this.score);
}

var stu = new Student("x", 10, [], 1, 100);
console.log(stu);   //Person { name: 'x', age: 10, friends: [], sno: 1, score: 100 }
stu.running();      // x running
stu.studying();     // x studying 100

输出stuPerson类的,因为输出的是constructorname属性,而这里的constructor使用的是Person的所以最后输出的名字是Person

function inheritPrototype(SubType, SuperType){
    SubType.prototype = CreateObject(SuperType.prototype);
    Object.defineProperty(SubType.prototype, 'constructor', {
        enumerable : false,
        configurable : true,
        writable : true,
        value : SubType
    });
}
inheritPrototype(Student, Person);

手动设置StudentconstructorStudent自己就行了

原型判断方法补充

var obj = {
    name : "w",
    age : 19
};

var info = Object.create(obj, {
    address : {
        value : "BJ",
        enumerable : true
    }
});

console.log(info);
console.log(info.hasOwnProperty('address'));    // true
console.log(info.hasOwnProperty('name'));       // false

console.log("address" in info);                 // true
console.log("name" in info);                    // true

创建对象时,为新对象添加属性描述符
info.hasOwnProperty('address')判断属性是否是自己的属性
"name" in info判断对象是否存在name属性


instanceof用于检测构造函数的prototype是否出现在某个实例对象的原型链上

function CreateObject(o){
    function Fn (){}
    Fn.prototype = o;
    return new Fn;
}

function inheritPrototype(SubType, SuperType){
    SubType.prototype = CreateObject(SuperType.prototype);
    Object.defineProperty(SubType.prototype, 'constructor', {
        enumerable : false,
        configurable : true,
        writable : true,
        value : SubType
    });
}

function Person(){
}

function Student() {
}

inheritPrototype(Student, Person);

var stu = new Student()
console.log(stu instanceof Student);    // true
console.log(stu instanceof Person);     // true
console.log(stu instanceof Object);     // true

instanceof后面的必须是构造函数

原型的继承关系

JavaScript当中每个对象都有一个特殊的内置属性[[prototype]],这个特殊的对象可以指向另一个对象,一般把[[prototype]]称为隐式原型(一般看不到、不会改、用不到)

var obj = {};
console.log(obj.__proto__);

函数是一个对象,所以也有隐式原型[[prototype]]
函数存在一个显示原型prototype

当创建一个函数后,JS引擎会自动给函数对象添加属性Foo.prototype = { constructor : Foo }
定义Foo()函数时,相当于new Funtion()创建函数对象,这时编译器执行Foo.__proto__ = Function.prototype,而Function.prototype = { constructor : Function }

Javascript Object Layout

Function是极为特殊的对象,它的prototype__proto__相等

function Foo(){

}

console.log(Foo.__proto__);
console.log(Foo.prototype); 
console.log(Foo.prototype === Foo.__proto__);   // false
console.log(Foo.prototype.constructor);         // Function : Foo
console.log(Foo.__proto__.constructor);         // Function : Function
console.log(Function.prototype === Function.__proto__); // true

Javascript Object Layout

Javascript Object Layout

ES6~ES12

JS面向对象(ES6及后续版本,前面是旧版JS的创建对象,比较复杂)

理论上 class 的底层实现方式还是 上述的旧版创建代码
使用babel可以将代码装成旧版本代码

  1. 每一个类都有自己的构造函数(方法),这个方法的名称固定为constructor
  2. 通过new操作符,操作一个类的时候会调用类的constructor方法
  3. 每个类只能有一个constructor方法,如果有多个会抛出异常
// 类声明
class Person{
    constructor(name, age){
        this.name = name;
        this.age = age;
        this._address = "";
    }

    eating() {
        console.log(this.name + " eating");
    }

    running() {
        console.log(this.name + " running");
    }

    // 访问器
    get address(){
        return this._address;
    }

    set address(value){
        this._address = value;
    }

    // 静态方法
    static createPerson(){
        return new Person("", 1);
    }
};

console.log(Person.prototype);
console.log(Person.prototype.constructor);  // 指向当前Person
console.log(typeof Person);                 // function

// 类的表达式 用的比较少
var Animal = class {
};

var p1 = new Person("x", 1);
var p2 = new Person("y", 2);
  • 类的继承

super关键字,一般用在三个地方:子类的构造函数、实例方法、静态方法
子类的构造函数中使用this或者返回默认对象之前,必须先通过super调用父类的构造函数

class Person{
    constructor(name, age){
        this.name = name;
        this.age = age;
    }
    eating() {
        console.log(this.name + " eating");
    }
};

class Student extends Person {
    constructor(name, age, sno){
        super(name, age);   // 调用父类构造方法
        // super.eating();  // 调用父类的方法
        this.sno = sno;
    }

    // 方法的重写
    eating() {
        console.log("Student " + this.name + " eating");
    }
};

  • 使用babel转换ES6为ES5

在线babel网站

babel转换代码

继承内置类

class MyArray extends Array{
    firstItem(){
        return this[0];
    }

    lastItem(){
        return this[this.length-1];
    }
}

var arr = new MyArray(1, 2, 3);
console.log(arr.firstItem());
console.log(arr.lastItem());

扩展数组功能

类的混入 mixin

Javascirpt的类只支持单继承,也就是说它只能有一个父类

class Person{

}

class Runner {
    running(){

    }
}

class Eater {
    eating() {

    }
}

function mixinRunner(BaseClass){
    class NewClass extends BaseClass {
        running() {
            console.log("running");
        }
    }
    return NewClass;
}

class Student extends Person{

}

var NewStudent = mixinRunner(Student);
var ns = new NewStudent();
ns.running();

通过mixinRunner扩展类的功能

多态

字面量增强写法

  1. 属性的简写(Property Shorthand):当外界存在变量,并且字面量的key跟外界变量名相同,可以直接变量名存入KV(下面的name和age的例子)
  2. 方法的简写(Method Shorthand):字面量绑定函数箭头函数的this作用域不同,箭头函数的this是父级作用域
  3. 计算属性名(Computed Property Names):可以使用[name+123]来设置key的名称(下面的[name+123]例子)
var name = "w";
var age = 10;
var obj = {
    name,
    age,
    foo : function() {
        console.log(this);
    },
    bar() {
        console.log(this);
    },
    baz : () => {
        console.log(this);
    },
    [name + 123] : "hgggg"
}

console.log(obj);
obj.foo();
obj.bar();
obj.baz();

数组的解构

var names = ["1", "2", "3"];

// 解构数组
var [item1, item2, item3] = names;
console.log(item1, item2, item3);   // 1 2 3

// 解构后面的元素
var [, itema, itemb] = names;
console.log(itema, itemb);          // 2 3

// 解构一个元素,后面的放到新数组
var [itemc, ...newNames] = names;
console.log(itemc);                 // 1
console.log(newNames);              // 2 3

// 结构默认值 如果没有解构出来的值
var [item11, item22, item33, item44 = "aaa"] = names;
console.log(item44);

对象的解构

var obj = {
    name : "w",
    age : 15,
    height : 190
};

var {name, age, height} = obj;
console.log(name, age, height);

var {height, age, name} = obj;      // 读取顺序无关
console.log(name, age, height);

var {name : NewName} = obj;         // 给换个新的变量名
console.log(NewName);               // w

var {address : newAddress = "BJ"} = obj;    // 设置默认值
console.log(newAddress);

let、const、var

  • let 定义变量
    • 变量会被创建在包含他们的词法环境被实例化时,但是是不可以访问他们的,直到词法绑定被求值

一般而言,我们声明的变量和环境记录是被添加到环境变量中的,但是ECMA标准并没有规定这个对象是window对象还是其他对象,不同的引擎会有不同的实现。V8引擎是通过VariableMap一个HashMap来实现存储的。而window对象早期是GO对象,在新的实现中其实是浏览器添加的全局对象,并且一直保持了Windowsvar之间值的相等性

console.log(foo);   // undefined 不报错
var foo = "foo";

console.log(bar);   // Error 
let bar = "bar";

bar实际上是被创建了出来,但是不能访问


  • const :constant 常量/衡量
    • 本质上是传递的值不可以修改
    • 如果传递的是引用类型,内存地址不可更改,但是可以修改内存里面的值
const name = "a";
name = "B";             // error

const obj = {};
obj.name = "B";         // success

块级作用域

ES5之前只有两个东西会形成作用域:

  1. 全局作用域
  2. 函数作用域

作用域可访问外部对象,外部对象不可访问作用域内部数据

ES6开始出现块级作用域,对var声明的对象无效,对let、const、class、function声明的类型有效

{
    let x = "x";
    var y = "y";

    function demo(){
        console.log(x);
    }
}

console.log(y);     // y
console.log(x);     // Error
demo();

demo()方法可能可以执行,部分浏览器为了兼容旧版本让function不管块级作用域,如果是只支持ES6的浏览器会访问不到demo()

if(true){
    // 块级作用域
}

switch(color){
    case "red":
        // 块级作用域
        break;
}

for(let i=0; i<10; i++){
    // 块级作用域
}
console.log(i); // Error

暂时性死区:在ES6中使用let、const声明的变量,在声明和初始化之前,变量都是不可以访问的(temporal dead zon)

var foo = 'foo';

if(true){
    console.log(foo);   // ERROR

    let foo = 'abc';
}

理论上块级作用于外存在foo对象,但是console.log(foo)报错
因为该块级作用域定义了let foo = 'abc',搜易console.log(foo)是要访问等于abcfoo,但是代码还没跑到foo = 'abc'这一行,所以foo不可访问(暂时性死区)

强烈建议不要使用var定义变量

letconst更适合开发中使用

  1. 优先推荐使用const,保证数据不会被随意修改,安全性
  2. 明确知道一个变量后续会需要被重新赋值,再改用let

模板字符串

const name = "x";
const age = 15;
const height = 190;
console.log("my name is " + name + ", age is " + age + ", height is " + height);

const message = `my name is ${name}, age is ${age}, height is ${height}`;
console.log(message);

const message1 = `my name is ${name}, age is ${age * 2}, height is ${height / 100.0}`;
console.log(message1);

标签模板字符串

  • 标签模板字符串调用函数时
    • 第一个参数时模板字符串中整个字符串,被切成多块
    • 第二个参数是模板字符串中,第一个${}
function foo(m, n){
    console.log(m, n);
}

// 正常调用
foo(10, 20);

// 标签模板字符串调用
foo``
foo`hello`

const name = "xyz";
const age = 19;
foo`hello${name}wo${age}rld`    // ['hello','wo','rld'] xyz

部分框架使用了这个技术,比如react的stypled-components的三方库

函数的默认参数

有默认值的形参最好放到最后(在C++中,有默认值的形参必须放在最后一个)

function ES5Foo(m, n){
    m = m || "aaa"; // 如果是undefined 就设置默认值
    n = n || "bbb"; // 如果传入是0,也会被赋值,是BUG
}

function foo(m = "aaa", n = "bbb"){
    console.log(m, n);
}

foo();
foo(1);
foo(1, 2);
var obj = {
    name : "xtz",
    age : 10,
    height : 180
};
var obj2 = {
    name : "qwer",
    height : 190
}
function printInfo({name, age} = {name:"", age:19}){
    console.log(name, age);
}
function printInfo1({name = "", age = 0} = {}){
    console.log(name, age);
}
printInfo();            // "" 10
printInfo(obj);         // xtz 10
printInfo(obj2);        // qwer undefined
printInfo1();            // "" 10
printInfo1(obj);         // xtz 10
printInfo1(obj2);        // qwer undefined
function baz(x, y, z){

}
function baz1(x, y, z = 1){

}
function baz2(x = 2, y = 3, z = 1){

}
console.log(baz.length);    // 3
console.log(baz1.length);   // 2
console.log(baz2.length);   // 0

存在默认值的参数,不计算如length中

函数的剩余参数(rest parameter)

如果函数的最后一个参数是...为前缀的,那么它会将剩余的参数放到该参数的中,并作为一个数组
剩余参数必须放在函数形参的最后

function foo(m, n, ...args){
    console.log(m, n);
    console.log(args);
    console.log(arguments);
}

foo(20, 30, 40, 50, 60);    
// 20 30
// [40, 50, 60]
// [20, 30, 40, 50, 60]
  • 剩余参数arguments的区别
    • 剩余参数只包含那些没有对应形参的实参arguments对象包含传给函数的所有实参
    • arguments对象不是一个真正的数组(不包含一些数组的操作),而剩余参数是一个真正的数组可以进行数组的所有操作
    • arguments对象是早期JS为了方便所有参数提供的一个数据解构,而剩余参数是为了替代arguments而设置的

箭头函数

箭头函数没有显式原型,所以不能作为构造函数,使用new来创建对象

var bar = () => {
    console.log(this);
    console.log(arguments);
}
const b = new bar();    // Error

箭头函数的thisarguments是查找使用父级作用域的

展开语法(spread syntax)

  • 可以在函数调用/数组构造时,将数组表达式或string在语法层面展开
  • 可以在构造字面量对象时,将对象表达式按k-v的方式展开
const names = ["x", "y", "z"];
const name = "123";
function foo(x, y, z){
    console.log(x, y, z);
}

foo(...names);  // x y z
foo(...name);   // 1 2 3

const newNames = [...names, ...name];   // x y z 1 2 3

const info = {
    name : "x",
    age : 20
};
const obj = {...info, address : "BJ", ...names};
console.log(obj);

...展开运算符实际上进行的是一个浅拷贝

数值表示

const num1 = 100;   // 十进制 100
const num2 = 0b100; // 二进制 4
const num3 = 0o100; // 八进制 64
const num4 = 0x100; // 十六进制 256

// 大数值
const num5 = 10_000_000_000_000_000;    // 使用_做分隔符,方便理解大数字的位数

Symbol基本使用

ES6中新增的基本数据类型,翻译为符号

  • 为什么需要Symbol

    • ES6之前对象的属性名都是字符串形式,很容易造成属性名的冲突
    • 存在一个对象,想往该对象中添加一个新的属性和值,但是不确定原本内部是否存在相同的属性名,很容易造成冲突,从而覆盖它内部的某个属性
    • 又或者前面提到的混入mixin,如果出现同名属性,必然有一个会被覆盖掉
  • 为了解决冲突问题,Symbol可以用来生成一个独一无二的值

    • Symbol函数可以传入一个描述
    • Symbol值是通过Symbol函数来生成的,生成后可以作为属性名
    • 对象的属性名可以是字符串,也可以是Symbol值
const s1 = Symbol()
const s2 = Symbol()

console.log(s1 === s2); // false

// ES10 之后,可以添加Symbol描述
const s3 = Symbol("aa");
console.log(s3.description);

const obj = {
    [s1] : "abv",
    [s2] : "qwer"
};

obj[s3] = "asd";
const s4 = Symbol();
Object.defineProperty(obj, s4, {
    // ...
});

console.log(obj[s1], obj[s2]);
// 不可通过 . 来获取属性

console.log(obj);   // 空

// 需要通过下面两个函数的方式来获取所有Symbol的key
console.log(Object.getOwnPropertyNames(obj));
console.log(Object.getOwnPropertySymbols(obj));

// 创建相同的Symbol
const s11 = Symbol.for("aa");
const s22 = Symbol.for("aa");
console.log(s11 === s22);
console.log(Symbol.keyFor("aa"));

不可通过 . 来获取Symbol,因为 . 是把key变成字符串再查找
obj.s1 变成 obj["s1"]

使用Symbol作为key的属性名,在遍历/Object.key等方法是遍历不到这些Symbol值的

Set与WeakMap(ES6)

Set新增数据结构,可以用来保存数据,类似数组,但是元素不能重复

const s = new Set();

// 添加
s.add(10);
s.add(10);
s.add(20);
s.add(30);

s.add({});  // 字面量A
s.add({});  // 字面量B A、B地址不同 并不是同一个东西

console.log(s);

// 去重
const arr = [10, 10, 20, 30, 40];
const arrSet = new Set(arr);
const newArr = [...arrSet];     // 支持展开运算符
console.log(newArr);    

// 常用方法 属性
console.log(arrSet.size);   // Set内元素个数
arrSet.delete(10);          // 删除Set内的元素
arrSet.has(10);             // 判断Set内是否存在某个元素
arrSet.clear();             // 清除Set内所有元素

// 遍历Set
arrSet.forEach(item => {
    console.log(item);
})

for(const item of arrSet){
    console.log(item);
}

WeakSet类似*Set*,也是内部元素不能重复的数据结构

  • 与Set的区别
    • WeakSet只能存放对象类型,不能存放基本数据类型
    • WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行运行,那么GC可以对该对象进行回收
const weakSet = new WeakSet();

// 不能存放基本数据类型
weakSet.add(10);        // TypeError

weakSet.add(value);     // 返回weakSet本身
weakSet.delete(value);  // 返回boolean类型
weakSet.has(value);     // 返回boolean类型

WeakSet不能遍历,因为WeakSet只是对对象的弱引用,如果遍历获取其中的元素,有可能造成对象的不正常销毁

WeakSet在开发中很少用到

WeakSet的应用

const personSet = new WeakSet();
class Person(){
    constructor() {
        personSet.add(this);
    }
    running() {
        if(!personSet.has(this)){
            throw new Error("不能通过非构造方法创建出来的对象调用running方法");
        }
        console.log("running ", this);
    }
}

const p = new Person();
p.running();
p.running.call({name, "why"});

使用WeakSet是因为当Person对象需要被销毁时,不会因为Set的强引用而不会被GC

Map与WeakMap(ES6)

Map新增数据结构,可以用来保存映射关系

对象映射关系只能用字符串Symbol作为属性名,如果使用对象,会自动将对象转换成字符串来作为key

// 对象字面量只能用字符串和Symbol作为key的测试用例
const obj1 = {};
const obj2 = {};
const info = {
    [obj1] : "a",
    [obj2] : "b";
};
console.log(info);      // { [Object object] : "b" }

const map = new Map();
map.set(obj1, "a");     // object作为key,插入到map中
map.set(obj2, "b");
console.log(map);

// 常用方法
const map2 = new Map([[obj1, "a"], [obj2, "b"]]);   // 用数组作为构造参数

map2.set("key", "value");
console.log(map2.get("key"));
console.log(map2.has("key"));
console.log(map2.delete("key"));
map2.clear();

// 遍历map
map2.forEach((value, key) => {
    console.log(value, " ", key);
});

for(const item of map2) {
    // 这里item是一个数组 index 0 是key index 1 是value
    console.log(item[0], item[1]);
}

// 直接对数组进行解构
for(const [key, value] of map2) {
    console.log(key, " ", value);
}

WeakMap也是键值对容器,与Map的区别

  1. WeakMap的key只能使用对象,不接受其他的类型作为key
  2. WeakMap的key对对象的引用是弱引用,如果没有其他对象引用该对象,那么可以GC回收对象
const obj1 = {};
const obj2 = {};

const weakMap = new WeakMap();
weakMap.set(1, "b");  // TypeError
weakMap.set(obj1, "a");

console.log(weakMap.get(obj1));
console.log(weakMap.has(obj1));
console.log(weakMap.delete(obj1));

不支持forEach,也不能用其他方式遍历

WeakMap的应用

vue3响应式原理

const obj1 = {
    name : "x",
    age : 10
};

const obj2 = {
    name : "z",
    height : 190,
    address : "BJ"
}

function obj1NameFn1(){
    console.log("Obj1 Name Fn1 被执行");
}
function obj1NameFn2(){
    console.log("Obj1 Name Fn2 被执行");
}
function obj2HeightFn1(){
    console.log("Obj2 height Fn1 被执行");
}
function obj2HeightFn2(){
    console.log("Obj2 height Fn2 被执行");
}

const weakMap = new WeakMap();
const obj1Map = new Map();
const obj2Map = new Map();

obj1Map.set("name", [obj1NameFn1, obj1NameFn2]);
weakMap.set(obj1, obj1Map);

obj2Map.set("height", [obj2HeightFn1, obj2HeightFn2]);
weakMap.set(obj2, obj2Map);

// Proxy / Object.defineProperty 监听obj1或obj2的属性发生改变
obj1.name = "qwer";

// 监听到数据变化后
const targetMap = weakMap.get(obj1);
const fns = targetMap.get("name");
fns.forEach(item => item());

使用WeakMap是因为当obj对象需要被销毁时,不会因为Map的强引用而不会被GC

Array Includes(ES7)

  • includesindexOf的区别:对NaN的判断
const names = ['a', 'b', 'c', NaN];
if(names.indexOf('a') !== -1){
    // 就版本判断是否包含指定值
}

// Array.includes(searchElement : string, formIndex ?: number): boolean
if(names.includes('a')){
    console.log("包含 a")
}
console.log(names.includes('a', 0));    // true
console.log(names.includes('a', 1));    // false
console.log(names.includes('a', 2));    // false

console.log(names.includes(NaN));       // true
console.log(names.indexOf(NaN));        // -1

fromIndex参数表示从第几个开始查找
indexOf无法找到NaN

指数运算符(ES7)

const result = Math.pow(3, 3);
const result2 = 3 ** 3;
console.log(result, result2);

Object Values(ES8)

之前通过Object.keys获取一个对象所有的key,ES8提供Object.values来获取所有的value值

const obj = {
    name : "x",
    age : 10
}
console.log(Object.keys(obj));                  // ['name', 'age']
console.log(Object.values(obj));                // ['x', 10]

// 用的少
console.log(Object.values(["q", "w", "r"]));    // ['q', 'w', 'r']
console.log(Object.values("qwer"));             // ['q', 'w', 'e', 'r']

Obejct Entries(ES8)

通过Object.entries可以获取一个数组,数组中会存放可枚举属性的键值对数组

const obj = {
    name : "x",
    age : 10
}

console.log(Object.entries(obj));   // [ [ 'name', 'x' ], [ 'age', 10 ] ]

console.log(Object.entries(["a", "b", "c"]));   // [ [ '0', 'a' ], [ '1', 'b' ], [ '2', 'c' ] ]
console.log(Object.entries("qwer"));    // [ [ '0', 'q' ], [ '1', 'w' ], [ '2', 'e' ], [ '3', 'r' ] ]

本质就是一个二维数组,第一个维度是item,第二个维度是键值对

String Padding(ES8)

字符串填充

某些字符串我们需要对其进行前后的填充,去实现某种格式化的效果,ES8中增加了padStartpadEnd方法,分别是队字符串和首位进行填充的

const message = "Hello World";
// string.padStart(maxLength : number, fillString ?: string) : string
const newMessage1 = message.padStart(15);
console.log(newMessage1);           //     Hello World
const newMessage2 = message.padStart(15, "*");
console.log(newMessage2);           // ****Hello World
const newMessage3 = message.padStart(15, "*").padEnd(20, "-");
console.log(newMessage3);           // ****Hello World-----

案例

// 身份证号、银行卡号只显示最后四位
const cardNumber = "4532132468321312135433";
const lastFourCardNumber = cardNumber.slice(-4);
const finalCard = lastFourCardNumber.padStart(cardNumber.length, "*");
console.log(finaleCard);    // ******************5433

Trailing-Commas(ES8)

支持函数调用额函数声明中参数列表最后多个逗号

为了有些人习惯、有些语言而添加

function foo(m, n,){    // ES8之前报错

}
foo(1, 2,);

Object Descriptors(ES8)

Object.getOwnPropertyDescriptors获取对象的所有属性描述符

前面有写

Flat FlatMap(ES10)

flat()方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回

// Array.flat(depth? : 1) : (number | number[]) []
const nums = [1, 2, [3, [4, 5], 6], 7, 8, [9, 10]];
const newNums = nums.flat();    // 默认做一次降维
console.log(newNums);

const newNums2 = nums.flat(2);  
console.log(newNums2);

flatMap()方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组

  1. flatMap()是先进行map操作,在做flat操作
  2. flatMap()中的flat深度为1
// Array.flatMap(callback : (this : undefined, value : number | number[] | number[][], index : number, array : (number | number[] | number[][])[]) => any, thisArg ?: undefined) : any[]

const nums = [1, 2, 3];
const newNum = nums.flatMap(item => {
    return item * 2;
});
console.log(newNum);    // [2, 4, 6]

const messages = ["Hello World", "Hello Java", "Hello Cpp"];
const words = messages.flatMap(item => {
    return item.split(" ");
});
console.log(wrods); // [ "Hello", "World","Hello","Java","Hello","Cpp"]
  1. 这里的messages先通过map()转换为[["Hello", "World"],["Hello", "Java"],["Hello", "Cpp"]]
  2. 再通过flat()将二维数组转换成一维数组

Object fromEntries(ES10)

通过entries创建对象

const obj = {
    name : "x",
    age : 10
};
const entries = Object.entries(obj);
console.log(entries);

const newObj = {};
for(const entry of entries){
    newObj[entry[0]] = entry[1];
}
console.log(newObj);

const newObj2 = Object.fromEntries(entries);
console.log(newObj2);

const queryString = "name=w&age=10&height=190";
const queryParams = new URLSearchParams(queryString);
const paramObj = Object.fromEntries(queryParams);
console.log(paramObj);  // {"name": "w","age": "10","height": "190"}

trimStart trimEnd(ES10)

去除前、后的空白字符

const message = "    hello wworld    ";
console.log(message.trim());        // 
console.log(message.trimStart());   // 
console.log(message.trimEnd());     // 

BigInt(ES11)

早期JS不能正确的表示过大的数字,大于Number.MAX_SAFE_INTEGER的数值的表示可能是不正确的

console.log(Number.MAX_SAFE_INTEGER);   // 9007199254740991

ES11引入BigInt来表示大数字,就是在数字后加上n

const bigint = 90071992547409910n;
console.log(bigint);
bigint + 10;            // TypeError: Cannot mix BigInt and other type
bigint + BigInt(10);    // 90071992547409920n
const num = Number(bigint); // 不一定正确 不安全

Nullish Coalescing Operator(ES11)

空值合并操作、空值合并运算

// 空值合并运算 ??

let foo;
let foo1 = 0;
let bar1 = foo1 || "default value";
let bar = foo ?? "default value";

console.log(bar);   // default value
console.log(bar1);  // default value

针对bar1的值,我们目标肯定是0,但是由于||运算符将0、false和空字符串认为是false,所以这里使用??是最好的选择

Optional Chaining(ES11)

可选链

可选链是ES11新增的一个特性,主要作用是在代码中进行nullundefined判断时更加清晰和简介

const info = {
    name : "x",
    friend : {
        name : "a",
        friend : {
            name : "q"
        }
    }
};

console.log(info.friend.friend.name);

// 为了代码健壮性 需要判断info中是否存在friend,info.friend中是否存在friend
if(info && info.friend && info.friend.friend){
    console.log(info.friend.friend.name);
}

// 可选链的使用
console.log(info?.friend?.friend?.name);

注意代码的健壮性

Global This(ES11)

获取全局对象的GlobalThis

// 获取某个环境下的全局对象

// 浏览器下正确,但是node环境下错误
console.log(window);

// node下
console.log(global);

let myGlobalObj = undefined;
if(window !== undefined){
    myGlobalObj = window;
}
else{
    myGlobalObj = global;
}

// ES11后
console.log(globalThis);    // 浏览器中等于window,node中等于global

不用写复杂的if...else去给myGlobalObj赋值,可以直接用globalThis替代

其他ES11

  1. Dynamic Import:动态导入
  2. Promise.allSettled
  3. import meta
  4. ...

FinalizationRegistry、WeakRef(ES12)

  • FinalizationRegistry对象可以让你在对象被垃圾回收时请求一个回调
    • FinalizationRegistry提供:当一个在注册表中注册的对象被回收时,请求在某个时间点上调用一个清理回调(清理回调有时被称为finalizer)
    • 可以通过调用register方法,注册任何你想要清理回调的对象,传入该对象和所含的值
// FinalizationRegistry
const finalRegistry = new FinalizationRegistry((value) => {
    console.log("注册对象被销毁", value);
});

let obj = { name: "w" };
let info = {};
let obj2 = new WeakRef(obj);        // 弱引用
finalRegistry.register(obj, "obj_1");
finalRegistry.register(info, "info_1");

console.log(obj2);                  // WeakRef对象
console.log(obj2.deref());          // 获得弱引用对象
obj = null;

这个用浏览器测试比较方便,而且JS垃圾回收不是实时的可能得等一会
最后应该是输出obj_1和info_1

WeakRef就是弱引用,let obj3 = obj;是强引用,即使obj = null内存也不会被GC,因为obj3还在指向内存块,let obj2 = new WeakRef(obj);是弱引用,obj = null后内存块就会被GC

WeakRef.prototype.deref()函数,如果原指向对象没有被销毁,则返回原指向对象;如果原指向对象被销毁,则返回undefined(搭配前面的可选链进行操作即可)

logical assignment operators(ES12)

逻辑赋值运算

// 1. ||= 逻辑或赋值运算
let message = undefined;
message = message || "default value";
message ||= "default value";

// 2. &&= 逻辑与赋值运算
let obj = {
    name : "x",
    foo : function() {
        console.log("run");
        return {};
    }
};
obj = obj && obj.foo(); // 判断obj是否存在,存在就覆盖obj的值为obj.foo()
obj &&= obj.foo();

// 3. ??= 逻辑空赋值运算
let msg = "";
msg ??= "default vlaue";
msg = msg ?? "default value";

理解为 x = x + 1 等价于 x += 1即可

Proxy-Reflect

监听操作

监听对象的属性被赋值或获取的操作,去根据这个值进行一些其他的操作(数据驱动框架中常用)

const obj = {
    name: "x",
    age: 10,
    height: 190
};
Object.keys(obj).forEach(key => {
    let value = obj[key];
    Object.defineProperty(obj, key, {
        get: function() {
            console.log(`${key} get`);
            return value;
        },
        set: function(newValue) {
            value = newValue;
            console.log(`${key} set`);
        }
    });
});
console.log(obj.name);
obj.age = 120;
console.log(obj.age);

可以使用属性描述符来监听属性的赋值和获取操作

  • 使用Object.defineProperty的缺点
    • Object.defineProperty的初衷不是监听一个对象属性
    • 对对象更丰富的操作,比如新增属性、删除属性的监听是做不到

使用Proxy可以监听到对对象的13种操作

Proxy基本属性

ES6新增Proxy类,如果我们希望监听一个对象的相关操作,我们需要先创建一个Proxy对象,之后该对象的所有操作,都通过Proxy对象来完成,代理对象可以监听我们想要对原对象进行哪些操作

  1. 需要new Proxy()对象,并且传入待处理对象和捕获器对象,可以称之为handlerlet p = new Proxy(target, handler)
  2. 捕获器提供13种操作,对对应操作重写方法即可自定义监听
  3. 之后对对象的所有操作都改为操作Proxy对象,因为我们需要在handler进行监听
序号 捕获器对象 作用 对应操作
1 getPrototypeOf(target) 当读取被代理对象target的原型prototype时会触发该操作 Object.getPrototypeOf()方法的捕捉器
2 setPrototypeOf(target, prototype) 给target设置prototype时触发 Object.setPrototypeOf()方法的捕捉器
3 isExtensible(target) 判断target是否可扩展时触发 Object.isExtensible()方法的捕捉器
4 preventExtensions(target) 设置target不可扩展时触发 Object.preventExtensions()方法的捕捉器
5 getOwnPropertyDescriptor(target, prop) 获取target[prop]的属性描述时触发 Object.getOwnPropertyDescriptor()方法的捕捉器
6 defineProperty(target, property, descriptor) 定义target的某个属性prop的属性描述descriptor时触发 Object.defineProperty()方法的捕捉器
7 has(target, prop) 当判断target是否拥有属性prop时,触发 in操作符的捕捉器
8 get(target, property, receiver) 读取target的属性property时触发 属性获取操作的捕捉器
9 set(target, property, value, receiver) 设置target的属性property为值value时触发 属性设置操作的捕捉器
10 deleteProperty(target, property) 删除target的属性property时触发 delete操作符的捕捉器
11 ownKeys(target) 获取targeet的所有属性key s时触发 Object.ownKeys()方法的捕捉器
12 apply(target, thisArg, argumentsList) 当目标target为函数,且被调用时触发 函数调用操作的捕捉器
13 construct(target, argumentsList, newTarget) 给target为构造函数的代理对象构造实例时触发 new操作符的捕捉器

get中的receiver为Proxy或继承Proxy的对象
set中的receiver是最初被调用的对象(通常是Proxy本身)

const obj = {
    name : 'x',
    age : 10
};
let objProxy = new Proxy(obj, {
    get : function(target, key, receiver){
        // target 是 obj,key 是 取值的键名, receiver 是 代理对象 objProxy
        console.log(`${key} get`);
        return target[key];
    },
    set : function(target, key, newValue, receiver){
        console.log(`${key} set ${newValue}`);
        target[key] = newValue;
    },
    has : function(target, key) {
        console.log(`${key} in Obj?`);
        return key in target;
    },
    deleteProperty : function(target, key){
        console.log(`delete ${key} in target`);
        delete target[key];
    }
});

objProxy.name = "y";
objProxy.age = 15;

console.log(objProxy.name);
console.log(objProxy.age);
console.log("name" in objProxy);

delete objProxy.age;

function foo(){

}

// foo()                // 直接调用
// foo.apply({}, [])    // 通过apply调用
// new foo();           // new出来调用

const fooProxy = new Proxy(foo, {
    apply : function(target, thisArg, argumentsList) {
        // target就是函数对象 thisArg是调用函数的对象
        console.log(target, thisArg, argumentsList);
        target.apply(thisArg, argumentsList);
    },
    construct : function(target, argList, newTarget){
        // 通过new调用的监听
        console.log(target, " new function");
        return new target(...argList);
    }
});

fooProxy.apply({}, ["qw", "er"]);   // [Function: foo] {} [ 'qw', 'er' ]
new fooProxy();

Reflect的作用

Reflect是ES6新增的API,他是一个对象(不用new,直接使用),字面意思是反射

Reflect提供很多操作JS对象的方法,有点像Object中操作对象的方法

Reflect.getPrototypeOf(target)类似Object.getPrototypeOf()
Reflect.defineProperty(target, propertyKey, attributes类似Object.defineProperty()

  • 为什么新增Reflect
    • 早期ECMA规范没有考虑到对对象本身的操作设计会更加规范,所以将这些API放到Object上
    • Object作为一个构造函数,这些操作实际上放在Object中并不合适
    • 另外还包含一些类似in、delete操作符,让JS看起来很奇怪
    • 所以新增Reflect,将这些操作放到Reflect

一句话概括就是Object功能太多,新增Reflect分担对象操作功能

比较Reflect和Object——MDN文档

序号 常见方法 对应功能
1 Reflect.getPrototypeOf(target) 类似Object.getPrototype()
2 Reflect.serPrototypeOf(target, prototype) 设置对象原型的函数,返回boolean表示是否更新成功
3 Reflect.isExtensible(target) 类似Object.isExtensible()
4 Reflect.preventExtensions(target) 类似Object.preventExtensions(),返回boolean
5 Reflect.getOwnPropertyDescriptor(target, propertyKey) 类似Object.getOwnPropertyDescriptor(),如果对象存在,返回对应属性描述符,不存在返回undefined
6 Reflect.definePeoperty(target, propertyKey, attributes) 类似Object.definePeoperty(),如果设置成功返回true
7 Reflect.ownKeys(target) 类似Object.ownKeys(),不受enumerable影响
8 Reflect.has(target, propertyKey) 判断一个对象是否存在某个属性,和in运算符的功能完全相同
9 Reflect.get(target, propertyKey[, receiver]) 获取对象上某个属性的值,类似target[name]
10 Reflect.set(target, propertyKey, value[, receiver]) 将值分配给属性的函数,返回一个Boolean表示是否设置成功
11 Reflect.deleteProperty(target, propertyKey) 作为函数的delete操作符,相当于执行delete target[name]
12 Reflect.apply(target, thisArgument, argumentsList) 对一个函数进行调用操作,同时可以传入一个数组作为调用参数,和Function.prototype.apply()功能类似
13 Reflect.construct(target,argumentsList[, newTarget]) 对构造函数进行new操作,相当于执行new target(...args)
const obj = {
    name: 'x',
    age: 10
};
let objProxy = new Proxy(obj, {
    get: function(target, key, receiver) {
        // target 是 obj,key 是 取值的键名, receiver 是 代理对象 objProxy
        console.log(`${key} get`);
        return Reflect.get(target, key, receiver);
    },
    set: function(target, key, newValue, receiver) {
        console.log(`${key} set ${newValue}`);
        Reflect.set(target, key, newValue, receiver);
    },
    has: function(target, key) {
        console.log(`${key} in Obj?`);
        return Reflect.has(target, key);
    },
    deleteProperty: function(target, key) {
        console.log(`delete ${key} in target`);
        Reflect.deleteProperty(target, key);
    }
});
objProxy.name = "y";
objProxy.age = 15;

console.log(objProxy.name);
console.log(objProxy.age);
console.log("name" in objProxy);

delete objProxy.age;

不直接对目标对象进行操作,而是通过Reflect对象对目标对象进行操作
相对于对对象的直接操作,使用Reflect的好处就是Reflect绝大多数函数都会返回Boolean提醒是否操作成功
tip: Object.freeze(target)冻结对象后不可设置值,这时Reflect的功能就体现出来了

Receiver的作用

const obj = {
    _name : "x",
    get name(){
        return this._name;
    },
    set name(value){
        this._name = value;
    }
}

const objProxy = new Proxy(obj, {
    get : function(target, key) {
        console.log("get");
        return Reflect.get(target, key);
    },
    set : function(target, key, value){
        console.log("set");
        Reflect.set(target, key, value);
    }
});

objProxy.name = "y";
console.log(objProxy.name);

运行结果:一次set、一次get,this._name = value没有走代理而是直接设置了obj
如果希望对obj的所有操作都经过objProxy,这个结果就是错误的

receiver就是创建出来的代理对象

receiver === objProxy

const obj = {
    _name : "x",
    get name(){
        return this._name;
    },
    set name(value){
        this._name = value;
    }
}

const objProxy = new Proxy(obj, {
    get : function(target, key, receiver) {
        console.log("get");
        return Reflect.get(target, key, receiver);
    },
    set : function(target, key, value, receiver){
        console.log("set");
        Reflect.set(target, key, value, receiver);
    }
});

objProxy.name = "y";
console.log(objProxy.name);

Reflect.get(target, key, receiver)会让this._name里的this变成receiver对象,而不再是obj对象

Promise(ES6)

// 使用setTimeout模拟网络请求
function requestData(url, succeessCallback, errorCallback){
    setTimeout(() => {
        // 获得请求结果
        if(url === "qwer"){
            // 请求成功
            let name = "result";
            succeessCallback(name);
        }else{
            // 请求失败
            let error = "404";
            errorCallback(error);
        }
    }, 3000);
}
requestData("qwer", (success) => {
    console.log(success);
}, (error) => {
    console.log(error);
});

如果是自己封装的requestData必须提前设计好回调函数,并且使用好
如果是别人封装的requestData必须查看源码才知道如何使用回调函数

更好的方案Promise承诺(规范好了所有代码的编写逻辑)

  • 什么是Promise
    • Promise一个类,当我们需要给调用者一个承诺:待会会给你回调数据时,可以创建一个Promise的对象
    • 在通过new创建Promise对象时,我们需要传入一个回调函数,称之为executor
    • 这个回调函数会被立即执行,并且给传入另外两个回调函数resolve,reject
    • 成功时 调用resolve回调函数时,会执行Promise对象的then方法传入的回调函数
    • 失败时 调用reject回调函数时,会执行Promise对象的catch方法传入的回调函数
function requestData(url) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            // 获得请求结果
            if (url === "qwer") {
                // 请求成功
                let name = "result";
                resolve(name);
            } else {
                // 请求失败
                let error = "404";
                reject(error);
            }
        }, 1000);
    });
}
let promise = requestData("qwer");
promise.then((res) => {
    console.log(`request success ${res}`);
}).catch((err) => {
    console.log(`request failed ${err}`);
});

// 等价操作
// promise.then((res) => {
//     console.log(`request success ${res}`);
// }, (err) => {
//     console.log(`request failed ${err}`);
// });

因为Promise构造时的传入的回调函数会立即执行,所以异步逻辑直接写入到回调函数中即可
Promise.then支持传入两个回调函数,第一个表示resolve,第二个表示reject,可以省略编写catch

综上可以发现,Promise其实就是JS官方提供的一套回调函数的标准,看到Promise就知道函数应该使用

  • 根据Promise使用的过程,可以分为三个状态
    1. Pedning(待定):初始状态,既没有reslove也没有reject
    2. 已敲定(fulfilled):意味着操作完成,就是执行了reslove()
    3. 已拒绝(rejected):意味着操作失败,就是执行了reject()
new Promise((resolve, reject) => {
    console.log("----");
    resolve("1111");
    console.log("++++");
    reject("2222");
}).then((res) => {
    console.log(res);
}, (err) => {
    console.log(err);
});

强烈建议运行代码查看结果
可以发现 先执行了exector部分的代码,再执行了resolve()代码,又因为整个Promise的状态已经被确定为fulfiled,所以不会执行reject()的方法

  • resolve()函数的参数
    1. 普通的值和对象(就当普通的函数传递一样使用,会从Pedning转到fulfilled状态)
    2. 传入一个Promise对象(当前Promise的状态会由传入的Promise的状态来决定,看下面代码 例子1)
    3. 传入一个对象,并且对象有then方法,那么会执行该then方法,并且由then方法决定后续状态(下面代码 例子2)
// 例子 1
const promise1 = new Promise((resolve, reject) => {});
new Promise((reslove, reject) => {
    reslove(promise1);
}).then((res) => {
    console.log("reslove");
    console.log(res);
}, (err) => {
    console.log("reject");
    console.log(err);
})

// 例子 2
new Promise((resolve, reject) => {
    const obj = {
        then : function(resolve, reject){
            reject("123");
        }
    }
    resolve(obj);
}).then((res) => {
    console.log("resolve", res);
}, (err) => {
    console.log("reject", err);
})

强烈建议运行代码查看结果
对于例子1,因为promise1对象处于Pedning状态,所以影响了新newPromise不执行reslove
对于例子2,因为executor部分的obj存在then方法,而then方法执行了reject(),从而导致newPromise变成了rejected状态,而不会执行后续的resolve()方法

Promise的状态一旦决定,就无法更改


  1. 同一个Promise可以多次调用then方法,而所有绑定的then方法在执行resolve时都会被执行
const promise = new Promise((resolve, reject) => {
    resolve("--");
});
promise.then(res => {
    console.log("1");
});
promise.then(res => {
    console.log("2");
});
promise.then(res => {
    console.log("3");
});
  1. then传入的回调函数方法是可以有返回值的
    1. 如果返回值是普通值(基本数据类型或字面量对象),那么会将返回值作为一个新的Promiseresolve的形参
      • 从下面的代码 样例 1 可见,后续的then都是调用因返回普通值而新建的Promise对象的then
    2. 如果返回的是Promise对象,可参见前面resolve()函数的参数这一栏的情况

无论如何,then传入的回调函数的返回值都会出发new Promise,并把返回值传入到新的Promiseresolve的形参中

const test = Promise.resolve({name:"x"});
// 等价于
// const promise = new Promise((resolve, reject) => {
//     resolve({name : "x"});
// })

// 样例 1
// 返回值为普通值
const promise = new Promise((resolve, reject) => {
    resolve("promise 1 ");
});
promise.then((res) => {
    console.log(res);
    return "aaa";
}).then(res => {
    console.log("new promise 2 : ", res);
    return "bbb";
}).then(res => {
    console.log("new promise 3 : ", res);
});

// 样例2
// 返回值为Promise
const promise = new Promise((resolve, reject) => {
    resolve("promise 1 ");
});
promise.then((res) => {
    console.log(res);
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("return Promise");
        }, 1000)
    });
}).then(res => {
    console.log(res);
});

  • finally方法:无论Promise处于fulfilled还是rejected状态,最终都会执行的代码
const promise = new Promise((resolve, reject) => {
    reject("err");
});
promise.then(res => {
    console.log(res);
}, err => {
    console.log(err);
}).finally(() => {
    console.log("finally");
})

Promise.all等待所有Promise对象都进入fulfilled状态时触发,返回值是一个数组,数组的item就是resolve函数的参数

如果Promise数组中存在某个Promise触发了reject,则会导致Promise.all立即停止监听,并返回该reject的参数

// 全部都执行 resolve
const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    }, 1000);
});
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(2);
    }, 2000);
});
const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(3);
    }, 3000);
});

Promise.all([p1, p2, p3]).then(res => {
    console.log(res);
}, (err) => {
    console.log(err);
});

// 某个Promise触发了reject
const p4 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    }, 1000);
});
const p5 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(2);
    }, 2000);
});
const p6 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(3);
    }, 3000);
});

Promise.all([p4, p5, p6]).then(res => {
    console.log(res);
}, (err) => {
    console.log(err);
});

Promise.allSettled等待所有的Promise都有结果的时候才会触发

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    }, 1000);
});
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(2);
    }, 2000);
});
const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(3);
    }, 3000);
});

Promise.allSettled([p1, p2, p3]).then(res => {
    console.log("resolve ", res);
}, (err) => {
    console.log("reject", err);
});
// 输出
// resolve  [
//   { status: 'fulfilled', value: 1 },
//   { status: 'rejected', reason: 2 },
//   { status: 'fulfilled', value: 3 }
// ]

Promise.race只要有一个Promise变成fulfilled或者rejected状态,那么就结束
如果Promise中有一个触发了reject,就会直接拿到reject的结果并结束

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    }, 1000);
});
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(2);
    }, 2000);
});
const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(3);
    }, 3000);
});
Promise.race([p1, p2, p3]).then(res => {
    console.log("resolve ", res);
}, (err) => {
    console.log("reject", err);
});
// 输出
// resolve  1

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    }, 1000);
});
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(2);
    }, 500);
});
const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(3);
    }, 3000);
});
Promise.race([p1, p2, p3]).then(res => {
    console.log("resolve ", res);
}, (err) => {
    console.log("reject", err);
});
// 输出
// reject 2

ES12的方法 Promise.any会等到一个fulfilled状态,才会决定新的Promise的状态,如果所有的Promise都是reject的,那么也会等到所有的Promise都变成rejected状态

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(1);
    }, 1000);
});
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(2);
    }, 500);
});
const p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(3);
    }, 3000);
});
Promise.any([p1, p2, p3]).then(res => {
    console.log("resolve ", res);
}, (err) => {
    console.log("reject", err);
});
// 输出
// resolve  1

迭代器生成器 (iterator - generator)

迭代器

生成器可以处理异步代码

迭代器可以使用户在容器对象上遍历对象,使用该接口无需关心对象的内部实现细节

JavaScript中,迭代器也是一个具体对象,这个对象需要符合迭代器协议

迭代器协议定义了产生一系列值的标准方式,在js中就是实现特定的next方法

  • next方法有一些要求
    • 无参或一个参数的函数,返回一个应当拥有以下两个属性的对象
    • done(boolean)
      • 如果迭代器可以产生序列中的下一个值,则为false
      • 如果容器已经被迭代完毕,则为true
    • value:该值可选,如果done = true,则value作为迭代结束后默认返回值

当所有元素都访问完了,最后再访问的时候done = true,其他时候done = false

// 迭代器对象基本形状
// const iterator = {
//     next : function(){
//         return {
//             done : true,
//             value : 123
//         }
//     }
// };

const names = ["q", "w", "e", "r"];
let index = 0;
const namesIterator = {
    next : function() {
        if(index < names.length){
            return {
                done : false,
                value : names[index++]
            }
        }
        else{
            return {
                done : true,
                value : undefined
            }
        }
    }
}
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());

// 输出
// { done: false, value: 'q' }
// { done: false, value: 'w' }
// { done: false, value: 'e' }
// { done: false, value: 'r' }
// { done: true, value: undefined }
// { done: true, value: undefined }

function createArrayIterator(arr){
    let index = 0;
    return {
        next : function(){
            if(index < arr.length){
                return { done : false, value : arr[index++]};
            }
            else{
                return { done : true, value : undefined};
            }
        }
    }
}

可迭代对象

Symbol.iterator

const iteratorObj = {
    names: ["q", "w", "e"],
    [Symbol.iterator]: function() {
        let index = 0;
        return {
            next: () => {   // 匿名函数
                if (index < this.names.length) {
                    return { done: false, value: this.names[index++] };
                } else {
                    return { done: true, value: undefined };
                }
            }
        }
    }
};

const iterator = iteratorObj[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
const iterator1 = iteratorObj[Symbol.iterator]();
console.log(iterator1.next());
console.log(iterator1.next());
console.log(iterator1.next());
console.log(iterator1.next());
const iterator2 = iteratorObj[Symbol.iterator]();
console.log(iterator2.next());
console.log(iterator2.next());

for (let item of iteratorObj) {
    console.log(item);
}
// 错误例子 
const errorObj = {
    names: ["q", "w", "e"],
    [Symbol.iterator]: function() {
        let index = 0;
        return {
            next: function() {   // 匿名函数
                if (index < this.names.length) {
                    return { done: false, value: this.names[index++] };
                } else {
                    return { done: true, value: undefined };
                }
            }
        }
    }
};

for (let item of errorObj) {
    console.log(item);
}
  1. 注意[Symbol.iterator]中的next使用的是箭头函数,如果next绑定的是function,那么this指向的就是[Symbol.iterator]返回的对象,而不是iteratorObj对象,而只有iteratorObj对象才有names属性
  2. 当使用箭头函数时,不绑定this,而是使用上层作用域作为this,而上层作用域就是iteratorObj
  3. 在错误示例中,this指向的是{next: function() {if (index < this.names.length) {return { done: false, value: this.names[index++] };} else {return { done: true, value: undefined };}}对象
  4. for...of...通过迭代器判断返回值的done是否为true来决定是否停止遍历

原生迭代器对象

平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象:String、Array、Map、Set、arguments对象、NodeList集合

const names = [1, 2, 3, 4, 5];
console.log(names[Symbol.iterator]);
console.log(names[Symbol.iterator]().next());

const set = new Set();
set.add(1);
set.add(2);
set.add(3);
console.log(set[Symbol.iterator]);
console.log(set[Symbol.iterator]().next());

可迭代对象的应用

  1. Javascript语法中:for...of...、展开语法(Spread syntax)、yield*、解构赋值(Destructuring assignment)
  2. 创建按一些对象时:new Map([Iterable])new WeakMap([iterable])new Set([iterable])new WeakSet([iterable])
  3. 一些方法调用:Promise.all(iterable)Promise.race(iterable)Array.from(ietrable)
const iteratorObj = {
    names: ["q", "w", "e"],
    [Symbol.iterator]: function() {
        let index = 0;
        return {
            next: () => {   // 匿名函数
                if (index < this.names.length) {
                    return { done: false, value: this.names[index++] };
                } else {
                    return { done: true, value: undefined };
                }
            }
        }
    }
};

// 展开语法
const indexs = [1, 2, 3, 4, 5];
const newIndexs = [...indexs, ...iteratorObj];
console.log(newIndexs);                 // [1, 2, 3, 4, 5, 'q', 'w', 'e']

// 结构语法
const [index1, index2, index3] = iteratorObj;
console.log(index1, index2, index3);    // ['q', 'w', 'e']

// 创建一些对象
const set = new Set(iteratorObj);       // 可以通过可迭代对象创建Set
console.log(set);                       // Set(3) { 'q', 'w', 'e' }

const array = Array.from(iteratorObj);  
console.log(array);                     // [ 'q', 'w', 'e' ]

自定义类的可迭代

  • 教室案例
    • 教室的名称、位置、学生
    • 可以进入新学生
    • 可迭代对象
class ClassRoom {
    constructor(address, name, students) {
        this.address = address;
        this.name = name;
        this.students = students;
    }

    entry(newStudent) {
        this.students.push(newStudent);
    }

    [Symbol.iterator]() {
        let index = 0;
        return {
            next: () => {
                if (index < this.students.length) {
                    return { done: false, value: this.students[index++] };
                } else {
                    return { done: true, value: undefined };
                }
            },
            // 监听迭代器终止
            return : () => {    
                console.log("迭代器终止");
                return { done : true, value : undefined };
            }
        }
    }
};

const c1 = new ClassRoom("", "", [1, 2, 3]);
for (let s of c1) {
    console.log(s);
}

for (let s of c1) {
    if (s == 2) {
        break;
    }
    console.log(s);
}

可以通过添加return属性监听迭代器的迭代终止,注意返回值

生成器

比较特殊的迭代器

ES6中新增的一种函数控制、使用的方方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行

return 虽然可以暂停函数执行,但后续代码无法继续执行

生成器对象是由生成器函数产生的

  • 生成器函数也是一个函数,但是和普通函数有一些区别
    • 生成器函数需要在function的后面加一个符号:*
    • 生成器函数可以通过yield关键字来控制函数的执行流程
    • 生成器函数的返回值是一个Generator(生成器)
    • 生成器事实上是一种特殊的迭代器
function* foo() {
    console.log("start");

    const v1 = 100;
    console.log(v1);
    const n1 = yield v1;

    const v2 = 200;
    console.log(v2, n1);
    const n2 = yield v2;

    const v3 = 300;
    console.log(v3, n2);
    const n3 = yield v3;

    console.log("end", n3);
    return "123";
}

foo(); // 直接执行foo,不会执行任何代码
const generator = foo();
console.log("---------");

// 开始执行第一段代码 看下图
console.log("---------", generator.next(666));
// 开始执行第二段代码 
console.log("---------", generator.next(777));
// 开始执行第三段代码 
console.log("---------", generator.next(888));
// 开始执行第四段代码 
console.log("---------", generator.next(999));

// 运行结果
// ---------
// start
// 100
// --------- { value: 100, done: false }
// 200 777
// --------- { value: 200, done: false }
// 300 888
// --------- { value: 300, done: false }
// end 999
// --------- { value: '123', done: true }

生成器函数执行

生成器函数以yiled为分界线,分段执行
通过上述代码的next的返回值可见,返回值的结构与迭代器next结构相同,可见生成器就是特殊的迭代器
当生成器函数遇到yield的时候停止执行,done的值为false;当生成器函数遇到return的时候,done的值就变成true
yield v1;表示返回生成器的值为v1的值
const n1 = yield v1;表示用n1接受next传入参数的值


// return

const generator = foo();
console.log(generator.next(10));
console.log(generator.return(20));
console.log(generator.next(30));
console.log(generator.next(40));

使用return(20)相当于在对应给yield前面加上了return 20,不仅当前yield部分不执行,后续所有部分都不会执行


// throw
function* foo() {
    console.log("start");
    const v1 = 100;
    try {
        yield v1;
    } catch (err) {
        console.log("err", err);
    }

    const v2 = 200;
    console.log("第二部分");
    yield v2;

    console.log("end");
}

const generator = foo();
console.log(generator.next(10));
console.log(generator.throw(20));
console.log(generator.next(30));

throw()抛出异常

代码运行可知,如果有try...catch捕获到了异常,代码仍然会继续执行


生成器替代迭代器

function* createArrayIterator(arr) {
    for (const item of arr) {
        yield item;
    }
}

function* createArrayIterator2(arr) {
    yield* arr;
}

const names = [1, 2, 3, 4];
const namesIterator = createArrayIterator(names);
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());

const namesIterator1 = createArrayIterator2(names);
console.log(namesIterator1.next());
console.log(namesIterator1.next());
console.log(namesIterator1.next());
console.log(namesIterator1.next());
console.log(namesIterator1.next());

createArrayIterator2就是createArrayIterator的简易写法,yield*后面必须跟上一个可迭代对象

class ClassRoom {
    constructor(address, name, students) {
        this.address = address;
        this.name = name;
        this.students = students;
    }

    entry(newStudent) {
        this.students.push(newStudent);
    }

    [Symbol.iterator] = function*() {
        yield* this.students;
    }
};
const c1 = new ClassRoom("", "", [1, 2, 3]);
for (let s of c1) {
    console.log(s);
}

通过生成器,简化迭代器的写法

异步函数的处理方法(前面Promise)

  • 需求:三次请求,请求就是返回原字符串,然后分别往初始字符串后添加"aa", "bb"

模拟:通过 用户ID->用户信息->部门信息->其他信息,这种链式请求

function requestData(url){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(url);
        }, 1000);
    })
}

// 方案1
requestData("test").then(res => {
    requestData(res + "aa").then(res => {
        requestData(res + "bb").then(res => {
            console.log(res);
        })
    })
})

// 方案2
requestData("test").then(res=>{
    return requestData(res + "aa")
}, () => {}).then(res => {
    return requestData(res + "bb")
}, () => {}).then(res => {
    console.log(res);
}, () => {});

// 方案3 Promise + generator
function* getData() {
    let val1 = yield requestData("test");
    let val2 = yield requestData(val1 + "aa");
    let val3 = yield requestData(val2 + "bb");
    console.log(val3);
}

const generator = getData();
// generator返回的是对象,它的value是Promise对象
generator.next().value.then(res => {
    generator.next(res).value.then(res => {
        generator.next(res).value.then(res => {
            generator.next(res);
        })
    })
})

// 方案4 (方案3的升级版)
function execGeneractor(generatorFunc) {
    const generactor = generatorFunc();

    function exec(res) {
        const result = generactor.next(res);
        if (result.done === true) {
            return result.value;
        }
        result.value.then(res => {
            exec(res);
        });
    }
    exec();
}

execGeneractor(getData);

// 方案5
async function getData(){
    const res1 = await requestData("test");
    const res2 = await requestData(res1 + "aa");
    const res3 = await requestData(res2 + "bb");
    console.log(res3);
}
getData();
  • 方案1:嵌套,回调套回调
  • 方案2:没有回调套回调,利用Promise的then的特性,可读性差
  • 方案3:利用生成器,但是回调嵌套了
  • 方案4:利用生成器、递归,自动执行代码到结束为止,并且扩展性强
  • 方案5:async + await组合(Promise和生成器组合的语法糖)

async-await-事件循环(ES8)

异步函数 async function

  • async关键字用于声明一个异步函数
    • asyncasynchronous单词的缩写,异步、非同步
    • syncsynchronous单词的缩写,同步,同时
async function foo1(){

}

const foo2 = async() => {
    
}

class Foo{
    async foo3(){

    }
}

异步函数的写法

异步函数的代码在函数中没有特殊内容时,函数的执行流程跟普通函数是一样的

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

foo();  // 1 2 3 4

异步函数的返回值一定是Promise(没有返回值也是Promise)

// 返回普通值
async function foo(){
    console.log("start...");
    console.log("mid code...");
    console.log("end...");
    // 没有返回值时 默认return undefined
    return 123;
}

// 返回带then方法对象
async function foo1(){
    console.log("start...");
    console.log("mid code...");
    console.log("end...");
    // 没有返回值时 默认return undefined
    return {
        then : function(resolve, reject){
            resolve("hhh");
        }
    };
}

// 返回Promise
async function foo2(){
    console.log("start...");
    console.log("mid code...");
    console.log("end...");
    // 没有返回值时 默认return undefined
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("www");
        }, 1000);
    });
}

const promise = foo();
promise.then(res => {
    console.log("promise run code", res);
})
const promise1 = foo1();
promise1.then(res => {
    console.log("promise1 run code", res);
})
const promise2 = foo2();
promise2.then(res => {
    console.log("promise run resolve", res);
}, (err) => {
    console.log("promise run err", err);
});

promise执行的时机是foo()函数return的时候
如果异步函数返回的是含有then方法的对象,跟Promise返回含有then方法对象一样(resolve()函数的参数<=搜索关键字)
如果异步函数返回值是Promise,会等待resolve或者reject执行完毕才会出发then


异步函数的异常,会被捕获为Promisereject的值

async function foo2() {
    console.log("foo2 start");

    throw new Error("error message");

    console.log("foo2 end");
}

foo2().catch(err => {
    console.log(err);
});

console.log("other code...");

继续执行后续代码,打印other code...

function foo1() {
    console.log("foo2 start");

    throw new Error("error message");

    console.log("foo2 end");
}
foo1();
console.log("other code...");

普通函数遇到异常,直接整个中断不会执行最后的输出other code...

await

async函数另外一个特殊之处,就是它可以在内部使用await关键字,而在普通函数中不可以使用

await if only valid in async function

一般而言await后面会跟着也给表达式,并且返回一个Promise

await 表达式(Promise)

function requestData(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("1234");
        }, 10000)
    });
}

// await跟上表达式
async function foo(){
    let res = await requestData();  // 在调用resolve时给到res结果
    console.log("-----", res);
    console.log("-----");

    let res = await requestData();  // 在调用resolve时给到res结果
    console.log("+++++", res);
    console.log("+++++");
}
foo();

await之后的代码会等待await有返回值之后才会执行
可以把await后面的代码理解为是在Promise里面then回调中执行的

// await跟上其他的值
async function foo1(){
    const res1 = await 123;
    console.log(res1);

    const res2 = await {
        then : function(resolve, reject) {
            resolve(234);
        }
    }
    console.log(res2);
}

foo1();

await后面跟普通的值会立即返回
如果返回对象中含有名为thenfunction,会执行then方法,并返回resolve的值

function requestData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject("1234");
        }, 100)
    });
}

async function foo2() {
    const res2 = await requestData();
    console.log(res2);
    console.log("------------");
}

foo2().catch(err => {
    console.log(err);
});

如果await后面的Promise执行reject函数,那么整个foo2都会立即终止,并且触发异步函数catch

async function bar(){
    console.log(222);
    return new Promise((resolve, reject) => {
        resolve();
    });
}

async function foo(){
    console.log(111);
    await bar();
    console.log(333);
}

foo();
console.log(444);
// 111
// 222
// 444
// 333

function foo1(){
    console.log(111);
    bar().then(() => {
        console.log(333);
    }, () => {
        console.log(333);
    });
}

比较foo()foo1()理解await 可以把await后面的代码理解为是在Promise里面then回调中执行的

事件循环

浏览器事件循环

JavaScript是单线程的,但是JavaScript的线程应该有自己的容器进程浏览器或者Node

  • 浏览器

    • 多数的浏览器其实是多进程的,当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出
    • 每个进程又有很多的线程,其中包括执行JavaScript代码的线程
  • JavaScript是在一个单独的线程中执行的

    • JavaScript同一时刻只能做一件事
    • 如果做的事非常耗时,就意味着当前线程会被阻塞
  • 真正耗时的操作,实际上并不是JavaScript线程在执行

    • 浏览器的每个进程都是多线程的,耗时的操作可以交给其他线程完成
    • 比如网络请求、定时器等,只需要在特定的时候执行传入的回调即可

浏览器事件循环

如果在执行JavaScript代码的过程中,需要异步操作(比如setTimeout),这个函数会被放入到调用栈中,执行会立即结束,并不会阻塞后续代码的的执行

蓝色为JavaScript线程
黄色为浏览器其他进程
绿色为任务队列

  • 任务队列
    • 宏任务队列(macrotask queue): 定时器、ajax、DOM事件、UI Rendering回调
    • 微任务队列(microtask queue): queueMicrotask、Promise的then回调、MutationObserver

在执行任何一个宏任务之前,都需要保证微任务队列已经被清空

一般而言都是微任务先执行,在执行宏任务

main script中的代码优先执行(编写的顶层script代码),然后再判断执行宏任务/微任务

// setTimeout1
setTimeout(function (){
    console.log("setTimeout1");
    new Promise((resolve, reject) => {
        resolve();
    }).then(function() {
        new Promise(function (resolve, reject){
            resolve()
        }).then(function() {
            console.log("then4");
        });
        console.log("then2");
    });
});

new Promise(function(resolve, reject) {
    console.log("promise1");
    resolve();
}).then(function() {
    console.log("then1");
});

setTimeout( function() {
    console.log("setTimeout2");
})

console.log(2);

queueMicrotask(() => {
    console.log("queueMicrotask");
})

new Promise(function(resolve, reject){
    resolve();
}).then(function() {
    console.log("then3");
});

// promise1
// 2
// then1
// queueMicrotask
// then3
// setTimeout1
// then2
// then4
// setTimeout2

Promiseexector回调函数不会进入任务队列,而是直接执行
建议运行查看代码执行流程

浏览器事件循环

序号 执行顺序 注释
1 setTimeout回调函数加入到宏任务队列 默认设置为0s,但是不会立即执行回调函数内容,而是加入到宏任务队列,因为此时main script代码并未执行完
2 console.log("promise1"); Promiseexector回调函数不会进入任务队列,而是直接执行
3 promise1resolve(),将promise1then加入到微任务队列 代码顺序执行,而根据浏览器规定,Promise的then加入到微任务
4 setTimeout回调函数加入到宏任务队列 与第一个setTimeout相同处理
5 console.log(2) main script代码直接执行
6 queueMicrotask将回调加入到微任务队列 浏览器规定queueMicrotask加入到微任务队列
7 执行promiseresolve,将该Promisethen添加到微任务 Promisethen添加到宏任务
8 先清空微任务队列,依次执行console.log("then1")console.log("setTimeout2")console.log("then3"); 在执行任何一个宏任务之前,都需要保证微任务队列已经被清空
9 执行宏任务中第一个setTimeoutnew一个新的Promise并将then加入到微任务
10 因为添加了新的微任务,此时微任务列表不为空,所以执行微任务新增Promisethen中的console.log("then2")以及new Promise并加入新任务到微任务队列 微任务不为空,时刻注意
11 因为微任务队列不为空,所以执行console.log("then4")
12 微任务空,执行宏任务中最后一个setTimeout

async function async1(){
    console.log("async1 start");
    await async2();
    console.log("async1 end");
}

async function async2(){
    console.log("async2");
}

console.log("main script start");

setTimeout(function(){
    console.log('setTimeout');
}, 0);

async1();

new Promise(function(resolve, reject) {
    console.log("promise1");
    resolve();
}).then(() => {
    console.log("promise2");
}, () => {})

console.log("main script end");

/**
 * main script start
 * async1 start
 * async2
 * promise1
 * main script end
 * async1 end
 * promise2
 * setTimeout
 */
序号 执行顺序 注释
1 console.log("main script start"); main script 优先执行
2 setTimeout加入到宏任务队列 前面案例有讲,浏览器规则
3 执行async1函数,直接console.log("async1 start")然后将async2作为Promiseexector直接执行console.log("async2")并将后续代码作为Promisethen async函数运行规则前面有讲
4 直接执行Promiseexector——console.log("promise1")并将then加入到微任务队列 Promiseexector直接执行
5 console.log("main script end") main script代码直接执行
6 根据微任务队列顺序执行console.log("async2 end")console.log("async2 end") 优先清空微任务
7 执行console.log('setTimeout') 清空宏任务

Promise.resolve().then(() => {
    console.log(0);
    // return Promise.resolve(4);
    
    // return 4;

    return {
        then : function(resolve, reject){
            resolve(4);
        }
    }
}).then((res) => {
    console.log(res);
});

Promise.resolve().then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(5);
}).then(() => {
    console.log(6);
});

分别执行Promise的三种return,比较差异
return {then : function() {}}then方法会被推移到微任务的下一次轮询中执行
return Promise.resolve() 因为return不是普通的值会往后推移一次,又因为Promise.resolve会再往后推移一次,所以累计往后推移两次

Node事件循环

  1. 开启Node进程
  2. Node进程是多线程的
  3. 多个线程中的一个是JS线程,负责执行JS代码
  4. 定时器、耗时操作交给其他线程处理
  5. 再将任务提交到任务队列中(跟浏览器的任务队列类似)
  • 浏览器中的事件循环(event loop)是根据HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现,而Node中是由libuv实现的

  • Node的架构图

    • libuv中主要维护EventLoop(事件循环)和worker threads(线程池)
    • EventLoop负责调用系统的一些其他操作:文件IO、Network、child-processes等
  • libuv是一个多平台的专注于异步IO的库,最初是为Node开发的,现在也用到Luvit、Julia、pyuv等地方

Node架构

  • 事件循环像是一个桥梁,连接着应用程序和JavaScript和系统调用之间的通道

    • 无论是文件IO、数据库、网络IO、定时器、子进程,在完成对应的操作后,都会将对应的结果和回调函数放到事件循环(任务队列)中
    • 事件循环会不同的从任务队列中取出对应的事件(回调函数)来执行
  • 完整的事件循环Tick分成很多阶段

    1. 定时器:本阶段执行已经被setTimeoutsetInterval的调度回调函数
    2. 待定回调:对某些系统操作(比如TCP错误类型)执行回调,比如TCP连接时接收到ECONNERFUSED(链接拒绝错误)
    3. idel、prepare:仅系统内部调用
    4. 轮询:检索新的I/O事件:执行与I/O相关的回调
    5. 检测setImmediate()回调函数在这里执行
    6. 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)
  • Node的任务队列也区分宏任务微任务

    • 宏任务:setTimeout、setInterval、IO事件、setImmediate、close事件
    • 微任务:Promise的then回调,process.nextTick、queueMicrotask

Node事件循环Tick的每个阶段执行任务都会按照优先清空微任务队列,再处理宏任务

  • 但是Node的事件循环不只是 微任务队列宏任务队列

    • 微任务队列
    • next tick queue : process.nextTick
    • other queue : Promise的then回调、queueMicrotask
    • 宏任务队列
    • timer queue : setTimeout、setInterval
    • poll queue : IO事件
    • check queue : setImmediate
    • close queue : Close事件
  • 所以在每次事件循环的tick中,会按照如下的顺序执行代码

    1. next tick microtask qeue
    2. other microtask queue
    3. timer queue
    4. poll queue
    5. check queue
    6. close queue

错误处理

function sum(num1, num2){
    // 目标num1、num2是数字,对于其他数据类型希望抛出异常
    if (typeof num1 !== "number" || typeof num2 !== "number"){
        throw "parameters is error type";
    }
    return num1 + num2;
}

console.log(sum({}, true)); // 一次错误的函数调用
  • 封装了工具函数或者其他函数库,想告诉外界封装函数在某些情况下出现了错误,并且想要调用者知道这个错误,就需要通过throw抛出错误信息

  • throw语句用于抛出一个用户自定义异常,当遇到throw语句时,当前函数的执行会被停止(throw后面的语句不会执行)

class MyError{
    constructor(errorcode, errorMessage){
        this.errorMessage = errorMessage;
        this.errorcode = errorcode;
    }
}

class MyError2 extends Error {

}

function foo(type){
    console.log("start");

    if(type === 0){
        throw "param can't 0";      // 直接返回字符串
    } else if (type === 1){
        throw { errorcode : -1, errorMessage : "type 不能为 1"};    // 返回对象,包含更多信息
    } else if (type === 2){
        throw new MyError(-2, "type 不能为2");  // 返回指定类型对象
    } else if (type === 3){
        throw new Error("type不能为3");         // 使用系统提供的Error对象,打印信息更多(函数调用栈)
    } else if (type === 4){
        throw new TypeError("类型错误");        // 抛出Error的子类
    }

    console.log("end");
}

foo(0);
console.log("other code");
  • Error包含三个属性
    • message:创建error对象时传入的message
    • name:Error的名称,通常和类的名称一致
    • stack:整个Error的错误信息,包括函数的调用栈,当我们直接打印Error对象时,打印的就是stack
  • Error的一些子类

    • RangeError:下标值越界
    • SyntaxError:语法解析错误
    • TypeError:类型错误
  • 针对异常的两种处理方法

    • 不处理,会继续将异常往外层函数抛出,一直抛出到顶层调用为止(main script),如果到顶层都没处理,程序会直接中止
    • 使用try...catch...捕获异常
function foo() {
    throw new Error("foo err");liu'lan
}

function bar() {
    try {
        foo();
    } catch (err){
        console.log(err);
    } finally {
        console.log("bar end");
    }
}

catch参数中的err就是foo抛出的Error对象
finally不管是否发生异常,最后finally的代码一定会执行

JS模块化

  • 模块化
    • 模块化的最终目的是将程序划分成一个个小的结构
    • 结构中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的结构
    • 结构希望暴露的变量函数对象等到处给其他结构使用
    • 可以通过某种方式,导入另外结构中的变量函数对象

一般来说一个文件就是一个模块

因为JavaScript是在ES6(2015年)才支持模块化,所以在此之前有很多其他的社区规范:AMDCMDCommonJS

CommonJS用的还是很多,AMDCMD现在用的少,其他的规范就很少使用了

CommondJS

  • CommonJS是一个规范,最初叫ServerJS,后来为了体现它的广泛性改名CommonJS,简称CJS

    • Node时CommonJS在服务器端一个具有代表性的实现
    • Browserify时CommonJS在浏览器中的一种实现
    • webpack打包工具具备对CommonJS的支持和转换
  • Node中对CommonJS进行了支持和实现

    • Node中每一个js文件都是一个单独的模块
    • 模块中包括CommonJS规范的核心变量:exports(导出)、module.exorts、require(导入)
    • 可以使用上述变量来方便的进行模块化开发

exportsmodule.exports可以负责对模块中的内容进行导出
require函数可以导入其他模块(自定义模块、系统模块、第三方库模块)中的内容

// run1.js
const name = "x";
const age = 19;

function sum(num1, num2){
    return num1 + num2;
}

// 1. module.export
module.exports = {
    name,
    age,
    sum,
    aaa : "aaa"
}

modulerun.js模块本身的对象,exports也是一个对象

// run2.js
const {name, age, sum, aaa} = require("./run1.js");
console.log(name);          // x
console.log(age);           // 19
console.log(sum(1, 2));     // 3
console.log(aaa);           // aaa

module.exportsrequire(file)指向的同一块内存区域

// run1.js
const name = "x";
const age = 19;

function sum(num1, num2){
    return num1 + num2;
}

// 2.exports
exports.name = name;
exports.age = age;
exports.sum = sum;
exports.aaa = "aaa";

// Error
exports = {
    name,
    age,
    sum
}

CommondJS底层中是先module.exports = {}然后exports = module.exports
最后导出出去的肯定是module.exports,所以重新给exports赋值不会影响到module.exports从而导致导出失败

require规范

  • 常见require的查找规则(require(XX))
    • 如果XX是一个Node的核心模块,比如path、http,直接返回核心模块,并且停止查找(require("path"))
    • 如果XX是一个路径
    • 如果有后缀名,按照后缀名的格式查找对应的文件(require("./abc.js"))
    • 如果没有后缀名
      1. 直接查找文件XX
      2. 查找XX.js文件
      3. 查找XX.json文件
      4. 查找XX.node文件
    • 如果没有找到对应的文件,将XX作为目录,并查找目录下的index文件
      1. XX/index.js文件
      2. XX/index.json文件
      3. XX/index.node文件
    • 既不是路径也不是核心模块,去node_modules查找

模块的加载过程

  1. 模块再被第一次引入的时候,模块中的js代码会被执行一次
  2. 模块被多次引入时,会缓存,最终只加载(运行)一次
    • 因为模块对象module有个属性loaded,ture表示已经加载,false表示未加载
  3. 循环引入的加载顺序,深度优先搜索:main->aaa->ccc->ddd->eee->bbb

循环引用的加载顺序

暂停 跳过 工作暂时不涉及 为时间紧张暂不学习

JSON-数据存储

JSON是非常重要的数据格式,轻量级资料交换格式

  • JSON使用场景

    • 网络数据的传输JSON数据
    • 项目的配置文件
    • 非关系型数据库(NoSQL)将json作为存储格式
  • JSON的顶层支持三种类型的值

    • 简单值:数字(Number)、字符串(String,不支持单引号)、布尔类型(Boolean)、null类型
    • 对象值:k-v组成,key是字符串类型,而且必须双引号,值可以是简单值、对象值、数组值
    • 数组值:数组的值可以是简单值、对象值、数组值
123
"123"
null
true
{
    "name" : "x",
    "frien" : {
        "name" : "y"
    },
    "hobbies" : ["1", "2"]
}
[
    "abc",
    1234,
    {
        "name" : "x"
    }
]

JSON注释不能写
最后一个item后面不能加上逗号

JS中使用JSON

const obj = {
    name : "x",
    age : 10,
    hobbies : ["1", "2"]
}

// 将obj转成JSON格式的字符串
const objString = JSON.stringify(obj);
console.log(objString);

// JSON格式转成对象
const info = JSON.parse(objString);
console.log(info);

一些细节

const obj = {
    name : "x",
    age : 10,
    hobbies : ["1", "2"]
}
// JSON.stringify(value : any, replacer ?: (this : any, key : string, value : any) => any, splace ? : string | number) : string

// 直接转换
const objString = JSON.stringify(obj);
console.log(objString);

// stringify的第二个参数 replacer

// replacer传入数组 设定哪些key是需要转换的
const objString2= JSON.stringify(obj, ['name', 'age']);
console.log(objString2);

// 传入回调函数 遍历整个kv对,对数据进行处理
const objString3 = JSON.stringify(obj, (key, value) => {
    if(key === "age"){
        value += 1;
    }  
    return value;
});
console.log(objString3);

// stringify的第三个参数用于美化,第二个参数设null表示使用默认的

// 传入数字,表示缩进空格数目
const jsonString4 = JSON.stringify(obj, null, 2);
console.log(jsonString4)

const jsonString5 = JSON.stringify(obj, null, 4);
console.log(jsonString5)

// 传入字符,表示缩进不使用空格,而是指定字符串
const jsonString6 = JSON.stringify(obj, null, ".");
console.log(jsonString6)

JSON.stringify方法的一些解释和用法

const obj = {
    name : "x",
    age : 10,
    hobbies : ["1", "2"],
    toJSON : function() {
        return "myself JSON String";
    }
}

const objString = JSON.stringify(obj);
console.log(objString);

const objString3 = JSON.stringify(obj, (key, value) => {
    if(key === "age"){
        value += 1;
    }  
    return value;
});
console.log(objString3);

当对象中存在toJSON方法时,JSON.stringify会直接使用toJSON的返回值

const objString = '{"name":"x","age":10,"hobbies":["1","2"]}';
const obj = JSON.parse(objString);

// JSON.parse(text : string, reviver : (this : any, key : string, value : any) => any) : any

// 第二个参数 reviver,对每个key、value进行操作
const obj2 = JSON.parse(objString, (key, value) => {
    if(key === "age"){
        value += 1;
    }
    return value;
});
console.log(obj2);

利用JSON做深拷贝

就是利用JSON.stringifyJSON.parse做对象转换

自定义深拷贝函数

  • 几种对象赋值关系
    • 引入的赋值:指向同一个对象,互相之间影响(x = y)
    • 对象的浅拷贝:只是浅层的拷贝,内部引入对象时,依然会互相影响(x = [...y])
    • 对象的深拷贝:两个对象不再有任何关系,不会互相影响
// 简单的深拷贝
let s1 = Symbol();
const boj = {
    name : "x",
    foo : function() {
        return 1;
    }
    [s1] : "qwe",
}
boj.inner = obj;    // 某些情况需要自己引用自己
const info = JSON.parse(JSON.stringify(boj));

利用JSON深拷贝对象时,对函数无法进行处理
如果Symbol作为key,也无法处理
不支持循环引用

  • 自定义深拷贝函数
    • 自定义深拷贝的基本功能
    • Symbol的key进行处理
    • 其他数据类型的值进程处理:数组、函数、Symbol、Set、Map
    • 对循环引用的处理
function isObject(value){
    const valueType = typeof value;
    return (value !== null) && (valueType === "object" || valueType === "function");
}

// 因为deepClone可能在多个地方调用,所以将noteObjs声明为局部变量
// 又因为Map是强引用会影响对象的销毁,所以需要使用WeakMap弱引用
function deepClone(originValue, noteObjs = new WeakMap()){
    // 判断是否是set类型 这里写的是浅拷贝 一般而言够用
    if (originValue instanceof Set){
        return new Set([...originValue]);
    }

    // 判断是否是map类型 这里写的是浅拷贝 一般而言够用
    if (originValue instanceof Map){
        return new Map([...originValue]);
    }
    
    // 一般而言函数可以复用 直接返回就行
    if(typeof originValue === "function"){
        return originValue;
    }
    
    // 普通类型直接return 普通类型是值拷贝
    if(!isObject(originValue)){  
        return originValue;
    }

    if(noteObjs.has(originValue)){
        return noteObjs.get(originValue);
    }
    
    // 不做判断,数组直接赋值给对象是错误的效果
    // const newObj = {};
    const newObj = Array.isArray(originValue) ? [] : {};    
    noteObjs.set(originValue, newObj);  // 记录一下 表示该对象本次被处理了 防止对象中循环引用
    for(const key in originValue){
        newObj[key] = deepClone(originValue[key], noteObjs);  // 递归拷贝复制
    }
    
    // 对Symbol的key做特殊处理,因为Symbol作为key时遍历不到
    const symbolKeys = Object.getOwnPropertySymbols(originValue);
    for(const sKey of symbolKeys){
        // 一般而言Symbol是为了防止对象内部key的冲突,不同对象倒不影响
        // const newsKey = Symbol(sKey.description);
        // newObj[newsKey] = deepClone(originValue[sKey]);
        newObj[sKey] = deepClone(originValue[sKey], noteObjs);
    }

    return newObj;
}
const s1 = Symbol("aaa");
const s2 = Symbol("bbb");
const obj = {
    name : "x",
    age : 10,
    friend : {
        name : "y",
        age : 11,
        address : {
            city : "BJ"
        }
    },
    hobbies : [1, 2, 3],
    foo : function(){
        console.log("qwer");
    },
    [s1] : "aaa",
    s2key : s2,
    set : new Set([1, 2, 3, 4]),
    map : new Map([["x", 1], ["y", 2]])
}

obj.inner = obj;    // 自己引用自己

const newObj = deepClone(obj);
console.log(newObj);