Skip to content

手写题

js
/**
 *
 * @param task  返回一个promise的异步任务
 * @param count 需要重试的次数
 * @param time  每次重试间隔多久
 * @returns 返回一个新promise
 */
const retry = (task, count = 5, time = 3 * 1000) => {
  return new Promise((resolve, reject) => {
    let errorCount = 0;
    const run = () => {
      task()
        .then((res) => resolve(res))
        .catch((err) => {
          errorCount++;
          if (errorCount < count) {
            setTimeout(run, time);
          } else {
            reject(err);
          }
        });
    };
    run();
  });
};

防抖节流

防抖

在给定的时间间隔内只允许你提供的回调函数执行一次,以此降低它的执行频率

实现

js
/**
 * 防抖函数
 * @param {*} fn 回调函数
 * @param {*} delay 延迟的时间
 * @param {*} immediate 立即执行
 */
const debounce = (fn, delay, immediate) => {
  let timer = null; // 使用闭包缓存结果
  return function () {
    let callNow = immediate && !timer; // 立即执行
    // 如果已经在防抖了
    if (timer) {
      clearTimeout(timer);
    }
    // 有立即执行, 即马上执行,否则等待执行
    if (callNow) {
      fn.call(this, ...arguments);
    } else {
      timer = setTimeout(() => {
        fn.call(this, ...arguments);
        // 执行完毕后
        timer = null;
      }, delay);
    }
  };
};

防抖场景

  • search 搜索联想,用户在不断输入值时,用防抖来节约请求资源。
  • window 触发 resize 的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次

节流

单位时间内只能触发一次

实现

js
/**
 * 节流函数
 * @param {*} fn 回调函数
 * @param {*} delay 延迟时间
 * @returns
 */
function throttle(fn, delay) {
  var flag = false; // 开始的时候, false标志未开始, true标志开始
  return function () {
    if (flag) return;
    flag = true; // 标志开始
    setTimeout(() => {
      fn.apply(this, arguments); // 当运行完这次事件
      flag = false; // 标志回未开始
    }, delay);
  };
}

节流场景

  • 鼠标不断点击触发,mousedown(单位时间内只触发一次)
  • 监听滚动事件,比如是否滑到底部自动加载更多,用 throttle 来判断

深拷贝

实现

js
const isObject = (val) => typeof val === "object" && val !== null;
const toTypeString = (val) => Object.prototype.toString.call(val);

const deepCopy = (obj, hash = new WeakMap()) => {
  // 非对象类型不拷贝
  if (!isObject(obj)) return obj;
  // 对象为正则或者日期 对象, 也不进行拷贝
  if (toTypeString(obj) === "[object RegExp]") return obj;
  if (toTypeString(obj) === "[object Date]") return obj;

  // 如果有循环引的现象, 不在继续拷贝,返回之前的
  if (hash.has(obj)) return obj;
  // 开始拷贝
  let newObj = Array.isArray(obj) ? [] : {};
  hash.set(obj, newObj); // 存储
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 不拷贝继承的属性
      newObj[key] = isObject(obj[key]) ? deepCopy(obj[key], hash) : obj[key];
    }
  }
  return newObj;
};

注意点

  • 循环引用的处理
  • 不拷贝继承的属性的处理

模板字符串解析

题目描述

js
let template = "我是{{name}},年龄{{age}},性别{{sex}}";
let data = {
  name: "小明",
  age: 18,
};

// 要求写一个函数使编译结果为
render(template, data); // 我是小明,年龄18,性别undefined

实现

js
function render(template, data) {
  // \为转义
  let str = template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
    /*
     *match =>{{name}} 字符串中匹配到的
     *key =>name  代表括号匹配的字符串
     */
    return data[key];
  });
  return str;
}

LRU 算法

描述

  • LRU 算法也叫做最近最少使用算法

  • LRU 算法的原理: 每次获取值的时候都会重新设置值, 所以最近最少使用的必然是最前面的那个

实现

js
/**
 * capacity 存储的最大容量
 */
class LRUCache {
  constructor(capacity) {
    this.secretKey = new Map();
    this.capacity = capacity;
  }
  // 获取值
  get(key) {
    if (this.secretKey.has(key)) {
      let templateValue = this.secretKey.get(key);
      this.secretKey.delete(key); // 删除当前的
      this.secretKey.set(key, templateValue); // 重新设置, 并排在最后
      return templateValue;
    } else {
      return -1;
    }
  }
  // 设置值
  set(key, value) {
    // 如果已经存在, 修改
    if (this.secretKey.has(key)) {
      this.secretKey.delete(key); // 删除当前的
      this.secretKey.set(key, value); // 重新设置, 并排在最后
    } else if (this.secretKey.size < this.capacity) {
      // 如果cache足够, 继续放
      this.secretKey.set(key, value); // 重新设置, 并排在最后
    } else {
      // 先添加进去
      this.secretKey.set(key, value);
      // 然后再删除第一个(即最近最少使用的)
      this.secretKey.delete(this.secretKey.keys().next().value);
    }
  }
}

// 测试
let cache = new LRUCache(2); // 最大容量2
// 存放俩个
cache.set("test", "test组件");
cache.set("test2", "test2组件");

// 获取
cache.get("test"); // test组件

