关于前端:高频js手写题之实现数组扁平化深拷贝总线模式

36次阅读

共计 7825 个字符,预计需要花费 20 分钟才能阅读完成。

前言

今人学识无遗力,少壮时间老始成。纸上得来终觉浅,绝知此事要躬行。看懂一道算法题很快, 但咱们必须将这道题的思路理清、手写进去。

三道 js 手写题的思路和代码实现

数组扁平化

演示成果

将[1, [1, 2], [1, [2]]] 变成 [1, 1, 2, 1, 2]

第一种:间接应用.flat

console.log([1, [1,2],[1,[2]]].flat(3));
  • 能够将多维数组,降维,传的参数是多少就降多少维
  • 个别间接传参数为 Infinity(简略粗犷) 第二种: 递归办法的办法 + 借用数组的 API 实现

(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;
}

深浅拷贝

浅拷贝的实现

  1. 明确浅拷贝的局限性: 只能拷贝一层对象。如果存在对象的嵌套, 那么浅拷贝将无能为力
  2. 对于根底数据类型做一个最根本的拷贝
  3. 对援用类型开拓一个新的存储, 并拷贝一层对象属性
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')

深拷贝的最终版 ,

深拷贝的思路:

  1. 对于日期和正则的类型时, 进行解决 new 一个新的
  2. 对 a: {val: a} 这种循环援用时, 应用以 weakMap 进行奇妙解决
  3. 应用 Reflect.ownKeys 返回一个由指标对象本身的属性键组成的数组,
  4. 对于剩下的拷贝类型为 object 和 function 但不是 null 进行递归操作,
  5. 对于除了上述的类型外间接进行 ”key” 的赋值操作。细节解决:
  6. 利用 getOwnPropertyDescriptors 返回指定对象所有本身属性(非继承属性)的形容对象
  7. 将失去的属性利用 Object.create 进行继承原型链
  8. 对于 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

  • Object.create()办法创立一个新对象,应用现有的对象来提供新创建的对象的__proto__,
  • Object.create 如果该参数被指定且不为 undefined,该传入对象的自有可枚举属性 (即其本身定义的属性,而不是其原型链上的枚举属性) 将为新创建的对象增加指定的属性值和对应的属性描述符。
const person = {isHuman: false,};
const me = Object.create(person);
console.log(me.__proto__ === person);  // true

Object.getPrototypeOf 办法返回指定对象的原型 (外部[[Prototype]] 属性的值)继承原型链

WeakMap 对象是一组键值对的汇合,其中的键是弱援用对象,而值能够是任意。因为 WeakMap 是弱援用类型,能够无效避免内存透露, 作为检测循环援用很有帮忙,如果存在循环,则援用间接返回 WeakMap 存储的值。能够从这里理解更多的 WeapMap 和 Map 的区别

  1. Reflect.ownKeys == Object.getOwnPropertyNames(target) contact (Object.getOwnPropertySymbols(target)。
  2. Object.getOwnPropertyNames()办法返回一个由指定对象的所有本身属性的属性名(包含不可枚举属性但不包含 Symbol 值作为名称的属性)组成的数组。
  3. 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)}, {}, {} ] }

  1. 参数有两个(name, fn)name 为指定事件, fn 是一个回调函数
  2. 对于 fn 进行判断: 是否不存在、是否是非法的(为 function)、判断不能反复增加事件 on 的如下代码
function EventEmitter() {this.__events = {}
}

// 判断是否是非法的 listener
function 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)

正文完
 0