Published on

ES6 新特性

Authors
  • avatar
    Name
    游戏人生
    Twitter

变量声明

ES6推荐使用 let 声明局部变量,相比之前的 var,let 具有块级作用域,可以避免变量提升和污染全局作用域的问题。

const 则用来声明常量,一旦赋值就不能改变。const 也具有块级作用域,并且必须在声明时就初始化。

特点

  • 不会进行变量提升,在变量声明之前调用都会报错

解构赋值

解构赋值是一种快速从数组或对象中提取数据并赋值给变量的简洁写法。它可以减少代码量,提高可读性,并且支持默认值、嵌套结构、别名等特性。

示例

// 数组解构:按照数组元素位置对应赋值给变量
let [f, g, h] = [1, 2, 3];
console.log(f); // 1
console.log(g); // 2
console.log(h); // 3

// 对象解构:按照对象属性名对应赋值给同名变量(也可以使用别名),可以设置默认值
let {name, age = 20} = {name: "Alice", age: 18};
console.log(name); // Alice
console.log(age); // 18

// 解构赋值的应用场景:交换变量、函数参数、返回值等
let u = 6;
let v = 7;
[u, v] = [v, u]; // 不需要使用临时变量来交换u和v的值
console.log(u); // 7
console.log(v); // 6

特点

  • 在解构对象时,会去查找原型链上的属性和方法

展开运算符

展开运算符可以将一个数组或对象展开为多个元素或属性,用于函数调用、数组合并、对象复制等场景。

const obj = { a: 1, b: 2 };
const obj2 = {...obj};
obj.b = 5;
console.log(obj2);  // {a: 1, b: 2}

箭头函数

箭头函数是一种使用 => 符号定义函数的简洁写法。它可以省略 function 关键字、参数括号、返回值括号等,使得代码更加简洁和清晰。

示例

const getName = () => {
  return 'Lily';
}

特点

  • 不改变 this 的指向,即箭头函数内部的 this 始终指向定义时所在的对象
  • 箭头函数可以使用 call 和 apply,但是会忽略第一个参数
  • 箭头函数不能作为构造函数,且没有原型对象

模板字符串

模板字符串是一种使用反引号 “ 包裹字符串,并且支持插入变量或表达式的新语法 。它可以避免使用 + 号连接字符串和变量,并且支持多行字符串和标签模板等特性。

String.raw 用于获取原始字符串

示例

const str = "c:\workspace\network"
console.log(str);

会打印出

浏览器会将 \ \n 识别为特殊字符。而使用String.raw 之后,就可以正常打印出字符串。

const str = String.raw`c:\workspace\network`
console.log(str);

数组常用操作

  • concat 合并数组并返回新数组,不改变原数组
  • entries 返回数组的迭代器(类似于generator的返回)
  • every 检查数组中的每一项是否符合条件
  • some 检查数组中是否存在某一项符合条件
  • flatMap 相当于先调map,再调flat(1)
  • splice 在给定的索引处删除或替换了一些元素,改变原数组
  • Array.from 通过浅拷贝的方式生成数组
  • reverse 反转数组,改变原数组
  • pop 删除并返回最后一个元素,改变原数组
  • push 添加一个元素并返回新数组的长度
  • shift 删除第一个元素并返回该元素,与pop对应
  • unshift 在数组头部添加一个元素并返回新数组的长度,与push对应
  • sort 正数升序、负数降序,会改变原数组
  • toSorted 与 sort 方法一致,它返回一个新数组,不改变原数组
  • with(index, value) 返回一个全新的数组,将 index 索引处的元素替换为 value
  • toReversed 反转数组,不改变原数组,返回一个新数组
  • toSpliced 返回一个新数组,并在给定的索引处删除或替换了一些元素

Map和Set

Set

Set 是一个数据的列表。它与数组的区别是 Set 里面的数据不允许重复。这里判定重复的方法是绝对相等 ===。 所以 2 和 ‘2’ 可以在同一个 Set 里。

如果存储对象是引用对象,则set会确保内存地址的唯一。所以如果两个变量指向同一个对象。set会只保留一个。

Set 常用方法及属性

  • add 添加项
  • delete 删除项
  • has 查看是否有某项
  • clear 清空 set
  • size 元素的数量