// 再次存放, 因为最大容量为2, 并且在上面已经获取了test ,因此最少使用的是test2, 所以剔除
cache.set("test3", "test3组件");
// cache =>  Map(2) {'test' => 'test组件', 'test3' => 'test3组件'}

数字千分位分割

为什么数字使用千分位分割, 有以下几个原因

  • 防止被浏览器表示为手机号码,影响本意
  • 能让语音阅读整体阅读, 比如 123456789,读 一亿两千三百四十五万六千七百八十九;而不是 一二三四五六七八九
  • 更加符合国际通用规范

实现

js
function formatNum(num) {
  // 1. 转化为字符串更好操作
  let strNum = num.toString();
  // 2. 看是否有小数点以及获取到小数
  let decimals = "";
  if (strNum.includes(".")) {
    // 包含小数点
    let [num, decimal] = strNum.split("."); // 数组解构 => 解构出来例如123.45 => [123, 45] 45为小数
    strNum = num;
    decimals = decimal;
  }
  // 3. 看num长度
  let len = strNum.length;
  if (len < 3) {
    // 整数部分不够三位, 不分割
    return decimals ? strNum + `.${decimals}` : decimals;
  } else {
    // 分割
    // 可以进行
    let temp = "";
    let remainder = len % 3; // 获取到是否3的倍数
    decimals ? (temp = "." + decimals) : temp; // 获取到分数
    if (remainder > 0) {
      let beforeNum = strNum.slice(0, remainder); // 截取前面不足3位的
      /**
       * match返回的是一个数组
       * 如果使用g标志,则将返回与完整正则表达式匹配的所有结果,但不会返回捕获组。
       * 如果未使用g标志,则仅返回第一个完整匹配及其相关的捕获组(Array)。
       * 例如: '123455'.match(/\d{3}/g) => ['123', '455']
       * '123455'.match(/\d{3}/) 没有使用g标志 ['123', index: 0, input: '123455', groups: undefined]
       */
      let middleNum = strNum.slice(remainder, len).match(/\d{3}/g).join(","); // 截取中间
      let afterNum = temp; // 小数点
      return beforeNum + middleNum + afterNum;
    } else {
      let middleNum = strNum.slice(0, len).match(/\d{3}/g).join(",");
      let afterNum = temp;
      return middleNum + afterNum;
    }
  }
}

函数柯里化

描述

  • 柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术

实现

js
/**
 *
 * @param {*} fn 函数
 * @param  {...any} args 多个参数
 */
function currying(fn, ...args) {
  // 1.获取到fn参数个数
  const len = fn.length;
  // 2.收集参数
  let allArgs = [...args];
  const dfs = (...args2) => {
    // 继续收集参数
    allArgs = [...allArgs, ...args2]; // 3.收集返回函数的参数(使用闭包, 一直保存allArgs)
    if (len === allArgs.length) {
      // 4.1收集完毕
      return fn(...allArgs);
    } else {
      // 4.2继续收集
      return dfs;
    }
  };
  return dfs;
}

let fn = (a, b, c) => {
  return a + b + c;
};
// let add = currying(fn, 3, 4)
// let ret = add(5)
// 连续使用 -- 收集到一定参数才会去调用函数, 否则还是返回函数
let ret = currying(fn, 3, 4)(5);
console.log(ret); //12

new 操作符

new 操作符分析

  • new 出来的对象 this 指向实例本身
  • 实例.__proto === 构造函数.prototype
  • 会自行函数
  • 根据返回值类型返回

实现

js
/**
 *
 * @param {*} fn 构造函数
 * @param  {...any} args  剩余参数
 */
const myNew = (fn, ...args) => {
  // 1.创建一个对象, 并且以fn.prototype作为原型
  let obj = Object.create(fn);
  // 2.调用函数, 并将this执行obj
  let ret = fn.call(obj, ...args);
  // 3.判断返回值
  return ret instanceof Object ? ret : obj;
};

// 测试
function Person(name) {
  this.name = name;
}
const p = myNew(Person, "xm");

列表与树形结构互转

列表转成树形结构

js
let data = [
  {
    id: 1,
    text: "节点1",
    parentId: 0, //这里用0表示为顶级节点
  },
  {
    id: 2,
    text: "节点1_1",
    parentId: 1, //通过这个字段来确定子父级
  },
  {
    id: 3,
    text: "节点2_1",
    parentId: 2, //通过这个字段来确定子父级
  },
];

// 转成
// [
//   {
//       id: 1,
//       text: '节点1',
//       parentId: 0,
//       children: [
//           {
//               id:2,
//               text: '节点1_1',
//               parentId:1
//           }
//       ]
//   }
// ]

转化函数

js
function listToTree(data) {
  let temp = {};
  let treeDate = [];
  // 减少循环操作
  data.forEach((item) => {
    temp[item.id] = item;
  });
  // 遍历加入
  for (let key in temp) {
    // 如果不是最大父级
    if (temp[key].parentId !== 0) {
      // 找到父亲, 判断有没有children属性
      if (!temp[temp[key].parentId].children) {
        temp[temp[key].parentId].children = [];
      }
      // 有就加入进来
      temp[temp[key].parentId].children.push(temp[key]);
    } else {
      treeDate.push(temp[key]);
    }
  }
  return treeDate;
}

listToTree(data);

树形结构转成列表

