前端面试--手写题

205 阅读29分钟

数组去重

1. ES6方法 (Set + Array.from )
    const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];

    Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8]
2. 使用map存储不重复的数字

    const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];

    uniqueArray(array); // [1, 2, 3, 5, 9, 8]

    function uniqueArray(array) {
      let map = {};
      let res = [];
      for(var i = 0; i < array.length; i++) {
        if(!map.hasOwnProperty([array[i]])) {
          map[array[i]] = 1;
          res.push(array[i]);
        }
      }
      return res;
    }
3.使用 filter

/**
 * 使用 filter 和 indexOf 实现数组去重。
 *
 * @param {Array} arr - 需要去重的数组。
 * @returns {Array} 去重后的数组。
*/

const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];

function uniqueWithFilter(arr) {
  // 使用 filter 方法,只保留第一次出现的元素
  return arr.filter((item, index) => arr.indexOf(item) === index);
}

uniqueWithFilter(array)   // [1, 2, 3, 5, 9, 8]
4. 使用 reduce
 /**
 * 使用 reduce 实现数组去重。
 *
 * @param {Array} arr - 需要去重的数组。
 * @returns {Array} 去重后的数组。
*/

const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];

function uniqueWithReduce(arr) {

  // 使用 reduce 方法构建一个新数组
  return arr.reduce((accumulator, currentValue) => {
  
    // 如果新数组中没有当前值,则添加到新数组中
    if (!accumulator.includes(currentValue)) {
      accumulator.push(currentValue);
    }
    return accumulator;
  }, []);
}

uniqueWithReduce(array)     // [1, 2, 3, 5, 9, 8]
5. 使用 ES6 ... 操作符和 Set
 
/**
 * 使用 ES6 Spread 操作符和 Set 实现数组去重。
 *
 * @param {Array} arr - 需要去重的数组。
 * @returns {Array} 去重后的数组。
 */
 
    const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8]; 
    function uniqueWithSpreadAndSet(arr) {
      // 先将数组转换为 Set,然后使用 Spread 操作符将其转换回数组
      return [...new Set(arr)];
    }
    
    uniqueWithSpreadAndSet(array)    //  [1, 2, 3, 5, 9, 8]

数组扁平化flatten

1. 递归实现

普通的递归思路很容易理解,就是通过循环递归的方式,一项一项地去遍历,如果每一项还是一个数组,那么就继续往下遍历,利用递归程序的方法,来实现数组的每一项的连接:

    let arr = [1, [2, [3, 4, 5]]];
    
    function flatten(arr) {
      let result = [];

      for(let i = 0; i < arr.length; i++) {
        if(Array.isArray(arr[i])) {
          result = result.concat(flatten(arr[i]));
        } else {
          result.push(arr[i]);
        }
      }
      return result;
    }
    
    flatten(arr);  //  [1, 2, 3, 4,5]
2. reduce 函数迭代

从上面普通的递归函数中可以看出,其实就是对数组的每一项进行处理,那么其实也可以用reduce 来实现数组的拼接,从而简化第一种方法的代码,改造后的代码如下所示:

    let arr = [1, [2, [3, 4]]];
    
    function flatten(arr) {
        return arr.reduce(function(prev, next){
            return prev.concat(Array.isArray(next) ? flatten(next) : next)
        }, [])
    }
    
    console.log(flatten(arr));//  [1, 2, 3, 4,5]
3. 扩展运算符实现

这个方法的实现,采用了扩展运算符和 some 的方法,两者共同使用,达到数组扁平化的目的:

    let arr = [1, [2, [3, 4]]];
    
    function flatten(arr) {
        while (arr.some(item => Array.isArray(item))) {
            arr = [].concat(...arr);
        }
        return arr;
    }
    
    console.log(flatten(arr)); //  [1, 2, 3, 4,5]
4. split 和 toString

可以通过 splittoString 两个方法来共同实现数组扁平化,由于数组会默认带一个 toString 的方法,所以可以把数组直接转换成逗号分隔的字符串,然后再用 split 方法把字符串重新转换为数组,如下面的代码所示:

    let arr = [1, [2, [3, 4]]];
    
    function flatten(arr) {
        return arr.toString().split(',');
    }
    
    console.log(flatten(arr)); //  [1, 2, 3, 4,5]
5. ES6 中的 flat

我们还可以直接调用 ES6 中的 flat 方法来实现数组扁平化。flat 方法的语法:arr.flat([depth])

其中 depthflat 的参数,depth 是可以传递数组的展开深度(默认不填、数值是 1),即展开一层数组。如果层数不确定,参数可以传进 Infinity,代表不论多少层都要展开:

    let arr = [1, [2, [3, 4]]];
    
    function flatten(arr) {
      return arr.flat(Infinity);
    }
    
    console.log(flatten(arr)); //  [1, 2, 3, 4,5]
6. 正则和 JSON 方法

在第4种方法中已经使用 toString 方法,其中仍然采用了将 JSON.stringify 的方法先转换为字符串,然后通过正则表达式过滤掉字符串中的数组的方括号,最后再利用 JSON.parse 把它转换成数组:

    let arr = [1, [2, [3, [4, 5]]], 6];
    
    function flatten(arr) {
      let str = JSON.stringify(arr);
      str = str.replace(/([|])/g, '');
      str = '[' + str + ']';
      return JSON.parse(str); 
    }
    
    console.log(flatten(arr)); //  [1, 2, 3, 4,5]

对象扁平化

/* 题目*/
var entryObj = {
    a: {
        b: {
            c: {
                    dd: 'abcdd'
            }
        },
        d: {
            xx: 'adxx'
        },
        e: 'ae'
    }
}

// 要求转换成如下对象
var outputObj = {
	'a.b.c.dd': 'abcdd',
	'a.d.xx': 'adxx',
	'a.e': 'ae'
}

