JS常见手写题 | 青训营

64 阅读4分钟
  • 关于对象深拷贝的手写
  • 关于对象合并的方案
  • 函数柯里化
  • queryURLParams实现
  • AOP面向切片编程

浅拷贝

浅拷贝:一个新的对象对原始对象的属性值进行精确的拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值,如果是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生改变。

  • Object.assign()
  • 扩展运算符
  • 数组方法实现Array.prototype.slice/concat,注:concat方法不会改变原数组,而是返回新数组。

数组的浅拷贝

let newArr = [...arr]
newArr = arr.concat([])
newArr = arr.slice()

若是引用数据类型,拷贝出来的数据依然有关联:

image.png

对象的浅拷贝

function shallowClone(obj) {
    if (!obj || typeof obj !== 'object) return
    
    let newObj = Array.isArray(object) ? [] : {}
    
    for (let key in object) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = obj[key]
        }
    }
    return newObj
}

对象的深拷贝

  1. JSON.parse(JSON.stringify()),无法处理函数、正则、时间格式、原型上的属性和方法
  2. 递归实现

注意:循环引用多个属性引用同一个对象(重复拷贝)的问题

// 循环拷贝:对象的属性引用自己
let obj = {name: 'lihua'}
obj.obj = obj

重复拷贝问题

let obj = {}
let obj1 = {a: obj, b: obj}
实现深拷贝
  • 使用hash 存储已拷贝过的对象,避免循环拷贝和重复拷贝
  • 兼容数组和对象
  • 避免重复拷贝
function isObject(target) {
  return typeof target === "object" && target !== null;
}

function deepClone(target, hash = new WeakMap()) {
  if (!isObject(target)) return target;
  if (hash.get(target)) return hash.get(target);
  // 兼容数组和对象
  let newObj = Array.isArray(target) ? [] : {};
  // 关键代码,解决对象的属性循环引用 和 多个属性引用同一个对象的问题,避免重复拷贝
  hash.set(target, newObj);
  for (let key in target) {
    if (target.hasOwnProperty(key)) {
      if (isObject(target[key])) {
        newObj[key] = deepClone(target[key], hash); // 递归拷贝
      } else {
        newObj[key] = target[key];
      }
    }
  }
  return newObj;
}

循环引用示例

let obj = { a: 1 };
let obj1 = {
  key1: info,
  key2: info,
  list: [1, 2],
};

obj1.key3 = obj1;
let val = deepClone(obj1);
console.log(val);

image.png

两个对象的合并

几种情况的分析:

  • A:options中的key值,B:params中的key值
  • A&B都是原始值类型:B替换A即可
  • A是对象&B是原始值:抛出异常信息
  • A是原始值&B是对象:B替换A即可
  • A&B都是对象:依次遍历B中的每一项,替换A中的内容
const options = {
  url: "",
  method: "GET",
  headers: {
    "Content-Type": "application/json",
  },
  data: null,
  arr: [10, 20, 30],
  config: {
    xhr: {
      async: true,
      cache: false,
    },
  },
};

const params = {
  url: "http://www.xxx.cn/api/",
  headers: {
    "X-Token": "EF00F987DCFA6D31",
  },
  data: {
    lx: 1,
    from: "weixin",
  },
  arr: [30, 40],
  config: {
    xhr: {
      cache: true,
    },
  },
};

function isObj(target) {
  return typeof target === "object" && target !== null;
}

function merge(options, params = {}) {
  for (let key in params) {
    let isA = isObj(options[key]),
      isB = isObj(params[key]);
    // A是对象&B是原始值:抛出异常信息
    if (isA && !isB) throw new TypeError(`${key} in params must be object`);
    // A&B都是对象:依次遍历B中的每一项,替换A中的内容
    if (isA && isB) {
      options[key] = merge(options[key], params[key]);
    }
    // A&B都是原始值类型:B替换A即可
    options[key] = params[key];
  }
  return options;
}
console.log(merge(options, params));

打印结果: image.png

函数柯里化

