盘点那些 JS 手写题

简介: 盘点那些 JS 手写题

盘点那些 JS 手写题

1. JS 基础

1. 手写 Object.create

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__ proto __

「语法」

// 返回一个新对象,带着指定的原型对象和属性。
Object.create(proto,[propertiesObject])
  • proto:新创建对象的原型对象,必须为null或者原始包装对象,否则会抛出异常
  • propertiesObject:可选参数,需要是一个对象,该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符

「实现」

function createObject(proto, propertiesObject = {}) {
  // 类型检验
  if (!['object', 'function'].includes(typeof proto)) {
    throw new TypeError("proto必须为对象或者函数");
  }
  function f() {
    for(const key in propertiesObject) {
      if (propertiesObject[key]['enumerable']) {
        this[key] = propertiesObject[key]['value'];
      }
    }
  }
  f.prototype = proto
  return new f();
}

2. 手写 instanceof 方法

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

「语法」

object instanceof constructor

「实现」

function myInstanceof(obj, objType) {
  // 首先用typeof来判断基础数据类型,如果是,直接返回false
  if (obj === null || typeof obj !== 'object') return false;
  // getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
  let proto = Object.getPrototypeOf(obj);
  //循环往下寻找,直到找到相同的原型对象
  while(proto !== null) {
    //找到相同原型对象,返回true
    if (proto === objType.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
}

3. 手写 new 操作符

「语法」

new constructor[([arguments])]
  • constructor:一个指定对象实例的类型的类或函数。
  • arguments:一个用于被 constructor 调用的参数列表。

「描述」

new 关键字会进行如下的操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this

「实现」

function myNew() {
  const constructor = Array.prototype.shift.call(arguments);
  // 判断第一个参数是否为构造函数
  if (typeof constructor !== 'function') {
    throw new TypeError('第一个参数不是构造函数');
  }
  // 新建一个空对象,对象的原型为构造函数的 prototype 对象
  const newObject = Object.create(constructor.prototype);
  // 将 this 指向新建对象,并执行函数
  const result = constructor.call(newObject, arguments);
  // 判断返回结果
  return result && ['object', 'function'].includes(typeof result) ? result : newObject;
}

4. 手写一个通用的类型检测方法

「实现」

function getType(obj) {
  const type = typeof obj;
  // 如果是基本数据类型或函数对象,直接返回
  if (type !== 'object') {
    return type;
  }
  // 对于typeof返回结果是 object 的,再进行如下的判断
  const result = Object.prototype.toString.call(obj).split(' ')[1].replace(']', '');
  return result;
}

5. 手写 call 函数

「语法」

function.call(thisArg, arg1, arg2, ...)
  • thisArg:可选的。在 function 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于「非严格模式」下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装。
  • arg1, arg2, ...:指定的参数列表。
  • 返回值:使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined

「实现」

Function.prototype.myCall = function(context) {
  // 判断调用对象
  if (typeof this !== 'function') {
    throw new TypeError('该对象不存在 myCall 方法');
  }
  const args = [...arguments].slice(1);
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context ? Object(context) : window;
  // 给context新增一个独一无二的属性以免覆盖原有属性
  const key = Symbol();
  // 将调用函数设置成对象的方法
  context[key] = this;
  // 通过隐式绑定的方式调用函数
  const result = context[key](...args);
  // 将属性删除
  delete context[key];
  return result;
}

6. 手写 apply 函数

「语法」

func.apply(thisArg, [argsArray])
  • thisArg:必选的。在 func 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于「非严格模式」下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装。
  • argsArray:可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 nullundefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。
  • 返回值:调用有指定this值和参数的函数的结果。

「实现」

Function.prototype.myApply = function(context) {
  // 判断调用对象
  if (typeof this !== 'function') {
    throw new TypeError('该对象不存在 myApply 方法');
  }
  // 判断 context 是否存在,如果未传入则为 window
  context = context ? Object(context) : global;
  // 给context新增一个独一无二的属性以免覆盖原有属性
  const key = Symbol();
  // 将调用函数设置成对象的方法
  context[key] = this;
  let result = null;
  // 通过隐式绑定的方式调用函数
  if (arguments[1]) {
    result = context[key](...arguments[1]);
  } else {
    result = context[key]();
  }
  // 将属性删除
  delete context[key];
  return result;
}

7. 手写 bind 函数

「语法」

function.bind(thisArg[, arg1[, arg2[, ...]]])
  • thisArg:调用绑定函数时作为 this 参数传递给目标函数的值。如果使用new运算符构造绑定函数,则忽略该值。当使用 bindsetTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果 bind 函数的参数列表为空,或者thisArgnullundefined,执行作用域的 this 将被视为新函数的 thisArg
  • arg1, arg2, ...:当目标函数被调用时,被预置入绑定函数的参数列表中的参数。
  • 返回值:返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。
Function.prototype.myBind = function (context) {
  // 获取参数
  let args = [...arguments].slice(1);
  const fn = this;
  // 判断 context 是否存在,如果未传入则为 window
  context = context ? Object(context) : global;
  function newFn() {
    // 判断是否 new 调用
    if (this instanceof newFn) {
      return new fn(...args, ...arguments)
    }
    return fn.apply(context, [...args,...arguments])
  }
  if (fn.prototype) {
    // 复制源函数的 prototype 给 newFn 一些情况下函数没有prototype,比如箭头函数
    newFn.prototype = Object.create(fn.prototype);
  }
  return newFn;
}

8. 手写 Array.isArray

「语法」

Array.isArray(obj)

Array.isArray() 用于确定传递的值是否是一个 Array

「实现」

Array.myIsArray = function (obj) {
  return Object.prototype.toString.call(obj) === '[object Array]';
}

9. 手写数组的 flat 方法

flat()方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

「语法」

var newArray = arr.flat([depth])
  • depth :可选。指定要提取嵌套数组的结构深度,默认值为 1。
  • 「注」 使用 Infinity,可展开任意深度的嵌套数组
  • 返回值:一个包含将数组与子数组中所有元素的新数组。

「实现」

// 递归
function myFlat(arr, depth = 1) {
  if (!Array.isArray(arr) || depth <= 0) {
    return arr;
  }
  return arr.reduce((prev, cur) => {
    if (Array.isArray(cur)) {
      return prev.concat(myFlat(cur, depth - 1));
    }
    return prev.concat(cur);
  }, []);
}
// 非递归
function myFlat(arr, depth = 1) {
  let result = arr;
  while(depth > 0) {
    let hasArray = false;
    result = result.reduce((prev, cur) => {
      if (Array.isArray(cur)) {
        hasArray = true;
      }
      return prev.concat(cur);
    }, []);
    if (!hasArray) break;
    depth --;
  }
  return result;
}

10. 手写数组的 push 方法

「语法」

arr.push(element1, ..., elementN)
  • elementN:被添加到数组末尾的元素
  • 返回值:当调用该方法时,新的 length 属性值将被返回。

「实现」

Array.prototype.myPush = function() {
  for(let i = 0; i < arguments.length; i++) {
    this[this.length] = arguments[i];
    // 处理类数组的长度
    if (!Array.isArray(this)) {
      this.length ++;
    }
  }
  return this.length;
}

11. 手写数组的 filter方法

「语法」

var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])
  • callback:用来测试数组的每个元素的函数。返回 true 表示该元素通过测试,保留该元素,false 则不保留。它接受以下三个参数:
  • element:数组中当前正在处理的元素。
  • index:可选。正在处理的元素在数组中的索引。
  • array:可选。调用了 filter 的数组本身。
  • thisArg:可选。执行 callback 时,用于 this 的值。

