JavaScript 之手写它们它们和它们

一、实现一个 new 操作符

new 的原理是什么?通过 new的方式创建对象和通过字面量创建有什么区别?

new原理

在调用 new 的过程中会发生四件事情

  • 创建一个空对象,构造函数中的this指向这个空对象
  • 新对象被链接到原型
  • 执行构造函数方法,属性和方法被添加到this引用的对象中
  • 如果构造函数中没有返回新对象,那么返回this,即创建这个新对象,否则,返回构造函数中返回的对象

实现一个 new 并测试

//实现2
function myNew(func, ...args) {
  const obj = {};     // 新建一个空对象
  const result = func.call(obj, ...args);  // 执行构造函数
  obj.__proto__ = func.prototype;    // 设置原型链

  // 注意如果原构造函数有Object类型的返回值,包括Functoin, Array, Date, RegExg, Error
  // 那么应该返回这个返回值
  const isObject = typeof result === 'object' && result !== null;
  const isFunction = typeof result === 'function';
  if(isObject || isFunction) {
    return result;
  }

  // 原构造函数没有Object类型的返回值,返回我们的新对象
  return obj;
}

//test
function Dog(age) {
  this.age = age;
}
Dog.prototype.say = function () {
  console.log("汪汪汪");
}

const myDog = myNew(Dog, 2);

console.log(myDog.age);  // 2
myDog.say();   // 汪汪汪

二、实现一个 call、apply

call、apply、bind三者的用法及区别、手写

1、call特性(非严格模式)

  1. 如果 obj.call(null),那么 this 应该指向 window
  2. 如果 obj1.call(obj2),那么谁调用它,this 指向谁(这里就是 obj2了)
  3. call 可以传入多个参数,所以可以利用 arguments 这个字段来获取所有参数。将 arguments 转换数组后,获取除第一个参数外的其他参数
  4. 设置一个变量,用完删掉它

实现一个call并测试

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>手写 call并测试</title>
</head>

<body>
  <button class="btn">123</button>

  <script>
    (function () {
      //手写实现call
      Function.prototype.myCall = function (context) {
        // 不传context的话,默认上下文为window
        const newContext = context || window;
        // 设置fn为调用 myCall 的方法
        newContext.fn = this;
        // 获取剩余参数
        const otherArg = Array.from(arguments).slice(1);
        // 调用这个方法,将剩余参数传递进去
        const result = newContext.fn(otherArg);
        // 删除这个变量
        delete newContext;
        return result;
      };
      
	  //test
      const debounce = function (fn) {
        let timer = null;
        return function () {
          clearTimeout(timer);
          timer = setTimeout(() => {
            fn.myCall(this, arguments);
          }, 1000);
        }
      }

      let time = 0;
      const getNumber = function () {
        console.log(++time);
      }

      const btn = document.querySelector('.btn');
      btn.addEventListener('click', debounce(getNumber));
    })()
  </script>
</body>

</html>
//test
function getName() {
  console.log(this.name)
}

let obj = {
  a: 1,
  name: 'xiuzhu'
}
getName.myCall(obj) //xiuzhu

2、apply特性(非严格模式)

apply:和call基本上一致,唯一区别在于传参方式

call后面传递的参数是以逗号的形式分开的,apply传递的参数是数组形式apply是以A开头,所以这里可以跟Array有关联,即参数为数组 [arg1, arg2...] ,虽然写的是一个数组,但是也相当于一个一个传递

fn.call(obj, 1, 2);
fn.apply(obj, [1, 2]);

实现一个apply并测试

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>手写apply</title>
</head>