function flat(obj, path = '', res = {}, isArray) {
  for (let [k, v] of Object.entries(obj)) {
    if (Array.isArray(v)) {
      let _k = isArray ? `${path}[${k}]` : `${path}${k}`;
      flat(v, _k, res, true);
    } else if (typeof v === 'object') {
      let _k = isArray ? `${path}[${k}].` : `${path}${k}.`;
      flat(v, _k, res, false);
    } else {
      let _k = isArray ? `${path}[${k}]` : `${path}${k}`;
      res[_k] = v;
    }
  }
  return res;
}

console.log(flat({ a: { aa: [{ aa1: 1 }] } }))

数字千分位分割

    formatNumber(number) {
      if (typeof number !== "number") {
          return null;
      }
      if (isNaN(number)) {
          return null;
      }

      let result = [];
      let tmp = number + "";
      let num = number;
      let suffix = "";
      if (tmp.indexOf(".") !== -1) {
          suffix = tmp.substring(tmp.indexOf(".") + 1);
          num = parseInt(tmp.substring(0, tmp.indexOf(".")));
      }
      while (num > 0) {
          result.unshift(num % 1000);
          num = Math.floor(num / 1000);
      }
      let ret = result.join(",");
      if (suffix !== "") {
          ret += "." + suffix;
      }
      return ret;
  }
  
  
  ## 写法二 (包含小数)
 let format = n => {
    let num = n.toString() // 转成字符串
    let decimals = ''
        // 判断是否有小数
    num.indexOf('.') > -1 ? decimals = num.split('.')[1] : decimals
    let len = num.length
    if (len <= 3) {
        return num
    } else {
        let temp = ''
        let remainder = len % 3
        decimals ? temp = '.' + decimals : temp
        if (remainder > 0) { // 不是3的整数倍
            return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',') + temp
        } else { // 是3的整数倍
            return num.slice(0, len).match(/\d{3}/g).join(',') + temp 
        }
    }
}
format(12323.33)  // '12,323.33'


### 不包含小数

let format = n => {
    let num = n.toString() 
    let len = num.length
    if (len <= 3) {
        return num
    } else {
        let remainder = len % 3
        if (remainder > 0) { // 不是3的整数倍
            return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',') 
        } else { // 是3的整数倍
            return num.slice(0, len).match(/\d{3}/g).join(',') 
        }
    }
}
format(1232323)  // '1,232,323'

实现斐波那契数列

    // 递归
    function fn (n){
        if(n==0) return 0
        if(n==1) return 1
        return fn(n-2)+fn(n-1)
    }
    
    // 优化
    function fibonacci2(n) {
        const arr = [1, 1, 2];
        const arrLen = arr.length;

        if (n <= arrLen) {
            return arr[n];
        }

        for (let i = arrLen; i < n; i++) {
            arr.push(arr[i - 1] + arr[ i - 2]);
        }

        return arr[arr.length - 1];
    }
    
    // 非递归
    function fn(n) {
        let pre1 = 1;
        let pre2 = 1;
        let current = 2;

        if (n <= 2) {
            return current;
        }

        for (let i = 2; i < n; i++) {
            pre1 = pre2;
            pre2 = current;
            current = pre1 + pre2;
        }

        return current;
    }

实现数组的 push、filter、map 方法

    // push
    Array.prototype.myPush = function (...args) {
      const length = this.length
      for (let i = 0; i < args.length; i++) {
        this[this.length + i] = args[i]
      }
      return this.length
    }

    // filter
    Array.prototype.myFilter = function (callback) {
      const newArr = []
      for (let i = 0; i < this.length; i++) {
        if (callback(this[i], i, this)) {
          newArr.push(this[i])
        }
      }
      return newArr
    }

    // map
    Array.prototype.myMap = function (callback) {
      const newArr = []
      for (let i = 0; i < this.length; i++) {
        newArr.push(callback(this[i], i, this))
      }
      return newArr
    }

    const list = [1, 2, 3, 4, 5]

    console.log(list.myPush(6)) // 6
    console.log(list.myFilter((i) => i > 3)) //[ 4, 5, 6 ]
    console.log(list.myMap((i) => i + 1)) // [ 2, 3, 4, 5, 6, 7 ]

实现 jsonp

    // 动态的加载js文件
    function addScript(src) {
      const script = document.createElement('script');
      script.src = src;
      script.type = "text/javascript";
      document.body.appendChild(script);
    }
    
    addScript("http://xxx.xxx.com/xxx.js?callback=handleRes");
    
    // 设置一个全局的callback函数来接收回调结果
    function handleRes(res) {
      console.log(res);
    }
    
    // 接口返回的数据格式
    handleRes({a: 1, b: 2});

获取URL中的参数

  • /?&/igm ,前面是 或者 & ,任意字符直到遇到 =,使用非贪婪模式,等号后面是非 & 符号的任意字符,然后去匹配就好了
  • 理论上可以用 matchAll,然后用迭代器去处理
function name(url) {
    const _url = url || window.location.href;
    const _urlParams = _url.match(/[?&](.+?=[^&]+)/igm);
    return _urlParams ? _urlParams.reduce((a,b) => {
        const value = b.slice(1).split('=');
        a[value[0]] = value[1];
        return a;
    }, {}) : {} 
    
}


function getParams() {
  const params = {};
  const search = location.search.substring(1); // 去掉问号
  const pairs = search.split('&'); // 按 & 分割参数

  for (let i = 0; i < pairs.length; i++) {
    const pair = pairs[i].split('=');
    const key = decodeURIComponent(pair[0]); // 解码参数名
    const value = decodeURIComponent(pair[1] || ''); // 解码参数值(如果没有值,则默认为 "")
    params[key] = value; // 存储为对象属性
  }

  return params;
}


##  使用 URLSearchParams

const getSearchParams = () => {
  const search = new URLSearchParams(window.location.search)
  const paramsObj = {}
  for (const [key, value] of search.entries()) {
    paramsObj[key] = value
  }
  return paramsObj
}

