乐趣区

关于javascript:高频js笔试题看这一篇就够了

二叉树档次遍历

// 二叉树档次遍历

class Node {constructor(element, parent) {
    this.parent = parent // 父节点 
    this.element = element // 以后存储内容
    this.left = null // 左子树
    this.right = null // 右子树
  }
}

class BST {constructor(compare) {
    this.root = null // 树根
    this.size = 0 // 树中的节点个数

    this.compare = compare || this.compare
  }
  compare(a,b) {return a - b}
  add(element) {if(this.root === null) {this.root = new Node(element, null)
      this.size++
      return
    }
    // 获取根节点 用以后增加的进行判断 放右边还是放左边
    let currentNode = this.root 
    let compare
    let parent = null 
    while (currentNode) {compare = this.compare(element, currentNode.element)
      parent = currentNode // 先将父亲保存起来
      // currentNode 要不停的变动
      if(compare > 0) {currentNode = currentNode.right} else if(compare < 0) {currentNode = currentNode.left} else {currentNode.element = element // 相等时 先笼罩后续解决}
    }

    let newNode = new Node(element, parent)
    if(compare > 0) {parent.right = newNode} else if(compare < 0) {parent.left = newNode}

    this.size++
  }
  // 档次遍历 队列
  levelOrderTraversal(visitor) {if(this.root == null) {return}
    let stack = [this.root]
    let index = 0 // 指针 指向 0
    let currentNode 
    while (currentNode = stack[index++]) {
      // 反转二叉树
      let tmp = currentNode.left
      currentNode.left = currentNode.right
      currentNode.right = tmp
      visitor.visit(currentNode.element)
      if(currentNode.left) {stack.push(currentNode.left)
      }
      if(currentNode.right) {stack.push(currentNode.right)
      }
    }
  }
}
// 测试
var bst = new BST((a,b)=>a.age-b.age) // 模仿 sort 办法

// ![](http://img-repo.poetries.top/images/20210522203619.png)
// ![](http://img-repo.poetries.top/images/20210522211809.png)
bst.add({age: 10})
bst.add({age: 8})
bst.add({age:19})
bst.add({age:6})
bst.add({age: 15})
bst.add({age: 22})
bst.add({age: 20})

// 应用访问者模式
class Visitor {constructor() {this.visit = function (elem) {elem.age = elem.age*2}
  }
}

// ![](http://img-repo.poetries.top/images/20210523095515.png)
console.log(bst.levelOrderTraversal(new Visitor()))

实现类的继承

类的继承在几年前是重点内容,有 n 种继承形式各有优劣,es6 遍及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深刻了解的去看红宝书即可,咱们目前只实现一种最现实的继承形式。

function Parent(name) {this.parent = name}
Parent.prototype.say = function() {console.log(`${this.parent}: 你打篮球的样子像 kunkun`)
}
function Child(name, parent) {
    // 将父类的构造函数绑定在子类上
    Parent.call(this, parent)
    this.child = name
}

/**  1. 这一步不必 Child.prototype =Parent.prototype 的起因是怕共享内存,批改父类原型对象就会影响子类 2. 不必 Child.prototype = new Parent()的起因是会调用 2 次父类的构造方法(另一次是 call),会存在一份多余的父类实例属性 3. Object.create 是创立了父类原型的正本,与父类原型齐全隔离 */
Child.prototype = Object.create(Parent.prototype);
Child.prototype.say = function() {console.log(`${this.parent}好,我是练习时长两年半的 ${this.child}`);
}

// 留神记得把子类的结构指向子类自身
Child.prototype.constructor = Child;

var parent = new Parent('father');
parent.say() // father: 你打篮球的样子像 kunkun

var child = new Child('cxk', 'father');
child.say() // father 好,我是练习时长两年半的 cxk

实现 apply 办法

apply 原理与 call 很类似,不多赘述

// 模仿 apply
Function.prototype.myapply = function(context, arr) {var context = Object(context) || window;
  context.fn = this;

  var result;
  if (!arr) {result = context.fn();
  } else {var args = [];
    for (var i = 0, len = arr.length; i < len; i++) {args.push("arr[" + i + "]");
    }
    result = eval("context.fn(" + args + ")");
  }

  delete context.fn;
  return result;
};

实现双向数据绑定

let obj = {}
let input = document.getElementById('input')
let span = document.getElementById('span')
// 数据劫持
Object.defineProperty(obj, 'text', {
  configurable: true,
  enumerable: true,
  get() {console.log('获取数据了')
  },
  set(newVal) {console.log('数据更新了')
    input.value = newVal
    span.innerHTML = newVal
  }
})
// 输出监听
input.addEventListener('keyup', function(e) {obj.text = e.target.value})

参考:前端手写面试题具体解答

实现非负大整数相加

JavaScript 对数值有范畴的限度,限度如下:

Number.MAX_VALUE // 1.7976931348623157e+308
Number.MAX_SAFE_INTEGER // 9007199254740991
Number.MIN_VALUE // 5e-324
Number.MIN_SAFE_INTEGER // -9007199254740991

如果想要对一个超大的整数 (> Number.MAX_SAFE_INTEGER) 进行加法运算,然而又想输入个别模式,那么应用 + 是无奈达到的,一旦数字超过 Number.MAX_SAFE_INTEGER 数字会被立刻转换为迷信计数法,并且数字精度相比以前将会有误差。

实现一个算法进行大数的相加:

function sumBigNumber(a, b) {
  let res = '';
  let temp = 0;

  a = a.split('');
  b = b.split('');

  while (a.length || b.length || temp) {temp += ~~a.pop() + ~~b.pop();
    res = (temp % 10) + res;
    temp  = temp > 9
  }
  return res.replace(/^0+/, '');
}

其次要的思路如下:

