Skip to content

一文搞懂手写底层源码

1.手写 Promise.all()

Promise.all() 方法接收一个 promise 的 iterable 类型(注:Array,Map,Set 都属于 ES6 的 iterable 类型)的输入。 —— 说明所传参数都具有 Iterable,也就是可遍历。

并且只返回一个 Promise 实例。—— 说明最终返回是一个 Promise 对象。

那个输入的所有 promise 的 resolve 回调的结果是一个数组。—— 说明最终返回的结果是一个数组,且数组内数据要与传参数据对应。

这个 Promise 的 resolve 回调执行是在所有输入的 promise 的 resolve 回调都结束,或者输入的 iterable 里没有 promise 了的时候。—— 说明最终返回时,要包含所有的结果的返回

它的 reject 回调执行是,只要任何一个输入的 promise 的 reject 回调执行或者输入不合法的 promise 就会立即抛出错误,并且 reject 的是第一个抛出的错误信息。—— 说明只要一个报错,立马调用 reject 返回错误信息

js
const PromiseAll = (iterator) => {
  const promises = Array.from(iterator); // 对传入的数据进行浅拷贝,确保有遍历器(也可以不拷贝,都行)
  const len = promises.length; // 长度
  let index = 0; // 每次执行成功+1,当等于长度时,说明所有数据都返回,则可以resolve
  let tempList = []; // 用来存放结果的数组
  return new Promise((resolve, reject) => {
    for (let i of promises) {
      promises[i]
        .then((res) => {
          tempList[i] = res;
          if (++index === len) {
            //根据空数组(执行完毕的数组)length等于当前遍历索引来判断,即如果全都执行完毕了
            resolve(data); //全部执行完毕了,再去resolve,返回的依然是Promise,一个成功对象!
          }
        })
        .catch((err) => {
          reject(err);
        });
    }
  });
};

const promise1 = Promise.resolve("promise1");
const promise2 = new Promise(function (resolve, reject) {
  setTimeout(resolve, 2000, "promise2"); //resolve可以不加括号,'promise2'为其命名
});
const promise3 = new Promise(function (resolve, reject) {
  setTimeout(resolve, 1000, "promise3");
});

PromiseAll([promise1, promise2, promise3]).then(function (values) {
  console.log(values);
});

3.手写深拷贝

简单地进行深拷贝:

js
const s1 = Symbol();
const s2 = Symbol();

const obj = {
  name: "why",
  friend: {
    name: "kobe",
  },
  foo: function () {
    console.log("foo function");
  },
  [s1]: "abc",
  s2: s2,
};

obj.inner = obj; //对象有个inner属性指向他自己,如果用了json转换之后就不可以了,会报错

const info = JSON.parse(JSON.stringify(obj)); //主要是这里实现了深拷贝,但是会有问题!因为json在转换的过程中不识别函数和symbol!
console.log(info === obj);
obj.friend.name = "james";
console.log(info);

手写实现:

js
function isObject(value) {
  //const valueType = typeof value
  //return (value !== null) && (valueType === "object" || valueType === "function")
  return value instanceof Object;
}

function deepClone(originValue) {
  // 判断传入的originValue是否是一个对象类型
  if (!isObject(originValue)) {
    return originValue; //如果不是对象,那说明是一个属性值,直接返回就可以了!后面的每一层属性值都是这里返回的!
  }

  const newObject = {}; //每次递归返回的一个新的拷贝值的对象
  for (const key in originValue) {
    //把每个属性都遍历进行重新赋值!使用了for in
    //每一个都等于递归!
    newObject[key] = deepClone(originValue[key]); //将属性值一一对应进行深拷贝,一层一层最终返回成一个完全一样的对象,拷贝旧对象的这个键值,给新对象!
  }
  return newObject; //第一层,以及后面的每一层对象,都是这里返回的!
}

// 测试代码
const obj = {
  name: "why",
  age: 18,
  friend: {
    name: "james",
    address: {
      city: "广州",
    },
  },
};

const newObj = deepClone(obj);
console.log(newObj === obj);

obj.friend.name = "kobe";
obj.friend.address.city = "成都";
console.log(newObj);

4.手写图片懒加载

html
<div class="container">
  <img src="loading.gif" data-src="pic.png" />
  <img src="loading.gif" data-src="pic.png" />
  <img src="loading.gif" data-src="pic.png" />
  <img src="loading.gif" data-src="pic.png" />
  <img src="loading.gif" data-src="pic.png" />
  <img src="loading.gif" data-src="pic.png" />
