1. symbol
Symbol的基本用法
Symbol能够创建独一无二的类型
let s1 = Symbol('kin')
let s2 = Symbol('kin')
s1 === s2 // 结果是false
可以作为对象的key使用:
let obj = {
name: 'Lucy',
say: 'hello world',
[s1]: 'ok'
}
for (let key in obj) {
console.log(key)
}
Symbol作为key时,Symbol属性不能被for in 枚举,能够通过Reflect.ownKeys(obj)获取
Symbol.for
let s3 = Symbol.for('kin')
let s4 = Symbol.for('kin')
s3 === s4 // 复用
Symbol可以做元编程
元编程即可以改写js语法本身
Symbol.toStringTag: 修改数据类型
let obj = {
[Symbol.toStringTag]: 'kin'
}
Object.prototype.toString.call(obj) // 结果 "[object kin]"
Symbol.toPrimitive: 隐式类型转化
let obj = {}
console.log(obj + 1) // [object Object]1
let obj = {
[Symbol.toPrimitive](type) {
return '123'
}
}
console.log(obj + 1) // 1231
Symbol.hasInstance: 改写instanceof的功能
let instance = {
[Symbol.hasInstance](value) {
return 'name' in value
}}
console.log({name: 'kin'} instanceof instance) // true
2. set、map 与 weakSet、weakMap
Set
Set对象是值的集合,Set中的元素只会出现一次,即 Set 中的元素是唯一的。
let s = new Set([1,2,3,3])
常用方法
-
在
Set对象尾部添加一个元素。返回该Set对象。 -
返回一个新的迭代器对象,该对象包含
Set对象中的按插入顺序排列的所有元素的值的[value, value]数组。为了使这个方法和Map对象保持相似, 每个值的键和值相等。 -
返回一个布尔值,表示该值在
Set中存在与否。 -
Set.prototype.forEach(*callbackFn*[, *thisArg*\])按照插入顺序,为Set对象中的每一个值调用一次callBackFn。如果提供了
thisArg参数,回调中的this会是这个参数。
处理交集、并集、差集
Object.prototype.toString.call(new Map()) // "[object Map]"
Object.prototype.toString.call(new Set()) // "[object Set]"
let arr1=[1,2,3,4]
let arr2=[3,4,5,6]
// 求并集
function union(arr1, arr2) {
let s = new Set([...arr1, ...arr2])
return [...s]
}
// 求交集
function intersection(arr1, arr2) {
let s2 = new Set(arr2)
return arr1.filter(one => s2.has(one))
}
Map
Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值) 都可以作为一个键或一个值。
常用方法
-
返回键对应的值,如果不存在,则返回undefined。
-
设置Map对象中键的值。返回该Map对象。
WeakMap 与 Map
- WeakMap的key只能是对象,Map的key可以是任意类型
- key被置为null时,WeakMap会被垃圾回收,Map则不会被垃圾回收。举例如下:
class Test { }
let my = new Test()
let newMap = new Map()
newMap.set(my, 1)
my = null
此处使用Map,当my变成null时,newMap中仍然保留Test实例,class Test仍然在内存中,没被销毁。
class Test { }
let my = new Test()
let newMap = new WeakMap()
newMap.set(my, 1)
my = null
而使用WeakMap时,当my被置为null后,newMap中Test实例也不存在,class Test被垃圾回收机制销毁了。
3. Reflect
Es6 后续新增的方法放Reflect 上
Reflect.ownKeys(obj)
获取对象的属性列表
Reflect.apply
我们在源码中经常看到Function.prototype.apply.call的写法,例如:
function sum(...argvs) {
console.log('sum')
}
sum.apply = function () {
console.log('apply')
}
Function.prototype.apply.call(sum, null, [1, 2])
因为直接调用函数的apply方法没法确保调用的是原生的apply,可能已经被改写了。所以通过Function.prototype.apply.call来确保调用到原生的apply。
可以这样理解call的功能:1. 改变apply中的this为sum,2. 让apply执行
Function.prototype.apply.call(sum, null, [1, 2])
Function.prototype.apply(null, [1,2]) // 等价于apply执行,并且apply内的this是sum
sum([1,2]) // 等价于sum执行,并且this是null
Reflect上也有apply方法,并且写法更加简单:
Reflect.apply(sum, null, [1,2])
4. reduce
数组上提供的方法,能够收敛数组,把数组转化成其他类型数据
基本用法
数组不能为空,若为空则reduce第二个参数必须填,否则报错。
[1,2,3].reduce((prev, current, index, array) => {}, '')
[].reduce((prev, current, index, array) => {}, '') // 数组不能空,若为空则reduce第二个参数必须填,否则报错
reduce实现
Array.prototype.reduce = function (callback, prev) {
for (let i = 0; i < this.length; i++) {
if (!prev) {
prev = callback(this[i], this[i + 1], i + 1, this)
i++
} else {
prev = callback(prev, this[i], i, this)
}
}
return prev
}
用reduce实现map
Array.prototype.map = function (cb) {
return this.reduce((prev, next, index) => {
prev.push(cb(next, index, this))
return prev
}, [])
}
用reduce实现拍平多维数组 flat
数组有一个自带的flat方法,能够拍平多维数组。比如:
[1,2,3,[1,2,[4]]].flat(2) // [1, 2, 3, 1, 2, 4]
用reduce也能够实现拍平多维数组的功能:
Array.prototype.flat = function (level) {
return this.reduce((prev, next, i) => {
if (Array.isArray(next)) {
if (level > 1) {
next = next.flat(--level)
}
prev = [...prev, ...next]
} else {
prev.push(next)
}
return prev
}, [])
}
用reduce实现compose
compose即组合函数,用来把多个函数组合起来,常用在中间件中。比如有3个函数,sum,len,addPrefix,需要一层层的去调用这三个函数addPrefix(len(sum('a', 'b')))。我们也可以通过compose(sum, len, addPrefix)来实现这个功能。
function sum(a, b) {
return a + b
}
function len(str) {
return str.length
}
function addPrefix(str) {
return '¥' + str
}
addPrefix(len(sum('a', 'b')))
通过执行compose(sum, len, addPrefix)产生一个新函数fn,再调用fn('a', 'b')实现3个函数的组合调用。
function compose(...fns) {
return fns.reduce((prev, next) => {
return (...argvs) => {
return prev(next(...argvs))
}
})
}
let fn = compose(addPrefix, len, sum)
fn('a', 'b')
为了更好的理解compose的实际执行结果,
let fn = compose(addPrefix, len, sum) //等价于
let fn = function (...argvs2) {
return function (...argvs) {
return addPrefix(len(...argvs))
}(sum(...argvs2))
}
5. defineProperty
作用:对object的每个属性用此方法定义,可以使用get和set拦截对象的取值和设置值。
let obj = {}, _a = 'xxx'
Object.defineProperty(obj, 'a',
{
enumerable: true,
configurable: true,
get() {
return _a
},
set(value) {
_a = value
}
})
obj.a = 'aaa'
console.log(obj)
-
可添加描述符
enumerable和configurable都是描述符enumerable表示obj是否可以被for in或Object.keys枚举configurable表示obj是否可以被删除,例如delete obj.a
-
使用set和get时需要借助第三方变量实现对
a属性的监控 -
缺点:如果要把对象的属性全部转化成getter + setter,需要遍历所有对象,用defineProperty重新定义属性,性能不高
-
数组采用这种方式,索引很多,性能很差
-
对象中嵌套对象,需要递归处理
-
6. proxy
作用:使用get和set拦截对象属性的取值和设置值。
优点:proxy 是不用改写原对象,不用对object的属性进行重新定义,性能高。如果访问到的属性是对象时,再代理即可,不用递归。
缺点:proxy 是es6的api, 但是兼容性差
vue3 中对数据的拦截就改用了proxy。
常用的api:
- get:获取属性时触发,例如 proxy.xxx
- set:设置proxy属性时触发,proxy.a = 1
- ownKeys:获取proxy的key触发,Object.getOwnPropertyNames(proxy)或Reflect.ownKeys 或 Object.keys都会触发
- deleteProperty:删除proxy上的属性时触发,eg. delete proxy.xxx 触发
- has:使用in语法时触发,eg. 'a' in proxy
let obj = {xxx:1}
let proxy = new Proxy(obj, {
get() {
console.log('Proxy get', arguments, Reflect.get(...arguments))
return Reflect.get(...arguments);
}, // proxy.xxx
set() {
console.log('Proxy set')
return Reflect.set(...arguments);
},
ownKeys(target) { // Object.getOwnPropertyNames(proxy) 或Reflect.ownKeys 或 Object.keys
console.log('Proxy ownKeys')
return Reflect.ownKeys(target)
},
deleteProperty(target, key) { // delete proxy.xxx 触发
console.log('Proxy deleteProperty')
if (key in target) {
delete target[key]
}
},
has(target, key) {
console.log('Proxy has')
return key in target
} // 'a' in proxy
})
7. 深拷贝和浅拷贝
-
... 展开运算符
... 等价于Object.assign 都是浅拷贝
-
实现一个深拷贝
-
JSON.string + JSON.parse
JSON.parse(JSON.stringify({ a: 2, b: undefined }))缺陷: 正则,函数,日期, undefined 这些类型不支持
-
递归对象,把对象属性都进行拷贝
function deepClone(obj, hash = new WeakMap()) { // 记录拷贝后的结果 if (obj == null) return obj // 正则 日期 函数 set map if (obj instanceof RegExp) return new RegExp(obj) if (obj instanceof Date) return new Date(obj) if (typeof obj !== 'object') return obj if (hash.has(obj)) return hash.get(obj) // 返回上次拷贝的结果 // 数组 || 对象 let newObj = new obj.constructor hash.set(obj, newObj) for (let key in obj) { if (obj.hasOwnProperty(key)) { // 不拷贝原型上的方法 newObj[key] = deepClone(obj[key], hash) } } return newObj }
其中增加if (hash.has(obj)) return hash.get(obj) 是为了避免循环引用。每次拷贝完对象的属性后都存储在hash里,在下次拷贝属性前先看一下是否有,如果有就直接返回。循环引用的测试例子如下:
var obj = {}
obj.b = {}
obj.b.a = obj.b
console.log(deepClone(obj))