js面试——踩雷几率99%的5道“JS手撕源码”题

文章探讨了JavaScript中的对象拷贝,包括浅拷贝与深拷贝的概念及实现方式,特别是深拷贝通过JSON序列化处理的限制和手动实现的全面拷贝代码。此外,还介绍了对象的合并,如何处理Object.assign()的局限性,以及如何自定义实现合并功能。文章进一步讨论了函数柯里化,包括bind方法的实现和柯里化在正则验证中的应用。最后,文章提到了面向切面编程(AOP)的概念,并展示了如何在JavaScript中实现函数的before和after钩子。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、关于对象“copy”深浅拷贝的探究

  • 浅拷贝      对象的浅拷贝是其属性与拷贝源对象的属性共享相同引用(指向相同的底层值)的副本。(只能copy第一层)

在 JavaScript 中,所有标准的内置对象复制操作(展开语法Array.prototype.concat()Array.prototype.slice()Array.from()Object.assign() 和 Object.create())创建的是浅拷贝而不是深拷贝。

  • 深拷贝  对象的深拷贝是指其属性与其拷贝的源对象的属性不共享相同的引用的副本。

如果一个 JavaScript 对象可以被序列化,则存在一种创建深拷贝的方式:使用 JSON.stringify() 将该对象转换为 JSON 字符串,然后使用 JSON.parse() 将该字符串转换回(全新的)JavaScript 对象:

let ingredients_list = ["noodles", { list: ["eggs", "flour", "water"] }];
let ingredients_list_deepcopy = JSON.parse(JSON.stringify(ingredients_list));

// 改变深拷贝的对象不会影响到原来的对象
ingredients_list_deepcopy[1].list = ["rice flour", "water"];

console.log(ingredients_list[1].list);
// Array(3) [ "eggs", "flour", "water" ]

但许多 JavaScript 对象根本不能序列化——例如,函数(带有闭包)、Symbol、在 HTML DOM API 中表示 HTML 元素的对象、递归数据以及许多其他情况。在这种情况下,调用 JSON.stringify() 来序列化对象将会失败。所以没有办法对这些对象进行深拷贝。

  • 手写全面的拷贝代码   

   1、先准备两个工具方法——检测数据类型、遍历数组和对象。

  • 定义变量
    let class2type = {};
    let toString = class2type.toString;
    let hasOwn = class2type.hasOwnProperty;
    let utils = {};
    [
      "Boolean",
      "Number",
      "String",
      "Symbol",
      "BigInt",
      "Object",
      "Array",
      "Date",
      "RegExp",
      "Error",
      "Function",
    ].forEach((name) => {
      class2type[`[object ${name}]`] = name.toLowerCase();
    });
    
    //class2type: {[object Boolean]: 'boolean', [object Number]: 'number', [object String]: 'string',....}
    
    //参照格式:Object.proretype.toString.call(1)   '[object Number]'
  • 检测数组类型
    //基本数据类型用typeof 判断,引用数据类型用toString 判断
    const toType = function toType(obj) {
      if (obj == null) return obj + "";
      return typeof obj === "object" || typeof obj === "function"
        ? class2type[toString.call(obj)] || "object"
        : typeof obj;
    };
  • 判断是否是数组和类数组
    // 检测是否为函数/window 返回布尔值  obj.nodeType 只读属性,当前节点的类型
    const isFunction = function isFunction(obj) {
      return typeof obj === "function" && typeof obj.nodeType !== "number";
    };
    
    const isWindow = function isWindow(obj) {
      return obj != null && obj === obj.window;
    };
    // 检测是否为数据或者类数组
    const isArrayLike = function isArrayLike(obj) {
      let length = !!obj && "length" in obj && obj.length, //true or false 类数组中会有length属性
        type = toType(obj);
      if (isFunction(obj) || isWindow(obj)) return false;
      return (
        type === "array" ||
        length === 0 ||
        (typeof length === "number" && length > 0 && length - 1 in obj)
      );
    };
  • 遍历数组和对象
    // 遍历数组/类数组/对象
    const each = function each(obj, callback) {
      callback = callback || Function.prototype;
      //类数组
      if (isArrayLike(obj)) {
        for (let i = 0; i < obj.length; i++) {
          let item = obj[i],
            result = callback.call(item, item, i);
          if (result === false) break;
        }
        return obj;
      }
      //对象
      for (let key in obj) {
        //obj.hasOwnProperty 如果obj是一个空对象就break
        if (!hasOwn.call(obj, key)) break;
        let item = obj[key],
          result = callback.call(item, item, key);
        if (result === false) break;
      }
      return obj;
    };
  • 挂载和导出
    utils.toType = toType;
    utils.isFunction = isFunction;
    utils.isWindow = isWindow;
    utils.isArrayLike = isArrayLike;
    utils.each = each;
    
    // 暴露API:支持浏览器导入和CommonJS/ES6Module规范
        if (typeof window !== "undefined") {
            window._ = window.utils = utils;
        }
        if (typeof module === "object" && typeof module.exports === "object") {
            module.exports = utils;
        }

    2、实现不可枚举属性的浅拷贝

    // 浅克隆
    function shallowClone(obj) {
        let type = _.toType(obj),
            Ctor = obj.constructor;
    
        // 对于Symbol/BigInt
        if (/^(symbol|bigint)$/i.test(type)) return Object(obj);
    
        // 对于正则/日期的处理
        if (/^(regexp|date)$/i.test(type)) return new Ctor(obj);
    
        // 对于错误对象的处理 
        if (/^error$/i.test(type)) return new Ctor(obj.message);
    
        // 对于函数
        if (/^function$/i.test(type)) {
            // 返回新函数:新函数执行还是把原始函数执行,实现和原始函数相同的效果
            return function () {
                return obj.call(this, ...arguments);
            };
        }
    
        // 数组或者对象
        if (/^(object|array)$/i.test(type)) {
            let keys = [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)],
                result = new Ctor();
            _.each(keys, key => {
                result[key] = obj[key];
            });
            return result;
           
        }
        //其他的基本数据类型
        return obj;
    }
    

       3、解决不能序列化对象的深拷贝

