6-基础-ESX

246 阅读10分钟

这一篇代码量偏重一些,从ES6到TypeScript是日常工作中陪伴开发者最多的,开发能力很大程度上指的也是JS能力,所以此处也考验以下动手能力。

面向对象的 ECMAScript 语法标准:
化繁为简:
在 ECMAScript 2022(ES13) 新增的 Array.prototype.at()、Object.hasOwn()、

一步访问尾元素

访问尾元素的方式的常见操作有通过指定索引为 length-1 的方式、通过 slice() 切片的方式、通过 pop() 弹出元素的方式、还有通过 reverse() 翻转数组后再获取的方式。这些访问的方式有一个共同特点就是都需要在正式访问前做一步前置工作,使用 pop() 还破坏了原数组,这都不是访问尾元素最好的方式。

方式前置工作
length-1获取数组总长度
slice()切片后结果为数组
pop()剔除原数组的尾元素
reverse()将数组顺序翻转

推荐方案

在 ECMAScript 2022(ES13) 加入的 Array.prototype.at() 函数支持接收一个整数值来获取该索引位置的元素,当传入一个负整数的时候将从数组的最后一项开始倒数。

案例讲解

访问尾元素的案例在实际的项目中也有很多,较为常见的就是获取一个图片地址中的文件名(如:案例讲解.png),那么下面就先来看一下常见的操作是如何完成这个小要求的吧;

先通过下面的代码来得到一个数组:

let url = "https://files.aliyuncs.com/picgo/202302141044787.png";
let fragments = url.split('/');
console.log(fragments);
  1. 相对索引访问:
let length = fragments.length;
let lastEle = fragments[length - 1];
console.log(lastEle); // 输出:202302141044787.png
  1. 切片后访问:
let fragment = fragments.slice(-1);
let lastEle = fragment[0];
console.log(lastEle); // 输出:202302141044787.png
  1. 弹出尾元素:
let lastEle = fragments.pop();
console.log(lastEle); // 输出:202302141044787.png
  1. 翻转数组顺序后访问:
let fragment = fragments.reverse();
let lastEle = fragment[0];
console.log(lastEle); // 输出:202302141044787.png

目的仅仅为了获取一个尾元素,非必要的操作就不加入到代码中了,下面看一下 at() 函数直截了当的获取尾元素:

let lastEle = fragments.at(-1);
console.log(lastEle); // 输出:202302141044787.png

扩展知识

at() 函数并非是数组特有的,它适用于任何具有 length 属性和以整数为键的属性构成的对象。

// 代码摘录:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/at
const arrayLike = {
  length: 2,
  0: "a",
  1: "b",
};
console.log(Array.prototype.at.call(arrayLike, -1)); // "b"

函数副作用

概念:副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互
简单理解:导致这个函数相同的输入,会产生不一样的输出,就是副作用。
由此而引出纯函数以及函数副作用相关的几个概念。

Pure Function

函数式编程中有一个非常重要的概念叫纯函数(Pure Function),JavaScript符合函数式编程的范式,所以也就有纯函数的概念。概念:纯函数就是一个函数的返回结果只依赖于它的参数,并且在执行过程中没有副作用,我们就把这个函数叫做纯函数。概念过于官方,关于纯函数可以简单理解为:

  • 确定的输入一定会产生确定的输出
  • 函数在执行过程中,不能产生副作用

即纯函数与外界交换数据只有一个渠道——参数和返回值,输入输出数据流全是显式(Explicit)。

Impure Function

概念:当给定相同的输入时,不纯函数可能不会返回一致的结果,并且它们可能会产生超出函数范围的影响。
在非纯函数中输入输出数据流全是隐式(Implicit)的,函数通过参数和返回值以外的渠道,和外界进行数据交换。比如读取/修改全局变量,都叫作以隐式的方式和外界进行数据交换。

Referential Transparent

引用透明的概念与函数的副作用相关,且受其影响,如果程序中两个相同值得表达式能在该程序的任何地方互相替换,而不影响程序的动作,那么该程序就具有引用透明性。它的优点是比非引用透明的语言的语义更容易理解,不那么晦涩,纯函数式语言没有变量,所以它们都具有引用透明性。

要想保证函数无副作用这项特性,需要保持良好的编程习惯:

  • 函数入口使用参数运算而不修改它
  • 函数内不修改函数外的变量
  • 运算结果通过函数返回给外部

