前端汇总 --003

411 阅读22分钟

一、 前端中获取电池信息,用到的就是关于 Battery Status API。

1.1 getBattery()获取

navigator.getBattery().then(function (battery) {
  // 获取设备电量剩余百分比
  var level = battery.level //最大值为1,对应电量100%
  console.log('Level: ' + level * 100 + '%')

  // 获取设备充电状态
  var charging = battery.charging
  console.log('充电状态: ' + charging)

  // 获取设备完全充电需要的时间
  var chargingTime = battery.chargingTime
  console.log('完全充电需要的时间: ' + chargingTime)

  // 获取设备完全放电需要的时间
  var dischargingTime = battery.dischargingTime
  console.log('完全放电需要的时间: ' + dischargingTime)
})

1.2 监听电池状态变化

navigator.getBattery().then(function (battery) {
  // 添加事件,当设备电量改变时触发
  battery.addEventListener('levelchange', function () {
    console.log('电量改变: ' + battery.level)
  })

  // 添加事件,当设备充电状态改变时触发
  battery.addEventListener('chargingchange', function () {
    console.log('充电状态改变: ' + battery.charging)
  })

  // 添加事件,当设备完全充电需要时间改变时触发
  battery.addEventListener('chargingtimechange', function () {
    console.log('完全充电需要时间: ' + battery.chargingTime)
  })

  // 添加事件,当设备完全放电需要时间改变时触发
  battery.addEventListener('dischargingtimechange', function () {
    console.log('完全放电需要时间: ' + battery.dischargingTime)
  })
})

1.3 浏览器兼容

image.png

二、代码优化

2.1 函数(方法)

2.11 形参不超过三个,对测试函数也方便。多了就使用对象参数。

  • 同时建议使用对象解构语法,有几个好处:

    1. 能清楚看到函数签名有哪些熟悉,
    2. 可以直接重新命名,
    3. 解构自带克隆,防止副作用,
    4. Linter检查到函数未使用的属性。
 // bad
 function createMenu(title, body, buttonText, cancellable) {}

 // good
 function createMenu({ title, body, buttonText, cancellable }) {}

2.12 自顶向下地书写函数,人们都是习惯自顶向下读代码,如,为了执行A,需要执行B,为了执行B,需要执行C。如果把A、B、C混在一个函数就很难读了

2.13 不使用布尔值来作为参数,遇到这种情况时,一定可以拆分函数。

 // bad
 function createFile(name, temp) {
   if (temp) {
     fs.create(`./temp/${name}`);
   } else {
     fs.create(name);
   }
 }

 // good
 function createFile(name) {
   fs.create(name);
 }

 function createTempFile(name) {
   createFile(`./temp/${name}`);
 }

2.14 封装复杂的判断条件,提高可读性。

 // bad
 if (!(obj => obj != null && typeof obj[Symbol.iterator] === 'function')) {
     throw new Error('params is not iterable')
 }

 // good
 const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function';
 if (!isIterable(promises)) {
     throw new Error('params is not iterable')
 }

2.15 在方法中有多条件判断时候,为了提高函数的可扩展性,考虑下是不是可以使用能否使用多态性来解决。

对象的多态性:

  • 其实多态最根本的作用就是通过把过程化的条件语句转化为对象的多态性,从而消除这些条件分支语句。
  • 抽象概括就是把 不变的事物可能改变的事物 分离开。

不变的事物是:开始渲染地图 可能改变的事物是:对象渲染什么地图

 // 地图接口可能来自百度,也可能来自谷歌
 var googleMap = {
     show: function (size) {
         console.log('开始渲染谷歌地图', size));
     }
 };
 var baiduMap = {
     render: function (size) {
         console.log('开始渲染百度地图', size));
     }
 };

 // bad: 出现多个条件分支。如果要加一个腾讯地图,就又要改动renderMap函数。
 function renderMap(type) {
     const size = getSize();
     if (type === 'google') {
         googleMap.show(size);
     } else if (type === 'baidu') {
         baiduMap.render(size);
     }
 };
 renderMap('google')

 // good:实现多态处理。如果要加一个腾讯地图,不需要改动renderMap函数。
 // 细节:函数作为一等对象的语言中,作为参数传递也会返回不同的执行结果,也是“多态性”的体现。
 function renderMap (renderMapFromApi) {
     const size = getSize();
     renderMapFromApi(size);
 }
 renderMap((size) => googleMap.show(size));

多态在设计模式中应用得比较广泛,比如 组合模式 / 策略模式等等。~~

2.2 注释

2.21 一般代码要能清晰的表达意图,只有遇到复杂的逻辑时才注释。

 // good:由于函数名已经解释不清楚函数的用途了,所以注释里说明。
 // 在nums数组中找出 和为目标值 target 的两个整数,并返回它们的数组下标。
 var twoSum = function(nums, target) {
     let map = new Map()
     for (let i = 0; i < nums.length; i++) {
         const item = nums[i];
         const index = map.get(target - item)
         if (index !== undefined){
             return [index, i]
         }
         map.set(item, i)
     }
     return []
 };

 // bad:加了一堆废话
 var twoSum = function(nums, target) {
     // 声明map变量
     let map = new Map()
     // 遍历
     for (let i = 0; i < nums.length; i++) {
         const item = nums[i];
         const index = map.get(target - item)
         // 如果下标为空
         if (index !== undefined){
             return [index, i]
         }
         map.set(item, i)
     }
     return []
 };

2.3 对象

2.31 多使用getter和setter(getXXX和setXXX)。好处:

  • 在set时方便验证。
  • 可以添加埋点,和错误处理。
  • 可以延时加载对象的属性。
