JS高级部分详解

1.浏览器运行原理

JavaScript是一门高级语言,它主要运行在浏览器上,其表达形式更接近人类的思维,但是机器并不能直接识别高级语言,所以需要被转换为机器指令。所以我们以下就探讨下JS代码是如何在浏览器中被执行的。

在浏览器中输入url后会发生什么

我们在讨论浏览器之前先思考下我们经常使用的浏览器在输入打开一个网址后会发生什么。

  1. 首先浏览器会对输入的url进行编码,保证一些特殊字符转义不会发生歧义,详细的编码规则可以自行研究。
  2. 进行dns解析查询ip
  3. 进行tcp连接的的三次握手。
  4. 建立连接,请求获取html文件,这里如果有缓存的话就需要考虑缓存相关情况。
  5. 获取html文件后就会进行html解析,解析html文档生成dom树,加载样式文件生成cssom树,如果遇到js就会去执行js文件。然后根据最终生成的dom树和cssom树生成渲染树,按照顺序进行布局排版将每一个节点布局在屏幕的正确位置上,最后遍历渲染绘制所有节点,为节点适用对应的样式。

    image-20211227201919393

浏览器内核

不同的浏览器有着不同的内核,以下是一些常用浏览器使用的内核:

  • Gecko:早期被NetscapeMozilla Firefox浏览器浏览器使用
  • Trident:微软开发,被IE4~IE11浏览器使用,但是Edge浏览器已经转向Blink
  • Webkit:苹果基于KHTML开发、开源的,用于SafariGoogle Chrome之前也在使用
  • Blink:是Webkit的一个分支,Google开发,目前应用于Google ChromeEdgeOpera

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

js引擎

高级的编程语言都是需要转成最终的机器指令来执行的,我们编写的js最终都会交由CPU执行,js引擎的作用就是将js代码翻译成cpu能够执行的机器代码。

常见的js引擎:

  • SpiderMonkey:第一款JavaScript引擎,由Brendan Eich开发(也就是JavaScript作者)。
  • Chakra:微软开发,用于IT浏览器。
  • JavaScriptCoreWebKit中的JavaScript引擎,Apple公司开发(小程序中就是使用的这个引擎来执行js的)。
  • V8Google开发的强大JavaScript引擎,也帮助Chrome从众多浏览器中脱颖而出。

这里主要聊一些v8引擎:

v8基本的认识可以件这篇文章深入理解Node.js (一)---Node基础概念

官方的解析图:
image-20211227203127642

  1. 首先代码经过blink交由stream进行编码的处理转换。
  2. scanner会进行词法分析,词法分析会将代码转为tokens
  3. tokens会进行解析和预解析,因为并不知所有的js在一开始都要执行,所以对所有的代码进行解析就会有一定的额效率问题,所以v8引擎就实现了lazy parsing(延迟加载)的方案,主要作用是将不必要的函数进行预解析,对于函数的全量解析则是在函数需要执行的时候再进行。
  4. token经过解析后会生成ast树,然后再交由v8引擎中的几个重要的模块进行处理

2.js执行的过程

首先讲解下一些概念名词

全局对象

js引擎在执行代码之前会在堆内存中创建一个全局对象,Golbal Object(GO)

  • 该对象所有作用域(scope)都可以访问。
  • 该对象中包括了很多的内置对象(DateArrayStringNumbersetTimeoutsetInterval等)。
  • 对象内部有个window属性指向自己。

image-20220102161714857

执行上下文栈

js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈。

执行上下文

在执行上下文栈中执行的是全局的代码块。为了全局代码能够正常的执行,内部还需要构建一个全局执行上下文(Global Execution Context,简称GEC),GEC会被放入到ECS中执行。然后全局执行上下文中存在一个变量对象(Variable Object,简称VO)指向GO

GEC被放入到ECS里面后包含两个部分:

  • 在代码执行前的编译阶段parse转成AST的过程中,会创建一个VO(指向GO),将全局的变量,函数等加入到GO中,此时不会进行赋值操作,这个过程也称之为作用域提升(hoisting)。
  • 对变量进行赋值或者执行函数中的代码。

例如下面一段代码:

var name = 'why';
console.log(name1);
var num1 = 100;

上述代码执行流程如下:

image-20220102195835442

代码中有函数怎么执行

在执行代码的过程中遇到了函数,就会根据函数体创建一个函数执行上下文(Function Execution Context,简称FEC),并且添加到ECS中。

FEC包含三个部分:

  • 在解析成AST树结构的时候会创建一个活动对象(Activation Object,简称AO),AO包含形参,argument,函数定义和指向函数的对象,定义的变量。
  • 作用域链:由VO(在函数中就是AO)和父级的VO构成,查找的时候会就近一层一层的查找。
  • this绑定的值。

例如以下代码:

var name = "zhangsan"
foo()
function foo() {
    console.log('foo')
}

执行流程:
image-20220102203713441

变量环境和记录

在新版的ECMA标准中,我们将变量对象称之为变量环境(VE),里面的属性和函数声明称之为环境记录。

简单总结

  • 作用域在代码编译的时候就会被确定。
  • 函数中的作用域和其调用位置没关系,与定义位置有关。

3.内存管理

分配内存

任何的编程语言再执行的过程中都需要分配内存,当然不同的语言对于内存的管理有所不同,js是自动管理内存的语言。

当然内存管理大致有以下生命周期:

  • 申请分配内存。
  • 使用内存,存放对象等。
  • 释放内存。

js在定义变量的时候分配内存,对于不同的数据类型内存分配的方式是不一样的:

  • 对于基本类型直接在栈空间进行分配。
  • 对于复杂数据类型会在堆空间进行分配,并将对应空间的指针返回值给栈空间中的变量引用。

当然不同的引擎对于内存空间的分配管理可能不同,这里便于理解就大致分为栈空间和堆空间。

垃圾回收

内存是有限的,所以内存不再使用的时候需要对其进行释放。在一些手动管理内存的编程语言中需要通过一些自己的方式来手动释放内存。这样的方式效率低下,而且对于编程者的要求更高,且容易造成内存泄漏(引用的内存空间不需要使用时,但又没有释放掉)。

当然大部分的现代编程语言都有自己的垃圾回收(Garbage Collection,简称GC)机制了。

当然GC如何知道那些对象不再使用就需要用到GC算法了。

常见的GC算法:

引用计数

当一个对象有一个引用指向它的时候,这个对象的引用计数就+1,当这个对象的引用计数为0的时候就表示没有引用指向它,所以就可以将这个对象销毁掉。

但是这个方式有个弊端就是,当遇到一个循环引用的对象的时候,这两个对象的引用永远都是2,所以永远不会被销毁。

image-20220102210335607

当然解决方式是将两个对象都赋值null

标记清除

该算法是设置一个根对象,垃圾回收器会定时从这个根开始,找所有从根开始有引用的对象,对于那些没有引用到的对象,就认为是不可用的对象,加上不可用的标记。例如函数内部声明一个变量,这个变量会被加上存在于上下文中的标记,在上下文中的变量逻辑上永远不应该被释放掉,只要上下文在运行中,我们就能访问到这个变量。所以当变量离开上下文,我们访问不到这个变量了,就会被加上离开上下文的标记。之后内存清理就会清除这些有标记的变量了。

当然标记清除中的标记方式有很多种,改算法是js引擎中较为常用的一种。

4.闭包

js中,函数非常的灵活,作为头等公民,它既可以作为一个函数的参数,也可以作为返回值返回。

接下来探讨下js闭包的定义。

我最初对闭包的认识是在接触到自执行函数的时候,那时候只知道自执行函数内部形成了闭包可以实现模块化。后来在一些书籍上了解到闭包是一个函数去访问另一个作用域中的变量。但是对于闭包这个概念始终感觉很模糊。

一些权威机构和大佬对闭包的解释:

维基百科

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

MDN

一个函数对其周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就被称之为闭包,也就是说,闭包可以让你在一个内层函数访问到其外层函数的作用域,在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

coderwhy的理解

一个普通的函数,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包。
广义角度来看:js中的函数都是闭包。
狭义角度来看:js函数中如果访问了外层作用域的变量,那该函数就是一个闭包。

闭包的访问过程

以下代码就是形成了闭包:

function fn() {
    const name = "zhangsan";
    return function bar() {
        console.log(name);
    }
}

const fn1 = fn();
fn1();

我们首先看下上述出现的函数作用域包含了那些部分:
image-20220106114834983

可以看到返回的函数内部作用域就包含了一个闭包(closure)。

基本执行过程:
在执行fn1
image-20220106120656979

执行fn1
fn上下文出栈,bar上下文入栈
image-20220106122559659

可以发现bar函数对于父级作用域有引用关系。所以fn的内部的活动对象不会被销毁。

如果要销毁可以执行以下代码:

fn = null;
fn1 = null;

闭包和内存泄漏

上面我们通过简单的画图解释了使用闭包执行的整个过程,这里就得出了一个结论就是使用闭包可能会导致内存泄漏,因为我们可能保留了一些变量的引用导致其继续占用内存。

这里用一个案例来展示下使用闭包时内存的一个大致使用情况。

function createFnArray() {
  var arr = new Array(1024 * 1024).fill(1);
  return function () {
    console.log(arr.length);
  };
}

var arrayFns = [];
for (var i = 0; i < 100; i++) {
  setTimeout(() => {
    arrayFns.push(createFnArray());
  }, i * 100);
}
setTimeout(() => {
  /* for (var i = 0; i < 50; i++) {
    setTimeout(() => {
      arrayFns.pop();
    }, 100 * i);
  } */
  arrayFns = null;
}, 10000);

使用谷歌浏览器的性能测试录制展示如下(20s):

image-20220106155303695

可以看到内存使用情况先循序递增在瞬间下降,这里时间轴只做个大致参考。因为浏览器还有其他的干扰因素导致测试结果不是理想状态。

循序清空数组一半(测试20s):

function createFnArray() {
  var arr = new Array(1024 * 1024).fill(1);
  return function () {
    console.log(arr.length);
  };
}

var arrayFns = [];
for (var i = 0; i < 100; i++) {
  setTimeout(() => {
    arrayFns.push(createFnArray());
  }, i * 100);
}
setTimeout(() => {
  for (var i = 0; i < 50; i++) {
    setTimeout(() => {
      arrayFns.pop();
    }, 100 * i);
  }
}, 10000);

image-20220106155712492

可以看到大致降低了一半的内存使用情况。

闭包和自由变量

还有一个问题,既然闭包中函数访问了外部作用域的自由变量,那么没有被访问的自由变量会不会被销毁呢?

首先在ECMA标准中说明的,闭包的函数有对外部函数的活动对象的引用关系,从之前我们写的执行流程图也可以看出,所以理论上外部的自由变量是一直存在的,不管内部是否有没有使用。但是不同的js引擎对于这个标准的处理有所不同,v8引擎就只会保留使用到的自由变量,其他未使用的的自由变量都会被销毁掉。

function foo() {
  var name = "why"
  var age = 18
  function bar() {
    //这里只是用到了age
      //打个断点调试
      debugger;
    console.log(age)
  }
  return bar
}
var fn = foo()
fn()

在谷歌浏览器中调试下试试:

image-20220106161109882

可以看到闭包中只有age这个变量,再在控制台上输出下agename,这里断点打在函数内部的,所以控制台是可以访问函数内部的变量的。
image-20220106161421819

5.this

thisjs中方便了我们使用一些属性和方法。

this的指向

this在全局环境下

  • 在浏览器,非严格模式下,this指向window

    console.log(this)//window
    function bar () {
        console.log(this)
    }
    bar();//window
  • node环境为{},但是在node环境中的普通函数中的thisglobal对象

当然一般在全局作用于下很少使用this
我们再看一下一组代码:

function bar () {
    console.log(this)
}
//直接调用
bar();//window

//通过对象调用
const obj = {
    name:'zhangsam',
    foo:bar
}
obj.foo();//obj对象

//通过call调用
bar.call('lisi')//lisi

通过上述结果我们可以以得出:

  • 函数在调用的时候会给this一个默认值。
  • this的值和函数的定义位置没有关系。
  • this的绑定和调用方式以及调用的位置有关系。
  • this是在运行时被绑定的。

this的绑定规则

  • 默认绑定
    函数的独立调用,没有被绑定到某个对象上调用。

    function(){
        console.log(this);
    }
    foo();
    function foo(func) {
        func();
    }
    
    const obj = {
        name:'zhangsan',
        bar:function(){
            console.log(this)
        }
    }
    
    foo(obj.bar);
  • 隐式绑定
    函数的调用位置是通过某个对象发起的。

    function foo() {
        console.log(this)
    }
    const obj = {
        bar:foo
    };
    obj.bar();

    隐式绑定有个前提,必须在调用的对象中有一个对函数的引用,如果没有这样的引用汇报错的,正是因为这个引用才将this间接的绑定到了这个对象上。

  • 显示绑定
    通过一些方法,显示的改变this指的对象。
    使用callapplybind方法。

    call和apply

  • new 绑定

    注意执行new发生了什么:

    1. 创建一个对象。
    2. 将新对象的__proto__设置为构造函数的prototype
    3. 将构造函数的this指向新对象。
    4. 执行构造函数中的代码。

      1. 如果构造函数返回非空对象,则返回该对象,否则返回刚创建的新对象。
    function person() {
        console.log(this)
    };
    
    person();
    const p = new person();