<body>
  <button class="btn">123</button>

  <script>
    (function () {
      //手写实现apply
      Function.prototype.myApply = function (context, arr) {
        // 不传context的话,默认上下文为window
        const newContext = context || window;
        // 设置fn为调用 myApply 的方法
        newContext.fn = this;
        // 调用方法将参数传入
        if (!arr) {
          result = newContext.fn();
        } else {
          result = newContext.fn(arr);
        }

        // 删除这个变量
        delete newContext;
        return result;
      };

      const debounce = function (fn, number) {
        let timer = null;
        return function () {
          clearTimeout(timer);
          timer = setTimeout(() => {
            fn.myApply(this, number);
          }, 1000);
        }
      }

      const getNumber = function (number) {
        console.log(number);
      }

      let number = [1, 2, 3, 4, 5];
      const btn = document.querySelector('.btn');
      btn.addEventListener('click', debounce(getNumber, number));
    })()
  </script>
</body>

</html>

三、实现一个 bind

call、apply、bind三者的用法及区别、手写

bind会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )

bind:语法和call一模一样,区别在于立即执行还是等待执行,bind不兼容IE6~8

改变 this 作用域会返回一个新的函数,这个函数不会马上执行

fn.call(obj, 1, 2); // 改变fn中的this,并且把fn立即执行
fn.bind(obj, 1, 2); // 改变fn中的this,fn并不执行

call和bind的区别

  • callfnthis改变为obj了,但是绑定的时候立即执行,当触发点击事件的时候执行的是fn的返回值undefined
document.onclick = fn.call(obj);
  • bind会把fn中的this预处理为obj,此时fn没有执行,当点击的时候才会把fn执行
document.onclick = fn.bind(obj);

实现一个bind并测试

Function.prototype.myBind = function(context = globalThis) {
  // 设置 fn 为调用 myCall 的方法
  const fn = this;
  // 获取该方法剩余参数
  const otherArg = [...arguments].slice(1);
  // 设置返回的一个新方法
  const result = function() {
    // 获取返回方法体的参数
    const resultArg = [...arguments];
    // 如果是通过 new 调用的,绑定 this 为实例对象
    if (this instanceof result) {
      fn.apply(this, otherArg.concat(resultArg));
    } else { // 否则普通函数形式绑定 context
      fn.apply(context, otherArg.concat(resultArg));
    }
  }
  // 绑定原型链
  result.prototype = Object.create(fn.prototype);
  // 返回结果
  return result;
};

//test
function foo(something) {
  console.log(this.a, something)
  return this.a + something
}
obj = {
  a: 2
}

let bar = foo.myBind(obj)
bar(3) //2 3

四、实现一个继承

原型链、原型继承和Class继承

1、组合继承

组合继承是最常用的继承方式

function Parent(value) {
  this.val = value
}
//原型式继承
Parent.prototype.getValue = function() {
  console.log(this.val)
}
//构造函数继承
function Child(value) {
  Parent.call(this, value)
}
//原型继承
Child.prototype = new Parent()

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true
  • 继承方式优点:实现了函数复用,且能在构造函数中传参,不会与父类引用属性共享,可以复用父类的函数
  • 缺点:会调用两次超类型的构造函数,一次在创建子类型原型的时候,另一次在子类型构造函数内部。就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费

2、寄生组合继承
引用类型最理想的继承范式

前面的组合继承有个缺点:每次创建实例时都会调用两次超类方法,一次是通过new设置原型的时候,另一次是用call执行的时候

function Parent(value) {
  this.val = value
}
//原型式继承
Parent.prototype.getValue = function() {
  console.log(this.val)
}
//构造函数继承
function Child(value) {
  Parent.call(this, value)
}
//寄生继承
//Object.create()是ES5规范化的原型式继承:一个是用作新对象原型的对象和
//(可选的)一个新对象定义额外属性的对象
// 如下是补充因为重写Parent原型而失去默认的constructor属性,最后将新创建的对象赋值给子类型的原型
Child.prototype = Object.create(Parent.prototype, {
  constructor: {
    value: Child,
    enumerable: false,
    writable: true,
    configurable: true
  }
})

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true
  • 思路:不需要为了指定子类型的原型而调用超类型的构造函数(我理解为就是不需要显示的new操作),通过上面的寄生式继承方式来继承超类型的原型即可。