  • 首先用字符串的形式来保留大数,这样数字在数学示意上就不会发生变化
  • 初始化 res,temp 来保留两头的计算结果,并将两个字符串转化为数组,以便进行每一位的加法运算
  • 将两个数组的对应的位进行相加,两个数相加的后果可能大于 10,所以可能要仅为,对 10 进行取余操作,将后果保留在以后位
  • 判断以后位是否大于 9,也就是是否会进位,若是则将 temp 赋值为 true,因为在加法运算中,true 会主动隐式转化为 1,以便于下一次相加
  • 反复上述操作,直至计算完结

查找字符串中呈现最多的字符和个数

例: abbcccddddd -> 字符最多的是 d,呈现了 5 次

let str = "abcabcabcbbccccc";
let num = 0;
let char = '';

 // 使其依照肯定的秩序排列
str = str.split('').sort().join('');
// "aaabbbbbcccccccc"

// 定义正则表达式
let re = /(\w)\1+/g;
str.replace(re,($0,$1) => {if(num < $0.length){
        num = $0.length;
        char = $1;        
    }
});
console.log(` 字符最多的是 ${char},呈现了 ${num}次 `);

小孩报数问题

有 30 个小孩儿,编号从 1 -30,围成一圈依此报数,1、2、3 数到 3 的小孩儿退出这个圈,而后下一个小孩 从新报数 1、2、3,问最初剩下的那个小孩儿的编号是多少?

function childNum(num, count){let allplayer = [];    
    for(let i = 0; i < num; i++){allplayer[i] = i + 1;
    }

    let exitCount = 0;    // 来到人数
    let counter = 0;      // 记录报数
    let curIndex = 0;     // 以后下标

    while(exitCount < num - 1){if(allplayer[curIndex] !== 0) counter++;    

        if(counter == count){allplayer[curIndex] = 0;                 
            counter = 0;
            exitCount++;  
        }
        curIndex++;
        if(curIndex == num){curIndex = 0};           
    }    
    for(i = 0; i < num; i++){if(allplayer[i] !== 0){return allplayer[i]
        }      
    }
}
childNum(30, 3)

实现数组的 flat 办法

function _flat(arr, depth) {if(!Array.isArray(arr) || depth <= 0) {return arr;}
  return arr.reduce((prev, cur) => {if (Array.isArray(cur)) {return prev.concat(_flat(cur, depth - 1))
    } else {return prev.concat(cur);
    }
  }, []);
}

实现公布 - 订阅模式

class EventCenter{
  // 1. 定义事件容器,用来装事件数组
    let handlers = {}

