ES6

84 阅读15分钟

es6 新增了哪些内容

  • letconst
  • 箭头函数
  • 展开运算符
  • 解构赋值
  • symbol类型
  • 对象的简写
  • setmap
  • es6模块化
  • class

1. let const

  • letconst 用来声明变量,替代了传统的 var
  • let 声明的变量是块级作用域,不像 var 会提升到函数顶部。
  • const 声明常量,必须初始化且不可重新赋值。

示例:

let x = 10;
const y = 20;

2. 箭头函数

  • 箭头函数语法更简洁,且不绑定 thisthis 会继承自外层函数。

语法:

const add = (a, b) => a + b;

3. 模板字符串

  • 使用反引号(` `)创建多行字符串,并且可以使用 ${} 来嵌入表达式。

示+

let name = 'Tom';
let greeting = `Hello, ${name}!`;

4. 解构赋值

  • 可以方便地从数组或对象中提取值。

数组解构:

let [a, b] = [1, 2];

对象解构:

let person = { name: 'Tom', age: 30 };
let { name, age } = person;

5. 默认参数

  • 函数参数可以设置默认值。

示例:

function greet(name = 'Stranger') {
  return `Hello, ${name}`;
}

6. 展开运算符(Spread Operator)

  • 使用 ... 来展开数组或对象。

数组展开:

let arr1 = [1, 2];
let arr2 = [...arr1, 3, 4];

对象展开:

let obj1 = { a: 1, b: 2 };
let obj2 = { ...obj1, c: 3 };

7. 类(Class)

  • ES6 引入了类的概念,更加面向对象。

示例:

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

  greet() {
    return `Hello, my name is ${this.name}`;
  }
}

构造函数new的运行方式

在构造函数的this中创建:

  1. this = {}
  2. this.__proto__ = Class.prototype
  3. return this

8. 模块(Modules)

关于 ES6 的模块化系统,以下是更详细的解释与示例:

a. 概述

  • 在 ES6 之前,JavaScript 没有官方的模块系统,开发者通常使用库(如 CommonJSAMD)来管理模块化。
  • ES6 引入了原生模块支持,通过 importexport 关键字实现模块化,允许开发者将代码分成独立的文件和模块,从而更好地管理和复用。

b. 模块的导出 (export)

在模块中使用 export 语句来导出变量、函数或类,以便其他模块可以导入并使用它们。

两种导出方式:

  • 命名导出 (Named Exports):
    可以导出多个变量、函数、类等,每个导出都有自己的名字。示例:
// module1.js
export const name = 'Tom';
export function greet() {
  return `Hello, ${name}!`;
}
export class Person {
  constructor(name) {
    this.name = name;
  }
}
  • 默认导出 (Default Export):
    每个模块只能有一个默认导出,导出时不需要给定名称,导入时可以使用任意名字。示例:
// module2.js
export default function() {
  return 'This is the default export';
}

c. 模块的导入 (import)

使用 import 语句从其他模块中导入变量、函数、类等。可以根据需要选择导入整个模块或模块中的特定部分。

命名导入:
导入指定的导出内容,导入时必须使用模块中定义的相同名字。

示例:

// main.js
import { name, greet } from './module1.js';

console.log(name);  // 输出 'Tom'
console.log(greet());  // 输出 'Hello, Tom!'

别名导入:
可以为导入的内容起一个别名,避免名字冲突或更好地理解代码。

示例:

import { name as userName, greet as sayHello } from './module1.js';

console.log(userName);  // 输出 'Tom'
console.log(sayHello());  // 输出 'Hello, Tom!'

默认导入:
默认导出可以直接使用任何名称来导入。

示例:

// main.js
import defaultFunction from './module2.js';

console.log(defaultFunction());  // 输出 'This is the default export'

d. 全部导入:

使用 * 关键字导入模块中的所有内容,并将其绑定到一个对象上,通过这个对象访问模块的所有导出。

示例:

import * as module1 from './module1.js';

console.log(module1.name);  // 输出 'Tom'
console.log(module1.greet());  // 输出 'Hello, Tom!'

e. 重新导出 (re-export)

