0%

前端面试必知必会1——ES6基础

1. symbol

Symbol 的基本用法

Symbol 能够创建独一无二的类型

1
2
3
let s1 = Symbol('kin')
let s2 = Symbol('kin')
s1 === s2 // 结果是false

可以作为对象的 key 使用:

1
2
3
4
5
6
7
8
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

1
2
3
let s3 = Symbol.for('kin')
let s4 = Symbol.for('kin')
s3 === s4 // 复用

Symbol 可以做元编程

元编程即可以改写 js 语法本身

Symbol.toStringTag: 修改数据类型

1
2
3
4
let obj = {
[Symbol.toStringTag]: 'kin',
}
Object.prototype.toString.call(obj) // 结果 "[object kin]"

Symbol.toPrimitive: 隐式类型转化

1
2
3
4
5
6
7
8
let obj = {}
console.log(obj + 1) // [object Object]1
let obj = {
[Symbol.toPrimitive](type) {
return '123'
},
}
console.log(obj + 1) // 1231

Symbol.hasInstance: 改写 instanceof 的功能

1
2
3
4
5
6
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 中的元素是唯一的。

1
let s = new Set([1, 2, 3, 3])

常用方法

处理交集、并集、差集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值) 都可以作为一个键或一个值。

常用方法

WeakMap 与 Map

  • WeakMap 的 key 只能是对象,Map 的 key 可以是任意类型
  • key 被置为 null 时,WeakMap 会被垃圾回收,Map 则不会被垃圾回收。举例如下:
1
2
3
4
5
class Test {}
let my = new Test()
let newMap = new Map()
newMap.set(my, 1)
my = null

此处使用 Map,当 my 变成 null 时,newMap 中仍然保留 Test 实例,class Test 仍然在内存中,没被销毁。

1
2
3
4
5
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的写法,例如:

1
2
3
4
5
6
7
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 执行

1
2
3
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 方法,并且写法更加简单:

1
Reflect.apply(sum, null, [1, 2])

4. reduce

数组上提供的方法,能够收敛数组,把数组转化成其他类型数据

基本用法

数组不能为空,若为空则 reduce 第二个参数必须填,否则报错。

1
2
[1,2,3].reduce((prev, current, index, array) => {}, '')
[].reduce((prev, current, index, array) => {}, '') // 数组不能空,若为空则reduce第二个参数必须填,否则报错

reduce 实现

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
Array.prototype.map = function (cb) {
return this.reduce((prev, next, index) => {
prev.push(cb(next, index, this))
return prev
}, [])
}

用 reduce 实现拍平多维数组 flat

数组有一个自带的flat方法,能够拍平多维数组。比如:

1
;[1, 2, 3, [1, 2, [4]]].flat(2) // [1, 2, 3, 1, 2, 4]

用 reduce 也能够实现拍平多维数组的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 个函数,sumlenaddPrefix,需要一层层的去调用这三个函数addPrefix(len(sum('a', 'b')))。我们也可以通过compose(sum, len, addPrefix)来实现这个功能。

1
2
3
4
5
6
7
8
9
10
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 个函数的组合调用。

1
2
3
4
5
6
7
8
9
function compose(...fns) {
return fns.reduce((prev, next) => {
return (...argvs) => {
return prev(next(...argvs))
}
})
}
let fn = compose(addPrefix, len, sum)
fn('a', 'b')

为了更好的理解 compose 的实际执行结果,

1
2
3
4
5
6
let fn = compose(addPrefix, len, sum) //等价于
let fn = function (...argvs2) {
return (function (...argvs) {
return addPrefix(len(...argvs))
})(sum(...argvs2))
}

5. defineProperty

作用:对 object 的每个属性用此方法定义,可以使用 get 和 set 拦截对象的取值和设置值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)
  1. 可添加描述符

    enumerableconfigurable都是描述符

    • enumerable 表示 obj 是否可以被 for in 或 Object.keys 枚举
    • configurable表示 obj 是否可以被删除,例如delete obj.a
  2. 使用 set 和 get 时需要借助第三方变量实现对a属性的监控

  3. 缺点:如果要把对象的属性全部转化成 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 都是浅拷贝

  • 实现一个深拷贝

  1. JSON.string + JSON.parse

    1
    JSON.parse(JSON.stringify({ a: 2, b: undefined }))

    缺陷: 正则,函数,日期, undefined 这些类型不支持

  2. 递归对象,把对象属性都进行拷贝

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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 里,在下次拷贝属性前先看一下是否有,如果有就直接返回。循环引用的测试例子如下:

1
2
3
4
var obj = {}
obj.b = {}
obj.b.a = obj.b
console.log(deepClone(obj))