高频js手写题之实现数组扁平化、深拷贝、总线模式

news2024/9/27 15:23:29

前言

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

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

image.png

深拷贝的最终版 ,

深拷贝的思路:

  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)

image.png 实现了对象的循环应用的拷贝

对于上述代码进行说明:

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

image.png 新定义的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)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/139881.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

抽象类与抽象方法

文章目录一、abstract关键字使用修饰类&#xff1a;抽象类修饰方法&#xff1a;抽象方法注意点抽象类的匿名子类一、abstract关键字使用 abstract&#xff1a;抽象的 可以修饰&#xff1a;类、方法 修饰类&#xff1a;抽象类 1、此类不可进行实例化 2、抽象类中一定有构造器…

报错 cannot import name ‘int‘ from ‘numpy‘

报错详情&#xff1a; 原因是因为np.int在numpy1.20已经被废弃掉了&#xff0c;可以通过 pip show numpy在命令行里查看。 现在使用的是np.int_ 或者 np.int32 或者 np.int64 猜测原因 但这个报错是在我自己的site-packages里的numpy的报错&#xff0c;我怀疑可能是numpy本身…

【linux】crontab

文章目录crontab简介crontab安装语法实例脚本无法执行问题常用的命令展示crontab的注意事项来源crontab简介 crontab命令常见于Unix和类Unix的操作系统之中&#xff0c;用于设置周期性被执行的指令。该命令从标准输入设备读取指令&#xff0c;并将其存放于“crontab”文件中&a…

linux系统中CAN驱动的通信方法与原理

大家好&#xff0c;今天主要和大家分享一下&#xff0c;如何使用linux系统下的CAN驱动实验。 目录 第一&#xff1a;CAN通信基本简介 第二&#xff1a;CAN通信的主要特点 第三&#xff1a;CAN通信协议 第四&#xff1a;程序代码的具体实现 第五&#xff1a;使能Linux内核自…

MATLAB-ezplot绘图函数

ezplot 函数与fplot 函数类似&#xff0c;该函数可以绘制显函数图形、隐函数图形和参数方程图形。ezplot函数的调用格式如下。 ezplot(f) ezplot(f, [ min , max ]) ezplot(f.[ xmin , xmax , ymin , ymax]) ezplot(x,y) ezplot(x,y , [tmin , tmax]) ezplot(.. . ,f…

jvm内存管理

参考链接 参考链接 Garbage Collection Concepts garbage collector的作用包括&#xff1a; 分配内存确定活着的对象不被清理回收死了的对象占用的内存 寻找和释放垃圾占用的内存空间的过程称为garbage collection一般情况下&#xff0c;整个堆或堆的一部分被填满时或者达到…

C++11 多线程

线程&#xff08;thread&#xff09;是操作系统能够进行运算调度的最小单位&#xff0c;它被包含在进程中&#xff0c;是进程中的实际运作单位&#xff0c;一条线程指的是进程中一个单一顺序的控制流&#xff0c;一个进程可以并发多个线程&#xff0c;每条线程执行不同的任务。…

FreeRTOS教程——定时器(二)

Free RTOS定时器 一、概念 一、概论 软件定时器允许设置一段时间&#xff0c;当设置的时间到达之后就执行指定的功能函数&#xff0c;被定时器 调用的这个功能函数叫做定时器的回调函数。回调函数的两次执行间隔叫做定时器的定时周期&#xff0c; 简而言之&#xff0c;当定时…

switch分支结构

一. 简介switch结合case&#xff0c;能够判断一个变量或表达式与一系列值中的某个值是否相等&#xff0c;这里的每个值都被称为一个分支。switch语句在执行时&#xff0c;会先进行值的匹配&#xff0c;匹配成功时会进入到对应case语句。再根据是否有 break语句&#xff0c;判断…

手把手教你正确地创建并配置一个SpringBoot项目

文章目录1. 安装Spring Boot Helper插件2. 创建SpringBoot项目3. 配置SpringBoot项目4. 选择修改配置&#xff08;选做&#xff09;4.1 修改端口号4.2 其他自定义配置5. SpringBoot热部署本文主要是针对IDEA社区版用户的&#xff0c;如果你是专业版的用户&#xff0c;那么是可以…