「实现」

2Array.prototype.myFilter = function(fn, thisArg) {
  // 判断第一个参数是否为函数
  if (typeof fn !== 'function') {
    throw new TypeError('fn 不是一个函数');
  }
  // 确定回调函数的 this 指向
  let context = thisArg || window;
  const result = [];
  for(let i = 0; i < this.length; i++) {
    if (fn.call(context, this[i], i, this)) {
      result.push(this[i]);
    }
  }
  return result;
}

12. 手写数组的 map 方法

「语法」

var new_array = arr.map(function callback(currentValue[, index[, array]]) {
 // Return element for new_array 
}[, thisArg])
  • callback:生成新数组元素的函数,使用三个参数:
  • currentValuecallback 数组中正在处理的当前元素。
  • index:可选。callback 数组中正在处理的当前元素的索引。
  • array:可选。map 方法调用的数组。
  • thisArg:可选。执行 callback 函数时值被用作this
  • 返回值:一个由原数组每个元素执行回调函数的结果组成的新数组。

「实现」

Array.prototype.myMap = function(fn, thisArg) {
   // 判断第一个参数是否为函数
  if (typeof fn !== 'function') {
    throw new TypeError('fn 不是一个函数');
  }
  // 确定回调函数的 this 指向
  let context = thisArg || window;
  const result = [];
  for(let i = 0; i < this.length; i++) {
    result.push(fn.call(context, this[i], i, this));
  }
  return result;
}

