本文最后更新于 2024-12-13,文章内容可能已经过时。

https://www.bilibili.com/video/BV1gYpmeKEvt/?buvid=Y4437341CCE36F5B4802ADE61D3DDD1FFAB6&from_spmid=united.player-video-detail.relatedvideo.0&is_story_h5=false&mid=iHGAF%2BHHpO2FODNks2NpkQ%3D%3D&p=2&plat_id=114&share_from=ugc&share_medium=iphone&share_plat=ios&share_session_id=D323A07F-0CF2-4673-A971-12475B7DBAF1&share_source=WEIXIN&share_tag=s_i&timestamp=1729053355&unique_k=Lk40k63&up_id=36139192&vd_source=eecc2c8229facae905b6daed1650b44b

浏览器工作原理和V8引擎

浏览器工作原理

当我们在网址输入一个站点敲下回车的时候,浏览器做了什么?

首先,浏览器会将域名通过DNS解析获取服务器的IP地址 ,然后向服务器发送请求,待服务器接到请求并返回响应给客户端index.html ,这样浏览器获取到html文件后开始对html结构进行解析 ,当遇到 link标签script标签的时候,浏览器会根据href 和 src属性 对应的链接并向服务器发送请求下载对应的css和js文件内容并执行!

以上解析步骤,就是通过浏览器内核(渲染引擎)来实现的!

浏览器渲染原理

在浏览器中,从服务器中获取html文件时,在本地是如何解析和渲染的?

如上图所示:

首先浏览器会同时解析加载HTML DOM 以及 Stylesheet 样式表,并生成DOM treeStyle rules样式规则,两者相结合后会生成Render tree 渲染树并进行布局,随后开始绘制界面,之后在浏览器界面中进行显示!

注意:

在浏览器解析DOM tree 的时候,JS是可以在这个阶段进行 DOM 操作的!

在加载Script脚本 的时候,HTML的解析进度会被阻塞并停止解析!

浏览器中的JS引擎

JS编写的语言是高级语言,原本是机器所无法理解的语言,因此 JS引擎,需要给机器做编码转换成机器所能理解的语言!

SpiderMonkey:第一款JavaScript引擎,由Brendan Eich开发(也就是JavaScript作者) ;

Chakra: 微软开发,用于IE浏览器;

JavaScriptCore : WebKit中的JavaScript引擎Apple公司开发口;

V8: Google开发的强大JavaScript引擎,也帮助Chrome从众多浏览器中脱颖而出;

浏览器内核和JS引擎关系

webkit 为例, webkit 实际上包含两部分内容:

WebCore内核: 主要负责html 解析 布局 绘制 渲染等相关工作!

JavaScriptCore引擎: 负责解析JS代码!

v8引擎原理

v8引擎c++ 编写的JavaScriptWebAssembly 引擎,用于chrome浏览器Nodejs ,并且可以在不同平台(win mac linux)下运行!

如上图所示,JS引擎会首先Parse解析 JS源代码并且进行语法分析词法解析,其次会生成AST语法树,接着通过ignition 转换成字节码,最后在转换成机器指令 !

注意: 字节码支持跨平台!

词法分析语法解析

如下有一段代码声明:

const name = "why";

通过 parse 解析后大概如下

{ tokens: [{ "type": "keyword", "value": "const"},{ "type": "identifier", "value": "name"} ] }

https://astexplorer.net/

以上链接可以编写JS代码在线解析AST语法树运行结果!

MachineCode 优化机器码

Turbofun这个库通过ignition收集执行信息的,获取执行频率比较高的函数,会被标记为hot函数,被标记hot函数会被 MachineCode 直接转换为机器指令无须在通过字节码转为汇编然后在转换为机器指令这个过程!

注意:

如果函数中的参数类型发生变化时

function sum(num1,, num2){
   return num1+num2;
}
sum(5, 10)
sum(20, 10)
sum('a', 'b')

MachineCode 会转换给字节码在转换给汇编最终翻译成机器指令!

那么我们的JavaScript源码是如何被解析(Parse过程)的呢?

Blink将源码交给V8引擎Stream获取到源码并且进行编码转换;Scanner会进行词法分析(lexical analysis ),词法分析会将代码转换成tokens;接下来tokens会被转换成AST树,经过ParserPreParser:

Parser就是直接将tokens转成AST树架构;

PreParser称之为预解析,为什么需要预解析呢?

必然会这是因为并不是所有的JavaScript代码在一开始时就会被执行那么对所有的JavaScript代码进行解析会影响网页的运行效率;

所以V8引擎就实现了Lazy Parsing( 延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行;

比如我们在一个函数outer内部定义了另外一个函数inner,那么inner函数就会进行预解析:

function outer(){
  function inner(){
    console.log("asdasd")
  }
 }
outer()

生成AST树之后会被Ignition转换为bytecode(字节码), 之后就是代码的执行过程了!

全局对象GlobalObject

GlobalObject 也称 go,是v8引擎源码解析到AST树生成时产生的一个全局对象! 即是window也是 go!

var name = "张三";
var age = 18;
var weight = 130;

var result = weight / age;

以上代码在v8引擎解析过程中会产生一个全局对象GlobalObject

var globalObject = {
  Math: {},
  String: {}.
  setTimeout: 'function',
  date: 'Date',
  name: undefined,
  age: undefined,
  weight: undefined,
  result: undefined,
  window: 'this'
}

以上全局对象代码为伪代码!

window 其实指向的就是globalObject全局对象 !

代码解析

代码在执行前,会先行解析,根据上述变量声明代码,在解析过程中,会将全局变量提取到全局对象中GlobalObject,并在代码开始执行前,赋值初始值为 undefind!

运行代码

为了能够运行代码,v8引擎有一个执行上下文栈(Execution Context Stack)(函数调用栈)!通常在函数调用时会进行入栈出栈

运行代码前,代码会提取到内存当中,然后划分两个区域!

代码运行过程

代码的运行过程,就是编写的代码,从开始解析执行的一个过程

全局执行上下文

执行变量赋值的一个过程!

基本类型

解析代码:如下代码 name 变量解析代码的时候被初始值undefind!

执行代码:执行代码 name 变量赋值时,name属性会从GO(GlobalObject)全局对象中找相应属性,若找到并其赋值为"张三"!

var name = '张三'
引用类型

解析代码: 遇到函数类型的时候,如下第一行代码先是调用了foo()函数 ,但是在解析代码这个阶段并不会执行此代码,因此会忽略函数调用,进行下一步代码解析,下一步为函数的声明,这个时候foo声明会被提取到全局对象(GO GlobalObject)当中,随后会开辟一个新的空间用来存储函数(引用类型)的空间,在全局对象中被声明foo函数变量会在新的空间(函数存储)中开辟一个内存地址,并进行映射关联!

foo( 123 ); // 在函数声明前调用函数
function foo( num ){
  console.log(m);
  var m = 12;
  var n = 13;
  console.log('foo fun')
}
var globalObject = {
  setTimeout: 'function',
  date: 'Date',
  window: 'window',
  foo: "0x0000"
}

foo函数: 对应函数存储空间中的一个地址(0x00000)!

执行代码: 当我们在全局上下文中调用foo()函数 时,会从全局对象中找到对应声明函数变量所映射的一个地址!

函数执行上下文

每当执行一个函数时,在执行上下文内都会生成一个函数执行上下文!

函数执行上下文,就是函数内部代码块的一些执行逻辑!

函数内部的一些变量声明等相关逻辑!

其实就是foo函数内部的这些!

函数内部声明的变量,会存放在AO(Activation Object)激活对象当中!

一旦函数内部执行完后,函数执行上下文会自动从执行上下文内部弹出并销毁,同样AO局部作用域内部变量也会被销毁!

// foo 函数内部声明的变量
console.log(m);
var m = 12;
var n = 13;
console.log('foo fun')

作用域链

作用域链,函数内部属于单独一个作用域,也是局部作用域,当函数内部访问的变量为定义或不存在时,会查找父作用域,至到全局作用域范围内位置!

总结

浏览器工作原理

  1. 浏览器分为两部分组成->(内核引擎)!
  2. 内核:
    • 内核主要负责页面html 解析 布局 绘制 渲染等工作!
    • DNS解析域名
    • 获取服务IP地址
    • 向后段发送请求
    • 返回HTML内容
    • 浏览器开始解析
    • 同时解析HTML生成(DOM tree)CSS生成(Style Rules)
    • 生成Render-Tree并进行布局调整
    • 开始绘制
    • 界面显示
  3. js引擎:
    • js引擎主要负责js代码解析转换为机器指令最终执行的一个过程!
    • js源代码 -> parse解析(进行词法分析 语法解析)
    • 解析结果 -> AST语法树的生成
    • 语法树 -> bytecode(字节code)接近于汇编编码_(更接近于机器指令)
    • 运行代码

代码运行过程

v8引擎: js源代码 -> parse解析[词法解析] -> AST语法树 -> bytecode字节码 -> 运行代码

GO(Global Object)

全局对象是在代码解析词法分析是创建的,解析代码时,会将变量声明提取到全局对象中,并初始值为undefind ,若是函数时

将会开辟一个新的函数存放空间并生成一个引用地址指向函数变量!

VO(Variable Object)

vo每个执行上下文会被关联到一个变量环境VO(Variable Object)!

全局上下文 VO 对应 GO 全局对象!

函数上下文 VO 对应 AO 激活对象!

执行上下文栈

执行代码的一个空间栈,包含全局执行上下文函数执行上下文!

全局执行上下文: 全局变量赋值,函数调用等相关逻辑操作!

函数执行上下文: 会执行函数内部的逻辑,如内部变量声明赋值,逻辑操作等,当函数执行完后会被从执行上下文中销毁!

// 全局代码
var name = "张三";
var age = 18;
var weight = 130;
function foo( num ){
  console.log('局部代码');
  var m = 12;
  var n = 13;
  console.log('foo fun')
}
foo( 1411 ); // 函数调用 函数执行上下文逻辑触发 并生成AO对象包含函数内部变量声明的信息!
全局执行上下文

当代码解析完毕时,开始执行代码,碰到变量赋值时,会依次在全局对象中找到对应属性进行赋值!

全局执行上下文中有一个VO(Variable Object)对象,且指向GO对象!

GO(Global Object)全局对象

var globalObject = {
  setTimeout: 'function',
  date: 'Date',
  window: 'window',
  name: undefind, // 执行过程中会将name值赋予张三
  age: undefind, // 执行代码 赋值18
  weight: undefind, // 执行代码 130
  foo: "0x0000" // 引用地址 指向函数的存放地址 函数需要手动调用
}
函数执行上下文

函数被调用时,会在执行上下文中插入一条函数执行上下文,此时会生成AO(Activation Object) ,用来存放函数内部变量声明数

函数在被解析的时候,作用域会默认加上父级作用域!

函数上下文中有一个VO(Variable Object)对象 ,且指向AO对象 !

当在函数内部试图访问某个变量值的时候,若函数内部没有声明,则会随着作用域链向上查找,直到GO全局对象上出现为止,否则报错显示变量未定义的问题!

AO(Activation Object)函数局部对象

{
  m: 12;
  n: 13;
}

内存管理和垃圾回收

内存管理

内存管理在各种编程语言内都会设计到内存的使用,我们的代码执行过程都是在内存管理中实现的 !

内存的生命周期: 内存的空间申请 内存的使用 以及不用的时候销毁!

内存有手动管理,和自动管理,像js java dart等是自动管理内存空间的!

在 js 中内存管理存储分为两种: 一种是, 一种是!

栈stack : 用来存放基本数据类型字符串 布尔值 数字 symbol等 !

堆Heap : 用来存储引用类型,如函数,对象,数组!

栈空间内部直接存储基础类型数据!

var name = "张三";
var age = 18;
var weight = 130;

堆空间内用来存储引用类型,主要是对复杂类型开辟空间后的一个引用地址存储,并会将地址指向声明变量名称!

var obj = {
  m: 12;
  n: 13;
}

栈空间内会存放一个叫obj的变量,其值是一个引用地址,引用地址在堆空间开辟后返回的一个引用地址而指向obj的!

垃圾回收

垃圾回收的算法,就是GC Counter 计数!

当一个对象被其它对象所引用时,counter会计数➕ 1

通过将引用对象 = null 的方式,减少对目标对象的引用直到计数为0的时候,会被回收!

还有一种算法就是标记清除

闭包

闭包可以理解为“定义在一个函数内部的函数”,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当内部函数在外部函数之外被调用时,就会形成闭包。

一个函数内部返回一个函数其函数内部有自由变量便可称为闭包!

function outerFunction() {
    var outerVar = 'I am outside!';
    function innerFunction() {
        console.log(outerVar); // 使用外部函数的变量
    }
    return innerFunction; // 返回内部函数,形成闭包
}

var innerFunc = outerFunction();
innerFunc(); // 输出 "I am outside!"

闭包与函数的区别,当捕捉到闭包的时候,其内部与外部关联的自由变量也同样会被捕捉,当自由变量即使丢失上下文(外部函数)内部也仍然可以访问到!

外部函数一旦执行完return之后便会销毁,其内部变量依旧可以被内部函数所访问!

一个普通的函数function,如果它可以访问外层作用于的自由变量,那么这个函数就是一个闭包 ;

从广义的角度来说: JavaScript中的函数都是闭包;

从狭义的角度来说:JavaScript中一个函数,如果访问了外层作用于的变量,那么它是一个闭包;

闭包内存泄漏问题

为什么闭包外部函数执行完后,函数执行上下文销毁后,AO对象却没销毁?

上图内表示两个普通函数,在上下文中从解析到执行的一个过程,当函数执行完毕后,函数上下文和AO对象都会被销毁!

foo函数AO对象没有被销毁的原因是,因为还被bar函数父级作用域所引用指向无法被销毁,因为bar函数内部存在对foo函数作用域的引用,且无法销毁!

总结

  1. 什么是闭包:
    • 闭包就是一个外部函数内部包含一个内部函数并且访问外部函数中自由变量的一个函数体,当函数被执行时,闭包就会形成!
  2. 闭包为什么会有内存泄漏问题:
    • 每个函数执行的时候都会生成一个AO对象,其内部函数外部自由变量访问时,内部外部函数产生了作用域之间的指向,导致外部函数销毁时,其AO对象还被内部函数父级作用域的指向挂载,且无法销毁!
    • 销毁的条件,就是当一个引用不存在被其它引用时,该对象就会被销毁!
  3. 如何处理内存泄漏:
    • 将不用的函数设置为null即可,只要从全局作用域中没有在对其它引用有所指向了,那么将会被销毁!

this 指向

在js中,this指向是动态绑定的,一般是谁调用,且this会指向调用者!

全局作用域下的this会指向window对象!

this指向绑定规则: 可分为默认绑定 隐式绑定 显式绑定 以及 new 绑定!

默认绑定: 就是直接调用 `foo()`,默认会绑定window对象!

隐式绑定: 就是通过函数调用 ,如`obj.foo()`则会 隐式绑定到obj对象上!

显式绑定: 就是通过 call apply bind 方式改变函数内部this指定!

function thisTest() {
  console.log("thisTest", this)
}

thisTest(); // 默认绑定

var obj = {
  name: "张三",
  age: 18,
  weight: 132,
  thisTest: thisTest
}

obj.thisTest(); // 隐式绑定


var obj2 = {
  name: "李四",
  age: 22,
  weight: 144,
}

thisTest.apply(obj2); // 显式绑定

以上代码会输出不同this指向!

thisTest(); 调用后指向 window对象, 其实调用是 window.thisTest(); window 前缀可以省略的!

obj.thisTest(); 调用后会指向obj对象!

thisTest.apply(obj2); 调用后会将this指向绑定到obj2对象上!

显示绑定

JavaScript中,call, apply, 和 bind 都是函数对象的方法,用于改变函数的执行上下文即函数内部的this值)。\

