二叉树层次遍历
// 二叉树层次遍历
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方法
// // 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 } }}
// 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 很相似,不多赘述
// 模拟 applyFunction.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+308Number.MAX_SAFE_INTEGER // 9007199254740991Number.MIN_VALUE // 5e-324Number.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 // trueNaN === 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中,又新增了Map和Set。四种数据结构各自有着自己特别的内部实现,但我们仍期待以同样的一套规则去遍历它们,所以ES6在推出新数据结构的同时也推出了一套 统一的接口机制 ——迭代器(Iterator)。
ES6约定,任何数据结构只要具备Symbol.iterator属性(这个属性就是Iterator的具体实现,它本质上是当前数据结构默认的迭代器生成函数),就可以被遍历——准确地说,是被for...of...循环和迭代器的 next 方法遍历。 事实上,for...of...的背后正是对next方法的反复调用。
在 ES6 中,针对Array、Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象这些原生的数据结构都可以通过for...of...进行遍历。原理都是一样的,此处我们拿最简单的数组进行举例,当我们用for...of...遍历数组时:
const arr = [1, 2, 3]const len = arr.lengthfor(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));
复制代码
局限性:
面试简版
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 能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:
无法解决循环引用的问题。举个例子:
const a = {val:2};a.target = a;
复制代码
拷贝a会出现系统栈溢出,因为出现了无限递归的情况。
无法拷贝一些特殊的对象,诸如 RegExp, Date, Set, Map等
无法拷贝函数(划重点)。
因此这个 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]); } } }}
复制代码
评论