有时候一个模块并不需要定义自己的内容,而是将从其他模块导入的内容重新导出。

示例:

// module3.js
export { name, greet } from './module1.js';

// 或者直接全部导出
export * from './module1.js';

这种情况常见于构建中间层模块时,通过重新导出可以组合多个模块的功能,简化导入流程。

f. 动态导入 (import())

ES6 也支持动态导入,即在运行时按需加载模块。动态导入返回一个 Promise,可以与 async/await 结合使用。

示例:

async function loadModule() {
  const module = await import('./module1.js');
  console.log(module.greet());  // 动态加载并调用 greet 函数
}

loadModule();

动态导入特别适用于按需加载脚本,减少初始加载时间,提升性能。

g. 模块的工作原理

  • 严格模式: ES6 模块默认使用严格模式(use strict),因此代码会有更多的限制,比如不能使用未声明的变量等。
  • 模块作用域: 每个模块都有自己的作用域,模块内的变量、函数不会污染全局作用域,也不会与其他模块的变量冲突。
  • 静态分析: importexport 是静态的,不能在条件语句或函数内部使用,编译器可以在编译时确定模块的依赖关系,便于优化。

h. 模块与浏览器

  • 在浏览器中使用模块时,通常通过 <script> 标签引入模块,并使用 type="module" 来告知浏览器这是一个 ES6 模块。

示例:

<script type="module" src="main.js"></script>
  • 通过这种方式引入的模块在浏览器中是异步加载的,因此不会阻塞页面的加载。同时,模块文件中的顶层 this 指向 undefined 而不是全局对象。

i. CommonJS 与 ES6 模块的区别

  • CommonJS 是 Node.js 中的模块系统,使用 requiremodule.exports。而 ES6 模块使用 importexport
  • CommonJS 是运行时加载,而 ES6 模块是编译时加载。这意味着 ES6 模块在静态分析时可以知道导入和导出内容,而 CommonJS 只有在代码执行时才确定。

CommonJS 示例:

// CommonJS 导出
module.exports = {
  name: 'Tom',
  greet: function() {
    return 'Hello!';
  }
};

// CommonJS 导入
const { name, greet } = require('./module');

总结来说,ES6 模块系统提供了现代化的模块化开发方式,解决了早期 JavaScript 没有原生模块支持的问题,使得代码的复用性和组织性大大提升。


9. Promise回调函数

  • Promise 用于处理异步操作避免回掉地狱,使得代码更加简洁且易于维护。

a. 基本使用

// resolve 函数 解决,完成的意思
// reject 函数 拒绝,失败的意思
let promise = new Promise((resolve, reject) => {
  if (resolve) {
    resolve('ok');
  } else {
    reject('err');
  }
}); // return 默认返回 Promise 对象
// then()   ---  Promise 中的异步操作 成功 以后执行 Promise 的 then 方法
// catch()  ---  Promise 中的异步操作 失败 以后执行 Promise 的 catch 方法
promise.then(result => console.log(result)); // ok
promise.catch(error => console.log(error));  // err
new Promise((resolve, reject) => {
  if (resolve) {
    resolve('ok');
  } else {
    reject('err');
  }
}).then(result => console.log(result)) // ok
  .catch(error => console.log(error)); // err
new Promise(function (resolve, reject) {
  console.log(2222);
  // ***这个参数函数里面可以写异步操作
  setTimeout(() => {
    //异步操作成功了 调用resolve  ,promise对象就知道了,promise内部异步操作成功了
    //****resolve(实参) 可以传递给then 里面函数的形参--
    console.log("延时器成功 红");

    resolve(99999);
    //***异步操作失败了,调用reject,然后 p.catch就会执行,并且 reject的实参,会给catch的形参
    // reject(11111);
  }, 2000);
})
  .then(function (r) {
    console.log("p,then 1 ");

    // *********多个异步操作有顺序
    // new Promise(异步1).then(new Prmise(异步2)).then(new Prmise(异步3))、.then(new Prmise(异步4)).then
    //*****必须在上一个then中 返回一个新的promise对象,这个promise对象成功以后,就会执行下一个then
    return new Promise(function (r, j) {
      setTimeout(() => {
        console.log("绿");
        r();
      }, 2000);
    });
    //   then的返回值就是当前的promise对象
  })
  .then(function () {
    console.log("p,then 2 ");

    setTimeout(() => {
      console.log("黄");
    }, 2000);
  });