call

call() 方法使用给定的this值单独提供的参数来调用函数,改变函数本身的this指向 !

如果 thisArg 参数传递是 null undefind 时,则默认会指向全局对象window!

function.call(thisArg, arg1, arg2, ...)
function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

console.log(new Food('cheese', 5).name); // 输出 "cheese"

apply

call方法相似,却别是参数类型不一样,参数类型以数组方式传递!

如果 thisArg 参数传递是 null undefind 时,则默认会指向全局对象window!

function.apply(thisArg, [argsArray])
  • thisArg:在函数运行时使用的this值。

  • argsArray:一个数组或类数组对象,其中包含了传递给函数的参数。

function sum(a, b) {
  return a + b;
}

function callSum1(a, b) {
  return sum.call(this, a, b);
}

function callSum2(a, b) {
  return sum.apply(this, [a, b]);
}

console.log(callSum1(1, 2)); // 输出 3
console.log(callSum2(1, 2)); // 输出 3

apply特别有用在不确定参数数量时或者当参数已经以数组形式存在时。

bind

bind() 方法创建一个新的函数当被调用时,其this值被设定为提供的值,其参数列表中的序列将被传递给绑定函数

如果 thisArg 参数传递是 null undefind 时,则默认会指向全局对象window!

function.bind(thisArg[, arg1[, arg2[, ...]]])
var person = {
  name: 'John',
  age: 25,
  greet: function() {
    console.log('Hello, my name is ' + this.name);
  }
};

var greet = person.greet.bind(person);
greet(); // 输出 "Hello, my name is John"
  • callapply直接调用函数,并立即执行。

  • call接受参数列表,而apply接受一个参数数组。

  • bind创建一个新函数,可以稍后调用,允许你指定this的值和初始参数。

new 绑定

Javascript中,函数是可以作为构造函数通过new操作符来创建!

new关键字调用函数,会有以下步骤:

  1. 创建一个新的对象

  2. 这个对象会被执行prototype 原型连!

  3. 这个新对象会绑定到函数调用的this上(this绑定在这个步骤完成!)

  4. 如果函数没有返回其它对象,则这个函数会返回这个新对象!

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

// 每产生一个新对象都会绑定到 函数的 this 上
var p1 = new Person("张三", 21);
// 每产生一个新对象都会绑定到 函数的 this 上
var p2 = new Person("李四", 24);
// 每产生一个新对象都会绑定到 函数的 this 上
var p3 = new Person("王五", 20);

绑定规则优先级

默认绑定(window) < 隐式绑定(obj.fun()) < 显示绑定(apply/call/bind) < new绑定

function thisTest() {
  console.log("thisTest", this)
}


thisTest(); // 默认绑定 window

var obj = {
  name: "张三",
  age: 18,
  weight: 132,
  thisTest: thisTest
}

obj.thisTest(); // 隐式绑定


var obj2 = {
  name: "李四",
  age: 22,
  weight: 144,
}

thisTest.apply(obj2); // 显式绑定


// new 绑定
function Person(name, age){
  this.name = name;
  this.age = age;
}

// 每产生一个新对象都会绑定到 函数的 this 上
var p1 = new Person("张三", 21);
// 每产生一个新对象都会绑定到 函数的 this 上
var p2 = new Person("李四", 24);
// 每产生一个新对象都会绑定到 函数的 this 上
var p3 = new Person("王五", 20);

显示绑定方法实现

这里对 apply call bind 方法手动实现!

call

// 判断是否为 undefind or null
function isNull(arg) {
  return Object.prototype.toString.call(arg) === "[object Null]" ||
    Object.prototype.toString.call(arg) === "[object Undefind]"
}

Function.prototype.mycall = function (thisArgs, ...args) {
  /* 获取函数的调用者 */
  var fun = this;
  /* 通过隐式调用改变函数内部的this指向 */
  /* 如果参数不是对象(基本类型) 则需要将参数基本类型转换为包装类 */
  thisArgs = !isNull(thisArgs) ? Object(thisArgs) : window;
  thisArgs.fun = fun;
  var result = thisArgs.fun(...args);
  return result;
}


function sum(num1, num2) {
  console.log(num1, num2, this); // 0 1 { name: "张三" }
  return num1 + num2;
}

var aaaa = sum.mycall({ name: "张三" }, 0, 1)

console.log("result", aaaa) // 1

这里通过 prototype 原型连的方式,将mycall函数方法挂载到Function构造器上,这样不同的函数都会继承该方法 !

一旦其它函数调用mycall方法时,就会隐式绑定一个this, 这个this就是调用者

根据调用者传递的thisArgs 将其转换为对象,并且把调用者函数以属性值的方式赋值给thisArgs对象中

然后通过隐式调用方式,改变调用者函数内部的this指向!

考虑到thisArgs 在调用者传递时,可能不为对象,可能是基本类型,可能是null undefind ,当为基本类型时,需要转换为基本类型的包装类对象,若是 undefind 或者是 null时,需要将this指向为 window!

Object(val)用来将基本类型转换为包装类!

apply

call 类似,就是参数传递格式不一致,apply需要传递数组类型!

Function.prototype.myapply = function (thisArgs, argArr) {

  /* 获取函数的调用者 */
  var fun = this;
  /* 通过隐式调用改变函数内部的this指向 */
  /* 如果参数不是对象(基本类型) 则需要将参数基本类型转换为包装类 */
  thisArgs = thisArgs ? Object(thisArgs) : window;
  console.log("args", ...argArr)
  argArr = !!argArr ? argArr : []; 
  thisArgs.fun = fun;
  var result = thisArgs.fun(...argArr);
  return result;
}


function sum(num1, num2) {
  console.log(num1, num2, this);
  return num1 + num2;
}

var aaaa = sum.myapply("张三", [3, 5])

console.log("result", aaaa)

bind

bind需要返回一个新的函数,并且参数传递方式也不一致,返回新的函数参数需要和调用者函数参数进行合并!

Function.prototype.mybind = function (thisArgs, ...args) {
  var fun = this;
  return function (...innerArgs) {
    var newArgs = [...args, ...innerArgs]
    console.log('newArgs', newArgs)
    thisArgs = !isNull(thisArgs) ? Object(thisArgs) : window;
    thisArgs.fun = fun;
    var result = thisArgs.fun(...newArgs);
    return result;
  }
}


var obj2 = {
  name: "李四",
  age: 22,
  weight: 144,
}

function sum(num1, num2) {
  console.log(num1, num2, this);
  return num1 + num2;
}

var bindResult = sum.mybind(obj2, 2)

console.log("result", bindResult(1))

箭头函数

箭头函数是ES6新的概念,从书写上变的更加便利了,但是与普通函数是有具体区别的!

  1. 箭头函数没有 this 指向,通常this指向会是它的父级,同样也没有 argments 参数对象!

  2. 如果在箭头函数中使用arguments参数对象,则会找父级作用域!

  3. 箭头函数不能做构造函数通过new关键字来实现!

  4. 箭头函数使用显示this绑定(apply call bind)无效!

const arg = () => { ... }

一个参数时可以省略括号

const arg = item => { ... }

多个参数一定需要指定括号

const arg = (item, index) => { ... }

函数体内返回一个对象时