js
let data = [
  {
    id: 1,
    text: "节点1",
    parentId: 0,
    children: [
      {
        id: 2,
        text: "节点1_1",
        parentId: 1,
      },
    ],
  },
];
// 转成
// [
//   {
//       id: 1,
//       text: '节点1',
//       parentId: 0 //这里用0表示为顶级节点
//   },
//   {
//       id: 2,
//       text: '节点1_1',
//       parentId: 1 //通过这个字段来确定子父级
//   }
//   ...
// ]

转化函数

js
function treeToList(data) {
  let res = [];
  const dfs = (tree) => {
    tree.forEach((item) => {
      if (item.children) {
        // 递归循环(dfs => 深度优先搜索)
        dfs(item.children);
        delete item.children;
      }
      res.push(item);
    });
  };
  dfs(data);
  return res;
}
treeToList(data);

继承

继承本次要实现 5 种继承

1.原型链继承

js
function Student() {
  this.name = ["vvv", "vvv2"];
}
Student.prototype.say = function () {
  return this.name;
};
function Vvv() {}
Vvv.prototype = new Student(); // 核心
// Vvv.prototype.constructor  === Student 为什么呢
// 因为let s = new Student() 即Vvv.prototype === s (即Vvv的原型是Student的实例)
// 那么Vvv.prototype.constructor = s.constructor
// s.constructor是Student
Vvv.prototype.constructor = Vvv;

let vvv3 = new Vvv();
vvv3.name.push("vvv3");
let vvv4 = new Vvv();
console.log(vvv4.name); // ['vvv', 'vvv2', 'vvv3']

原型链继承缺点

  • 问题 1:原型中包含的引用类型属性将被所有实例共享; 可以看到 p1 push 的时候 p2 也改变了
  • 问题 2:子类在实例化的时候不能给父类构造函数传参;

2.借用构造函数继承

js
function Student(name) {
  this.name = name;
  this.getName = function () {
    return this.name;
  };
}
function Vvv(name) {
  Student.call(this, name); // 核心
}
let vvv = new Vvv("vvv");
vvv.getName(); // vvv
let vvv2 = new Vvv("vvv2");
vvv2.getName(); // vvv2

借用构造函数继承优缺点

  • 优点: 借用构造函数实现继承解决了原型链继承的 2 个问题:引用类型共享问题以及传参问题
  • 缺点: 由于方法必须定义在构造函数中,所以会导致每次创建子类实例都会创建一遍方法

3.组合继承

js
function Student(name) {
  this.name = name;
}
Student.prototype.getName = function () {
  return this.name;
};
function Vvv(name) {
  // 第二次
  Student.call(this, name);
}
// 第一次
Vvv.prototype = new Student();
Vvv.prototype.constructor = Vvv;

let vvv = new Vvv("vvv");
vvv.getName(); // vvv
let vvv2 = new Vvv("vvv2");
vvv2.getName(); // vvv2

组合继承优缺点

  • 优点: 解决原型链继承和借用构造函数继承分别的缺点

  • 缺点: 调用了俩次父类构造函数

4.寄生组合继承

js
function Student(name) {
  this.name = name;
}
Student.prototype.getName = function () {
  return this.name;
};
function Vvv(name) {
  Student.call(this, name);
}
Vvv.prototype = Object.create(Student.prototype);
// 解释一下 Vvv.prototype = Object.create(Student.prototype)
// 用Object.create以父原型为原型创建对象赋值给子原型
// let obj = Object.create(Student.prototype)
// Vvv.prototype = obj
// obj 是什么呢? obj是以Student.prototype为原型的对象
Vvv.prototype.constructor = Vvv;

let vvv = new Vvv("vvv");
vvv.getName(); // vvv
let vvv2 = new Vvv("vvv2");
vvv2.getName(); // vvv2

寄生组合继承优点

  • 组合继承已经相对完善了,但还是存在问题,它的问题就是调用了 2 次父类构造函数, 第一次是在 new Student(),第二次是在 Student.call() 这里

  • 优点: 解决组合继承 2 次调用父类构造函数

5.class 继承

js
class Student {
  constructor(name) {
    this.name = name;
  }
}
// 核心: extends
class Vvv extends Student {
  constructor(name, age) {
    super(name);
    this.age = 18;
  }
}

class 继承

  • es6 class 实现继承已经相对很简单了

  • 其原理就是使用了寄生组合继承

sleep 函数

描述

sleep 函数作用是让线程休眠,等到指定时间在重新唤起

实现

js
function sleep(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve();
    }, time * 1000);
  });
}

对象的层次获取

实现

js
/*
*obj  传入的对象
*keyName 也就是a.c这样的形式
*/
function lookup(obj, keyName) {
    //首先判断keyName是否含有.
    if (keyName.indexOf('.') != -1 && keyName != '.') {
        //有就拆分
        let keys = keyName.split(".");
        let temp = obj;
        for (let i=0; i < keys.length; i++) {
            temp = temp[keys[i]];
        }
        //循环结束后返回temp
        return temp;
    }
    //没有. 就直接使用
    return obj[keyName];
}

// 测试
var obj = {
    a:{
        c:{
          d:123
        }
    }
}
lookup({a:{c:{d:123}}},'a.c.d')  //123

大数相加

在 js 中, Number.MAX_SAFE_INTEGER 常量表示在 JavaScript 中最大的安全整数(maxinum safe integer), 一旦超出这个数,就会出现精度丢失问题, 例如:

js
let c = 9007199254740991;
let d = 123456789999999999;
d > Number.MAX_SAFE_INTEGER; // true
c + d; // 132463989254741000

我们想要的结果是132463989254740990, 但是由于 d 超出了 JavaScript 中最大的安全整数, 所以导致运算错误, 因此我们需要实现一个add方法使得大数可以相加

实现

js
function add(a, b) {
  // 1.0 补齐位数
  let maxLength = Math.max(a.length, b.length);
  /**
   * mdn: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
   * padStart() 方法用另一个字符串填充当前字符串 (如果需要的话,会重复多次),
   * 以便产生的字符串达到给定的长度。从当前字符串的左侧开始填充。
   */
  a = a.padStart(maxLength, 0); // '009007199254740991'     从前开始补齐 用0来补齐
  b = b.padStart(maxLength, 0); // '123456789999999999'
  let t = 0;
  let f = 0; // 进位
  let sum = ""; // 拼接
  for (let i = maxLength - 1; i >= 0; i--) {
    t = parseInt(a[i]) + parseInt(b[i]) + f;
    f = Math.floor(t / 10); // 是否有进位
    sum = (t % 10) + sum;
  }
  // 是否还存在进位
  if (f !== 0) {
    sum = "" + f + sum;
  }
  return sum;
}

有效括号

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。

实现

js
const isValid = (s) => {
  // 字符为单数 或者字符以)]} 开头的都会无法闭合
  if (s.length % 2 === 1 || s[0] === ")" || s[0] === "]" || s[0] === "}") {
    return false;
  }
  // 存储括号闭合的类型
  const pairs = new Map();
  pairs.set(")", "(");
  pairs.set("]", "[");
  pairs.set("}", "{");

  // 使用栈的方式进行判断 先进后出
  const stack = [];
  // 遍历所有字符串
  for (let ch of s) {
    // ch中是否存在)]} 的一种
    if (pairs.has(ch)) {
      // 判断最后一组是否是有效括号
      if (!stack.length || stack[stack.length - 1] !== pairs.get(ch)) {
        return false;
      }
      stack.pop();
    } else {
      stack.push(ch);
    }
  }
  return !stack.length;
};

说明

html
举例子 例如: s = ()[]{} 开始进栈( 然后下一次)进栈, 然后进入pairs.has(ch) -> true
-> stack.pop() 当前栈为空 下一次进栈[ 同理 (记住 )]} 这三个是不会进入到栈中的 )

实现 forOf

MDN 解释: for...of语句可迭代对象(包括 ArrayMapSetStringTypedArray类数组arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子(迭代器),并为每个不同属性的值执行语句

可迭代对象

首先需要知道什么是可迭代对象

js
typeof obj[Symbol.iterator] == "function";

如果上述式子成立的话, 那么我们称obj是可迭代对象

tips:只有可迭代对象才能使用for..of

迭代器原理

在上面介绍了可迭代对象, 在可迭代对象中肯定会存在迭代器, 实现迭代器

js
const createIterator = (obj) => {
  let i = 0;
  return {
    next: function () {
      // done 表示是否已经迭代完成
      const done = i >= obj.length;
      const val = !done ? obj[i++] : undefined;
      return {
        done: done,
        value: val,
      };
    },
  };
};

实现 forOf

认识了可迭代对象迭代器原理就可以实现 forOf 了

js
/**
 *
 * @param {*} obj 可迭代对象
 * @param {*} callback 回调函数
 */
const forOf = (obj, callback) => {
  // iterable迭代器
  // result结果
  let iterable, result;
  // 如果传入对象Symbol.iterator属性类型不是function,抛出错误 (说明不是可迭代的对象)
  if (typeof obj[Symbol.iterator] !== "function") {
    throw new TypeError(result + "is not iterable");
  }
  // iterable = obj[Symbol.iterator]()
  // 也可以使用上述的createIterator
  iterable = createIterator(obj);
  result = iterable.next();

  while (!result.done) {
    // 回调处理val值
    callback(result.value);
    result = iterable.next();
  }
};

// 测试
forOf([1, 2, 3], (item) => {
  console.log(item); // 1,2,3
});

setTimeout 模拟实现 setInterval

为什么用 setTimeout 替代 setInterval? setInterval有什么问题?

点击了解

实现

js
/**
 * setTimeout 模拟实现 setInterval
 * @param {*} callback 回调函数
 * @param {*} delay 延迟时间
 */
const mySetInterval = (callback, delay) => {
  let timer = null;
  const interval = () => {
    callback(); // 执行回调
    timer = setTimeout(interval, delay); // 递归调用
  };
  interval(); // 初始执行
  return {
    // 清除定时器
    cancel: () => {
      clearTimeout(timer);
    },
  };
};

// 测试
let test = mySetInterval(() => {
  console.log(123);
}, 1000);

test.cancel(); // 清除定时器

实现对象扁平化

要求

实现 flattenObj 函数,为一个 javascript 对象(Object 或者 Array),返回值为扁平化后的结果

例如:

js
const input = {
  a: 1,
  b: [1, 2, { c: true }, [3]],
  d: { e: 2, f: 3 },
  g: null,
};
js
const flattenRes = flattenObj(input)
// 测试结果如下
{
  "a": 1,
  "b[0]": 1,
  "b[1]": 2,
  "b[2].c": true,
  "b[3][0]": 3,
  "d.e": 2,
  "d.f": 3,
   // "g": null,  值为null或者undefined,丢弃
}

实现

js
const flattenObj = obj => {
  // 接受结果
  let res = {}
  // 递归
  const dfs = (target, oldKey) => {
    // 遍历target
    for (let key in target) {
      let newKey // 用于作为老的key
      if (oldKey) {
        // 递归有老key 则组合起来
        if (Array.isArray(target)) {
          // 数组变为 老key[0]
          newKey = `${oldKey}[${key}]`
        } else {
          // 对象: 老key.a
          newKey = `${oldKey}.${key}`
        }
      } else {
        // 没有oldKey, 即初始化状况
        if (Array.isArray(target)) {
          // 数组变为 [0] [1]
          newKey = `[${key}]`
        } else {
          // 对象变为 'a' 'b'
          newKey = key
        }
      }
      if (Object.prototype.toString.call(target[key]) === '[object Object]' || Array.isArray(target[key])) {
        // 递归数组和对象 传进组织好的老key
        dfs(target[key], newKey)
      } else if (target[key] !== null && target[key] !== undefined) {
        // 递归出口 常规数据 直接赋值
        res[newKey] = target[key]
      }
    }
  }
  dfs(obj, '')
  return res
}

// 测试
const input = {
  a: 1,
  b: [1, 2, { c: true }, [3]],
  d: { e: 2, f: 3 },
  g: null
}

const flattenRes = flattenObj(input)
// 测试结果如下
{
  "a": 1,
  "b[0]": 1,
  "b[1]": 2,
  "b[2].c": true,
  "b[3][0]": 3,
  "d.e": 2,
  "d.f": 3,
   // "g": null,  值为null或者undefined,丢弃
}

实现 Object.assign

Object.assign

MDN: Object.assign() 方法将所有可枚举Object.propertyIsEnumerable() 返回 true)和自有Object.hasOwnProperty() 返回 true)属性从一个或多个源对象复制到目标对象,返回修改后的对象。直达 MDN 文档查看