高阶函数

概念:高阶函数是指以函数作为参数的函数,并且可以将函数作为结果返回的函数。
简单理解:参数和返回值有一个是函数类型的,就是高阶函数。

// 计算两个数值的不同算法
function calculate(x, y, f) {
    return f(x) + f(y);
}
// 绝对值求和
const result = calculate(-5, 6, Math.abs); 

console.log(result);// 11

在ES6中有很多高阶函数非常好用,比如filter、map、reduce这些在开发中都是使用频率很高的函数。reduce也是聚合函数,如下:计算数组求和:

const list = [1,2,3,4,5,6,7,8,9,10];
const total = list.reduce((val, oldVal) => val + oldVal);
console.log(total);// 55

运算符

可选链(?.)

可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。

let obj = {
   name:'test',
   sayHello:function(){
     console.log('HELLO');
   }
}
console.log(obj.name);// test

console.log(obj.ojbk);// undefined

console.log(obj.ojbk.link);// TypeError: Cannot read property 'link' of undefined

console.log(obj.ojbk?.link);// undefined

console.log(obj.ojbk?.print?.('abc'));// undefined

// 改进前写法
if(data && data.value && data.value.date){
  // ...
}
if(data && data.subscribe){
  data.subscribe();
}

// 改进后写法
if(data?.value?.date){
  // ...
}
data?.subscribe?.();

空值合并运算符(??)

当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数

if(value !== null && value !== undefined && value !== ''){
    //...
}
// 等同于
if((value??'') !== ''){
  //...
}

逻辑赋值运算符

ES2021 引入了三个新的逻辑赋值运算符(logical assignment operators),将逻辑运算符与赋值运算符进行结合。这三个运算符分别是:

  • ||= :逻辑或赋值运算符
  • &&= :逻辑与赋值运算符
  • ??=:逻辑空赋值运算符
let list1;
list1 ||= [1,2,3];
console.log(list1);// [1,2,3]

let list2 = ['a','b','c']
list2 ||= [100.2000]
console.log(list2);//  ['a','b','c']
let obj = {
    name:'name'
}
obj.name &&= 'NickName';// 等同于 obj.name && (obj.name = 'NickName')

console.log(obj.name);// "NickName"
let name;
name ??= "Linda";// 等同于 name ?? (name = "Linda")

console.log(name);// Linda

网络请求的取消

xhr

<script>
  function xhrSubmit() {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', 'http://www.boredapi.com/api/activity/', true);
    xhr.send();

    // 取消
    setTimeout(() => {
      xhr.abort();
    }, 1000);
  }
</script>

<button type="button" onClick="xhrSubmit()">xhr submit</button>

fetch

<script>
  function fetchSubmit() {
    const controller = new AbortController();
    void (async function () {
      const response = await fetch('http://www.boredapi.com/api/activity/', {
        signal: controller.signal,
      });
      const data = await response.json();
    })();

    // 取消
    setTimeout(() => {
      controller.abort();
    }, 500);
  }
</script>

<button type="button" onClick="fetchSubmit()">fetch submit</button>

沙箱机制

在接触过前端微服务之后,对此概念应该很熟悉了,在微服务框架里为了确保不同应用的运行环境独立,都要使用沙箱隔离机制,在qiankun框架源码中提到了三种沙箱:SnapshotSandbox、ProxySandbox、LegacySandbox.

SnapshotSandbox(快照沙箱)

概念:

快照沙箱实现来说比较简单,主要用于不支持 Proxy 的低版本浏览器,原理是基于diff来实现。
快照沙箱主要分为两种状态: 沙箱激活、沙箱失活

  • 沙箱激活:记录window当时的状态,我们把这个状态称之为快照,此时沙箱应用处于运行中,这个阶段有可能对window上的属性进行操作改变,同时也需要恢复上一次沙箱失活时记录的沙箱运行过程中对window做的状态改变,也就是上一次沙箱激活后对window做了哪些改变,现在也保持一样的改变。
  • 沙箱失活:记录沙箱自激活开始到失活的这段时间window上有哪些状态发生了变化,此时沙箱应用已经停止了对window的影响,失去了隔离作用,不同的属性状态以快照为准,恢复到未改变之前的状态。