五、实现一个 instanceof

判断数据类型typeof、instanceof、constructor和Object.prototype.toString.call()

instanceof运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。简单来说就是 instanceof 是用来判断 A 是否为 B 的实例。

根据上述描述,实现一个instanceof

function myInstanceof(left, right) {
  let prototype = right.prototype
  left = left.__proto__
  while (true) {
    if (left === null || left === undefined)
      return false
    if (prototype === left)
      return true
    left = left.__proto__
  }
}
//测试
var arr = [1, 2, 3];
console.log(myInstanceof(arr, Array)) // true

六、手写(Debouncing)和节流(Throttling)

函数防抖和函数节流

1、防抖(Debouncing)实现

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 1000) => {
  // 缓存一个定时器id
  let timer = 0
  // 这里返回的函数是每次用户实际调用的防抖函数
  // 如果已经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,延迟执行用户传入的方法
  return function (...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}

典型例子:限制鼠标连击触发。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>手写 call</title>
</head>

<body>
  <button class="btn">123</button>
  <script>
    (function () {
      const debounce = (func, wait = 1000) => {
        // 缓存一个定时器id
        let timer = 0
        // 这里返回的函数是每次用户实际调用的防抖函数
        // 如果已经设定过定时器了就清空上一次的定时器
        // 开始一个新的定时器,延迟执行用户传入的方法
        return function (...args) {
          if (timer) clearTimeout(timer)
          timer = setTimeout(() => {
            func.apply(this, args)
          }, wait)
        }
      }

      let time = 0;
      const getNumber = function () {
        console.log(++time);
      }

      const btn = document.querySelector('.btn');
      btn.addEventListener('click', debounce(getNumber));
    })()
  </script>
</body>

</html>

2、节流(Throttling)实现

每隔一段时间,只执行一次函数。

// func是用户传入需要节流的函数
// wait是等待时间
const throttle = (func, wait = 50) => {
  // 上一次执行该函数的时间
  let lastTime = 0
  return function(...args) {
    // 当前时间
    let now = +new Date()
    // 将当前时间和上一次执行函数时间对比
    // 如果差值大于设置的等待时间就执行函数
    if (now - lastTime > wait) {
      lastTime = now
      func.apply(this, args)
    }
  }
}

//test:1s输出一次1
setInterval(
  throttle(() => {
    console.log(1)
  }, 1000),
  1
)

七、手写深浅拷贝

探秘:深拷贝与浅拷贝

根据拷贝的层级不同可以分为浅拷贝和深拷贝,浅拷贝就是只进行一层拷贝,深拷贝就是无限层级拷贝

1、实现浅拷贝

//浅拷贝
function shallowClone(target) {
  let cloneTarget = {};
  for (const key in target) {
    cloneTarget[key] = target[key];
  }
  return cloneTarget;
}

2、实现深拷贝

深拷贝的问题其实可以分解成两个问题,浅拷贝+递归

简版

//深拷贝
function deepClone(target) {
  if (typeof target === 'object') {
    let cloneTarget = {};
    for (const key in target) {
      cloneTarget[key] = deepClone(target[key]);
    }
    return cloneTarget;
  } else {
    return target;
  }
}

完整版:解决参数检验不严谨、没有考虑数组、循环引用的问题

//代码
function isObject(x) {
  return Object.prototype.toString.call(x) === '[object Object]'
}

function deepClone(target, map = new Map()) {
  // 使用isObject对参数做检验,判断是否对象
  if (isObject(target)) {
    // 考虑数组
    let cloneTarget = Array.isArray(target) ? [] : {};
	//使用map解决循环引用问题
    if (map.has(target)) {
      return map.get(target);
    }
    map.set(target, cloneTarget);

    for (const key in target) {
      cloneTarget[key] = deepClone(target[key], map);
    }
    return cloneTarget;
  } else {
    return target;
  }
}

//测试用例
const target = {
  field1: 1,
  field2: undefined,
  field3: {
    child: 'child'
  },
  field4: [2, 4, 8]
};
target.target = target

var cloneTarget = deepClone(target)
console.log(cloneTarget)

输出:target属性,变为了一个Circular类型,即循环引用的意思。
在这里插入图片描述

八、实现一个JSON.stringify和JSON.parse

JSON.parse/JSON.stringify还是有很多局限性,大致如下:

  • 会忽略 undefined
  • 会忽略 Symbol
  • 无法序列化function,也会忽略
  • 无法解决循环引用,会报错
  • 深层对象转换爆栈

1、实现JSON.stringify

JSON.stringify(value[, replacer [, space]]):

  • Boolean | Number| String 类型会自动转换成对应的原始值。
  • undefined、任意函数以及symbol,会被忽略(出现在非数组对象的属性值中时),或者被转换成 null(出现在数组中时)。
  • 不可枚举的属性会被忽略
  • 如果一个对象的属性值通过某种间接的方式指回该对象本身,即循环引用,属性也会被忽略。

实现JSON.stringify
可以序列化引用类型和基本类型。


// 数据类型判断
function getType(attr) {
  let type = Object.prototype.toString.call(attr);
  let newType = type.substr(8, type.length - 9);
  return newType;
}
// 转换函数
function StringIfy(obj) {
  // 如果是非object类型 or null的类型直接返回 原值的String
  if (typeof obj !== "object" || getType(obj) === null) {
    return String(obj);
  }
  // 声明一个数组
  let json = [];
  // 判断当前传入参数是对象还是数组
  let arr = obj ? getType(obj) === "Array" : false;
  // 循环对象属性
  for (let key in obj) {
    // 判断属性是否在对象本身上
    if (obj.hasOwnProperty(key)) {
      // console.log(key, item);
      // 获取属性并且判断属性值类型
      let item = obj[key];
      if (item === obj) {
        console.error(new TypeError("Converting circular structure to JSON"));
        return false;
      }
      if (/Symbol|Function|Undefined/.test(getType(item))) {
        delete obj[key];
        continue;
      }
      // 如果为object类型递归调用
      if (getType(item) === "Object") {
        // consoarrle.log(item)
        item = StringIfy(item);
      }
      let IsQueto =
        getType(item) === "Number" ||
        getType(item) === "Boolean" ||
        getType(item) === "Null"
          ? ""
          : '"';
      // 拼接数组字段
      json.push((arr ? IsQueto : '"' + key + '":"') + String(item) + IsQueto);
    }
  }
  // 转换数组字段为字符串
  return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}");
}