this的绑定优先级

显示绑定(call,apply,bind)高于隐式绑定

// var obj = {
//   name: "obj",
//   foo: function() {
//     console.log(this)
//   }
// }

// obj.foo()

// 1.call/apply的显示绑定高于隐式绑定
// obj.foo.apply('abc')
// obj.foo.call('abc')

// 2.bind的优先级高于隐式绑定
// var bar = obj.foo.bind("cba")
// bar()


// 3.更明显的比较
function foo() {
  console.log(this)
}

var obj = {
  name: "obj",
  foo: foo.bind("aaa")
}

obj.foo()

注意bind绑定后的函数this不能被显示改变了,即连续调用bind或者使用bind再使用call或者apply,其this永远为第一次使用bind绑定后的this

const obj = {
  name:'zhangsan'
}
const obj2 = {
  name:'lisi'
}

function a() {
  console.log(this.name)
}

const b = a.bind(obj);
b()
b.call(obj2)
b.apply({name:'wangwu'})

new 高于显示绑定,不能和apply call bind一起使用

// 结论: new关键字不能和apply/call一起来使用

// new的优先级高于bind
function foo() {
  console.log(this)
}

// var bar = foo.bind("aaa")

// var obj = new bar()

总结:

new绑定 > 显示绑定(apply/call/bind) > 隐式绑定(obj.foo()) > 默认绑定(独立函数调用)。

this的特殊情况

忽略显示绑定:

当执行apply/call/bind:传入null/undefined时会自动将this绑定成全局对象

function foo() {
  console.log(this)
}

foo.apply("abc")
foo.apply({})
foo.apply(null)
foo.apply(undefined)

var bar = foo.bind(null)
bar()

间接函数引用:

间接函数引用导致丢失this

var obj1 = {
  name: "obj1",
  foo: function() {
    console.log(this)
  }
}

var obj2 = {
  name: "obj2"
};//这里不加分号或出现语法歧义导致报错
//
 obj2.bar = obj1.foo
 obj2.bar()

(obj2.bar = obj1.foo)()

箭头函数与this

箭头函数有着一些特殊规则:

  • 箭头函数不能绑定thisargument,不能通过显示绑定方法来给箭头函数绑定this
  • 箭头函数不能和new一起使用(没有显示原型,不能作为构造函数)。
  • 箭头函数的this为上层作用域终端的this

这里写几个案例:

class Test {
  constructor() {
  }
  fun = () => {
    console.log(this)
  };
  fun1(){
    console.log(this)
  }
}
let test = new Test()
test.fun()
test.fun.call(this)
test.fun1()
test.fun1.call(this)
//Test { fun: [Function: fun] }
//Test { fun: [Function: fun] }
//Test { fun: [Function: fun] }
//window
const obj = {
  name:'zhangsan',
  foo:() => {
    console.log(this);
  }
}
obj.foo()//window

6.函数式编程

使用js模拟call、apply、bind方法

这三个方法都是js原生自带的方法,用于改变函数内部的this指向,但是它们原生是使用C++实现的,这里我们为了了解this使用使用js来简单模拟下,当然会实现主要功能,但实际部分边界情况不会考虑。

call方法实现

Function.prototype.hCall = function(thisArg, ...args) {
  // 获取执行的函数
  const fn = this;
 // 转化this参数为对象,并判断边界
 thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window;
 // 隐式修改fn的this:
 thisArg.fn = fn;
 // 执行函数,并保存返回值
 const res = thisArg.fn(...args);
 // 删除this上的该函数,防止污染this对象
 delete thisArg.fn;
 // 返回函数执行结果
 return res;
}

function sum(a,b) {
  console.log(this);
  console.log(a,b);
}

sum.hCall(1);
sum.call(1,2,3);

apply方法实现

Function.prototype.happly = function(thisArg) {
  const fn = this;
  // 处理边界 零外还要考虑如果我们传入的this对象已经有了该函数对象怎么办(使用symbol)
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window;
  thisArg.fn = fn;
  let res = null;
  if(arguments[1]) {
    res = thisArg.fn(...arguments[1])
  } else {
    res = thisArg.fn();
  }
  delete thisArg.fn;
  return res;
}

function sum(a,b) {
  console.log(this);
  console.log(a,b);
}
sum.happly(1,[2,3]);
sum.apply(1,[2,3]);

bind方法实现

Function.prototype.hBind = function(thisArg,...args) {
  const  fn = this;
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window;
  function newFn(...otherArgs) {
    thisArg.fn =  fn;
    const res = thisArg.fn(...args,...otherArgs);
    delete thisArg.fn
    return res;
  }
  return newFn;
}

function sum(a,b,c,d) {
  console.log(this)
  console.log(a,b,c,d);
}

const a = sum.bind(0,1,2);
const b = sum.hBind(0,1,2);

arguments的相关认识

这个参数是传递给一个函数的参数类数组对象,注意不是数组,是类数组,本质上是对象,只是能调用length方法和通过索引访问。

function foo(num1, num2, num3) {
  // 类数组对象中(长的像是一个数组, 本质上是一个对象): arguments
  console.log(arguments);
  // 还可以获取当前函数 递归的时候可以用到
  console.log(arguments.callee)  
}

foo(10, 20, 30, 40, 50);

image-20220327182433478

arguments转为数组

有以下方法:

// 方式一 自行遍历放入一个数组总
const newArr = []
for (let i = 0; i < arguments.length; i++) {
  newArr.push(arguments[i] * 10)
}
console.log(newArr)
// 方式二 借用数组方法
// slice本身可以将一个可迭代对象转为数组,但是arguments本身不能调用slice方法,所以通过以下写法可以实现调用slice
const newArr2 = Array.prototype.slice.call(arguments)
// 这种方式也可以
// const newArr2 = [].slice.call(arguments)
console.log(newArr2)

// es6 方法 更推荐
  const newArr4 = Array.from(arguments)
  console.log(newArr4)
// 解构
  const newArr5 = [...arguments]
  console.log(newArr5)

注意:箭头函数也是没有arguments这个参数的,所以它的arguments属于其最近上层作用域普通函数中的。

纯函数

概念

函数式编程中有个很重要的概念就是纯函数。

如果一个函数满足以下条件,就可以称之为纯函数:

  • 函数输入相同的值,总是输出相同的值。
  • 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
  • 函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。

副作用的理解

再执行一个函数的时候除了返回函数值外,还对调用函数产生了附加影响,比如修改了全局对象,修改参数或者改变了外部内存或者环境。

案例

以下介绍几个纯函数案例:

slice

slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组。slice这里就属于纯函数,

const newNames1 = names.slice(0, 3)
console.log(newNames1)
console.log(names)

splice

splice执行的时候会对原数组进行修改,所以不是纯函数。

const newNames2 = names.splice(2)
console.log(newNames2)
console.log(names)
// 纯函数
function foo(num1, num2) {
  return num1 * 2 + num2 * num2
}

// bar不是一个纯函数, 因为它修改了外界的变量
var name = "abc" 
function bar() {
  console.log("bar其他的代码执行")
  name = "cba"
}
bar()
console.log(name)

// baz也不是一个纯函数, 因为我们修改了传入的参数
function baz(info) {
  info.age = 100
}
var obj = {name: "why", age: 18}
baz(obj)
console.log(obj)



// test是否是一个纯函数? 是一个纯函数
// 无论怎么调用,只要输入确定,输出就确定
 function test(info) {
   return {
     ...info,
     age: 100
   }
 }

 test(obj)
 test(obj)
 test(obj)
 test(obj)

纯函数的优势

  • 写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或 者依赖其他的外部变量是否已经发生了修改。
  • 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出。

比如reactvue中都不建议直接修改props的值。

柯里化

概念

柯里化用于将一个接收多个参数的函数,变为接收单个参数的函数,并返回另一个函数来处理剩下的参数的函数。

简单来讲柯里化就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数的过程。

基本结构

// 未柯里化的函数
function add(x, y, z) {
  return x + y + z
}

var result = add(10, 20, 30)
console.log(result)

// 柯里化的函数
function sum1(x) {
  return function(y) {
    return function(z) {
      return x + y + z
    }
  }
}

var result1 = sum1(10)(20)(30)
console.log(result1)

// 简化柯里化的代码
var sum2 = x => y => z => {
  return x + y + z
}
// 注意调用方式
console.log(sum2(10)(20)(30))

var sum3 = x => y => z => x + y + z
console.log(sum3(10)(20)(30))

为什么使用柯里化

在函数式编程中,我们往往希望一个函数的责任尽可能的单一,而不是讲一大堆逻辑交由一个函数进行处理。柯里化就是希望一个函数尽可能完成单一的事情,满足单一职责原则(SRP:single responsibility principle)。

function add(x, y, z) {
  x = x + 2
  y = y * 2
  z = z * z
  return x + y + z
}
console.log(add(10, 20, 30))

function sum(x) {
  x = x + 2
  return function(y) {
    y = y * 2
    return function(z) {
      z = z * z
      return x + y + z
    }
  }
}
console.log(sum(10)(20)(30))

同时使用柯里化还可以进行逻辑复用:

function sum(m, n) {
  return m + n;
}

// 如果要计算5和和另外一个数相加的结果
console.log(sum(5, 10));
console.log(sum(5, 15));
console.log(sum(5, 150));
console.log(sum(5, 20));
// 可以看出每次都要写上5这个参数

// 柯里化处理
function sumCur(count) {
  // 如果这里还有对count的处理逻辑,之后的返回的函数调用都会复用这里的逻辑了
  return function (num) {
    return count + num;
  };
}

//就不用一直写两个参数了。
const count = sumCur(5);
console.log(count(10));
console.log(count(15));
console.log(count(150));
console.log(count(20));

打印日志:

/*
 * @Date: 2022-03-27 18:57:49
 * @LastEditors: zhangheng
 * @LastEditTime: 2022-03-27 18:59:59
 */
function log(date, type, message) {
  console.log(
    `[${date.getHours()}:${date.getMinutes()}][${type}]: [${message}]`
  );
}

// 如果我们只是想输入debug 这个类别的log,每次输出都要协商debug这个参数
log(new Date(), "DEBUG", "查找到轮播图的bug");
log(new Date(), "DEBUG", "查询菜单的bug");
log(new Date(), "DEBUG", "查询数据的bug");

// 通过柯里化我们可以简化很多参数的使用。
// 柯里化的优化
var log = (date) => (type) => (message) => {
  console.log(
    `[${date.getHours()}:${date.getMinutes()}][${type}]: [${message}]`
  );
};

// 如果我现在打印的都是当前时间
var nowLog = log(new Date());
nowLog("DEBUG")("查找到轮播图的bug");
nowLog("FETURE")("新增了添加用户的功能");
// 都是debug类型
var nowAndDebugLog = log(new Date())("DEBUG");
nowAndDebugLog("查找到轮播图的bug");
nowAndDebugLog("查找到轮播图的bug");
nowAndDebugLog("查找到轮播图的bug");
nowAndDebugLog("查找到轮播图的bug");

// 都是feture功能
var nowAndFetureLog = log(new Date())("FETURE");
nowAndFetureLog("添加新功能~");

自动柯里化

我们上面都是自己写的柯里化结构函数,这里我们实现一个能将普通函数转为柯里化函数的方法:

/*
 * @Date: 2022-03-27 16:24:59
 * @LastEditors: zhangheng
 * @LastEditTime: 2022-03-27 16:44:08
 */
function add(x, y, z) {
  return x + y + z;
}

function currying(fn) {
  return function curried(...args) {
    // 首先判断参数个数,如果没有一次性传递玩才返回新函数,否则就直接执行函数了
    if (args.length >= fn.length) {
    // 注意绑定this
      return fn.apply(this, args);
    } else {
    // 没有达到个数时, 需要返回一个新的函数, 继续来接收的参数 
      return function (...args2) {
    // 接收到参数后, 需要递归调用curried来检查函数的个数是否达到   
        return curried.apply(this, [...args, ...args2]);
      };
    }
  };
}

const curryAdd = currying(add);

console.log(add(10, 20, 30));
console.log(curryAdd(10, 20, 30));
console.log(curryAdd(10)(20)(30));
console.log(curryAdd(10, 20)(30));
console.log(curryAdd(10)(20, 30));

组合函数

有时候我们需要对一个参数进行处理,我们可能将处理步骤分为多个函数进行调用处理:

function double(num) {
  return num * 2
}

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

var count = 10
var result = square(double(count))
console.log(result)

上述就是基本的调用过程,但是看得出,函数嵌套调用导致过程并不清晰易懂。

我们来优化下:

function double(num) {
  return num * 2
}

function square(num) {
  return num ** 2
}
// 实现最简单的组合函数
function composeFn(m, n) {
  return function(count) {
    return n(m(count))
  }
}
var newFn = composeFn(double, square)
console.log(newFn(10))

上述的调用过程就很清晰易懂了,但是这个组合函数并不通用,万一我们想传递不固定个数的函数呢,这里封装一个较为通用性的方法:

function double(num) {
  return num * 2
}
function square(num) {
  return num ** 2
}
// 通用组合函数实现
function hComponseFn(...fns) {
  // 判断下参数是否为函数
  const length = fns.length;
  for (let i = 0; i < length; i++) {
    if (typeof fns[i] !== "function") {
      throw new TypeError("参数必须为函数");
    }
  }
  return function (...args) {
    let index = 0;
    //如果没有传入函数参数就直接返回
    let result = length ? fns[index].apply(this, args) : args;
    while (++index < length) {
      result = fns[index].call(this, result);
    }
    return result;
  };
}

//这样使得函数执行过程清晰易懂
const newFn2 = hComponseFn(double, square);
console.log(newFn2(10));

js的严格模式

在讲解js的严格模式之前线补充一些额外的知识。

with语句

with语句用于扩展一个语句的作用域链。

const person = {
    name:'zhangsan',
    age:18
}
with(person) {
    console.log(name);
    console.log(age)
}

JavaScript在查找一个变量的额时候会通过作用域链来进行查找,作用域链是跟执行代码的上下文或者包含这个变量的函数有关,with语句会将某个对象添加到作用域链的顶端,如果在with的代码块中有个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。

所以我们可以通过with来简化对某一对象取值的多余代码:

const person = {
    name:'zhangsan',
    age:18
}
//下面with的打印和上面的一样
console.log(person.name);
console.log(person.age)
with(person) {
    console.log(name);
    console.log(age)
}

当然with也有很大的弊端,会导致语义不明:

function f(x, o) {
  with (o)
    print(x);
}

总之with知道大致用法就行,在实际的代码中并不推荐使用它,它会造成语义不明或者兼容问题。

eval

eval函数是JavaScript内置的一个函数,可以传入一个字符串进去,函数会将该字符串作为代码进行解析执行。

eval('const name = "zhangsan"; console.log(name);');

反正不建议在实际的开发中使用eval,它有以下弊端:

  • 可读性差。
  • 不安全,会执行传入的字符串。
  • 性能问题,eval的执行必须经过JS解释器,不能被JS引擎优化。

严格模式

严格模式是es5标准中就提出的,目的是为了显示JavaScript的灵活,因为JavaScript的语法非常的宽松,所以严格模式就是为了避免写出居于隐形错误的代码,对代码有更严格的检测和执行。

严格模式的作用:

  • 严格模式通过抛出错误来消除一些原有的 静默(silent)错误。
  • 严格模式让JS引擎在执行代码时可以进行更多的优化,不需要对一些特殊的语法进行优化。
  • 严格模式禁用了在未来版本中可能会定义的一些语法,保证了代码的执行兼容性。

开启严格模式

可以根据粒度来进行开启。

全局开启,在js文件的顶层添加 "use strict";即可

"use strict";

// xxx

局部开启,对一个函数开启严格模式:

// 在函数作用域的顶层添加即可
function foo() {
    "use strict";
}

开启后常见的语法限制:

  • 无法意外的创建全局变量 。
  • 严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常 。
  • 严格模式下试图删除不可删除的属性 。
  • 严格模式不允许函数参数有相同的名称 。
  • 不允许0的八进制语法 。
  • 在严格模式下,不允许使用with
  • 在严格模式下,eval不再为上层引用变量 。
  • 严格模式下,this绑定不会默认转成对象。

举几个例子:

禁止意外创建变量

message = "Hello World";
console.log(message);

function foo() {
  age = 20;
}

foo();
console.log(age);

开启前:

image-20220330223435355

正常打印。

开启后:

image-20220330223455840

不允许函数有相同的参数名

function foo(x, y, x) {
  console.log(x, y, x);
}

foo(10, 20, 30);

开启前:

image-20220330223715204

开启后:

image-20220330223730351

eval不再为上层引用变量

var jsString = ' var message = "Hello World"; console.log(message);';
eval(jsString);

console.log(message);

开启前:

image-20220330223957738

开启后:

image-20220330224009954

7.js面向对象

JavaScript支持多种编程范式,这里也支持面向对象编程。

对象在js中被设计为一组无需的属性集合,像是一个哈希表,由keyvalue组成。
key是一个标识符名称,value可以是任意类型,也可以是其他对象或者函数类型,当然如果值是一个函数,那么可以称之为对象的方法。

创建对象

构造函数方式

const obj = new Object();

obj.name = "why";
obj.age = 18;
obj.height = "1.88";
obj.running = function () {
  console.log(this.name);
};

字面量方式

const info = {
  name: "zhangsan",
  age: "18",
  height: 0.198,
};

工厂模式

上述方法在创建单个对象的时候可以使用,但是如果我们要批量创建对象的话,上面的两种方式就显得不太方便,我们要为每个对象进行属性的赋值,所以可以通过工厂模式批量进行对象的创建。

function createPerson(name, age) {
  // 创建一个对象
  const p = {};
  p.name = name;
  p.age = age;
}

const p1 = createPerson("zhangsan", 18);
const p2 = createPerson("lisi", 20);

构造函数方法

我们通过构造函数的方式能够创建多个对象,但是这有个问题就是,我们创建出来的对象打印出来的类型都为Object。但实际上我们要求创建的对象要有特定的类型,这里就要提到构造函数。

构造函数被称之为构造器,一般在创建函数就会调用,在js种构造函数就是一个普通的函数,但是根据其调用方式,有着不同的区别,如果该函数以new操作符进行调用,那该函数就称之为构造函数。

function Person() {
    
}
// 普通调用
Person()
// 通过new 方式进行调用
new Person()

首先说下new操作符进行了那些步骤:

  • 在内存中创建一个新的对象(空对象)。
  • 将创建出来的该对象的的[[prototype]]属性赋值为构造函数的[[prototype]]属性。
  • 构造函数内部的this,会指向创建出来的新对象。
  • 执行构造函数内部代码。
  • 如果构造函数没有返回非空对象,就返回创建的新对象,否则就返回构造函数的返回值。
function Fish(name) {
  this.name = name;
}
console.log(new Fish("wangwu")); // Fish { name: 'wangwu' }

相比之前工厂方法创建的对象类型都为Object,该方式可以创建特定类型的对象。

但是该方法还是有有个缺点就是如果定义了方法在构造函数中,那么每次调用构造函数都会重新的取定义该方法。最好的方法是将该方法定义在构造函数的原型中。

对象属性的相关操作

之前我们都是将属性直接定义在对象中,我们在添加属性的时候可以对属性进行一些限制,这是就需要属性描述符来进行属性的设置了:

Object.defineProperty

该方法会在对象上定义一个属性,并对这个属性进行配置,或者对对象某个现有属性进行配置,并返回该对象。

该方法有三个参数:

  • 要定义属性的方法。
  • 要修改的属性名。
  • 一个对象,包含对该属性的一些设置。

属性描述符分为两类,一种是数据属性,一种是存取属性。

数据属性描述符

数据属性描述符有以下四个类型:

  • [[Configurable]]:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符。默认我们定义的属性该值为true,当我们通过属性描述符来定义属性时该值为false
  • [[Enumerable]]:表示属性是否可以通过for-in或者Object.keys()返回该属性,默认我们定义属性该值为true,表示可枚举的,我们通过控制台打印也可见。当我们通过属性描述符来定义就是false
  • [[Writable]]:表示是否可以修改该属性的值,其默认值也是和上述两个属性描述符一致。
  • [[value]]:表示该属性的值,默认为undefined
const obj = {
  name: "why",
  age: 18,
};

Object.defineProperty(obj, "address", {
  value: "成都市",
  configurable: false,
  enumerable: false,
  writable: true,
});
// 设置了configurable为false后再通过该方法进行设置属性部分的配置不能重新设置了,否则就会报错
Object.defineProperty(obj, "address", {
  value: "成都市",
  configurable: true,// 这里改为true了就会报错
  enumerable: false,
  writable: true,
});

console.log(obj);
console.log(obj.address);
obj.address = "neijiang";
console.log(obj.address);

存取属性描述符

也有四个类型:

前两个和之前数据属性描述符一致,后两种不一样了,是以下两种方法:

  • [[get]]:当我们获取属性的时候会调用的函数,默认为undefined
  • [[set]]:当我们对属性进行重新赋值的时候会调用。默认为undefined

注意,存取属性描述符和数据描述符不一样的两个属性是不能共存的,即get或者set方法不能和valuewriteable共存。

const obj = {
  name: "why",
  age: 18,
};

Object.defineProperty(obj, "address", {
  configurable: false,
  enumerable: true,
  get() {
    return this._address;
  },
  set(newValue) {
    this._address = newValue;
  },
});

console.log(obj);
console.log(obj.address);
obj.address = "neijiang";
console.log(obj.address);

同时定义多个属性

Object.defineProperties可以同时定义多个属性

// 下面两种私有属性(_age)的定义和获取都是一样的
const obj = {
  _age: 18,
  set age(value) {
    this._age = value;
  },
  get age() {
    return this._age;
  },
};

const obj2 = {};
Object.defineProperties(obj2, {
  name: {
    configuarble: true,
    enumerable: true,
    writable: true,
    value: "heng",
  },
  age: {
    configurable: true,
    enumerable: true,
    set(value) {
      this._age = value;
    },
    get() {
      return this._age;
    },
  },
});

obj2.age = 11000;
console.log(obj.age);
console.log(obj2.age);

原型

JavaScript中每个对象都有个内置属性 [[prototype]],该对象指向另外一个对象。

当我们通过一个key来获取属性value的时候,我们首先会在当前对象上去查找,但如果没有找到就会去访问属性 [[prototype]]所指向的对象,再继续查询该key

获取原型对象

我们通过对象字面量或者Object方式创建的对象都有一个属性__proto__(该属性是早期浏览器自己添加的,存在一定的兼容性问题)。

还可以通过Object.getPrototypeOf方法获取。

函数的原型

之前说了所有的对象都有一个[[prototype]]属性指向原型对象。在js中函数实际上也是对象,每个函数都可通过prototype的属性来获取属性。

之前我们说过每个对象都有个[[prototype]]属性指向原型。而函数比较特殊,有个叫prototype的属性指向原型。所以该属性并不是函数也被称之为对象而拥有,而是正是作为函数才拥有。

constructor

原型对象上都有一个叫做constructor的属性,该属性指向当前函数对象。

function Person(){
    
}
function Person() {}

console.log(Person.prototype);// {}
console.log(Person.prototype.constructor);// [Function: Person]

重写原型对象

如果我们要在原型上添加很多的属性,可以直接重写整个原型对象

function Foo(name) {
  this.name = name;
}
const f1 = new Foo("lisi");
//如果修改了原型,那已经创建出来的实例原型仍然是之前的原型,并不会应用修改后的原型
Foo.prototype = {
  name: "why",
  age: 18,
};

const f2 = new Foo("lisi");
console.log(f1.name);
console.log(f1.__proto__);
console.log(f1.age);
console.log(f2.age);
function Foo(name) {
  this.name = name;
}

const f1 = new Foo("lisi");

//如果修改了原型,那已经创建出来的实例原型仍然是之前的原型,并不会应用修改后的原型
Foo.prototype = {
  name: "why",
  age: 18,
};

const f2 = new Foo("lisi");

console.log(f1.name);
console.log(f1.__proto__);
console.log(f1.age);
console.log(f2.age);
console.log(f1.__proto__.constructor);
console.log(f2.__proto__.constructor);
[Function: Foo]
[Function: Object]
//lisi
//{}
//undefined
//18

上述修改了原型后,也会导致修改后的原型constructor指向错误,所以我们可以修改该属性,推荐使用Object.defineProperty方法修改该属性,并设置为不可枚举。

之前说到的构造函数的缺点,我们可以将方法定义在构造函数的原型中了,这样每次创建对象就不用重复的定义方法了。这些方法同样可以给所有创建的对象实例所使用。

function Person(){
    
}
Person.prototype.runing = function(){
    console.log('xxx')
}

继承

面向对象有三大特性:封装,继承,多态。

JavaScript可以通过原型链来实现继承,这里先了解下原型链的含义。

首先,从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取。当然Object的原型就没有原型了,指向为null,这也被称之为顶层原型了。

通过Object创建的对象原型都是 [Object: null prototype] {},该原型上有很多方法,该原型的原型指向null

如果一个对象的原型是另外一个对象的实例,那么就可以通过原型进行访问另外一个对象的原型,继而查找也可以一级一级的查找,这样就形成了原型链。

通过原型链方式实现继承

function Person() {
  this.name = "why";
}

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

const stu = new Student();
console.log(stu)
console.log(stu.sno);
console.log(stu.name);
stu.eating();
// Person { sno: 111 }
// 111
// why
// whyeating

但是该方式有一些弊端:

  • 我们通过属性打印,并不能看到原型中的属性,所以看不见name属性。
  • 这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题,比如一个对象实例修改了name,那所有的对象实例获取该name属性都是修改过的。
  • 不能给Person传递参数,所以导致对象不能自定义。
  • 创建的子类类型仍然是父类的类型。

借用构造函数继承

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

Person.prototype.eating = function () {
  console.log(this.name + "eating");
};
function Student(name, age, friends, sno) {
  // 借用构造函数,复用父类的赋值逻辑
  Person.call(this, name, age, friends);
  this.sno = 111;
}
Student.prototype = new Person("why", 18, ["saber"]);