</div>
<script>
  var imgs = document.querySelectorAll("img");
  function lozyLoad() {
    //document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动过的距离
    //window.innerHeight 是浏览器可视区的高度
    var scrollTop =
      document.body.scrollTop || document.documentElement.scrollTop;
    var winHeight = window.innerHeight;
    for (var i = 0; i < imgs.length; i++) {
      //imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离)
      if (imgs[i].offsetTop < scrollTop + winHeight) {
        //当图片进入视野时
        imgs[i].src = imgs[i].getAttribute("data-src");
      }
    }
  }
  //在滚动的过程中调用函数
  window.onscroll = lozyLoad();
</script>

5.手写防抖和节流

防抖

js
//接一个订单,和等待时间
function debounce(fn, delay) {
  let timerId = null;
  return function () {
    const context = this;
    //如果接到新订单,但是已经在等待中了,就重新开始计时,再等3分钟
    if (timerId) {
      window.clearTimeout(timerId);
    }
    //3分钟没有接到订单就直接配送
    timerId = setTimeout(() => {
      fn.apply(context, arguments);
      timerId = null; //这个也是必要的
    }, delay);
  };
}

节流

js
function throttle(fn, delay) {
  // 设置一个触发开关
  let canUse = true;
  return function () {
    //都是return一个function,这个是一个异步的包含setTimeout的
    //如果开关为true,就立即触发技能,否则就不能触发
    if (canUse) {
      fn.apply(this, arguments); //触发技能,也可以直接fn()应该
      //触发技能后,直接关闭开关
      canUse = false;
      //在3秒后打开开关,这样新的请求才可能再请求成功!
      setTimeout(() => (canUse = true), delay);
    }
  };
}

7.js 手写轮播图(实现原理)

html
<style type="text/css">
  #counter {
    width: 600px;
    height: 300px;
    position: relative;
    overflow: hidden;
  }
  #list {
    width: 3600px;
    height: 300px;
    position: absolute;
    z-index: 1;
  }
  #list img {
    width: 600px;
    height: 300px;
    float: left;
  }
  .arrow {
    position: absolute;
    top: 110px;
    text-decoration: none;
    z-index: 2;
    display: none;
    width: 40px;
    height: 40px;
    font-size: 36px;
    font-weight: bold;
    line-height: 39px;
    text-align: center;
    color: #fff;
    background-color: rgba(0, 0, 0, 1);
    cursor: pointer;
  }
  .arrow:hover {
    background-color: rgba(0, 0, 0, 0.8);
  }
  #counter:hover .arrow {
    display: block;
  }
  #pre {
    left: 20px;
  }
  #next {
    right: 20px;
  }
  #list {
    transition: left 1s;
  }
  .list {
    transition: left 0.1s;
  }

  #buttons {
    position: absolute;
    height: 10px;
    width: 120px;
    left: 250px;
    bottom: 20px;
    z-index: 2;
  }
  #buttons span {
    border: 1px solid #ffffff;
    border-radius: 5px;
    float: left;
    width: 10px;
    height: 10px;
    background-color: #333;
    margin-right: 5px;
    cursor: pointer;
  }
  #buttons .on {
    background: orangered;
  }
</style>
<div id="counter">
  <div id="list" style="left:0px;">
    <!-- 设置初始偏移量为0px -->
    <img src="img/img1.jpg" alt="1" />
    <img src="img/img2.jpg" alt="2" />
    <img src="img/img3.jpg" alt="3" />
    <img src="img/img4.jpg" alt="4" />
    <img src="img/img5.jpg" alt="5" />
    <img src="img/img6.jpg" alt="6" />
  </div>
  <!-- 图片两边的左右点击切换图片按钮 -->
  <a href="javascript:;" id="pre" class="arrow">&lt;</a>
  <a href="javascript:;" id="next" class="arrow">&gt;</a>
</div>