从 MDN 文档中可以知道实现Object.assign的几个要求

  • 如果目标对象与源对象具有相同的 key,则目标对象中的属性将被源对象中的属性覆盖,后面的源对象的属性将类似地覆盖前面的源对象的属性。
  • Object.assign 方法只会拷贝源对象 可枚举的自身的 属性到目标对象。该方法使用源对象的 [[Get]] 和目标对象的 [[Set]],它会调用 getterssetters。故它分配属性,而不仅仅是复制或定义新的属性。如果合并源包含 getters,这可能使其不适合将新属性合并到原型中。(就是不希望合并原型上的属性)
  • 不可枚举的属性不会合并
  • 基本类型会被包装成对象

实现

js
/**
 * Object._assign
 * @param {*} target  目标对象,接收源对象属性的对象,也是修改后的返回值
 * @param  {...any} sources  源对象,包含将被合并的属性
 */
Object._assign = (target, ...sources) => {
  // 普通类型包装成对象 比如字符串等...
  target = Object(target);
  for (let i = 0; i < sources.length; i++) {
    // 过滤掉要合并的对象为null和undefined的情况
    // (null == null  -> true )(null == undefined -> true)
    if (sources[i] == null) continue; // 结束本次循环
    // 遍历要合并对象的属性
    for (let key in sources[i]) {
      // in运算符会查找原型对象上的可枚举属性,
      // 所以需要通过Object.prototype.hasOwnProperty方法过滤掉对象原型对象上的属性
      if (sources[i].hasOwnProperty(key)) {
        target[key] = sources[i][key];
      }
    }
  }
  return target;
};

测试

js
const proto = { p: "proto" };
const obj1 = { a: "aa" };
const obj2 = { b: "bb" };
// 以proto作为新对象的原型
const obj3 = Object.create(proto, {
  c: {
    value: "cc",
    enumerable: true,
  },
});
console.log(obj3); // {c: 'cc'}
// 输出obj3的构造函数的原型对象
console.log(obj3.__proto__); // {p: 'proto'}

// 说明不会合并原型链(__proto__) 上面的属性
const t1 = Object._assign({}, obj1, obj2);
console.log(t1); // {a: "aa", b: "bb"}
// 过滤合并对象为null、undefined的情况
const t2 = Object._assign({}, obj1, null, obj2, undefined);
console.log(t2); // {a: "aa", b: "bb"}
// 合并属性
const t3 = Object._assign({}, obj1, obj2, obj3);
console.log(t3); // {a: "aa", b: "bb", c: "cc"}

测试 2

js
const v1 = "abc";
const v2 = true;
const v3 = 10;
const v4 = Symbol("foo");

const obj = Object.assign({}, v1, null, v2, undefined, v3, v4);
// 基本类型会被包装,null和undefined会被忽略.
// 只有字符串包装器可以有自己的可枚举属性, 所以true 和 10不会被放进来
console.log(obj); // { "0": "a", "1": "b", "2": "c" }

实现 compose 组合函数

在函数式编程当中有一个很重要的概念就是函数组合, 实际上就是把处理数据的函数像管道一样连接起来, 然后让数据穿过管道得到最终的结果。例如:

js
function add(a, b) {
  return a + b;
}
function len(str) {
  return str.length;
}
function preFix(str) {
  return `###${str}`;
}
console.log(preFix(len(add(1, "1")))); // ###2