// const arg = () => { a:"111", b: "222" } 这中写法,在做词法分析时,无法认出 { ... } 内是代码块,还是一个对象!
const arg = () => { return { a: "111", b: "222" } }
const arg = () => ( { a: "111", b: "222" } ) // ( ... ) 内可以表达是一个整体,表示{ ... }是对象表达式

函数式编程

arguments

arguments 是函数中的参数数组对象,当我们传递参数时,该参数数组对象会帮我们收集成一个数组,虽然有数组的特性,但不是真正的数组! arguments 也有length属性,但是没有数组所特有的方法!

function argumentTest() {
  console.log('arguments', arguments, arguments.length);
  Array.from(arguments).forEach(item => {
    console.log(item)
  })
  var newArr = Array.prototype.slice.call(arguments);
}

argumentTest(1, 2, 3, 5, 6, 7)

arguments 无法直接使用 array中数组方法 map filter forEach,需要通过其它方法将 arguments 转换为数组后才能使用!

  1. Array.from() : 是Array中的一个静态方法,用来将字符串、DOM集合、NodeList转换为数组

  2. Array.prototype.slice.call(arguments) : 内部其实通过for循环,将arguments中的每一项添加到一个新的数组并返回!

纯函数

纯函数是指满足以下两个条件的函数:

  1. 无副作用(Side Effects):纯函数在执行过程中不会改变外部环境的状态,也就是说,它不会修改任何外部变量或对象的状态,也不会进行任何I/O操作(如打印到控制台、网络请求等)。

  2. 引用透明性(Referential Transparency):给定相同的输入,纯函数总是返回相同的输出,并且不依赖于且不会修改任何外部状态。这意味着你可以将纯函数调用替换为它的返回值,而不会改变程序的行为。

var arr = [1, 34, 6, 53, 32, 43]

var newArr = arr.slice(2, 4)
var splice = arr.splice(3)
console.log("newarr", newArr, arr)

如以上代码,splice 操作后会影响原数组的变化,而 slice 它操作后会返回一个新的数组!

// 相同输入 相同输出
// 对外部作用域没有副作用,不会操作函数外的变量,以及事件操作,或者IO操作
function add(num1, num2){
  return num1 + num2;
}


// 修改了外界的变量
var name = "张三"
function modify(){
  console.log("name", name)
  name = "王五";
}

函数柯里化

多个参数拆分,形成多个函数调用的过程就是柯里化

/* 
  柯里化
 */

function sum1(a) {
  return function (b) {
    return function (c) {
      return a + b + c;
    }
  }
}

console.log('柯里化' + sum1(1)(2)(3))

// 简化写法
var sum2 = a => b => c => {
  return a + b + c;
}

console.log('柯理化2 ' + sum2(3)(5)(2))

为什么使用柯里化

柯里化遵守了设计模式之一(单一原则),避免将所有的逻辑交给一个函数处理,将复杂逻辑分成处理,每一个函数有自己专一处理的逻辑!

var log = date => type => message => {
  console.log(`${date.getMinutes()}:${date.getSeconds()}  ${type} ${message}`);
}

var newLog = log(new Date())('DEBUG');
newLog('debug信息2');
newLog('debug信息3');
newLog('debug信息4');

// [21:29:04.089]  29:4  DEBUG debug信息2
// [21:29:04.089]  29:4  DEBUG debug信息3
// [21:29:04.089]  29:4  DEBUG debug信息4

以上代码,做了参数复用,第一次输出 date type 后,只需要关心message 即可!

柯里化函数的封装

/* 
  柯里化函数封装
  只要参数个数满足后 才会真正调用函数
 */
function currification(fun) {
  function currifi(...args) {
    // args 接受的参数满足 所有参数个数时调用函数
    if (args.length >= fun.length) {
      console.log('currification length', args.length, fun.length)
      fun.apply(this, args)
    } else {
      // 如果不满足 继续返回函数
      return function (...args2) {
        return currifi.apply(this, [...args, ...args2])
      }
    }
  }

  return currifi;
}

function add(x, y, z) {
  console.log("currification add", x + y + z);
  return x + y + z;
}

var testCurri = currification(add);

testCurri(1)(2, 3)

面向对象

对象是对某个事物具体的一个抽象和描述,比如生活中常见的汽车,汽车是具体的物,而汽车品牌,颜色,款式,则是汽车本身的一些特性具体描述!

var Car = {
  brand: "理想",
  color: "blue",
  type: "suv",
  spreed: "128/h"
}

面向对象的特性

  1. 封装: 属性 和 方法的代码封装!

  2. 继承: 子类继承父类对象的属性和方法,对代码进行复用!

  3. 多态: 同一个方法,可以在不同地方,执行不同结果!

创建对象的方式

  1. new Object()

    var car = new Object();
    car.brand = "理想";
    car.color = "blue",
    car.type = "suv",
    car.spreed = "128/h"
    
  2. 字面量形式

    var Person = {
      name: "张三",
      say: function (){
    
      }
    }
    

对象属性的操作

var Car = {
  brand: "理想",
  color: "blue",
  type: "suv",
  spreed: "128/h"
}
// 添加属性 
Car.style = "2022款"
// 修改属性
Car.brand = "长城炮"
// 删除属性
delete Car.color

对象属性的限制

通过Object.defineProperty限制对象的读取,遍历等操作!

  1. 参数1: obj 目标对象,针对此对象进行对象属性控制
  2. 参数2: obj 对象属性, 如果在对象中不存在该属性的话,则向该对象中新增属性,并配置该属性的读取遍历相关限制!
  3. 参数3: {} 配置对象,根据配置,来针对属性上的访问控制!

属性配置

属性 属性值 描述
configurable true / false 是否可以 delete 修改属性
enumerable true / false 该属性是否可枚举,可遍历,为false时,该属性无法被读取和遍历!
writable true / false 该属性是否可修改 写入!
value 任意类型 属性值

var car = {
  brand: "理想",
  // color: "blue",
  type: "suv",
  spreed: "128/h"
}
// 添加属性 
car.style = "2022款"
// 修改属性
car.brand = "长城炮"
// 删除属性
// delete Car.color

// 定义一个新的属性
Object.defineProperty(car, 'color', {
  value: "SkyBlue",
  configurable: true,
  writable: true,
  enumerable: true
})

console.log("Car", car)

getter 和 setter

配置对象中,value 属性 和 getter ,以及 writablesetter 不能同时存在!

var car = {
  brand: "理想",
  // color: "blue",
  type: "suv",
  spreed: "128/h",
  _color: "blue"
}
// 添加属性 
car.style = "2022款"
// 修改属性
car.brand = "长城炮"
// 删除属性
// delete Car.color

// 定义一个新的属性
Object.defineProperty(car, 'color', {
  configurable: true,
  enumerable: true,
  // value: "SkyBlue", 和 get 方法不能同时存在
  // writable: true, 和 set 方法不能同时存在
  get() {
    console.log("get color", this._color)
    return this._color;
  },
  set(value) {
    console.log("set color value", value)
    this._color = value
  }
})


car.color = "rose red"

console.log("Car", car.color)

定义多个属性

Object.defineProperties 可以帮我们定义多个属性!

var person = {
  name: "张三",
  _age: 18,
  _say: function () {
    console.log(`我是${this.name}`)
  }
}


Object.defineProperties(person, {
  weghit: {
    configurable: true,
    writable: true,
    enumerable: true,
    value: 130
  },
  age: {
    configurable: true,
    enumerable: true,
    get() {
      console.log("get age~")
      return this._age;
    },
    set(value) {
      this.age = value
    }
  }
})

console.log("person", person, person.age)

gettter 和 setter 简写方式

var person = {
  name: "张三",
  _age: 18,
  _say: function () {
    console.log(`我是${this.name}`)
  },
  // 效果类似于下面配置代码
  get age(){
    return this._age;
  },
  set age( value ){
    this._age = value;
  }
}


Object.defineProperties(person, {
  weghit: {
    configurable: true,
    writable: true,
    enumerable: true,
    value: 130
  },

  /* age: {
    configurable: true,
    enumerable: true,
    get() {
      console.log("get age~")
      return this._age;
    },
    set(value) {
      this.age = value
    }
  } */
})

console.log("person", person, person.age)

获取属性配置项

/* 获取对象中指定属性的配置项 */
Object.getOwnPropertyDescriptor( person, "name" )

/* 获取对象中所有属性配置项 */
Object.getOwnPropertyDescriptors( person )

// 获取结果 {"value":130,"writable":true,"enumerable":true,"configurable":true}

禁止对对象属性进行扩展

禁止对该对象进行属性添加

Object.preventExtensions(person)

禁止属性配置 / 删除属性

Object.seal(person)

禁止属性修改编辑(冻结)

被冻结的对象,属性且无法被修改!

Object.freeze(person)

构造函数

构造函数也可以称为普通函数,只要通过 new 关键字 创建出来的函数,都是构造函数!

new 操作符作用

new 操作符会创建一个空的对象

将函数的 prototype 给新建对象内部prototype(__proto__) 进行赋值

构造函数内部的 this 会指向创建的新对象!

执行函数内部的代码

如果函数内部没有返回空对象,则返回创建出来的新对象

function Person( name, age ){
  this.name = name;
  this.age = age;
  this.say = function(){
    console.log(`${this.name} say ~`)
  }
  this.eating = function(){
    console.log(`${this.name} eating ~`)
  }
  this.running = function(){
    console.log(`${this.name} running ~`)
  }
}

// 空对象的 this 指向 函数 this
// 函数中的 prototype 会赋值给 新对象的 prototype
// 执行函数代码
let person1 = new Person("张三", 19);

let person2 = new Person("李四", 22);

console.log("person1", person1)

原型

每个对象中都有一个[prototype],这个属性可以称之为原型!

隐式原型(对象)

隐式原型(__proto__)

当我们试图去获取对象中某一个属性时,这时会触发[Get]操作,首先在对象上查找该属性,若没有,就去原型上查找该属性,没有则返回Undefind!

每个对象都有一个内部属性[[Prototype]](在ES5之前,这个属性没有直接访问的方式,但在ES6中引入了Object.getPrototypeOf(obj)Object.setPrototypeOf(obj, proto)获取和设置对象的原型)。这个属性指向对象的原型。在ES5之前,通常使用__proto__属性来访问这个隐式原型,但请注意__proto__不是标准属性,不推荐直接使用。

var obj1 = {
  name: "张三"
}

// obj1.age = 18 对象本身查找
obj1.__proto__.age = 18 // 在对象原型上查找
console.log("obj1.age __proto__", obj1.__proto__)
console.log("obj1.age", obj1.age)

显式原型(函数)

显示原型(prototype)

每个函数对象都有一个特殊的属性叫做prototype这个属性是一个指针,指向函数的原型对象也就是通过这个函数创建的实例的原型

function person(){
  // new 关键字创建的新对象
  var obj = {}
  // 新建对象的隐式原型 会 指向 函数的显示原型!
  obj.__proto__ = person.prototype
  // 构造函数的this指向 会 新建的空对象
  person.apply(obj)
  return this // 如果没有返回空对象,则返回创建对象本身
}


var person = new person()

prototype中的属性

fun.prototype 对象中存在一个特殊属性(constructor),这个属性其实就是fun函数本身!

constructor 这个属性默认是不可枚举的,所以打印的时候,显示的空对象!

function Person() {
  
}
console.log(Person.prototype.constructor.prototype.constructor)

prototype 对象属性添加修改

Person.prototype.name = "张三";
Person.prototype.age = 18;