// good
function makeBankAccount() {
  let balance = 0;

  function getBalance() {
    return balance;
  }

  function setBalance(amount) {
    balance = amount;
  }

  return {
    getBalance,
    setBalance
  };
}

const account = makeBankAccount();
account.setBalance(100);

2.4 类 solid

2.41 单一职责原则 (SRP)  - 保证“每次改动只有一个修改理由”。

2.42 开闭原则 (OCP)  - 对扩展放开,但是对修改关闭。在不更改现有代码的情况下添加新功能。

2.43 接口隔离原则 (ISP)  - 定义是"客户不应被迫使用对其而言无用的方法或功能"。常见的就是让一些参数变成可选的。

 // bad
 class Dog {
     constructor(options) {
         this.options = options;
     }

     run() {
         this.options.run(); // 必须传入 run 方法,不然报错
     }
 }

 const dog = new Dog({}); // Uncaught TypeError: this.options.run is not a function
  
 dog.run()

 // good
 class Dog {
     constructor(options) {
         this.options = options;
     }

     run() {
         if (this.options.run) {
             this.options.run();
             return;
         }
         console.log('跑步');
     }
 }

2.44 依赖倒置原则(DIP)  - 程序要依赖于抽象接口(可以理解为入参),不要依赖于具体实现。这样可以减少耦合度。

 // bad
 class OldReporter {
   report(info) {
     // ...
   }
 }

 class Message {
   constructor(options) {
     // ...
     // BAD: 这里依赖了一个实例,那你以后要换一个,就麻烦了
     this.reporter = new OldReporter();
   }

   share() {
     this.reporter.report('start share');
     // ...
   }
 }

 // good
 class Message {
   constructor(options) {
     // reporter 作为选项,可以随意换了
     this.reporter = this.options.reporter;
   }

   share() {
     this.reporter.report('start share');
     // ...
   }
 }
 class NewReporter {
   report(info) {
     // ...
   }
 }
 new Message({ reporter: new NewReporter });

三、 好用的 Javascript 技巧

3.1 利用 reduce 进行数据结构的转换

有时候前端需要对后端传来的数据进行转换,以适配前端的业务逻辑,或者对组件的数据格式进行转换再传给后端进行处理,而 reduce 是一个非常强大的工具。

数组以key='classId'进行分类:

const arr = [
    { classId: "1", name: "张三", age: 16 },
    { classId: "1", name: "李四", age: 15 },
    { classId: "2", name: "王五", age: 16 },
    { classId: "3", name: "赵六", age: 15 },
    { classId: "2", name: "孔七", age: 16 }
];

groupArrayByKey(arr, "classId");

function groupArrayByKey(arr = [], key) {
    return arr.reduce((t, v) => (!t[v[key]] && (t[v[key]] = []), t[v[key]].push(v), t), {})
}

image.png

3.2 实现 Curring

JavaScript 的柯里化是指将接受多个参数的函数转换为一系列只接受一个参数的函数的过程。这样可以更加灵活地使用函数,减少重复代码,并增加代码的可读性。

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      };
    }
  };
}

function add(x, y) {
  return x + y;
}

const curriedAdd = curry(add);

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

通过柯里化,我们可以将一些常见的功能模块化,例如验证、缓存等等。这样可以提高代码的可维护性和可读性,减少出错的机会。

3.3 递归获取对象属性

递归通过将原始问题分割为结构相同的子问题,然后依次解决这些子问题,组合子问题的结果最终获得原问题的答案。

const user = {
  info: {
    name: "张三",
    address: { home: "Shaanxi", company: "Xian" },
  },
};

// obj是获取属性的对象,path是路径,fallback是默认值
function get(obj, path, fallback) {
  const parts = path.split(".");
  const key = parts.shift();
  if (typeof obj[key] !== "undefined") {
    return parts.length > 0 ?
      get(obj[key], parts.join("."), fallback) :
      obj[key];
  }
  // 如果没有找到key返回fallback
  return fallback;
}

console.log(get(user, "info.name")); // 张三
console.log(get(user, "info.address.home")); // Shaanxi
console.log(get(user, "info.address.company")); // Xian
console.log(get(user, "info.address.abc", "fallback")); // fallback

四、 js小众且好用的技巧

4.1 数组

4.11 数组去重

const removeDuplicates = list => [...new Set(list)]
removeDuplicates([0, 0, 2, 4, 5]) // [0,2,4,5]

4.12 多数组取交集

const intersection = (a, ...arr) => [...new Set(a)].filter((v) => arr.every((b) => b.includes(v)))

intersection([1, 2, 3, 4], [2, 3, 4, 7, 8], [1, 3, 4, 9])
// [3, 4]

4.13 查找最大值索引

const indexOfMax = (arr) => arr.reduce((prev, curr, i, a) => (curr > a[prev] ? i : prev), 0);
indexOfMax([1, 3, 9, 7, 5]); // 2

4.14 查找最小值索引

const indexOfMin = (arr) => arr.reduce((prev, curr, i, a) => (curr < a[prev] ? i : prev), 0)
indexOfMin([2, 5, 3, 4, 1, 0, 9]) // 5

4.2 数字转换

  • 将10进制转换成n进制,可以使用toString(n)
const toDecimal = (num, n = 10) => num.toString(n) 
// 假设数字10要转换成2进制
toDecimal(10, 2) // '1010'
  • 将n进制转换成10进制,可以使用parseInt(num, n)
// 10的2进制为1010
const toDecimalism = (num, n = 10) => parseInt(num, n)
toDecimalism(1010, 2)