函数柯里化

function curry(fn, args) {
  let length = fn.length;
  args = args || [];

  return function() {
    let subArgs = args.slice(0);
    subArgs = subArgs.concat(arguments);
    if(subArgs.length >= length) {
      return fn.apply(this, subArgs);
    } else {
      return curry.call(this, fn, subArgs);
    }
  }
}

// 更好理解的方式
function curry(func, arity = func.length) {
  function generateCurried(preArgs) {
    return function curried(nextArgs) {
      const args = [...preArgs, ...nextArgs];
      if(args.length >= arity) {
        return func(...args);
      } else {
        return generateCurried(args);
      }
    }
  }
  return generateCurried([]);
}


const myCurried = (fn, ...args) => { 
    if (args.length < fn.length) { 
        // 未接受完参数 
        return (..._args) => myCurried(fn, ...args, ..._args) 
     } else { 
         // 接受完所有参数,直接执行 
         return fn(...args) 
     } 
     
} 
 function add(a, b, c) { return a + b + c } 
 
 const curriedAdd = myCurried(add)

## es6实现方式

function curry(fn, ...args) { 
    return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args); 
}

实现 sleep 函数

作用:暂停 JavaScript 的执行一段时间后再继续执行

    function sleep(time) {
      return new Promise((resolve) => {
        setTimeout(resolve, time)
      })
    }

    // 使用
    async function test() {
      console.log('start')
      await sleep(2000)
      console.log('end')
    }
    test()

实现 Object.create

创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

// 定义一个名为 create 的函数,接受一个参数 obj,该参数是一个对象。
function create(obj) {

    // 定义一个空的构造函数 Func。
    function Func() {}

    // 将传入的对象 obj 设置为 Func 的原型。
    // 这意味着通过 Func 构造函数创建的新对象将继承 obj 的所有属性和方法。
    Func.prototype = obj;

    // 修正原型链中的构造函数指向。
    // 因为当我们设置 `Func.prototype = obj` 之后,Func.prototype.constructor 默认指向的是 Object 构造函数,
    // 所以我们需要显式地将其设置回 Func,以保持正确的原型链。
    Func.prototype.constructor = Func;

    // 使用 new Func() 创建一个新的实例,并返回这个实例。
    // 这个新实例将会继承 obj 的所有属性和方法。
    return new Func();
}

实现 instanceof 方法

instanceof 运算符用于判断构造函数的 prototype\color{tomato}{prototype}prototype 属性是否出现在对象的原型链的任何位置。

// 定义一个名为 myInstanceof 的函数,它接受两个参数:一个对象 obj 和一个构造函数  ctor。
 function myInstanceof (obj,ctor) {
 
     // 获取 obj 的原型对象
     let proto = Object.getPrototypeOf(obj);
     
     // 获取 ctor 的原型对象
     let prototype = ctor.prototype;
     
     // 开始一个无限循环,用于遍历原型链
     while(true) {
         
         // 如果当前原型对象不存在 (即原型链已经到达顶端),说明 obj 不是 ctor的实例
         if(!proto) {
             return false;
         }
         
         // 如果当前原型对象等于 ctor 的原型对象, 说明 obj 是 ctor 的实例
         if (proto === prototype) {
             return true;
         }
         
         /*
           如果当前原型对象不是 ctor 的原型对象,则继续向上查找原型链
           将当前原型对象的原型赋值给 proto, 继续循环。
         */
         proto = Object.getPrototypeOf(prototype)
     }
 }

实现 new 关键字

在调用 new 之后会发生这几个步骤

    1. 创建一个空对象
    1. 设置原型: 将空白的对象的原型设置为函数的 prototype 对象
    1. 让函数的 This 指向这个对象, 执行构造函数的代码 (为空白对象添加属性)
    1. 判断函数的返回值

    • 4.1. 如果是引用类型,直接返回,比如构造函数主动返回了一个对象: function T() {retrun {x :1}}
    • 4.2. 如果不是引用类型, 返回空白对象。
    // 调用方法: objectFactory(构造函数,构造函数参数)
    
    // 定义一个名为 objectFactory 的函数,用于模拟 new 操作符的行为。
    function objectFactory () {
    
        // 初始化 newObject 为 null, 稍后将被替换为新创建的对象
        let newObject = null;
        
        // 从 arguments 中 移除 第一个参数 (即构造函数),并将其赋值给 constructor
        // 使用 Array.prototype.shift.call(arguments) 是为了模拟数组的 shift  方法,因为 arguments 不是真正的数组。

        // let constructor = Array.prototype.shift.call(arguments)
        let constructor = Array.prototype.slice(1).call(arguments);
 
        let result = null// 检查 constructor 是否是一个函数, 如果不是,则输出错误信息并返回
        if (typeof consteuctor !== "function") {
            console.error ('type error');
            return
        }
        
        // 使用 Object.create 方法创建一个新对象,该对象的原型为 constructor 的 prototype
        // 这使得新对象能够继承 constructor 的原型上的 属性和方法
        newObject = Object.create (constructor.prototype);
        
        // 使用 apply 方法调用 constructor, 将 newObject 作为 this 的上下文, 并将剩余的 arguments 作为参数传递给 constructor。
        // 这一步实际上是执行 构造函数的逻辑, 并将新创建的对象作为 This 上下文。
        result = constructor.apply (newObject, arguments);、
        
        // 检查 constructor 返回的结果 是否是一个非空对象或者函数
        // 如果是, 则返回这个结果,否则返回新创建的对象 newObject。
        let flag = result && (typeof result === "function" || typeof result === 'object' )
        
        return flag ? result : newObject;
        
    }
       
    
   
 ## 另一写法
   function myNew(Con, ...arg) {
     let obj = Object.create(Con.prototype)
     let result = Con.apply(obj, arg)
     return typeof result === 'object' ? result : obj
   }