隐式原型对象(__proto__) 指向了 显示原型(函数)(prototype), 其内部属性会继承自prototype属性!

获取对象上的原型

Object.getPrototypeOf(obj)

显示与隐式关系

  • 当你通过一个构造函数创建一个新实例时(例如使用new Person()),这个新创建的对象的隐式原型([[Prototype]])会指向构造函数的显示原型(Person.prototype)

  • 这意味着,通过构造函数创建的所有实例都会共享同一个原型对象上的属性和方法

原型链

原型链: 当从对象中获取某个属性时,若对象不存在这个属性,则会去原型上查找,若还没有,则会沿着原型链找,直到顶层原型链为止!

JavaScript中,原型链是实现继承的一种机制。每个对象都有一个内部链接指向另一个对象,即它的原型对象。这个原型对象自身也有一个原型直到达到一个终点——null。这个终点称为原型链的顶端Top of the prototype chain)。

var fruit = {
  name: "苹果",
  word: "apple"
}

// fruit.color

console.log("fruit color", fruit.__proto__.__proto__)

顶层原型链: 即是 Object.prototype ,当访问__proto__返回为null时,则到了原型链顶端!

创建对象的方式:

字面量: var car = { brand: "逍客" };

new 关键字: var car = new Object();

new Object() 创建的对象,类似于调用构造函数一样,同样会去做,new 关键字内部创建流程:

  1. 在内部创建一个空的对象

  2. 将函数的prototype原型赋值与空对象的隐式原型__proto__

  3. 将函数的this指向空对象

  4. 执行函数体

  5. 返回 this 空对象

继承

原型链实现继承

通过父类将子类的共性提取出来,子类在创建构造函数时,将子类原型挂载到父类上,即可完成继承!

function Person( friends ) {
   this.friends = friends;
}

Person.prototype.eating = function () {
  console.log(`${this.name} 在吃饭 ~`)
}
Person.prototype.running = function () {
  console.log(`${this.name} 在吃饭 ~`)
}


function Student(name) {

  this.name = name;

}
// 将父类挂在到 子类原型上
Student.prototype = new Person()

Student.prototype.studying = function () {
  console.log(`${this.name} 在学习 ~`)
}


var stu1 = new Student("张三")

var stu2 = new Student("李四")

stu1.eating()

stu1.running()

stu1.studying()

原型链实现继承的一些弊端

  1. 子类继承父类时,继承父类的属性无法打印查看具体属性!

  2. 当父类中有引用类型数组时,多个子类修改数组项,会相互影响!

var stu1 = new Student("张三")

var stu2 = new Student("王五")

stu1.friends.push("koko")

console.log(`${stu1.name} 的朋友: ${stu1.friends}`)

console.log(`${stu2.name} 的朋友: ${stu2.friends}`)

Person 中 有一个 friends 数组时,一个子类修改此数组时,会影响到其它子类!

原型式继承(对象)

通过创建新的对象,使用 setPrototypeOf()父类原型继承到新的对象,并返回!

// 父类
var obj = {
  name: "张三",
  age: 18
}
// 1.
function createObject1(o) {
  var newObj = {}
  // 将 设置父类原型到 新的对象上
  Object.setPrototypeOf(newObj, o)
  return newObj;
}
// 2.
function createObject2(o) {
  var newObj = {}
  // 将 设置父类原型到 新的对象上
  // Object.setPrototypeOf(newObj, o)
  function Fun() { }
  Fun.prototype = o;
  return new Fun();
}

// 子类
var info = createObject2(obj)
// 3.
// var info = Object.create(obj) 与上方函数内部实现方式一样
console.log("info", info.__proto__) // log info {"name":"张三","age":18}

寄生式继承

/* 寄生式继承 */
var person = {
  running: function (name) {
    console.log(name + "  running ~ ")
  }
}


function createStudent(name) {
  var stu = Object.create(person)
  stu.name = name;
  stu.studying = function () {
    console.log(" studying ~ ", stu.name)
  }
  return stu;
}

var stu1 = createStudent("张三")

var stu2 = createStudent("王五")

stu1.running(stu1.name)

stu2.running(stu2.name)

寄生式组合原型式继承

/* 寄生式组合继承 */

function Person() {
  // 这里的属性无法被继承,因为给Student原型赋值的是Person的原型,这里的friends并不存在原型里,只有Person对象自己可以访问
  this.friends = [];
}
// 这里原型添加的属性和方法可实现继承! 不过引用类型都是同一个地址,多个子类继承时,push添加项,都会互相影响
Person.prototype.friends = [];

Person.prototype.eating = function () {
  console.log("extends person eating fun ~ " + this.name)
}

Person.prototype.running = function () {
  console.log("extends person running fun ~" + this.name)
}

function Student(name, age, weight, sno) {
  this.name = name;
  this.age = age;
  this.weight = weight;
  this.sno = sno;
}

/* 
  子类继承父类
  create 函数会创建一个新的对象,并且将参数对象的原型赋值与新的对象并返回!
  prototype 每个函数中特有的原型属性,且原先内部包含 construtar 函数 会指向 函数本身
  这里当把子类原型替换成父类原型后,需要重新将 construtar 构造函数定义成 子类,否则 construtar 会被新的对象替换掉
 */

/* Student.prototype = Object.create(Person.prototype)

Object.defineProperty(Student.prototype, 'constructor', {
  value: Student,
  enumerable: false,
  configurable: true,
  writable: true
}) */

// 实现继承工具函数
inheritPrototype(Person, Student);

Student.prototype.studying = function () {
  console.log(`${this.name} 正在学习~`)
}

var stu1 = new Student("张三", 18, 120, '0012938')

stu1.eating();
stu1.running();
stu1.studying();
stu1.friends.push('bobo')
var stu2 = new Student("李四", 22, 130, '0012939')

stu2.eating();
stu2.running();
stu2.studying();
stu2.friends.push('lolo')

console.log("stu1.friends", stu1.friends)
console.log("stu2.friends", stu2.friends)

// friends 子类操作 相互会有影响
// stu1.friends ["bobo","lolo"]
// stu2.friends ["bobo","lolo"]

Student.prototype, 'constructor'

这里 Student函数 原有的 prototype 内部的 constuctor 函数是指向Student函数本身的,由于继承Person prototype后,就指向的新对象,因此新对象不存在 constructor 这个属性,则会寻着原型链查找,最终指向了Person,因此需要手动将 constructor 指向 Student函数!

添加继承原型工具函数

function inheritPrototype(superObj, subObj) {
  subObj.prototype = Object.create(superObj.prototype)

  Object.defineProperty(subObj.prototype, 'constructor', {
    value: subObj,
    enumerable: false,
    configurable: true,
    writable: true
  })
}

对象方法补充

create(obj)

创建一个新的对象,两个参数,一个是目标对象,一个是配置对象!

根据目标对象,会创建一个新的对象,并把目标对象赋值与新对象的__proto__原型对象上!

配置对象,可添加属性以及配置,并将属性加入创建的新对象不是原型!

var aaaa = {
  test: "sdaasdwa"
}

var adsainfo = Object.create(aaaa, {
  address: {
    value: "address ip",
    enumerable: true,
    configurable: true,
    writable: true
  }
})
console.log("adsainfo obj", adsainfo) // adsainfo obj {"address":"address ip"}
console.log("adsainfo __proto__", adsainfo.__proto__) // __proto__ {"test":"sdaasdwa"}

判断对象中有没有该属性

hasOwnProperty

hasOwnProperty 方法,可根据参数判断目标对象中是否存在该属性,有则返回true 相反为false!

Object.hasOwnProperty("test") // false test属性是复刻目标对象的属性被存放在新对象的原型当中
Object.hasOwnProperty("address") // true address属性在对象中创建
in操作符

in 操作符也可以判断是否存在该属性!

console.log('test' in adsainfo); // true 不管在当前对象 或者是 在原型对象上都是为 true

for .... in 无论是在本身对象 或者 是在对象原型上,都会被遍历出来!

  • hasOwnProperty: 该方法只判断本身对象中是否存在该属性即便是原型上存在,也为false!

  • in 操作符: 该操作符 无论是 本身对象 或者是 原型上的属性,都会被作为参考!

instanceof

instanceof 用来判断函数prototype原型是否存在构造函数原型链中!

instanceof Obj 后面这个只能是个函数不能是对象字面量!

function Obj() {

}

var stu1 = new Obj();

console.log("stu1 in obj", stu1 instanceof Obj) // true

isPrototypeOf

isPrototypeOf() 方法用于检查一个对象是否存在于另一个对象的原型链上。如果调用对象位于另一个对象的原型链中,则返回 true否则返回 false

function Obj() {

}

var stu1 = new Obj();

console.log("stu1 in obj", stu1 instanceof Obj)
console.log("stu1 in obj", Obj.prototype.isPrototypeOf(stu1))

var obj = {
  name: "张三"
}

var info = Object.create(obj)

// console.log("stu1 in obj", obj instanceof info) 无法判断
console.log("isPrototypeOf in obj", obj.isPrototypeOf(info))
  • instanceof: 判断函数 prototype 原型是否在 构造函数原型链中!

  • isPrototypeOf: 可以用于任何对象包括函数数组正则表达式等。

总结

  1. new 操作符的作用
    • 通过 new 操作符来调用函数时,这个函数被称为构造函数
    • 首先 会在函数内部创建一个空对象
    • 其次 会将函数中的原型对象 prototype 赋值给对象隐式原型 __proto__
    • 之后 会将构造函数中的 this 指向空的对象
    • 最后 执行函数体 然后返回对象
  2. 原型
    • 每个对象上都有一个[[prototype]]原型属性,原型可以帮我们实现属性和方法共享!
    • 隐式原型: 是对象中存在的原型对象(__proto__),
    • 显示原型: 是函数内部特有的原型对象(prototype)
  3. 原型链
    • 当试图从某个对象中get获取属性时,若此对象中获取不到,则会去对象的prototype原型中查找,若还是查不到,则会向上个原型进行查找,直到Object.prototype查找为null为止!
    • 每个对象内部都有一个原型,每一个原型都会指向另一个对象, 而这个原型自身也有一个原型,必然形成一个链接!

ES6相关

面向对象

class 类

类的声明

class 类是ES6新出的声明对象的一个语法糖,通过babel代码转换后,其实内部也是通过函数对象原型链实现的!

// 声明一个 Teacher类
class Teacher {

}

// 声明一个 Teacher函数(类)
function Teacher(){ 

}
var teacher = new Teacher();
console.log("Teacher class", typeof Teacher) // log Function
console.log("Teacher class", Teacher.prototype) // log {}
console.log("Teacher class", Teacher.prototype.__proto__) // log {}
console.log("Teacher class", Teacher.prototype.contructor) // 指向 Teacher()构造函数
console.log("Teacher class", Teacher.prototype === teacher.__proto__) // true 

new 关键字创建了Teacher 类,其内部会帮我们做以下处理:

创建一个新对象,并将Teacher.prototype 赋值给空对象隐式原型(___proto__)

随后会将函数this指向新建的对象

执行函数体

如果没有返回对象的话,则返回这个新建绑定的对象!

类的构造函数

通过类的contructor构造函数进行参数传递!

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

var teacher = new Teacher("张三", 18);

console.log("Teacher class", teacher) // {"name":"张三","age":18}
类中的方法和访问器
class Teacher {
  constructor(name, age) {
    this.name = name;
    this.age = age;
    // 下划线代表属性私有访问
    this._lectureTime = new Date().toLocaleDateString()
  }
  // 内部方法其实也定义在了 Teacher 类的原型对象prototype上
  teaching() {
    console.log(`${this.name} 在授课~`)
  }