注意

Set 中 NaN 是同一个值,尽管 js 中 NaN !== NaN,但是一个 Set 中只能有一个 NaN。undefined 和 null也只能有一个

示例

const set1 = new Set(); // 初始化
const set2 = new Set([1, 2, 3, 4]); // 返回类数组对象

set1.add('a');
set1.add('b');
set1.has('a'); // true
set1.delete('a');

WeakSet

WeakSet 与set 主要有两个区别:

  • WeakSet 只能存储对象 ,即 内存地址, 存的值如果不是内存地址会报错
  • WeakSet 是弱引用 ,即如果引用类型对象只有在 weakSet 里有调用,则垃圾回收机制运行的时候会回收它

基于这个特点,WeakSet 的参数只能是对象应用(即参数只能是变量名);WeakSet 不能 forEach 遍历,因为保存的对象随时可能消失。

WeakSet 也没有 size 属性。

Map

Map是一种类似于对象的集合,但是它可以使用任意类型的值作为键,包括数字,字符串 ,set ,array, Object 等。

常用方法

  • set(key, value) 插入键值对
  • get(key) 根据 key 值获取数据
  • has(key) 判断是否存在某个 key 值
  • delete(key) 删除 key 值
  • clear() 清空键值对

示例

const map = new Map();

map.set('key1', 'value1');
map.set('key2', 'value2');
map.set('key3', 'value3');

console.log(map.get('key1')); // 输出: value1
console.log(map.size); // 输出: 3

map.forEach((value, key) => {
  console.log(`${key} = ${value}`);
});

WeakMap

WeakMap 与 Map 类似,都是键值对的数据结构。不同点:

  • WeakMap 只接受对象作为键, null 除外。使用普通值会报错
  • WeakMap 的 【键名】指向的对象可以被垃圾回收机制回收

它设计的目的在于,如果需要对对象添加一些文字说明的情况,则可能会导致被描述对象的内存无法被正常回收。

WeakMap 没有 size 属性,不能使用 forEach 循环。

Set 与 Map 区别

  • 数据存储方式

    Map是一种键值对的集合,每个键对应一个值;Set是一组唯一值的集合,不允许重复。

  • 键的类型

    在Map中,键可以是任意数据类型,包括基本数据类型和对象;在Set中,值本身就是键,因此只能存储基本数据类型和对象。

  • 迭代顺序

    Map会按照插入顺序迭代元素,可以通过迭代器访问键值对;Set不保留插入顺序,迭代器只能访问值本身。

  • 大小和性能

    Map可以动态增长并且可以使用size属性获取元素个数;Set的大小也可以通过size属性获取。在大型数据集上,Map通常比Set执行更好。

  • 元素查找

    在Map中,可以通过键来查找对应的值;在Set中,可以直接判断值是否存在

异步

Promise

Promise 是一种用于处理异步操作的对象,它表示一个未来可能完成或失败的事件。

Promise 三种状态:

  • pending(等待)
  • fulfilled(成功)
  • rejected(失败)

状态只能从 pending 变成 fullfilled 或者 rejected,一旦变更后无法再修改状态

Promise 可以通过 then 方法添加成功或失败时执行的回调函数,也可以通过 catch 方法添加失败时执行的回调函数。

Promise 静态方法(接受参数均为任务数组)

  • Promise.all 返回一个任务,任务数据全部成功则成功,任意一个失败则失败
  • Promise.any 返回一个任务,任务数组任意一个成功则成功,全部失败则失败
  • Promise.allSettled 返回一个任务,任务数组全部已决则成功,该任务不会失败
  • Promise.race 返回一个任务,任务数组任意一个已决则已决,状态和其一致

手写 Promise 实现

class MyPromise {
  constructor(executor) {
    this.status = 'pending';
    this.value = '';
    this.reason = '';
    this.callBackResoles = [];
    this.callBackRejects = [];

    const resolve = (value) => {
      if(this.status === 'pending'){
        this.status = 'resolved';
        this.value = value;
        this.callBackResoles.forEach(fn => fn());
      }
    };

    const reject = (reason) => {
      if(this.status === 'pending'){
        this.status = 'rejected';
        this.reason = reason;
        this.callBackRejects.forEach(fn => fn());
      }
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }

  }

