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

331 阅读6分钟

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])

常用方法

处理交集、并集、差集

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则不会被垃圾回收。举例如下:
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个函数,sumlenaddPrefix,需要一层层的去调用这三个函数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)
  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
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

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

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

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

    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))