拦截构造函数调用

function Person(name) {
 if (new.target !== undefined) { // 检查 `new.target` 是否定义
   this.name = name; // 如果定义,则设置 `this.name` 为传入的名字
 } else {
   throw new Error('必须使用 new 命令生成实例'); // 如果未定义,则抛出错误
 }
}

## 另一种写法

function Person(name) { 
    // 检查 `new.target` 是否指向 `Person` 构造函数 
   if (new.target === Person) { 
       
       // 如果指向 `Person`,则设置 `this.name` 为传入的名字 
       this.name = name; 
      } else { 
         
         // 如果不指向 `Person`,则抛出错误
          throw new Error('必须使用 new 命令生成实例'); 
      }
   }

// 因为使用了 `new` 关键字,并且 `new.target` 指向 `Person`
var person = new Person('张三'); // 正确, 

// 因为没有使用 `new` 关键字,`new.target` 为 `undefined`
var notAPerson = Person.call(person, '张三'); // 报错,

防抖函数

防抖是n秒内会重新计时

/**
* 创建一个防抖函数,确保原函数不会在指定时间内被连续调用多次。
*
* @param {Function} fn - 需要被防抖处理的函数。
* @param {number} wait - 在调用 fn 之后等待的时间(毫秒),在此期间重复调用将不会触发 fn。
* @returns {Function} 返回一个包装后的函数,该函数会在 wait 毫秒内抑制 fn 的多次调用。
*/
 
 
 function debounce(fn, wait) { 
     // 定义一个全局变量用于保存定时器的句柄
     let timer = null; 
     
     // 返回一个新函数, 这个函数会在 wait 毫秒内抑制 fn 的多次调用
     return function() { 
         // 使用 apply 和 arguments 保证 fn 能够获取正确的 this 上下文所有参数
         if(timer) { 
         
             // 如果有活跃的定时器, 清除它, 以确保不会重复触发 fn
             clearTimeout(timer); 
             timer = null; 
          } 
             
          // 设置新的定时器, 在 wait 毫秒后 执行 fn, 并传递调用时所有参数   
          timer = setTimeout(() => { 
              fn.apply(this, arguments); 
           }, wait);
              
    }
}

节流函数

n 秒内不重新计时

/**
 * 创建一个节流(throttle)函数,确保原函数不会在指定时间内被连续调用多次。
 *
 * @param {Function} fn - 需要被节流处理的函数。
 * @param {number} delay - 在调用 fn 之后等待的时间(毫秒),在此期间重复调用将不会触发 fn。
 * @returns {Function} 返回一个包装后的函数,该函数会在 delay 毫秒内抑制 fn 的多次调用。
 */
    function throttle(fn, delay) {
      // 定义一个全局变量用于保存定时器的句柄
      let timer = null; 

      // 返回一个新的函数,这个函数会在 delay 毫秒内抑制 fn 的多次调用
      return function() {
      
        // 如果已经有活跃的定时器,则直接返回,不执行 fn
        if (timer) return;

        // 设置新的定时器,在 delay 毫秒后执行 fn,并传递调用时的所有参数
        timer = setTimeout(() => {
          
          // 清除定时器,以便下次调用可以再次设置新的定时器
          timer = null;
      
          // 使用 apply 和 arguments 保证 fn 能够获取正确的 this 上下文和所有参数
          return fn.apply(this, arguments);
        }, delay);
      };
    }

实现 call 函数

执行步骤:

  • 判断 call 的调用者是否为函数,不是函数需要抛出错误,call 调用者就是上下文 this ,也就是需要被调用的函数
  • 判断需要被调用的函数的的上下文对象是否传入,不存在就设置为 window
  • 处理传入的参数,截取第一个参数后的所有参数,作为被调用函数
  • 将需要被调用的函数,绑在传入的上下文上,作为一个属性
  • 使用传入的上下文调用这个函数,并返回结果
  • 删除绑定的属性
  • 返回结果
/**
* 模拟 Function.prototype.call 方法,用于改变函数的 this 上下文并调用该函数。
*
* @param {Object} context - 要设置为函数 this 上下文的对象。
* @param {...any} args - 传递给函数的参数列表。
* @returns {*} 函数的返回值。
*/

  Function.prototype.myCall = function(context) {
    // 检查 this 是否是一个函数
    if (typeof this !== 'function') {
      throw new TypeError('need function'); // 如果不是函数,抛出错误
    }

    // 获取除第一个参数(context)之外的所有参数,并将其转换为数组
    let args = Array.prototype.slice.call(arguments, 1);

    // 如果没有提供 context 参数,则默认使用 window 对象(浏览器环境下)
    // 或者 globalThis(Node.js 环境下)
    context = context || (typeof window !== 'undefined' ? window : globalThis);

    // 在 context 对象上定义一个临时方法 fn,并将其指向 this 函数
    context.fn = this;

    // 调用 context.fn 并传递参数
    let result = context.fn(...args);

    // 删除 context 对象上的 fn 属性
    delete context.fn;

    // 返回函数的执行结果
    return result;
  };

实现apply函数

  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况
  • 判断传入上下文对象是否存在,如果不存在,则设置为 window
  • 将函数作为上下文对象的一个属性
  • 判断参数值是否传入
  • 使用上下文对象调用这个方法,并保存返回结果
  • 删除刚才新增的属性。
  • 返回结果
/**
* 模拟 Function.prototype.apply 方法,用于改变函数的 this 上下文并调用该函数。
*
* @param {Object} context - 要设置为函数 this 上下文的对象。
* @param {Array} [argsArray] - 传递给函数的参数数组。
* @returns {*} 函数的返回值。
*/

    Function.prototype.myApply = function (context) {
      // 判断调用对象是否为函数
      if (typeof this !== "function") {
        throw new TypeError("Error"); // 如果不是函数,抛出错误
      }

      let result = null;

      // 判断 context 是否存在,如果未传入则为 window
      context = context || window;

      // 将函数设为对象的方法
      context.fn = this;

      // 调用方法
      if (arguments[1]) {
        // 如果第二个参数存在,使用 ...arguments[1] 将参数数组展开
        result = context.fn(...arguments[1]);
      } else {
        // 如果没有提供参数数组,则直接调用 fn
        result = context.fn();
      }

      // 将属性删除
      delete context.fn;

      // 返回结果
      return result;
    };