<script type="text/javascript">
  var counter = document.getElementById("counter");
  var list = document.getElementById("list");
  var pre = document.getElementById("pre");
  var next = document.getElementById("next");
  var timer;

  //html与js结合式书写 : 变量名.style.left=数值
  var nextlist = parseInt(list.style.left); //接收偏移量的值
  // var index=1;
  // var buttons=document.getElementById("buttons").getElementById('span');

  //偏移量的改变
  function animals(offset) {
    var newlist = parseInt(list.style.left) + offset; //定义参数随时传递新的偏移量值
    list.style.left = newlist + "px"; //偏移量需要单位‘像素px’,否则计算机识别不出,图片将不会移动位置
    //到达最后一张时,点击右耳朵则返回到第一张
    if (newlist < -3001) {
      list.style.left = 0 + "px";
      list.setAttribute("class", "list");
    }
    //在第一张时,点左耳朵则返回到最后一张
    if (newlist > 0) {
      list.style.left = -3000 + "px";
      list.setAttribute("class", "list");
    }
  }
  // 点击左右耳朵触发函数
  pre.onclick = function () {
    animals(600); //点击左边耳朵,图片往左移一张,偏移量加600
  };
  next.onclick = function () {
    animals(-600); //点击右边耳朵,图片往右移一张,偏移量减600
  };
  // 开始定时器
  function start() {
    timer = setInterval(function () {
      next.onclick();
    }, 2000);
  }
  start();
  // 关闭定时器
  function stop() {
    clearInterval(timer);
  }
  // 鼠标移出时,开始定时器
  counter.onmouseleave = start;
  // 鼠标移入时,关闭定时器
  counter.onmouseenter = stop;
</script>

8.定时器的暂停和结束

(获取当前时间如何实现)

9.使用原生的方式实现 ajax 请求

10.css 手写轮播图:利用 animation

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>轮播图</title>
  </head>
  <!-- style 里面是css样式 因为代码较少就没有再次创建 -->
  <style>
    /* 盒子1的样式 */
    .box1 {
      width: 722px;
      height: 358px;
      margin: 0 auto;
      /* 超出的位置将隐藏并且不占位 */
      overflow: hidden;
    }
    .box {
      width: 2200px;
      height: 352px;
      animation: move 20s infinite;
    }
    @keyframes move {
      0% {
        transform: translateX(0);
      }
      30% {
        transform: translateX(-722px);
      }
      60% {
        transform: translateX(-1422px);
      }
      100% {
        transform: translateX(-1422px);
      }
    }
    .box img {
      float: left;
      height: 352px;
      width: 722px;
    }
  </style>
  <body>
    <!--外层一个容器,里面是一个大长条包着几个小内容在滚动,每次translateX一个小内容的宽度即可,较为简单-->
    <div class="box1">
      <div class="box">
        <img src="./images/img.jpg" alt="" />
        <img src="./images/img2.jpg" alt="" />
        <img src="./images/img3.jpg" alt="" />
      </div>
    </div>
  </body>
</html>

渐变实现轮播图,更为简单

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>轮播图</title>
  </head>
  <!-- style 里面是css样式 因为代码较少就没有再次创建 -->
  <style>
    /* 盒子1的样式 */
    .focus {
      margin: 0 auto;
      width: 700px;
      height: 322px;
      animation: focus 20s infinite;
    }
    @keyframes focus {
      0% {
        background-image: url("./images/img.jpg");
      }
      50% {
        background-image: url("./images/img2.jpg");
      }
      100% {
        background-image: url("./images/img3.jpg");
      }
    }
  </style>
  <body>
    <!--外层一个容器,里面通过background-image去变化即可-->
    <div class="focus"></div>
  </body>
</html>

11.手写 indexOf

indexOf 是 JavaScript 字符串和数组中的方法,用于查找给定值在字符串或数组中第一次出现的位置索引。以下是一个手写的 indexOf 函数示例,可以用于数组和字符串:

js
function myIndexOf(arrOrStr, target, startIndex = 0) {
  if (Array.isArray(arrOrStr)) {
    //数组的情况
    for (let i = startIndex; i < arrOrStr.length; i++) {
      //遍历查找匹配值的索引
      if (arrOrStr[i] === target) {
        return i;
      }
    }
  } else if (typeof arrOrStr === "string") {
    //字符串的情况
    for (let i = startIndex; i < arrOrStr.length; i++) {
      if (arrOrStr[i] === target) {
        //注意:字符串也是可以直接用下标进行遍历的!
        return i;
      }
    }
  } else {
    throw new TypeError("Unsupported data type");
  }
  return -1; //没有匹配值
}

// 测试数组
const arr = [1, 2, 3, 4, 5];
console.log(myIndexOf(arr, 3)); // 输出 2