13. 手写字符串的 repeat 方法

「语法」

str.repeat(count)
  • count:介于 0+Infinity 之间的整数。表示在新构造的字符串中重复了多少遍原字符串。
  • 返回值:包含指定字符串的指定数量副本的新字符串。

「实现」

String.prototype.myRepeat = function(count) {
  if (count < 0) {
    throw new RangeError('repeat count must be non-negative');
  }
  if (count === Infinity) {
    throw new RangeError('repeat count must be less than infinity');
  }
  let result = '';
  for(let i = 0; i < count; i++) {
    result += this;
  }
  return result;
}

14. 手写 Promise

15. 手写 Promise.all

「语法」

Promise.all(iterable);
  • iterable:一个可迭代对象,如 ArrayString
  • 返回值
  • 如果传入的参数是一个空的可迭代对象,则返回一个「已完成(already resolved)」状态的 Promise
  • 如果传入的参数不包含任何 promise,则返回一个「异步完成(asynchronously resolved)」Promise。注意:Google Chrome 58 在这种情况下返回一个「已完成(already resolved)」状态的 Promise
  • 其它情况下返回一个「处理中(pending)」Promise。这个返回的 promise 之后会在所有的 promise 都完成或有一个 promise 失败时「异步」地变为完成或失败。返回值将会按照参数内的 promise 顺序排列,而不是由调用 promise 的完成顺序决定。

「实现」

function promiseAll(promiseList) {
  return new Promise((resolve, reject) => {
    if(!Array.isArray(promiseList)){
      throw new TypeError(`argument must be a array`)
    }
    let resolvedCounter = 0;
    const result = [];
    for(let i = 0; i < promiseList.length; i++) {
      Promise.resolve(promiseList[i]).then(value => {
        resolvedCounter ++;
        result[i] = value;
        if (resolvedCounter === promiseList.length) {
          resolve(result);
        }
      }, reason => {
        reject(reason);
      })
    }
  })
}

16. 手写 Promise.race

「语法」

Promise.race(iterable);
  • iterable:可迭代对象,类似Array
  • 返回值:一个「待定的」Promise只要给定的迭代中的一个promise解决或拒绝,就采用第一个promise的值作为它的值,从而「异步」地解析或拒绝(一旦堆栈为空)。

「实现」

function promiseRace(promiseList) {
  return new Promise((resolve, reject) => {
    for (let i = 0, len = promiseList.length; i < len; i++) {
      promiseList[i].then(resolve, reject)
    }
  })
}

17. 手写 Promise.any

「语法」

Promise.any(iterable);
  • iterable:一个可迭代的对象, 例如 Array。
  • 返回值
  • 如果传入的参数是一个空的可迭代对象,则返回一个 「已失败(already rejected)」 状态的 Promise
  • 如果传入的参数不包含任何 promise,则返回一个 「异步完成」「asynchronously resolved」)的 Promise
  • 其他情况下都会返回一个「处理中(pending)」Promise。只要传入的迭代对象中的任何一个 promise 变成成功(resolve)状态,或者其中的所有的 promises 都失败,那么返回的 promise 就会 「异步地」(当调用栈为空时) 变成成功/失败(resolved/reject)状态。

「实现」

function promiseAny(promiseList) {
  return new Promise((resolve, reject) => {
    if(!Array.isArray(promiseList)){
      throw new TypeError(`argument must be a array`)
    }
    let rejectedCounter = 0;
    const result = [];
    for(let i = 0; i < promiseList.length; i++) {
      Promise.resolve(promiseList[i]).then(resolve, reason => {
        result[i] = reason;
        rejectedCounter ++;
        if (rejectedCounter === promiseList.length) reject(new Error('All promises were rejected'));
      })
    }
  })
}

18. 手写 Promise.allSettled

Promise.allSettled(iterable);
  • iterable:一个可迭代的对象,例如Array,其中每个成员都是Promise
  • 返回值:一旦所指定的 promises 集合中每一个 promise 已经完成,无论是成功的达成或被拒绝,「未决议的」Promise将被「异步」完成。那时,所返回的 promise 的处理器将传入一个数组作为输入,该数组包含原始 promises 集中每个 promise 的结果。对于每个结果对象,都有一个 status 字符串。如果它的值为 fulfilled,则结果对象上存在一个 value 。如果值为 rejected,则存在一个 reason 。value(或 reason )反映了每个 promise 决议(或拒绝)的值。