实现bind

  • 先判断调用者是否为函数

  • 缓存当前需要 bind 的函数,就是上面的调用者,也是 bind 函数的上下文

  • 返回一个函数,利用闭包原理实现对 this 的保存

  • 函数内部用 apply 函数来处理函数调用

    • 需要判断函数作为构造函数的情况,这个时候的*this*就是当前调用这个闭包函数的 this
    • 作为普通函数,直接使用传入的上下文就好了
/**
* 模拟 Function.prototype.call 方法,用于改变函数的 this 上下文并调用该函数。
*
* @param {Object} context - 要设置为函数 this 上下文的对象。
* @param {...any} args - 传递给函数的参数列表。
* @returns {*} 函数的返回值。
*/


    Function.prototype.myCall = function(context) {
      // 检查 this 是否是一个函数
      if (typeof this !== 'function') {
        throw new TypeError('need function'); // 如果不是函数,抛出错误
      }

      // 获取除第一个参数(context)之外的所有参数,并将其转换为数组
      let args = Array.prototype.slice.call(arguments, 1);
      //  let args = [...arguments].slice(1);
     

      // 如果没有提供 context 参数,则默认使用 window 对象(浏览器环境下)
      // 或者 globalThis(Node.js 环境下)
      context = context || (typeof window !== 'undefined' ? window : globalThis);

      // 定义一个临时方法 fn,并将其指向 this 函数
      fn = this;
      return function Fn() {
       // 根据调用方式,传入不同绑定值
        return fn.apply(
          this instanceof Fn ? this : context,
          args.concat(...arguments)
        );
  
    };

浅拷贝

浅拷贝是创建一个新对象,这个新对象有着原始对象属性值的一份精确拷贝。如果属性值是基本数据类型,如字符串、数字、布尔值等,拷贝的就是值本身。但如果属性值是引用类型(如对象、数组),拷贝的只是其引用,而不是引用指向的实际对象。

// ES6 的 Object.assign 方法用于合并多个对象的属性到一个目标对象中。

// 例如:
    const target = {};
    const source1 = { a: 1 };
    const source2 = { b: 2 };
    const merged = Object.assign(target, source1, source2);
    console.log(merged);     // 输出: { a: 1, b: 2 }


// 扩展运算符(spread operator)用于将对象的属性展开到新的对象中。
// 例如:
    const obj1 = { a: 1 };
    const obj2 = { b: 2 };
    const merged = { ...obj1, ...obj2 };
    console.log(merged);    // 输出: { a: 1, b: 2 }


// 数组的浅拷贝可以通过多种方式实现,例如使用 slice 或 concat 方法。
// 例如:
    const original = [1, 2, 3];
    const copy = original.slice(); // 浅拷贝
    const copy2 = original.concat(); // 浅拷贝


// 手动实现浅拷贝
function shallowCopy(object) {
  // 检查 object 是否为 null 或不是一个对象
  if (!object || typeof object !== 'object') return;

  // 创建一个新对象,如果 object 是数组,则创建一个新数组
  let newObj = Array.isArray(object) ? [] : {};

  // 遍历 object 的所有属性
  for (let key in object) {
  
    // 检查属性是否是 object 的自身属性(而非继承来的属性)
    if (object.hasOwnProperty(key)) {
    
      // 将属性复制到新对象中
      newObj[key] = object[key];
    }
  }

  // 返回新对象
  return newObj;
}

深拷贝

深拷贝(Deep Copy)是指创建一个对象的完全独立的副本,不仅复制对象本身,还包括对象内部的所有嵌套对象。换句话说,深拷贝会递归地复制对象及其所有子对象,确保新创建的对象与原始对象之间没有任何引用关系。

深拷贝的特点
  1. 完全独立:深拷贝创建的对象与原始对象之间没有任何引用关系,即使原始对象发生改变,也不会影响到深拷贝的对象。
  2. 递归复制:对于对象内部的嵌套对象,深拷贝也会递归地进行复制。
  3. 适用于所有数据类型:深拷贝适用于基本数据类型(如数字、字符串、布尔值等)和复杂数据类型(如对象、数组等)。
深拷贝的应用场景
  • 当需要在不改变原始数据的情况下对数据进行修改时。
  • 当需要将数据结构传递给函数,而不希望函数修改原始数据时。
  • 当需要在前端框架中更新状态时,以确保视图更新。
可能的问题
  • json 方法出现函数或 symbol 类型的值的时候,会失效
  • 处理循环引用问题
  • 处理可迭代类型的数据
  • 处理包装类型
  • 处理普通类型
## JSON.parseJSON.stringify
/**
 * 使用 JSON.stringify 将对象转换为 JSON 字符串,再使用 JSON.parse 将 JSON 字符串转换回 JavaScript 对象。
 *这种方法简单易用,但对于某些类型的数据(如函数、循环引用、日期对象等)无法正确处理。
 */


function deepCopy(obj) {
 return JSON.parse(JSON.stringify(obj));
}


## 递归函数
/**
* 使用递归函数手动实现深拷贝,这种方法可以处理各种类型的数据,包括函数和循环引用
* 适用域所有数据类型, 但是实现起来相对复杂

* 实现深拷贝功能,递归地复制对象及其所有子对象。
* @param {any} obj - 需要进行深拷贝的目标对象。
* @returns {any} 深拷贝后的对象。
*/


  function deepCopy(obj) {
    // 如果目标是 null,则直接返回 null
    if (obj === null) return null;

    // 如果目标不是对象,则直接返回目标
    if (typeof obj !== 'object') return obj;

    // 根据目标对象的类型创建一个新对象或数组
    let copy = Array.isArray(obj) ? [] : {};

    // 遍历目标对象的所有属性
    for (let key in obj) {
      // 检查属性是否是对象自身的属性
      if (obj.prototype.hasOwnProperty(key)) {
        // 对每个属性递归调用 deepCopy 方法
        copy[key] = deepCopy(obj[key]);
      }
    }

    // 返回深拷贝后的对象
    return copy;
  }

## 第三方库
/**
* 使用 lodash 等第三方库提供的深拷贝方法,如 `_.cloneDeep`。

* 这些库通常提供了高性能的实现,并处理了各种边缘情况。
*/


const _ = require('lodash'); 
// 定义一个原始对象
const originalObj = {
  name: 'Alice', // 名字
  age: 30,       // 年龄
  address: {     // 地址
    city: 'New York', // 城市
    zip: 10001    // 邮编
  },
  hobbies: ['reading', 'swimming'] // 爱好
};

// 使用  _.cloneDeep 函数对 originalObj 进行深拷贝
const deepCopiedObj = _.cloneDeep(originalObj);



##  简单版本参考vue版本
/**
 * 实现深拷贝功能,递归地复制对象及其所有子对象,处理循环引用。
 *
 * @param {any} target - 需要进行深拷贝的目标对象。
 * @param {WeakMap} [cache=new WeakMap()] - 用于存储已拷贝的对象引用,防止循环引用。
 * @returns {any} 深拷贝后的对象。
 */
 
 
    const deepClone = (target, cache = new WeakMap()) => {
      // 如果目标是 null 或者不是对象,则直接返回目标
      if (target === null || typeof target !== 'object') {
        return target;
      }

      // 检查目标对象是否已经被拷贝过
      if (cache.get(target)) {
        return target;
      }

      // 根据目标对象的类型创建一个新对象或数组
      const copy = Array.isArray(target) ? [] : {};

      // 将目标对象和新创建的拷贝对象存入缓存中
      cache.set(target, copy);

      // 遍历目标对象的所有属性
      Object.keys(target).forEach(key => {
        // 对每个属性递归调用 deepClone 方法
        copy[key] = deepClone(target[key], cache);
      });

      // 返回深拷贝后的对象
      return copy;
    };

实现 Object.assign

就是实现一个浅拷贝

/**
 * 模拟 Object.assign 方法,用于合并多个对象的属性到一个目标对象中。
 *
 * @param {Object} target - 目标对象,将被合并的对象属性添加到此对象中。
 * @param {...Object} source - 一个或多个源对象,其属性将被合并到目标对象中。
 * @returns {Object} 合并后的目标对象。
 */
 
 

Object.myAssign = function (target, ...source) {
  // 检查目标对象是否为 null
  if (target === null) {
    throw new TypeError('can not be null'); // 如果是 null,则抛出错误
  }

  // 使用 Object() 方法确保目标对象是一个对象
  let ret = Object(target);

  // 遍历所有源对象
  source.forEach(obj => {
  
    // 检查源对象是否为 null
    if (obj !== null) {
    
      // 遍历源对象的所有可枚举属性
      for (let key in obj) {
      
        // 检查属性是否是对象自身的属性
        if (obj.hasOwnProperty(key)) {
        
          // 将源对象的属性复制到目标对象中
          ret[key] = obj[key];
        }
      }
    }
  });

  // 返回合并后的目标对象
  return ret;
};

手写 ajax

    function get() {
  //创建ajax实例
  let req = new XMLHTTPRequest()
  if (req) {
    //执行open 确定要访问的链接 以及同步异步
    req.open('GET', 'http://test.com/?keywords=手机', true)
    //监听请求状态
    req.onreadystatechange = function () {
      if (req.readystate === 4) {
        if (req.statue === 200) {
          console.log('success')
        } else {
          console.log('error')
        }
      }
    }
    //发送请求
    req.send()
  }
}

实现一个深度比较 isEqual

function isEqual(obj1, obj2) {
   //不是对象,直接返回比较结果
   if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
      return obj1 === obj2
   }
   //都是对象,且地址相同,返回true
    if (obj2 === obj1) return true
  
    //是对象或数组
    let keys1 = Object.keys(obj1)
    let keys2 = Object.keys(obj2)
  
    //比较keys的个数,若不同,肯定不相等
    if (keys1.length !== keys2.length) return false
      for (let k of keys1) {

        //递归比较键值对
        let res = isEqual(obj1[k], obj2[k])
        if (!res) return false
      }
      return true
    }

    const obj1 = {
      a: 100,
      b: {
        x: 100,
        y: 200,
      },
    }
    const obj2 = {
      a: 200,
      b: {
        x: 100,
        y: 200,
      },
}
console.log(isEqual(obj1, obj2)) //false

