Skip to content
当前页导航

为什么 0.1 + 0.2 !== 0.3 ?

在 mdn 中对 Number 的定义中有写 js 采用的是 IEEE 754 标准的双精度 64 位格式表示数字。

十进制浮点数(小数)在转成二进制时,会丢失精度,所以 0.1 和 0.2 的二进制表示形式是不准确的,当它们俩存下来的时候,就发生了精度丢失,所以在计算时,使用的其实是精度丢失后的数。这样结果就不准确了。

如何解决:

  • 利用 toFixed 方法,四舍五入
js
let num = 0.1 + 0.2
num = Number(num.toFixed(1))
  • 将数字乘以 10 ,转成整数后计算,再把结果除以 10

js 运算符

  • ** 幂运算符,它等价于 Math.pow(),不同之处在于,它还接受 BigInt 作为操作数。
js
console.log(2 ** 3) // 8
console.log((2 ** 3) ** 2) // 64
console.log(3n ** 2n) // 9n
  • **= 幂赋值,把算出来的幂结果,重新赋值给左边的变量,类似 +=
js
let a = 3
console.log((a **= 2)) // 9
console.log((a **= 0)) // 1
  • &&= 逻辑赋值,当操作符左边为真时,把右边的数据赋值给左边
js
let a = 1
let b = 0

a &&= 2
console.log(a) // 2

b &&= 2
console.log(b) // 0
  • ||= 逻辑赋值,当操作符左边为假时,把右边的数据赋值给左边
js
let a = 1
let b = 0

a ||= 2
console.log(a) // 1

b ||= 2
console.log(b) // 2
  • ?? 空值合并运算符,当左侧数据为 null 或者 undefined 时,返回右侧数据,否则返回左侧数据。
js
const foo = null ?? 'default string'
console.log(foo) // 'default string'

const baz = 0 ?? 42
console.log(baz) // 0
  • ??= 逻辑空赋值运算符,当左侧数据为 null 或者 undefined 时,把右侧数据赋值给左侧数据。
js
const a = { duration: 50 }

a.duration ??= 10
console.log(a.duration) // 50

a.speed ??= 25
console.log(a.speed) // 25
  • ?. 可选链运算符,访问可能不存在的对象属性时,返回 undefined
js
const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
}

const dogName = adventurer.dog?.name
console.log(dogName) // undefined

把类数组对象转成数组的方法有哪些

  • 使用 Array.form 方法
js
const obj = { 0: 'a', 1: 'b', length: 2 }
Array.form(obj)
  • 使用数组的 slice 方法
js
;[].slice.call(obj)
  • 使用扩展运算符 ...
  • for 循环

箭头函数的特点

  • 没有 arguments
  • 没有 new.target 和 super
  • 没有原型对象 prototype
  • 没有自己的 this ,它指向父级作用域的 this
  • call、apply、bind 方法没法改变箭头函数的 this
  • 不可以用作构造函数

函数的 arguments 对象

正常模式下,arguments对象可以在运行时修改

正常模式下,arguments对象带有一个callee属性,返回它所对应的原函数。可以通过 arguments.callee,达到调用函数自身的目的。这个属性在严格模式里面是禁用的,因此不建议使用。

严格模式下,arguments 对象与函数参数不具有联动关系。也就是说,修改 arguments 对象不会影响到实际的函数参数。

js
// 正常模式下
var f = function (a, b) {
  arguments[0] = 3
  arguments[1] = 2
  return a + b
}

f(1, 1) // 5

function foo(num1, num2) {
  arguments[1] = 3
  console.log(arguments) // [2, 3]
  console.log(num1) // 2
  console.log(num2) // undefined
  console.log(arguments[0] + arguments[1]) // 5
}
foo(2)

// 严格模式
;('use strict')
function f(a, b) {
  arguments[0] = 3
  console.log(arguments) // [3, 2]
  return a * b
}
console.log(f(1, 2)) // 2

// callee 一般用在递归的时候,防止原函数的函数名被更改,但是在严格模式下,这个属性被禁用,所以不推荐使用
function f(num) {
  if (num <= 1) return num
  return num + f(num - 1)
}
console.log(f(6)) // 21
function f(num) {
  if (num <= 1) return num
  return num + arguments.callee(num - 1)
}
console.log(f(6)) // 21
// 演示函数名被更改
var f = function (num) {
  if (num <= 1) return num
  // return num + f(num - 1) // 这里使用f就会报错
  return num + arguments.callee(num - 1)
}
var f2 = f
f = null
console.log(f2(6)) // 21
// 演示防止函数名被更改,且不使用callee
var foo = function f(num) {
  if (num <= 1) return num
  return num + f(num - 1)
}
foo(6)

函数的 caller 属性

这个属性返回调用当前函数的函数,如果是在全局作用域中调用则为 null 。在严格模式下,此属性会报错。

它跟 arguments.callee.caller 返回的结果是一样的

js
function inner() {
  console.log(inner.caller)
  // console.log(arguments.callee.caller)
}
function outer() {
  inner()
}

outer()
// ƒ outer() {
//   inner()
// }

inner() // null

new.target 属性

用来判断一个函数是否是通过 new 出来的

js
function f() {
  if (new.target) {
    console.log(new.target.name)
  } else {
    throw '请使用new生成一个实例'
  }
}
// const a = f() // 报错
const b = new f() // f

递归是什么,递归的优缺点,如何优化递归

优点:代码简洁,易于理解

缺点:

  1. 时间和空间的消耗比较大

递归由于是函数调用自身,而函数的调用时消耗时间和空间的,每一次函数调用,都需要在内存栈中分配空间以保存参数,返回值和临时变量,而往栈中压入和弹出数据也都需要时间,所以降低了效率。

  1. 重复计算

