JS-每周3道面试题(第二周)

第二周,慢慢积累,做好准备,迎接蜕变

1. 介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

  • set

    • Set本身是一个构造函数,用来生成 Set 数据结构。
    • 成员唯一、无序且不重复。
    • [value, value],键值与键名是一致的(或者说只有键值,没有键名)。
    • 可以遍历,方法有:add、delete、has等等。
const set = new Set([1,2,2,3,3,3])
[...set]    // [1,2,3]
复制代码

    set实例的属性和方法

  • 属性

    • Set.prototype.constructor: 构造函数,默认就是Set函数。
    • Set.prototype.size: 返回Set实例的成员总数。
  • 操作方法

    • add(val): 添加某个值,返回Set结构本身。
    • delete(val): 删除某个值,返回一个布尔值,表示删除是否成功。
    • has(val): 返回一个布尔值,表示该值是否为Set的成员。
    • clear(): 清除所有成员,没有返回值。
  • 遍历方法

    • keys(): 返回键名的遍历器。
    • values(): 返回键值的遍历器。
    • entries(): 返回键值对的遍历器。
    • forEach(): 使用回调函数遍历每个成员。
  • WeakSet

    • WeakSet 结构与 Set 类似,也是不重复的值的集合。
    • 成员只能是对象,而不能是其他类型的值。
    • WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用
    • WeakSet 的成员是不适合引用的,因为它会随时消失,所以不可遍历
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a); // WeakSet {[1, 2], [3, 4]}
复制代码
  • 方法

    • WeakSet.prototype.add(value): 向 WeakSet 实例添加一个新成员。
    • WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
    • WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
  • Map

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。可以遍历,方法很多,可以和各种数据格式转换

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false

const map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);

map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
复制代码

任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数。这就是说,Set和Map都可以用来生成新的 Map。