设计一个图片懒加载 SDK

    const throttle = (fn, delay) => {
      let timer = null
      return function (...args) {
        if (!timer) {
          setTimeout(() => {
            fn.apply(this, args)
            timer = null
          }, delay)
        }
      }
    }

    const lazyLoadImages = () => {
      const images = document.querySelectorAll('img[data-src]')
      images.forEach((img) => {
        const rect = img.getBoundingClientRect()
        if (rect.top < window.innerHeight) {
          img.src = img.dataSet.src
          img.removeAttribute('data-src')
        }
      })
    }

    const throttledLazyLoad = throttle(lazyLoadImages, 100)

    window.addEventListener('scroll', throttledLazyLoad)

手写 Promise

    const PENDING = "pending";
    const RESOLVED = "resolved";
    const REJECTED = "rejected";

    function MyPromise(fn) {
      // 保存初始化状态
      var self = this;

      // 初始化状态
      this.state = PENDING;

      // 用于保存 resolve 或者 rejected 传入的值
      this.value = null;

      // 用于保存 resolve 的回调函数
      this.resolvedCallbacks = [];

      // 用于保存 reject 的回调函数
      this.rejectedCallbacks = [];

      // 状态转变为 resolved 方法
      function resolve(value) {
        // 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变
        if (value instanceof MyPromise) {
          return value.then(resolve, reject);
        }

        // 保证代码的执行顺序为本轮事件循环的末尾
        setTimeout(() => {
          // 只有状态为 pending 时才能转变,
          if (self.state === PENDING) {
            // 修改状态
            self.state = RESOLVED;

            // 设置传入的值
            self.value = value;

            // 执行回调函数
            self.resolvedCallbacks.forEach(callback => {
              callback(value);
            });
          }
        }, 0);
      }

      // 状态转变为 rejected 方法
      function reject(value) {
        // 保证代码的执行顺序为本轮事件循环的末尾
        setTimeout(() => {
          // 只有状态为 pending 时才能转变
          if (self.state === PENDING) {
            // 修改状态
            self.state = REJECTED;

            // 设置传入的值
            self.value = value;

            // 执行回调函数
            self.rejectedCallbacks.forEach(callback => {
              callback(value);
            });
          }
        }, 0);
      }

      // 将两个方法传入函数执行
      try {
        fn(resolve, reject);
      } catch (e) {
        // 遇到错误时,捕获错误,执行 reject 函数
        reject(e);
      }
    }

    MyPromise.prototype.then = function(onResolved, onRejected) {
      // 首先判断两个参数是否为函数类型,因为这两个参数是可选参数
      onResolved =
        typeof onResolved === "function"
          ? onResolved
          : function(value) {
              return value;
            };

      onRejected =
        typeof onRejected === "function"
          ? onRejected
          : function(error) {
              throw error;
            };

      // 如果是等待状态,则将函数加入对应列表中
      if (this.state === PENDING) {
        this.resolvedCallbacks.push(onResolved);
        this.rejectedCallbacks.push(onRejected);
      }

      // 如果状态已经凝固,则直接执行对应状态的函数

      if (this.state === RESOLVED) {
        onResolved(this.value);
      }

      if (this.state === REJECTED) {
        onRejected(this.value);
      }
    };