//测试
let target = {
  name: "name",
  age: undefined,
  func: function() {},
  sym: Symbol("setter")
};
console.log(StringIfy(target))
let json = JSON.stringify(target)
console.log(json) //"{"name":"name"}"

1、实现JSON.parse
json字符串转换为对象

//方法一:直接用eval
function jsonParse(opt) {
    return eval('(' + opt + ')');
}
jsonParse(jsonStringify({x : 5}))
// Object { x: 5}
jsonParse(jsonStringify([1, "false", false]))
// [1, "false", falsr]
jsonParse(jsonStringify({b: undefined}))
// Object { b: "undefined"}

//方法二:Function
var jsonStr = '{ "age": 20, "name": "jack" }'
var json = (new Function('return ' + jsonStr))();

九、实现一个 JS 函数柯里化

什么是柯里化?

用通俗易懂的话说:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

函数柯里化的主要作用和特点就是参数复用、提前返回和延迟执行。

函数柯里化的实现

function curry(fn, args) {
  var length = fn.length;
  var args = args || [];
  return function () {
    newArgs = args.concat(Array.prototype.slice.call(arguments));
    //如果参数个数小于最初的fn.length,则递归调用,继续收集参数
    if (newArgs.length < length) {
      return curry.call(this, fn, newArgs);
    }
    //参数收集完毕,则执行fn
    return fn.apply(this, newArgs);
  }
}