b. Promise的其它方法

实例方法

then promise 成功以后

catch promise 失败以后

finally 在家吗后台改变后执行

静态方法

Promise.all([p1,p2,p3])
  • 作用

全部,数组全部成功,那么Promise成功,有一个失败那么Promise就失败

  • 参数

数组,数组要放一组Promise对象

Promise.race([p1,p2,p3])
  • 作用

竞赛,数组谁最快,Promise的结果就是谁

  • 参数

数组,数组要放一组Promise对象

Promise.any()跟Promise.race()方法很像,只有一点不同,就是Promise.any()不会因为某个 Promise 变成rejected状态而结束,必须等到所有参数 Promise 变成rejected状态才会结束。

Promise.resolve()

创建一个处于成功状态的Promise

Promise.rereject()

创建一个处于失败状态的Promise


c. async/await

  • 结合 Promise 使用,async 函数返回一个 Promise,await 用于等待异步操作完成。

示例:

async function fetchData() {
  let response = await fetch('https://api.example.com/data');
  let data = await response.json();
  console.log(data);
}

这些是 ES6 中最常用和最重要的特性,它们大大简化了 JavaScript 的开发体验,并使代码更加简洁和易于维护


10. this

a. 全局环境中的 this

  • 在全局环境中(非严格模式下),this 指向全局对象,在浏览器中就是 window
  • 在严格模式下(strict mode),全局环境中的 thisundefined

示例:

console.log(this);  // 浏览器中指向 window 对象

严格模式下:

'use strict';

console.log(this);  // undefined

b. 函数中的 this

  • 在非严格模式下,函数中的 this 指向调用该函数的对象。如果函数是直接调用的,那么 this 仍然指向全局对象(或 undefined
    在严格模式下)。
  • 在严格模式下,函数中的 this 默认为 undefined,如果函数被作为对象的方法调用,那么 this 指向调用该方法的对象。

非严格模式下:

function showThis() {
 console.log(this);
}

showThis();  // 非严格模式下,this 指向 window

严格模式下:

'use strict';

function showThis() {
 console.log(this);
}

showThis();  // undefined

作为对象方法调用:

const obj = {
 name: 'Tom',
 showThis: function () {
     console.log(this);
 }
};
obj.showThis();  // this 指向 obj 对象

c. 构造函数中的 this

  • 当使用 new 关键字调用构造函数时,this 指向新创建的对象。
  • 构造函数的主要作用是初始化新对象的属性。

示例:

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

const tom = new Person('Tom');
console.log(tom.name);  // 输出 'Tom'

在这个例子中,this 指向通过 new 创建的对象 tom

d. 箭头函数中的 this

  • 箭头函数不绑定自己的 this,它的 this 继承自外层的上下文(即定义箭头函数时所在的环境)。
  • 这意味着箭头函数中的 this 永远不会动态改变。

示例:

const obj = {
 name: 'Tom',
 showThis: function () {
     const arrowFunc = () => {
         console.log(this);  // this 继承自外层的 showThis 方法
     };
     arrowFunc();
 }
};
obj.showThis();  // this 指向 obj 对象

let fn = () => {
  console.log(this)
}
fn() // window

在这个例子中,arrowFuncthis 继承了 showThis 中的 this,即 obj

e. 方法中的 this

  • 当函数作为对象的方法被调用时,this 指向调用该方法的对象。

示例:

const obj = {
 name: 'Tom',
 greet: function () {
     console.log(this.name);
 }
};

obj.greet();  // this 指向 obj,输出 'Tom'

在这个例子中,this 指向调用 greet 方法的对象 obj

f. 事件处理函数中的 this

  • 在传统的 DOM 事件处理程序中,this 通常指向触发事件的 DOM 元素。

示例:

const button = document.querySelector('button');
button.addEventListener('click', function () {
 console.log(this);  // this 指向被点击的 button 元素
});
  • 如果使用箭头函数,this 将继承自外层上下文,而不再指向 DOM 元素。