快照沙箱存在两个重要的问题:

  • 会改变全局window的属性,如果同时运行多个沙箱应用,多个应用同时改写window上的属性,势必会出现状态混乱,这也就是为什么快照沙箱无法支持多个应用同时运行的原因。
  • 会通过for(prop in window){}的方式来遍历window上的所有属性,window属性众多,这其实是一件很耗费性能的事情。
// 基础沙箱定义
class SnapshotSandBox {
  windowSnapshot = {};
  modifyPropsMap = {};

  // 激活
  active() {
    for (const prop in window) {
      this.windowSnapshot[prop] = window[prop];
    }
    Object.keys(this.modifyPropsMap).forEach(prop => {
      window[prop] = this.modifyPropsMap[prop];
    })
  }

  // 失活
  inactive() {
    for (const prop in window) {
      if (window[prop] !== this.windowSnapshot[prop]) {
        this.modifyPropsMap[prop] = window[prop];
        window[prop] = this.windowSnapshot[prop];
      }
    }
  }
}

// 在沙箱环境中运行程序
let snapshotSandBox = new SnapshotSandBox();

snapshotSandBox.active();
window.userName = 'ssb01';
console.log(window.userName);// ssb01

snapshotSandBox.inactive();
console.log(window.userName);// undefined

snapshotSandBox.active();
console.log(window.userName);// ssb01

LegacySandbox(代理沙箱)

class LegacySandBox {
  addedPropsMapInSandbox = new Map();
  modifiedPropsOriginalValueMapInSandbox = new Map();
  currentUpdatedPropsValueMap = new Map();
  proxyWindow;

  setWindowProp(prop, value, toDelete = false) {
    if (value === undefined && toDelete) {
      delete window[prop];
    } else {
      window[prop] = value;
    }
  }

  active() {
    this.currentUpdatedPropsValueMap.forEach((value, prop) => {

      this.setWindowProp(prop, value);
    });
  }

  inactive() {
    this.modifiedPropsOriginalValueMapInSandbox.forEach((value, prop) => {
      this.setWindowProp(prop, value);
    });
    this.addedPropsMapInSandbox.forEach((_, prop) => {
      this.setWindowProp(prop, undefined, true);
    });
  }

  constructor() {
    const fakeWindow = Object.create(null);
    this.proxyWindow = new Proxy(fakeWindow, {
      set: (target, prop, value, receiver) => {
        const originalVal = window[prop];
        if (!window.hasOwnProperty(prop)) {
          this.addedPropsMapInSandbox.set(prop, value);
        } else if (!this.modifiedPropsOriginalValueMapInSandbox.has(prop)) {
          this.modifiedPropsOriginalValueMapInSandbox.set(prop, originalVal);
        }
        this.currentUpdatedPropsValueMap.set(prop, value);
        window[prop] = value;
      },
      get: (target, prop, receiver) => {
        return target[prop];
      }
    });
  }
}


// 在沙箱环境中运行程序
let legacySandBox = new LegacySandBox();
legacySandBox.active();
legacySandBox.proxyWindow.AppName = 'App 01';
console.log(window.AppName);// App 01


legacySandBox.inactive();
console.log(window.AppName);// undefined

legacySandBox.active();
console.log(window.AppName);// App 01

LegacySandBox沙箱解决了SnapshotSandBox沙箱中每次都需要遍历Window下所有属性的问题,相比快照沙箱提升了很大的运行性能,准确讲是极大减少了额外开销的性能。LegacySandBox沙箱机制仍然存在两个问题:

  • 对window存在状态污染问题
  • 不支持多个沙箱应用运行

ProxySandbox(代理沙箱)

Proxy代理沙箱不用遍历window,对性能是一种优化,同时支持多个沙箱应用,目前应用比较广泛的一种沙箱机制。

class ProxySandBox {
  proxyWindow;
  isRunning = false;
  active() {
    this.isRunning = true;
  }
  inactive() {
    this.isRunning = false;
  }
  constructor() {
    const fakeWindow = Object.create(null);
    this.proxyWindow = new Proxy(fakeWindow, {
      set: (target, prop, value, receiver) => {
        if (this.isRunning) {
          target[prop] = value;
        }
      },
      get: (target, prop, receiver) => {
        return prop in target ? target[prop] : window[prop];
      }
    });
  }
}
// 在沙箱环境中运行程序
let proxySandBox1 = new ProxySandBox();
proxySandBox1.active();
proxySandBox1.proxyWindow.city = 'Beijing';
console.log('active:proxySandBox1:window.city:', proxySandBox1.proxyWindow.city);// Beijing