递归中又很多计算都是重复的,递归的本质时把一个问题分解成两个或多个小 问题,多个小问题存在重叠的部分,即存在重复计算,如斐波那契数列的递归实现。

  1. 调用栈溢出

递归可能时调用栈溢出,每次调用时都会在内存栈中分配空间,而栈空间的容量是有限的,当调用的次数太多,就可能会超出栈的容量,进而造成调用栈溢出。

如何优化递归:

利用尾调用优化。因为由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。

尾调用优化

ES6 新增了一个内存优化管理机制,让 js 在满足条件的情况下可以重用栈帧。这项优化非常适合尾调用。

什么是尾调用:外部函数的返回值是一个内部函数的返回值。

看一下优化的例子:

js
function a() {
  return b()
}
function b() {
  return 1
}

在 ES6 未优化之前,上面例子在内存中执行的操作:

  • js 执行到 a 函数,第一个栈帧被推到栈上。
  • 执行 a 函数的内容,直到 return 语句。计算返回值,这时要先执行 b 函数。
  • 执行到 b 函数,第二个栈帧被推到栈上。
  • 执行 b 函数的内容,计算返回值。
  • 将返回值传递给 a 函数,然后 a 再返回值
  • 将 a 和 b 函数的栈帧弹出去

在 ES6 优化之后,上面例子在内存中执行的操作

  • js 执行到 a 函数,第一个栈帧被推到栈上。
  • 执行 a 函数的内容,直到 return 语句。计算返回值,这时要先执行 b 函数。
  • js 引擎发现把第一个栈帧 a 函数先弹出去也没有问题,因为 b 函数的返回值就是 a 函数的返回值
  • 弹出 a 函数的栈帧
  • 执行到 b 函数,栈帧被推到栈上。
  • 执行 b 函数的内容,计算返回值。
  • 将 b 函数的栈帧弹出去

对比:优化之前每多调用一次嵌套函数,就会增加一个栈帧。而优化之后,无论调用多少次,都只有一个栈帧。这就是尾调用优化的关键。

尾调用优化的条件:

  • 开启严格模式
  • 外部函数的返回值是对尾调用函数的调用
  • 尾调用函数返回(return)后,没有其他的逻辑(代码)
  • 尾调用函数内没有引用外部作用域中的变量

TIP

为什么要开启严格模式,因为在非严格模式下,函数中允许使用函数的 arguments 和 caller 属性,它们两者都会引用外部的栈帧,不符合优化条件。

js
// 没有优化的尾调用
function b() {
  return 1
}

// 无优化:尾调用没有返回
function a() {
  b()
}

// 无优化:尾调用没有直接返回
function a() {
  const c = b()
  return c
}

// 无优化:尾调用返回后还执行了toString()
function a() {
  return b().toString()
}

// 无优化:尾调用是一个引用了外部变量的闭包
function a() {
  let age = 2
  function c() {
    return age
  }
  return c()
}
js
// 符合优化的尾调用

// 有优化:栈帧销毁前执行了参数计算
function foo(a, b) {
  return inner(a + b)
}

// 有优化,初始返回值不涉及栈帧
function foo(a, b) {
  if (a < b) {
    return a
  }
  return inner(a + b)
}

// 有优化,两个函数都是在尾部
function foo(bol) {
  return bol ? a() : b()
}

利用尾调用优化斐波那契数列

js
// 没有优化之前。一般运行到 n 为 45 就需要近7秒,
// 运行到fibonacci(50) 会出现浏览器假死现象,毕竟递归需要堆栈,数字过大内存不够。
function fibonacci(n) {
  if (n < 2) {
    return 1
  }
  return fibonacci(n - 2) + fibonacci(n - 1)
}
console.log(fibonacci(6))

// 优化之后,当 n 为 1000 多的时候也只有近 20ms
;('use strict')
function fib(n, current = 0, next = 1) {
  if (n < 2) {
    return next
  }

  return fib(n - 1, next, current + next)
}
console.log(fib(6))

数组拉平

js
// 方法1:通过数组的 flat 方法,会移除数组中的空项。会返回一个新数组。
let arr = [1, 2, [3, 4, [5, 6]]]

// 拉平1层
let res = arr.flat(1)
// 拉平所有
arr.flat(Infinity)

// 方法2,利用 reduce 和concat写一个递归 ,当reduce中的值不为数组时,直接合并到reduce的总数中,当值为数组时,递归当前方法
let arr2 = [1, 2, , [3, 4, [5, 6]]]
function flatDeep(arr, depth = 1) {
  if (depth > 0) {
    return arr.reduce((acc, val) => {
      if (Array.isArray(val)) {
        return acc.concat(flatDeep(val, depth - 1))
      } else {
        return acc.concat(val)
      }
    }, [])
  } else {
    return arr.slice()
  }
}
console.log(flatDeep(arr2, Infinity)) // [1, 2, 3, 4, 5, 6]  会把空的占位符给去掉

// 方法3,利用循环加递归
let arr2 = [1, 2, , [3, 4, [5, 6, [7, 8]]]]
function flatDeep(arr, depth = 1) {
  let result = []
  if (depth > 0) {
    ;(function flat(arr, depth) {
      for (const item of arr) {
        if (Array.isArray(item)) {
          flat(item, depth - 1)
        } else {
          result.push(item)
        }
      }
    })(arr, depth)
  } else {
    return arr.slice()
  }
  return result
}
console.log(flatDeep(arr2, Infinity)) // [1, 2, undefined, 3, 4, 5, 6, 7, 8]

//方法4,利用 数组的 some 和 concat 方法
const arr = [1, 2, , [3, 4, [5, 6]]]