  then(onFulfilled, onRejected) {
    if(this.status === 'resolved') {
      onFulfilled(this.value);
    } else if(this.status === 'rejected') {
      onRejected(this.reason);
    } else {
      if(onFulfilled) {
        this.callBackResoles.push(() => onFulfilled(this.value));
      }

      if(onRejected) {
        this.callBackRejects.push(() => onRejected(this.reason));
      }
    }
  }
}

const promise1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('1');
  }, 1000);
});

promise1.then(res => {
  console.log(' premise then :', res);
});

手写 Promise.all

static all(promises) {
  return new MyPromise((resolve, reject) => {
    let count = 0;
    let values = [];
    let reasons = [];

    promises.forEach((promise, index) => {
      promise.then(value => {
        count++;
        values[index] = value;
        checkAllPromise();
      }, reason => {
        reasons[index] = reason;
        checkAllPromise();
      });
    });
    function checkAllPromise() {
      if(count === promises.length) {
        if(count === 0){
          reject(reasons);
        } else {
          resolve(values); // 全部解析成功 返回结果
        }
      }
    }
  });
}

const promise1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('1');
  }, 1000);
});

const promise2 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve("333");
  }, 1000);
});

MyPromise.all([promise1, promise2]).then((res) => {
  console.log(' finished : ', res);
});

Generator 函数

Generator 函数是 ES6 提供的一种异步编程解决方案,Generator 函数内部可以封装多个状态, 因此又可以理解为是一个状态机。generator函数返回值为一个可迭代的函数

创建 Generator 函数

在普通函数后面添加上 *,即可创建 generator 函数。

function* getMsg() {
  console.log("this is generator");
}

函数的执行

generator 函数必须使用 next 执行。

getMsg().next();

yield 关键字

yield实际就是暂缓执行的标示,每执行一次 next(),相当于指针移动到下一个yield位置。以此实现函数的分段执行。

示例

function* getMsg() {
  yield "hello";
  yield "generator";
  return;
}
var fun = getMsg();
console.log(fun.next()); // { value: 'hello', done: false }
console.log(fun.next()); // { value: 'generator', done: false }
console.log(fun.next()); // { value: 'undefined', done: true }

可以通过 […generator()] 快速获得返回函数的执行结果

async/await

Async

async 关键字的函数,是声明异步函数,返回值是 Promise 对象,如果 async 关键字函数返回的不是Promise,会自动用 Promise.resolve() 包装。

async function getName() {
  return 'Lily';
}
getName(); // 返回 Promise 对象

Await

await 是一个运算符,用于等待一个 Promise 对象的结果,只能在异步函数中使用。

const fetchName = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('Lily');
    }, 2000);
  });
};

const getName = async() => {
  const name = await fetchName();
  console.log(name);
};

getName();

处理异常

使用 async / await 一般需要使用 try…catch.. 处理异常。

总结

  • async / await 可以看作自带启动器的 generator 函数的语法

co函数库 是一个 generator 函数的自启动执行器

Proxy

在目标对象之前架设一层”拦截”,外界对该对象的访问,都必须先通过这层拦截 。 Proxy 就是专门为对象设置访问代理器的,无论是读还是写都要经过代理,通过proxy就能轻松监视对象的读写过程。

创建 Proxy

const target={
  name: 'Lily',
  age: 23,
};

const proxy = new Proxy(target, {
  get(){  }, // 监视对象属性的访问
  set(){  } // 监视对象设置属性的过程
});

实例方法

  • get 用于拦截某个属性的读取操作,参数为(目标对象, 属性名, Proxy 实例本身[可选])
  • set 用来拦截某个属性的赋值操作,参数为(目标对象, 属性名, 属性值, Proxy 实例本身[可选])
  • apply 拦截函数的调用、call 和 apply 操作,参数为(目标对象、目标对象的上下文对象(this) 和 目标对象的参数数组)

示例

var handler = {
  apply(target, ctx, args){
    return Reflect.apply(...arguments);
  }
};

装饰器

装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法。 装饰器是一种函数,写成@ + 函数名。它可以放在类和类方法的定义前面。