const stu = new Student("lisi", 20, ["altria"], 111);
console.log(stu.__proto__);
console.log(stu);
console.log(stu.sno);
console.log(stu.name);
stu.eating();
// Person { name: undefined, age: undefined, friends: undefined }
// Person { name: 'lisi', age: 20, friends: [ 'altria' ], sno: 111 }
// 111
// lisi
// lisieating

该方式解决了部分原型链的弊端,但是还有一些其他的弊端:

  • Person构造函数被调用了至少两次。
  • stu的原型对象会多出来一些无用属性,这些属性在子类的原型中也有(所有的子类实例事实上会拥有两份可以访问的属性,一份在当前实例,一份在原型)。

父类原型直接赋值给子类原型

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

Person.prototype.eating = function () {
  console.log(this.name + "eating");
};
function Student(name, age, friends, sno) {
  // 借用构造函数,复用父类的赋值逻辑
  Person.call(this, name, age, friends);
  this.sno = sno;
}

function Teacher(name, age, friends, tno) {
  // 借用构造函数,复用父类的赋值逻辑
  Person.call(this, name, age, friends);
  this.tno = tno;
}

Student.prototype = Person.prototype;
Teacher.prototype = Person.prototype;
Student.prototype.doHomework = function () {
  console.log("做作业");
};

const stu = new Student("lisi", 20, ["altria"], 222);
const tea = new Teacher("lisi", 20, ["altria"], 333);
console.log(stu.__proto__);
console.log(stu);
stu.doHomework();
tea.doHomework();
// { eating: [Function (anonymous)], doHomework: [Function (anonymous)] }
// Person { name: 'lisi', age: 20, friends: [ 'altria' ], sno: 222 }
// 做作业
// 做作业

这种方式在遇到对子类元素的原型上加上方法时会导致方法加到父类的原型上,所以导致所有的继承于父类的子类都会有该方法。(上述老师是不应该有doHomework这个方法的)。当然这解决了父类原型上有多余属性的问题,但是该方法从面向对象的角度来说不太合适。作为一个了解吧。

原型式继承

// 原型式继承
const obj = {
  name: "zhangsan",
  age: 18,
};

// 三种实现方法,要求创建一个新对象,其原型为传入的对象
// 实现方式1
function createObj(o) {
  const newObj = {};
  // 设置原型
  Object.setPrototypeOf(newObj, o);
  return newObj;
}
// 实现方式2
function createObj2(o) {
  function Foo() {}
  Foo.prototype = o;
  return new Foo();
}
// 实现方式3 Object自带方法
//Object.create(o);

//const info = createObj(obj);
//const info = createObj2(obj);
const info = Object.create(obj);
console.log(info);
console.log(info.__proto__);
// {}
// { name: 'zhangsan', age: 18 }

该方法也有一个弊端就是如果我要为子类添加一个属性或者其他方法,那该方法并不能对所有的实例共享。需要一个一个添加。

const obj = {
  name: "zhangsan",
  age: 18,
};
const info = Object.create(obj);
//为所有的实例添加一个name属性
info.name = xxx;
info2.name = xxx
// ....

寄生式继承

const obj = {
  name: "zhangsan",
  age: 18,
};

// 寄生说白了就是为了解决原型式继承的缺点,这里定义一个函数将之前添加的逻辑抽离出来
function studentObject(obj, name) {
  const stu = Object.create(obj);
  stu.name = name;
  return stu;
}
const info = studentObject(obj, "张三");
console.log(info);
console.log(info.__proto__);

这个方式只是定义了一个方法,将上一个原型式继承的弊端给优化了下,不用一个一个给实例添加属性。

寄生组合式继承

上述的一些方法,有的是社区提供的思路,但或多或少都有一些弊端,但是这也给了我们很多思路想法,这里展示一个较为完美的继承方式:

function inheritPrototype(sub, sup) {
  sub.prototype = Object.create(sup.prototype);
  // 增强对象
  Object.defineProperty(sub.prototype, "constructor", {
    enumerable: false,
    configurable: true,
    writable: true,
    value: sub,
  });
}

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 = Object.create(Person.prototype);
// // 增强对象
// Object.defineProperty(Student.prototype, "constructor", {
//   enumerable: false,
//   configurable: true,
//   writable: true,
//   value: Student,
// });
// 封装对上述代码复用的工具函数
inheritPrototype(Student, Person);
Student.prototype.studying = function () {
  console.log(this.sno, "studying");
};

const stu = new Student("saber", 18, [111, 222], 100, 200);
console.log(stu);
stu.studying();
stu.running();
// Student {
//  name: 'saber',
//  age: 18,
//  friends: [ 111, 222 ],
//  sno: 100,
//  score: 200
//  }
// 100 studying
// saber running

对象方法的补充

获取对象的属性描述符:

  • getOwnPropertyDescriptor获取单个
  • getOwnPropertyDescriptors获取所有
var obj = {
  name: "why",
  age: 18,
};
console.log(Object.getOwnPropertyDescriptor(obj, "name"));
console.log(Object.getOwnPropertyDescriptors(obj));

禁止对象继续添加新的属性:

var obj = {
  name: "why",
  age: 18,
};
Object.preventExtensions(obj);
obj.height = 1.88;
obj.address = "广州市";
console.log(obj);

密闭对象,禁止对象配置/删除里面的属性:

Object.seal(obj);// 实际是调用preventExtensions,且将现有属性的configurable:false

冻结对象,不允许修改现有属性(实际上是调用seal,并将现有属性的writable: false)

Object.freeze(obj);
hasOwnProperty

对象是否有某一个属于自己的属性(不是在原型上的属性)

const obj = {
  name: "why",
  age: 18,
};
// 这里的第二个参数是创建对象的初始属性
const info = Object.create(obj, {
  address: {
    value: "北京市",
    enumerable: true,
  },
});
console.log(info.hasOwnProperty("address"));
console.log(info.hasOwnProperty("name"));
// true
// false
in/for in 操作符

判断某个属性是否在某个对象或者对象的原型上

var obj = {
  name: "why",
  age: 18,
};

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

//in 操作符: 不管在当前对象还是原型中返回的都是true
console.log("address" in info);
console.log("name" in info);

for (var key in info) {
  console.log(key);
}
// true
// true
// address
// name
// age
instanceof

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

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右边传入的是可执行的函数

isPrototypeOf

用于检测某个对象,是否出现在某个实例对象的原型链上。

这个和上面的一样,只是传入的是一个对象,左边是参照对象,右边是被检测的对象。

function Person() {}

var p = new Person();
console.log(Person.prototype.isPrototypeOf(p));

var obj = {
  name: "why",
  age: 18,
};

var info = Object.create(obj);
console.log(obj.isPrototypeOf(info));
// true
// true

原型继承关系

以下是一张解释原型继承的关系图:

image-20220403202041201

首先我们之前说的原型分为函数的原型和对象的原型。

函数的原型都可以通过prototype进行访问,这个原型被称之为显示原型。

对象中的原型一般可以通过 __ptoto__访问,被称之为隐式原型。

但是函数也是对象,所以它也有一个隐式原型。

但是注意,一般的函数这两个原型是不相等的:

function Foo() {
}
console.log(Foo.prototype === Foo.__proto__)
// false

解释:

Foo作为对象:其原型是Function构造函数的原型。

Foo作为函数:其原型就是Foo.prototype = { constructor: Foo }

当然要注意构造函数Function,其显示原型和隐式原型都是一样的。
可以这么理解:

Function = new Function()

ES6的class

了解了js的对象和继承之后可以看到js面向对象其实书写起来并不简洁,而es6就引入了class关键字来直接定义类。

当然class语法只是一个语法糖,遇到低版本浏览器,还是需要babel转译成es5语法才会被正常运行。

基本使用

定义方法:

class Person {
  constructor(name,age){
    this.name = name;
    this.age = age;
  }
  getAge(){
    return this.age;
  }
}
const p = new Person();

这里我们了解babel转换后的代码:

// 如果函数以普通的函数调用就会报错
function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

// 定义属性的一个方法
function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(target, descriptor.key, descriptor);
  }
}

// 
function _createClass(Constructor, protoProps, staticProps) {
   // 方法都定义到原型中 
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  // 静态方法直接定义在类上 
  if (staticProps) _defineProperties(Constructor, staticProps);
  Object.defineProperty(Constructor, "prototype", { writable: false });
  return Constructor;
}

// 这里使用到了魔法注释,起作用是标记这个函数是一个纯函数
// webpack 识别到这个函数的时候就会进行tree shaking处理,一种优化
// 为什么使用函数包裹,其实还是为了保证内外变量不会互相影响吧
var Person = /*#__PURE__*/ (function () {
  function Person(name, age) {
   // 检查函数是否是以类调用,而不是函数调用
    _classCallCheck(this, Person);

    this.name = name;
    this.age = age;
  }
  
    // 这里创建一个函数进行构建是为了复用性
  _createClass(Person, [
    {
      key: "getAge",
      value: function getAge() {
        return this.age;
      }
    }
  ]);

  return Person;
})();

继承

继承也是实现了extends方法,本质上还是采用了寄生组合式继承:

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }

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

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

  personMethod() {
    console.log("处理逻辑1")
    console.log("处理逻辑2")
    console.log("处理逻辑3")
  }

  static staticMethod() {
    console.log("PersonStaticMethod")
  }
}

// Student称之为子类(派生类)
class Student extends Person {
  // JS引擎在解析子类的时候就有要求, 如果我们有实现继承
  // 那么子类的构造方法中, 在使用this之前调用super
  constructor(name, age, sno) {
    super(name, age)
    this.sno = sno
  }

  studying() {
    console.log(this.name + " studying~")
  }

  // 类对父类的方法的重写
  running() {
    console.log("student " + this.name + " running")
  }

  // 重写personMethod方法
  personMethod() {
    // 复用父类中的处理逻辑
    super.personMethod()

    console.log("处理逻辑4")
    console.log("处理逻辑5")
    console.log("处理逻辑6")
  }

  // 重写静态方法
  static staticMethod() {
    super.staticMethod()
    console.log("StudentStaticMethod")
  }
}

babel转换后的代码:

// babel自定义了一个方法来获取变量的类型
function _typeof(obj) {
  "@babel/helpers - typeof";
  return (
    (_typeof =
      "function" == typeof Symbol && "symbol" == typeof Symbol.iterator
        ? function (obj) {
            return typeof obj;
          }
        : function (obj) {
            return obj &&
              "function" == typeof Symbol &&
              obj.constructor === Symbol &&
              obj !== Symbol.prototype
              ? "symbol"
              : typeof obj;
          }),
    _typeof(obj)
  );
}

function _get() {
  if (typeof Reflect !== "undefined" && Reflect.get) {
    _get = Reflect.get;
  } else {
    _get = function _get(target, property, receiver) {
      var base = _superPropBase(target, property);
      if (!base) return;
      var desc = Object.getOwnPropertyDescriptor(base, property);
      if (desc.get) {
        return desc.get.call(arguments.length < 3 ? target : receiver);
      }
      return desc.value;
    };
  }
  return _get.apply(this, arguments);
}

function _superPropBase(object, property) {
  while (!Object.prototype.hasOwnProperty.call(object, property)) {
    object = _getPrototypeOf(object);
    if (object === null) break;
  }
  return object;
}

function _inherits(subClass, superClass) {
   // 边界判断 
  if (typeof superClass !== "function" && superClass !== null) {
    throw new TypeError("Super expression must either be null or a function");
  }
   //
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { value: subClass, writable: true, configurable: true }
  });
  Object.defineProperty(subClass, "prototype", { writable: false });
   // 为了静态类型的继承,将子类的隐式原型设置为父类
  if (superClass) _setPrototypeOf(subClass, superClass);
}

function _setPrototypeOf(o, p) {
    // 对函数的一个重写,检测是否有 Object.setPrototypeOf 这个方法
  _setPrototypeOf =
    Object.setPrototypeOf ||
    function _setPrototypeOf(o, p) {
      o.__proto__ = p;
      return o;
    };
  return _setPrototypeOf(o, p);
}

function _createSuper(Derived) {
  var hasNativeReflectConstruct = _isNativeReflectConstruct();
  return function _createSuperInternal() {
    var Super = _getPrototypeOf(Derived),
      result;
    if (hasNativeReflectConstruct) {
      var NewTarget = _getPrototypeOf(this).constructor;
      result = Reflect.construct(Super, arguments, NewTarget);
    } else {
      result = Super.apply(this, arguments);
    }
    return _possibleConstructorReturn(this, result);
  };
}

function _possibleConstructorReturn(self, call) {
  if (call && (_typeof(call) === "object" || typeof call === "function")) {
    return call;
  } else if (call !== void 0) {
    throw new TypeError(
      "Derived constructors may only return object or undefined"
    );
  }
  return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {
    // 这里的void 0 是为了获取准确的undefined类型,因为undefined类型可能会被重新写
  if (self === void 0) {
    throw new ReferenceError(
      "this hasn't been initialised - super() hasn't been called"
    );
  }
  return self;
}

function _isNativeReflectConstruct() {
  if (typeof Reflect === "undefined" || !Reflect.construct) return false;
  if (Reflect.construct.sham) return false;
  if (typeof Proxy === "function") return true;
  try {
    Boolean.prototype.valueOf.call(
      Reflect.construct(Boolean, [], function () {})
    );
    return true;
  } catch (e) {
    return false;
  }
}

function _getPrototypeOf(o) {
  _getPrototypeOf = Object.setPrototypeOf
    ? Object.getPrototypeOf
    : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o);
      };
  return _getPrototypeOf(o);
}

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(target, descriptor.key, descriptor);
  }
}