概念:将使用多个参数的一个函数,转换成一系列使用一个参数的函数
原理:用闭包把参数保存起来,当参数的长度等于原函数时,就开始执行原函数
function mycurry(fn) {
  // fn.length 表示函数中参数的长度
  // 函数的length属性,表示形参的个数,不包含剩余参数,仅包括第一个有默认值之前的参数个数(不包含有默认值的参数)
  if (fn.length <= 1) return fn;
  // 自定义generator迭代器
  const generator = (...args) => {
    // 判断已传的参数与函数定义的参数个数是否相等
    if (fn.length === args.length) {
      return fn(...args);
    } else {
      // 不相等,继续迭代
      return (...args1) => {
        return generator(...args, ...args1);
      };
    }
  };
  return generator;
}
function fn(a, b, c, d) {
  return a + b + c + d;
}
let fn1 = mycurry(fn);
console.log(fn1(1)(2)(3)(4));

打印结果:10

queryURLParams实现

  • parseParam 函数接受一个 URL 作为输入,然后从 URL 中提取查询参数部分(即 ? 后面的部分)。
  • paramsStr 变量通过正则表达式提取出查询参数部分。
  • paramsArr 变量将提取出来的查询参数部分以 & 分割,并存储为一个数组,每个元素都是一个查询参数。
  • paramsObj 变量用于存储最终解析后的参数对象。
  • paramsArr 数组被遍历,对每个查询参数进行处理:
    • 如果参数包含 =,表示它带有值,那么将键值对分割并解码。
    • 如果值是数字字符串,将其转换为数字。
    • 如果 paramsObj 中已经存在相同的键,则将值存为数组(因为一个键可以有多个值)。
    • 如果没有值(即没有 =),则将键存为 true
  • 最后,函数返回解析后的 paramsObj 对象。
function parseParam(url) {
  const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
  const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中
  let paramsObj = {};
  // 将 params 存到对象中
  paramsArr.forEach(param => {
    if (/=/.test(param)) { // 处理有 value 的参数
      let [key, val] = param.split('='); // 分割 key 和 value
      val = decodeURIComponent(val); // 解码
      val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字

      if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值
        paramsObj[key] = [].concat(paramsObj[key], val);
      } else { // 如果对象没有这个 key,创建 key 并设置值
        paramsObj[key] = val;
      }
    } else { // 处理没有 value 的参数
      paramsObj[param] = true;
    }
  })

  return paramsObj;
}
let url =
  "http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled";
console.log(parseParam(url));

结果为:

{ user: 'anonymous', id: [ 123, 456 ], city: '北京', enabled: true }

AOP面向切面编程

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过”动态织入“的方式掺入业务逻辑模块中.

  • before 方法的实现是,在原函数执行之前执行传入的回调函数。它创建了一个新的代理函数 proxy,这个代理函数首先执行传入的回调函数,然后再调用原函数,传递相同的参数。这样就实现了在原函数执行前添加功能的目的。
  • after 方法的实现是,在原函数执行之后执行传入的回调函数。类似于 before 方法,它也创建了一个代理函数 proxy。不过这次,它先调用原函数,获取到其返回值,并将其保存在 res 变量中。然后,它执行传入的回调函数,并返回之前保存的原函数返回值。
Function.prototype.before = function before(callback) {
  if (typeof callback !== "function")
    throw new TypeError("callback must be function");
  // this->func
  let _self = this;
  return function proxy(...params) {
    // this !== func 调用时候才知道
    //控制callback和func本身的先后执行顺序
    callback.call(this, ...params);
    return _self.call(this, ...params);
  };
};
Function.prototype.after = function after(callback) {
  if (typeof callback !== "function")
    throw new TypeError("callback must be function");
  let _self = this;
  return function proxy(...params) {
    let res = _self.call(this, ...params);
    callback.call(this, ...params);
    return res;
  };
};

let func = () => {
  // 主要的业务逻辑
  console.log("func");
};
/* func.before(() => {
    console.log('===before===');
})(); */

func
  .before(() => {
    console.log("===before===");
  })
  .after(() => {
    console.log("===after===");
  })();

打印结果: image.png