  // 2. 增加事件办法,参数:事件名 事件办法
  addEventListener(type, handler) {
    // 创立新数组容器
    if (!this.handlers[type]) {this.handlers[type] = []}
    // 存入事件
    this.handlers[type].push(handler)
  }

  // 3. 触发事件,参数:事件名 事件参数
  dispatchEvent(type, params) {
    // 若没有注册该事件则抛出谬误
    if (!this.handlers[type]) {return new Error('该事件未注册')
    }
    // 触发事件
    this.handlers[type].forEach(handler => {handler(...params)
    })
  }

  // 4. 事件移除,参数:事件名 要删除事件,若无第二个参数则删除该事件的订阅和公布
  removeEventListener(type, handler) {if (!this.handlers[type]) {return new Error('事件有效')
    }
    if (!handler) {
      // 移除事件
      delete this.handlers[type]
    } else {const index = this.handlers[type].findIndex(el => el === handler)
      if (index === -1) {return new Error('无该绑定事件')
      }
      // 移除事件
      this.handlers[type].splice(index, 1)
      if (this.handlers[type].length === 0) {delete this.handlers[type]
      }
    }
  }
}

Object.is

Object.is解决的次要是这两个问题:

+0 === -0  // true
NaN === NaN // false
const is= (x, y) => {if (x === y) {
    // + 0 和 - 0 应该不相等
    return x !== 0 || y !== 0 || 1/x === 1/y;
  } else {return x !== x && y !== y;}
}

实现类的继承

实现类的继承 - 简版

类的继承在几年前是重点内容,有 n 种继承形式各有优劣,es6 遍及后越来越不重要,那么多种写法有点『回字有四样写法』的意思,如果还想深刻了解的去看红宝书即可,咱们目前只实现一种最现实的继承形式。

// 寄生组合继承
function Parent(name) {this.name = name}
Parent.prototype.say = function() {console.log(this.name + ` say`);
}
Parent.prototype.play = function() {console.log(this.name + ` play`);
}

function Child(name, parent) {
  // 将父类的构造函数绑定在子类上
  Parent.call(this, parent)
  this.name = name
}

/** 
 1. 这一步不必 Child.prototype = Parent.prototype 的起因是怕共享内存,批改父类原型对象就会影响子类
 2. 不必 Child.prototype = new Parent()的起因是会调用 2 次父类的构造方法(另一次是 call),会存在一份多余的父类实例属性
3. Object.create 是创立了父类原型的正本,与父类原型齐全隔离
*/
Child.prototype = Object.create(Parent.prototype);
Child.prototype.say = function() {console.log(this.name + ` say`);
}

// 留神记得把子类的结构指向子类自身
Child.prototype.constructor = Child;
// 测试
var parent = new Parent('parent');
parent.say() 

var child = new Child('child');
child.say() 
child.play(); // 继承父类的办法

ES5 实现继承 - 具体

第一种形式是借助 call 实现继承

function Parent1(){this.name = 'parent1';}
function Child1(){Parent1.call(this);
    this.type = 'child1'    
}
console.log(new Child1);

这样写的时候子类尽管可能拿到父类的属性值,然而问题是父类中一旦存在办法那么子类无奈继承。那么引出上面的办法

第二种形式借助原型链实现继承:

function Parent2() {
    this.name = 'parent2';
    this.play = [1, 2, 3]
  }
  function Child2() {this.type = 'child2';}
  Child2.prototype = new Parent2();