function flatten(arr, depth) {
  // 循环结束条件为:arr 中不包含数组
  while (arr.some((item) => Array.isArray(item)) && depth > 0) {
    depth--
    arr = [].concat(...arr)
  }

  return arr
}

console.log(flatten(arr, 5)) //  [1, 2, undefined, 3, 4, 5, 6]

深拷贝和浅拷贝

  • 浅拷贝:它是拷贝一个对象的时候,如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存中指向此对象的指针,所以如果其中一个对象改变了,另一个对象也就会跟着改变
  • 深拷贝:它会将属性是引用类型的值,从堆内存中完整的拷贝一份出来,从堆内存中开辟一个新区域存放新对象,这样修改任何一个对象都不会影响另一个对象。

浅拷贝的方式

md
1. Object.assign()

- 不会拷贝对象的继承属性
- 不会拷贝对象的不可枚举属性
- 可以拷贝 Symbol 类型的属性

2. 展开运算符 ...
3. 数组的 concat 方法
4. 数组的 slice 方法
5. 直接赋值,例如 const a = {age: 18}; const b = a

深拷贝的方式

  • 较完美的拷贝方式,可以拷贝以下数据类型,以及循环引用的问题。不考虑有 dom 元素
js
let c = {}

let a = {
  name: '狄仁杰',
  age: 28,
  sex: '',
  food: ['香蕉', '苹果'],
  othe: { weight: '20kg', color: 'red' },
  reg: /ab/,
  time: new Date(),
  childr: c,
  say() {
    console.log(12)
  },
}

c.parent = a

function deepClone(obj, map = new WeakMap()) {
  if (obj === null || obj === undefined) return obj
  if (typeof obj !== 'object') return obj
  if (obj instanceof RegExp) return new RegExp(obj)
  // 需要 拷贝 dom 元素,就加下面这个
  // if (obj instanceof HTMLElement) return obj.cloneNode(true)

  // 检测 map 中是否已经有了该引用对象,如果有,说明是循环引用,则直接返回就行。
  if (map.get(obj)) {
    // 这里是防止循环引用,如果已经有了,直接打断返回就行
    return obj
  }

  map.set(obj, obj) // 往 map 存入引用对象

  let cloneObj = new obj.constructor()

  for (const key in obj) {
    if (Object.hasOwnProperty.call(obj, key)) {
      cloneObj[key] = deepClone(obj[key], map)
    }
  }

  return cloneObj
}

let b = deepClone(a)
console.log('b =>', b)
b.age = 12
console.log('a =>', a)
console.log('b =>', b)

其他深拷贝的方式,但是都有各自的缺点。

  • JSON.parse(JSON.stringify(obj))

    • 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;
    • 拷贝 Date 引用类型会变成字符串;
    • 无法拷贝不可枚举的属性;
    • 无法拷贝对象的原型链;
    • 拷贝 RegExp 引用类型会变成空对象;
    • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
    • 无法拷贝对象的循环引用,即 a 对象引用 b 对象, b 对象引用 a 对象。循环引用会导致堆栈溢出,会报错(Maximum call stack size exceeded)
  • js 原生 api structuredClone() 方法,structuredClone(obj)

    • 无法在 babel 中使用,函数、DOM 节点、属性描述符、setter、getter 不能拷贝
    js
    const obj = {
      say() {},
    }
    // // 报错  failed to execute 'structuredClone' on 'Window': say() {} could not be cloned.
    const end = structuredClone(obj)
    
    const obj = {
      a: document.getElementsByTagName('title')[0],
    }
    // 报错  failed to execute 'structuredClone' on 'Window': HTMLTitleElement object could not be cloned.
    const end = structuredClone(obj)

    数组去重

  1. 无对象的数组去重
js
// 使用ES6新增的Set对象,去除数组中的重复值。
let arr = [1, 2, 2, 3, 4, 4, 5]
let newArr = [...new Set(arr)]
console.log(newArr) // [1, 2, 3, 4, 5]

/*
使用filter和indexOf方法遍历原数组,
对于每一个元素,判断它在原数组中第一次出现的位置是否等于当前位置,相同则保留,不同则去除。
*/
let arr = [1, 2, 2, 3, 4, 4, 5]
let newArr = arr.filter((item, index, arr) => {
  return arr.indexOf(item) === index
})
console.log(newArr) // [1, 2, 3, 4, 5]

/*
使用reduce方法:定义一个新的空数组作为初始值,遍历原数组,
将每个元素与新数组进行比较,如果不存在则将其添加到新数组中。
*/
let arr = [1, 2, 2, 3, 4, 4, 5]
let newArr = arr.reduce((prev, cur) => {
  if (!prev.includes(cur)) {
    prev.push(cur)
  }
  return prev
}, [])
console.log(newArr) // [1, 2, 3, 4, 5]

/*
使用Object对象:利用对象的属性唯一性,遍历原数组,
将每个元素作为对象的属性名和属性值,如果已存在则不操作,不存在则将其添加到新数组中。
*/
let arr = [1, 2, 2, 3, 4, 4, 5]
let newObj = {}
let newArr = []
for (let i = 0; i < arr.length; i++) {
  if (!newObj[arr[i]]) {
    newObj[arr[i]] = arr[i]
    newArr.push(arr[i])
  }
}
console.log(newArr) // [1, 2, 3, 4, 5]

/*
使用Map对象:类似于使用对象,但使用Map对象实现更简单。
遍历原数组,将每个元素作为Map对象的键和值,如果已存在则不操作,不存在则将其添加到新数组中。
*/
let arr = [1, 2, 2, 3, 4, 4, 5]
let newMap = new Map()
let newArr = []
for (let i = 0; i < arr.length; i++) {
  if (!newMap.has(arr[i])) {
    newMap.set(arr[i], arr[i])
    newArr.push(arr[i])
  }
}
console.log(newArr) // [1, 2, 3, 4, 5]
  1. 对象数组去重