特点

  • 作用于Class类,类的属性以及方法上
  • 接收三个参数:对象、属性名称、属性描述符(属性描述符通常用于传递给Object.defineProperty)
  • 类似于高阶函数,简化了调用方式,相当于一种语法糖

示例

function SetProto(target) {
  target.prototype.name = 'Lily';
  target.prototype.getAge = function() {
    console.log('Add function');
    return 20;
  };
}

@SetProto
class Test { }

const test = new Test();
console.log(test.name); // Lily

方法装饰器(必须返回descriptor)

// 调用方法前输出日志
function Log(target, name, descriptor) {
  var oldValue = descriptor.value;

  descriptor.value = function() {
    console.log(`Calling function ${name} with ${Object.values(arguments)}, result: `, oldValue.apply(this, arguments));
    return oldValue.apply(this, arguments);
  };

  return descriptor;
}

class Test {
  @Log
  add(a, b) {
    return a + b;
  }
}

let test = new Test();
test.add(1, 2);

模块化

模块化是指将一个大的程序文件,拆分成许多小的文件,然后将小文件组合起来。

模块化的优点

  • 防止命名冲突
  • 代码复用
  • 高可维护性

模块化雏形(IIFE)

立即执行函数的作用是创建一个独立的作用域。这个作用域里面的变量,外面访问不到。

const module = (() => {
  let count = 0;
  return {
    increase: () => ++count;
    reset: () => {
      count = 0;
    }
  }
})();

module.increase();
module.reset();

优缺点

  • 避免作用域命名污染
  • 提升性能(减少了对作用域的查找)
  • 避免全局命名冲突
  • 有利于代码压缩(可以用简单字符串代替)
  • 保存闭包状态
  • 颠倒代码执行顺序

CJS(commonjs)

CommonJs 是 node.js 制定的标准,特征如下:

  • 通过 module + export 去对外暴露接口
  • 通过 require 去调用其他模块

示例

// 引入部分
const dependencyModule1 = require('../dependencyModule1');
const dependencyModule2 = require('../dependencyModule2');

// 核心逻辑
let count = 0;

// dependencyModule1, dependencyModule2……
// 代码块1
const increase = () => ++count;

// 代码块2
const reset = () => {
  // dependencyModule1
  count = 0;
}

// 暴露接口部分
export.increase = increase;
export.reset = reset;
// 等价于
module.exports = {
  increase,
  reset
};

// 引入模块
const { increse, reset } = require('dep.js');
increase();
reset();

暴露的本质是 exports 对象

特点

  • 使用 require 导入时,若导入模块未执行则会先执行,已经执行过的则直接导入
  • requirejs 导入的为对象的值,因此修改导入的对象不会改变原对象
  • require 支持动态导入,但是同步加载,只有导入的模块代码执行完成才能成功导入

AMD

AMD 主要解决了 CommonJS 无法异步导入的问题,通过 define 定义模块,通过 require 和回调的方式导入模块。经典的实现框架 require.js

定义及引入方式

// 通过define来定义一个模块,然后再用require去加载
define(id, [depends], callback)
require([module], callback)

示例

// 定义模块
define('amdModule', ['dependencyModule1', 'dependencyModule2'],      (dependencyModule1, dependencyModule2) => {
  // 业务逻辑
  let count = 0

  // 代码块1
  const increase = () => {
    // dependencyModule2……
    ++count;
  }

  // 代码块2
  const reset = () => {
    // dependencyModule1
    count = 0;
  }
})

// 引入模块
require(['amdModule'], amdModule => {
  amdModule.increse();
})

AMD模块在浏览器端可以通过RequireJS实现,也支持动态导入。

UMD

UMD 则是一种同时兼容AMD、CMD 与 CommonJS的方式,每一个模块都由一个自执行函数包裹,在自执行的函数中通过判断 module、define 等关键字实现兼容处理。

(
  // 目标:一次去定位区分CJS和AMD
  // 1. CJS factory
  // 2. module module.exports
  // 3. define
  typeof module === 'Object'
  && module.exports
  && typeof define !== 'function'
  ? // 是CJS
  factory => module.exports = factory(require, exports, module)
  : // 是AMD
  define
)