function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  Object.defineProperty(Constructor, "prototype", { writable: false });
  return Constructor;
}

var Person = /*#__PURE__*/ (function () {
  function Person(name, age) {
    _classCallCheck(this, Person);

    this.name = name;
    this.age = age;
  }

  _createClass(
    Person,
    [
      {
        key: "running",
        value: function running() {
          console.log(this.name + " running~");
        }
      },
      {
        key: "eating",
        value: function eating() {
          console.log(this.name + " eating~");
        }
      },
      {
        key: "personMethod",
        value: function personMethod() {
          console.log("处理逻辑1");
          console.log("处理逻辑2");
          console.log("处理逻辑3");
        }
      }
    ],
    [
      {
        key: "staticMethod",
        value: function staticMethod() {
          console.log("PersonStaticMethod");
        }
      }
    ]
  );

  return Person;
})(); // Student称之为子类(派生类)

var Student = /*#__PURE__*/ (function (_Person) {
    // 实现寄生组合继承
  _inherits(Student, _Person);
 
  var _super = _createSuper(Student);

  // JS引擎在解析子类的时候就有要求, 如果我们有实现继承
  // 那么子类的构造方法中, 在使用this之前
  function Student(name, age, sno) {
    var _this;

    _classCallCheck(this, Student);
     // 这里思考为什么不直接通过Person.call(this,name,age)?
    // 因为这里做了限制不能将Person作为普通函数调用  
    _this = _super.call(this, name, age);
    _this.sno = sno;
    return _this;
  }

  _createClass(
    Student,
    [
      {
        key: "studying",
        value: function studying() {
          console.log(this.name + " studying~");
        } // 类对父类的方法的重写
      },
      {
        key: "running",
        value: function running() {
          console.log("student " + this.name + " running");
        } // 重写personMethod方法
      },
      {
        key: "personMethod",
        value: function personMethod() {
          // 复用父类中的处理逻辑
          _get(_getPrototypeOf(Student.prototype), "personMethod", this).call(
            this
          );

          console.log("处理逻辑4");
          console.log("处理逻辑5");
          console.log("处理逻辑6");
        } // 重写静态方法
      }
    ],
    [
      {
        key: "staticMethod",
        value: function staticMethod() {
          _get(_getPrototypeOf(Student), "staticMethod", this).call(this);

          console.log("StudentStaticMethod");
        }
      }
    ]
  );

  return Student;
})(Person);

创建类继承自内置类

我们默认创建的类其实是继承了内置的Object

class Person {}

完整的写法为:

class Person extends Object {}

另外我们还可以通过继承对内置的类进行扩展

class MyArr extends Array {
  // 自行扩展方法
}

类的混入

JavaScript中类只支持单继承,但是我们要实现多继承的话只能自己模拟:

// js中只支持单继承,即只支持一个父类

class Person {
  teach() {
    console.log("教书");
  }
}

class Runner {
  running() {
    console.log("润");
  }
}

class Eating {
  eating() {
    console.log("吃饭");
  }
}

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

function mininEater(BaseClass) {
  class NewClass extends BaseClass {
    eating() {
      console.log("吃饭");
    }
  }
  return NewClass;
}

//class Student extends Person {}

//const NewStudent = mininEater(mininRunner(Student));
//const ns = new NewStudent();

//ns.running();
//ns.eating();

//可定义一个辅助函数进行嵌套的混入
function mix(BaseClass, ...Mixins) {
  return Mixins.reduce(
    (accumulator, current) => current(accumulator),
    BaseClass
  );
}

class Student extends mix(Person, mininEater, mininRunner) {}

const ns = new Student();
ns.running();
ns.eating();
ns.teach();

多态

传统的面向对现象的多态需要满足三个前提:

  1. 必须要有继承。
  2. 子类需要重写父类方法。
  3. 必须有父类引用指向子类对象。

JavaScript中的多态定义相对要灵活些: 当对不同的数据类型执行同一个操作时, 如果表现出来的行为(形态)不一样, 那么就是多态的体现。

传统的多态:

class Shape {
  getArea() {}
}

class Rectangle extends Shape {
  getArea() {
    return 100
  }
}

class Circle extends Shape {
  getArea() {
    return 200
  }
}

var r = new Rectangle()
var c = new Circle()

// 多态: 当对不同的数据类型执行同一个操作时, 如果表现出来的行为(形态)不一样, 那么就是多态的体现.
function calcArea(shape: Shape) {
  console.log(shape.getArea())
}

calcArea(r)
calcArea(c)

JavaScript的多态:

function calcArea(foo) {
  console.log(foo.getArea())
}

var obj1 = {
  name: "why",
  getArea: function() {
    return 1000
  }
}

class Person {
  getArea() {
    return 100
  }
}

var p = new Person()

calcArea(obj1)
calcArea(p)


// 以下参数类型不一样也被称之为多态的体现
function sum(m, n) {
  return m + n
}

sum(20, 30)
sum("abc", "cba")

8.ES6/7/8/9/10/11/12...基本语法

字面量的增强

字面量的增加包括:

  • 属性的简写。
  • 方法的简写。
  • 计算属性的简写。
// 属性的简写
const name = 'zhangsan';
const age = 18

const obj = {
    name,
    age,
    //方法的简写
    age(){
      // todo
    },
    // 计算属性名
    [name+age]:'组合的计算属性'
}

解构

结构分为数组的解构和对象的解构。

数组解构

数组结构是有顺序区分的

const names = ['zhangsan','lisi', 'saber'];

// 全部解构
const [name1, name2, name3] = names;
console.log(name1, name2, name3);
// zhangsan lisi saber

// 部分解构
const [, , name2] = names;
console.log(name2);
// saber

// 结构部分剩下的放数组
const [name1, ...newNames] = names;
console.log(name1, newNames);
// zhangsan [ 'lisi', 'saber' ]

// 结构默认值
const [name1, name2, name3, name4 = "hahah"] = names;
console.log(name1, name2, name3, name4);

对象解构

对象解构没有顺序区分

const obj = {
  name: "why",
  age: 18,
  height: 1.88,
  person:{
      name:'zhangsan'
  }  
}
const { name, age, height } = obj
console.log(name, age, height)
// why 18 1.88

// 默认值
const { name, age, height,friends='9' } = obj
console.log(name, age, height,friends)
// why 18 1.88 9

// 连续解构
const { name, age, height,person:{name} } = obj
const {
  person: { name },
} = obj;
console.log(name);
// zhangsan


// 解构后重写命名
const { name: newName } = obj;
console.log(newName);
// why

let/const

  • let/const声明的变量不能重新声明。
  • const声明的变量不允许修改。

关于let/const与作用域的关系,,我们如果在声明前了使用let/const所定义的变量,会报错。这与var有着很大的区别。

详细的描述可以查看 ECMAScript 2015 Language Specification – ECMA-262 6th Edition

image-20220416175630854

其中写道:使用let/const声明的变量实在其包含的词法环境被实例化时创建,但是在这些变量的词法绑定被声明前,它们不能以任何方式被访问。

当然对于这样的处理是否是一种作用域提升,官方并没有实际的说明。

另外注意,let/const创建变量是不会被放到window中的。

最新的ECMA标准中对于上下文的描述如下:

我们将变量对象称之为变量环境(VE),里面的属性和函数声明称之为环境记录。每一个执行上下文会被关联到一个变量环境。

但是对于这个变量环境对象是否是window或者其他对象就需要看JS引擎的实现了,比如v8引擎是通过过VariableMap的一个hashmap来实现它们的存储的。

window对象是早期的GO对象,在最新的实现中其实是浏览器添加的全局对象,并且 一直保持了windowvar之间值的相等性。

块级作用域

es5中只有两个块级作用域:函数作用域和全局作用域。

{
  var name = "zhangsan";
}
console.log(name);
// zhangsan

在ES6中新增了块级作用域,并且通过let、const、function、class声明的标识符是具备块级作用域的限制的。

另外注意函数虽然拥有块级作用域,但是一个在块级作用域的函数声明仍然可以在外部被访问(不同的浏览器有不同实现的)。

{
  var name = "zhangsan";
  function foo() {
    console.log("12345");
  }
}
foo();
// 12345

总之,现在的开发不推荐使用var声明变量,在使用let/const的时候也是首先选择const,如果确定变量之后会被改变那就该使用为let

字符串模板

模板字符串

es6之前我们对于对象和字符串的拼接是十分麻烦的,而且遇到换行还更加不好拼接。

const firstName = "name1";
const lastName = "name2";

const name = "myname" + firstName + lastName;
// ${}中放置变量即可,而且对于换行也是直接换行即可
const message = `my name is ${name}, 
age is ${age}, height is ${height}`

标签模块字符串

function foo(m, n, o) {
  console.log(m, n, o);
}
// 第一个参数是完整的字符串,只是被其中的变量切换成多块
foo`hello${121} world${213}`;
// [ 'hello', ' world', '' ] 121 213

第一个参数是完整的字符串(被变量所分割为数组),剩余的参数就是模板字符串中的变量了。这个语法在React的一个css in js(styled-components)库中有被应用。

函数的默认参数

对应的参数传入了就使用传入的参数,否则就使用默认

另外默认值会改变函数的length的个数,默认值以及后面的参数都不计算在length之内了

// 1.ES6可以给函数参数提供默认值
function foo(m = "aaa", n = "bbb") {
  console.log(m, n);
}

// foo()
foo(0, "");

// 2.对象参数和默认值以及解构
function printInfo({ name, age } = { name: "why", age: 18 }) {
  console.log(name, age);
}

printInfo({ name: "kobe", age: 40 });

// 另外一种写法
function printInfo1({ name = "why", age = 18 } = {}) {
  console.log(name, age);
}

printInfo1();

// 3.有默认值的形参最好放到最后(在ts中是严格要求的)
function bar(x, y, z = 30) {
  console.log(x, y, z);
}

// bar(10, 20)
bar(undefined, 10, 20);

// 4.有默认值的函数的length属性
function baz(x, y, z, m, n = 30) {
  console.log(x, y, z, m, n);
}

console.log(baz.length);
// 0 
// kobe 40
// why 18
// undefined 10 20
// 4

函数的剩余参数

这其实是一种代替arguments的方式,注意剩余参数的表达式要放在函数参数的最后。

function foo(name, ...args) {
  console.log(name, args);
}

foo("zhangsan", "saber", "altria", "altoria");
// zhangsan [ 'saber', 'altria', 'altoria' ]

展开语法

以下场景使用展开语法

  • 函数参数中
  • 构建对象字面量
  • 数组构造时
// 函数的参数
function foo(name, ...args) {
  console.log(name, args);
}

const names = ["zhangsan", "saber", "altria", "altoria"];
foo(...names);
// zhangsan [ 'saber', 'altria', 'altoria' ]




// 构建对象
const obj1 = {
  name: "zhangsan",
  age: 18,
};

const obj2 = {
  ...obj1,
  friends: "999",
};

console.log(obj2);
// { name: 'zhangsan', age: 18, friends: '999' }


// 构建数组
const arr1 = [1, 2, 3];
const arr2 = [3, 4, 5];
const arr3 = [...arr2, ...arr1];
console.log(arr3);
// [ 3, 4, 5, 1, 2, 3 ]

注意构建对象的时候,如果之后添加的属性在之前的展开对象中那是会覆盖掉之前的属性。

而且,注意展开运算符其实是一种浅拷贝。

数值的表示

es6之后的版本都对数值进行了规范。

const num1 = 100 // 十进制

// b -> binary
const num2 = 0b100 // 二进制
// o -> octonary
const num3 = 0o100 // 八进制
// x -> hexadecimal
const num4 = 0x100 // 十六进制

console.log(num1, num2, num3, num4)

// 大的数值的连接符(ES2021 ES12)
const num = 10_000_000_000_000_000
console.log(num)

Symbol

以往的对象中的属性名其实一直是字符串,而我们向一个对象中添加属性可能会导致覆盖已有的属性,特别时我们使用第三方库的对象或者在开发中使用别人定义的对象。

我们使用Symbol来定义属性名就能有效的避免这个问题了

基本使用

// Symbol本质上是一个函数,其创建出来的变量是唯一的
const s1 = Symbol()
const s2 = Symbol()

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

添加描述

我们可以看到上述两个Symbol类型的变量打印结果都是一样的,所以为了区分,我们可以在创建的时候添加一个参数作为描述来对变量进行标识。

const s3 = Symbol("aaa");
// 打印描述符
console.log(s3.description);
console.log(s3);
// aaa
// Symbol(aaa)

作为对象的key

const s1 = Symbol()
const s2 = Symbol()
const s3 = Symbol("aaa");
const obj = {
  [s1]: "abc",
  [s2]: "cba",
};

// 新增属性
obj[s3] = "nba";
// 注意以Symbol作为key的属性是不能使用对象.语法获取的
// Symbol作为key的属性不能遍历出来
// 可以通过Object.getOwnPropertySymbols(obj);获取Symbol的key数组,进行遍历获取属性值

创建一样的Symbol

key相同的情况是相同的