//test
function multiFn(a, b, c) {
  return a * b * c;
}

var multi = curry(multiFn);
console.log(multi(2)(3)(4)) //都输出24
console.log(multi(2, 3, 4))
console.log(multi(2)(3, 4))
console.log(multi(2, 3)(4))

十、请实现 Promise 的这个世界

过一遍Promise/A+规范:

  • 三种状态pending| fulfilled(resolved) | rejected
  • 当处于pending状态的时候,可以转移到fulfilled(resolved)或者rejected状态
  • 当处于fulfilled(resolved)状态或者rejected状态的时候,就不可变。

必须有一个then异步执行方法,then接受两个参数且必须返回一个promise:

// onFulfilled 用来接收promise成功的值
// onRejected 用来接收promise失败的原因
promise1=promise.then(onFulfilled, onRejected);

忆往昔:Promise用法

var promise = new Promise((resolve, reject) => {
  if (操作成功) {
    resolve(value)
  } else {
    reject(error)
  }
})
promise.then(function (value) {
  // success
}, function (value) {
  // failure
})

1、实现一个Promise

function myPromise(constructor) {
  let self = this;
  self.status = "pending" //定义状态改变前的初始状态
  self.value = undefined;//定义状态为resolved的时候的状态
  self.reason = undefined;//定义状态为rejected的时候的状态
  function resolve(value) {
    //两个==="pending",保证了状态的改变是不可逆的
    if (self.status === "pending") {
      self.value = value;
      self.status = "resolved";
    }
  }
  function reject(reason) {
    //两个==="pending",保证了状态的改变是不可逆的
    if (self.status === "pending") {
      self.reason = reason;
      self.status = "rejected";
    }
  }
  //捕获构造异常
  try {
    constructor(resolve, reject);
  } catch (e) {
    reject(e);
  }
}

//在myPromise的原型上定义链式调用的then方法
myPromise.prototype.then = function (onFullfilled, onRejected) {
  let self = this;
  switch (self.status) {
    case "resolved":
      onFullfilled(self.value);
      break;
    case "rejected":
      onRejected(self.reason);
      break;
    default:
  }
}

var p = new myPromise(function (resolve, reject) { resolve(123) });
p.then(function (x) { console.log(x) })
//输出123

2、手撕 Promise.all

Promise.all 接收一个 promise 对象的数组作为参数,当这个数组里的所有 promise 对象全部变为resolve或 有 reject 状态出现的时候,它才会去调用 .then 方法,它们是并发执行的。

实现方法

Promise.myAll = function (promises) {
  return new Promise(function (resolve, reject) {
    var resolvedCounter = 0
    var promiseNum = promises.length
    var resolvedValues = new Array(promiseNum)
    for (var i = 0; i < promiseNum; i++) {
      (function (i) {
        Promise.resolve(promises[i]).then(function (value) {
          resolvedCounter++
          resolvedValues[i] = value
          if (resolvedCounter == promiseNum) {
            return resolve(resolvedValues)
          }
        }, function (reason) {
          return reject(reason)
        })
      })(i)
    }
  })
}

// test
var p1 = Promise.resolve(1),
  p2 = Promise.reject(2),
  p3 = Promise.resolve(3);
Promise.myAll([p1, p2, p3]).then(function (results) {
  //then方法不会被执行
  // 成功输出则输出数组
  console.log(results);
}).catch(function (e) {
  //catch方法将会被执行,输出结果为:2
  console.log(e);
});

es6
Promise.prototype.myAll = function (promises) {
  return new Promise((resolve, reject) => {
    let resolvedCounter = 0
    let promisesNum = promises.length
    let resolvedValue = new Array(promisesNum)

    promises.forEach((promise, i) => {
      promise.then(
        value => {
          resolvedCounter++
          resolvedValue[i] = value
          if (resolvedCounter === promisesNum) {
            return resolve(resolvedValue)
          }
        },
        reason => {
          return reject(reason)
        }
      )
    })
  })
}