// 测试字符串
const str = "Hello, world!";
console.log(myIndexOf(str, "o")); // 输出 4

// 测试未找到的情况
console.log(myIndexOf(arr, 6)); // 输出 -1

这个示例中,myIndexOf 函数接受一个数组或字符串、要查找的目标值以及一个可选的起始索引。它会遍历数组或字符串,从指定的起始索引开始,查找目标值第一次出现的位置索引。如果找到了,返回索引值;如果未找到,返回 -1。这是一个简化的手写示例,实际上内置的 indexOf 方法在处理一些特殊情况时可能会更加复杂。

12.手写 instanceof:遍历原型链 getPrototypeOf

instanceof 是 JavaScript 中用于检查一个对象是否是某个构造函数的实例的运算符。您可以通过检查对象的原型链来判断它是否是特定构造函数的实例。以下是一个手写的 instanceof 函数示例:

思路:大概就是看这个对象的原型是否指向了构造函数的原型对象(对象的原型对象是否是构造函数的原型对象)

注意:对象有可能是继承过来的,而不是由构造函数直接创建的,所以不能单单看一层的原型指向,而是要遍历对象的原型链,看它是否有某一层的原型对象是构造函数的原型对象!

其实遍历原型链就是遍历链表!

js
function myInstanceOf(obj, constructor) {
  // 检查参数是否为对象和函数,如果不是的话,就直接失败
  if (typeof obj !== "object" || obj === null) {
    return false;
  }

  if (typeof constructor !== "function") {
    throw new Error("Right-hand side of instanceof is not callable");
  }

  let prototype = Object.getPrototypeOf(obj); // 获取对象的原型
  while (prototype !== null) {
    if (prototype === constructor.prototype) {
      //看对象的隐式原型是否指向构造函数的原型对象! ——> 关键点
      return true;
    }
    prototype = Object.getPrototypeOf(prototype); //如果不是,就接着往原型链上层去获取原型对象 ——> 指针向后移动一位,遍历链表,也是关键点
  }
  return false;
}

// 测试示例
class MyClass {}
const obj = new MyClass();
console.log(myInstanceOf(obj, MyClass)); // true
console.log(myInstanceOf(obj, Array)); // false

在这个示例中,myInstanceOf 函数接受一个对象一个构造函数作为参数,然后检查对象的原型链中是否存在与构造函数的原型相匹配的原型,如果有则返回 true,否则返回 false

请注意,这只是一个简单的手写示例,实际上,内置的 instanceof 运算符在处理一些特殊情况时可能会更加复杂,比如处理原始值的包装对象等。

13.手写在原型链上给对象属性溯源的方法:类似手写 instanceof

实际上主要是用了:getPrototypeOf + hasOwnProperty

  • 实现要求:如下效果,也就是说findPrototypeByProperty 函数可以帮我们寻找一个对象上面属性的源头是哪里(是在它原型链的哪个原型对象上面),实现这样一个 findPrototypeByProperty 函数
js
const foo = { a: 1 };

const bar = Object.create(foo); //bar的原型对象是foo
bar.b = 2;

const baz = Object.create(bar); //baz的原型对象是bar
baz.c = 3;

console.log(findPrototypeByProperty(baz, "c") === baz); // true
console.log(findPrototypeByProperty(baz, "b") === bar); // true
console.log(findPrototypeByProperty(baz, "a") === foo); // true

您想要实现一个函数 findPrototypeByProperty,该函数会在原型链中寻找包含指定属性的对象,并返回。下面是一个实现的示例:

js
function findPrototypeByProperty(obj, prop) {
  while (obj !== null) {
    if (obj.hasOwnProperty(prop)) {
      //hasOwnProperty是由对象实例调用,参数是一个属性名,判断这个属性是否直属于这个对象,而不是在原型链的!
      return obj;
    }
    obj = Object.getPrototypeOf(obj); //获取对象的原型对象,也就是去到原型链的上一层
  }
  return null;
}

const foo = { a: 1 };
const bar = Object.create(foo);
bar.b = 2;
const baz = Object.create(bar);
baz.c = 3;

console.log(findPrototypeByProperty(baz, "c") === baz); // true
console.log(findPrototypeByProperty(baz, "b") === bar); // true
console.log(findPrototypeByProperty(baz, "a") === foo); // true