优缺点

  • 解决了服务、客户端异步动态依赖的问题
  • 会有引入成本,没有考虑按需加载

CMD

CMD相比AMD模块化规范,其优点在于,在页面初始化时,不会一次性去加载所有的子模块,而是可以在项目运行期间,需要使用到时,再去按需加载。主要应用框架 sea.js

实现方式

// 依赖就近
define('module', (require, exports, module) => {
  let $ = require('map');
  // ...代码块1
  if (xxx) {
    return;
  }

  //加载依赖-异步模式
  const module_async = require.async('./module_async');

  if (xxx) {
    return;
  }

  let depends2 = require('./dependencyModule2');
  // 代码块2
})

优缺点

  • 按需加载,依赖就近 (有点)
  • 依赖打包
  • 扩大了模块内的体积

ESM(ES Module)

ES Module 模式是 ES6 新引入的模块化规范。被认为是大一统的模块化设计规范。

在 html 文件中中引用

<script type="module">
  console.log('this is es module')
</script>

在 NodeJs中引用

在 package.json 中添加 type: module。

基本特性

  • ESM 自动采用严格模式,不管你是否在模块的头部加上 use strict
  • 每个模块都有自己的上下文,都运行在单独的私有作用域中,不污染全局作用域
  • 每一个模块只加载一次(是单例的),若再去加载同目录下同文件,直接从内存中读取
  • ESM 是通过 CORS 的方式请求外部 JS 模块的
  • ESM 的 script 标签加 type=”module”,等同于 defer 属性

导入导出

导入模块使用 import 关键字,导出模块使用 export 关键字。

// 创建一个名为 math.js 的模块文件,并且导出两个函数:add 和 multiply
export function add(x,y) {
  return x + y;
}

export function multiply(x,y) {
  return x * y;
}

// 在另一个文件中,使用 import 关键字导入 math.js 模块
import { add, multiply } from "./math.js"; // 导入指定的函数(需要使用花括号)
console.log(add(1,2)); // 3
console.log(multiply(2,3)); // 6

// 可以使用 as 关键字给导入或导出的函数起别名
import {add as plus, multiply as times} from "./math.js";
console.log(plus(1,2)); // 3
console.log(times(2,3)); // 6

export {add as plus, multiply as times}; // 导出并重命名函数

// 可以使用 * 符号导入或导出所有的函数(需要起一个别名)
import * as math from "./math.js"; // 导入所有函数并起一个别名为math
console.log(math.add(1,2)); // 3
console.log(math.multiply(2,3)); // 6

export * from "./math.js"; // 导出所有函数

// 可以使用 default 关键字指定一个默认的导出(只能有一个,默认导出不需要花括号)
export default function subtract(x,y) {
  return x - y;
}

import subtract from "./math.js"; // 导入默认导出(不需要花括号)
console.log(subtract(5,4)); // 1

动态加载模块

import('/modules/myModule.js')
  .then((module) => {
    // Do something with the module.
  });

export 具名导出时导出的是引用地址,export default 导出时与其他模块化一样是复制值

迭代器和生成器

迭代器是一种遵循迭代协议的对象,可以按照一定的顺序访问一个集合中的元素。迭代器有一个 next 方法,每次调用返回一个包含 valuedone 属性的对象,value 表示当前元素的值,done 表示是否还有更多元素。ES6提供了一种新的语法 for…of 循环,可以方便地遍历迭代器。

生成器是一种特殊的函数,可以返回一个迭代器对象,并且可以在函数体内使用 yield 关键字暂停和恢复执行。生成器使用 function* 关键字声明,并且可以接收参数。(Generator 函数)

创建迭代器

// 创建一个迭代器对象:使用 Symbol.iterator 符号作为属性名,并且返回一个具有 next 方法的对象
let iterator = {
  [Symbol.iterator]() {
    let i = 0;
    return {
      next() {
        if (i < 5) {
          return {value: i++, done: false}; // 返回当前元素的值和状态
        } else {
          return {done: true}; // 返回结束状态
        }
      }
    };
  }
};

// 使用 for...of 循环遍历迭代器对象(不需要调用 next 方法)
for (let value of iterator) {
  console.log(value); // 0 1 2 3 4
}