js
/*
利用reduce()方法去重
使用Array.prototype.reduce()方法遍历数组中的每个元素,并将其与之前的元素进行比较。
如果当前元素在之前的结果数组中不存在,则将其添加到结果数组中。
*/
let arr = [
  { id: 1, name: 'Tom' },
  { id: 2, name: 'Jerry' },
  { id: 1, name: 'Tom' },
]
let result = arr.reduce((prev, cur) => {
  let ids = prev.map((item) => item.id)
  if (ids.indexOf(cur.id) === -1) {
    prev.push(cur)
  }
  return prev
}, [])
console.log(result) // [{id: 1, name: 'Tom'}, {id: 2, name: 'Jerry'}]

/* 
利用对象的属性名去重
利用了对象的属性名不能相同的特性,遍历数组将其作为对象的属性存储,
当出现重复元素时,由于对象属性已经存在,不会进行覆盖操作,因此可以实现数组中对象的去重。
*/
let arr = [
  { id: 1, name: 'Tom' },
  { id: 2, name: 'Jerry' },
  { id: 1, name: 'Tom' },
]
let obj = {}
let result = []
for (let i = 0; i < arr.length; i++) {
  if (!obj[arr[i].id]) {
    obj[arr[i].id] = true
    result.push(arr[i])
  }
}
console.log(result) // [{id: 1, name: 'Tom'}, {id: 2, name: 'Jerry'}]

/*
利用ES6中的Map数据结构去重
遍历数组将其作为key存储到Map对象中,当出现重复元素时,
则将该元素对应的value设置为true。最后将value为false的元素筛选出来得到结果数组。
*/
let arr = [
  { id: 1, name: 'Tom' },
  { id: 2, name: 'Jerry' },
  { id: 1, name: 'Tom' },
]
let map = new Map()
let result = []
for (let i = 0; i < arr.length; i++) {
  if (!map.has(arr[i].id)) {
    map.set(arr[i].id, true)
    result.push(arr[i])
  }
}
console.log(result) // [{id: 1, name: 'Tom'}, {id: 2, name: 'Jerry'}]

如何判断对象是否是通过 proxy 代理的

可以通过修改 Object.prototype.toString.call 返回的数据,来实现,而要修改 Object.prototype.toString.call 返回的数据,则需通过 Symbol.toStringTag 。

对象的 Symbol.toStringTag 属性,指向一个方法。在该对象上面调用 Object.prototype.toString 方法时,如果这个属性存在,它的返回值会出现在 toString 方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制 [object Object] 或 [object Array] 中 object 后面的那个字符串。

js
// 代理 Proxy 本身,然后在赋值给 Proxy
Proxy = new Proxy(Proxy, {
  // 拦截 new 操作符,更改属性
  construct(target, args) {
    // result 是 new Proxy() 生成的原本的实例
    const result = new target(...args)
    // 获取原本实例的类型
    const oldToStringTag = Object.prototype.toString
      .call(result)
      .slice(1, -1)
      .split(' ')[1]
    // 改写 result ,手动添加被 proxy 代理的标志
    result[Symbol.toStringTag] = 'Proxy-' + oldToStringTag
    return result
  },
})

const target = { age: 1 }
const proxy = new Proxy(target, {})
console.log(Object.prototype.toString.call(proxy)) // [object Proxy-Object]

const proxy2 = new Proxy(proxy, {})
console.log(Object.prototype.toString.call(proxy2)) // [object Proxy-Proxy-Object]

cookie、localStorage 和 sessionStorage 的区别

类型存储容量生命周期数据发送访问权限API 使用
cookie每个域名下通常为 4KB 左右,每个 Cookie 的大小也有限制,一般为几 KB。可以设置过期时间,不设置的话,会在浏览器关闭后被删除。每个 HTTP 请求都会带上相应的 Cookie 信息,用于与服务器进行交互。具有域名限制,可以在指定域名和路径下访问。通过 document.cookie 设置
localStorage每个域名下通常为 5MB 或更多。除非主动清除或通过 JavaScript 删除,否则数据将一直保存在客户端,即使浏览器关闭。数据存储在客户端,不会自动随每个 HTTP 请求发送到服务器。在同一域名下共享访问权限,每个域名有独立的存储空间。setItem、getItem 等
sessionStorage每个域名下通常为 5MB 或更多。数据仅在当前会话期间有效。当用户关闭浏览器标签页,新开标签页,关闭浏览器时,数据将被删除。数据存储在客户端,不会自动随每个 HTTP 请求发送到服务器。在同一域名下共享访问权限,每个域名有独立的存储空间。setItem、getItem 等
  • sessionStorage
    • 打开多个相同的 URL 的 Tabs 页面,会创建各自的 sessionStorage,互不影响。
    • 刷新页面,不会丢失缓存。
    • 在当前页面,通过 window.open 或者 a 标签的 _target="blank" 打开新页面或者标签,会共享 sessionStorage

只能一个个设置,不存在的数据是追加,已存在的数据是替换。

  1. Path 属性:通过设置 Path 属性,可以限制 Cookie 只在特定路径下可用。默认情况下,Cookie 适用于设置它们的页面所在的路径。
js
document.cookie = 'cookieName=cookieValue; Path=/path'
  1. Expires 或 Max-Age 属性:通过设置 Expires 属性或 Max-Age 属性,可以指定 Cookie 的过期时间。Expires 属性是一个日期/时间字符串,指示 Cookie 何时过期。Max-Age 属性是一个以秒为单位的整数,指示 Cookie 从当前时间开始多久后过期。
