最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 数组扁平化、深拷贝、总线模式js手写

    正文概述 掘金(徙倚何依)   2021-04-11   429

    前言

    古人学问无遗力,少壮工夫老始成。纸上得来终觉浅,绝知此事要躬行。看懂一道算法题很快,但我们必须将这道题的思路理清、手写出来。将一道题的思路理一理在进行coding,我们可以来到死磕36道js手写题。

    三道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')
    

    数组扁平化、深拷贝、总线模式js手写 深拷贝的最终版

    深拷贝的思路:

    1. 对于日期和正则的类型时, 进行处理 new一个新的
    2. 对a: { val: a } 这种循环引用时, 使用以weakMap进行巧妙处理
    3. 使用Reflect.ownKeys返回一个由目标对象自身的属性键组成的数组,
    4. 对于剩下的拷贝类型为object和function但不是null进行递归操作,
    5. 对于除了上述的类型外直接进行"key"的赋值操作。

    细节处理:

    1. 利用getOwnPropertyDescriptors返回指定对象所有自身属性(非继承属性)的描述对象
    2. 将得到的属性利用Object.create进行继承原型链
    3. 对于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)
    

    数组扁平化、深拷贝、总线模式js手写 实现了对象的循环应用的拷贝

    对于上述代码进行说明:

    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', '若离老师')
    

    数组扁平化、深拷贝、总线模式js手写 新定义的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)
    

    数组扁平化、深拷贝、总线模式js手写

    尾声

    我只是一个前端小菜鸡, 正在学习js手写36道题目。代码如有问题,欢迎指出。

    本文总结参考:

    36道js手写题


    起源地下载网 » 数组扁平化、深拷贝、总线模式js手写

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元