4.3 正则

4.31 手机号格式化

const formatPhone = (str, sign = '-') => str.replace(/(\W|\s)/g, "").split(/^(\d{3})(\d{4})(\d{4})$/).filter(item => item).join(sign)

formatPhone('13123456789') // '131-2345-6789'
formatPhone('13 1234 56 789', ' ') // '131 2345 6789'

4.32 去除多余空格

const setTrimOut = str => str.replace(/\s\s+/g, ' ')
const str = setTrimOut('hello,   jack') // hello, jack

4.4 日期

4.41 判断日期是否为今天

const isToday = (date) => date.toISOString().slice(0, 10) === new Date().toISOString().slice(0, 10)

4.42 日期转换

当你需要将日期转换为为 YYYY-MM-DD 格式:

const formatYmd = (date) => date.toISOString().slice(0, 10);
formatYmd(new Date())

4.43 获取某年月份天数

const getDaysNum = (year, month) => new Date(year, month, 0).getDate()  
const day = getDaysNum(2024, 2) // 29

4.5 函数

4.51 异步函数判断

const isAsyncFunction = (v) => Object.prototype.toString.call(v) === '[object AsyncFunction]'
isAsyncFunction(async function () {}); // true

4.6 数字

4.61 截断数字

当你需要将小数点后的某些数字截断而不取四舍五入

const toFixed = (n, fixed) => `${n}`.match(new RegExp(`^-?\d+(?:.\d{0,${fixed}})?`))[0]
toFixed(10.255, 2) // 10.25

4.62 四舍五入

当你需要将小数点后的某些数字截断,并取四舍五入

const round = (n, decimals = 0) => Number(`${Math.round(`${n}e${decimals}`)}e-${decimals}`)
round(10.255, 2) // 10.26

4.63 补零

当你需要在一个数字num不足len位数的时候前面补零操作

const replenishZero = (num, len, zero = 0) => num.toString().padStart(len, zero)
replenishZero(8, 2) // 08

4.7 对象

4.71 字符串转对象

const strParse = (str) => JSON.parse(str.replace(/(\w+)\s*:/g, (_, p1) => `"${p1}":`).replace(/\'/g, "\""))

strParse('{name: "jack"}')

4.72反转对象键值

当你需要将对象的键值对交换

const invert = (obj) => Object.keys(obj).reduce((res, k) => Object.assign(res, { [obj[k]]: k }), {})
invert({name: 'jack'}) // {jack: 'name'}

4.73 比较两个对象

const isEqual = (...objects) => objects.every(obj => JSON.stringify(obj) === JSON.stringify(objects[0]))
isEqual({name: 'jack'}, {name: 'jack'}) // true
isEqual({name: 'jack'}, {name: 'jack1'}, {name: 'jack'}) // false

4.8 随机颜色生成

const getRandomColor = () => `#${Math.floor(Math.random() * 0xffffff).toString(16)}`
getRandomColor() // '#4c2fd7'

4.9 颜色格式转换

当你需要将16进制的颜色转换成rgb

const hexToRgb = hex => hex.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i, (_, r, g, b) => `#${r}${r}${g}${g}${b}${b}`).substring(1).match(/.{2}/g).map((x) => parseInt(x, 16));
hexToRgb('#00ffff'); // [0, 255, 255]
hexToRgb('#0ff'); // [0, 255, 255]

4.10 uuid

const uuid = (a) => (a ? (a ^ ((Math.random() * 16) >> (a / 4))).toString(16) : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid))
uuid()

4.11 强制等待

const sleep = async (t) => new Promise((resolve) => setTimeout(resolve, t));
sleep(2000).then(() => {console.log('time')});

4.12 双位运算符 ~~

可以使用双位操作符来替代正数的 Math.floor( ),替代负数的Math.ceil( )。双否定位操作符的优势在于它执行相同的操作运行速度更快。

Math.floor(4.9) === 4      //true
// 简写为:
~~4.9 === 4      //true

不过要注意,对正数来说 ~~ 运算结果与 Math.floor( ) 运算结果相同,而对于负数来说与Math.ceil( )的运算结果相同:

~~4.5                // 4
Math.floor(4.5)      // 4
Math.ceil(4.5)       // 5

~~-4.5        		// -4
Math.floor(-4.5)     // -5
Math.ceil(-4.5)      // -4

4.13 + 正号

这个符号既是加号,也是正号,可以最优雅的将某个值转换成数字。

const timestamp = +new Date() // 时间戳
console.log(+'18' === 18) // true

4.14 可选链操作符 ??

只有左侧的值为null或undefined的时候才使用右侧的值。

4.15 空值赋值运算符 ??=

和空值合并运算符??类似(可常量、可变量)。
当??=左侧的值为null、undefined的时候,会将右侧的值赋值给左侧变量。

4.16 使用BigInt支持大整数计算问题

es2020引入了一种新的数据类型 BigInt,用来表示任意位数的整数
例如

// 超过 53 个二进制位的数值(相当于 16 个十进制位),无法保持精度
Math.pow(2, 53) === Math.pow(2, 53) + 1 // true

// BigInt
BigInt(Math.pow(2, 53)) === BigInt(Math.pow(2, 53)) + BigInt(1) // false

除了使用BigInt来声明一个大整数,还可以使用数字后面加n的形式

1234 // 普通整数
1234n // BigInt

4.17 使用哈希前缀#将类字段设为私有

在类中通过哈希前缀#标记的字段都将被私有,子类实例将无法继承

例如