js
// 设置Cookie在2024年8月31日过期:
document.cookie =
  'cookieName=cookieValue; Expires=Fri, 31 Aug 2024 23:59:59 GMT'

// 设置Cookie在10分钟后过期:
var expirationDate = new Date()
expirationDate.setTime(expirationDate.getTime() + 10 * 60 * 1000)
document.cookie =
  'cookieName=cookieValue; Expires=' + expirationDate.toUTCString()

需要注意的是,Expires 属性和 Max-Age 属性只能在创建或更新 Cookie 时设置,无法通过 JavaScript 代码修改已存在的 Cookie 属性。

另外,如果你想立即删除一个 Cookie,可以将 Expires 属性设置为一个过去的日期。

请注意,在设置过期时间时,要使用合适的日期/时间格式,并考虑到不同浏览器对日期/时间格式的支持。同时,要注意在设置 Path 属性时选择适当的路径,以确保 Cookie 在需要的范围内可访问。

  1. 安全传输:
  • Cookie:每个 HTTP 请求都会自动携带相应域名下的 Cookie 信息,可能被拦截或窃取。
  • localStorage:数据存储在客户端,不会自动发送到服务器,因此不易被拦截或窃取。
  1. 存储敏感信息:
  • Cookie:将 Secure 属性设置为 true,可以设置为只能通过 HTTPS 传输,document.cookie = "cookieName=cookieValue; Secure"。这样可以确保 Cookie 只在受信任的加密连接中传输,并且不能被恶意脚本访问。将 HttpOnly 属性设置为 true,可以防止客户端的 JavaScript 代码修改 Cookie,document.cookie = "cookieName=cookieValue; HttpOnly"。这样可以减少 XSS 攻击的风险,因为恶意脚本无法访问和修改具有 HttpOnly 属性的 Cookie。
  • localStorage:默认情况下,localStorage 没有安全限制,因此存储敏感信息时需要额外的处理,例如加密数据或使用其他安全机制。
  1. 跨站脚本攻击(XSS)、跨站请求伪造(CSRF)攻击:
  • Cookie:由于每个 HTTP 请求都会自动携带 Cookie 信息,如果存在攻击,攻击者可以获取并利用 Cookie 中的敏感信息。
  • localStorage:localStorage 存储在客户端,不会自动发送到服务器,所以它对攻击的影响较小。但是,仍然要注意防止通过恶意脚本访问和修改 localStorage 数据。

为了增加数据的安全性,无论是使用 Cookie 还是 localStorage,都应该采取以下措施:

  • 使用 HTTPS 协议传输数据,确保数据在网络传输过程中的安全性。
  • 对存储的敏感信息进行加密处理。
  • 在设置 Cookie 或存储数据时,限制域名和路径,使其只在必要的范围内可访问。
  • 定期清理过期的 Cookie 和不再需要的 localStorage 数据。

在安全性方面,如果涉及到敏感信息的存储和传输,推荐使用 Cookie。Cookie 具有以下优势:

  1. 服务器端控制:Cookie 是由服务器生成并发送给客户端的,因此服务器可以更好地控制 Cookie 的安全性。可以设置 Cookie 的属性(如 Secure、HttpOnly)来增强安全性,并且可以通过设置过期时间限制 Cookie 的有效期。

  2. 自动发送:每个 HTTP 请求都会自动携带相应域名下的 Cookie 信息,这个特性使得 Cookie 在某些情况下更便于数据的传输和识别。

  3. 可以跨域访问:Cookie 在同一域名下共享访问权限,可以被不同页面或子域名下的请求所使用,而 localStorage 只能在同一域名下访问。

如果主要关注数据存储的安全性,尤其是防止 XSS 攻击和 CSRF 攻击,推荐使用 localStorage。

  • localStorage 存储在客户端,不会自动发送到服务器,因此可以减少安全风险。

实现一个函数,可以将数组转化为树状数据结构

js
const arr = [
  { id: 1, name: 'i1' },
  { id: 2, name: 'i2', parentId: 1 },
  { id: 4, name: 'i4', parentId: 3 },
  { id: 3, name: 'i3', parentId: 2 },
  { id: 8, name: 'i8', parentId: 7 },
]

function buildTree(arr) {
  let roots = []
  arr.forEach((item) => {
    if (!item.parentId) {
      roots.push(item)
    }
  })
  let tree = handleChildren(roots, arr)
  return tree
}

function handleChildren(roots, arr) {
  roots.forEach((item) => {
    let filt = arr.filter((val) => {
      return val.parentId === item.id
    })
    item.children = filt
    if (item.children.length > 0) {
      handleChildren(item.children, arr)
    }
  })
  return roots
}

buildTree(arr)

惰性函数

函数有很多分支需要判断,但是只有在第一次调用的时候才会执行,执行后会重新修改此函数,再次调用时就不用判断。这就是惰性函数

改造前,不是惰性函数

js
// 实现获取 HTML 标签的样式,getComputedStyle 方法是新浏览器中的方法, el.currentStyle 方法是兼容IE8及以下的浏览器
var p = document.getElementsByTagName('p')[0]
function getCss(el, attr) {
  if ('getComputedStyle' in window) {
    console.log('getComputedStyle')
    return window.getComputedStyle(el)[attr]
  } else {
    console.log('currentStyle')
    return el.currentStyle[attr]
  }
}
// 这样每次都会输出 getComputedStyle  或  currentStyle
console.log(getCss(p, 'color'))
console.log(getCss(p, 'width'))
console.log(getCss(p, 'height'))

改造后,变为了惰性函数