const set = new Set([
  ['foo', 1],
  ['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1

const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3
复制代码
  • 实例的属性和方法
    • size属性:返回 Map 结构的成员总数。
    • set(key, value)方法:设置键名key对应的键值为value,然后返回整个 Map 结构。
    • get(key)方法:读取key对应的键值,如果找不到key,返回undefined。
    • has(key)方法:返回一个布尔值,表示某个键是否在当前 Map 对象之中。
    • delete(key)方法:删除某个键,返回true。如果删除失败,返回false。
    • clear()方法:清除所有成员,没有返回值。
  • 遍历方法
    • keys(): 返回键名的遍历器。
    • values():返回键值的遍历器。
    • entries():返回所有成员的遍历器。
    • forEach():遍历 Map 的所有成员。
const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"
复制代码

Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...)。例如

const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

[...map.keys()] // [1,2,3]
复制代码
  • WeakMap

    • WeakMap结构与Map结构类似,也是用于生成键值对的集合。
    • WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
    • WeakMap的键名所指向的对象,不计入垃圾回收机制。
    • 不能遍历,方法同get,set,has,delete

WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。

2. 介绍下深度优先遍历和广度优先遍历,如何实现?

这是一个需要遍历的DOM树

<div id="parent">

    <div id="child-1">
      <div id="child-1-1">
        <div id="child-1-1-1"></div>
        <div id="child-1-1-2"></div>
        <div id="child-1-1-3"></div>
      </div>

      <div id="child-1-2">
        <div id="child-1-2-1"></div>
        <div id="child-1-2-2"></div>
      </div>

      <div id="child-1-3"></div>
    </div>

    <div id="child-2"></div>

    <div id="child-3"></div>

  </div>
复制代码
  • 深度优先遍历DFS
    let deepTraversal = (node) => {
        let nodes = []
        if (node !== null) {
          nodes.push(node)
          let children = node.children
          for (let i = 0; i < children.length; i++) {
            nodes = nodes.concat(deepTraversal(children[i]))
          }
        }
        return nodes
      }
    
    let res = document.querySelector('#parent')
    
    console.log(deepTraversal(res))
复制代码

结果

假设初始状态是图中所有顶点均未被访问,则从某个顶点v出发,首先访问该顶点然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和v有路径相通的顶点都被访问到。若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。

  • 广度优先遍历BFS
    let widthTraversal = (node) => {
      let nodes = []
      let stack = []
      if (node) {
        stack.push(node)
        while (stack.length) {
          let item = stack.shift()
          let children = item.children
          nodes.push(item)
            // 队列,先进先出
            // nodes = [] stack = [parent]
            // nodes = [parent] stack = [child1,child2,child3]
            // nodes = [parent, child1] stack = [child2,child3,child1-1,child1-2]
            // nodes = [parent,child1,child2]
          for (let i = 0; i < children.length; i++) {
            stack.push(children[i])
          }
        }
      }
      return nodes
    }
复制代码

结果

从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直至图中所有已被访问的顶点的邻接点都被访问到。 如果此时图中尚有顶点未被访问,则需要另选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止。

3. 浅拷贝和深拷贝

引用网上一张图片说明一下数据类型和浅拷贝和深拷贝的关系

JS中的值有两种类型:

  1. 基本类型:string、number、boolean、undefined、null
  2. 复杂基本类型:object

存放位置:

  1. 基本类型的值存放在栈(stock)中,大小确定,并且是不可以改变的。栈(stack)为自动分配的内存空间,它由系统自动释放
  2. 复杂基本类型的值存放在堆中,大小不确定,可以改变,在栈中保存的是指向堆中真正值的地址指针。堆(heap)则是动态分配的内存,大小不定也不会自动释放。

赋值操作、浅拷贝与深拷贝的关系

  • 基本数据类型的赋值(=)是在内存中新开辟一段栈内存,是两个完全无关的对象。
var a = 1
var b = a
a++
console.log(a)  // 2
console.log(b)  // 1
复制代码
  • 引用类型的赋值是传址,也就是说引用类型的赋值是对象保存在栈中的地址的赋值,这样的话两个变量就指向同一个对象,因此两者之间操作互相有影响。
var a = {num:1}
var b = a

a.num = 2
console.log(a.num)  // 2
console.log(b.num)  // 2
复制代码
  • 将源对象拷贝到目标对象中,但不包括源对象的子对象

  • 将源对象拷贝到目标对象中,包括源对象的子对象

对于复杂基本类型的数据,三种操作的对比

操作 和原数据是否指向同一对象 第一层数据为基本数据类型 原数据中包含子对象
赋值 改变会使原数据改变 改变会使原数据改变
浅拷贝 改变不会使原数据改变 改变会使原数据改变
深拷贝 改变不会使原数据改变 改变不会使原数据改变

实现浅拷贝

  1. Object.assign(target, source, source...)
    function shallowCopy(src) {
        var dst = {};
        for (var prop in src) {
            if (src.hasOwnProperty(prop)) {
                dst[prop] = src[prop];
            }
        }
        return dst;
    }
复制代码

实现深拷贝

JSON.parse(JSON.stringify(obj))实现深拷贝是有局限性的,当数据中有function或undefined等数据时,是不能实现的。

    function deepClone(obj) {
        // 因为typeof null == "object" 但 null并不是对象,是基本类型
        if (obj == null) return
        var res = Array.isArray(obj) ? [] : {}
        
        for (key in obj) {
            if (obj.hasOwnProperty(key)) {
                if ( typeof obj[key] == "object" ) {
                    res[key] = deepClone(obj[key])
                } else {
                    res[key] = obj[key]
                }
            }
        }
        return res
    }
复制代码

验证一下

    var source = {
        name: 'source',
        do: function (n) {
          console.log(n)
        },
        obj: {
          name: '子对象obj'
        }
      }
    
      var target = deepClone(source)
    
      target.name = 'target'
      target.do = function (n) {
        console.log(n * 2)
      }
      target.obj.name = '拷贝子对象成功'
    
      console.log(source)
      console.log(target)
复制代码

可以看到修改了拷贝对象的子对象并没有对源对象产生影响,拷贝成功。

欢迎指正和交流

转载于:https://juejin.im/post/5cf478715188251a9c4cf961

猜你喜欢

转载自blog.csdn.net/weixin_34221276/article/details/91480863