const a = Symbol.for("aaa");
const b = Symbol.for("aaa");
console.log(a === b);
// true


// 获取对应的key
const key = Symbol.keyFor(a);
console.log(key);
// aaa

Set

在ES6中新增了两种数据结构,一种是set一种是map

set可以用来保存数据类似于数组,但是set中的数据不能有重复的存在。

创建set

Set是一个构造函数:

const set1 = new Set();

常见方法

const set1 = new Set();

// 属性 返回set中元素个数
console.log(set1.size);

// 添加一个元素, 返回set对象本身
set1.add(1);

//删除一个元素, 返回boolean类型
set1.delete(1)

// 判断是否存在某个元素, 返回boolean类型
set1.has(1)

// 清空set中所有元素,无返回值
set1.clear();

// 支持forEach和for of遍历

适用场景

set中不能存在重复的元素,我们可以利用这一特性为数组进行去重。

// 可以向构造函数传入一个可迭代对象
const set1 = new Set([1,1,2,3,4,5,5,6]);
console.log(set1)
// Set(6) { 1, 2, 3, 4, 5, 6 }

WeakSet

该数据类型其实大部分与Set一样,有以下区别:

  • WeakSet中只能存放对象类型,不能存放基本类型。
  • WeakSet中对于对象的引用是弱引用,如果没有其他引用对该数据类型中的某个对象进行引用,那么该对象就会被垃圾回收掉。
  • 不能被遍历,存储到WeakSet中的元素是不能被获取到的。

常见方法

const weakSet = new WeakSet();

// 添加一个元素,返回weakSet对象本身
weakSet.add(1);

// 删除一个元素,返回boolean
weakSet.delete(2)

// 判断是否存在某个元素,返回boolean
weakSet.has()

适用场景

对于该数据类型有个概念叫弱引用,我们先讲解下弱引用是什么回事。

我们通过字面量创建一个对象,如下:

const obj = {};

obj这个标识符就保存着这个对象的引用地址,并且我们称这种引用为强引用,垃圾回收机制在执行回收的时候检查到有obj引用着,就不会对该对象进行回收。

而我们将该对象放入WeakSet中:

const obj = {};
const weakSet = new WeakSet();
weakSet.add(obj);

此时WeakSet内部就有一项对该对象形成引用关系,但是该引用关系是弱引用。如果我们将标识符obj置为null,即切断obj对对象的引用,我们可能会以为WeakSet中还有对该对象的引用,所以垃圾回收是不会回收该对象的,但是由于垃圾回收是不管对象弱引用关系的,只会检查该对象和obj的强引用关系被切断了,所以照样进行回收。

打个比方就是老板和你是好朋友,老板的宝马也会经常让你开,但是有一天老板因为偷税进去了,还没收了宝马车,不仅老板开不了了,你当然也开不了了。你和这个车的关系就像弱引用一样。

当然接下来展示一个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);
  }
}

let p = new Person();

p.running();
p.running.call({ name: "why" });
p = null;

Map

Map用于存储key--value的映射关系,普通的对象中我们只能使用字符串或者Symbol类型来作为属性的key。而Map支持任意类型作为key

创建方法
const map = new Map()

常用方法

const map = new Map()
// 返回Map中的元素个数
console.log(map.size)

// 添加一个值
map.set('key', 'value');

// 根据 key 获取 value
map.get('key')

// 判断是否包含某个key 返回boolean
map.has('key');

// 删除某一key
map.delete('key');

//清空全部元素
map.clear();

// 也可以使用forEach或者for of进行遍历

WeakMap

也是和WeakSet类似,和Map有以下区别:

  • key只能使用对象。
  • key对于对象是弱引用。
  • 不能遍历。

常用方法

Map要少些方法

  • set
  • get
  • has
  • delete

适用场景

Vue的响应式对于依赖关系的存储

includes

ES7新增的数组方法

以往我们如果需要判断一个数组中是否包含某个元素,需要通过indexOf获取结果。但是使用indexOf有一些缺点:

  • indexOf不能判断含有NaN的情况。
  • indexOf查找不到的时候返回的是数值-1这其实不太符合规范。
  • indexOf不能判断稀疏数组。
// 稀疏数组,存在未定义的数组项(includes认为为undefined)
// 测试环境为node
const a1 = [1, 2, 3, 4, 5, NaN, , 333];
const a2 = [1, 2, 3, 4, 5, NaN, undefined, 333];

console.log(a1.indexOf(undefined));
console.log(a1.includes(undefined));
console.log(a2.indexOf(undefined));
console.log(a2.includes(undefined));
// -1
// true
// 6
// true

includes就能弥补上述的indexOf缺点,另外这两个方法数组和字符串都有,而且还可以指定第二个参数----position从当前数组的哪个索引位置开始搜寻,默认值为 0

指数运算符号

计算数字的乘方可以通过Math.pow来进行,ES7允许我们使用**来计算乘方。

const res1 = Math.pow(3,3);
const res2 = 3 ** 3;

Object.keys

获取对象的所有key

const obj = {
  name: "why",
  age: 18,
};

console.log(Object.keys(obj));
// [ 'name', 'age' ]

Object values

我们可以通过Object.keys获取一个对象的所有key,在ES8中我们可以通过Object.values获取所有的value值。

const obj = {
  name: "why",
  age: 18,
};

console.log(Object.keys(obj));
console.log(Object.values(obj));

//特殊用法
console.log(Object.values(["abc", "cba", "nba"]));
//将字符串转为数组
console.log(Object.values("abc"));

// [ 'name', 'age' ]
// [ 'why', 18 ]
// [ 'abc', 'cba', 'nba' ]
// [ 'a', 'b', 'c' ]

Object entries

获取对象的key,value,组成一个数组然后放入一个数组中

const obj = {
  name: "why",
  age: 18
}

console.log(Object.entries(obj))
const objEntries = Object.entries(obj)
objEntries.forEach(item => {
  console.log(item[0], item[1])
})

console.log(Object.entries(["abc", "cba", "nba"]))
console.log(Object.entries("abc"))
// [ [ 'name', 'why' ], [ 'age', 18 ] ]
// name why
// age 18
// [ [ '0', 'abc' ], [ '1', 'cba' ], [ '2', 'nba' ] ]
// [ [ '0', 'a' ], [ '1', 'b' ], [ '2', 'c' ] ]

String Padding

对一个字符串进行填充操作

padStart/padEnd

两个方法只是填充的方法不一样,函数的第一个参数是当前字符串要填充到的长度,如果小于或者等于就返回当前字符串。

第二个参数是填充字符串,如果填充的字符串超过了目标长度就会截取字符串,优先保留左边的以满足目标长度。

const message = "Hello World";

const newMessage = message.padStart(15, "*").padEnd(20, "-");
console.log(newMessage);

// 案例,隐藏身份证号
const cardNumber = "321324234242342342341312";
const lastFourCard = cardNumber.slice(-4);
const finalCard = lastFourCard.padStart(cardNumber.length, "*");
console.log(finalCard);
// ****Hello World-----
// ********************1312

Trailing Commas

允许我们在定义函数参数或者传参的时候在尾部添加一个逗号

function foo(a,b,) {
    
}
foo(1,2,)

这个只是为了满足一些程序员的习惯吧,当然我的编辑器默认格式化就会去掉尾部的逗号,另外我在定义对象属性的时候习惯在最后一个属性值后边加上一个逗号。

flat

该方法可以将一个数组按照指定的深度递归铺平,就是将一个数组降维,参数是降维的维度数,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

const arr1 = [0, 1, 2, [3, 4]];

console.log(arr1.flat());
// expected output: [0, 1, 2, 3, 4]

const arr2 = [0, 1, 2, [[[3, 4]]]];

console.log(arr2.flat(2));
// expected output: [0, 1, 2, [3, 4]]

faltMap

该方法可以看成组合方法,对一个对象先map然后再进行flat,最后返回一个新数组。

const messages = ["Hello World", "hello lyh", "my name is coderwhy"]
const words = messages.flatMap(item => {
  return item.split(" ")
})

console.log(words)

Object fromEntries

该方法是将一个entries数组转为普通的对象。

const queryString = "name=why&age=18&height=1.88";
const queryParams = new URLSearchParams(queryString);
console.log(queryParams);
for (const param of queryParams) {
  console.log(param);
}

const paramObj = Object.fromEntries(queryParams);
console.log(paramObj);
// URLSearchParams { 'name' => 'why', 'age' => '18', 'height' => '1.88' }
// [ 'name', 'why' ]
// [ 'age', '18' ]
// [ 'height', '1.88' ]
// { name: 'why', age: '18', height: '1.88' }

trimStart/trimEnd

去除字符串开头或者尾部的空格

const message = "    Hello World    "

console.log(message.trim())
console.log(message.trimStart())
console.log(message.trimEnd())
// Hello World
// Hello World    
//    Hello World

bigInt

BigInt 是一种内置对象,它提供了一种方法来表示大于 2^53 - 1 的整数。这原本是Javascript中可以用 Number 表示的最大数字。BigInt可以表示任意大的整数。

如果我们用普通的number保存大数进行运算会发生意想不到的情况的:

// 最大的安全数字,在这之内的运算结果可以保证精切
const maxInt = Number.MAX_SAFE_INTEGER;
console.log(maxInt); // 9007199254740991
console.log(maxInt + 1);
console.log(maxInt + 2);

// ES11之后: BigInt,在数字后边加上n即可表示该类型
const bigInt = 900719925474099100n;
//大数和普通的number相运算是不会进行隐式转换的
//console.log(bigInt + 10);
console.log(bigInt + 10n);

const num = 100;
console.log(bigInt + BigInt(num));

const smallNum = Number(bigInt);
console.log(smallNum);
// 9007199254740991
// 9007199254740992
// 9007199254740992
// 900719925474099110n
// 900719925474099200n
// 900719925474099100

空值合并类型操作符

以前使用 || 有个问题就是判断条件是转为boolean来进行判断

对于前一个值是空字符串也是直接返回后一个值,而??是前面为null,undefined才会返回后一个值。

const foo = undefined;
// const bar = foo || "default value"
const bar = foo ?? "defualt value";
const bar1 = "" ?? "defualt value";

console.log(bar);
console.log(bar1);
console.log(bar2);
// defualt value
// 

可选链

我们通过对象获取属性操作符.的时候,如果我们从一个null或者undefined获取属性值,这时会报错的。

通过?.来获取属性,如果对象为undefined就不会继续获取了

const info = {
  name: "why",
  // friend: {
  //   girlFriend: {
  //     name: "hmm"
  //   }
  // }
}

console.log(info.friend?.girlFriend?.name)// 报错
console.log(info.friend?.girlFriend?.name)// 并不会

xxx赋值操作符

ES12新增了部分逻辑赋值操作符:

  • 逻辑或赋值 ||=
  • 逻辑与赋值 &&=
  • 逻辑空赋值 ??=
// 逻辑或赋值
let message = 1;
message ||= 'default';
// 相当于
message = message || 'default';    
// 逻辑与赋值
let message = 1;
message &&= 'default';
// 相当于
message = message && 'default';    
// 逻辑空赋值
let message = 1;
message ??= 'default';
// 相当于
message = message ?? 'default';    

Global this

js可以运行在不同的环境上,但是不同环境的全局对象是有所不同的。
比如浏览器里面全局对象window可以通过this获取。

node中需要通过global获取

所以GlobalThis就是为了同一全局对象获取方式。

浏览器:

image-20220423151757139

node中:

console.log(globalThis);
/* 
<ref *1> Object [global] {
  global: [Circular *1],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  }
}
*/

FinalizationRegistry

可以指定注册一个对象,并指定一个回调函数。当该对象被垃圾回收的时候执行回调函数。

注册对象

// 1. 首先定义实例
const finalRegistry = new FinalizationRegistry((value) => {
  console.log("注册在finalRegistry的对象, 某一个被销毁", value)
})
// 2.注册对象
let obj = { name: "why" }
let info = { age: 18 }

finalRegistry.register(obj, "obj")
finalRegistry.register(info, "value")

// 手动回收
obj = null
info = null

在浏览器中执行下这个代码,垃圾回收可能会等待几秒。

WeakRefs

如果我们默认将一个对象赋值给另外一个引用,那么这个引用是一个强引用,但是可以通过WeakRefs来创建弱引用。

实例1:

// WeakRef.prototype.deref:
// > 如果原对象没有销毁, 那么可以获取到原对象
// > 如果原对象已经销毁, 那么获取到的是undefined
const finalRegistry = new FinalizationRegistry((value) => {
  console.log("注册在finalRegistry的对象, 某一个被销毁", value);
});

let obj = { name: "why" };
let info = new WeakRef(obj);

finalRegistry.register(obj, "obj");

obj = null;

setTimeout(() => {
  console.log(info.deref()?.name);
  console.log(info.deref() && info.deref().name);
}, 10000);

实例2:

class Counter {
  constructor(element) {
    // Remember a weak reference to the DOM element
    this.ref = new WeakRef(element);
    this.start();
  }

  start() {
    if (this.timer) {
      return;
    }

    this.count = 0;

    const tick = () => {
      // Get the element from the weak reference, if it still exists
      const element = this.ref.deref();
      if (element) {
        element.textContent = ++this.count;
      } else {
        // The element doesn't exist anymore
        console.log("The element is gone.");
        this.stop();
        this.ref = null;
      }
    };

    tick();
    this.timer = setInterval(tick, 1000);
  }