手写 Promise.then

then 方法返回一个新的 promise 实例,为了在 promise 状态发生变化时(resolve / reject 被调用时)再执行 then 里的函数,我们使用一个 callbacks 数组先把传给then的函数暂存起来,等状态改变时再调用。

那么,怎么保证后一个 **then** 里的方法在前一个 **then** (可能是异步)结束之后再执行呢? 我们可以将传给 then 的函数和新 promiseresolve 一起 push 到前一个 promisecallbacks 数组中,达到承前启后的效果:

  • 承前:当前一个 promise 完成后,调用其 resolve 变更状态,在这个 resolve 里会依次调用 callbacks 里的回调,这样就执行了 then 里的方法了
  • 启后:上一步中,当 then 里的方法执行完成后,返回一个结果,如果这个结果是个简单的值,就直接调用新 promiseresolve,让其状态变更,这又会依次调用新 promisecallbacks 数组里的方法,循环往复。。如果返回的结果是个 promise,则需要等它完成之后再触发新 promiseresolve,所以可以在其结果的 then 里调用新 promiseresolve
/**
 * then 方法用于处理 Promise 的成功和失败情况,并返回一个新的 Promise 对象。
 *
 * @param {Function} onFulfilled - 当 Promise 成功时调用的回调函数。
 * @param {Function} [onRejected] - 当 Promise 失败时调用的回调函数。
 * @returns {MyPromise} 一个新的 Promise 对象。
*/

    then(onFulfilled, onRejected) {

      // 保存当前 Promise 的 this 上下文
      const self = this;

      // 返回一个新的 Promise 对象
      return new MyPromise((resolve, reject) => {

        // 封装前一个 Promise 成功时执行的函数
        let fulfilled = () => {
          try {

            // 调用 onFulfilled 回调函数,并传入当前 Promise 的值
            const result = onFulfilled(self.value);

            // 检查 result 是否是一个 MyPromise 实例
            if (result instanceof MyPromise) {

              // 如果 result 是一个 MyPromise 实例,则继续使用 then 方法处理
              result.then(resolve, reject);
            } else {

              // 如果 result 不是一个 MyPromise 实例,则直接调用 resolve 方法
              resolve(result);
            }
          } catch (err) {

            // 如果 onFulfilled 回调函数抛出异常,则调用 reject 方法
            reject(err);
          }
        };

        // 封装前一个 Promise 失败时执行的函数
        let rejected = () => {
          try {
            // 调用 onRejected 回调函数,并传入当前 Promise 的原因
            const result = onRejected(self.reason);

            // 检查 result 是否是一个 MyPromise 实例
            if (result instanceof MyPromise) {
              
              // 如果 result 是一个 MyPromise 实例,则继续使用 then 方法处理
              result.then(resolve, reject);
            } else {
              
              // 如果 result 不是一个 MyPromise 实例,则直接调用 reject 方法
              reject(result);
            }
          } catch (err) {
            
            // 如果 onRejected 回调函数抛出异常,则调用 reject 方法
            reject(err);
          }
        };

        // 根据当前 Promise 的状态决定如何处理
        switch (self.status) {
          case PENDING: // 如果当前状态为 pending
          
            // 将 fulfilled 和 rejected 函数添加到回调队列中
            self.onFulfilledCallbacks.push(fulfilled);
            self.onRejectedCallbacks.push(rejected);
            break;
          case FULFILLED: // 如果当前状态为 fulfilled
            
            // 立即执行 fulfilled 函数
            fulfilled();
            break;
          case REJECTED: // 如果当前状态为 rejected
            
            // 立即执行 rejected 函数
            rejected();
            break;
        }
    });
 }