  get lectureTime() {
    console.log("get 获取 lectureTime 属性拦截")
    return this._lectureTime;
  }

  set lectureTime(newValue) {
    console.log("set 设置 lectureTime 属性拦截")
    this._lectureTime = newValue
  }

}
// 函数中的写法
/* function Teacher(name, age) {
  this.name = name;
  this.age = age;
}

Teacher.prototype.teaching = function () {
  console.log(`${this.name} 在授课~`)
} */

var teacher = new Teacher("张三", 18);
// teacher.teaching();
console.log(`${teacher.name} 授课时间为 ${teacher.lectureTime}`)
静态方法

在定义方法之前加入前缀 static 则 该函数表示为静态方法,在不用实例化的情况下,可以通过类名直接访问静态方法!

class Teacher {
  constructor(name, age) {
    this.name = name;
    this.age = age;
    // 下划线代表属性私有访问
    this._lectureTime = new Date().toLocaleDateString()
  }
  // 内部方法其实也定义在了 Teacher 类的原型对象prototype上
  teaching() {
    console.log(`${this.name} 在授课~`)
  }

  // 访问器
  get lectureTime() {
    console.log("get 获取 lectureTime 属性拦截")
    return this._lectureTime;
  }

  set lectureTime(newValue) {
    console.log("set 设置 lectureTime 属性拦截")
    this._lectureTime = newValue
  }

  // 静态方法 可以直接通过类来访问此方法,无需通过new创建对象后访问
  static staticTeacherFun() {
    console.log("调用了静态方法 staticTeacherFun ~")
  }

}
// 静态方法
Teacher.staticTeacherFun();
var teacher = new Teacher("张三", 18);
// teacher.teaching();
console.log(`${teacher.name} 授课时间为 ${teacher.lectureTime}`)

继承

class中继承很简单,通过extends关键字可实现继承父类!

class Animals {
  constructor(name, type) {
    this.name = name;
    this.type = type;
  }

  eating() {
    console.log(`${this.name} in eating ~`)
  }

  running() {
    console.log(`${this.name} in running ~`)
  }

  sleep() {
    console.log(`${this.name} in sleep ~`)
  }
}


class Dog extends Animals {
  constructor(name, type) {
    // 在 this 之前 调用 super 父级构造函数, 否则会报错
    super(name, type);

  }
}

class Cat extends Animals {
  constructor(name, type) {
    // 在 this 之前 调用 super 父级构造函数, 否则会报错
    super(name, type);

  }
}

var dog = new Dog("柯基犬", "犬科");

var cat = new Cat("波斯猫", "猫科");

dog.eating();

cat.running();

上面代码,一个父类(Animals)动物,两个子类(Dog)狗(Cat)猫

通过 extends Animals方式继承了父类

在子类constructor中使用了super函数super是调用了父类的构造函数,必须在this之前调用!

方法重写

如果父类中的方法不满足业务需求时,我们可以在子类中重写父类方法!

class Cat extends Animals {
  constructor(name, type) {
    // 在 this 之前 调用 super 父级构造函数, 否则会报错
    super(name, type);

  }
  // 方法重写
  sleep(){
    super.sleep(); // 通过 super 可以直接调用父类方法
    // 加入自己的逻辑处理
  }
}

Babel ES6转换ES5

如以下class代码,babel会帮我们创建Animals函数,并将函数中的方法挂载到这个函数的原型对象上!

class Animals {
  constructor(name, type) {
    this.name = name;
    this.type = type;
  }

  eating() {
    console.log(`${this.name} in eating ~`)
  }

  running() {
    console.log(`${this.name} in running ~`)
  }

  sleep() {
    console.log(`${this.name} in sleep ~`)
  }
}

对象字面量增强

属性和方法简写以及属性名计算

var name = "张三"
var age = 22
// 以前写法
var obj = {
  name: name,
  age: age,
  eating: function (){}
}
obj[name + 123] = "属性名计算"

// 现在写法
var obj = {
  name,
  age,
  eating() {},
  running: () => {},
  [name + 123]: "属性名计算"
}

数据结构

数组

数组是根据顺序进行结构

var arr = ["张三", "李四", "王五"]

// 结构三项
var [item1, item2, item3] = arr
console.log(item1, item2, item3) // 张三 李四 王五

// 结构后两项
var [, item2, item3] = arr
console.log(item2, item3) // 李四 王五

// 结构第一项 后一项结构为数组
var [item1, ...newArr] = arr
console.log(item1, newArr) // 张三 ["李四","王五"]

// 结构中的默认值 若 结构中的值不存在,则给默认值
var [item1, item2, item3, item4 = "齐六"] = arr
console.log(item1, item2, item3, item4) // 张三 李四 王五 齐六

// 在函数中使用
function _objFun([, item2, item3]) {
  console.log("_objFun", item2, item3) // 李四 王五
}

_objFun(arr)

对象

对象是根据key 值进行结构

var _obj = { name: "张三", age: 22, weghit: 122 }

var { name, age, weghit } = _obj
console.log(name, age, weghit) // 张三 22 122

// 取部分属性值
var { age, weghit } = _obj
console.log(age, weghit) // 22 122

// 默认值
var { age, weghit, sno = "0123" } = _obj
console.log(age, weghit, sno) // 22 122 0123

// 别名,避免与其它命名冲突
var { name: newName } = _obj
console.log(newName) // 张三

// 在函数中使用
function _objFun({ name, age, weghit }) {
  console.log("_objFun", name, age, weghit) // 张三 22 122
}

_objFun(_obj)

let 和 const

letconst 以及 var 关键字都是用来声明变量的一种方式,除了声明变量以外,也是有点差别的!

var:函数作用域,存在变量提升容易污染变量 ,并且可以重复定义变量值,后面会覆盖前面已声明过的变量!

let:块级作用域,不存在变量提升,不能在声明变量前进行访问,存在暂时性死区,不能重复定义变量!

const:块级作用域,与let的区别是,const是常量声明,声明变量时必须有初始值,且值一旦被赋值,则无法不能被修改!

注意: const 虽然值不能被修改,但是对象的属性值是可以改的,内存中对象引用地址不可改!

没有let和const之前,通过var声明的变量都会保存在window全局对象当中!

let 和 const 之后,就出现了variableMap 对象!

块级作用域

ES6中块级作用域的出现,防止了var声明时可以在块级之外访问的问题!

在块级内部通过var声明的变量,作用域是无效的,可以在块级之外访问定义的变量!

常见的块级有: if switch for 等表达式,都是块级的表现!

let const class 都是受块级作用域限制的,var在块级作用域中无效!

{
  // 块级代码
  var age = 12;
  let foo = "aaa"
  function fun() { }
  class obj { }
}

console.log("var ~", age) // var ~ 12
console.log("let ~", foo) // 报错 foo is not defined
console.log("fun ~", fun)
console.log("class ~", obj)

if 块级作用域

if (true) {
  var age = 12;
  let foo = "aaa"
}

console.log("var ~", age) // 12
console.log("let ~", foo) // 报错 foo is not defined

switch 块级作用域


switch ("a") {
  case "a":
    var age = 12;
    let foo = "aaa"
}

console.log("var ~", age) // 12
console.log("let ~", foo) // 报错 foo is not defined

for 块级作用域

for (var i = 0; i <= 5; i++) {
  // var i = 0; 实际上在这里定义了 i 的变量
  // i++
}
console.log(i) // 6

还有一个弊端就是,for循环体中使用function函数其内部访问了 i 的属性 ,那么这个函数,会随着作用域向上全局查找 i 的属性,这时 i 的属性值已经累加到 4 ,且不是随着 i 属性的累加 依次获取到的值!

for (var i = 0; i <= 5; i++) {
  function foo(){
    console.log(i) // 6 获取的不是 0 1 2 3 4
  }
}
console.log(i) // 6

通过立即执行函数,闭包的形式获取

for (var i = 0; i < 5; i++) {
  (function(n){
     function foo(){
        console.log(n) //获取是 0 1 2 3 4
     }
  })(i)
}

暂时性死区

在块级作用域中通过let 或者 const 声明变量时,在声明变量之前访问则会报错,这种情况称之为暂时性死区!

var foo = "name"
if( true ) {
  // 访问报错
  console.log("foo",foo)
  let foo = "asda"
}

模版字符串

基础用法

let _name = "张三";
let _age = 18;
let _weghit = 127;

// 最初的写法
console.log("名字是" + _name + "年龄是" + _age + "体重是" + _weghit)

// 模版字符串 (基础写法)
console.log(`名字是${_name} 年龄是${_age} 体重是${_weghit}`)

// 模版字符串 (属性计算)
console.log(`年龄加2${_age + 2}`)

// 模版字符串 (函数调用)
console.log(`年龄加2${computedAge()}`)


function computedAge() {
  return _age + 2;
}

标签模版字符串

以函数的形式进行调用!

let _name = "张三";
let _age = 18;
/*
  参数一: 字符串数组,会以${}方式进行拆分!
  参数二: ${name} 第一个变量
  参数三: ${age} 第二个变量
*/
function _foo(a, b, c) {
  console.log("_foo", a, b, c)
}

_foo`Hello${_name}World${_age}` //  _foo ["Hello","World",""] 张三 18

函数的默认参数

参数只有为 undefind ,或者参数不传的时候,才会有默认值!

function _foo(name = "张三", age = 18) {
  console.log(name, age)
}

_foo("李四") // 李四 18 age 不传 默认 18

对象参数结构默认值

function _fooObj({ name = "张三", age = 18 }) {
  console.log("_fooObj", name, age)
}

_fooObj({})
function _fooObj({ name = "张三", age = 18 } = {}) {
  console.log("_fooObj", name, age)
}

function _fooObj({ name = "张三", age = 18, id } = { id: 0910923 }) {
  console.log("_fooObj", name, age, id)
}
_fooObj()

通常有默认值的参数放最后,可以省略默认参数传递!

注意: 函数默认值,会影响函数中 length 属性长度大小,有默认值的参数 包括后面的参数都不会算在 length 当中!

function _fooObj(name, age = 18, id) {
  console.log("_fooObj", name, age)
}

_fooObj.length // 1  [age = 18, id] 不算在长度范围内

函数中剩余参数

function _foo(m, n, ...args) {
  console.log("_foo", m, n, args)  // _foo 1 23 [3,54,5,2,3,2]
  console.log("_foo", arguments) //  _foo {"0":1,"1":23,"2":3,"3":54,"4":5,"5":2,"6":3,"7":2}
}

_foo(1, 23, 3, 54, 5, 2, 3, 2)

剩余参数 ...args arguments对象相似,

args: 会将多余的参数收集成一个数组,且不会影响原有的参数!

arguments: 会将所有的参数,收集成一个arguments对象!

箭头函数的补充

箭头函数中因为不能使用构造函数也不能通过new关键字进行实例化所以没有自己的原型prototype也没有自己的this绑定!

箭头函数中没有 this ,默认从上级获取

箭头函数中没有arguments,默认从上级获取

箭头函数不能通过 new 关键字实例化构造函数

展开运算符

const names = ["张三", "李四", "王五"];
const desc = "描述内容";
const obj123 = { name: "张三", age: 18 };

// 展开 names 中的数据
console.log("names", ...names); // names 张三 李四 王五
// 展开 names 中的数据放到一个新的数组中
console.log("newNames", [...names]); // newNames ["张三","李四","王五"]
// 展开 字符串
console.log("desc", ...desc); // desc 描 述 内 容
// 展开 对象浅复制
console.log("obj123", { ...obj123, newName: "newName" }); // obj123 {"name":"张三","age":18,"newName":"newName"}