class Person {
    #privateField;
    #privateMethod() {
        return 'hello world';
    }
    constructor() {
        this.#privateField = 42;
    }
}

const instance = new Person()
console.log(instance.privateField); //undefined
console.log(instance.privateMethod); //undefined

可以看到,属性privateField和方法privateMethod都被私有化了,在实例中无法获取到

五、 高频JavaScript手写代码

5.1 函数柯里化

柯里化函数就是高阶函数的一种,好处主要是实现参数的复用和延迟执行,不过性能上就会没那么好,要创建数组存参数,要创建闭包,而且存取argements比存取命名参数要慢一点

实现 add(1)(2)(3) 要求参数不固定,类似 add(1)(2, 3, 4)(5)() 这样也行,我这实现的是中间的不能不传参数,最后一个不传参数,以此来区分是最后一次调用然后累计结果

// 每次调用的传进来的参数做累计处理
function reduce (...args) {
    return args.reduce((a, b) => a + b)
}
function currying (fn) {
  // 存放每次调用的参数
  let args = []
  return function temp (...newArgs) {
    if (newArgs.length) {
      // 有参数就合并进去,然后返回自身
      args = [ ...args, ...newArgs ]
      return temp
    } else {
      // 没有参数了,也就是最后一个了,执行累计结果操作并返回结果
      let val = fn.apply(this, args)
      args = [] //保证再次调用时清空
      return val
    }
  }
}
let add = currying(reduce)
console.log(add(1)(2, 3, 4)(5)())  //15
console.log(add(1)(2, 3)(4, 5)())  //15

5.2 Promise.all

Promise.all 可以把多个 Promise 实例打包成一个新的 Promise 实例。传进去一个值为多个 Promise 对象的数组,成功的时候返回一个结果的数组,返回值的顺序和传进去的顺序是一致对应得上的,如果失败的话就返回最先 reject 状态的值

如果遇到需要同时发送多个请求并且按顺序返回结果的话,Promise.all就可以完美解决这个问题