示例:

button.addEventListener('click', (e) => {
 console.log(this);  // this 继承自外层上下文,通常是 window
});

g. call apply bind 改变 this 的指向

  • JavaScript 提供了 callapplybind 三个方法来显式地绑定 this 的指向。(无法改变箭头函数的this

call 方法:

  • call 立即调用函数,并将 this 绑定到第一个参数上。

示例:

function greet() {
 console.log(`Hello, my name is ${this.name}`);
}

const person = {name: 'Tom'};
greet.call(person);  // this 指向 person,输出 'Hello, my name is Tom'

apply 方法:

  • applycall 类似,但传递参数时使用数组。

示例:

function greet(greeting) {
 console.log(`${greeting}, my name is ${this.name}`);
}

const person = {name: 'Tom'};
greet.apply(person, ['Hi']);  // this 指向 person,输出 'Hi, my name is Tom'

bind 方法:

  • bind 返回一个新函数,并将 this 绑定到指定的对象。不同于 callapplybind 不会立即执行函数。

示例:

function greet(greeting) {
 console.log(`${greeting}, my name is ${this.name}`);
}

const person = {name: 'Tom'};
const boundGreet = greet.bind(person);
boundGreet('Hello');  // this 指向 person,输出 'Hello, my name is Tom'

h. this 在类中的使用

  • 在类的实例方法中,this 指向该类的实例对象。

示例:

class Person {
 constructor(name) {
     this.name = name;
 }

 greet() {
     console.log(`Hello, my name is ${this.name}`);
 }
}

const tom = new Person('Tom');
tom.greet();  // this 指向 tom,输出 'Hello, my name is Tom'
  • 在类的静态方法中,this 指向类本身,而不是类的实例。

示例:

class Person {
 static greet() {
     console.log(this);  // this 指向类 Person 而非实例
 }
}

Person.greet();  // 输出 class Person

i. this 在闭包中的问题

  • 当一个函数内部包含另一个函数时,内部函数的 this 不会自动继承外部函数的 this。这常常导致一些 this 指向问题。

示例:

const obj = {
  name: 'Tom',
  greet: function () {
    function innerFunc() {
      console.log(this);  // 在非严格模式下,this 指向全局对象
    }

    innerFunc();
  }
};

obj.greet();  // this 指向全局对象(非严格模式下),不是 obj

解决方法:

  • 可以使用箭头函数,或者将外层函数的 this 存储在变量中。

使用箭头函数:

const obj = {
  name: 'Tom',
  greet: function () {
    const innerFunc = () => {
      console.log(this);  // this 继承自外层的 greet 函数,指向 obj
    };
    innerFunc();
  }
};

obj.greet();  // this 指向 obj

存储 this

const obj = {
  name: 'Tom',
  greet: function () {
    const self = this;

    function innerFunc() {
      console.log(self);  // 使用 self,指向 obj
    }

    innerFunc();
  }
};

obj.greet();  // self 指向 obj

总结:

  • 全局环境: 在非严格模式下,this 指向全局对象;在严格模式下为 undefined
  • 函数调用: 直接调用函数,this 在非严格模式下指向全局对象,在严格模式下为 undefined
  • 对象方法: 方法调用时,this 指向调用该方法的对象。
  • 构造函数: this 指向新创建的对象。
  • 箭头函数: this 继承自定义时

11. 面向对象

面向对象编程(Object-Oriented Programming,简称 OOP)
是一种通过对象来组织代码的编程范式。它通过模拟现实世界中的实体和行为,增强程序的灵活性、可维护性和复用性。在面向对象编程中,对象是程序的核心构建块。对象通过类定义,其行为通过方法表示,数据通过属性存储。

a. 概述

  • 类:众多对象共有属性和方法的一个抽象集合
  • 面向对的3个特点:封装、继承、多态
    • 封装:
      1. 把数据(属性)和数据的处理(方法)集中在一起
      2. 封装是将对象的属性和方法隐藏起来,只允许通过对象的方法访问它们。这种方式限制了外部对对象数据的直接访问,确保了对象的内部状态的安全性和完整性。
      3. JavaScript 中,可以通过闭包或将属性设为私有来实现封装。
    • 继承:
      1. 一个类型的实例能访问另外一个类型里的属性和方法
      2. 继承允许一个类(子类)从另一个类(父类)继承属性和方法,从而重用已有的代码,并根据需要进行扩展和定制。
      3. JavaScript 中,继承使用 extends 关键字实现,子类可以通过 super() 调用父类的构造函数。
    • 多态:
      1. 同一个方法作用于不同对象有不同的结果
      2. 多态性允许不同类的对象通过相同的接口调用不同的方法。不同的子类可以拥有自己的方法实现,而父类定义的接口可以被复写。
      3. JavaScript 中,多态通常通过方法的重写实现,即子类可以重写父类中的方法。

b. 对象

JavaScript 中,实例对象是通过构造函数或者类创建的具体对象实例。每一个实例对象都拥有构造函数或类的属性和方法。

每个对象都有自己的属性和方法,同时可以从它的原型链中继承属性和方法。

ⅰ. 创建对象的方式

1. ES6 引入 class 之前创建对象的方法
1. 字面量
let Student = {
    name: "张三",
    age: 20,
    sayHello() {
    },
};
2. 工厂函数

工厂函数:指返回对象的普通函数,而不是使用 new 关键字来创建实例的构造函数。

function Student(name, age) {
    let obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.sayHello = function () {
        console.log(this.name);
    };
    return obj;
}

优点:减少代码冗余

弊端:无法区分具体的类型

3. 构造函数

当一个函数和 new 运算符一起使用时,那么这个函数称为构造函数,构造函数可以看成是一个类,需要在构造函数里把共有属性和方法定义出来,这些属性和方法能被实例对象访问到。

function Person(name, age) {
    this.name = name; //this指向实例  实例属性
    this.age = age;
    this.sayHello = function () {
        //实例方法
        console.log(this.name); //this指向实例
    };
}

let zhang = new Person('张', 18);

好处:减少代码冗余,区分具体类型

缺点:每实例化一个对象,所有方法都会重新创建一遍,就增加内存的消耗

4. 借助原型对象

每一个函数都有一个内部属性 prototype ,这个属性是一个对象,这个对象里的属性和方法能被实例所共享,称这个对象为原型对象。

Person.prototype.name = '张三'
Person.prototype.age = 18
Person.prototype.sayHello = function () {
  console.log(this.name)
}

优点:方法可以共享

缺点:属性写死了

5. 构造函数、原型对象组合式

属性放到构造函数里,方法放到原型对象上

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

Person.prototype.sayHello = function () {
  console.log(this.name); //this指向实例对象
};
2. ES6 使用 class 创建对象

class 是 ES6 引入的语法糖,用于定义类并通过 new 关键字创建实例对象。虽然 class 看起来像面向对象编程中的类,但在 JavaScript 中依然是基于原型链的。

class Person {
    constructor(name, age) {//一个构造函数的原型对象上的constructor指向这个构造函数
        //构造函数
        this.name = name;
        this.age = age;
    }

    sayHello() {
        console.log(this.name);
    }
}

12. 闭包与继承

a. 闭包

闭包: 在一个函数外部访问函数内部的局部变量

一个局部变量的函数调用完毕之后会销毁

由于闭包的存在,函数的局部变量不会销毁会常驻内存

function fn() {
    var a = 10;
    return function () {
        a++;
        console.log(a)
    }
}
let f = fn();
fn() // 11
fn() // 12

闭包的好处: 减少全局空间污染,减少命名冲突

闭包的缺点: 一些局部变量可以通过闭包访问,但是访问过后不会销毁,出现一块内存既不能清也不能用(内存泄露)

b. 继承

继承: 一个类型的实例能够访问另一个类型里的属性和方法

一个类型的实例能够访问该类型内部定义的属性和方法,原型 是正在创建类(构造函数)时,添加了实例属性和属性方法

ⅰ. 构造函数继承

function Person(name, age) {    
  this.name = name;    
  this.age = age;    
  this.sayHello = function () {        
    console.log(this.name);    
  };
}
function Male(name, age) {    
  Person.call(this, name, age);    
  this.houjie = true;
}
function Female(name, age) {    
  Person.call(this, name, age);    
  this.getBaby = function () {        
    console.log("good baby");    
  };
}

ⅱ. 原型继承

没一个实例对象内部都有一个__proto__属性,这个指向的够早函数的原型对象prototype

实例去访问圣属性和方法按照一下是顺序进行,先看自身有没有,会在__proto__指向的那个原型对象上找,如果没有找到,会在原型对象的__proto__指向的那个原型对象上找,以此类推,直到找到Object.prototype为止,这种沿着__proto__指向顺序进行查找形成的一个练市结构,成为原型链

function Person() {};
Person.prototype.name = "张三";
Person.prototype.age = 20;
Person.prototype.sayHello = function () {    
  console.log(this.name);
};
/*  
let p1 = new Person();
console.log(p1.__proto__ === Person.prototype);
console.log(p1.name); 

//实例去访问属性和方法按照以下顺序进行,先看自身有没有,如果自身没有,会在__proto__指向的那个原型对象上找,如果没找到,会在原型对象的__proto__指向的那个原型对象上找,以此类推,直到找到Object.prototype为止,这种沿着__proto__指向顺序进行查找形成的一个链式结构,称为原型链

console.log(Person.prototype.__proto__);
console.log(p1.a);
console.log(p1.toString); 
*/
function Male() {}
Male.prototype = new Person(); 
//让一个类型的原型对象变成另外一个类型的实例
let m1 = new Male();
m1.sayHello();
console.log(m1.__proto__.__proto__ === Person.prototype);
console.log(m1.__proto__.__proto__.__proto__ === Object.prototype);
console.log(m1.__proto__.__proto__.__proto__.__proto__); 
//null//console.log(Array.prototype);
//请设计两个方法,用于取数组的最大值和最小值,这个两个方法能被任意数组实例访问
//getMin  
getMaxArray.prototype.getMin = function (arr) {    
  return Math.min(...this);
};
Array.prototype.getMax = function (arr) {    
  return Math.max(...this);
};
let arr1 = [11, 7, 22];
console.log(arr1.getMin());

ⅲ. 混合继承

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

Person.prototype.sayHello = function () {
    console.log(this.name);
};

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

//Male.prototype = Person.prototype; //地址
for (let i in Person.prototype) {
    Male.prototype[i] = Person.prototype[i];
}
Male.prototype.fn1 = function () {
    console.log("这时子类的方法");
};
let m1 = new Male("赵六", 20);
console.log(m1.name);
m1.sayHello();
m1.fn1();
let p1 = new Person("赵家", 100);
p1.fn1();

ⅳ. 寄生式混合继承

Object.create()

//Object.create()
let obj1 = {a: 1};
let obj2 = Object.create(obj1);
console.log(obj2.__proto__ === obj1);

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

Person.prototype.sayHello = function () {
    console.log(this.name);
};

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

Male.prototype = Object.create(Person.prototype);
Male.prototype.constructor = Male;
let m1 = new Male("zhaojin", 20);
console.log(m1.__proto__.__proto__ === Person.prototype);
// 现在有一个实例,但是不明确这个实例到底是哪个类的实例console.log(m1.__proto__.constructor);

ⅴ. 类的继承

class Male extends Person {
    constructor(name, age) {
        super(name, age);
        //super指的父类的构造函数
        this.a = 1;
    }

    sayHi() {
        console.log("hi");
        super.sayHello();
        //super指父类的原型对象
    }

    static foo() {
        super.fn();
        //super指向父类
    }
}

let m1 = new Male("赵子龙", 20);
m1.sayHello();
m1.sayHi();
Male.foo();

13. 设计模式

设计模式(Design pattern) 代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

a. 单例模式

单例模式:它确保一个类只有一个实例,并提供了一个全局访问弄点来访问该实例

类提供一个静态方法,每次调用得到相同对象()
在静态方法内部,确保对象只被创造一次,下次在调用静态方法直接返回第一次创建的对象

class Dog(){
  constructor(){}
  bb(){}
}
let d3 = Dog.bb();
let d4 = Dog.bb();
console.log(d3);
// d3和d4都是一条狗,但是是同一条狗
console.log(d3 === d4) // false

ⅰ. 静态类方法(全局变量)

let obj = false; // 通过定义一个全局变量实现判断类是否被调用

class Dog(){
  static shareInstance() {
    if(!obj) {
      obj = new Dog();
    }
    return obj;
  }
}

let d3 = Dog.shareInstance();
let d4 = Dog.shareInstance();
d3.bb()
console.log(d4)
console.log(d3 === d4)

ⅱ. 静态类方法(静态属性)

class Dog(){
  static obj = false;

  static shareInstance() {
    if(!obj) {
      obj = new Dog();
    }
    return obj;
  }
}

let d3 = Dog.shareInstance();
let d4 = Dog.shareInstance();
d3.bb()
console.log(d4)
console.log(d3 === d4)

ⅲ. 闭包

class Dog(){}
let shareInstance = (function () {
  let obj = false;
  retuen function(){
    if(!obj){
      obj = new Dog()
    }
    return obj
  }
})()

b. 观察者模式

观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。

let btn = document.querySelector("button");

//dom元素上有一个对象  {click:[fn1,f2]}
//当我们调用添加观察的方法时候,会在内部对象上添加一个属性(事件名),属性值是一个数组,存所有的回调函数
//1、添加观察者
//作为参数的函数-一般叫回调函数

//如果是第一次调用,给这内部对象,增加click属性,属性值是一个数组,数组方法第一个回调函数
//dom元素上有一个对象  {click:[fn1]}
btn.addEventListener("click", function () {
  console.log("click1 回调函数");
});

let fn2 = function () {
  console.log("click2 回调函数");
};

//作为参数的函数-传入是函数名--函数的地址
//如果不是第一次调用,找到对应的事件的数组,添加到元素
btn.addEventListener("click", fn2);

// 2、移出观察者
// 这样可以移除
//btn.removeEventListener("click", fn2);
// 下面的写法为什么不能移除?
// 函数在js中也是一个对象,每次使用function 定义函数的时候,都是新创建的了一个函数对象,等同于 new Function(),下面的函数和上面定义的函数地址一定不一样
//   btn.removeEventListener("click", function () {
//     console.log("click1 回调函数");
//   });

//   3、 通知所有的观察者
//遍历对应事件 的数组,调用数组中所有函数
setTimeout(() => {
  btn.dispatchEvent(new Event("click"));
}, 2000);
//事件总线--一个观察者模式的实现,用来处理 事件监听,移除,触发(统一管理事件)
class EventBus {
  obj = {};
  //dom元素上有一个对象  {click:[fn1,f2]}
  //当我们调用添加观察的方法时候,会在内部对象上添加一个属性(事件名),属性值是一个数组,存所有的回调函数

  //  如果是第一次调用,给这内部对象,增加click属性,属性值是一个数组,数组方法第一个回调函数
  //    如果不是第一次调用,找到对应的事件的数组,添加到元素
  addEventListenter(eventName, eventCallback) {
    //判断是否是 对应的事件,是否在 obj上,
    //   如果不在,添加一个事件名作为属性名,属性值是一个数组,数组中存回调函数
    //   ?怎么判断一个对象上是否存在某一个属性
    //判断变量的值,是否作为属性名存在于这个对象上
    this.obj[eventName];
    if (this.obj[eventName] === undefined) {
      //如果不在,添加一个事件名作为属性名,属性值是一个数组,数组中存回调函数
      this.obj[eventName] = [eventCallback];
    } else {
      // 如果在,找到对应的事件的数组,添加到元素
      this.obj[eventName].push(eventCallback);
    }
    console.log(this.obj);
  }
  removeEventListenter(eventName, eventCallback) {
    //   移除知道这个事件,对应数组,删除数组中对应元素
    let arr = this.obj[eventName];
    //找到函数在数组中下标,删除对应下标的元素
    let index = arr.indexOf(eventCallback);
    arr.splice(index, 1);
    console.log(this.obj);
  }
  dispatchEvent(eventName, event) {
    //   找到这个事件,对应数组,遍历所有元素,调用元素
    let arr = this.obj[eventName];
    arr.forEach((v) => {
      //调用回调函数,传入实参
      v(event);
    });
  }
}
let book = new EventBus();

book.addEventListenter("挪威的森林", function (e) {
  console.log("留了电话---一旦书到了,就通知我 1");
  console.log(e);
});
let fn2 = function (e) {
  console.log("留了电话---一旦书到了,就通知我 2 ");
  console.log(e);
};
book.addEventListenter("挪威的森林", fn2);

//   book.removeEventListenter("挪威的森林", fn2);

setTimeout(() => {
  //5天后书到了,书店通知所有的观察值
  book.dispatchEvent("挪威的森林", {
    title: "挪威的森林",
    content: "xxxxx",
    author: "村上春树",
  });
}, 5000);
</script>

  <script src="https://unpkg.com/mitt/dist/mitt.umd.js"></script>
  <script>
  // https://github.com/developit/mitt
  const emitter = mitt();

// listen to an event
emitter.on("foo", (e) => console.log("foo", e));
setTimeout(() => {
  // fire an event
  emitter.emit("foo", { a: "b" });
}, 1000);

14. 深拷贝和浅拷贝

a. 浅拷贝

浅拷贝是指复制对象的第一层属性,如果属性是引用类型(如数组或对象),则只复制引用,而不复制实际的值。因此,源对象和拷贝对象中的引用属性指向同一个内存地址。

ⅰ. *展开运算符

let obj = {
  name: "张三",
  age: 28,
  child: {
    name: "张三锋",
    age: 1,
  },
}
let obj2 = {...obj};

ⅱ. 使用 Object.assign():

const original = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, original);