我们想输出的是一个多层函数嵌套的运行结果,即把前一个函数的运行结果赋值给后一个函数。但是如果需要嵌套多层函数,那这种类似于f(g(h(x)))的写法可读性太差,我们考虑能不能写成(f, g, h)(x)这种简单直观的形式,于是 compose()函数就正好帮助我们实现。

实现

js
const composeRight = (...fns) => {
  return function (...args) {
    const len = fns.length; // 获取到函数个数
    // 处理fns < 2 的情况
    if (len === 0) return args;
    if (len === 1) return fns[0](...args);
    // 处理fns > 2的情况
    const lastFn = fns && fns.pop(); // 获取到最后一个函数
    // 获取到最后一个函数并执行, 并获取函数返回结果作为reduce的初始值
    let prev = lastFn(...args);
    // reduceRight 从右往左进行reduce
    return fns.reduceRight((prev, cur) => {
      return cur(prev);
    }, prev);
  };
};

测试

js
function add(a, b) {
  return a + b;
}
function len(str) {
  return str.length;
}
function preFix(str) {
  return `###${str}`;
}

let retFn = composeRight(preFix, len, add); // 返回一个函数
console.log(retFn);
// ...args => 1, '1', 然后开始执行
// lastFn(...args) = > add(1, '1') => 11 => 开始进行reduce
/**
 * return fns.reduceRight((prev, cur) => {
 *     return cur(prev)  // 开始的cur是len => len(11) => 2, 最后是preFix
 *  }, 11)
 */
console.log(retFn(1, "1")); // ###2

实现 instanceof

MDN 描述: instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

语法:

js
object instanceof constructor
object  -> 某个实例对象
constructor  -> 某个构造函数

理解: instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。

实现

js
/**
 * instanceof
 * @param {*} left 某个实例对象
 * @param {*} right 某个构造函数
 */
const myInstanceof = (left, right) => {
  // 处理边界情况
  if ((typeof left !== "object" || left === null) && typeof left !== "function")
    return false;
  if (!right.prototype)
    throw new TypeError("Right-hand side of 'myInstanceof' is not an object");

  let leftProto = Object.getPrototypeOf(left);
  const rightPrototype = right.prototype;
  while (leftProto !== null) {
    if (leftProto === rightPrototype) {
      return true;
    }
    leftProto = Object.getPrototypeOf(leftProto);
  }
  return false;
};

测试 1

左边不为对象

js
console.log(myInstanceof(1, Object)); // false
1 instanceof Object; // false

测试 2

右边不为构造函数

js
const obj = {};
console.log(myInstanceof(obj, 1)); // Uncaught TypeError: Right-hand side of 'myInstanceof' is not an object
obj instanceof 1; // 报错 Uncaught TypeError: Right-hand side of 'instanceof' is not an object

测试 3

左边为对象, 右边为构造函数

js
const fn = () => {};
console.log(myInstanceof(fn, Function)); // true
fn instanceof Function; // true

不匹配的现象

js
const fn = () => {};
console.log(myInstanceof(fn, Array)); // false
fn instanceof Array; // false

实现发布订阅模式

实现发布订阅模式 具有on off once emit方法

实现

js
class EventEmitter {
  constructor() {
    // 所有订阅事件(事件中心)
    this.events = {};
  }
  /**
   * 订阅事件
   * @param {*} type 订阅的类型
   * @param {*} callback 事件
   */
  on(type, callback) {
    if (!this.events[type]) {
      this.events[type] = [callback];
    } else {
      this.events[type].push(callback);
    }
  }
  // 取消订阅事件
  off(type, callback) {
    this.events[type] &&
      (this.events[type] = this.events[type].filter((fn) => fn !== callback));
  }
  // 只触发一次事件, 之后不再触发
  once(type, callback) {
    const fn = () => {
      callback();
      this.off(type, fn);
    };
    // 在订阅事件的同时, 触发一次后就会取消订阅的事件
    this.on(type, fn);
  }
  // 发布(触发)事件
  emit(type, ...rest) {
    this.events[type] && this.events[type].forEach((fn) => fn(...rest));
  }
}

测试

测试emit

js
const events = new EventEmitter();

const handle = (...rest) => {
  console.log(rest);
};

events.on("click", handle);

events.emit("click", 1, 2, 3, 4); // [1, 2, 3, 4]

测试off

js
const events = new EventEmitter();

const handle = (...rest) => {
  console.log(rest);
};

events.on("click", handle);

events.emit("click", 1, 2, 3, 4); // [1, 2, 3, 4]

events.off("click", handle);

events.emit("click", 1, 2); // 取消了不打印

测试once

js
events.once("dbClick", () => {
  console.log(123456);
});
events.emit("dbClick"); // 123456, 只有第一次触发
events.emit("dbClick");
events.emit("dbClick");
events.emit("dbClick");
events.emit("dbClick");

实现 call/apply/bind

call apply bind 这三种方法都是可以改变 this 的指向的, bind 的实现会较难

call

call 的作用

  • 1.会改变 this 指向
  • 2.会自动调用函数

实现

js
/**
 *
 * @param {*} context context (改变的this指向对象)
 * @param  {...any} args 参数
 */