Promise.all总结

  • 接收一个 Promise 实例的数组或具有 Iterator 接口的对象,
  • 如果元素不是 Promise 对象,则使用 Promise.resolve 转成 Promise 对象
  • 如果全部成功,状态变为 resolved,返回值将组成一个数组传给回调
  • 只要有一个失败,状态就变为 rejected,返回值将直接传递给回调
    all() 的返回值也是新的 Promise 对象

3、手撕Promise.allSettled
我们期望传入的这组 promise 无论执行失败或成功,都能获取每个 promise 的执行结果,为此,ES2020 引入了 Promise.allSettled()
Promise.allSettled() 可以获取数组中每个 promise 的结果,无论成功或失败

Promise.myAllSettled = function (promises) {
  if (promises.length === 0) return Promise.resolve([])

  const _promises = promises.map(
    item => item instanceof Promise ? item : Promise.resolve(item)
  )

  return new Promise((resolve, reject) => {
    const result = []
    let unSettledPromiseCount = _promises.length

    _promises.forEach((promise, index) => {
      promise.then((value) => {
        result[index] = {
          status: 'fulfilled',
          value
        }

        unSettledPromiseCount -= 1
        // resolve after all are settled
        if (unSettledPromiseCount === 0) {
          resolve(result)
        }
      }, (reason) => {
        result[index] = {
          status: 'rejected',
          reason
        }

        unSettledPromiseCount -= 1
        // resolve after all are settled
        if (unSettledPromiseCount === 0) {
          resolve(result)
        }
      })
    })
  })
}

// 测试
var p1 = Promise.resolve(1),
  p2 = Promise.reject('错误'),
  p3 = Promise.resolve(3)

Promise.myAllSettled([p1, p2, p3]).then(function (results) {
  console.log(results)
}).catch(function (e) {
  console.log(e)
})
//输出
[
  { status: 'fulfilled', value: 1 },
  { status: 'rejected', reason: '错误' },
  { status: 'fulfilled', value: 3 }
]

4、手撕Promise.race

/**
* @func Promise 函数对象的 race()
* @param  promises 请求数组
* @returns 返回一个 Promise,其结果由第一个完成的 Promise 决定
*/
Promise.myRace = function (promises) {
  // 返回一个新的 Promise
  return new Promise((resolve, reject) => {
    // 遍历 promises 取出每个 Promsie 的结果
    promises.forEach(p => {
      Promise.resolve(p).then(
        value => {
          // 一旦有一个成功了, return 的 Promise 就成功
          resolve(value);
        },
        reason => {
          // 一旦有一个失败了, return 的 Promise 就失败
          reject(reason);
        }
      )
    })
  })
}

5、手撕Promise.finally

用于指定不管Promise对象最后状态如何都会执行的操作,接收一个回调函数作为参数。

// 实现Promise.finally
Promise.prototype.finally = function (callback) {
  let P = this.constructor
  return this.then(
    value => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  )
}

6、Promise串行

Promise串行习题

定义 type Task = () => Promise (即 Task 是一个 类型,是一个返回值是 Promise 的函数类型)
假设有一个数组 tasks: Task[](每一项都是一个 Task 类型的数组)
实现一个方法 function execute(tasks: Task[]): Promise,该方法将 tasks 内的任务 依次 执行,并返回一个结果为数组的 Promise ,该数组包含任务执行结果(以执行顺序排序)
要求: 忽略异常任务,并在结果数组中用 null 占位
限制: 不添加任何依赖,仅使用 Promise,不使用 Generator 或 async

思路大致如下图
先做一个Promise实例,然后把每个Task循环的放置到上一个promise的then回调里。

在这里插入图片描述
需要注意的几点:

  • 无论每个Task是成功还是失败,它都不能阻断下一个Task的执行
  • 最后的then需要把每个Task的执行结果"决议"出去