  stop() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = 0;
    }
  }
}

const counter = new Counter(document.getElementById("counter"));
counter.start();
setTimeout(() => {
  document.getElementById("counter").remove();
}, 5000);

Proxy

ES6新增了一个类Proxy,该类可以创建一个对象的代理对象,我们可以通过代理对象来监听所有对于该对象的操作,也可以通过代理对象来进行对原对象的所有操作。

创建代理对象

const obj = {name:'zhangsan'};
// 参数第一个是要代理的原对象,第二个是相关操作的处理对象
const objProxy = new Proxy(obj, {});

Proxy的捕获器

const obj = {
  name: "why",
  age: 18,
};

const objProxy = new Proxy(obj, {
  //重写捕获器
  get(target, key) {
    console.log("获得了", key);
    return target[key];
  },
  set(target, key, newvalue) {
    if (!(key in target)) {
      console.log("增加了一个属性", key);
      target[key] = newvalue;
    } else {
      console.log("修改了", key);
      target[key] = newvalue;
    }
  },
  // 监听 in 操作
  has(target, key) {
    console.log("监听对象操作");
    return key in target;
  },
});
objProxy.name = "zhangsan";
objProxy.age = 29;
objProxy.newName = "xiaoxin";

console.log("name" in objProxy);

当然Proxy有13个对象操作的捕获器。捕获器如下:

handler 对象的方法

handler 对象是一个容纳一批特定属性的占位符对象。它包含有 Proxy 的各个捕获器(trap)。

所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。

另外注意其中的construtapply。我们可以对于个函数进行创建代理对象,而它调用方式的不同就会触发不同的触发器。

Reflect

ReflectES6新增的内置对象,主要提供了很多操作对象的API,其实大部分类似Object中的API。

其实内置对象主要是让对象API设计的更加规范而不是所有都集中在Object上。

基本使用

// 代替对象本身直接获取或者设置属性
const obj = {
  name: "why",
  age: 18,
};

const objProxy = new Proxy(obj, {
  //重写捕获器
  get(target, key) {
    console.log("获得了", key);
    // return target[key];
    return Reflect.get(target, key);
  },
  set(target, key, newvalue) {
    if (!(key in target)) {
      console.log("增加了一个属性", key);
      //target[key] = newvalue;
      const res = Reflect.set(target, key, newvalue);
      console.log("设置成功了吗", res);
    } else {
      console.log("修改了", key);
      target[key] = newvalue;
    }
  },
  // 监听 in 操作
  has(target, key) {
    console.log("监听对象操作, ");
    return key in target;
  },
});
objProxy.name = "zhangsan";
objProxy.age = 29;
objProxy.newName = "xiaoxin";

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

Receiver的作用

image-20220423161749711

在使用proxygetset拦截方法时,其方法有一个参数叫receiver,接下来讲解下其作用:

// 该参数在拦截其中作为代理对象的实例,我们可以配合reciver使用
const obj = {
  _name: "why",
  age: 18,
  get name() {
    console.log("原对象中的get方法");
    return this._name;
  },
};

const objProxy = new Proxy(obj, {
  //重写捕获器
  get(target, key, reciver) {
    console.log("获得了", key);
    // return target[key];
      // 传入可以修改原对象中get方法里的this(原对象中的get方法里的this是指向自身的)为代理对象,这时候通过代理对象访问就可以触发代理拦截器了。这使得拦截更加的全面
    return Reflect.get(target, key, reciver);
  },
  set(target, key, newvalue) {
    console.log("修改了", key);
    target[key] = newvalue;
  },
});

//objProxy.name = "zhangsan";
console.log(objProxy.name);
// 获得了 name
// 原对象中的get方法
// 获得了 _name
// why

Reflect中的constructor方法

该方法类似于new,和Object.create()有些类似

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

function Teacher() {}

const names = Reflect.construct(Student, ["zhangsanm"], Teacher);
console.log(names);
console.log(names.__proto__ === Teacher.prototype);

MDN详细介绍:Reflect.constructor

Promise

Promise是一个异步解决方案,是一个构造函数,类。

在以往我们使用一些异步请求的时候往往是通过回调函数来接受结果,比如以下代码:

const requestData = (url, successCb, failCb) => {
  setTimeout(() => {
    if (url === "saber") {
      successCb("请求成功", url);
    } else {
      failCb("请求失败");
    }
  }, 3000);
};

requestData(
  "saber",
  (res, url) => {
    console.log(res, url);
  },
  (err) => {
    console.log(err);
  }
);

requestData(
  "saberX",
  (res, url) => {
    console.log(res, url);
  },
  (err) => {
    console.log(err);
  }
);

如果是简单的请求使用这种倒没有什么问题,但是如果遇到多个请求需要等前一个请求结果到达后再发送一个请求,这时候写法上就会出现回调函数的嵌套,这对于后续的维护和可读性是十分不友好的。

所以我们使用Promise可以将上述的代码改写为如下:

const requestData = (url) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (url === "saber") {
        resolve("请求成功");
      } else {
        reject("请求失败");
      }
    }, 3000);
  });
};

requestData("saber").then((res) => {
  console.log(res);
});

requestData("saberX")
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.log(err);
  });

可以看到我们将嵌套写法改为了链式调用。

基本结构

Promise在使用的过程中有三种状态:

  • pending 待定状态,初始状态。
  • fulfilled 操作成功状态,执行了resolve就会处于这个状态。
  • rejected 操作失败状态。执行了reject就会处于这个状态。

Executor

我们在创建Promise的时候需要传递一个回调函数,这个回调函数就是Executor,这个回调函数会被立即执行,并传入两个参数,resolvereject。这两个参数是函数,我们就可以在这个回调函数中调用这些函数来确定当前Promise的状态。

要注意,一旦Promise的状态被确定,即从pending改为其他状态就会被锁定,即不能再次改变状态。总之,Promise只能被改变一次状态。

resolve传入不同值的区别

普通值或者对象

如果resolve传入的是基本类型值,或者一个普通的对象,那这个值就会作为then回调函数的属性。

Promise

如果传入的是Promise,那这个新的Promise的状态就会决定原Promise的状态

thenable类型

thenable类型指的是实现了then方法的对象,如果resolve传入了这样的参数那就会调用这个对象中的then方法,根据then方法的结果来决定Promise的状态。

then方法多次调用

同一个Proimise可以多次调用then方法,这些then方法中的回调函数都会被执行。

const promise = new Promise((resolve, reject) => {
  resolve("hahaha");
});
// 下述代码中的回调函数都会被执行
promise.then((res) => {
  console.log(res);
});
promise.then((res) => {
  console.log(res);
});

promise.then((res) => {
  console.log(res);
});

then 方法的返回值

then 方法本事是有返回值的,其返回值是一个Promise,所以我们就可以进行链式调用。

then中回调函数返回一个普通的函数,Promise对象,或者thenable,这些值对于返回的Promise的状态影响和上述resolve传入不同值的区别一样。

then方法抛出一个异常的时候,那返回的Promise就处于reject状态。

catch方法

catch方法中的回调函数在Promise转为reject后执行,catch依然可以返回一个promise,规判断后续Promise状态规则一样。

promise
  .then((res) => {
    console.log(res);
    return {
      then: function (resolve, reject) {
        reject(222222);
      },
    };
  })
  .then(
    (res) => {
      console.log("res:", res);
    },
    (err) => {
      console.log("err", err);
      return 3333;
    }
  )
  .then((res) => {
    console.log("res", res);
  });

finally

finally是不接受参数的,无论前面的Promise是什么状态最后都会执行。

resolve方法

我们可以直接调用Promise类上的方法:

Promise.resove()
// 语法糖,等价于
new Promise((resolve) => {
    resolve()
})

reject方法

resolve一样。

all方法

all方法用于将多个Promise包裹在一起组成一个新的Promise

新的Promise状态由旧的Promise共同决定:

当所有的Promise都返回resolve的时候,这个新的Promise的状态就是fulfilled,并将所有Promise的返回值构成一个数组(顺序是保持和原来一致)作为resolve的参数。

当有一个Promise返回reject的时候,新的Promisereject状态,并将第一个reject的返回值作为reject参数。

// 创建多个Promise
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(11111);
  }, 1000);
});

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(22222);
  }, 2000);
});

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(33333);
  }, 3000);
});

// 注意如果这个数组中有不是promise的,内部还通过Promise.resolve进行了一层包裹
Promise.all([p2, p1, p3, "aaaa"])
  .then((res) => {
    console.log(res);
  })
  .catch((err) => {
    console.log("err:", err);
  });

allSettled

all方法有一个缺陷就是当有一个Promisereject后,新的Promise就会变为reject,那如果有在之前就已经resolvedPromise,以及依然处于pendingPromise,我们是获取不到对应结果的。

所以allSettled就是在所有的Promise都有结果后(无论fulfilled还是reject),才会有最终结果,且新的Promise一定为fulfilled状态。

race方法

用法和allallSettled一样。当多个Promise中有一个先有结果就返回这个结果作为参数。

any方法

用法和上述的allrace等方法一样。会等待一个fulfilled状态,才会决定新的Promise状态,当所有的Promise都为reject状态的时候就会报一个AggregateError错误

// 创建多个Promise
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(11111);
  }, 1000);
});

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(22222);
  }, 2000);
});

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(33333);
  }, 3000);
});

Promise.any([p2, p1, p3])
  .then((res) => {
    console.log(res);// 222
  })
  .catch((err) => {
    console.log("err:", err);
  });

迭代器

迭代器适用于确实用于在容器对象上遍历访问的对象,用户无需关心容器对象内部的实现细节。

总之迭代器就是用于帮助我们对某个数据结构进行遍历的对象。

JavaScript中,迭代器也是一个具体的对象。其有以下要求:

  • 一个无参或者有一个参数的函数,返回一个拥有两个属性的对

    • done,一个boolean类型的属性,如果迭代器可以产生序列中的下一个值,则为false,如果迭代器已经迭代完毕就返回true
    • value 迭代器返回的任何JavaScript值。donetrue时可以省略。

    一个基本的迭代器:

        function myIterator(items) {
          let i = 0;
          return {
            next: function () {
              if (i < items.length) {
                return {
                  done: false,
                  value: items[i++]
                }
              }
              return {
                done: true,
                value: items[i]
              }
            }
          }
        }
        let iteritor = myIterator([1, 2, 3, 4]);
        console.log(iteritor.next());
        console.log(iteritor.next());
        console.log(iteritor.next());
        console.log(iteritor.next());
        console.log(iteritor.next());

可迭代对象

这个和迭代器对象是不同的概念,可迭代器对象指的是实现了迭代协议的对象,即实现了@@iterator方法,在代码中我们可以使用Symbol.iterator来访问该属性。

当对象变为可迭代对象的时候我们就可以对该对象进行一些迭代操作,比如执行for of语句时就会调用对象中的@@iterator迭代器方法。

JS原生的很多对象都已经实现了可迭代协议,所以就可以用来进行迭代操作。比如:StringArrayMapSetarguments对象、NodeList集合等。

可迭代对象使用场景

  • for of
  • 展开语法
  • 解构语法

...

自定义可迭代对象

还可以设置中断监听函数,只需要在返回的迭代器对象中实现return方法即可

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 classroom = new Classroom("3幢5楼205", "计算机教室", [
  "james",
  "kobe",
  "curry",
  "why",
]);
classroom.entry("lilei");

for (const stu of classroom) {
  console.log(stu);
  if (stu === "why") break;
}

生成器

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

生成器实际上也叫做特殊的迭代器,生成器函数执行会返回一个迭代器,生成器内部可以使用yield来控制执行流程。

定义方式

function* foo() {}

const iterator = foo();
console.log(iterator);
console.log(iterator.next()); // { value: undefined, done: true }

执行流程

当我们执行next方法,函数就会运行到yield前暂停执行,再执行next就会继续,再次遇到yield就会再次暂停,如果遇到return就会终止执行。

function* foo() {
  console.log("函数开始执行~")

  const value1 = 100
  console.log("第一段代码:", value1)
  yield value1

  const value2 = 200
  console.log("第二段代码:", value2)
  yield value2

  const value3 = 300
  console.log("第三段代码:", value3)
  yield value3

  console.log("函数执行结束~")
  return "123"
}

// generator本质上是一个特殊的iterator
const generator = foo()
console.log("返回值1:", generator.next())
console.log("返回值2:", generator.next())
console.log("返回值3:", generator.next())
console.log("返回值3:", generator.next())

生成器传递参数

我们可以给生成器返回的迭代器中的next方法传递参数。

注意我们第一次执行next防范给其传递的参数时会被忽略的。从第二个next开始,传递给next的参数会作为上一个yield的返回值。

function* foo(num) {
  console.log("函数开始执行~");

  const value1 = 100 * num;
  console.log("第一段代码:", value1);
  const n = yield value1;

  const value2 = 200 * n;
  console.log("第二段代码:", value2);
  const count = yield value2;

  const value3 = 300 * count;
  console.log("第三段代码:", value3);
  yield value3;

  console.log("函数执行结束~");
  return "123";
}