Function.prototype.myCall = function (context, ...args) {
  // 1.context为空或者null时 ,this指向window
  if (!context || context == null) {
    context = window;
  }
  // 2. 创建独一无二的一个 fn
  let fn = Symbol();
  // 3. 将this指向context中的fn, this指的是调用者
  context[fn] = this;
  // 4. 调用函数fn, 并获的返回值
  const ret = context[fn](...args);
  // 5.删除 fn
  delete context[fn];
  // 返回函数返回值
  return ret;
};

测试

js
// 测试
let obj = {
  a(params) {
    console.log(this, params);
  },
};
let obj2 = {
  b() {},
};
let obj3 = {
  c() {},
};
// obj.a(123) // this->obj
obj.a.call(obj2, 1234); // this -> obj2
obj.a.myCall(obj2, 1234); // this -> obj2

总结

实现 call 共 6 步

  • 1.context 为空或者 null 时 ,this 指向 window
  • 2.创建独一无二的一个 fn
  • 3.将 this 指向 context 中的 fn, this指的是调用者
  • 4.调用函数 fn, 并获的返回值
  • 5.删除 fn
  • 6.返回函数返回值

apply

apply 的作用

  • 1.会改变 this 指向
  • 2.会自动调用函数
  • 3.参数为数组(跟call的区别)
实现
js
/**
 * 实现方法和call差不多
 * apply
 * @param {*} context context (改变的this指向对象)
 * @param  {...any} args  参数
 */

Function.prototype.myApply = function (context, args) {
  // 1.context为空或者null时 ,this指向window
  if (!context || context == null) {
    context = window;
  }
  // 2.创建独一无二的一个 fn
  let fn = Symbol();
  // 3.将this指向context中的fn, this指的是调用者
  context[fn] = this;
  // 4.调用函数fn, 并获的返回值
  const ret = context[fn](...args);
  // 5.删除 fn
  delete context[fn];
  // 6.返回函数返回值
  return ret;
};

测试方式和总结跟call的差不多

bind(重点)

bind 的作用

  • 1.新函数的 this 被指定为 bind 的第一个参数
  • 2.返回一个函数
  • 3.其余参数将作为新函数的参数
  • 4.在 bind 的时候,可以进行传参
  • 5.当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效

实现

js
Function.prototype.myBind = function (context, ...args1) {
  // 1.context为空或者null时 ,this指向window
  if (!context || context == null) {
    context = window;
  }
  // 2. 创建独一无二的一个 fn
  let fn = Symbol();
  context[fn] = this;
  let self = this; // 需要保存当前指向
  // 3.会返回一个函数, 这个函数也可以传入参数
  const result = function (...args2) {
    // 第一种情况 :若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象
    // 此时由于new操作符作用  this指向result实例对象  而result又继承自传入的self 根据原型链知识可得出以下结论
    // this.__proto__ === result.prototype   // this instanceof result =>true
    // this.__proto__.__proto__ === result.prototype.__proto__ === self.prototype; // this instanceof self =>true

    if (this instanceof self) {
      // 此时this指向指向result的实例  这时候不需要改变this指向
      this[fn] = self;
      this[fn](...[...args1, ...args2]); // 这里使用es6的方法让bind支持参数合并
    } else {
      // 如果只是作为普通函数调用  那就很简单了 直接改变this指向为传入的context
      context[fn](...[...args1, ...args2]);
    }
  };
  // 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法
  // 实现继承的方式: 使用Object.create
  result.prototype = Object.create(this.prototype);
  return result;
};

测试

js
function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.add = function () {
  return this.x + this.y;
};

let F = Point.myBind(null, 1);
// let F = Point.bind(null, 1)
let f = new F(6);
f.add(); // 7

总结

bind的实现相对较难理解

  • bind返回的是一个函数
  • 返回的函数可以作为构造函数(此时将不在改变指向, 使用的是new出来的实例作为this指向)
  • 并且在可以二次传入参数, 第一个是在使用bind的时候, 第二次是在返回的函数身上(重点)

实现 create

MDN 描述: Object.create() 方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)。

为什么使用 Object.create

语法

js
 // 返回一个新对象,带着指定的原型对象和属性。
Object.create(proto,[propertiesObject])
  • proto:新创建对象的原型对象,必须为null或者原始包装对象,否则会抛出异常

  • propertiesObject:可选参数,需要是一个对象,该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符

为何要使用Object.create创建对象,而不直接使用对象字面量的形式或者使用Object创建对象呢?

js
const obj = {
  name: "nordon",
};

const newObj = Object.create(obj);
const nweObj2 = Object(obj);

通过输出可以看到通过字面量和使用Object创建的对象是一致的,且其引用地址是一致的:obj === newObj2true

通过Object.create常见的对象会在objnewObj之间增加一层,这个时候引用地址是解耦的:obj === newObj为 false,这样的好处可以保证新创建的对象和原有对象解耦,当我们操作newObj时并不会影响原有数据

Object.create 的应用场景

利用Object.create实现继承

js
function Person(name) {
  this.name = name;
  this.permission = ["user", "salary", "vacation"];
}

Person.prototype.say = function () {
  console.log(`${this.name} 说话了`);
};

function Staff(name, age) {
  Person.call(this, name);
  this.age = age;
}

Staff.prototype = Object.create(Person.prototype, {
  constructor: {
    // 若是不将Staff constructor指回到Staff, 此时的Staff实例zs.constructor则指向Person
    value: Staff,
  },
});