对策:

  • 每一个Task外层包装一层Promise,捕获Task的reject状态
  • 可以利用一个中间变量,缓存所有Task的输出结果,然后在最后一个Promise的then里把中间变量“决议”出去
function execute(tasks) {
  return tasks.reduce(
    (previousPromise, currentPromise) => previousPromise.then((resultList) => {
      return new Promise(resolve => {
        currentPromise().then(result => {
          resolve(resultList.concat(result))
        }).catch(() => {
          resolve(resultList.concat(null))
        })
      })
    }),
    Promise.resolve([])
  )
}

//测试用例
let Task = (result, isSuccess = true) => {
  return () => new Promise((resolve, reject) => {
    setTimeout(() => {
      if (isSuccess) {
        console.log(`success: ${result}`);
        resolve(result);
      } else {
        console.log(`error: ${result}`);
        reject(result);
      }
    }, 1000);
  });
}

execute([
  Task('A'),
  Task('B'),
  Task('X', false),
  Task('C'),
]).then(resultList => {
  // 这里期望打印 ["A", "B", null, "C"]
  console.log(resultList)
})
// aysnc/await版本
// aycns/await要与迭代器进行配合,所以应该利用for of来配合使用。
const execute = async (tasks = []) => {
  const resultList = [];
  for (task of tasks) {
    try {
      resultList.push(await task());
    } catch (e) {
      resultList.push(null);
    }
  }
  return resultList;
}

十一、实现indexOf

  • StringArray 两种数据类型都可以调用该API;
  • 该接口有两个参数: str.indexOf(searchVal, fromIndex);其中,searchVal支持多字符,且对大小写敏感;
  • 匹配成功,返回首次匹配子内容的索引;
  • 匹配失败,返回-1
//基于字符串
function sIndexOf(str, searchStr, fromIndex) {
  let strLen = str.length
  let searchLen = searchStr.length
  if (searchLen === 0) {
    if (fromIndex > strLen) return strLen
    else if (fromIndex > 0 && fromIndex < strLen) return fromIndex
  }
  if (fromIndex < 0) fromIndex = 0
  // if (fromIndex >= strLen) return -1
  for (let i = fromIndex; i < strLen - searchLen; i++) {
    if (str.slice(i, i + searchLen) === searchStr) return i
  }
  return -1
}

//基于数组
function aIndexOf(arr, searchVal, fromIndex = 0) {
  let arrLen = arr.length
  if (fromIndex < 0) fromIndex += arrLen
  // if (fromIndex >= arrLen) return -1
  for (let i = fromIndex; i < arrLen; i++) {
    if (arr[i] === searchVal) return i
  }
  return -1
}

//封装
String.prototype._indexOf = Array.prototype._indexOf = function (searchVal, fromIndex) {
  let data = this
  let isArray = Array.isArray(data)
  let isString = Object.prototype.toString.call(data) == '[object String]'
  if (!isArray && !isString) throw new TypeError('String or Array')
  if (isArray) return aIndexOf(data, searchVal, fromIndex)
  if (isString) return sIndexOf(data, searchVal, fromIndex)
}

十二、实现一个Array.prototype.reduce

Array.prototype.reduce语法

Array.prototype.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

数组的reduce方法接收两个参数

  • 第一个参数为一个函数,函数有四个参数,是accumulator、currentValue、index、array。分别代表:
    accumulator:累加器,累加回调函数的返回值。
    currentValue:当前元素。
    index:当前元素的索引。
    array:当前元素所属的数组对象。
  • 第二个参数为可选参数 initialValue,代表传递给函数的初始值。

实现

Array.prototype.myReduce = function(callback, initialValue) {
  let accumulator = initialValue ? initialValue : this[0];
  for (let i = initialValue ? 0 : 1; i < this.length; i++) {
    let _this = this;
    accumulator = callback(accumulator, this[i], i, _this);
  }
  return accumulator;
};

