# js

# 实现一个判断两个值是否相等

// 判断是否为对象或者数组
function isObject(obj) {
  return typeof obj === 'object' && obj !== null
}
// 全相等
function isEqual(obj1, obj2) {
  if (!isObject(obj1) || !isObject(obj2)) {
    // 值类型(注意,参与 equal 的一般不会是函数)
    return obj1 === obj2
  }
  if (obj1 === obj2) {
    return true
  }
  // 两个都是引用类型或数组,而且不相等
  // 1. 先取出 obj1 和 obj2 的 keys,比较个数
  const obj1Keys = Object.keys(obj1)
  const obj2Keys = Object.keys(obj2)
  if (obj1Keys.length !== obj2Keys.length) {
    return false
  }
  // 2. 已 obj1 为基准,和 obj2 依次递归比较
  for (let key in obj1) {
    // 比较当前 key 的 val -- 递归
    const res = isEqual(obj1[key], obj2[key])
    if (!res) {
      return false
    }
  }
  // 3. 全相等
  return true
}

# 数组的 pop push unshift shift 分别是什么

这几个方法都会对原数组进行改变。

// pop
// 改变元素组,移除最后面的元素,返回值是移除的元素
const popRes = arr.pop()
console.log(arr, popRes) // [10, 20, 30], 40

// push
// 改变原数组,向原数组末尾添加元素,返回值为添加元素后数组的长度
const pushRes = arr.push(50)
console.log(arr, pushRes) // [10, 20, 30, 40, 50], 5

// unshift
// 改变原数组,向数组的头添加元素,返回值为修改后的数组的长度
const unshiftRes = arr.unshift(1)
console.log(arr, unshiftRes) // [1, 20, 30, 40, 50], 5

// shift
// 修改原数组,从数组的头取出一个元素,返回值为取出的元素
const shiftRes = arr.shift()
console.log(arr, shiftRes) // [20, 30, 40] 10

# 数组的 API 有哪些是纯函数

纯函数就是不改变元素(没有副作用),且返回一个数组;

// concat
// 合并数组 ,返回一个新的数组
const arr1 = arr.concat([50, 60])
console.log(arr, arr1) // [10, 20, 30, 40] , [10, 20, 30, 40, 50, 60]

// map
// 对原数组的每个元素进行操作,返回处理后的数组
const arr2 = arr.map(num => num * 10)
console.log(arr, arr2) // [10, 20, 30, 40] , [100, 200, 300, 400]

// filter
// 对原数组进行过滤,返回指定函数过滤后的数据
const arr3 = arr.filter(num => num > 25)
console.log(arr, arr3) // [10, 20, 30, 40] (2) [30, 40]

// slice
// 下面操作相当于对数组的一个浅拷贝
const arr4 = arr.slice()
console.log(arr, arr4) // [10, 20, 30, 40] (4) [10, 20, 30, 40]
arr.push(50)
console.log(arr, arr4) // [10, 20, 30, 40, 50] (4) [10, 20, 30, 40]

// 非纯函数
// push pop shift unshift forEach some every reduce

# 数组 slice 和 splice 区别

  • 功能区别(slice 切片,splice 剪接)
  • 参数和返回值
  • 是否为纯函数

# slice

slice() 方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。 语法:

arr.slice([begin[, end]])

参数:

  1. begin 可选 提取起始处的索引(从 0 开始),从该索引开始提取原数组元素。 如果该参数为负数,则表示从原数组中的倒数第几个元素开始提取,slice(-2) 表示提取原数组中的倒数第二个元素到最后一个元素(包含最后一个元素)。 如果省略 begin,则 slice 从索引 0 开始。 如果 begin 大于原数组的长度,则会返回空数组。
  2. end 可选 提取终止处的索引(从 0 开始),在该索引处结束提取原数组元素。slice 会提取原数组中索引从 begin 到 end 的所有元素(包含 begin,但不包含 end)。 slice(1,4) 会提取原数组中从第二个元素开始一直到第四个元素的所有元素 (索引为 1, 2, 3的元素)。 如果该参数为负数, 则它表示在原数组中的倒数第几个元素结束抽取。 slice(-2,-1) 表示抽取了原数组中的倒数第二个元素到最后一个元素(不包含最后一个元素,也就是只有倒数第二个元素)。 如果 end 被省略,则 slice 会一直提取到原数组末尾。 如果 end 大于数组的长度,slice 也会一直提取到原数组末尾。
// slice
// 纯函数,不改变原数组
// 不传参数,就默认截取所有的,也就是相当于复制了一份数组
const arr1 = arr.slice() // [10, 20, 30, 40, 50]
// 从第一个位置到第四个位置进行截取,截取到第四个位置结束,第四个也不包含
const arr2 = arr.slice(1, 4) // [20, 30, 40]
// 只传一个参数就是从指定位置起截取到数组末尾
const arr3 = arr.slice(2) // [30, 40, 50]
// 负值-如果只传一个参数,表示从倒数第几个元素开始提取
const arr4 = arr.slice(-2) // [40, 50]
// 表示抽取了原数组中的倒数第二个元素到最后一个元素(不包含最后一个元素,也就是只有倒数第二个元素)。
const arr5 = arr.slice(-2, -1) // [40]
console.log(arr, arr1, arr2, arr3, arr4, arr5)

# splice 非纯函数

splice() 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。

语法:

array.splice(start[, deleteCount[, item1[, item2[, ...]]]])

参数:

  1. start​ 指定修改的开始位置(从0计数)。如果超出了数组的长度,则从数组末尾开始添加内容;如果是负值,则表示从数组末位开始的第几位(从-1计数,这意味着-n是倒数第n个元素并且等价于array.length-n);如果负数的绝对值大于数组的长度,则表示开始位置为第0位。
  2. deleteCount 可选 整数,表示要移除的数组元素的个数。 如果 deleteCount 大于 start 之后的元素的总数,则从 start 后面的元素都将被删除(含第 start 位)。 如果 deleteCount 被省略了,或者它的值大于等于array.length - start(也就是说,如果它大于或者等于start之后的所有元素的数量),那么start之后数组的所有元素都会被删除。 如果 deleteCount 是 0 或者负数,则不移除元素。这种情况下,至少应添加一个新元素。
  3. item1, item2, ... 可选 要添加进数组的元素,从start 位置开始。如果不指定,则 splice() 将只删除数组元素。

返回值:

由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组。

// splice 非纯函数

// 从第一个位置起删除两个元素,包括第一个位置,然后将后面的需要添加的项从第一个位置起插入到原数组中
const spliceRes = arr.splice(1, 2, 'a', 'b', 'c')
console.log(arr, spliceRes) // [10, "a", "b", "c", 40, 50], [20, 30]

// 第一个位置起删除两个元素, 不进行插入元素
const spliceRes = arr.splice(1, 2)
console.log(arr, spliceRes) // [10, 40, 50] , [20, 30]

// 从第一个位置插入元素,不删除元素。返回值为空数组
const spliceRes = arr.splice(1, 0, 'a', 'b', 'c')
console.log(arr, spliceRes) // [10, "a", "b", "c", 20, 30, 40, 50], []

# [10, 20, 30].map(parseInt)

parseInt(string, radix) 将一个字符串 string 转换为 radix 进制的整数, radix 为介于2-36之间的数。

参数:

  1. string 要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用 ToString 抽象操作)。字符串开头的空白符将会被忽略。
  2. radix 可选 从 2 到 36,代表该进位系统的数字。例如说指定 10 就等于指定十进位。请注意,通常预设值不是 10 进位!

返回值:

从给定的字符串中解析出的一个整数。 或者 NaN,当 radix 小于 2 或大于 36 ,或第一个非空格字符不能转换为数字。 注意下面运算符,先算指数再算乘法

parseInt('123', 5) // 将'123'看作5进制数,返回十进制数38 => 15^2 + 25^1 + 3*5^0 = 38

如果 parseInt 遇到的字符不是指定 radix 参数中的数字,它将忽略该字符以及所有后续字符,并返回到该点为止已解析的整数值。 parseInt 将数字截断为整数值。 允许前导和尾随空格。

由于某些数字在其字符串表示形式中使用e字符(例如 6.022×23 表示 6.022e23 ),因此当对非常大或非常小的数字使用数字时,使用 parseInt 截断数字将产生意外结果。 parseInt不应替代Math.floor()。

如果 radix 是 undefined、0、null 或未指定的,JavaScript 会假定以下情况:

  1. 如果输入的 string以 "0x"或 "0x"(一个0,后面是小写或大写的X)开头,那么radix被假定为16,字符串的其余部分被解析为十六进制数。
  2. 如果输入的 string以 "0"(0)开头, radix被假定为8(八进制)或10(十进制)。具体选择哪一个radix取决于实现。ECMAScript 5 澄清了应该使用 10 (十进制),但不是所有的浏览器都支持。因此,在使用 parseInt 时,一定要指定一个 radix。
  3. 如果输入的 string 以任何其他值开头, radix 是 10 (十进制)。

如果第一个字符不能转换为数字,parseInt会返回 NaN。 为了算术的目的,NaN 值不能作为任何 radix 的数字。你可以调用isNaN函数来确定parseInt的结果是否为 NaN。如果将NaN传递给算术运算,则运算结果也将是 NaN。

[10, 20, 30].map(parseInt) // [10, NaN, NaN]
// 拆解
[10, 20, 30].map((num, index) => {
  return parseInt(num, index)
})

一个更严格的解析函数:

filterInt = function (value) {
  if(/^(\-|\+)?([0-9]+|Infinity)$/.test(value))
    return Number(value);
  return NaN;
}

console.log(filterInt('421'));               // 421
console.log(filterInt('-421'));              // -421
console.log(filterInt('+421'));              // 421
console.log(filterInt('Infinity'));          // Infinity
console.log(filterInt('421e+0'));            // NaN
console.log(filterInt('421hop'));            // NaN
console.log(filterInt('hop1.61803398875'));  // NaN
console.log(filterInt('1.61803398875'));     // NaN

# new Object() 和 Object.create 区别

  • {} 等同于 new Object(),原型 Object.prototype
  • Object.create(null) 没有原型
  • Object.create({...}) 可以指定原型
const obj1 = {
  a: 10
}
const obj4 = new Object(obj1)
console.log(obj1 === obj4) // true

const obj2 = new Object({
  a: 10
})
console.log(obj1 === obj2) // false

// 指定原型
// Object.create 创建一个空对象,将空对象的原型指向了传入的对象
const obj3 = Object.create({
  a: 10
})
console.log(obj1, obj2, obj3)

# 手写字符串 trim 保证浏览器兼容性

String.prototype.miTrim = function () {
  return this.replace(/^\s+/, '').replace(/\s+$/, '')
}
console.log('  a    s   '.miTrim())

# 获取多个数字中的最大值

function max() {
  const nums = Array.prototype.slice.call(arguments)
  let max = 0
  nums.forEach(n => {
    if (n > max) {
      max = n
    }
  })
  return n
}

// 或者
Math.max()

# 如何捕获 JS 中的异常

try {
  // todo
} catch (error) {
  console.error(error) // 手动捕获 catch
} finally {
  // todo
}

// 自动捕获
window.onerror = function (message, source, lineNum, colNum, error) {
  // 第一,对跨域的 js,如 CDN 的,不会有详细的报错信息
  // 第二,对于压缩的 js,还要配置 sourceMap 反查到未压缩代码的行、列
}

# 获取当前页面 url 参数

  • 传统方式,查找 location.search
  • 新 API:URLSearchParams
function query(name) {
  // 类似 arr.slice(1) 从第一个开始截取到最后
  const search = location.search.substr(1)
  const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i')
  const res = search.match(reg)
  if (res === null) {
    return null
  }
  return res[2]
}

// URLSearchParams
function query () {
  const search = location.search
  const p = new URLSearchParams(search)
  return p.get(name)
}

# 将 URL 参数解析为 JS 对象

// 传统方法,分析 search
function queryToObj() {
  const res = {}
  const search = location.search.substr(1)
  search.split('&').forEach(paramStr => {
    const arr = paramStr.split('=')
    const key = arr[0]
    const val = arr[1]
    res[key] = val
  })
  return res
}


// URLQueryParams
function queryToObj() {
  const res = {}
  const pList = new URLSearchParams(location.search)
  pList.forEach((val, key) => {
    res[key] = val
  })
  return res
}

# 手写 flatern 考虑多层级

// 简单的如果一层的话
Array.prototype.concat.apply([], arr)
// 相当于 [].concat(arr)

// 使用递归
function flat(arr) {
  // 验证 arr 中,还有没有深层数组 []
  const isDeep = arr.some(item => item instanceof Array)
  if (!isDeep) {
    return arr
  }
  const res = Array.prototype.concat.apply([], arr)
  return flat(res)
}

// 使用扩展运算符
function flat(arr) {
  const result = [];

  arr.forEach((i) => {
    if (Array.isArray(i))
      result.push(...flat(i));
    else
      result.push(i);
  })
  
  return result;
}

const res = flat([1, 3, [4, 5, [3, 5, [5, 6, [6, 7]]]], 6])
console.log(res)

# 数组去重

// 传统方式
function unique(arr) {
  const res = []
  arr.forEach(item => {
    // indexOf 也相当于循环
    if (res.indexOf(item) < 0) {
      res.push(item)
    }
  })
  return res
}

const res = unique([1, 3, 55, 4, 3, 1, 44, 55, 4])
console.log(res)

// 使用 Set 无序,不能重复
function unique(arr) {
  return [...new Set(arr)]
}

# 深拷贝

Object.assign() 不是深拷贝。

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。 如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性。

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source);

console.log(target); // { a: 1, b: 4, c: 5 }
console.log(returnedTarget); // { a: 1, b: 4, c: 5 }

# 介绍 RAF requestAnimationFrame

  • 要想动画流畅,更新频率要 60 帧/s,即 16.67ms 更新一次视图
  • setTimeout 要手动控制频率,而 RAF 浏览器会自动控制
  • 后台标签或隐藏 iframe 中,RAF 会暂停,而 setTimeout 依然执行
// 3s 把宽度从 100px 变为 640px。即增加 540px

const $div1 = document.getElementById('div1')
let curWidth = 100
const maxWidth = 640
// setTimeout
function animate() {
  curWidth = curWidth + 3
  $div1.style.width = curWidth + 'px'
  if (curWidth < maxWidth) {
    setTimeout(animate, 16.7)
  }
}

function animate() {
  curWidth = curWidth + 3
  $div1.style.width = curWidth + 'px'
  if (curWidth < maxWidth) {
    window.requestAnimationFrame(animate)
  }
}
animate()

评 论:

更新: 11/21/2020, 7:00:56 PM