js
var p = document.getElementsByTagName('p')[0]
function getCss(el, attr) {
  if ('getComputedStyle' in window) {
    console.log('getComputedStyle')
    getCss = function (el, attr) {
      return window.getComputedStyle(el)[attr]
    }
  } else {
    console.log('currentStyle')
    getCss = function (el, attr) {
      return el.currentStyle[attr]
    }
  }
  return getCss(el, attr)
}
// 下面三次打印,最终只会输出一次  getComputedStyle  或  currentStyle
console.log(getCss(p, 'color'))
console.log(getCss(p, 'width'))
console.log(getCss(p, 'height'))

实现一个可缓存的函数

输入相同的值,永远会返回同一个结果,没有明显的副作用。纯函数可以用来缓存已经执行过的函数的结果

js
let memoize = function (fn) {
  let cache = {}
  return function () {
    //把传入的参数作为对象的key值
    let key = JSON.stringify(arguments)
    //如果有对应的key,则直接取缓存cache里的值,没有则执行函数
    //fn.apply(fn, arguments) 利用apply是防止fn的指向出现问题,直接把fn绑在自己身上。函数式编程尽量不要使用this
    cache[key] = cache[key] || fn.apply(fn, arguments)
    return cache[key]
  }
}

function sum(a, b) {
  console.log('执行了')
  return a + b
}

let newf = memoize(sum)

console.log(newf(1, 2))
console.log(newf(1, 2))
console.log(newf(3, 2))

柯里化函数

对于一个有多个参数的函数,当只传入其中部分函数时,会返回一个新的函数来接收剩余的参数,直到返回结果

js
function summation(a, b, c) {
  return a + b + c
}

// 模拟  _.curry()柯里化 的实现
function curry(fn) {
  return function part(...arg) {
    // 判断实参和形参的个数是否相等
    if (arg.length < fn.length) {
      //实参少了
      return function () {
        //把前一次的参数和后一次的参数合并起来,再执行part函数,去判断实参和形参的个数是否相等
        let arr = [...arg, ...arguments]
        return part(...arr)
      }
    }
    return fn(...arg) //实参和形参的个数相等
  }
}

let summaSky = curry(summation)

console.log(summaSky(1)(2, 3))

组合函数(compose)

将函数串联起来执行,一个函数的输出结果是另一个函数的输入参数,即第一个函数的返回值为第二个函数的参数,以此类推

js
// 先看普通函数
const fn1 = (x, y) => x + y
const fn2 = (x) => x + 10
const fn3 = (x) => x * 2
// 下面这种写法,嵌套严重,阅读起来很不方便
let res = fn3(fn2(fn1(1, 2)))
console.log(res) // 26

// 改成函数组合方式书写
function compose(...funs) {
  return function (...args) {
    if (funs.length === 0) return args
    if (funs.length === 1) return funs[0](...args)
    return funs.reduce((res, fn) => {
      return Array.isArray(res) ? fn(...res) : fn(res)
    }, args)
  }
}

let res1 = compose(fn1, fn2, fn3)(1, 2)
console.log(res1) // 26

[‘1‘,‘2‘,‘3‘].map(parseInt)输出结果

首先要明确两点:

  1. map 方法中的回调函数,第一个参数是数组的每一项,第二个参数是每一项的下标。
  2. parseInt 方法第一个参数是目标字符串,第二个参数是以哪个进制进行解析,默认不传是 10 进制,或者传 undefined 和 0 ,也是 10 进制,能传的范围为 2-36 之间,传其他值都返回 NaN。
js
console.log(['1', '2', '3'].map(parseInt))

// 第一步 parseInt('1', 0) , 第二个参数为 0 ,所以会根据十进制来解析,  => 1
// 第二步 parseInt('2', 1) , 第二个参数为 1 ,不在参数区间范围,  => NaN
// 第二步 parseInt('3', 2) , 第二个参数为 2 ,用二进制来解析,但是二进制只能是 0 和 1 组成,  => NaN

// 最终结果为 [1, NaN, NaN]

为什么大厂不允许直接定义一个变量为 undefined 。

因为 undefined 不是一个关键字,js 允许定义一个名为 undefined 的变量。所以当不在全局作用域下,会出现预估之外的结果。

js
;(function () {
  const undefined = 1
  const a = undefined
  console.log(a) // 1
})()

// 所以应养成一个习惯,当想定义一个 undefined 的变量时,应用 void 定义,它会直接返回 undefined
;(function () {
  const undefined = 1
  const a = void 0
  console.log(a) // undefined
})()

Map 和 WeakMap

Map

Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值) 都可以作为一个键或一个值。

Map 在初始化的时候,也可以接受一个包含键值对的二维数组作为参数。

js
const map1 = new Map()

map1.set('a', 1)
console.log(map1.get('a'))

const map = new Map([
  ['name', '张三'],
  ['title', 'Author'],
])
console.log(map) // Map(2) {"name" => "张三", "title" => "Author"}

Object 和 map 的比较

map

选择 Object 还是 Map

对于多数 Web 开发任务来说,选择 Object 还是 Map 只是个人偏好问题,影响不大。不过,对于在乎内存和性能的开发者来说,对象和映射之间确实存在显著的差别。

  1. 内存占用

Object 和 Map 的工程级实现在不同浏览器间存在明显差异,但存储单个键/值对所占用的内存数量都会随键的数量线性增加。批量添加或删除键/值对则取决于各浏览器对该类型内存分配的工程实现。不同浏览器的情况不同,但给定固定大小的内存, Map 大约可以比 Object 多存储 50%的键/值对。

  1. 插入性能

