前言
古人学问无遗力,少壮工夫老始成。纸上得来终觉浅,绝知此事要躬行。看懂一道算法题很快,但我们必须将这道题的思路理清、手写出来。
三道 js 手写题的思路和代码实现
数组扁平化
演示效果
将[1, [1, 2], [1, [2]]] 变成 [1, 1, 2, 1, 2]
第一种: 直接使用.flat
console.log([1, [1,2],[1,[2]]].flat(3));
复制代码
(1)
function flattten(arr) { var result = []; for(var i = 0, len = arr.length; i < len; i++) { if(Array.isArray(arr[i])) { // Array.isArray 判断是否为数组 result = result.concat(flattten(arr[i])) // concat() 方法用于连接两个或多个数组。 } else { result.push(arr[i]) } } return result;}
复制代码
(2)
function flatten(arr) { return arr.reduce((pre, cur) => { return pre.concat(Array.isArray(cur) ? flatten(cur) : cur); }, []);}
复制代码
第四种: some + ...(扩展运算符) + .concat
function flattten(arr) { // some() 方法用于检测数组中的元素是否满足指定条件(函数提供)。 // some() 方法会依次执行数组的每个元素: // 如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检测。 // 如果没有满足条件的元素,则返回false。
while(arr.some(item => Array.isArray(item))) { console.log(arr) arr = [].concat(...arr) // ... 会将多维数组降维一层 } return arr}
复制代码
第五种: 将多维数组转换成字符串,在进行操作
(1)
function flatten(arr) { let str = arr.toString(); str = str.replace(/(\[|\])/g, '').split(',').map(Number) return str;}
复制代码
/([|])/g 正则表达式 () 代表一个分组, \是转义字符(因为正则表达式规则中有 [ 和 ]的语法, 用\就可以让规则忽略[和]) /g 为全局匹配, 只要遇到了[ 和 ], 就用''这个来代替。
replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。(2)
function flatten(arr) { let result = arr.toString(); result = result.replace(/(\[|\])/g, ''); result = '[' + result + ']'; result = JSON.parse(result); // JSON.parse()可以把JSON规则的字符串转换为JSONObject return result;}
复制代码
深浅拷贝
浅拷贝的实现
明白浅拷贝的局限性: 只能拷贝一层对象。 如果存在对象的嵌套, 那么浅拷贝将无能为力
对于基础数据类型做一个最基本的拷贝
对引用类型开辟一个新的存储, 并拷贝一层对象属性
function deepClone(target) { if(typeof target === 'object' && target != null) { // 判断是数组还是对象 const targetclone = Array.isArray(target)? []:{} // 键值是否存在 for(let prop in target) { if(target.hasOwnProperty(prop)) { // hasOwnProperty() 方法不会检测对象的原型链, // 只会检测当前对象本身,只有当前对象本身存在该属性时才返回 true。 targetclone[prop] = (typeof target[prop] === 'object')? deepClone(target[prop]):target[prop] } } return targetclone; } else { return target; }} let arr1 = [ 1, 2, { val: 4, xdm: { dd: 99 } } ]; let str = shallowerClone(arr1) console.log(arr1, 'arr1') console.log(str, 'str') str.push({mo: '兄弟们'}) console.log('str.push-----------') console.log(arr1, 'arr1') console.log(str, 'str + push')
复制代码
深拷贝的最终版 ,参考 前端手写面试题详细解答
深拷贝的思路:
对于日期和正则的类型时, 进行处理 new 一个新的
对 a: { val: a } 这种循环引用时, 使用以 weakMap 进行巧妙处理
使用 Reflect.ownKeys 返回一个由目标对象自身的属性键组成的数组,
对于剩下的拷贝类型为 object 和 function 但不是 null 进行递归操作,
对于除了上述的类型外直接进行"key"的赋值操作。 细节处理:
利用 getOwnPropertyDescriptors 返回指定对象所有自身属性(非继承属性)的描述对象
将得到的属性利用 Object.create 进行继承原型链
对于 a: { val: a} 循环引用使用 weakMap.set 和 get 进行处理。 实现代码
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)const deepClone = function (obj, hash = new WeakMap()) { if (obj.constructor === Date) return new Date(obj) // 日期对象直接返回一个新的日期对象 if (obj.constructor === RegExp) return new RegExp(obj) //正则对象直接返回一个新的正则对象 //如果循环引用了就用 weakMap 来解决 if (hash.has(obj)) return hash.get(obj) let allDesc = Object.getOwnPropertyDescriptors(obj) //遍历传入参数所有键的特性 let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc) //继承原型链 hash.set(obj, cloneObj) for (let key of Reflect.ownKeys(obj)) { // 针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法 cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key] // typeof obj[key] !== 'function') } return cloneObj}
复制代码
检测代码
let obj = { num: 0, str: '', boolean: true, unf: undefined, nul: null, obj: { name: '我是一个对象', id: 1 }, arr: [0, 1, 2], func: function () { console.log('我是一个函数') }, date: new Date(0), reg: new RegExp('/我是一个正则/ig'), [Symbol('1')]: 1,};Object.defineProperty(obj, 'innumerable', { enumerable: false, value: '不可枚举属性' });obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))obj.loop = obj // 设置loop成循环引用的属性let cloneObj = deepClone(obj)cloneObj.arr.push(4)console.log('obj', obj)console.log('cloneObj', cloneObj)console.log(cloneObj.func)
复制代码
实现了对象的循环应用的拷贝
对于上述代码进行说明:
Object.getOwnPropertyDescriptors 返回指定对象所有自身属性(非继承属性)的描述对象。可以去这里了解更多 api
const person = { isHuman: false,};const me = Object.create(person);console.log(me.__proto__ === person); // true
复制代码
Object.getPrototypeOf 方法返回指定对象的原型(内部[[Prototype]]属性的值)继承原型链
WeakMap 对象是一组键值对的集合,其中的键是弱引用对象,而值可以是任意。因为 WeakMap 是弱引用类型,可以有效防止内存泄漏,作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值。可以从这里了解更多的 WeapMap 和 Map 的区别
Reflect.ownKeys == Object.getOwnPropertyNames(target) contact (Object.getOwnPropertySymbols(target)。
Object.getOwnPropertyNames()方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括 Symbol 值作为名称的属性)组成的数组。
Object.getOwnPropertySymbols() 方法返回一个给定对象自身的所有 Symbol 属性的数组
事件总线(发布订阅模式)
原理:
事件总线
是发布/订阅模式的实现,其中发布者发布数据,并且订阅者可以监听这些数据并基于这些数据作出处理。这使发布者与订阅者松耦合。发布者将数据事件发布到事件总线,总线负责将它们发送给订阅者
on 或 addListener(event, listenr)
就是为指定事件添加一个监听器到监听数组的尾部。
off 或 removeListener(event, listenr)
移除指定事件的某个监听器, 监听器必须是该事件已经注册过的监听事件。
emit(event, [arg1], [arg2] ...)
按照参数的顺序执行每个监听器, 如果事件有注册监听返回 true, 否则返回 false。 利用 Node.js 来了解 事件总线
var events = require('events');var eventEmitter = new events.EventEmitter();eventEmitter.on('say', function(name) { console.log('Hello', name);})eventEmitter.emit('say', '若离老师');function helloA(name) { console.log("helloAAAAAAA", name)}
function helloB(name) { console.log("helloBBBBBBB", name)}
eventEmitter.on('say', helloA)eventEmitter.on('say', helloB)eventEmitter.emit('say', '若离老师')eventEmitter.off('say', helloB);eventEmitter.emit('say', '若离老师')
复制代码
新定义的 eventEmitter 是接收 events.EventEmitter 模块 new 之后返回的一个实例,eventEmitter 的 emit 方法,发出 say 事件,通过 eventEmitter 的 on 方法监听,从而执行相应的函数。当触发 off 时, 将 say 事件上的响应函数删除。
on 实现代码:
on 的实现思路
对于 on 为指定事件添加一个监听器: 形式为{"say": [ {listener:(函数) , once:(false or true)}, {}, {} ] }
参数有两个(name, fn)name 为指定事件, fn 是一个回调函数
对于 fn 进行判断: 是否不存在、是否是合法的(为 function)、判断不能重复添加事件 on 的如下代码
function EventEmitter() { this.__events = {}}
// 判断是否是合法的 listenerfunction isValidListener(listener) { if (typeof listener === 'function') { return true; } else if (listener && typeof listener === 'object') { // listener 作为自定义事件的回调,必须是一个函数, // 另外判断是否是object这块递归的去找对象中是否还存在函数,如果不是函数, // 自定义事件没有回调肯定是不行的 return isValidListener(listener.listener); } else { return false; }}// 顾名思义,判断新增自定义事件是否存在function indexOf(array, item) { var result = -1 item = typeof item === 'object' ? item.listener : item; for (var i = 0, len = array.length; i < len; i++) { if (array[i].listener === item) { result = i; break; } } return result;}EventEmitter.prototype.on = function(eventName, listener){ if (!eventName || !listener) return; // 判断回调的 listener 是否为函数 if (!isValidListener(listener)) { throw new TypeError('listener must be a function'); } let events = this.__events; console.log(events) // var listeners = events[eventName] = events[eventName] = events[eventName] || []; events[eventName] = events[eventName] || []; let listeners = events[eventName] // listenerIsWrapped 表示是否已经封装了{listener: listener,once: false} let listenerIsWrapped = (typeof listener === 'object'); // 不重复添加事件,判断是否有一样的 if (indexOf(listeners, listener) === -1) { listeners.push(listenerIsWrapped ? listener : { listener: listener, once: false }); } return this; // this指向EventEmitter,返回的是实际调用这个方法的实例化对象};
复制代码
连等赋值操作的坑:A = B = C 其中执行的顺序为 B=C A = B emit 的代码实现
emit 的思路
从 this._events 中拿出相应的监听事件进行执行(注意多个事件的执行)
emit 的如下代码
EventEmitter.prototype.emit = function(eventName,...args) { // 直接通过内部对象获取对应自定义事件的回调函数 let listeners = this.__events[eventName]; if (!listeners) return; // 需要考虑多个 listener 的情况 for (let i = 0; i < listeners.length; i++) { let listener = listeners[i]; if (listener) { listener.listener.call(this, ...args || []); // 给 listener 中 once 为 true 的进行特殊处理 if (listener.once) { this.off(eventName, listener.listener) } } } return this;};
复制代码
listener.listener.call(this, ...args || []); 将 this 绑定到 listener.listener 然后进行执行相应的函数。 例如:当执行到 fn1 时, fn1.call(this, name, age)。相当于执行函数 fn1()。 off 的代码实现
off 的思路
将监听事件上相应的函数进行删除
off 的代码如下
EventEmitter.prototype.off = function(eventName, listener) { // 进行基础的判断 let listeners = this.__events[eventName]; let index = -1; if(!listeners) return; for(let i = 0; i < listeners.length; i++) { if(listeners[i] && listeners[i].listener === listener) { index = i; break; } } if(index !== -1) { listeners.splice(index, 1, null); } return this;
}
复制代码
发布订阅模式的检测代码:
let eventBus = new EventEmitter()let fn1 = function(name, age) { console.log(`${name} ${age}`)}let fn2 = function(name, age) { console.log(`hello, ${name} ${age}`)}let fn3 = function(name, age) { console.log(`hello myname is, ${name} ${age}`)}eventBus.on('say', fn1)eventBus.on('say', fn2)eventBus.on('say', fn3)eventBus.emit('say','布兰', 12)eventBus.off('say', fn1)console.log('使用off删除了say事件上的fn1函数-------')eventBus.emit('say','布兰', 12)
复制代码
评论