// 生成器上的next方法可以传递参数
const generator = foo(5);
console.log(generator.next());
// 第二段代码, 第二次调用next的时候执行的
console.log(generator.next(10));
console.log(generator.next(25));

提前结束return

return传值后生成器结束执行,后续的next不会继续生成值了

function* foo(num) {
  console.log("函数开始执行~");

  const value1 = 100 * num;
  console.log("第一段代码:", value1);
  const n = yield value1;

  const value2 = 200 * n;
  console.log("第二段代码:", value2);
  const count = yield value2;

  const value3 = 300 * count;
  console.log("第三段代码:", value3);
  yield value3;

  console.log("函数执行结束~");
  return "123";
}

const generator = foo(10);

console.log(generator.next());

// 第二段代码的执行, 使用了return
// 那么就意味着相当于在第一段yield代码的后面加上return, 就会提前终端生成器函数代码继续执行
console.log(generator.return(15));
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());
// 函数开始执行~
// 第一段代码: 1000
// { value: 1000, done: false }
// { value: 15, done: true }
// { value: undefined, done: true }
// { value: undefined, done: true }
// { value: undefined, done: true }
// { value: undefined, done: true }
// { value: undefined, done: true }
// { value: undefined, done: true }

throw异常抛出

我们可以调用throw在函数体外抛出异常,在生成器内部捕获异常,但是在catch语句中并不能继续yield,在外部的yield可以继续。

function* foo() {
  console.log("代码开始执行~");

  const value1 = 100;
  let aa;
  try {
    aa = yield value1;
  } catch (error) {
    console.log("捕获到异常情况:", error);
    console.log(aa);
    yield "abc";
  }

  console.log("第二段代码继续执行");
  const value2 = 200;
  yield value2;

  console.log("代码执行结束~");
}

const generator = foo();

const result = generator.next();
console.log(result);
console.log(generator.throw("error message"));
console.log(generator.next());
console.log(generator.next());

生成器替代迭代器

function* createIterator(arr) {
  /* 
 // 写法一
 for (const item of arr) {
    yield item;
  } */
  // 写法二
  yield* arr;
}

const arr = [1, 2, 3, 4, 5];
const iterator = createIterator(arr);
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
// 以下用于生成器替代迭代器的案例

// 创建一个函数 可以迭代10-20内的数字
// 方式1
function createRangerIterator(start, end) {
  let index = start;
  return {
    next: function () {
      if (index < end) {
        return { done: false, value: index++ };
      } else {
        return { done: true, value: undefined };
      }
    },
  };
}

// 方式二
function* createRangerIterator_G(start, end) {
  let index = start;
  while (index < end) {
    yield index++;
  }
}

// 方式三 class方式
class RoomStudent {
  constructor(students) {
    this.students = students;
  }

  *[Symbol.iterator]() {
    yield* this.students;
  }
}

const rangeIterator = createRangerIterator_G(10, 15);
console.log(rangeIterator.next());
console.log(rangeIterator.next());
console.log(rangeIterator.next());
console.log(rangeIterator.next());
console.log(rangeIterator.next());
console.log(rangeIterator.next());

const students = new RoomStudent([1, 2, 3, 4, 5]);
for (student of students) {
  console.log(student);
}

异步代码请求方案

我们可以使用生成器来执行一些异步代码。下面模拟了asyncawait的实现原理。

// request.js
function requestData(url) {
  // 异步请求的代码会被放入到executor中
  return new Promise((resolve, reject) => {
    // 模拟网络请求
    setTimeout(() => {
      // 拿到请求的结果
      console.log(url);
      resolve(url);
    }, 2000);
  });
}

// 需求获得url后再次发送请求,这样会导致回调函数嵌套过多
// 方式1
// requestData("topzhang.cn").then((res) => {
//   console.log(res);
//   requestData(res + "addd").then((res) => {
//     console.log(res);
//   });
// });

// 当然方式二,使用返回的promise进行链式调用
// requestData("topzhang.cn")
//   .then((res) => {
//     console.log(res);
//     return requestData(res + "addd");
//   })
//   .then((res) => {
//     console.log(res);
//   });

// 方式三 生成器+promise实现
function* request_G() {
  const res1 = yield requestData("zhangsan");
  const res2 = yield requestData(res1 + "aaa");
  const res3 = yield requestData(res2 + "bbb");
  const res4 = yield requestData(res3 + "ccc");
  const res5 = yield requestData(res4 + "ddd");
  console.log(res5);
}
exec(request_G);
// const requestIterator = request_G();
// requestIterator.next().value.then((res) => {
//   console.log(res);
//   requestIterator.next(res).value.then((res) => {
//     console.log(res);
//     requestIterator.next(res).value.then((res) => {
//       console.log(res);
//     });
//   });
// });

// 上述调用迭代器的步骤可以通过封装函数实现下

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

// 这样你就知道了await async的原理实现了

async和await

async用于声明一个异步函数。

async foo() {
    
}
const bar = async function() {
    
}
const fa = async () => {
    
}

异步函数的执行流程

异步函数内部代码执行流程和普通函数是一致的,默认情况也是同步执行。

async function foo() {
  console.log("foo function start~")

  console.log("内部的代码执行1")
  console.log("内部的代码执行2")
  console.log("内部的代码执行3")

  console.log("foo function end~")
}


console.log("script start")
foo()
console.log("script end")
script start
// foo function start~
// 内部的代码执行1
// 内部的代码执行2
// 内部的代码执行3
// foo function end~
// script end

异步函数特点

异步函数和普通函数的区别就是异步函数的返回值是一个Promise

async function foo() {
  console.log("foo function start~");

  console.log("中间代码~");

  console.log("foo function end~");

  // 1.返回一个值

  // 2.返回thenable
  return {
    then: function (resolve, reject) {
      reject("hahahah");
    },
  };

  // 3.返回Promise
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("hehehehe");
    }, 2000);
  });
}

// 异步函数的返回值一定是一个Promise
const promise = foo();
promise.then(
  (res) => {
    console.log("promise then function exec:", res);
  },
  (err) => {
    console.log("err", err);
  }
);

异步函数内部如果有异常时,那么程序它并不会像普通函数一样报错,而是会作为Promisereject来传递:

async function foo() {
  console.log("foo function start~")

  console.log("中间代码~")

  // 异步函数中的异常, 会被作为异步函数返回的Promise的reject值的
  throw new Error("error message")

  console.log("foo function end~")
}

// 异步函数的返回值一定是一个Promise
foo().catch(err => {
  console.log("coderwhy err:", err)
})

console.log("后续还有代码~~~~~")

await

await只能在async定义的异步函数中使用,通常await后跟上一个表达式,这个表达式会返回一个Promise,那await会等待后面Promise的状态变化为fulfilled后再继续执行异步函数中剩余的代码。

如果await后面跟的是一个普通的值,那就会直接返回这个值。
如果跟的是一个thenable,那就会调用或者对象中的then方法来决定值。
如果后面跟的是一个Promise,且状态为reject,那将会将这个reject结果作为整个异步函数Promisereject值。

async function foo() {
  const res = await 1;
  console.log(res);
}

console.log("start");
foo();
console.log("end");
// start
// end
// 1
async function foo() {
  const res = await {
    then(resolve, reject) {
      resolve("hahas");
    },
  };
  console.log(res);
}

console.log("start");
foo();
console.log("end");

事件循环

进程和线程

首先介绍下进程和线程:

进程是正在执行程序的一个实例。操作系统负责管理正在运行的进程,并为每个进程分配特定的事件来占用CPU,分配特定的资源。

线程是进程中的单条流向,也可以指操作系统能够运算调度的最小单位。线程也具有进程中的部分属性,所以线程也可以称之为轻量型的进程。比如浏览器是一个进程,每个tab就可以看作一个一个的线程。

事件循环基本概念

我们的JS代码是在一个单独的线程中执行的,如果代码中有非常耗时的操作那当前线程就会被阻塞。所以有些耗时的操作是放置其他的线程的调用栈中来执行,比如定时器和网络请求。JS主线程就会继续执行后续的同步代码。当其他线程中调用栈中的函数开始执行的时候就会将回调函数放入事件队列中,这个事件队列在有回调函数的时候就会通知JS主线程从这个队列中依次取出执行。上述的整个过程就称之为事件循环。

宏任务和微任务

在事件循环中其实维护着两个事件队列,分别称之为宏任务队列,微任务队列。

我们所执行的一些耗时事件会分别放置在这两个队列中,比如宏任务队列中放置ajax,定时器,DOM监听,UI rendering等。微任务队列放置Promise的then回调,Mutation Observer API,queueMicrotask() 等。

事件循环对于两个队列的优先级执行顺序的规范是主线程的JS代码先执行,然后再执行事件队列中的任务(回调),但是在执行宏任务队列前要保证微任务队列是空的,即要先执行完微任务队列中的任务。

Node中的事件循环

事件循环其实在不同的平台和浏览器实现上有些不同,但整体的效果基本一致,事件循环就好比一个桥梁。在Node平台中事件循环主要作为JS代码的系统调用的通道。

关于Node的事件循环教程可以参阅网址

Node目前的事件循环主要参照libuv这个异步IO库实现。

Node中一次完整的事件循环叫做一个tick,它又分为很多个阶段,一般由如下的阶段组成:

  • 定时器(timer):定时器中的回调函数。
  • 待定回调(pending callback)一些系统操作执行回调。
  • idle,prepare 仅系统使用。
  • 轮询(poll)检索新的I/O事件,执行与I/O相关的一些回调。
  • 检测(check)setImmediate()回调函数执行。
  • 关闭的回调函数

所以在node处理宏任务和微任务队列时,也不和浏览器一样只是维护着两个队列。
node维护的队列更加的多。

image-20220522151543058

所以就会按照如下顺序执行代码:

image-20220522151714074

错误处理

在开发中我们需要对使用者传递的参数做一些校验,如果用户没有按照我们的要求去传递对应的参数我们应该给用户一个错误提示,这就需要我们主动去告知用户。

throw

我们可以通过throw去抛出一个异常来告知用户。

throw可以跟上基本类型和对象类型,通常我们会抛出一个对象类型,这样会包含更多信息。

对于对象类型,我们可以自己封装一个error类也可以使用系统自带的Error类。

throw new Error('error');

会打印更多的信息:

image-20220518143725881

我们使用默认系统创建的error对象包含三个属性:

  • messsage:创建Error对象时传入的message
  • name:Error的名称,通常和类的名称一致。
  • stack:整个Error的错误信息,包括函数的调用栈,当我们直接打印Error对象时,打印的就是stack

我们一般不会去修改这些属性。

另外Error类还有一些子类:

  • RangeError:下标值越界时使用的错误类型。
  • SyntaxError:解析语法错误时使用的错误类型。
  • TypeError:出现类型错误时,使用的错误类型。

throw后面的代码也是和return一样,不会继续执行。

异常传递

我们如果在一个函数内抛出一个异常,如果调用函数的地方并没有处理异常,那就会将该异常抛出到上层,直到最顶层的函数调用。

function foo() {
  throw new Error("error");
}

function bar() {
  foo();
}

function test() {
  bar();
}

test();

image-20220518144819588

可以看到调用栈的顺序。

异常捕获

一旦我们抛出异常,但是并没有对异常进行捕获处理,那么整个程序就会被强制终止。

我们可以使用try catch来进行捕获异常并处理。这样就不会导致我们的程序强制终止了

function foo() {
  throw new Error("error");
}

function bar() {
  try {
    foo();
  } catch (error) {
    console.log("异常捕获了");
  }
}

function test() {
  bar();
}

test();

image-20220518145102607

另外try catch后还可以加上finally表示最后一定会执行的语句。在ES10中,catch后面绑定的error可以省略。

try{
    ...
} catch(err) {
    
} finally {
    ...
}

注意:如果tryfinally中都有返回值,那么会使用finally当中的返回值。

模块化

参阅

esModule和commandJS的区别

区别一、首先commonJs是被加载的时候运行,esModule是编译的时候运行。

区别二、commandJs输出的是值的浅拷贝,而esModule是值的引用。

/* foo.js */
export const name = "why";
export let age = 18;

export default function addAge() {
  age = 100;
}




/* main.js */
import addAge, { name, age } from "./foo.js";

addAge();
setTimeout(() => {
  console.log(age);
}, 3000);
// 三秒后输出100
/* why.js */
const name = "why";
let age = 18;

function sum(num1, num2) {
  return num1 + num2;
}
const friends = {
  arr: [1, 2, 3, 4],
};

// 1.导出方案 module.exports
module.exports = {
  // aaa: "hahahahaah",
  // bbb: "bbb"
  name,
  age,
  addAge: function () {
    age++;
    console.log(age);
  },
  friends,
  changeFriends() {
    friends.arr.push(999);
  },
  sum,
};



/* main.js */
const { name, age, addAge, sum, friends, changeFriends } = require("./why.js");

addAge();
changeFriends();
console.log(age);
console.log(friends);
// 19
// 18
// { arr: [ 1, 2, 3, 4, 999 ] }

区别三、comandJs具有缓存,在第一次被加载时,会完整运行整个文件并输出一个对象,拷贝(浅拷贝)在内存中。下次加载文件时,直接从内存中取值。

包管理工具

参阅

BOM

DOM

浏览器存储方案

Last modification:May 20, 2023
如果觉得我的文章对你有用,请随意赞赏