浅拷贝:展开对象时,会对该对象的属性进行浅复制,在修改新对象中的复制过来的属性时,不会影响到原对象中的属性值!

如果对象中包含引用类型对象的话,只会复制该对象的引用地址,其在新对象中修改复制过来对象的属性时,是会具体影响到原有对象的数据!

数值(进制)表示

const num = 100; // 十进制
const num1 = 0b100; // 二进制
const num2 = 0o100; // 八进制
const num3 = 0x100; // 十六进制

Symbol基本使用

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

Symbol数据类型是独一无二的,用来解决对象中,属性命名冲突的问题!

const _name = Symbol("张三");
const _age = Symbol(18);

console.log("Symbol", _name.description, _age.description)
// Symbol 张三 18

作为对象 key 值的使用

const _s1 = Symbol();
const _s2 = Symbol();
const _s3 = Symbol();

const _obj11 = {
  [_s1]: "李四",
  [_s2]: 22
}

_obj11[_s3] = 122

console.log("symbol", _obj11[_s3])

Symbol 不能使用. 操作符来获取Symbol数据,例如: obj11.s3 ,这样是不允许的,因为. 操作符,内部会将获取的key作为字符串来进行处理!

Symbol 作为对象中的key时,无法通过Object.keys()来获取对象中的key值,也无法通过Object.getOwnPropertyNames()获取!

Object.getOwnPropertySymbols() 可以帮我们获取对象中所有的Symbol值,并返回一个数组,然后通过for...of遍历key获取!

let symbols = Object.getOwnPropertySymbols(_obj11)
for (key of symbols) {
  console.log("_obj11[key]", _obj11[key])
}

Symbol中如何定义相同值,需要使用Symbol.for来实现!

const s1 = Symbol.for("aaa")
const s2 = Symbol.for("aaa")
// 获取s1的值
const getS1 = Symbol.keyFor(s1);
console.log(s1 === s2) // true

数据结构(Set Map)

数据结构,就是存储数据的地方,最初的存储方式,是数组对象形式,如今新加了Set Map 以及 WeakSetWeakMap存储方式!

强引用

一个对象被一个变脸所引用时,这种赋值的方式便是强引用!

let obj = { name: 'example' }; // obj 是一个强引用

只要 obj 变量存在,它所引用的对象就不会被垃圾回收机制回收

弱引用

在 ES6 中,引入了 WeakMap WeakSet 这两种数据结构,它们的键(key)就是弱引用

弱引用,当一个对象没有被指向时,此时便可随时可被垃圾回收!

const weakMap = new WeakMap();
const obj = { name: 'example' };

weakMap.set(obj, 'some value'); // obj 作为键是弱引用

Set 数组结构

Set存放不可重复的一组数据的集合 !

Set 可存放基本类型引用类型!

Set存放的引用类型属于 强引用!

const _arr = [12.33, 12, 45, 12]

// set 可以接受数组参数来转换成 一组唯一的 set 数据集合 
const _set = new Set(_arr);

_set.add(22) // 添加数据

_set.add(34)

_set.add(42)

console.log("_set.size", _set, _set.size) // 获取集合存储长度

_set.delete(42) // 删除指定数据

_set.has(42) // 判断有没有包含

_set.clear() // 清除集合

使用 for...of 或者forEach 可遍历set集合数据!

_set.forEach(item => {
  console.log(item)
})


for (let item of _set) {
  console.log(item)
}

将set集合转换为数组!

const _newArr = Array.from(_set);
const _newArr = [..._set]
console.log("newArr", _newArr)

WeakSet 结构

WeakSet 也是一组不可重复数据的数据集合!

WeakSet 只能存放对象,不能存放基本类型!

WeakSet弱引用,随时会被垃圾回收!

WekSet不可遍历的,防止结合内部引用不能正常回收的问题!

const arrObj = [{ name: "张三"}, {name: "李四" }]
const _set = new WeakSet(arrObj);

Map 对象结构

Map 是键值对存储结构,与普通对象的区别是:

普通对象: 无法以对象作为 key,当key为引用对象时,会将转换为[Object object]字符串形式来作为 key值!

Map集合: 内部 key 可以将对象用作键来进行存储 !

const zhangsan = {
  name: "张三",
  age: 18
}

const lisi = {
  name: "李四",
  age: 18
}
const _map = new Map()

// 添加 set key value
_map.set(zhangsan, "张三")
_map.set(lisi, "李四")
_map.set("_mapObj", "okopwdw")

// 获取 get key value
console.log(_map.get(lisi)) // 李四

// 判断 key 是否存在 map结构中
_map.has(zhangsan)

// 根据 key 删除某项
_map.delete(zhangsan)

// 清除
_map.clear()

Map 结构遍历: 也是通过 for...offorEach 形式去遍历!

_map.forEach((value, key) => {
  console.log("foreach", value, key)
  /* 
     log foreach 张三 {"name":"张三","age":18}
     log foreach 李四 {"name":"李四","age":18}
     log foreach okopwdw _mapObj
   */
})


for (let item of _map) {
  /* 
    返回一个数组
    [{"name":"张三","age":18},"张三"]
    [{"name":"李四","age":18},"李四"]
    ["_mapObj","okopwdw"]
   */
  console.log("for...of", item[0], item[1])
}


for (let [key, value] of _map) {
  /* 
    返回一个数组
    [{"name":"张三","age":18},"张三"]
    [{"name":"李四","age":18},"李四"]
    ["_mapObj","okopwdw"]
   */
  console.log("for...of", key, value)
}

WeakMap 结构

weakMapmap 一样,存储键值对,区别如下:

map: 键 可存储 基本类型引用类型, 且引用类型为 强引用,并且可遍历!

weakMap: 只能存储 对象引用类型,且引用类型为 弱引用,其内部元素是不可遍历的,没有forEach遍历方法,也没有clear方法!

const _weakMap = new WeakMap(); // 其方法与map方法一样 没有 clear 和 forEach方法

Promise异步任务处理

promise 异步任务处理,主要在异步任务处理完成时做出的一个结果反馈!

function request(res, successCallback, errCallback) {
  setTimeout(() => {
    let { success, data } = res
    if (success) {
      successCallback(data)
    } else {
      errCallback({ errMsg: "err..." })
    }
  }, 100)
}

let _data = { success: true, data: { message: "", id: "1283923351" } };

request(_data, (data) => {
  console.log("success", data)
}, (err) => {
  console.log("err", err)
})

早期使用了回调函数来处理异步函数内部返回结果!

替换成 promise

function request(res) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let { success, data } = res
      if (success) {
        resolve(data)
      } else {
        reject({ errMsg: "err..." })
      }
    }, 100)
  })

}

let _data = { success: true, data: { message: "", id: "1283923351" } };

request(_data).then((data) => {
  console.log("success data", data)
}).catch(e => {
  console.log("error e", e)
})

promise 中有两个回调函数,一个是 resolvereject !

resolve : 当结果成功执行时,调用该回调函数,会自动帮我们调用 .then方法!

reject : 当结果执行失败时,调用该回调函数,会自动帮我们调用.catch方法!

Promise三种状态

promise 中有三种执行状态

pending:在代码结果为确定之前!

then: fulfilledresolve 为确定结果时!

catch: reject 驳回 拒绝状态!

new Promise((resolve, reject) => {
  // pending 等待状态
}).then(data => {
  // resolve 确定
}, e => {
  // reject 失败 驳回 拒绝
})

resolve参数

  1. 如果resolve参数中是普通值或者对象,则状态由pending状态变为fufilled 确定状态!

  2. 如果参数传入一个新的 promise 对象时,则状态交给新的 promise 来决定!

  3. 如果传入一个对象,并且对象中有 then 方法,那么状态交 then 方法来决定!

const newPromise = new Promise(( resolve, reject )=> {
  reject("err");
})

new Promise((resolve, reject) => {
  // pending 等待状态
  // resolve(newPromise) // 这里状态由新的 promise 来确定
  resolve({
    then(resolve, reject){
      resolve("") // 状态交给 then 来处理
    }
  })
}).then(data => {
  // resolve 确定
}, e => {
  // reject 失败 驳回 拒绝
})

对象方法

promise 对象中的方法都是挂载到 prototype 上面,包括(then catch finally)

then
  1. 同一个promise then 方法可以调用多次,当 resolve 时,多个 then 方法会同时被执行!

  2. then方法可以有返回值,不管是 promise 还是 普通值,返回结果都是 promise 包装后的值!

  3. 链式调用then时,则 then 中的 参数值,则是上一个then中返回的值!

  4. 如果一个 then 没有返回值的时候,则是 undefind!

  5. 如果返回一个对象,且对象内实现了thenable !

const newPromise = new Promise((resolve, reject) => {
  // reject("err");
  resolve("true")
})

newPromise.then((res) => {
  console.log("多个 then 同时被执行!", res)
})

newPromise.then((res) => {
  console.log("多个 then 同时被执行!", res)
})

newPromise.then((res) => {
  console.log("多个 then 同时被执行!", res)
  return "猜猜我是谁"
}).then(res => {
  console.log("上一个 then 的 返回值res", res)
  return {
    then(resolve, reject) {
      resolve("对象中实现了 thenable ~")
    }
  }
}).then(res => {
  console.log("获取到了对象内thenable中返回到值", res)
})
catch

异常捕获: 当 promisereject 时 或者 promisethrow抛出异常时,会被catch捕获!

const promise2 = new Promise((resolve, reject) => {
  // reject("error");
  throw new Error("捕获到 throw 异常~")
}).then(data => {
  // resolve 确定
}, e => {
  // reject 失败 驳回 拒绝
  // console.log("捕获到 reject", e)
  console.log("捕获到 throw", e)
})

catch 捕获异常时,会从上往下依次捕获异常! 如 捕获promise2 -> 其次then(会返回一个新的promise)

finally

无论是resolvereject 状态下都会执行该方法!

const promise2 = new Promise((resolve, reject) => {
  // reject("error");
  // throw new Error("捕获到 throw 异常~")
  resolve("true")
})

promise2.then(res => {

}).catch(e => {

}).finally(()=>{
  console.log("最终都会执行该代码!")
})

类方法

通过Promise类直接调用方法!

resolve
const promise = Promise.resolve({"aaa": "aaaa"})
reject
const promise = Promise.reject({"aaa": "aaaa"})
all

all方法会接受一个包含多个promise的数组,当所有promise返回结果为resolve时则会执行then,其中有一个promisereject时,则执行catch方法!

const p1 = new Promise((resolve, reject) => {
  resolve("asdadas")
})

const p2 = new Promise((resolve, reject) => {
  resolve("asdadas")
})

const p3 = new Promise((resolve, reject) => {
  resolve("asdadas")
})


Promise.all([p1, p2, p3]).then(res => {
  console.log("p1, p2, p3", res) // p1, p2, p3 ["asdadas","asdadas","asdadas"]
})
allSettled

all方法当其中一个promisereject时,则立刻会进行处理,则其它为resolvepromise无法获取到!

allSettled方法,无论是 resolve 或者是 reject 时都会返回结果!

const p1 = new Promise((resolve, reject) => {
  resolve("asdadas")
})

const p2 = new Promise((resolve, reject) => {
  reject("asdadas")
})

const p3 = new Promise((resolve, reject) => {
  resolve("asdadas")
})