MyPromise.all = function (promisesList) {
  return new MyPromise((resolve, reject) => {
    if (!Array.isArray(promiselList) return reject(new Error('必须是数组'))
    if (!promisesList.length) return resolve([])
    let arr = [], count = 0
    // 直接循环同时执行传进来的promise
    for (let i = 0, len = promisesList.length; i < len; i++) {
      // 因为有可能是 promise 有可能不是,所以用resolve()不管是不是都会自动转成promise
      Promise.resolve(promise).then(result => {
          // 由到promise在初始化的时候就执行了,.then只是拿结果而已,所以执行完成的顺序有可能和传进来的数组不一样
          // 也就是说直接push到arr的话,顺序有可能会出错
          count++
          arr[i] = result
          // 不能用arr.length===len,是因为数组的特性
          // arr=[]; arr[3]='xx'; console.log(arr.length) 这打印出来会是4 而不是1
          if(count === len) resolve(arr)
      }).catch(err => reject(err))
    }
  })
}

5.3 双向数据绑定

let obj = {}
let input = document.getElementById('input')
let box = document.getElementById('box')
// 数据劫持
Object.defineProperty(obj, 'text', {
  configurable: true,
  enumerable: true,
  get() {
    // 获取数据就直接拿
    console.log('获取数据了')
  },
  set(newVal) {
    // 修改数据就重新赋值
    console.log('数据更新了')
    input.value = newVal
    box.innerHTML = newVal
  }
})
// 输入监听
input.addEventListener('keyup', function(e) {
  obj.text = e.target.value
})

六、 JavaScript进阶知识点

6.1 Set、Map

Set 和 Map 都是强引用(下面有说明),都可以遍历,比如 for of / forEach

  • Set

允许存储任何类型的唯一值,只有键值(key)没有键名,常用方法 addsizehasdelete等等

  • Map

是键值对的集合;常用方法 setgetsizehasdelete等等

6.2 WeakSet、WeakMap

WeakSet 和 WeakMap 都是弱引用,对 GC 更加友好,都不能遍历

  • WeakSet

成员只能是对象或数组,方法只有 addhasdelete

  • WeakMap

键值对集合,只能用对象作为 key(null 除外),value 可以是任意的。方法只有 getsethasdelete

6.3 类型转换

JS没有严格的数据类型,所以可以互相转换

  • 显示类型转换:Number()String()Boolean()
  • 隐式类型转换:四则运算,判断语句,Native 调用,JSON 方法

6.31 Number()

原始类型转换

  • 数字:转换后还是原来的值
  • 字符串:如果能被解析成数字,就得到数字,否则就是 NaN,空字符串为0
  • 布尔值:true 转为1,false 转为0
  • undefined: 转为 NaN
  • null:转为0 引用类型转换

6.32 String()

原始类型转换

  • 数字:转换成相应字符串
  • 字符串:转换后还是原来的值
  • 布尔值:true 转为"true",false 转为"false"
  • undefined: 转为"undefined"
  • null:转为"null" 引用类型转换

6.33 Boolean()

原始类型转换

  • 0
  • -0
  • ""
  • null
  • undefined
  • NaN

以上统一转为false,其他一律为true

6.34 隐式转换

// 四则运算  如把String隐式转换成Number
console.log(+'1' === 1) // true

// 判断语句  如把String隐式转为Boolean
if ('1') console.log(true) // true

// Native调用  如把Object隐式转为String
alert({a:1}) // "[object Object]"
console.log(([][[]]+[])[+!![]]+([]+{})[!+[]+!![]]) // "nb"

// JSON方法 如把String隐式转为Object
console.log(JSON.parse("{a:1}")) // {a:1}

几道隐式转换题

console.log( true+true  ) // 2                   解:true相加是用四则运算隐式转换Number 就是1+1
console.log(  1+{a:1}   ) // "1[object Object]"  解:上面说了Native调用{a:1}为"[object Object]"  数字1+字符串直接拼接
console.log(   []+[]    ) // ""                  解:String([]) =》 [].toString() = "" =》 ""+"" =》 ""
console.log(   []+{}    ) // "[object Object]"   解:"" + String({}) =》 "" + {}.toString() = "" + "[object Object]" =》 "[object Object]"
console.log(   {}+{}    ) // "[object Object][object Object]" 和上面同理

6.4 箭头函数

  • 箭头函数写法更简洁
  • 箭头函数本身没有 this,所以没有 prototype
  • 箭头函数不支持 new
  • 箭头函数的 this 继承自外层第一个作用域的 this, 严格和非严格模式下都一样,修改被继承的this指向,那么箭头函数的 this 指向也会跟着改变
  • 箭头函数指向全局时,arguments 会报错,否则 arguments 继承自外层作用域
  • 箭头函数不支持函数形参重名

6.5 浅拷贝

第一层是引用类型就拷贝指针,不是拷贝值。拷贝栈不拷贝堆

    1. 展开运算符 ...
    1. Object.assign() 把obj2合并到obj1
    1. 手写
    1. 数组浅拷贝 用Array方法 concat()和slice()
let obj1 = { a:1, b:{ c:3 } } let obj2 = { ...obj1 }

Object.assign(obj1, obj2)

function clone(target){
    let obj = {}
    for(let key in target){
        obj[key] = target[key]
    }
    return obj
}

// 4. 数组浅拷贝  用Array方法 concat()和slice()
let arr1 = [ 1,2,{ c:3 } ]
let arr2 = arr1.concat()
let arr3 = arr1.slice()

6.6 深拷贝

拷贝栈也拷贝堆,重新开僻一块内存

  • 1. JSON.parse(JSON.stringify()) 该方法可以应对大部分应用场景,但是也有很大缺陷,比如拷贝其他引用类型,拷贝函数,循环引用等情况
  • 2. 手写递归

2. 手写递归

function clone(target, map = new Map()){
    if (typeof target === 'object') { // 引用类型才继续深拷贝
        let obj = Array.isArray(target) ? [] : {} // 考虑数组
        //防止循环引用
        if (map.get(target)) {
            return map.get(target) // 有拷贝记录就直接返回
        }
        map.set(target,obj) // 没有就存储拷贝记录
        for (let key in target) {
            obj[key] = clone(target[key]) // 递归
        }
        return obj
    } else {
        return target
    }
}

深拷贝优化版

// WeakMap 对象是键/值对集合,键必须是对象,而且是弱引用的,值可以是任意的
function clone(target, map = new WeakMap()){
    // 引用类型才继续深拷贝 
    if (target instanceof Object) {
        const isArray = Array.isArray(target)
        // 克隆对象和数组类型
        let cloneTarget = isArray ? [] : {} 
        
        // 防止循环引用
        if (map.get(target)) {
            // 有拷贝记录就直接返回
            return map.get(target) 
        }
        // 没有就存储拷贝记录
        map.set(target,cloneTarget) 
        
        // 是对象就拿出同级的键集合  返回是数组格式
        const keys = isArray ? undefined : Object.keys(target)
        // value是对象的key或者数组的值 key是下标 
        forEach(keys || target, (value, key) => { 
            if (keys) {
                // 是对象就把下标换成value
                key = value 
            }
            // 递归
            cloneTarget[key] = clone(target[key], map) 
        })
        return cloneTarget
    } else {
        return target
    }
}

6.7 new

new 干了什么?

  • 创建一个独立内存空间的空对象
  • 把这个对象的构造原型(__proto__)指向函数的原型对象prototype,并绑定this
  • 执行构造函数里的代码
  • 如果构造函数有return就返回return的值,如果没有就自动返回空对象也就是this

有一种情况如下,就很坑,所以构造函数里面最好不要返回对象返回基本类型不影响

function Person(name){
    this.name = name
    console.log(this) // { name: 'alex' }
    return { age: 18 }
}
const p = new Person('alex')
console.log(p) // { age: 18 }

6.8 原型

image.png

function Parent(){} // 这就是构造函数
let child = new Parent() // child就是实例

Parent.prototype.getName = function(){ console.log('alex') } // getName是构造函数的原型对象上的方法
child.getName() // 'alex' 这是继承来的方法
  • prototype :它是构造函数的原型对象。每个函数都会有这个属性,强调一下,是函数,其他对象是没有这个属性的
  • __proto__ :它指向构造函数的原型对象。每个对象都有这个属性,强调一下,是对象,同样,因为函数也是对象,所以函数也有这个属性。不过访问对象原型(child.__proto__)的话,建议用Es6Reflect.getPrototypeOf(child)或者Object.getPrototypeOf(child)方法
  • constructor :这是原型对象上的指向构造函数的属性,也就是说代码中的 Parent.prototype.constructor === Parent 是为 true 的\

6.9 原型污染

原型污染是指攻击者通过某种手段修改js的原型

Object.prototype.toString = function () {alert('原生方法被改写,已完成原型污染')};

6.91 怎么解决原型污染

  1. Object.freeze(obj)冻结对象,然后就不能被修改属性,变成不可扩展的对象
Object.freeze(Object.prototype)
Object.prototype.toString = 'hello'
console.log(Object.prototype.toString) // ƒ toString() { [native code] }
  1. 不采用字面量形式,用Object.create(null)创建一个没有原型的新对象,这样不管对原型做什么扩展都不会生效
const obj = Object.create(null)
console.log(obj.__proto__) // => undefined
  1. Map 数据类型,代替Object类型

    Map 对象保存键/值对的集合。任何值(对象或者原始值)都可以作为一个键或一个值。所以用 Map 数据结构,不会对 Object 原型污染

    Map 和 Object 不同点

  • Object 的键只支持 String/Number/Symbol 两种类型,Map 的键可以是任意值,包括函数、对象、基本类型
  • Map 中的键值是有序的,Object 中的键不是
  • Map 在频繁增删键值对的场景下有性能优势
  • 用 size 属性直接获取一个Map的键值对个数,Object 的键值对个数不能直接获取

有一种情况:

JSON.parse('{ a:1, __proto__: { b: 2 }}')

结果不会改写Object.prototype,因为 V8 会自动忽略 JSON.parse 里面名为 proto 的键

七、 超级实用的reduce使用技巧

7.1 reduce 是数组的方法,可以对数组中的每个元素依次执行一个回调函数,从左到右依次累积计算出一个最终的值。其语法为:

arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

其中,callback 是每个元素执行的回调函数,其包含 4 个参数:

  • accumulator:累积器,即上一次回调函数执行的返回值。
  • currentValue:当前元素的值。
  • index:当前元素的下标。
  • array:原始数组。

initialValue 是可选的,表示累积器的初始值。

reduce 函数的执行过程如下:

  1. 如果没有提供 initialValue,则将数组的第一个元素作为累积器的初始值,否则将 initialValue 作为累积器的初始值。
  2. 从数组的第二个元素开始,依次对数组中的每个元素执行回调函数。
  3. 回调函数的返回值作为下一次回调函数执行时的累积器的值。
  4. 对数组中的每个元素执行完回调函数后,reduce 函数返回最后一次回调函数的返回值,即最终的累积值。

7.2 常用使用技巧:

7.21 计算数组中每个元素出现的次数

const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const count = fruits.reduce((accumulator, currentValue) => {
  accumulator[currentValue] = (accumulator[currentValue] || 0) + 1;
  return accumulator;
}, {});
console.log(count); // Output: { apple: 3, banana: 2, orange: 1 }

7.22 拍平嵌套数组

const nestedArray = [[1, 2], [3, 4], [5, 6]];
const flattenedArray = nestedArray.reduce((accumulator, currentValue) => accumulator.concat(currentValue), []);
console.log(flattenedArray); // Output: [1, 2, 3, 4, 5, 6]

7.23 按条件分组

const people = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 35 },
  { name: 'David', age: 25 },
  { name: 'Emily', age: 30 }
];
const groupedPeople = people.reduce((accumulator, currentValue) => {
  const key = currentValue.age;
  if (!accumulator[key]) {
    accumulator[key] = [];
  }
  accumulator[key].push(currentValue);
  return accumulator;
}, {});
console.log(groupedPeople);
// Output: {
//   25: [{ name: 'Alice', age: 25 }, { name: 'David', age: 25 }],
//   30: [{ name: 'Bob', age: 30 }, { name: 'Emily', age: 30 }],
//   35: [{ name: 'Charlie', age: 35 }]
// }