在这个示例中,findPrototypeByProperty 函数接受一个对象和一个属性名称作为参数。它会在给定的对象及其原型链上查找是否有包含指定属性的对象,并返回找到的对象。如果找不到包含该属性的对象,则返回 null。函数使用了 hasOwnProperty 方法来检查属性是否存在于当前对象中,然后通过 Object.getPrototypeOf 方法获取对象的原型对象,从而遍历整个原型链

请注意,如果多个对象的原型链上都包含指定属性,函数将返回第一个找到属性的对象。

13.手写数组扁平化(数组拍平):将嵌套数组转化为一维数组

**数组扁平化是指将多层嵌套的数组结构转换为一维数组。**以下是一些手写数组扁平化的方法示例:

  1. **递归方法:**类似手写对象的深拷贝
js
function flattenArrayRecursive(arr) {
  let result = []; //每次递归都用一个新数组接收被拍了一次的数组
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      result = result.concat(flattenArrayRecursive(arr[i])); //拼接一个递归的拍平数组
    } else {
      result.push(arr[i]);
    }
  }
  return result;
}

const nestedArray = [1, [2, [3, 4]], 5, [6]];
const flattenedArray = flattenArrayRecursive(nestedArray);
console.log(flattenedArray); // 输出 [1, 2, 3, 4, 5, 6]
  1. 使用 reduce 方法优化(还是递归):
js
function flattenArrayWithReduce(arr) {
  return arr.reduce((result, current) => {
    //免去了for循环
    if (Array.isArray(current)) {
      return result.concat(flattenArrayWithReduce(current));
    } else {
      return result.concat(current);
    }
  }, []); //[]是初始数据
}

const nestedArray = [1, [2, [3, 4]], 5, [6]];
const flattenedArray = flattenArrayWithReduce(nestedArray);
console.log(flattenedArray); // 输出 [1, 2, 3, 4, 5, 6]
  1. 使用 ES6 扩展运算符 + some 优化(避免递归):——> 最完美的方法

arr.some() 是 JavaScript 数组的一个方法,用于检查数组中是否至少有一个元素满足指定的测试函数。如果测试函数对任何一个元素返回 true,则 some() 方法返回 true,否则返回 false

注意:arr.every()方法的作用和arr.some()类似,不同的地方在于只有当数组中的所有元素都满足指定条件时,返回 true,否则返回 false

arr.concat() 方法可以接收多个参数,这些参数可以是数组或值。它会将所有参数中的数组元素和值连接到调用它的数组中,并返回一个新的数组。以下是使用 concat() 方法接收多个参数的示例:

js
const arr1 = [1, 2];
const arr2 = [3, 4];
const concatenatedArray = arr1.concat(arr2, 5, 6);

console.log(concatenatedArray); // 输出 [1, 2, 3, 4, 5, 6]
console.log(arr1); // 输出 [1, 2](原数组未改变)
console.log(arr2); // 输出 [3, 4](原数组未改变)

在上述示例中,concatenatedArray 包含了 arr1arr256 这些参数中的元素。注意,concat() 方法不会改变原始数组 arr1arr2,而是返回一个新数组,该数组包含了所有连接的元素。

注意:如果 concat 的参数里面有数组,会默认对数组进行…即展开,把[]去掉,再加入到前面的数组里面!

思路:利用 some 函数判断数组是否是嵌套数组,如果是的话利用 concat 会去掉[]的特性来实现每次去掉一层的效果!(也可以说是因为扩展运算符…去掉了一层[])

js
function flattenArrayWithSpread(arr) {
  while (arr.some((item) => Array.isArray(item))) {
    //arr.some(item => Array.isArray(item)) 可以用于检查数组 arr 中是否至少有一个元素是数组。如果数组中有一个元素是数组,则返回 true,否则返回 false。即判断是否有嵌套数组。
    arr = [].concat(...arr); //利用concat可以接收多个参数(和push一样)和concat会把每一项参数的[]去掉的特性,实现了每次都可以去掉一层的效果!而...去掉的一层和前面的[]空数组抵消了!
  }
  return arr;
}

const nestedArray = [1, [2, [3, 4]], 5, [6]];
const flattenedArray = flattenArrayWithSpread(nestedArray);
console.log(flattenedArray); // 输出 [1, 2, 3, 4, 5, 6]

这些方法都可以将多层嵌套的数组扁平化为一维数组。根据您的喜好和项目需求,可以选择适合您的方法。