// 深克隆:只要有下一级的,我们就克隆一下(浅克隆)
function deepClone(obj, cache = new Set()) {
  let type = _.toType(obj),
    Ctor = obj.constructor;
  //不是对象或数组走浅拷贝
  if (!/^(object|array)$/i.test(type)) return shallowClone(obj);

  // 利用cache.has(obj)来避免无限套娃 obj{xxx:obj}(这种就是套娃情况会一直找下去)
  if (cache.has(obj)) return obj;
  cache.add(obj);

  let keys = [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)],
    result = new Ctor();
  _.each(keys, (key) => {
    // 再次调用deepClone的时候把catch传递进去,保证每一次递归都是一个cache
    result[key] = deepClone(obj[key], cache);
  });
  return result;
}

二、关于对象“merge合并”的完整方案

我们常用到的对象合并方法:Object.assign() 静态方法将一个或者多个源对象中所有可枚举自有属性复制到目标对象,并返回修改后的目标对象。

   但是object.assign()方法会直接替换同属性的值,不会让两个相同键(属性)的属性值进行合并 。解决办法如下代码:遍历赋值。

/* 
 * 两个对合并的意义:
 *   + 插件组件封装:参数处理
 *   + 业务需求
 *   + ...
 */

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.zhufengpeixun.cn/api/',
    headers: {
        'X-Token': 'EF00F987DCFA6D31'
    },
    data: {
        lx: 1,
        from: 'weixin'
    },
    arr: [30, 40],
    config: {
        xhr: {
            cache: true
        }
    }
};
// 基于浅比较实现的对象的合并
// let xx = Object.assign(options, params);

/* 
 * 几种情况的分析
 *   A->options中的key值  B->params中的key值
 *   1.A&B都是原始值类型:B替换A即可
 *   2.A是对象&B是原始值:抛出异常信息
 *   3.A是原始值&B是对象:B替换A即可
 *   4.A&B都是对象:依次遍历B中的每一项,替换A中的内容
 */
// params替换options
function isObj(value) {
    // 是否为普通对象
    return _.toType(value) === "object";
}

function merge(options, params = {}) {
    _.each(params, (_, key) => {
        let isA = isObj(options[key]),
            isB = isObj(params[key]);
        if (isA && !isB) throw new TypeError(`${key} in params must be object`);
        if (isA && isB) {
            options[key] = merge(options[key], params[key]);
            return;
        }
        options[key] = params[key];
    });
    return options;
}

console.log(merge(options, params));
/*{
    "url": "http://www.zhufengpeixun.cn/api/",
    "method": "GET",
    "headers": {
        "Content-Type": "application/json",
        "X-Token": "EF00F987DCFA6D31"
    },
    "data": {
        "lx": 1,
        "from": "weixin"
    },
    "arr": [
        30,
        40
    ],
    "config": {
        "xhr": {
            "async": true,
            "cache": true
        }
    }
}*/

三、“函数柯里化”的两大运用:bind&currying

官方的解释是,把接受多个参数的函数变换成只接后一个单一参数的函数,并返回接受余下的参数和结果的新函数的技术。

  一个常用的用柯里化机制实现的就是bind方法:

Function.prototype.bind = function bind(context, ...params) {
  // this/self->func  context->obj  params->[10,20]
  let self = this;

  return function proxy(...args) {
    // 把func执行并且改变this即可  args->是执行proxy的时候可能传递的值
    self.apply(context, params.concat(args));
  };
};
function currying(fn, ...rest1) {
  return function(...rest2) {
    return fn.apply(null, rest1.concat(rest2))
  }
}

  用柯里化函数来实现正则验证:

// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}

check(/\d+/g, 'test1') //true
check(/[a-z]+/g, 'test') //true

// Currying后
function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}
let hasNumber = curryingCheck(/\d+/g) //对应reg参数

hasNumber('test1') // true
hasNumber('testtest') // false

四、queryURLParams的三种实现方案

可以去参考知乎的这篇文章  2022年了!你有几种获取URL参数的方法? - 知乎

五、彻底玩转AOP面向切片编程

面向切面编程(Aspect-Oriented Programming,AOP)是一种编程范式,是在面向对象编程(OOP)的基础上的一种补充。AOP提供了一种灵活的、易于维护和跨多个对象和组件的切面(Aspect)的方式,可以在不修改原有代码的情况下实现代码的特定功能。

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===');
})();


/* function handle(func, before, after) {
    before();
    func();
    after();
} */

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值