Promise.allSettled([p1, p2, p3]).then(res => {
  /* [
        {"status":"fulfilled","value":"asdadas"},
        {"status":"rejected","reason":"asdadas"},
        {"status":"fulfilled","value":"asdadas"}
   ]*/
  console.log("p1, p2, p3", res) 
}).catch(e => {
  console.log("e", e)
})
race

接受多个promise,返回其中一个先执行完的promise,且结果为resolve fuillled状态的!

但是,如果 promise 其中一个为 reject 时,则 race 也会将reject结果返回出去!

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => { resolve(1111) }, 1000)
})

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

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


Promise.race([p1, p2, p3]).then(res => {
  console.log("p1, res) // 1111
}).catch(e => {
  console.log("e", e)
})
any

race 类似,区别就是,any会等 promise 结果为 resolve 时,才会返回结果!

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => { resolve(1111) }, 4000)
})

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

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


Promise.any([p1, p2, p3]).then(res => {
  console.log("p1", res) // 3333
}).catch(e => {
  console.log("e", e)
})

ES7相关

数组相关

includes

在数组中是否包含指定元素

console.log("aaa", [12, 123, 32, 12].includes(32)) // true

第二个参数为索引值,意思是从什么地方开始

console.log("aaa", [12, 123, 32, 12].includes(32,3)) // false

指数运算方法

const result = Math.pow(3, 3) // 27 3的3次方

console.log("3**3", 3 ** 3) // 27

ES8相关

Object.values

获取对象中所有的 value(值), 如果是数组则返回数组本身,若是字符串时,则拆分生数组!

console.log("object.values 对象", Object.values({ name: "张三", age: 22 })) // 对象中所有的 value  对象 ["张三",22]

console.log("object.values 数组", Object.values([12, 32, 44, 11])) // 返回数组本身 [12,32,44,11]

console.log("object.values 字符串", Object.values("abcs")) // 将字符串拆分成数组 ["a","b","c","s"]

Object.entries

可以将对象形式转换为键值对二维数组!

console.log("Object.entries", Object.entries({ name: "张三", age: 22 }))
// Object.entries [["name","张三"],["age",22]]

console.log("Object.entries 数组", Object.entries(["张三", "李四", "王五"]))
//  数组 [["0","张三"],["1","李四"],["2","王五"]]

console.log("Object.entries 字符串", Object.entries("ajs"))
// [["0","a"],["1","j"],["2","s"]]

padStart & padEnd

字符串填充通过 padStartpadEnd方式来进行前后填充!

第一个参数: 填充当前字符串后生成的字符串的长度。如果此参数小于当前字符串的长度,则将按原样返回当前字符串

console.log("padStart", "Hello world".padStart(15, "*")) // padStart ****Hello world
console.log("padEnd", "Hello world".padEnd(15, "-")) // padEnd Hello world----

getOwnPropertyDescriptors

获取对象中所有属性描述符!

let __obj = {
  name: "张三"
}

console.log("getOwnPropertyDescriptors", Object.getOwnPropertyDescriptors(__obj))

// {"name":{"value":"张三","writable":true,"enumerable":true,"configurable":true}}

Async Function

ES9相关

ES10相关

flat & flatMap

flat 对多维数组进行降维操作,第二个参数可以设置降维的层次!

console.log("flat", [12, 23, 52, [12, 34, 55], [22, 12, [23, 66]]].flat())
// [12,23,52,12,34,55,22,12,[23,66]] 看到会将二维数组进行降维

console.log("flat", [12, 23, 52, [12, 34, 55], [22, 12, [23, 66]]].flat(2))
// [12,23,52,12,34,55,22,12,23,66] 第二个参数设置降维的层次

flatMap多维数组进行降维操作,并且可以通过回调函数,更加灵活的处理数据!

const __arr = ["Hello World", "is it this~"]

const __newArr = __arr.flatMap(item => {
  console.log("flatMap item", item)

  return item.split(" "); // 这里分割后 [["hello","world"]] 二维数组, flatMap 会自动降维
})

console.log("__newArr", __newArr) // __newArr ["Hello","World","is","it","this~"]

fromEntries

fromEntries 可以将二维数组转换为对象格式!

let __obj = {
  name: "张三",
  age: 18
}

console.log(Object.entries(__obj)) // [["name","张三"],["age",18]]
console.log(Object.fromEntries(Object.entries(__obj))) // {"name":"张三","age":18}

查询字符串场景应用

let queryString = "https://www.baidu.com?name=akjdksa&age=18&id=982183921123"

let queryParam = new URLSearchParams(queryString.split("?")[1])

console.log("queryParam", Object.fromEntries(queryParam))
// {"name":"akjdksa","age":"18","id":"982183921123"}

trimStart & trimEnd

空格去除 首部 和 尾部

console.log("trim", "   hello world   ".trimStart().trimEnd())

ES11相关

BigInt

bigint 用来解决超出安全数字范围导致结果显示的问题!

console.log("最大安全数据范围内 MAX_SAFE_INTEGER", Number.MAX_SAFE_INTEGER) // 9007199254740991

以上就是安全整数范围内,当数字大于该数字时,且无法保证数字显示正确问题!

使用 bigInt时 数字后面加 n 标识这个数字为BigInt,否则当类型不一致时,BigInt 和 Number 累加会报错!

let bigInt = 9007199254740991n

// let addResult = 9007199254740991n + 2 // 错误的

let addResult = 9007199254740991n + 2n

let addResult2 = addResult + BigInt(2)

console.log("bigInt", addResult, addResult2)

空值运算符(??)

当某个变量不存在时, 可以设置默认值!

let __a = 0

console.log(" 空值运算符 ", __a ?? "avc") // 0
console.log(" 空值运算符 ", __a || "avc") // avc

可选链运算符(?.)

可选链操作符,常用在对象获取某个属性时,当这个对象不存在或者undefind时,获取该对象属性就会避免报错的问题!

undefined.a
VM509:1 Uncaught TypeError: Cannot read properties of undefined (reading 'a')
    at <anonymous>:1:11
(匿名) @ VM509:1

使用可选链

undefined?.a // 避免报错,且不会调用一个不存在对象的属性

GlobalThis全局对象

兼容 浏览器node 环境下获取全局对象的方法!

console.log("global", globalThis)

ES12相关

监听对象销毁

FinalizationRegistry 通过该类可以实现帮我们监听某个对象的销毁!

let testObj = {}

let final = new FinalizationRegistry(( value ) => {
  console.log("监听到销毁的对象", this, value) // value => "params"
})

final.register(testObj, "params")

testObj = null

WeakRef弱引用

通过 weakRef 接受对象,可以返回一个弱引用对象!

通过deref方法可以获取target对象!

let testObj = { name: "张三" }
let weakObj = new WeakRef(testObj).deref()
console.log(weakObj.name)

逻辑赋值运算

逻辑或运算(||=)

当变量值为0时,返回的是默认值,因为0转换为布尔值为false!

let __message = undefined

// message = message || "default value!"

__message ||= "default value!"

console.log("__message", __message)

逻辑与运算(&&=)

let bbb = {
  name: "张三"
}
bbb &&= bbb.name

逻辑赋值运算(??=)

当变量值为0时,则返回0,而不是返回默认值!

__message ??= "default value!"

对象属性监听

defineProperty

基本使用

Object.defineProperty可以帮我们配置对象属性操作限制,也可以通过 get set 方法实现对对象属性的监听!

defineProperty对原有目标对象属性进行操作和修改!

let __obj1 = {
  name: "张三",
  age: 18
}

Object.defineProperty(__obj1, "name", {
  get() {
    console.log("读取了 name 属性!")
  },
  set(val) {
    console.log(`设置了 name 属性 ${val}`)
  }
})

__obj1.name // 读取属性 触发get方法

__obj1.name = "李四" // 设置属性 触发set方法

监听多个属性

实现多个对对象多个属性进行监听

Object.keys(__obj1).forEach(key => {
  let value = __obj1[key]
  Object.defineProperty(__obj1, key, {
    get() {
      console.log(`读取了 ${key} 属性! ${value}`)
      return value;
    },
    set(val) {
      console.log(`设置了 ${key} 属性 ${value}`)
      value = val;
    },
  })
})

__obj1.name = "李四"
__obj1.name

__obj1.age = 22
__obj1.age

缺点

defineProperty 的设计初衷,是用来设置对象属性描述符的!

1. 无法监听对象中新添加(obj.xxx = xxx)或者删除(delete obj.xx)的属性!

2. 无法监听数组中属性的变化!

proxy

proxy 是一个代理对象,可以将目标对象生成一个代理对象对代理对象属性进行访问修改操作时,proxy内部会通过捕获器进行监听!

在修改和访问对象属性时,是对代理对象本身的一个操作,其代理对象会间接性修改目标原有对象的属性!

基本使用

let __obj1 = {
  name: "张三",
  age: 18
}


let proxy_obj = new Proxy(__obj1, {
  // get 捕获器
  get(target, key) {
    console.log(`get proxy obj ${target} ${key} ${target[key]}`);
    return target[key];
  },
  // set 捕获器
  set(target, key, value) {
    console.log(`set proxy obj ${target} ${key} ${value}`);
    target[key] = value;
  }
});

proxy_obj.name
proxy_obj.age
proxy_obj.age = 22
proxy_obj.age

捕获器

除了 getset 捕获器以外 还有 11 中捕获器!

操作的是代理对象,而不是目标对象!

in操作符监听

"name" in proxy_obj

  // in 操作符监听
  has(target, key) {
    console.log(`监听了 in 操作 ${target[key]} ${key}`)
    return key in target;
  },
delete操作监听

delete proxy_obj.age

// delete 操作符监听
  deleteProperty(target, key) {
    console.log(`监听了 delete 操作 ${target[key]} ${key}`)
    return target[key];
  }

对函数监听

proxy 可以对函数的一个监听,如 apply new操作符!

function __foo() {

}

let proxy_fun = new Proxy(__foo, {
  apply(target, thisArgs, argArr) {
    console.log(`apply 对 函数调用的监听 ${thisArgs} ${argArr}`)
    return target.apply(thisArgs, argArr)
  },
  // 构造函数 对 new 操作符的一个监听
  construct(target, argArr) {
    console.log(`construct 对 new 操作符的监听 ${argArr}`)
    return new target(...argArr);
  }
})

proxy_fun.apply({}, ['name', 'id'])

new proxy_fun("张三");

receiver 参数的作用

receiverproxy代理对象 中捕获器函数中的一个参数,它其实就是proxy的一个实例

receiver 可以改变目标对象this指向,以确保目标对象能够正确指向代理对象 !

let __obj2 = {
  _name: "王五",
  age: 22,
  get name(){
    // 确保 this 指向的是 proxy 而不是 obj ,若是 obj 则代理对象且无法触发监听作用!
    return this._name;
  },
  set name( val ){
    this._name = val;
  }
}

let proxy_obj2 = new Proxy(__obj2, {
  get(target, key, receiver) {
    // Reflect get:  {"name":"王五","age":22} name
    console.log("Reflect get: ", target, key)
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    console.log("Reflect set: ", target, key, value)
    let isSet = Reflect.set(target, key, value, receiver)
    if (isSet) {
      // 设置成功
    }
  },
})

proxy_obj2.name
proxy_obj2.name = "齐六"

Reflect

Object对象的一个规范化,基本Object对象所拥有的方法,Reflect对象上也会有,也是与proxy捕获器对应的方法一一对应!

Reflect 主要用于 Proxyhandler 对象中,它允许你定义如何拦截和修改对象的操作。

let __obj2 = {
  name: "王五",
  age: 22
}