let s = new Staff("xm", 18);
s.say(); // xm 说话了

实现

js
/**
 * Object.create()
 * 用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)
 * @param {*} proto 原型(对象)
 * @returns 返回对象
 */
const create = (proto) => {
  // 创建一个构造函数
  function F() {}
  F.prototype = proto;
  return new F(); // 返回实例
};

Object 的实现相对来说比较简单, 但是也需要理解原型与原型链上得知识

实现并行限制的 Promise 调度器

描述

题目描述: 实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有两个

js
addTask(1000,"1");
addTask(500,"2");
addTask(300,"3");
addTask(400,"4");
输出顺序是:2 3 1 4

整个的完整执行流程:

一开始1、2两个任务开始执行
500ms时,2任务执行完毕,输出2,任务3开始执行
800ms时,3任务执行完毕,输出3,任务4开始执行
1000ms时,1任务执行完毕,输出1,此时只剩下4任务在执行
1200ms时,4任务执行完毕,输出4

实现

js
class Scheduler {
  constructor(limit) {
    this.queue = []; // 队列
    this.maxCount = limit; // 保证最大的运行数量
    this.runCounts = 0; // 正在运行的个数
  }
  // 添加任务
  add(time, order) {
    const promiseCreator = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log(order);
          resolve();
        }, time);
      });
    };
    // 加入到队列中
    this.queue.push(promiseCreator);
  }
  // 开始任务
  start() {
    for (let i = 0; i < this.maxCount; i++) {
      this.request();
    }
  }
  // 执行任务
  request() {
    if (!this.queue.length || this.runCounts >= this.maxCount) return;
    // 从队列中拿出任务开始执行
    this.runCounts++;
    const task = this.queue.shift();
    // 执行(调用)任务后返回promise
    task().then(() => {
      // 执行完毕后runCounts--
      this.runCounts--;
      // 并执行下一个任务
      this.request();
    });
  }
}

测试

js
const scheduler = new Scheduler(2);
const addTask = (time, order) => {
  scheduler.add(time, order);
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
scheduler.start(); // 2 3 1 4

// 一开始1、2两个任务开始执行
// 500ms时,2任务执行完毕,输出2,任务3开始执行
// 800ms时,3任务执行完毕,输出3,任务4开始执行
// 1000ms时,1任务执行完毕,输出1,此时只剩下4任务在执行
// 1200ms时,4任务执行完毕,输出4

总结

实现并行限制的 Promise 调度器需要掌握promise的知识,通过定时器来控制输出任务的顺序

实现 LazyMan

题目描述

js
const vvv = new LazyMan('vvv')
vvv.sleep(2).eat('dinner')
// 输出
Hi! my name is vvv
等待2s
eat dinner
js
vvv.eat('dinner').sleep(2)
// 输出
Hi! my name is vvv
eat dinner
等待2s
js
vvv.eat("dinner").sleepFirst(2);
// 输出

sleep方法会延迟后面的输出, eat方法直接输出, sleepFirst方法延迟会在开始

实现

js
class LazyMan {
  constructor(name) {
    this.tasks = []; // 任务列表
    const task = () => {
      console.log(`Hi! my name is ${name}`);
      this.run(); // 执行一个任务
    };
    this.tasks.push(task);
    // 开始执行任务
    setTimeout(() => {
      this.run();
    }, 0);
  }
  // 直接输出
  eat(food) {
    const task = () => {
      console.log(`eat ${food}`);
      this.run();
    };
    this.tasks.push(task);
    return this;
  }
  // 执行任务, 取出第一个, 调用
  run() {
    const task = this.tasks.shift(); // 取第一个任务执行
    task && task();
  }
  // 延迟输出
  sleep(time) {
    this.sleepWrapper(time);
    return this;
  }
  // 将延迟会在开始
  sleepFirst(time) {
    this.sleepWrapper(time, true);
    return this;
  }
  sleepWrapper(time, first) {
    const task = () => {
      setTimeout(() => {
        console.log(`等待${time}s`);
        this.run();
      }, time * 1000);
    };
    if (first) {
      this.tasks.unshift(task);
    } else {
      this.tasks.push(task); // 在栈尾加入
    }
  }
}

总结

lazyMan 主要考察的点是对链式调用以及栈结构

实现 maxRequest

描述

实现 maxRequest,成功后 resolve 结果,失败后重试,尝试超过一定次数才返回真正的 reject

实现

js
/**
 * maxRequest
 * @param {*} fn 需要执行的函数
 * @param {*} max 最大尝试次数
 */
const maxRequest = (fn, max) => {
  return new Promise((resolve, reject) => {
    // idx最大请求次数
    const dfs = (idx) => {
      Promise.resolve(
        fn()
          .then((val) => {
            // 成功的话直接resolve
            resolve(val);
          })
          .catch((err) => {
            // 失败就直接给idx-1 再递归
            if (idx - 1 > 0) {
              help(idx - 1);
            } else {
              // 如果最后一次了就reject返回
              reject(err);
            }
          })
      );
    };
    dfs(max);
  });
};


⭐️⭐️⭐️ 好啦!!!本文章到这里就结束啦。⭐️⭐️⭐️

✿✿ ヽ(°▽°)ノ ✿

撒花 🌸🌸🌸🌸🌸🌸

上次更新于: