一、number的常用方法
1、其它类型转换为number类型
null ===> o
undefined ===> NaN
true ===> 1
false ===> 0
'123' ===> 123
'' ==> 0
' ' ===> 0
Symbol('test') ===> NaN
[1,2] ===> NaN
{test: "name"} ===> NaN
paeseInt('123px') ===> 123
paeseFloat('123.12px') ===> 123.123
// 上边两个参数,遇到第一个不是数字的字符串后就会停止转换,如果传入的参数不是字符串,则会先转换为字符串
paeseInt(true) ===> paeseInt('true') ===> NaN
- 使用Number()函数进行转换
- 使用+num 进行转换,效果类似于Number()
- paeseInt('123px') ==> 123 将字符串转换为数字,如果传入的参数不是字符串,则会转换为字符串后再运算
- paeseFloat('123.123px') ====> 123.123 将字符串转换为浮点数,如果传入的参数不是字符串,则会转换为字符串后再运算
2、浮点数进行运算时会存在精度问题
0.1 + 0.2 = 0.3000000000000001
解决方案,使用big.js进行精确运算
import Big from 'big.js'
const num = new Big(123)
big.plus(123) // 加法
big.minus(1) // 减法
big.div(10) // 除法
big.times(10) // 乘法
// 不建议使用tofixed方法,因为big的tofixed方法也不是四舍五入
3、toFixed()不是四舍五入而是银行家舍入法
解决方案,使用Math.round()进行二次封装,就是正常的四舍五入
function round(number, precision) {
return Math.round(+number + "e" + precision) / Math.pow(10, precision);
}
4、数字(金额)格式化工具库numeral
import numeral from 'numeral'
function formatMoney(num) {
return numeral(num).format('0,0.00')
}
二、字符串的常用方法
1、字符串的常用方法
const str = 'test str'
console.log(str.split(' ')) // 按传入的参数将字符串分割成数组 ['test', 'str']
console.log(str.trim()) // 去除首尾空格 test str
console.log(str.replace('s', '替换')) // 替换字符串中的指定字符,默认只替换第一个,可以传入正则表达式
console.log(str.replaceAll('s', '替换')) // 替换字符串中的所有指定字符
console.log(str.indexOf('s')) // 查找字符串中是否包含指定字符,返回索引,如果不包含指定字符,返回-1
console.log(str.includes('s')) // 判断字符串中是否包含指定字符,返回true/false,官方推荐用法
console.log(str.toLowerCase()) // 转换为小写
console.log(str.toUpperCase()) // 转换为大写
console.log(str.charAt(0)) // 获取指定索引的字符
console.log(str[0]) // 获取指定索引的字符,字符串内部保存使用的是数组格式,可以使用数组的方式访问
console.log(str.slice(0, 2)) // 获取指定索引范围的字符串
console.log(str.slice(-2)) // 截取字符串的最后两个字符,slice函数可以传入负数
2、其它类型转换为string类型推荐的方案
使用String() 或者是es6的模板字符串包裹需要转换的变量,这样可以实现null和undefined的转换,object类型的转换使用JSON.stringify()
3、将字符串转换成可执行对象
在实际工作中,会遇见各种各样的奇葩,有的神奇动态页面为了实现动态页面,会直接将一段表单的代码当成字符串发送给后端,后端又原样返回这个字符串,以实现表单动态管理,原来的代码实现的方式是用的eval()语句,eval()语句执行效率很慢,浏览器在运行的时候发现这个语句,它就不能动态的分析代码,对代码进行优化。而且,xss攻击的风险也急剧提升。一个正常的程序员此时此刻就该想一个其它的解决方案。但是就有那不正常的人。为了解决这种奇葩,以下是一个替代方案。
const str1 = `{name: 'zhangsan',age: 18,children: [{name: '我不是children'}]}`
let obj = (new Function("return " + str1))()
console.log(Object.prototype.toString.call(obj)) // [object Object]
这种情况也会有xss攻击的风险,下面是一些讨巧的方案。
// 直接从源头干掉这俩函数
window.console = null
window.alert = null
使用xss库对接口返回的数据进行过滤
import { filterXSS } from 'xss'
const html = filterXSS('<script>alert("xss");</scr' + 'ipt>', {})
// 输出之后的html字符串就是对潜在危险过滤了的
三、数组的常用方法
1、数组的常用方法
// 数组的遍历
const arr = [{ id: 1, name: 'test' }, { id: 2, name: 'test2' }]
arr.forEach(i => {
console.log(i) // 对数组的每一项执行对应的方法
})
const newArr = arr.map(i => {
return {
...i,
id: ++i.id,
} // 对数组的每一项执行对应的方法,将执行后的结果作为新的数组返回
})
const result = arr.filter(i => {
return i.id === 2 //筛选出数组中符合条件的那一项作为结果返回
})
const result1 = arr.find(i => i.id === 2) // 查找到数组中符合条件的那一项并返回
// 使用for循环,自己手写for循环会比调用forEach等方法快一些
for (let i = 0; i < arr.length; i++) {
console.log(arr[i])
}
// for...of
for (const i of arr) {
console.log(i)
}
const arr1 = [1, 2, 3]
console.log(arr1.includes(2)) // 判断数组中是否包含某一项元素,返回true或false
// 自己手动封装一个数组的循环函数
Array.prototype.selfForEach = function (fn) {
// 执行之前进行自定义操作
console.log('self forEach')
for (let i = 0; i < this.length; i++) {
// 自定义函数的第一项,数组的对应项,第二项,索引值,第三项,数组本身
fn(this[i], i, this)
}
// 执行完毕后添加自定义操作
}
arr.selfForEach((item, index, arr) => {
console.log(item, index, arr)
})
/**
* 控制台输出
* self forEach
* {id: 1, name: 'test1'} 0 [{…}, {…}]
* {id: 2, name: 'test2'} 1 [{…}, {…}]
*/
2、数组方法中改变原数组的方法
- push()方法,在数组的最后增加一项
- pop(),删除数组的最后一项,并返回该元素
- unshift(),向数组的第一项添加一个元素,返回新的数组的长度
- splice(),方法。这个方法的第一个参数是操作的开始位置,第二个参数是进行操作的数量(可以为0),第三个参数可选,需要替换的元素,这个函数就可以实现数组的增,删,改
const arr1 = [1, 2, 3]
arr1.splice(1,1)
console.log(arr1) // [1,3]
arr1.splice(1,0,4)
console.log(arr1) // [1,4,3]
arr1.splice(1,1,[4,5])
console.log(arr1) // [1,[4,5],3]
- shift(),删除数组的第一个元素,返回该值
- sort(),对数组进行排序,默认排序是将数组的每一项转换成字符串,然后比较他们的UTF-16的值,不是我们常规认为的大小。可以传入一个比较函数
- 如果 `compareFunction(a, b)` 小于 0 ,那么 a 会被排列到 b 之前。
- 如果 `compareFunction(a, b)` 等于 0 , a 和 b 的相对位置不变。
- 如果 `compareFunction(a, b)` 大于 0 , b 会被排列到 a 之前。
- `compareFunction(a, b)` 必须总是对相同的输入返回相同的比较结果,否则排序的结果将是不确定的。
- reverse()方法,数组反转,将原数组的前后顺序对调
- fill()方法,根据传入的参数,该百年原数组中指定位置的值,返回修改后的数组
const arr3 = [1, 2, 3]
// const result3 = arr3.every(item => item > 2)
// console.log(arr3, result3)
const target3 = arr3.fill(2)
console.log(target3, arr3) // [2,2,2] [2,2,2]
3、数组方法中不改变原数组的方法
1、concat(),可以用于连接一个或多个数组。不会改变原有数组,而仅仅是返回被连接数组的副本,属于浅拷贝; 2、join(),数组转换为字符串的方法,参数可以是分隔符号; 3、reduce(),数组进行累加的方法; 4、slice(),返回数组中被截取的部分; 5、map,forEach,filter,findIndex,这些都不会改变原数组; 6、every(), 检查数组的每一项是否符合传入的cb,符合返回true,否则false;
const arr = [1,2,3]
const result = arr.every(item=> item > 2)
console.log(result) // false
7、find()方法,返回查询的项的值,否则返回undefined
const arr = [{id: 1, test: 'name'}]
const result = arr.find(item=>item.id ===1)?.test // 'name'
8、toReversed(),不改变原数组的情况下对数组进行反转 9、some(),检查数组中是否至少有一项通过了cb(), 返回布尔值
const arr = [1,2,3]
console.log(arr.some(i=> i> 4)) // false
console.log(arr.some(i=>i > 2)) // true
4、数组中很有意思的方法
这部分介绍的方法可能和前边重复,单独列出来是因为它很神奇
A. flat()方法,可以将多维数组递归成一维数组,可以接收一个可选参数,默认是1,传入的参数表示需要展开的数组的层数,可以传入Infinity,表示无穷大,将整个数组展开成一维
const arr1 = [1, 2, [[[3]]]]
console.log(arr1.flat()) // [1,2,[[3]]]
console.log(arr1.flat(2)) // [1,2,[3]]
console.log(arr1.flat(Infinity)) // [1,2,3]
B. toString()方法
在我企图将flat方法转换后的数组转换成字符串输出的时候,无意间发现了这个方法的奇效。经过试验,这种方法甚至比flat方法运行速度还要快,但是flat方法不会丢失原来的数据类型,原来是number,转换后还是number,使用toString方法之后,会将数组中的数据全部转换成字符串。
const arr1 = [1, 2, [[[3]]]]
console.log(arr1.toString()) // 1,2,3
console.time('flat')
console.log(arr1.flat(Infinity))
console.timeEnd('flat') // flat: 0.06396484375 ms
console.time('toString')
console.log(arr1.toString().split(',')) // [1,2,3] 可以实现和flat方法类似的功能
console.timeEnd('toString') // toString: 0.053955078125 ms
C、Array.isArray()
判断一个变量的数据类型是不是数组,返回true/false
D、Array.from()
将其它数据类型的数据转换成数组,比如Set数据类型。利用Set数据类型不存储重复值得特性可以实现数组的去重
const setArr = [1, 1, 2, 2, 3, 3, true, true, false, false, null, null, undefined, undefined]
// const newResult1 = Array.from(new Set(setArr)) // 或者是下边的写法
const newResult = [...new Set(setArr)] // 这里还使用到可扩展运算符,扩展运算符进行的不是深拷贝,
// 它只能将数组的第一层展开,如果是引用数据类型,运算后得到的依然是地址值
console.log(newResult) // [1, 2, 3, true, false, null, undefined]
四、object的常用方法及排雷
1、遍历一个obj,推荐使用Object.keys()方法,Mdn官网明确写了,不推荐使用for...in循环
const obj = {
name: "test",
age: 18
}
// 推荐方法一:for...of
for(let i of Object.keys(obj)) {
console.log(obj[i])
}
// 推荐方法二:还是借助Objct.keys()方法
Object.keys(obj).forEach(item=>{
console.log(obj[item])
})
2、遍历的其它方法Object.values() Object.entries()
const obj = {
name: "test",
age: 18
}
Object.values(obj) // 获取到所有的value值
Object.entries(obj) // 以数组的形式输出obj的key value [['name', 'test'], ['age', 18]]
3、判断一个对象本身是否存在某个属性,推荐使用obj.hasOwnProperty([属性名])而不是in操作符
const obj = {
name: "test",
age: 18
}
console.time('in')
if ('age' in obj1) {
console.log(obj1.age)
}
console.timeEnd('in') // in: 0.033935546875 ms
console.time('hasOwnProperty')
if (obj1.hasOwnProperty('age')) {
console.log(obj1.age)
}
console.timeEnd('hasOwnProperty') // hasOwnProperty: 0.010986328125 ms
PS. console.time()和console.timeEnd()可以对js中一段代码的执行时间进行计时,优化代码的执行效率可以使用这个工具。 在上边的输出结果中,可以很明显的看到,hasOwnProperty()的执行效率每次都会比in操作符高,这是因为in操作符除了会对obj自身的属性进行查找之外,还会顺着原型链进行查找,在实际运用中这是不必要的。当前的obj属性不算很多,这种差异在大一点的obj身上会更明显。 for...in循环和Object.keys()方法值执行效率差异也是因为for...in循环会查找原型链,而Object.keys()只会返回obj自身的可枚举属性。
4、Vue 2.x响应式的实现原理Object.defineProperty(obj, '属性名', {'配置项'})
let genderValue = 'man'
Object.defineProperty(obj1, 'gender', {
enumerable: true, // 是否可枚举,默认false
configurable: true, //是否可删除,默认false
// value: 'man', // 值,设置了get,set这个值就不能被配置
// writable: true, // 能否被修改,默认false, 设置了get,set这个值就不能被配置
get() { // 每次访问obj.gender都会执行
console.log('get gender')
return genderValue
},
set(newValue) { // 想要对obj.gender赋值时会执行
genderValue = newValue
console.log(newValue, 'set gender')
}
})
五、实现深拷贝【推荐lodash】
深拷贝和浅拷贝: 由于引用数据类型在赋值运算符运算的时候传递的是object的地址值,在对赋值后的对象进行操作时会影响到原对象,这就是浅拷贝;而在实际项目中,往往需要实现的是创建一个新的对象,对新对象的操作不影响原对象,也就是我们说的深拷贝。
1、JSON.parse(JSON.stringify(obj))【数据不安全】
利用JSON方法实现深拷贝,使用最便捷,但是如果拷贝的原对象中有值为undefined,返回的新对象中就会丢失该属性,在大多数情况下这无可厚非,因为后台接口返回的值大多情况下没有值都会是null,不会是undefined,但是万一遇到不聪明的后端,这时候接口就会报错。
const obj2 = {
name: 'zhangsan',
age: undefined,
gender: 'man'
}
console.log(JSON.parse(JSON.stringify(obj2))) // {name: 'zhangsan', gender: 'man'}
2、自己写一个deepClone函数,递归实现深拷贝
这种情况需要特别处理引用数据类型,比如Date,RegExp,还有循环引用的Object,尤其是函数方法,需要进行特殊处理,比如直接复制函数引用/重写。
3、使用工具库lodash/lodash-es,_.cloneDeep
这个函数库支持按需引入,可以只引入自己需要的函数库,是比较推荐,比较稳定的一种实现方式
import { cloneDeep } from 'lodash'
const obj = {name: 'test', age: 18}
const newObj = cloneDeep(obj) // 会严格返回原obj
4、借助messageChannel实现深拷贝,大多数方案里面没有提到这种用法,messageChannel还可以实现跨标签页、iframe、组件通信
这个api浏览器很早就支持,我后续项目中应该会多使用这种方式
function deepClone(target) {
return new Promise((resolve) => {
const { port1, port2 } = new MessageChannel()
port1.postMessage(target)
port2.onmessage = (e) => {
resolve(e.data)
}
})
}
deepClone(obj2).then(res => {
console.log(res) // {name: 'zhangsan', age: undefined, gender: 'man'}
})
5、利用webWorker
// index.js
wondow.onload = function init() {
const obj = {}
const worker = new Worker('./worker.js')
// 向webworker发送消息
worker.postMessage(obj)
// 接受webworker返回的信息
worker.onmessage = (e) => {
console.log(e.data) // 返回的数据存储在e.data里面
console.log(obj === e.data) // false
}
}
// worker.js
// 在worker里面接受和发送信息也是同样的api
onmessage = (e) => {
postMessage(e.data)
}
六、闭包的实际运用
1、闭包是面试经常被问到的问题,在实际工作中合理的利用闭包,也有奇效。比如说,我利用闭包实现一个页面生命周期里面只会被触发一次的函数,这个函数用来懒加载依赖。
// 以vue应用举例
const app = createApp(App)
function installModule() {
// 标记当前函数是否被触发
let isInstalled = false
return async () => {
const module = await import('依赖名称')
app.install(module)
isInstalled = true
}
}
const installOnce = app()
然后在路由拦截器或者其它任何业务需要的地方,触发这个函数就可以了。当然,同样的需求也可以以其它的方式实现,但是这样实现可以体现闭包思想。
2、借助闭包,实现复杂计算的缓存