shallowCopy.b.c = 3; // 修改浅拷贝中的 b.c
console.log(original.b.c); // 输出 3,原对象也被影响

ⅲ. 自己封装一个拷贝函数

封装一个拷贝函数,遍历对象,把原对象里面的内容都都添加到新对象中

function clone(value) {
  // 遍历对象,把原对象里面的内容都都添加到新对象中
  let newObj = {};
  for (let key in value) {
    //key 属性名
    //v 属性值
    let v = value[key];
    newObj[key] = v;
  }
  //返回新对象
  return newObj;
}

ⅳ. 第三方库_.clone()方法

使用第三方庫lodash,使用_.clone()方法

// 引入js文件
let obj4 = _.clone(obj);

b. 深拷贝

深拷贝是指复制对象及其所有嵌套的对象,确保源对象和拷贝对象之间没有共享的引用。因此,修改拷贝对象不会影响原对象。

ⅰ. 通过第三方库_.cloneDeep()方法

使用 Lodash 的 cloneDeep 方法进行递归拷贝非常简单。首先确保你已安装 Lodash:

npm install lodash

然后你可以这样使用 cloneDeep

const _ = require('lodash');

const original = { a: 1, b: { c: 2 } };
const deepCopy = _.cloneDeep(original);

deepCopy.b.c = 3; // 修改深拷贝中的 b.c
console.log(original.b.c); // 输出 2,原对象不受影响

cloneDeep 会自动处理嵌套对象和数组,确保深拷贝的效果。还有其他相关问题吗?

ⅱ. 使用 JSON.parseJSON.stringify

const original = { a: 1, b: { c: 2 } };
const deepCopy = JSON.parse(JSON.stringify(original));

deepCopy.b.c = 3; // 修改深拷贝中的 b.c
console.log(original.b.c); // 输出 2,原对象不受影响

ⅲ. 通过递归深拷贝

function deepClone(obj) {
    if (obj === null || typeof obj !== 'object') {
        return obj; // 基础类型直接返回
    }

    if (Array.isArray(obj)) {
        return obj.map(deepClone); // 递归处理数组
    }

    const clonedObj = {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            clonedObj[key] = deepClone(obj[key]); // 递归处理对象属性
        }
    }
    return clonedObj;
}

const original = { a: 1, b: { c: 2 } };
const deepCopy = deepClone(original);

deepCopy.b.c = 3; // 修改深拷贝中的 b.c
console.log(original.b.c); // 输出 2,原对象不受影响