7.24 将多个数组合并为一个对象

const keys = ['name', 'age', 'gender'];
const values = ['Alice', 25, 'female'];
const person = keys.reduce((accumulator, currentValue, index) => {
    accumulator[currentValue] = values[index];
    return accumulator;
  }, {});
console.log(person); // Output: { name: 'Alice', age: 25, gender: 'female' }

7.25 将字符串转换为对象

const str = 'key1=value1&key2=value2&key3=value3';
const obj = str.split('&').reduce((accumulator, currentValue) => {
  const [key, value] = currentValue.split('=');
  accumulator[key] = value;
  return accumulator;
}, {});
console.log(obj); 
// Output: { key1: 'value1', key2: 'value2', key3: 'value3' }

7.26 将对象转换为查询字符串

const params = { foo: "bar", baz: 42 };
const queryString = Object.entries(params).reduce((acc, [key, value]) => {
  return `${acc}${key}=${value}&`;
}, "?").slice(0, -1);
console.log(queryString); // "?foo=bar&baz=42"

7.27 打印斐波那契数列

const fibonacci = n => {
  return [...Array(n)].reduce((accumulator, currentValue, index) => {
    if (index < 2) {
      accumulator.push(index);
    } else {
      accumulator.push(accumulator[index - 1] + accumulator[index - 2]);
    }
    return accumulator;
  }, []);
};
console.log(fibonacci(10)); // Output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

7.28 检查字符串是否是回文字符串

const str = 'racecar';
const isPalindrome = str.split('').reduce((accumulator, currentValue, index, array) => {
  return accumulator && currentValue === array[array.length - index - 1];
}, true);
console.log(isPalindrome); // Output: true

7.29 检查括号是否匹配

const str = "(()()())";
const balanced = str.split("").reduce((acc, cur) => {
  if (cur === "(") {
    acc++;
  } else if (cur === ")") {
    acc--;
  }
  return acc;
}, 0) === 0;
console.log(balanced); // true

7.210 递归获取对象属性

const user = {
  info: {
    name: "Jason",
    address: { home: "Shaanxi", company: "Xian" },
  },
};
function get(config, path, defaultVal) {
  return path.split('.').reduce((config, name) => config[name], config) || defaultVal;
  return fallback;
}
get(user, "info.name"); // Jason
get(user, "info.address.home"); // Shaanxi
get(user, "info.address.company"); // Xian
get(user, "info.address.abc", "default"); // default