// 使用
let arr = [1, 2, 3, 4];
let sum = arr.myReduce((prev, next) => {
  prev += next
  return prev
}, 5)
console.log(sum); // 15

十三、各种排序算法的实现

常见排序算法的实现与介绍

参考文章

Promise串行

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 这是一个典型的防抖和节流函数:function debounce(func, wait = 20, immediate = true) { let timeout; return function() { let context = this, args = arguments; let later = function() { timeout = null; if (!immediate) func.apply(context, args); }; let callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; };防抖函数(debounce):用于控制函数在一定时间内只能触发一次,在这段时间内,如果再次调用,则会取消上一次的调用,等到调用的间隔超过一定时间后,才会真正的触发。节流函数(throttle):用于控制函数在一定时间内只能触发一次,在这段时间内,如果再次调用,则会取消上一次的调用,但是不会取消下一次的调用,等到调用的间隔超过一定时间后,才会真正的触发。 ### 回答2: 防抖和节流是常用于前端开发的两个函数,用于优化页面的性能和用户体验。 防抖函数的作用是防止某个事件在短时间内频繁触发,减少不必要的计算和请求,只执行最后一次触发事件的操作。防抖函数的实现原理是设置一个定时器,在事件触发后等待一段时间,如果在此期间再次触发了该事件,则重新计时,直到事件触发的间隔大于等于设定的等待时间,才执行对应的操作。这种方式可以用于处理输入框输入时的自动搜索提示,滚动页面时触发的加载更多等场景。 节流函数的作用是限制某个事件在一段时间内的触发次数,减少处理的频率,限制资源的消耗。节流函数的实现原理是通过设置一个定时器,在事件触发后设定的时间间隔内,无论事件触发多次,只执行一次对应的操作。这种方式可以用于处理页面滚动、窗口调整大小等事件的处理,保持操作的可靠性和稳定性。 下面是一个简单的防抖函数的示例代码: ```javascript function debounce(fn, delay) { let timer = null; return function() { clearTimeout(timer); timer = setTimeout(fn, delay); }; } ``` 下面是一个简单的节流函数的示例代码: ```javascript function throttle(fn, interval) { let timer = null; return function() { if (!timer) { timer = setTimeout(function() { fn(); timer = null; }, interval); } }; } ``` 以上是两个比较常见的函数,用于优化页面性能和用户体验。在实际开发中,可以根据具体的需求和场景选择适当的防抖和节流策略,以提升页面的响应速度和用户交互的流畅性。 ### 回答3: 防抖和节流函数是前端开发常用的两种函数优化技巧。它们的作用是对频繁触发的函数进行优化,减少资源的消耗和提高性能。 防抖函数的作用是对于频繁触发的函数,只在最后一次触发后的一段时间内执行该函数,而忽略前面的触发。具体实现如下: ```javascript function debounce(func, delay) { let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); }, delay); }; } ``` 使用防抖函数可以有效地解决一些频繁触发的问题,比如输入框输入内容时的实时搜索功能。通过设置适当的延迟时间,可以避免在每次输入时都进行搜索,只在用户停止输入一段时间后进行搜索,减少了无用的搜索请求。 节流函数的作用是对于频繁触发的函数,限制其执行频率,例如每隔一定的时间间隔才执行一次。具体实现如下: ```javascript function throttle(func, delay) { let timer; return function (...args) { if (!timer) { timer = setTimeout(() => { func.apply(this, args); timer = null; }, delay); } }; } ``` 节流函数常用于一些高频触发的事件,比如页面的滚动事件。通过限制处理函数的执行频率,可以减少过多的计算和渲染操作,提升页面的性能和流畅度。 综上所述,防抖函数和节流函数都是为了优化频繁触发的函数而存在的。防抖函数通过延迟执行函数,减少了不必要的函数执行次数。节流函数通过限制函数执行的频率,降低了函数的触发次数。这两种函数都能提高页面的性能和用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值