  console.log(new Child2());

看似没有问题,父类的办法和属性都可能拜访,但实际上有一个潜在的有余。举个例子:

var s1 = new Child2();
  var s2 = new Child2();
  s1.play.push(4);
  console.log(s1.play, s2.play); // [1,2,3,4] [1,2,3,4]

明明我只扭转了 s1 的 play 属性,为什么 s2 也跟着变了呢?很简略,因为两个实例应用的是同一个原型对象

第三种形式:将前两种组合:

function Parent3 () {
    this.name = 'parent3';
    this.play = [1, 2, 3];
  }
  function Child3() {Parent3.call(this);
    this.type = 'child3';
  }
  Child3.prototype = new Parent3();
  var s3 = new Child3();
  var s4 = new Child3();
  s3.play.push(4);
  console.log(s3.play, s4.play); // [1,2,3,4] [1,2,3]

之前的问题都得以解决。然而这里又徒增了一个新问题,那就是 Parent3 的构造函数会多执行了一次(Child3.prototype = new Parent3();)。这是咱们不愿看到的。那么如何解决这个问题?

第四种形式: 组合继承的优化 1

function Parent4 () {
    this.name = 'parent4';
    this.play = [1, 2, 3];
  }
  function Child4() {Parent4.call(this);
    this.type = 'child4';
  }
  Child4.prototype = Parent4.prototype;

这里让将父类原型对象间接给到子类,父类构造函数只执行一次,而且父类属性和办法均能拜访,然而咱们来测试一下

var s3 = new Child4();
  var s4 = new Child4();
  console.log(s3)

子类实例的构造函数是 Parent4,显然这是不对的,应该是 Child4。

第五种形式(最举荐应用):优化 2

function Parent5 () {
    this.name = 'parent5';
    this.play = [1, 2, 3];
  }
  function Child5() {Parent5.call(this);
    this.type = 'child5';
  }
  Child5.prototype = Object.create(Parent5.prototype);
  Child5.prototype.constructor = Child5;

这是最举荐的一种形式,靠近完满的继承。

实现 redux-thunk

redux-thunk 能够利用 redux 中间件让 redux 反对异步的 action

// 如果 action 是个函数,就调用这个函数
// 如果 action 不是函数,就传给下一个中间件
// 发现 action 是函数就调用
const thunk = ({dispatch, getState}) => (next) => (action) => {if (typeof action === 'function') {return action(dispatch, getState);
  }

  return next(action);
};
export default thunk

循环打印红黄绿

上面来看一道比拟典型的问题,通过这个问题来比照几种异步编程办法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯一直交替反复亮灯?

三个亮灯函数:

function red() {console.log('red');
}
function green() {console.log('green');
}
function yellow() {console.log('yellow');
}

这道题简单的中央在于 须要“交替反复”亮灯,而不是“亮完一次”就完结了。

(1)用 callback 实现

const task = (timer, light, callback) => {setTimeout(() => {if (light === 'red') {red()
        }
        else if (light === 'green') {green()
        }
        else if (light === 'yellow') {yellow()
        }
        callback()}, timer)
}
task(3000, 'red', () => {task(2000, 'green', () => {task(1000, 'yellow', Function.prototype)
    })
})

这里存在一个 bug:代码只是实现了一次流程,执行后红黄绿灯别离只亮一次。该如何让它交替反复进行呢?

下面提到过递归,能够递归亮灯的一个周期:

const step = () => {task(3000, 'red', () => {task(2000, 'green', () => {task(1000, 'yellow', step)
        })
    })
}
step()

留神看黄灯亮的回调里又再次调用了 step 办法 以实现循环亮灯。

(2)用 promise 实现

const task = (timer, light) => 
    new Promise((resolve, reject) => {setTimeout(() => {if (light === 'red') {red()
            }
            else if (light === 'green') {green()
            }
            else if (light === 'yellow') {yellow()
            }
            resolve()}, timer)
    })
const step = () => {task(3000, 'red')
        .then(() => task(2000, 'green'))
        .then(() => task(2100, 'yellow'))
        .then(step)
}
step()

这里将回调移除,在一次亮灯完结后,resolve 以后 promise,并仍然应用递归进行。

(3)用 async/await 实现

const taskRunner =  async () => {await task(3000, 'red')
    await task(2000, 'green')
    await task(2100, 'yellow')
    taskRunner()}
taskRunner()

Function.prototype.apply()

第一个参数是绑定的 this,默认为window,第二个参数是数组或类数组

Function.prototype.apply = function(context = window, args) {if (typeof this !== 'function') {throw new TypeError('Type Error');
  }
  const fn = Symbol('fn');
  context[fn] = this;

  const res = context[fn](...args);
  delete context[fn];
  return res;
}

实现数组的 filter 办法

Array.prototype._filter = function(fn) {if (typeof fn !== "function") {throw Error('参数必须是一个函数');
    }
    const res = [];
    for (let i = 0, len = this.length; i < len; i++) {fn(this[i]) && res.push(this[i]);
    }
    return res;
}

实现观察者模式

观察者模式(基于公布订阅模式)有观察者,也有被观察者

观察者须要放到被观察者中,被观察者的状态变动须要告诉观察者 我变动了 外部也是基于公布订阅模式,收集观察者,状态变动后要被动告诉观察者

class Subject { // 被观察者 学生
  constructor(name) {
    this.state = 'happy'
    this.observers = []; // 存储所有的观察者}
  // 收集所有的观察者
  attach(o){ // Subject. prototype. attch
    this.observers.push(o)
  }
  // 更新被观察者 状态的办法
  setState(newState) {
    this.state = newState; // 更新状态
    // this 指被观察者 学生
    this.observers.forEach(o => o.update(this)) // 告诉观察者 更新它们的状态
  }
}

class Observer{ // 观察者 父母和老师
  constructor(name) {this.name = name}
  update(student) {console.log('以后' + this.name + '被告诉了', '以后学生的状态是' + student.state)
  }
}

let student = new Subject('学生'); 

let parent = new Observer('父母'); 
let teacher = new Observer('老师'); 

// 被观察者存储观察者的前提,须要先接收观察者
student. attach(parent); 
student. attach(teacher); 
student. setState('被欺侮了');

实现一个迭代器生成函数

ES6 对迭代器的实现

JS 原生的汇合类型数据结构,只有 Array(数组)和Object(对象);而ES6 中,又新增了 MapSet。四种数据结构各自有着本人特地的外部实现,但咱们仍期待以同样的一套规定去遍历它们,所以 ES6 在推出新数据结构的同时也推出了一套 对立的接口机制 ——迭代器(Iterator)。

ES6约定,任何数据结构只有具备 Symbol.iterator 属性(这个属性就是 Iterator 的具体实现,它实质上是以后数据结构默认的迭代器生成函数),就能够被遍历——精确地说,是被 for...of... 循环和迭代器的 next 办法遍历。事实上,for...of...的背地正是对 next 办法的重复调用。

在 ES6 中,针对 ArrayMapSetStringTypedArray、函数的 arguments 对象、NodeList 对象这些原生的数据结构都能够通过for...of... 进行遍历。原理都是一样的,此处咱们拿最简略的数组进行举例,当咱们用 for...of... 遍历数组时:

const arr = [1, 2, 3]
const len = arr.length
for(item of arr) {console.log(` 以后元素是 ${item}`)
}

之所以可能按程序一次一次地拿到数组里的每一个成员,是因为咱们借助数组的 Symbol.iterator 生成了它对应的迭代器对象,通过重复调用迭代器对象的 next 办法拜访了数组成员,像这样:

const arr = [1, 2, 3]
// 通过调用 iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()

// 对迭代器对象执行 next,就能一一拜访汇合的成员
iterator.next()
iterator.next()
iterator.next()

丢进控制台,咱们能够看到 next 每次会按程序帮咱们拜访一个汇合成员:

for...of... 做的事件,根本等价于上面这通操作:

// 通过调用 iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()

// 初始化一个迭代后果
let now = {done: false}

// 循环往外迭代成员
while(!now.done) {now = iterator.next()
    if(!now.done) {console.log(` 当初遍历到了 ${now.value}`)
    }
}

能够看出,for...of...其实就是 iterator 循环调用换了种写法。在 ES6 中咱们之所以可能开心地用 for...of... 遍历各种各种的汇合,全靠迭代器模式在背地给力。

ps:此处举荐浏览迭代协定 (opens new window),置信大家读过后会对迭代器在 ES6 中的实现有更深的了解。

实现深拷贝

简洁版本

简略版:

const newObj = JSON.parse(JSON.stringify(oldObj));

局限性:

  • 他无奈实现对函数、RegExp 等非凡对象的克隆
  • 会摈弃对象的constructor, 所有的构造函数会指向Object
  • 对象有循环援用, 会报错

面试简版

function deepClone(obj) {
    // 如果是 值类型 或 null,则间接 return
    if(typeof obj !== 'object' || obj === null) {return obj}

    // 定义后果对象
    let copy = {}

    // 如果对象是数组,则定义后果数组
    if(obj.constructor === Array) {copy = []
    }

    // 遍历对象的 key
    for(let key in obj) {
        // 如果 key 是对象的自有属性
        if(obj.hasOwnProperty(key)) {
          // 递归调用深拷贝办法
          copy[key] = deepClone(obj[key])
        }
    }

    return copy
} 

调用深拷贝办法,若属性为值类型,则间接返回;若属性为援用类型,则递归遍历。这就是咱们在解这一类题时的外围的办法。

进阶版

  • 解决拷贝循环援用问题
  • 解决拷贝对应原型问题
// 递归拷贝 (类型判断)
function deepClone(value,hash = new WeakMap){ // 弱援用,不必 map,weakMap 更适合一点
  // null 和 undefiend 是不须要拷贝的
  if(value == null){return value;}
  if(value instanceof RegExp) {return new RegExp(value) }
  if(value instanceof Date) {return new Date(value) }
  // 函数是不须要拷贝
  if(typeof value != 'object') return value;
  let obj = new value.constructor(); // [] {}
  // 阐明是一个对象类型
  if(hash.get(value)){return hash.get(value)
  }
  hash.set(value,obj);
  for(let key in value){ // in 会遍历以后对象上的属性 和 __proto__指代的属性
    // 补拷贝 对象的__proto__上的属性
    if(value.hasOwnProperty(key)){
      // 如果值还有可能是对象 就持续拷贝
      obj[key] = deepClone(value[key],hash);
    }
  }
  return obj
  // 辨别对象和数组 Object.prototype.toString.call
}
// test

var o = {};
o.x = o;
var o1 = deepClone(o); // 如果这个对象拷贝过了 就返回那个拷贝的后果就能够了
console.log(o1);

实现残缺的深拷贝

1. 简易版及问题

JSON.parse(JSON.stringify());

预计这个 api 能笼罩大多数的利用场景,没错,谈到深拷贝,我第一个想到的也是它。然而实际上,对于某些严格的场景来说,这个办法是有微小的坑的。问题如下:

  1. 无奈解决 循环援用 的问题。举个例子:
const a = {val:2};
a.target = a;

拷贝 a 会呈现零碎栈溢出,因为呈现了有限递归的状况。

  1. 无奈拷贝一些非凡的对象,诸如 RegExp, Date, Set, Map
  2. 无奈拷贝 函数(划重点)。

因而这个 api 先 pass 掉,咱们从新写一个深拷贝,简易版如下:

const deepClone = (target) => {if (typeof target === 'object' && target !== null) {const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {if (target.hasOwnProperty(prop)) {cloneTarget[prop] = deepClone(target[prop]);
      }
    }
    return cloneTarget;
  } else {return target;}
}

当初,咱们以刚刚发现的三个问题为导向,一步步来欠缺、优化咱们的深拷贝代码。

2. 解决循环援用

当初问题如下:

let obj = {val : 100};
obj.target = obj;

deepClone(obj);// 报错: RangeError: Maximum call stack size exceeded

这就是循环援用。咱们怎么来解决这个问题呢?

创立一个 Map。记录下曾经拷贝过的对象,如果说曾经拷贝过,那间接返回它行了。

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;

const deepClone = (target, map = new Map()) => {if(map.get(target))  
    return target; 


  if (isObject(target)) {map.set(target, true); 
    const cloneTarget = Array.isArray(target) ? []: {}; 
    for (let prop in target) {if (target.hasOwnProperty(prop)) {cloneTarget[prop] = deepClone(target[prop],map); 
      } 
    } 
    return cloneTarget; 
  } else {return target;} 
}

当初来试一试:

const a = {val:2};
a.target = a;
let newA = deepClone(a);
console.log(newA)//{val: 2, target: { val: 2, target: [Circular] } }

如同是没有问题了, 拷贝也实现了。但还是有一个潜在的坑, 就是 map 上的 key 和 map 形成了强援用关系,这是相当危险的。我给你解释一下与之绝对的弱援用的概念你就明确了

在计算机程序设计中,弱援用与强援用绝对,

被弱援用的对象能够在任何时候被回收,而对于强援用来说,只有这个强援用还在,那么对象无奈被回收。拿下面的例子说,map 和 a 始终是强援用的关系,在程序完结之前,a 所占的内存空间始终不会被开释。

怎么解决这个问题?

很简略,让 map 的 key 和 map 形成弱援用即可。ES6 给咱们提供了这样的数据结构,它的名字叫 WeakMap,它是一种非凡的 Map, 其中的键是弱援用的。其键必须是对象,而值能够是任意的

略微革新一下即可:

const deepClone = (target, map = new WeakMap()) => {//...}

3. 拷贝非凡对象

可持续遍历

对于非凡的对象,咱们应用以下形式来甄别:

Object.prototype.toString.call(obj);

梳理一下对于可遍历对象会有什么后果:

["object Map"]
["object Set"]
["object Array"]
["object Object"]
["object Arguments"]

以这些不同的字符串为根据,咱们就能够胜利地甄别这些对象。

const getType = Object.prototype.toString.call(obj);

const canTraverse = {'[object Map]': true,
  '[object Set]': true,
  '[object Array]': true,
  '[object Object]': true,
  '[object Arguments]': true,
};

const deepClone = (target, map = new Map()) => {if(!isObject(target)) 
    return target;
  let type = getType(target);
  let cloneTarget;
  if(!canTraverse[type]) {
    // 解决不能遍历的对象
    return;
  }else {
    // 这波操作相当要害,能够保障对象的原型不失落!let ctor = target.prototype;
    cloneTarget = new ctor();}

  if(map.get(target)) 
    return target;
  map.put(target, true);

  if(type === mapTag) {
    // 解决 Map
    target.forEach((item, key) => {cloneTarget.set(deepClone(key), deepClone(item));
    })
  }

  if(type === setTag) {
    // 解决 Set
    target.forEach(item => {target.add(deepClone(item));
    })
  }

  // 解决数组和对象
  for (let prop in target) {if (target.hasOwnProperty(prop)) {cloneTarget[prop] = deepClone(target[prop]);
    }
  }
  return cloneTarget;
}

不可遍历的对象

const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

对于不可遍历的对象,不同的对象有不同的解决。

const handleRegExp = (target) => {const { source, flags} = target;
  return new target.constructor(source, flags);
}

const handleFunc = (target) => {// 待会的重点局部}

const handleNotTraverse = (target, tag) => {
  const Ctor = targe.constructor;
  switch(tag) {
    case boolTag:
    case numberTag:
    case stringTag:
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}

4. 拷贝函数

  • 尽管函数也是对象,然而它过于非凡,咱们独自把它拿进去拆解。
  • 提到函数,在 JS 种有两种函数,一种是一般函数,另一种是箭头函数。每个一般函数都是
  • Function 的实例,而箭头函数不是任何类的实例,每次调用都是不一样的援用。那咱们只须要
  • 解决一般函数的状况,箭头函数间接返回它自身就好了。

那么如何来辨别两者呢?

答案是: 利用原型。箭头函数是不存在原型的。

const handleFunc = (func) => {
  // 箭头函数间接返回本身
  if(!func.prototype) return func;
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 别离匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
  } else {return new Function(body[0]);
  }
}

5. 残缺代码展现

const getType = obj => Object.prototype.toString.call(obj);

const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;

const canTraverse = {'[object Map]': true,
  '[object Set]': true,
  '[object Array]': true,
  '[object Object]': true,
  '[object Arguments]': true,
};
const mapTag = '[object Map]';
const setTag = '[object Set]';
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const handleRegExp = (target) => {const { source, flags} = target;
  return new target.constructor(source, flags);
}

const handleFunc = (func) => {
  // 箭头函数间接返回本身
  if(!func.prototype) return func;
  const bodyReg = /(?<={)(.|\n)+(?=})/m;
  const paramReg = /(?<=\().+(?=\)\s+{)/;
  const funcString = func.toString();
  // 别离匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString);
  const body = bodyReg.exec(funcString);
  if(!body) return null;
  if (param) {const paramArr = param[0].split(',');
    return new Function(...paramArr, body[0]);
  } else {return new Function(body[0]);
  }
}

const handleNotTraverse = (target, tag) => {
  const Ctor = target.constructor;
  switch(tag) {
    case boolTag:
      return new Object(Boolean.prototype.valueOf.call(target));
    case numberTag:
      return new Object(Number.prototype.valueOf.call(target));
    case stringTag:
      return new Object(String.prototype.valueOf.call(target));
    case symbolTag:
      return new Object(Symbol.prototype.valueOf.call(target));
    case errorTag: 
    case dateTag:
      return new Ctor(target);
    case regexpTag:
      return handleRegExp(target);
    case funcTag:
      return handleFunc(target);
    default:
      return new Ctor(target);
  }
}

const deepClone = (target, map = new WeakMap()) => {if(!isObject(target)) 
    return target;
  let type = getType(target);
  let cloneTarget;
  if(!canTraverse[type]) {
    // 解决不能遍历的对象
    return handleNotTraverse(target, type);
  }else {
    // 这波操作相当要害,能够保障对象的原型不失落!let ctor = target.constructor;
    cloneTarget = new ctor();}

  if(map.get(target)) 
    return target;
  map.set(target, true);

  if(type === mapTag) {
    // 解决 Map
    target.forEach((item, key) => {cloneTarget.set(deepClone(key, map), deepClone(item, map));
    })
  }

  if(type === setTag) {
    // 解决 Set
    target.forEach(item => {cloneTarget.add(deepClone(item, map));
    })
  }

  // 解决数组和对象
  for (let prop in target) {if (target.hasOwnProperty(prop)) {cloneTarget[prop] = deepClone(target[prop], map);
    }
  }
  return cloneTarget;
}

实现数组的 push 办法

let arr = [];
Array.prototype.push = function() {for( let i = 0 ; i < arguments.length ; i++){this[this.length] = arguments[i] ;
    }
    return this.length;
}

实现 Object.freeze

Object.freeze解冻一个对象,让其不能再增加 / 删除属性,也不能批改该对象已有属性的可枚举性、可配置可写性,也不能批改已有属性的值和它的原型属性,最初返回一个和传入参数雷同的对象

function myFreeze(obj){
  // 判断参数是否为 Object 类型,如果是就关闭对象,循环遍历对象。去掉原型属性,将其 writable 个性设置为 false
  if(obj instanceof Object){Object.seal(obj);  // 关闭对象
    for(let key in obj){if(obj.hasOwnProperty(key)){
        Object.defineProperty(obj,key,{writable:false   // 设置只读})
        // 如果属性值仍然为对象,要通过递归来进行进一步的解冻
        myFreeze(obj[key]);  
      }
    }
  }
}
退出移动版