7.3 手写 reduce

可以通过手写一个简单的 reduce 函数来更好地理解它的实现原理:

function myReduce(arr, callback, initialValue) {
  let accumulator = initialValue === undefined ? arr[0] : initialValue;
  for (let i = initialValue === undefined ? 1 : 0; i < arr.length; i++) {
    accumulator = callback(accumulator, arr[i], i, arr);
  }
  return accumulator;
}

八、 Set、Map使用技巧

8.1 Set和Map的一些常用方法:

Set:

  • new Set(): 创建一个新的Set对象
  • add(value): 向Set对象中添加一个新的值
  • delete(value): 从Set对象中删除一个值
  • has(value): 检查Set对象中是否存在指定的值
  • size: 获取Set对象中的值的数量
  • clear(): 从Set对象中删除所有值

Map:

  • new Map(): 创建一个新的Map对象
  • set(key, value): 向Map对象中添加一个键值对
  • get(key): 根据键获取Map对象中的值
  • delete(key): 从Map对象中删除一个键值对
  • has(key): 检查Map对象中是否存在指定的键
  • size: 获取Map对象中的键值对数量
  • clear(): 从Map对象中删除所有键值对

Set和Map是非常有用的数据结构,它们可以提高程序的性能和可读性,并且可以简化代码的编写。

8.11 Set

8.111 去重

使用 Set 可以轻松地进行数组去重操作,因为 Set 只能存储唯一的值。

const arr = [1, 2, 3, 1, 2, 4, 5];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // [1, 2, 3, 4, 5]

8.112 数组转换

可以使用 Set 将数组转换为不包含重复元素的 Set 对象,再使用 Array.from() 将其转换回数组。

const arr = [1, 2, 3, 1, 2, 4, 5];
const set = new Set(arr);
const uniqueArr = Array.from(set);
console.log(uniqueArr); // [1, 2, 3, 4, 5]

8.113 优化数据查找

使用 Set 存储数据时,查找操作的时间复杂度为 O(1),比数组的 O(n) 要快得多,因此可以使用 Set 来优化数据查找的效率。

const dataSet = new Set([1, 2, 3, 4, 5]);

if (dataSet.has(3)) {
  console.log('数据已经存在');
} else {
  console.log('数据不存在');
}

8.114 并集、交集、差集

Set数据结构可以用于计算两个集合的并集、交集和差集。以下是一些使用Set进行集合运算的示例代码:

const setA = new Set([1, 2, 3]);
const setB = new Set([2, 3, 4]);

// 并集
const union = new Set([...setA, ...setB]);
console.log(union); // Set {1, 2, 3, 4}

// 交集
const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log(intersection); // Set {2, 3}

// 差集
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log(difference); // Set {1}

8.115 模糊搜索

Set 还可以通过正则表达式实现模糊搜索。可以将匹配结果保存到 Set 中,然后使用 Array.from() 方法将 Set 转换成数组。

const data = ['apple', 'banana', 'pear', 'orange'];

// 搜索以 "a" 开头的水果
const result = Array.from(new Set(data.filter(item => /^a/i.test(item))));
console.log(result); // ["apple"]

8.116 使用 Set 替代数组实现队列和栈

可以使用 Set 来模拟队列和栈的数据结构。

// 使用 Set 实现队列
const queue = new Set();
queue.add(1);
queue.add(2);
queue.add(3);
queue.delete(queue.values().next().value); // 删除第一个元素
console.log(queue); // Set(2) { 2, 3 }

// 使用 Set 实现栈
const stack = new Set();
stack.add(1);
stack.add(2);
stack.add(3);
stack.delete([...stack][stack.size - 1]); // 删除最后一个元素
console.log(stack); // Set(2) { 1, 2 }

8.12 Map

8.121 将 Map 转换为对象

const map = new Map().set('key1', 'value1').set('key2', 'value2');
const obj = Object.fromEntries(map);

8.122 将 Map 转换为数组

const map = new Map().set('key1', 'value1').set('key2', 'value2');
const array = Array.from(map);

8.123 记录数据的顺序

如果你需要记录添加元素的顺序,那么可以使用Map来解决这个问题。当你需要按照添加顺序迭代元素时,可以使用Map来保持元素的顺序。

const map = new Map();
map.set('a', 1);
map.set('b', 2);
map.set('c', 3);
map.set('d', 4);

for (const [key, value] of map) {
  console.log(key, value);
}
// Output: a 1, b 2, c 3, d 4

8.124 统计数组中元素出现次数

可以使用 Map 统计数组中每个元素出现的次数。

const arr = [1, 2, 3, 1, 2, 4, 5];

const countMap = new Map();
arr.forEach(item => {
  countMap.set(item, (countMap.get(item) || 0) + 1);
});

console.log(countMap.get(1)); // 2
console.log(countMap.get(2)); // 2
console.log(countMap.get(3)); // 1

8.125 统计字符出现次数

使用Map数据结构可以方便地统计字符串中每个字符出现的次数。

const str = 'hello world';
const charCountMap = new Map();
for (let char of str) {
  charCountMap.set(char, (charCountMap.get(char) || 0) + 1);
}
console.log(charCountMap); // Map { 'h' => 1, 'e' => 1, 'l' => 3, 'o' => 2, ' ' => 1, 'w' => 1, 'r' => 1, 'd' => 1 }

8.126 缓存计算结果

在处理复杂的计算时,可能需要对中间结果进行缓存以提高性能。可以使用Map数据结构缓存计算结果,以避免重复计算。