let proxySandBox2 = new ProxySandBox();
proxySandBox2.active();
proxySandBox2.proxyWindow.city = 'Shanghai';
console.log('active:proxySandBox2:window.city:', proxySandBox2.proxyWindow.city);// Shanghai

console.log('window:window.city:', window.city);// undefined
proxySandBox1.inactive();
proxySandBox2.inactive();

console.log('inactive:proxySandBox1:window.city:', proxySandBox1.proxyWindow.city);// Beijing
console.log('inactive:proxySandBox2:window.city:', proxySandBox2.proxyWindow.city);// Shanghai
console.log('window:window.city:', window.city);// undefined

Decorator和装饰器模式

随着TypeScript和ES6里引入了Class(类),在一些场景下我们需要额外的特性来支持标注或修改类及其成员。装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上,这种声明方式已经在各大前端框架中广泛普及应用。其最大的有点就是解耦,可以在不侵入原码的情况下而通过标注的方式修改类实现功能。在ES6中,并没有支持该用法,在ES2017中才有,所以我们不能直接在浏览器端运行,需要进行编码后再运行。目前想要在 JS 中使用装饰器,需要通过 babel 将装饰器语法转义成 ES5 语法执行。

// 覆盖原类写法
// function CIDecorator(options) {
//     return function (target) {
//         return class extends target {
//             constructor() {
//                 super();
//             }
//         }
//     }
// }

// 类装饰器
function CIDecorator(options) {
  return function (target) {
    target.prototype.id = options.id;
    target.prototype.status = options.status;
  }
}

// 方式装饰器
function SayDecorator(options) {
  return function (target, name, descriptor) {
    const method = descriptor.value;
    descriptor.value = (msg) => {
      const title = options.title;
      msg += title;
      return method.apply(target, [msg]);
    }
  }
}

// 装饰
@CIDecorator({
  id: '1234567',
  status: 'online',
})
class CustomerInfo {
  constructor() {
    this.name = "CustomerInfo";
  }

  // 装饰
  @SayDecorator({
    title: 'Welcome To ES2017'
  })
  say(msg) {

    console.log(`Hello,I am Here !!! And ${msg} `);
  }
}


// 运行程序
const info = new CustomerInfo();
console.log(info.name);   // online
console.log(info.id);     // 1234567
console.log(info.status); // online
info.say('Do you hear me?'); 
// "Hello,I am Here !!! And Do you hear me?Welcome To ES2017"

Proxy和代理模式

Vue3的大流行无疑推动了Proxy在前端的熟知程度,看下ES6中对Proxy的描述:

Proxy用于修改某些操作的默认行为,在目标对象前架设一个“拦截”层,外界对该对象的访问都必须先通过这一层拦截,因此提供了一种机制可以对外界的访问进行过滤和改写。——《ES6标准入门》- 第12章

Proxy代理的对象中有许多的方法,如getOwnPropertyDescriptor、deleteProperty、get、set、has、ownKeys等都是用于拦截不同的情况而出现的。

class CustomerInfo {
  constructor(age) {

    this.age = age;
  }
  age = 0;
  name = "YeCong";

  play() {

    console.log('Run in the park~');
  }
}

const _info = new CustomerInfo(23);
const proxyInfo = new Proxy(_info, {
  set(target, property, value, receiver) {

    if (property == 'age' && value < 18) {
      console.log('发现未成年');
    }

    return Reflect.set(target, property, value, receiver);
  },
  get(target, property, receiver) {

    return Reflect.get(target, property, receiver);
  },
  apply(target, thisArg, args) {
    return Reflect.apply(target, thisArg, args);;
  },
});

console.log('name: ', proxyInfo.name)
console.log('age: ', proxyInfo.age);

proxyInfo.age = 15;
console.log('age: ', proxyInfo.age);

proxyInfo.play();

// 输出
// name:  YeCong
// Proxy.html:48 age:  23
// Proxy.html:33 发现未成年
// Proxy.html:51 age:  15
// Proxy.html:24 Run in the park~