RHCE第三天之ssh远程连接服务

文章目录一、连接加密技术简介二、SSH的工作过程三、 SSH远程连接服务配置四、SSH实验SSH&#xff08;Secure Shell Protocol&#xff0c;安全的壳程序协议&#xff1a; 它可以通过数据包加密技术将等待传输的数据包加密后再传输到网络上。ssh协议本身提供两个服务器功能&#…

爬虫学习-验证码识别

反爬机制&#xff1a;验证码&#xff0c;识别验证码图片中的数据&#xff0c;用于模拟登陆识别验证码的操作人工肉眼识别(不推荐&#xff09;第三方自动识别(推荐)python第三方库&#xff1a;tesseract、ddddocr(7条消息) 小白都能轻松掌握&#xff0c;python最稳定的图片识别库…

探索用于NLP的Gensim库

Gensim的名字源自于"Generate Similar," 这个词是指Gensim可以用于生成类似的文本。这个词也可以被解释为"Generative Similarity," 表示Gensim可以用于生成相似的文本。Gensim是一个用于文本处理的库,可以用于计算文本之间的相似度,以及生成类似的文本。…

实验四:ESP8266WIFI通讯实验

本实验开发板基于&#xff1a;GD32F103我们首先需要看一下原理图 根据原理图可以看到&#xff0c;ESP8266是通过PA2 PA3这个串口进行通讯&#xff0c;PA13是控制它的复位&#xff0c;从芯片手册中可以看到PA2PA3是串口1&#xff0c;PA2是串口1的发送&#xff0c;PA3是串口1的接…

时间序列分析之ARIMA预测

预备知识 时间序列分析原理 时间序列分析之auto_arima自动调参 一、定义 ARIMA模型(Autoregressive Integrated Moving Average model)&#xff0c;差分整合移动平均自回归模型&#xff0c;又称整合移动平均自回归模型&#xff0c;时间序列预测分析方法之一。 ARIMA(p,d,q){A…

Go语言设计与实现 -- Channel

稍微需要注意一点的用法 类型断言 type dog struct {Name stringColor string }func main() {allChan : make(chan any, 10)allChan <- dog{Name: "lxy", Color: "yellow"}// 如果你这么写代码的话&#xff0c;你虽然拿到了一条狗&#xff0c;但是你…

Map遍历方法及效率

在大学的时候记得学过通过迭代器进行Map的遍历&#xff0c;但是从参加工作后&#xff0c;基本都是通过for循环遍历&#xff0c;没用过迭代器&#xff0c;于是去了解了Map的几种遍历方法并通过运行测试各自的速度。 注意&#xff1a;这里只讲通过遍历同时能过获取key和value的遍…

Imaging组件格式转换,Imaging图像转换

Imaging组件格式转换,Imaging图像转换 Imaging是一个.NET组件&#xff0c;它提供了一种加载、编辑和保存图片的简单方法。图像处理允许文件格式转换和图像转换(调整大小、裁剪或旋转以及翻转)。 使用GemBox.Imageing&#xff0c;您将获得一个快速可靠的组件&#xff0c;它易于使…

聊聊帮助别人这件事--爱摸鱼的美工(10)

聊聊帮助别人这件事 曾经我是一个不懂拒绝的人 有时帮助别人是本性的善良 有时内心也感觉是被迫而已 不快乐为什么还要做&#xff1f; 后来&#xff0c;我减少了无用社交 后来&#xff0c;我脸皮厚了学会了拒绝 才发现&#xff0c;恰如其分的帮助 让自己和别人都舒服 才发现&am…

【微服务】Nacos配置管理

文章目录统一配置管理在nacos中添加配置文件从微服务拉取配置配置热更新方式一方式二配置共享配置共享的优先级Nacos集群搭建集群结构图搭建集群统一配置管理 Nacos除了可以做注册中心&#xff0c;同样可以做配置管理来使用。 当微服务部署的实例越来越多&#xff0c;达到数十、…