「实现」

function promiseAllSettled(promiseList) {
  return new Promise((resolve, reject) => {
    if(!Array.isArray(promiseList)){
      throw new TypeError(`argument must be a array`)
    }
    let counter = 0;
    const result = [];
    for(let i = 0; i < promiseList.length; i++) {
      Promise.resolve(promiseList[i]).then(value => {
        result[i] = { status: 'fulfilled', value };
        counter ++;
        if (counter === promiseList.length) resolve(result);
      }, reason => {
        result[i] = { status: 'rejected', reason };
        counter ++;
        if (counter === promiseList.length) resolve(result);
      })
    }
  })
}

2. 场景应用

1. 手写防抖函数

函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。

function debounce(fn, wait) {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn(...args);
    }, wait);
  }
}

2. 手写节流函数

函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。

function throttle(fn, delay) {
  let timer = null;
  return (...args) => {
    if (timer) return;
    timer = setTimeout(() => {
      fn(...args);
      timer = null;
    }, delay);
  }
}

3. 实现 AJAX 请求

AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。

创建AJAX请求的步骤:

  • 「创建一个 XMLHttpRequest 对象。」
  • 在这个对象上「使用 open 方法创建一个 HTTP 请求」,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
  • 在发起请求前,可以为这个对象「添加一些信息和监听函数」。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
  • 当对象的属性和监听函数设置完成后,最后调「用 sent 方法来向服务器发起请求」,可以传入参数作为发送的数据体。
function myAJAX(data, method = 'GET', url, isAsync) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    // 创建 Http 请求
    xhr.open(method, url, isAsync);
    // 设置监听函数
    xhr.onreadystatechange = function() {
      if (this.readyState !== 4) return;
      // 当请求成功时
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    }
    // 设置请求失败时的监听函数
    xhr.onerror = function() {
      reject(new Error(this.statusText));
    };
    // 设置请求头信息
    xhr.responseType = "json";
    xhr.setRequestHeader("Accept", "application/json");
    // 发送 Http 请求
    xhr.send(JSON.stringify(data));
  });
}

4. 手写深拷贝函数

function deepCopy(obj) {
  if (!obj || typeof obj !== 'object') {
    throw new TypeError('the parameter are not object');
  }
  const newObj = Array.isArray(obj) ? [] : {};
  for(let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
    }
  }
  return newObj;
}

5. 实现 add(1)(2)(3)

函数柯里化概念:柯里化(Currying)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。

  • 参数长度固定
function add(m) {
  var temp = function (n) {
    return add(m + n);
  }
  temp.toString = function () {
    return m;
  }
  return temp;
};
console.log(add(3)(4)(5).toString()); // 12
console.log(add(3)(6)(9)(25).toString()); // 43

对于add(3)(4)(5),其执行过程如下:

  1. 先执行add(3),此时m=3,并且返回temp函数;
  2. 执行temp(4),这个函数内执行add(m+n),n是此次传进来的数值4,m值还是上一步中的3,所以add(m+n)=add(3+4)=add(7),此时m=7,并且返回temp函数
  3. 执行temp(5),这个函数内执行add(m+n),n是此次传进来的数值5,m值还是上一步中的7,所以add(m+n)=add(7+5)=add(12),此时m=12,并且返回temp函数
  4. 由于后面没有传入参数,等于返回的temp函数不被执行而是打印,了解JS的朋友都知道对象的toString是修改对象转换字符串的方法,因此代码中temp函数的toString函数return m值,而m值是最后一步执行函数时的值m=12,所以返回值是12。
  • 参数长度不固定
function add() {
  let val = [...arguments].reduce((prev, next) => prev + next);
  return function temp() {
    if (arguments.length) {
      val = [...arguments].reduce((prev, next) => (prev + next), val);
      return temp;
    } else {
      return val;
    }
  }
}
console.log(add(1, 2)(3, 4, 5)(5)(5, 5)()); // 30
// or
function add() {
  let val = [...arguments].reduce((prev, next) => prev + next);
  function temp() {
    let newVal = [...arguments].reduce((prev, next) => prev + next);
    return add(val, newVal);
  }
  temp.toString = function() {
    return val;
  }
  return temp;
}
console.log(add(1, 2)(3, 4, 5)(5)(5, 5).toString()); // 30