const cache = new Map();
function fibonacci(n) {
  if (n === 0 || n === 1) {
    return n;
  }
  if (cache.has(n)) {
    return cache.get(n);
  }
  const result = fibonacci(n - 1) + fibonacci(n - 2);
  cache.set(n, result);
  return result;
}
console.log(fibonacci(10)); // 55

8.127 使用 Map 进行数据的分组

const students = [
  { name: "Tom", grade: "A" },
  { name: "Jerry", grade: "B" },
  { name: "Kate", grade: "A" },
  { name: "Mike", grade: "C" },
];

const gradeMap = new Map();
students.forEach((student) => {
  const grade = student.grade;
  if (!gradeMap.has(grade)) {
    gradeMap.set(grade, [student]);
  } else {
    gradeMap.get(grade).push(student);
  }
});

console.log(gradeMap.get("A")); // [{ name: "Tom", grade: "A" }, { name: "Kate", grade: "A" }]

8.128 使用 Map 过滤符合条件的对象

在实际开发中,我们常常需要在一个对象数组中查找符合某些条件的对象。此时,我们可以结合使用 Map 和 filter 方法来实现。比如:

const users = [
  { name: 'Alice', age: 22 },
  { name: 'Bob', age: 18 },
  { name: 'Charlie', age: 25 }
];
const userMap = new Map(users.map(user => [user.name, user]));
const result = users.filter(user => userMap.has(user.name) && user.age > 20);
console.log(result); // [{ name: 'Alice', age: 22 }, { name: 'Charlie', age: 25 }]

九、css层叠

9.1 CSS 层叠上下文的 7 层简化图

image.png

9.2 图层 Layer

9.21 什么样的元素会创建新图层?

常见的条件,如下:

  • 产生 滚动条 的元素

    • 滚动条 单独生成图层
    • 产生 滚动的内容 单独生成图层
  • HTML 根元素

  • video 元素

  • canvas 元素

  • 拥有 CSS3 动画 的元素

  • position:fixed 的定位元素

  • 拥有 CSS 与 3D 变换 相关属性的元素

  • 拥有 CSS will-change 属性的元素

  • ......

9.22 怎么观察图层?

那当然是用浏览器的 Devtools 去观察了, 而 Devtools 又分为 Safari DevtoolsChrome Devtools,而关于其中的具体比较可见神光大佬的 什么 css 会新建图层?别猜了,Devtools 都写了

十、 new Date()

10.1 可恶的四宗罪

10.11. Safari浏览器不兼容YYYY-MM-DD这样的格式

new Date('2023-1-1');

这行代码无论在Macbook中还是iPhone中的Safari浏览器,返回的都是Invalid Date, Safari浏览器目前还理解不了YYYY-MM-DD这样的格式,只支持YYYY/MM/DD。

10.12、月份的索引是以0为起点的,而年份、日期却不是

new Date(2023,5,9);

得到的是一个反直觉的结果:2023年6月9日!!!

10.13、年份小于100,默认以19xx或20xx开头

10.14、日期初始化不统一,存在时区差异

'2018-01-01'和'2018/01/01'是不同的,存在一定时差

new Date('2018-01-01');

返回:

Mon Jan 01 2018 08:00:00 GMT+0800 (中国标准时间)

然而……

new Date('2018/01/01');

返回:

Mon Jan 01 2018 00:00:00 GMT+0800 (中国标准时间)

两种格式返回的时间是不同的,查了个北京时间与格林尼治时间的时差,8个小时啊!

10.2 多种初始化时间的方式:

10.21 实例化对象

// 通过时间戳
datex(123456789);

// 通过多个参数初始化
datex(2018,8,8);

// 通过时间字符串初始化
datex('2018-08-08');
datex('2018-04-04T16:00:00.000Z');

// 通过时间对象初始化
datex({year:2008,month:8,day:8,hour:8,minute:0,second:0});

// 通过时间数组初始化
datex([2018,8,8,8,8,0]);

// 无参数初始化
datex();

10.22 时间戳及克隆

// 返回时间戳(毫秒)
datex().getTime();

// 返回时间戳(秒)
datex().getUnix();

// 克隆
datex().clone();

10.23 时间对象输出

// 返回原生Date对象
datex().toDate();

// 返回时间字段对象
datex().toObject();

// 返回时间字段数组
datex().toArray();

// 返回字符串
datex().toString();

// 返回ISO字符串
datex().toISOString();

10.24 时间格式化

datex(123456789).format('YYYY-MM-DD');

10.25 时间计算及比较

// 设置某字段值
datex(2022,10,1).set('year',2020).format();

// 增减某字段值,负值为减
datex(2022,10,1).change('year',1).format();

// 返回某字段值
datex().get('month');

// 获取某字段起始时
// 例如:获取这个月初是星期几?
datex().startOf('month').format('W');

// 获取某字段末尾时
// 例如:获取这个月有多少天?(是不是很容易理解?end of month then get day!)
datex().endOf('month').get('day');

// 与某时间点差值
// 例如:北京2008年奥运会开幕式过去多少天了?
datex().diffWith('2008-8-8','day');

// 是否在某个时间点之前
datex('2008-08-08').isBefore('2022-02-02');

// 是否在某个时间点之后
datex('2008-08-08').isAfter('2022-02-02');

// 是否和某个时间点相等
datex('2008-08-08').isSame('2018-02-02','year');

// 是否在两个时间点之间
datex('2008-08-08').isBetween('2003-07-13','2022-02-02');

10.26 有效性

datex('2008-13-12').isValid();