注意:

  • 连续多个 then 里的回调方法是同步注册的,但注册到了不同的 callbacks 数组中,因为每次 then 都返回新的 promise 实例(参考上面的例子和图)
  • 注册完成后开始执行构造函数中的异步事件,异步完成之后依次调用 callbacks 数组中提前注册的回调

手写 Promise.all

一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了

/**
 * 实现 Promise.all 功能,等待所有 Promise 对象都完成(resolve 或 reject)。
 *
 * @param {Array<Promise>} promises - 一个 Promise 对象的数组。
 * @returns {Promise} 一个新的 Promise 对象,当所有输入的 Promise 对象都完成时,返回一个包含所有结果的数组。
*/

    function promiseAll(promises) {
    
          // 检查输入是否为数组
          if (!Array.isArray(promises)) {
            throw new TypeError('argument must be an array');
          }

          // 初始化计数器和结果数组
          let resolvedCounter = 0;
          let promiseNum = promises.length;
          let resolvedResult = [];

          // 返回一个新的 Promise 对象
          return new Promise(function (resolve, reject) {
          
            // 遍历所有的 Promise 对象
            for (let i = 0; i < promiseNum; i++) {
            
              // 使用 Promise.resolve 确保传入的是 Promise 对象
              Promise.resolve(promises[i]).then(
                function (value) {
                
                  // 当一个 Promise 对象完成时,增加计数器
                  resolvedCounter++;

                  // 存储该 Promise 对象的结果到结果数组中
                  resolvedResult[i] = value;

                  // 如果所有 Promise 对象都已完成,调用 resolve 方法
                  if (resolvedCounter === promiseNum) {
                    resolve(resolvedResult);
                  }
                },
                function (error) {
                  // 如果任何一个 Promise 对象失败,立即调用 reject 方法
                  reject(error);
                }
              );
            }
          });
      }

    // 测试代码
    let p1 = new Promise(function (resolve, reject) {
      setTimeout(function () {
        resolve(1);
      }, 1000);
    });

    let p2 = new Promise(function (resolve, reject) {
      setTimeout(function () {
       resolve(2);
      }, 2000);
    });

    let p3 = new Promise(function (resolve, reject) {
      setTimeout(function () {
        resolve(3);
      }, 3000);
    });

    // 使用 promiseAll 测试
    promiseAll([p3, p1, p2]).then(res => {
      console.log(res); // [3, 1, 2]
    });

手写 Promise.race

该方法的参数是 Promise 实例数组, 然后其 then 注册的回调方法是数组中的某一个 Promise 的状态变为 fulfilled 的时候就执行. 因为 Promise 的状态只能改变一次, 那么我们只需要把 Promise.race 中产生的 Promise 对象的 resolve 方法, 注入到数组中的每一个 Promise 实例中的回调函数中即可.

  /**
   * 实现 Promise.race 功能,返回一个 Promise,该 Promise 会在给定的 Promises 中任意一个最先完成(resolve 或 reject)时完成。
   *
   * @param {Array<Promise>} args - 一个包含 Promise 对象的数组。
   * @returns {Promise} 一个新的 Promise 对象,当给定的 Promises 中任意一个最先完成时,这个新的 Promise 也会完成。
 */
 
 
  Promise.race = function (args) {
  
    // 返回一个新的 Promise 对象
    return new Promise((resolve, reject) => {
    
      // 遍历所有的 Promise 对象
      for (let i = 0, len = args.length; i < len; i++) {
      
        // 使用 .then 方法监听每个 Promise 的完成状态
        args[i].then(
          function (value) {
          
            // 当一个 Promise 完成时,立即调用 resolve 方法
            resolve(value);
          },
          function (reason) {
          
            // 当一个 Promise 失败时,立即调用 reject 方法
            reject(reason);
          }
        );
      }
    });
  };
  

简单实现async/await中的async函数

async/await语法糖就是使用 Generator函数+自动执行器 来运作的,注意只要要实现async 函数就是实现一个generate函数+执行器的语法糖

/**
 * 定义一个异步函数 getNum,用于模拟异步操作,并在1秒后返回一个数值加1的结果。
 *
 * @param {number} num - 初始数值。
 * @returns {Promise} 一个 Promise 对象,将在1秒后解析为 num + 1。
*/


function getNum(num) {
  return new Promise((resolve, reject) => {
    
    // 使用 setTimeout 模拟异步操作
    setTimeout(() => {
      
      // 在1秒后解析 Promise,返回 num + 1
      resolve(num + 1);
    }, 1000);
  });
}

/**
 * 自动执行器函数,用于递归地执行 Generator 函数中的异步操作。
 *
 * @param {Function} func - 一个 Generator 函数。
*/
 
function asyncFun(func) {
  // 创建 Generator 函数的实例
  var gen = func();

  /**
   * 处理 Generator 函数中的下一步操作。
   *
   * @param {*} data - 从上一步异步操作返回的数据。
  */
  
  
  function next(data) {
    
    // 调用 Generator 函数的 next 方法,并传递上一步异步操作返回的数据
    var result = gen.next(data);

    // 如果 Generator 函数已经执行完毕,则返回最终结果
    if (result.done) return result.value;

    // 如果 Generator 函数还没有执行完毕,则继续处理下一步异步操作
    result.value.then(function (data) {
      
      // 递归调用 next 方法处理下一步操作
      next(data);
    });
  }

  // 开始执行 Generator 函数的第一步操作
  next();
}

// 定义一个 Generator 函数,用于按顺序执行异步操作
var func = function* () {
  
  // 第一步异步操作:获取初始数值加1的结果
  var f1 = yield getNum(1);

  // 第二步异步操作:获取第一步操作的结果加1的结果
  var f2 = yield getNum(f1);

  // 打印第二步操作的结果
  console.log(f2);
};

// 使用 asyncFun 自动执行器函数执行 Generator 函数
asyncFun(func);