向 Object 和 Map 中插入新键/值对的消耗大致相当,不过插入 Map 在所有浏览器中一般会稍微快一点儿。对这两个类型来说,插入速度并不会随着键/值对数量而线性增加。如果代码涉及大量插入操作,那么显然 Map 的性能更佳。

  1. 查找速度

与插入不同,从大型 Object 和 Map 中查找键/值对的性能差异极小,但如果只包含少量键/值对,则 Object 有时候速度更快。在把 Object 当成数组使用的情况下(比如使用连续整数作为属性),浏览器引擎可以进行优化,在内存中使用更高效的布局。这对 Map 来说是不可能的。对这两个类型而言,查找速度不会随着键/值对数量增加而线性增加。如果代码涉及大量查找操作,那么某些情况下可能选择 Object 更好一些。

  1. 删除性能

使用 delete 删除 Object 属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此。为此,出现了一些伪删除对象属性的操作,包括把属性值设置为 undefined 或 null 。但很多时候,这都是一种讨厌的或不适宜的折中。而对大多数浏览器引擎来说, Map 的 delete() 操作都比插入和查找更快。如果代码涉及大量删除操作,那么毫无疑问应该选择 Map 。

WeakMap

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

WeakMap 和 Map 的区别

  • WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名。 Map 则可以接受任何类型作为键。

WeakMap 实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值。如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了。

  • 在 Map 中,当一个对象用作键时,它与该键的引用关系是强引用关系。这意味着,即使没有其他引用指向该对象,它作为 Map 的键仍然会被保留在内存中。而在 WeakMap 中,当一个对象用作键时,它与该键的引用关系是弱引用关系。这意味着,如果没有其他的强引用指向该对象,它作为 WeakMap 的键可能会被垃圾回收机制清除掉。

  • 在 Map 中,可以使用迭代器来遍历 Map 的键值对。而在 WeakMap 中,由于键的弱引用关系,无法提供迭代器来直接遍历键值对。

实现基本集合操作

并集

js
let a = new Set([1, 2, 3])
let b = new Set([2, 3, 4])

// 求并集 两种方法
const union = (a, b) => {
  return new Set([...a, ...b])
}
const union = (a, b) => {
  let _union = new Set(a)
  for (let item of b) {
    _union.add(item)
  }
  return _union
}

console.log(union(a, b)) // Set(4) {1, 2, 3, 4}

交集

js
let a = new Set([1, 2, 3])
let b = new Set([2, 3, 4])

// 求交集 两种方法
const intersection = (a, b) => {
  return new Set([...a].filter((val) => b.has(val)))
}
const intersection = (a, b) => {
  const _intersection = new Set()
  for (const item of b) {
    if (a.has(item)) {
      _intersection.add(item)
    }
  }
  return _intersection
}

console.log(intersection(a, b)) // Set(2) {2, 3}

差集

js
let a = new Set([1, 2, 3])
let b = new Set([2, 3, 4])

// 求差集 两种方法  a 相对于 b 的差集
const difference = (a, b) => {
  return new Set([...a].filter((val) => !b.has(val)))
}
const difference = (a, b) => {
  const _difference = new Set(a)
  for (let item of b) {
    _difference.delete(item)
  }
  return _difference
}

console.log(difference(a, b)) // Set(1) {1}

window 中的各种宽高属性

md
1、window.innerWidth:  返回页面内容区域的宽度,包含浏览器滚动条,number

2、window.outerWidth:  返回整个浏览器的宽度,包含浏览器所有,工具栏、标签栏等,number

3、el.clientWidth:  返回当前元素的  content + padding,有滚动条会减去滚动条的宽度,number

4、el.offsetWidth:  返回当前元素的  content + padding + border,跟有无滚动条无关,number

5、el.clientWidth  和  el.offsetWidth  在元素为  documentElement  的时候,跟上述 3  和  4 情况不同,它俩的值都为    页面展示内容区域的宽度  -  浏览器滚动条

6、el.clientLeft:  返回当前元素的左边框宽度,number

7、el.clientTop:  返回当前元素的上边框宽度,number

8、el.offsetLeft:  返回当前元素与页面最左边的距离,number

9、el.offsetTop:  返回当前元素与页面最上边的距离,number

防抖和节流

防抖

防抖就是,触发一个事件不会立即执行,而会等待 n 秒后执行,如果在等待时间内,又多次触发事件,会重新计算时间。直到最后,执行的是最后一次触发点事件

适用于需要等待一段时间后执行某个操作的场景,比如输入框搜索建议、按钮点击避免重复提交、窗口的 resize、scroll 等。

html
<input oninput="handleInput(this)" type="text" />

<script>
  function http(e) {
    console.log('发起请求', e.value)
  }
  const handleInput = debounce(http)

  function debounce(fn, delay = 500) {
    let timer = null
    return function () {
      timer && clearTimeout(timer)
      timer = setTimeout(() => {
        fn.apply(this, arguments)
        timer = null
      }, delay)
    }
  }
</script>

节流

节流就是,在指定时间间隔内,只会执行一次事件(第一次的事件)过了这个时间又会执行新的时间间隔内的事件。就比如把时间分割成每 n 秒,执行一次事件。

适用于一些频繁操作,如拖拽的时候,获取元素的位置。监听 scroll 事件,获取滚动距离等。

html
<div id="div" draggable="true"></div>

<script>
  function http(e) {
    console.log('获取位置', e.pageX)
  }
  const handleDrag = throttle(http)

  function throttle(fn, delay = 500) {
    let timer = null
    return function () {
      if (timer) return
      timer = setTimeout(() => {
        fn.apply(this, arguments)
        clearTimeout(timer)
        timer = null
      }, delay)
    }
  }

  document.getElementById('div').addEventListener('drag', handleDrag)
</script>

webworker 为什么能提升 js 执行的性能