6. 将js对象转化为树形结构

// 转换前:
source = [{
  id: 1,
  pid: 0,
  name: 'body'
}, {
  id: 2,
  pid: 1,
  name: 'title'
}, {
  id: 3,
  pid: 2,
  name: 'div'
}]
// 转换为: 
tree = [{
      id: 1,
      pid: 0,
      name: 'body',
      children: [{
          id: 2,
          pid: 1,
          name: 'title',
          children: [{
            id: 3,
            pid: 1,
            name: 'div'
          }]
        }
      }]

代码实现:

function jsonToTree(data) {
  // 初始化结果数组,并判断输入数据的格式
  let result = []
  if(!Array.isArray(data)) {
    return result
  }
  // 使用map,将当前对象的id与当前对象对应存储起来
  let map = {};
  data.forEach(item => {
    map[item.id] = item;
  });
  // 
  data.forEach(item => {
    let parent = map[item.pid];
    if(parent) {
      (parent.children || (parent.children = [])).push(item);
    } else {
      result.push(item);
    }
  });
  return result;
}

7. 解析 URL Params 为对象

function parseParam(url) {
  // 将 ? 后面的字符串取出来
  const paramsStr = url.split('?')[1]; 
  // 将字符串以 & 分割后存到数组中
  const paramsArr = paramsStr.split('&'); 
  const paramsObj = {};
  // 将 params 存到对象中
  paramsArr.forEach(param => {
    // 处理有 value 的参数
    if (param.includes('=')) { 
      // 分割 key 和 value
      let [key, val] = param.split('='); 
      // 解码
      val = decodeURIComponent(val); 
      // 判断是否转为数字
      val = /^\d+$/.test(val) ? parseFloat(val) : val; 
      // 如果对象有 key,则添加一个值
      if (paramsObj.hasOwnProperty(key)) { 
        paramsObj[key] = [].concat(paramsObj[key], val);
      } else { // 如果对象没有这个 key,创建 key 并设置值
        paramsObj[key] = val;
      }
    } else { // 处理没有 value 的参数
      paramsObj[param] = true;
    }
  })
  return paramsObj;
}
let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
console.log(parseParam(url));
/* 结果
{ user: 'anonymous',
  id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型
  city: '北京', // 中文需解码
  enabled: true, // 未指定值得 key 约定为 true
}
*/

8. 实现发布-订阅模式

class EventCenter {
  // 1. 定义事件容器,用来装事件数组
  handlers = {}
  /**
   * 2. 注册事件
   * @param {string} type 事件名
   * @param {Function} handler 事件处理方法 
   */
  addEventListener(type, handler) {
    // 创建新数组容器
    if (!this.handlers[type]) {
      this.handlers[type] = [];
    }
    // 存入事件处理方法
    this.handlers[type].push(handler);
  }
  /**
   * 3. 触发事件
   * @param {string} type 事件名
   * @param {Array} params 方法参数列表
   */
  dispatchEvent(type, params) {
    // 如果没有注册该事件就抛出异常
    if(!this.handlers[type]) {
      throw new Error('The event is not registered');
    }
    // 触发事件
    this.handlers[type].forEach(handler => handler(...params));
  }
  /**
   * 4. 移除事件  
   * @param {string} type 事件名
   * @param {Function} handler 事件处理方法
   */
  removeEventListener(type, handler) {
    // 如果没有注册该事件就抛出异常
    if (!this.handlers[type]) {
      throw new Error('The event does not exist');
    }
    if (!handler) {
      // 移除该事件的所有处理方法
      delete this.handlers[type];
    } else {
      // 移除事件
      this.handlers[type] = this.handlers[type].filter(el => el !== handler);
      if (this.handlers[type].length === 0) {
        delete this.handlers[type];
      }
    }
  }
}

9. 实现每隔一秒打印 1,2,3,4

  • 利用闭包
for (var i = 0; i < 5; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);
    }, i * 1000);
  })(i);
}
  • 利用块级作用域
for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
}

10. 使用 setTimeout 实现 setInterval

在平常开发中,我们很少用到setInterval。因为在事件循环中,setInterval的延迟可能会积累,所以setTimeoutsetInterval要准确。接下来就用setTimeout来模拟实现setInterval

针对 setInterval 的这个缺点,我们可以使用 setTimeout 递归调用来模拟 setInterval,这样我们就确保了只有一个事件结束了,我们才会触发下一个定时器事件,这样解决了 setInterval 的问题。