let proxy_obj2 = new Proxy(__obj2, {
  get(target, key) {
    // Reflect get:  {"name":"王五","age":22} name
    console.log("Reflect get: ", target, key)
    return Reflect.get(target, key);
  },
  set(target, key, value) {
    // Reflect set:  {"name":"王五","age":22} name 齐六 
    console.log("Reflect set: ", target, key, value)
    let isSet = Reflect.set(target, key, value)
    if (isSet) {
      // 设置成功
    }
  },
})

proxy_obj2.name
proxy_obj2.name = "齐六"

construct

Reflect.construct 可以将子类的原型替换成父类!

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


function Teacher1() {

}

Teacher1.prototype.teaching = function () {
  console.log("teaching~")
}

// 执行 student 函数 生成 teacher 对象
let teacher1 = Reflect.construct(Student1, ['张三', 12], Teacher1)

console.log(" Reflect.construct", teacher1.__proto__ === Teacher1.prototype) // true

teacher1.teaching()

/*Teacher1*/ {
    "name": "张三",
    "age": 12
}

响应式

所谓的响应式,就是对象属性值发生变化时,会自动触发更新逻辑处理!

vue3 响应式

响应式对象

通过 proxy 将普通对象转换为 代理对象,通过捕捉器监听属性的变化!

// 响应式对象
const refObj = {
  name: "张三", // depend
  age: 22 // depend
}

// 响应式 代理对象
const proxyObj = new Proxy(refObj, {
  // 收集依赖
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver);
  },
  // 派发更新
  set(target, key, value, receiver) {
    // 根据对象 和 对象属性获取依赖对象
    const depend = getDepend(target, key);
    console.log("触发更新 set depend", depend.reactives)
    Reflect.set(target, key, value, receiver);
    // 派发更新
    depend.notify();
  },
})

依赖类封装(Depend)

// 依赖收集对象
class Depend {
  constructor() {
    // 收集响应式函数
    this.reactives = []
  }
  // 依赖收集
  addDepend(fun) {
    this.reactives.push(fun);
    console.log("addDepend this.reactives", this.reactives, fun)
  }
  // 派发通知触发更新
  notify() {
    this.reactives.forEach(fun => {
      // 执行响应式函数
      fun();
    })
  }
}

监听函数

监听函数watchFun,当对象属性发生变化时,watchFun会将响应式函数添加到依赖Deped对象属性集合中!

let activeFunction = null

// 响应式函数封装
function watchFun(fun) {
  activeFunction = fun;
  fun(); // 执行 fun 是为了函数体内访问了代理对象,当执行后会被代理对象捕获器捕获,然后进行依赖收集!
  activeFunction = null;
}

对象依赖结构存储

每一个对象的属性都对应一个依赖函数,当对象属性触发更新时,则会找到对象属性与之对应的依赖对象进行数据更新!

将对象中的每一个属性依赖 对象通过 Map结构进行存储!

随后通过 WeakMap 将对象作为 key 而 存储过属性和依赖的Map作为值进行存储!

之后就可以通过 获取 对象 在获取属性且对应的依赖进行依赖更新!

const depend = weakMap.get(obj).get("name")

let obj = {
  name: "张三", // depend 依赖对象
  age: 22 // depend 依赖对象
}
const objMap = new Map();
objMap.set("name", "nameDependObj");
objMap.set("age", "ageDependObj");



let info = {
  address: "北京市" // depend 依赖对象
}

const infoMap = new Map();
infoMap.set("address", "addressDependObj");

// 将 obj 和 info 对象与之对应的 Map 存储在 weakMap 中,
const weakMap = new WeakMap();
weakMap.set(obj, objMap);
weakMap.set(info, infoMap);

const depend = weakMap.get(obj).get("name")

depend.notify(); // 找到对应对象的对应属性获取依赖对象进行派发更新!

完整的代码

// 响应式对象
/* const refObj = {
  name: "张三", // depend
  age: 22 // depend
} */
let activeFunction = null

// 依赖收集对象
class Depend {
  constructor() {
    // 收集响应式函数
    this.reactives = new Set();
  }

  addDepend(fun) {
    this.reactives.add(fun);
  }
  depend() {
    if (activeFunction) {
      this.reactives.add(activeFunction);
    }
  }
  // 派发通知触发更新
  notify() {
    this.reactives.forEach(fun => {
      // 执行响应式函数
      fun();
    })
  }
}


// 响应式函数封装
function watchFun(fun) {
  activeFunction = fun;
  fun(); // 执行 fun 是为了函数体内访问了代理对象,当执行后会被代理对象捕获器捕获,然后进行依赖收集!
  activeFunction = null;
}

function reactive(obj) {
  return new Proxy(obj, {
    // 收集依赖
    get(target, key, receiver) {
      const depend = getDepend(target, key);
      // 把收集好的响应式函数集中到依赖对象里
      activeFunction && depend.addDepend(activeFunction);
      // depend.depend();
      return Reflect.get(target, key, receiver);
    },
    // 派发更新
    set(target, key, value, receiver) {
      // 根据对象 和 对象属性获取依赖对象
      const depend = getDepend(target, key);
      // console.log("触发更新 set depend", depend.reactives)
      Reflect.set(target, key, value, receiver);
      // 派发更新
      depend.notify();
    },
  })
}

const proxyObj = reactive({
  name: "张三", // depend
  age: 22 // depend
});

const proxyObj2 = reactive({
  address: "北京市", // depend
});

// 响应式 代理对象
/* const proxyObj = new Proxy(refObj, {
  // 收集依赖
  get(target, key, receiver) {
    const depend = getDepend(target, key);
    // 把收集好的响应式函数集中到依赖对象里
    depend.addDepend(activeFunction);
    return Reflect.get(target, key, receiver);
  },
  // 派发更新
  set(target, key, value, receiver) {
    // 根据对象 和 对象属性获取依赖对象
    const depend = getDepend(target, key);
    console.log("触发更新 set depend", depend.reactives)
    Reflect.set(target, key, value, receiver);
    // 派发更新
    depend.notify();
  },
}) */


/* 
  获取依赖函数
  target: 目标对象
  key: 目标对象属性
  desc: watchFun 内部响应式函数 不清楚是哪个对象或者是哪个对象中的属性,由此以来,在触发更新时无法准确去触发依赖更新!
  所以我们需要将 每个对象中的属性 和 对应的 属性依赖更新通过 map形式绑定!
  之后通过 weakMap 将 对象 与 map 进行绑定
 */
const targetMap = new WeakMap();
function getDepend(target, key) {
  // 根据obj 获取 map
  let map = targetMap.get(target);
  // 如果 weakMap中 根据目标对象target 没有获取到 对应map对时候,就新建一个map 存放在 weakMap中
  if (!map) {
    map = new Map();
    targetMap.set(target, map)
  }
  // 如果 Map 中 根据对象属性key 去获取 depend 依赖函数时 没获取到
  let depend = map.get(key)
  if (!depend) {
    // 创建依赖函数
    depend = new Depend();
    // 存放到 Map 结构中
    map.set(key, depend)
  }
  return depend;
}


// 监听响应式函数 将响应式函数添加到 depend 依赖收集类中
watchFun(function () {
  console.log(`name 属性触发了更新  ${proxyObj.name}`)
})

watchFun(function () {
  console.log(`address 属性触发了更新  ${proxyObj2.address}`)
})

proxyObj.name = "李四"
proxyObj2.address = "青岛"

封装proxy

const refObj = {
  name: "张三", // depend
  age: 22 // depend
}
function reactive(obj) {
  return new Proxy(refObj, {
    // 收集依赖
    get(target, key, receiver) {
      const depend = getDepend(target, key);
      // 把收集好的响应式函数集中到依赖对象里
      depend.addDepend(activeFunction);
      return Reflect.get(target, key, receiver);
    },
    // 派发更新
    set(target, key, value, receiver) {
      // 根据对象 和 对象属性获取依赖对象
      const depend = getDepend(target, key);
      console.log("触发更新 set depend", depend.reactives)
      Reflect.set(target, key, value, receiver);
      // 派发更新
      depend.notify();
    },
  })
}
// const proxyObj = reactive(refObj);

const proxyObj = reactive({
  name: "张三", // depend
  age: 22 // depend
});

Vue2 响应式

vue2使用的是Object.defineProperty 来监听对象属性变化!

需要遍历对象所有属性,并且添加gettersetter方法,然后进行依赖收集和派发更新!

function reactive(obj) {
  Object.keys(obj).forEach(key=> {
    let value = obj[key]
    Object.definePropertie(obj, key, {
      get(){
        // 依赖收集
        const depend = getDepend(obj, key);
        depend.depend();
        return value;
      },
      set( val ){
        // 派发更新
        value = val;
        const depend = getDepend(obj, key);
        depend.notify();
      }
    })
  })
  return obj;
}

总结

响应式概念:对象中某一属性值发生变化时,会触发一系列自动更新操作!

vue3中响应式原理:

vue3中的响应式是通过 proxy 代理对象实现的,proxy代理对象内部有13种捕获器,其中get set方法主要做了对象属性上的依赖收集派发更新!

vue2中响应式原理:

vue2中的响应式,主要是通过Object.defineProperty 来实现的,该方法是用来定义对象属性描述的,其中也是通过gettersetter进行对属性依赖收集派发更新!

Object.defineProperty 是有缺陷的,无法对对象新增的属性或删除的属性进行监听,同样数组也是!

JS进程线程

操作系统进程和线程

操作系统中,每打开一个应用,且都是一个进程线程则是进程的一个子集!

每一个进程中,至少会有一个线程在执行,这个线程则称为主线程!

通俗的讲,比如有一个工厂,则工厂有很多车间,而每个车间至少会有一个工人以上在处理自己的任务!

浏览器JS中的进程和线程

1. js 是单线程,意味着代码只能一行一行执行,若在代码中出现比较耗时的逻辑,则会导致线程被阻塞的问题!

2. 浏览器是多进程,且打开一个tab则会多一个进程,每一个进程下会有多个线程,其中包含js线程!

3. js中比较耗时的操作,一般都是浏览器帮忙处理,如网络请求 定时器等!

4. 网络请求,浏览器会单独一个线程向服务端发送请求,请求完毕后,会调用回调函数将结果返回!

事件循环

1. 事件循环: 就是 js 线程 -> 其它线程 -> 事件队列 -> js引擎一个闭环!

2. js线程: 解析代码遇到 setTimeout(()=> {}, 1000), js线程本身不会记时,会将回调函数通过其它线程方式来处理!

3. 其它线程:将每个回调函数放入事件队列中!

4. 事件队列: 特点便是先进先出,当队列中的回调函数处理完毕时,会交给js线程进行执行!

5. js线程:事件队列中取出已经处理完毕的回调函数并执行!

在执行代吗中,会碰到定时器 网络请求 或者是 promise.then 以及 用户操作事件等,这些操作都会导致耗时问题,

比如定时器1000毫秒的时间不可能由js单线程来完成这个记时操作,否则这个线程会阻塞导致其它代码无法执行, 所以浏览器会通过另一个线程来完成记时操作,完成后会通过回调函数的方式,来表示这个记时操作完成了!

同样网络请求也是,网络请求会建立连接 传输数据 断开链接,那么当浏览器请求从服务端获取到数据时,会调用回调函数并表示请求操作已完成!

宏任务和微任务

宏任务: setTimeoutsetIntervalsetImmediateI/OUI rendering

微任务: Promise.thenprocess.nextTickMutationObserver queueMicrotask

在事件队列中,分为宏任务队列 微任务队列,且在执行宏任务队列时,前提微任务队列以被清空的情况下,才能执行!