Web Workers 是一种在浏览器中运行 JavaScript 的技术,它允许创建一个或多个后台线程来执行 JavaScript 代码,而不影响主线程的性能。这样可以提升 JavaScript 的执行性能,原因如下:

  • 并行处理:Web Workers 允许你在主线程之外创建一个或多个工作线程。这意味着可以同时执行多个任务,而不会阻塞主线程。这种并行处理能力对于执行耗时的操作(如大量计算、数据处理或网络请求)特别有用。
  • 避免 UI 阻塞:在主线程上执行复杂或耗时的 JavaScript 代码可能会导致用户界面(UI)冻结或响应缓慢。通过将这些任务移至 Web Worker,主线程可以保持流畅,用户界面不会受到影响。
  • 资源共享:Web Workers 可以与主线程共享某些资源,如变量和数据,但它们在内存中是独立的。这意味着它们可以访问和操作同一份数据,而不会相互干扰。这种资源共享可以提高数据处理的效率。
  • 事件循环和消息传递:Web Workers 有自己的事件循环,这意味着它们可以处理自己的事件和定时器。主线程和工作线程之间可以通过消息传递进行通信,这种方式允许它们在不直接共享内存的情况下协同工作。
  • 优化执行环境:由于 Web Workers 在后台运行,它们可以更自由地进行垃圾回收和内存管理,这有助于优化执行环境,减少内存泄漏的风险。
  • 提高响应速度:通过将耗时任务移至 Web Worker,主线程可以更快地响应用户的操作,提高应用程序的整体响应速度。

需要注意的是,虽然 Web Workers 可以提升性能,但它们并不是万能的。例如它们不能访问某些 DOM 对象,以及它们与主线程之间的通信可能会引入延迟。创建和管理多个 Web Workers 也会带来额外的开销,而且在某些情况下,主线程和工作线程之间的通信可能会导致性能瓶颈。因此,合理地使用 Web Workers,以及在设计应用程序时考虑到这些因素,是实现性能提升的关键。

在实际项目中,Web Workers 被用于多种场景

  • 数据处理:在需要进行大量数据处理的应用中,如科学计算、数据分析或图像处理,可以将计算密集型的任务分配给 Web Workers。例如,一个图像编辑应用可能会使用 Web Worker 来处理图像的缩放、旋转或颜色转换,而主线程则负责更新用户界面。
  • 实时数据更新:在需要实时更新数据的应用中,如股票市场监控或体育比赛直播,Web Workers 可以用于轮询服务器以获取最新数据,然后将更新推送到主线程以更新 UI,而不会阻塞用户界面。
  • 离线应用:对于需要支持离线功能的 Web 应用,如离线地图或阅读器,Web Workers 可以帮助处理离线数据的加载和同步。例如,一个离线阅读应用可能会在后台线程中处理文章的下载和缓存,以便在没有网络连接时也能访问内容。
  • 音频和视频处理:在需要实时处理音频或视频的应用中,如在线音乐播放器或视频编辑器,Web Workers 可以用于执行音频效果处理、视频帧解码或其他媒体处理任务。
  • 游戏开发:在复杂的游戏开发中,Web Workers 可以用于处理游戏逻辑、AI、物理引擎等,而将渲染任务保留在主线程中。这样可以提高游戏的性能,尤其是在多玩家或实时交互游戏中。
  • Web 应用的模块化:在大型 Web 应用中,为了提高代码的可维护性和可扩展性,可以将不同的功能模块分配给不同的 Web Workers。这样可以并行加载和执行这些模块,提高应用的启动速度和响应时间。
  • 文件操作:对于需要处理大量文件或进行文件转换的应用,如在线文档编辑器或 PDF 阅读器,Web Workers 可以用来执行文件的读取、写入或转换操作。这样可以避免在主线程中进行文件操作时导致的延迟。
  • 网络请求:在需要进行大量网络请求的应用中,如数据同步或实时通信,可以使用 Web Workers 来处理这些请求。例如,一个实时聊天应用可能会在后台线程中处理消息的接收和发送,而主线程则负责更新聊天界面。
  • 长期任务:在需要执行长期任务的应用中,如视频转码、音频处理或大型数据集的导入,Web Workers 可以用来执行这些任务。例如,一个视频编辑应用可能会在后台线程中处理视频文件的转码,而用户可以在前台继续进行其他操作。

使用 ts 的时候,有没有什么心得

  • 类型注解
    • 尽可能在变量、函数参数和返回类型上使用类型注解。这有助于在编译时捕捉错误,提高代码的可读性和可维护性。
    • 使用联合类型(union types)和交叉类型(intersection types)来表达复杂的数据结构。
    ts
    const age: number = 18
  • 接口与类型别名
    • 使用接口(interface)定义对象的形状,类型别名(type)用于命名复杂的类型,如联合类型或元组。
    • 利用可选属性和索引签名来定义对象的灵活性。
  • 类与继承
    • 使用类(class)和继承来模拟面向对象编程。利用访问修饰符(public、private、protected)来控制成员的可见性。
    • 使用抽象类(abstract class)和接口来定义共享的行为和契约。
  • 泛型编程
    • 利用泛型(generics)来创建可重用的组件和函数,这样可以在不同的数据类型之间共享代码。
    • 使用泛型约束来限制泛型参数的类型,确保它们满足特定的条件。
  • 模块化
    • 使用模块(module)和命名空间(namespace)来组织代码,这有助于避免全局命名冲突,并使得代码更加模块化。
    • 利用 import 和 export 语句来导入和导出模块。
  • 类型断言
    • 在需要的时候使用类型断言(as 关键字或<Type>语法),但要谨慎使用,避免过度依赖,因为这可能会绕过类型检查。