实现思路是使用递归函数,不断地去执行 setTimeout 从而达到 setInterval 的效果

function mySetInterval(fn, timeout) {
  const timer = {
    flag: true
  }
  function foo() {
    if (timer.flag) {
      fn();
      setTimeout(foo, timeout)
    }
  }
  setTimeout(foo, timeout);
  return timer;
}

11. 手写一个 sleep / delay 函数

sleep 函数既是面试中常问到的一道代码题,也是日常工作,特别是测试中常用的一个工具函数。

const sleep = (seconds) => new Promise(resolve => setTimeout(resolve, seconds))
function delay (func, seconds, ...args) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      Promise.resolve(func(...args)).then(resolve)
    }, seconds)
  })
}

12. 对字符串进行编码压缩

例如:

  • 输入: 'aaaabbbccd'
  • 输出: 'a4b3c2d1',代表 a 连续出现四次,b连续出现三次,c连续出现两次,d连续出现一次

「实现」

function encode(str) {
  if (str.length === 0) return '';
  const result = [str[0]];
  let index = 1, count = 1;
  for(let i = 1; i < str.length; i++) {
    if (result[index - 1] === str[i]) {
      count ++;
    } else {
      result[index ++] = '' + count;
      count = 1;
      result[index ++] = str[i];
    }
  }
  result[index ++] = '' + count;
  return result.join('');
}

但是如果只出现一次,不编码数字,如 aaab -> a3b,那又如何呢?

function encode(str) {
  if (str.length === 0) return '';
  const result = [str[0]];
  let index = 1, count = 1;
  for(let i = 1; i < str.length; i++) {
    if (result[index - 1] === str[i]) {
      count ++;
    } else {
      if (count > 1) {
        result[index ++] = '' + count;
      }
      count = 1;
      result[index ++] = str[i];
    }
  }
  if (count > 1) {
    result[index ++] = '' + count;
  }
  return result.join('');
}

13. 实现 JSONP

JSONP,全称 JSON with Padding,为了解决跨域的问题而出现。虽然它只能处理 GET 跨域,虽然现在基本上都使用 CORS 跨域,但仍然要知道它。

JSONP 基于两个原理:

  1. 动态创建 script,使用 script.src 加载请求跨过跨域
  2. script.src 加载的脚本内容为 JSONP: 即 PADDING(JSON) 格式

「实现」

function jsonp ({ url, onData, params }) {
  const script = document.createElement('script')
  // 一、为了避免全局污染,使用一个随机函数名
  const cbFnName = `JSONP_PADDING_${Math.random().toString().slice(2)}`
  // 二、默认 callback 函数为 cbFnName
  script.src = `${url}?${stringify({ callback: cbFnName, ...params })}`
  // 三、使用 onData 作为 cbFnName 回调函数,接收数据
  window[cbFnName] = onData;
  document.body.appendChild(script)
}
// 发送 JSONP 请求
jsonp({
  url: 'http://localhost:10010',
  params: { id: 10000 },
  onData (data) {
    console.log('Data:', data)
  }
})
相关文章
|
1月前
|
JavaScript 前端开发 算法
js手写题:第二章
js手写题:第二章
32 0
|
1月前
|
JavaScript 前端开发 算法
js手写题:第一章
js手写题:第一章
24 0
|
10月前
|
前端开发 JavaScript UED
为什么选择 Next.js 框架?
为什么选择 Next.js 框架?
167 0
|
10月前
|
JavaScript 前端开发 容器
JS 原生手写轮播图
JS 原生手写轮播图
137 0
|
11月前
|
JavaScript
js手写new操作符
手写new核心代码
|
存储 消息中间件 设计模式
JS_手写实现
CSS重点概念精讲 JS_基础知识点精讲 网络通信_知识点精讲
|
JavaScript
看看这些常见的js手写题你会写多少个
看看这些常见的js手写题你会写多少个
|
JavaScript
js手写几个常见的面试题
面试中常见的几道经典题
108 0
|
JavaScript 前端开发
15个手写JS,巩固你的JS基础(面试高频)
15个手写JS,巩固你的JS基础(面试高频)
121 0
15个手写JS,巩固你的JS基础(面试高频)
|
Web App开发 前端开发 JavaScript
day02_js学习笔记_01_js的简介、js的基本语法
day02_js学习笔记_01_js的简介、js的基本语法 ============================================================================= =========================================...
1544 0