「2022 最新最全」高频的前端面试题汇总 javascript 手写篇(练完吊打面试官)

前言

作为一个程序员,代码能力毋庸置疑是非常非常重要的,前端面试的时候也经常会遇到手写 XXX 实现之类的问题,准备把一些常见的题目梳理总结一下,既能加深原生 js 基础,也方便以后复习这一类手写面试题的时候,找起来方便,节省时间。

1 手写防抖函数

原理:事件被触发 n 秒后再执行回调,n 秒内又被触发,则重新计时(类似 lol 按 b 键回城)。

// 函数防抖的实现
function debounce(fn, wait) {
  let timer = null;

  return function () {
    let context = this,
      args = arguments;

    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
}

适用场景:

  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
  • 窗口滚动条的监听:滚动条触发次数太过频繁并且通常只需要获取最后一次的位置

在线运行 / 预览地址

2 手写节流函数

防抖函数原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。(lol 技能冷却)

使用时间戳

// 函数节流的实现;
function throttle(fn, delay) {
  let curTime = Date.now();

  return function () {
    let context = this,
      args = arguments,
      nowTime = Date.now();

    // 如果两次时间间隔超过了指定时间,则执行函数。
    if (nowTime - curTime >= delay) {
      curTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

使用标记

// 函数节流的实现;
const throttle = (fn, delay = 500) => {
  let flag = true;
  return (...args) => {
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(this, args);
      flag = true;
    }, delay);
  };
};

适用场景:

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
  • 缩放场景:监控浏览器 resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

在线运行 / 预览地址

3 手写深拷贝

//使用JSON处理方法
const newObj = JSON.parse(JSON.stringify(Obj));

局限性:

  • 函数对象会被转成 null 、RegExp 会变成{}

  • 会抛弃对象的 constructor,所有的构造函数会指向 Object

  • 对象有循环引用,会报错

//递归实现深拷贝
function deepCopy(obj, dep = 0) {
  if (typeof obj !== "object" || dep < 1) {
    return typeof obj === "object" ? Object.assign({}, obj) : obj;
  }
  let clone = Array.isArray(obj) ? [] : {};
  for (const key in obj) {
    if (Object.hasOwnProperty.call(obj, key)) {
      if (obj[key] && typeof obj[key] === "object") {
        clone[key] = deepCopy(obj[key], dep - 1);
      } else {
        clone[key] = obj[key];
      }
    }
  }
  return clone;
}

实现简单通俗易懂一般能写出这个就 ok 了

在线运行 / 预览地址

4 手写 Object.create

function create(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}

ps:补充一个面试知识点 Object.create(null)可以创建一个不含原型链的纯净 Object 对象

5 手写 instanceof

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

实现思路:

  1. 首先获取类型的原型
  2. 然后获得对象的原型
  3. 然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null
//instanceof的实现
function myInstanceof(left, right) {
  function myInstanceof(left, right) {
    //获取对象原型
    let __proto__ = Object.getPrototypeOf(left),
      prototype = right.prototype; //获取构造函数的prototype
    while (__proto__) {
      if (__proto__ === prototype) {
        return true;
      }
      __proto__ = Object.getPrototypeOf(__proto__);
    }
    return false;
  }
}

在线运行 / 预览地址

6 手写 new 操作符

在调用 new 的过程中会发生四件事情(面试常问)

  1. 在堆去开辟一块内存创建了一个新的空对象
  2. 设置原型,将新对象的原型设置为函数的 prototype 对象。
  3. 让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
function objectFactory(constructor, ...rest) {
  let newObject = null;
  let result = null;
  // 判断参数是否是一个函数
  if (typeof constructor !== "function") {
    console.error("type error");
    return;
  }
  // 新建一个空对象,对象的原型为构造函数的 prototype 对象
  newObject = Object.create(constructor.prototype);
  // 将 this 指向新建对象,并执行函数
  result = constructor.apply(newObject, ...rest);
  // 判断返回对象
  let flag =
    result && (typeof result === "object" || typeof result === "function");
  // 判断返回结果
  return flag ? result : newObject;
}

在线运行 / 预览地址

7 手写 Function.prototype.call 方法

call 函数的实现步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 判断传入上下文对象是否存在,如果不存在,则为 window 。
  3. 处理传入的参数,截取第一个参数后的所有参数。
  4. 将函数作为上下文对象的一个属性。
  5. 使用上下文对象来调用这个方法,并保存返回结果。
  6. 删除刚才新增的属性。
  7. 返回结果。
// call函数实现
Function.prototype.myCall = function (context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 获取参数
  let args = [...arguments].slice(1),
    result = null;
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;
  // 将调用函数设为对象的方法
  context.fn = this;
  // 调用函数
  result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
};

8 手写 Function.prototype.apply 方法

apply 函数的实现步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  3. 将函数作为上下文对象的一个属性。
  4. 判断参数值是否传入
  5. 使用上下文对象来调用这个方法,并保存返回结果。
  6. 删除刚才新增的属性
  7. 返回结果
// apply 函数实现
Function.prototype.myApply = function (context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;
  // 将函数设为对象的方法
  context.fn = this;
  // 调用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }
  // 将属性删除
  delete context.fn;
  return result;
};

9 手写 Function.prototype.bind 方法

bind 函数的实现步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 保存当前函数的引用,获取其余传入参数值。
  3. 创建一个函数返回
  4. 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
// bind 函数实现
Function.prototype.myBind = function (context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  // 获取参数
  var args = [...arguments].slice(1),
    fn = this;
  return function Fn() {
    // 根据调用方式,传入不同绑定值
    return fn.apply(
      this instanceof Fn ? this : context,
      args.concat(...arguments)
    );
  };
};

10 手写函数柯里化

柯里化就是把接受「多个参数」的函数变换成接受一个「单一参数」的函数,并且返回接受「余下参数」返回结果的一种应用。

思路:

判断传递的参数是否达到执行函数的 fn 个数
没有达到的话,继续返回新的函数,并且返回 curry 函数传递剩余参数

//实现1
function curry(fn, args) {
  // 获取函数需要的参数长度
  let length = fn.length;

  args = args || [];

  return function () {
    let subArgs = args.slice(0);

    // 拼接得到现有的所有参数
    for (let i = 0; i < arguments.length; i++) {
      subArgs.push(arguments[i]);
    }

    // 判断参数的长度是否已经满足函数所需参数的长度
    if (subArgs.length >= length) {
      // 如果满足,执行函数
      return fn.apply(this, subArgs);
    } else {
      // 如果不满足,递归返回科里化的函数,等待参数的传入
      return curry.call(this, fn, subArgs);
    }
  };
}
//实现2
function currying(exeFunc) {
  let args = [];
  let currFunc = function (...rest) {
    args.push(...rest);
    if (rest.length <= 0) {
      return exeFunc(args);
    } else {
      return currFunc;
    }
  };
  return currFunc;
}
// es6 实现
function curry(fn, ...args) {
  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

在线运行 / 预览地址

11 手写 AJAX 请求

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

创建 AJAX 请求的步骤:

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

在线运行 / 预览地址

12 手写 A+规范的 promise

promise 是 Es6 新加的一个对象手写 promise 能帮助更好的理解

var PromisePolyfill = (function () {
  // 和reject不同的是resolve需要尝试展开thenable对象
  function tryToResolve(value) {
    if (this === value) {
      // 主要是防止下面这种情况
      // let y = new Promise(res => setTimeout(res(y)))
      throw TypeError("Chaining cycle detected for promise!");
    }

    // 根据规范2.32以及2.33 对对象或者函数尝试展开
    // 保证S6之前的 polyfill 也能和ES6的原生promise混用
    if (
      value !== null &&
      (typeof value === "object" || typeof value === "function")
    ) {
      try {
        // 这里记录这次then的值同时要被try包裹
        // 主要原因是 then 可能是一个getter, 也也就是说
        //   1. value.then可能报错
        //   2. value.then可能产生副作用(例如多次执行可能结果不同)
        var then = value.then;

        // 另一方面, 由于无法保证 then 确实会像预期的那样只调用一个onFullfilled / onRejected
        // 所以增加了一个flag来防止resolveOrReject被多次调用
        var thenAlreadyCalledOrThrow = false;
        if (typeof then === "function") {
          // 是thenable 那么尝试展开
          // 并且在该thenable状态改变之前this对象的状态不变
          then.bind(value)(
            // onFullfilled
            function (value2) {
              if (thenAlreadyCalledOrThrow) return;
              thenAlreadyCalledOrThrow = true;
              tryToResolve.bind(this, value2)();
            }.bind(this),

            // onRejected
            function (reason2) {
              if (thenAlreadyCalledOrThrow) return;
              thenAlreadyCalledOrThrow = true;
              resolveOrReject.bind(this, "rejected", reason2)();
            }.bind(this)
          );
        } else {
          // 拥有then 但是then不是一个函数 所以也不是thenable
          resolveOrReject.bind(this, "resolved", value)();
        }
      } catch (e) {
        if (thenAlreadyCalledOrThrow) return;
        thenAlreadyCalledOrThrow = true;
        resolveOrReject.bind(this, "rejected", e)();
      }
    } else {
      // 基本类型 直接返回
      resolveOrReject.bind(this, "resolved", value)();
    }
  }

  function resolveOrReject(status, data) {
    if (this.status !== "pending") return;
    this.status = status;
    this.data = data;
    if (status === "resolved") {
      for (var i = 0; i < this.resolveList.length; ++i) {
        this.resolveList[i]();
      }
    } else {
      for (i = 0; i < this.rejectList.length; ++i) {
        this.rejectList[i]();
      }
    }
  }

  function Promise(executor) {
    if (!(this instanceof Promise)) {
      throw Error("Promise can not be called without new !");
    }

    if (typeof executor !== "function") {
      // 非标准 但与Chrome谷歌保持一致
      throw TypeError("Promise resolver " + executor + " is not a function");
    }

    this.status = "pending";
    this.resolveList = [];
    this.rejectList = [];

    try {
      executor(tryToResolve.bind(this), resolveOrReject.bind(this, "rejected"));
    } catch (e) {
      resolveOrReject.bind(this, "rejected", e)();
    }
  }

  Promise.prototype.then = function (onFullfilled, onRejected) {
    // 返回值穿透以及错误穿透, 注意错误穿透用的是throw而不是return,否则的话
    // 这个then返回的promise状态将变成resolved即接下来的then中的onFullfilled
    // 会被调用, 然而我们想要调用的是onRejected
    if (typeof onFullfilled !== "function") {
      onFullfilled = function (data) {
        return data;
      };
    }
    if (typeof onRejected !== "function") {
      onRejected = function (reason) {
        throw reason;
      };
    }

    var executor = function (resolve, reject) {
      setTimeout(
        function () {
          try {
            // 拿到对应的handle函数处理this.data
            // 并以此为依据解析这个新的Promise
            var value =
              this.status === "resolved"
                ? onFullfilled(this.data)
                : onRejected(this.data);
            resolve(value);
          } catch (e) {
            reject(e);
          }
        }.bind(this)
      );
    };

    // then 接受两个函数返回一个新的Promise
    // then 自身的执行永远异步与onFullfilled/onRejected的执行
    if (this.status !== "pending") {
      return new Promise(executor.bind(this));
    } else {
      // pending
      return new Promise(
        function (resolve, reject) {
          this.resolveList.push(executor.bind(this, resolve, reject));
          this.rejectList.push(executor.bind(this, resolve, reject));
        }.bind(this)
      );
    }
  };

  // for prmise A+ test
  Promise.deferred = Promise.defer = function () {
    var dfd = {};
    dfd.promise = new Promise(function (resolve, reject) {
      dfd.resolve = resolve;
      dfd.reject = reject;
    });
    return dfd;
  };

  // for prmise A+ test
  if (typeof module !== "undefined") {
    module.exports = Promise;
  }

  return Promise;
})();

PromisePolyfill.all = function (promises) {
  return new Promise((resolve, reject) => {
    const result = [];
    let cnt = 0;
    for (let i = 0; i < promises.length; ++i) {
      promises[i].then((value) => {
        cnt++;
        result[i] = value;
        if (cnt === promises.length) resolve(result);
      }, reject);
    }
  });
};

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

在线运行 / 预览地址

13 手写千位分隔符

千位分隔符是处理日期常用的方式

function parseToMoney(num) {
  num = parseFloat(num.toFixed(3));
  let [integer, decimal] = String.prototype.split.call(num, ".");
  integer = integer.replace(/\d(?=(\d{3})+$)/g, "$&,");
  return integer + "." + (decimal ? decimal : "");
}

14 手写观察者模式

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。vue 双向绑定就是使用了观察者模式。

class Subject {
  constructor() {
    this.ObserverList = [];
  }
  add(observer) {
    this.ObserverList.push(observer);
  }
  remove(observer) {
    this.ObserverList = this.ObserverList.filter((item) => item !== observer);
  }
  notify(...arg) {
    this.ObserverList.forEach((cb) => cb.update(...arg));
  }
}

class Observer {
  constructor(name) {
    this.name = name;
  }
  update() {
    console.log(this.name);
  }
}

在线运行 / 预览地址

15 定义发布/订阅

发布/订阅模式:基于一个主题/事件通道,希望接收通知的对象(称为 subscriber)通过自定义事件订阅主题,被激活事件的对象(称为 publisher)通过发布主题事件的方式被通知。vue2 中的全局事件总线就是使用这种设计模式。

/**
 * 发布/订阅模式组件
 * @author  wilton
 */

// 定义发布/订阅类
class Pubsub {
  constructor() {
    this.topics = {};
    this.subUid = -1;
  }

  // 发布事件
  publish(topic, args) {
    if (!this.topics[topic]) return false;

    let subscribers = this.topics[topic];
    let len = subscribers ? subscribers.length : 0;

    while (len--) {
      subscribers[len].func(topic, args);
    }

    return this;
  }

  // 订阅事件
  subscribe(topic, func) {
    if (!this.topics[topic]) this.topics[topic] = [];

    let token = (++this.subUid).toString();
    this.topics[topic].push({
      token: token,
      func: func,
    });

    return token;
  }

  // 取消订阅
  unsubscribe(token) {
    for (let m in topics) {
      if (topics[m]) {
        for (let i = 0; i < topics[m].length; i++) {
          if (topics[m][i].token == token) {
            topics[m].splice(i, 1);
            return token;
          }
        }
      }
    }
    return this;
  }
}

在线运行 / 预览地址

16 使用 setTimeout 实现 setInterval

setInterval 的作用是每隔一段指定时间执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是每隔一段时间将事件加入事件队列中去,只有当执行栈为空的时候,才能去从事件队列中取出事件执行。所以可能会出现这样的情况,就是当前执行栈执行的时间很长,导致事件队列里边积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,因此就不能到间隔一段时间执行的效果。
针对 setInterval 的这个缺点,我们可以使用 setTimeout 递归调用来模拟 setInterval,这样我们就确保了只有一个事件结束了,我们才会触发下一个定时器事件,这样解决了 setInterval 的问题。

function mySetInterval(fn, timeout) {
  setTimeout(() => {
    fn();
    mySetInterval(fn, timeout);
  }, timeout);
}

17 将 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;
}

在线运行 / 预览地址

18 实现 sleep 函数

//使用Promise封装setTimeou
function timeout(delay) {
  return new Promise((resolve) => {
    setTimeout(resolve, delay);
  });
  //使用时间戳阻塞线程
  function sleep(time, fn) {
    let curDate = Date.now();
    while (Date.now() - curDate < time);
    fn();
  }
  sleep(3000, () => {
    console.log(1);
  });
}

19 手写 Es6 的 flat(数组扁平化)

数组扁平化就是将 [1, [2, [3]]] 这种多层的数组拍平成一层 [1, 2, 3]。

//数组扁平化
let arr = [[22, [1, 2, [5, 8], [9, 2]], [2, 7, 1, [9, 0, [1, 6]]]], [2]];

//1 reduce + concat
function flatten(arr, dep) {
  return dep > 1
    ? arr.reduce(
        (pre, cur) =>
          pre.concat(Array.isArray(cur) ? flatten(cur, dep - 1) : cur),
        []
      )
    : Array.prototype.slice.call(arr);
}

//2 some + concat + 结构
function _flatten(arr, dep = 0) {
  if (dep < 1) {
    return arr.slice();
  }
  while (dep && arr.some((item) => Array.isArray(item))) {
    arr = [].concat(...arr);
    dep--;
  }
  return arr;
}

//3 pust + apply
function __flatten(arr, dep = 0) {
  if (dep < 1) {
    return Array.prototype.slice.call(arr);
  }
  let result = [];
  arr.forEach((element) =>
    Array.isArray(element)
      ? result.push.apply(result, __flatten(element, dep - 1))
      : result.push(element)
  );
  return result;
}

//4 number only
function ___flatten(arr) {
  return arr
    .toString()
    .split(",")
    .map((item) => +item);
}

//5 stack
function ____flatten(arr, dep = 0) {
  let stack = [...arr];
  let result = [];
  while (stack.length) {
    let data = stack.pop();
    if (Array.isArray(data)) {
      stack.push(...data);
    } else {
      result.push(data);
    }
  }
  return result.reverse();
}

在线运行 / 预览地址

20 手写 Object.assign

浅拷贝

Object.myAssign = function (target, ...source) {
  if (target == null) {
    throw new TypeError("Cannot convert undefined or null to object");
  }
  let ret = Object(target);
  source.forEach(function (obj) {
    if (obj != null) {
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          ret[key] = obj[key];
        }
      }
    }
  });
  return ret;
};

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

var 作用域问题会导致一直输出最后一个数字

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

22js 解析 URL Params 为对象

let url =
  "http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled";
parseParam(url);
/* 结果
{ user: 'anonymous',
  id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型
  city: '北京', // 中文需解码
  enabled: true, // 未指定值得 key 约定为 true
}
*/

function parseParam(url) {
  const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
  const paramsArr = paramsStr.split("&"); // 将字符串以 & 分割后存到数组中
  let paramsObj = {};
  // 将 params 存到对象中
  paramsArr.forEach((param) => {
    if (/=/.test(param)) {
      // 处理有 value 的参数
      let [key, val] = param.split("="); // 分割 key 和 value
      val = decodeURIComponent(val); // 解码
      val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字

      if (paramsObj.hasOwnProperty(key)) {
        // 如果对象有 key,则添加一个值
        paramsObj[key] = [].concat(paramsObj[key], val);
      } else {
        // 如果对象没有这个 key,创建 key 并设置值
        paramsObj[key] = val;
      }
    } else {
      // 处理没有 value 的参数
      paramsObj[param] = true;
    }
  });

  return paramsObj;
}

23 实现一个 JSON.stringify

  • Boolean | Number| String 类型会自动转换成对应的原始值。
  • undefined、任意函数以及 symbol,会被忽略(出现在非数组对象的属性值中时),或者被转换成 null(出现在数组中时)。
  • 不可枚举的属性会被忽略
  • 如果一个对象的属性值通过某种间接的方式指回该对象本身,即循环引用,属性也会被忽略。
  • Function 会变成 null RegExp 对象会变成{}
function jsonStringify(obj) {
  let type = typeof obj;
  if (type !== "object") {
    if (/string|undefined|function/.test(type)) {
      obj = '"' + obj + '"';
    }
    return String(obj);
  } else {
    let json = [];
    let arr = Array.isArray(obj);
    for (let k in obj) {
      let v = obj[k];
      let type = typeof v;
      if (/string|undefined|function/.test(type)) {
        v = '"' + v + '"';
      } else if (type === "object") {
        v = jsonStringify(v);
      }
      json.push((arr ? "" : '"' + k + '":') + String(v));
    }
    return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}");
  }
}

在线运行 / 预览地址

24 实现 JSON.parse

let json = '{"name":"cxk", "age":25}';
let obj = eval("(" + json + ")");

使用 eval 实现简单但很容易被 xss 攻击

25 手写 js 的继承

继承有很多方式这里只归纳最优的两种寄生组合式和 es6 的 class 继承

//寄生组合式继承
function inheritPrototype(subType, superType) {
  // 创建对象,创建父类原型的一个副本
  let prototype = Object.create(superType.prototype);
  // constructor属性为子类构造函数
  prototype.constructor = subType;
  // 指定对象,将新创建的对象赋值给子类的原型
  subType.prototype = prototype;
}
//es6 class的extends
class Rectangle {
  // constructor
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  // Getter
  get area() {
    return this.calcArea();
  }
  // Method
  calcArea() {
    return this.height * this.width;
  }
}

const rectangle = new Rectangle(40, 20);
console.log(rectangle.area);
// 输出 800
// 继承
class Square extends Rectangle {
  constructor(len) {
    // 子类没有this,必须先调用super
    super(len, len);

    // 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
    this.name = "SquareIng";
  }
  get area() {
    return this.height * this.width;
  }
}
const square = new Square(20);
console.log(square.area);
// 输出 400

//extends继承的核心代码如下,其实和寄生组合式继承方式一样
function _inherits(subType, superType) {
  // 创建对象,创建父类原型的一个副本
  // 增强对象,弥补因重写原型而失去的默认的constructor 属性
  // 指定对象,将新创建的对象赋值给子类的原型
  subType.prototype = Object.create(superType && superType.prototype, {
    constructor: {
      value: subType,
      enumerable: false,
      writable: true,
      configurable: true,
    },
  });

  if (superType) {
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subType, superType)
      : (subType.__proto__ = superType);
  }
}

在线运行 / 预览地址

26 判断对象是否存在循环引用

循环引用对象本来没有什么问题,但是序列化的时候就会发生问题,比如调用 JSON.stringify()对该类对象进行序列化,就会报错: Converting circular structure to JSON.

const isCycleObject = (obj, parent) => {
  const parentArr = parent || [obj];
  for (let i in obj) {
    if (typeof obj[i] === "object") {
      let flag = false;
      parentArr.forEach((pObj) => {
        if (pObj === obj[i]) {
          flag = true;
        }
      });
      if (flag) return true;
      flag = isCycleObject(obj[i], [...parentArr, obj[i]]);
      if (flag) return true;
    }
  }
  return false;
};

在线运行 / 预览地址

27 数组的去重

数组去重也是常用的处理数组面试题

//双重循环
function unique(array) {
  // res用来存储结果
  var res = [];
  for (var i = 0, arrayLen = array.length; i < arrayLen; i++) {
    for (var j = 0, resLen = res.length; j < resLen; j++) {
      if (array[i] === res[j]) {
        break;
      }
    }
    // 如果array[i]是唯一的,那么执行完循环,j等于resLen
    if (j === resLen) {
      res.push(array[i]);
    }
  }
  return res;
}

//es6 set
function unique(arr) {
  return [...new Set(arr)];
}

//filter + indexOf
function unique_(arr) {
  return arr.filter((item, index) => arr.indexOf(item) === index);
}

//reduce + includes
function unique__(arr) {
  return arr.reduce(
    (pre, cur) => (pre.includes(cur) ? pre : pre.concat(cur)),
    []
  );
}

//map
function unique___(arr) {
  let mp = new Map();
  arr.forEach((element) => {
    if (!mp.has(element)) {
      mp.set(element, 1);
    }
  });
  return [...mp.keys()];
}

//排序后
function unique(array) {
  var res = [];
  var sortedArray = array.slice().sort();
  var seen;
  for (var i = 0, len = sortedArray.length; i < len; i++) {
    // 如果是第一个元素或者相邻的元素不相同
    if (!i || seen !== sortedArray[i]) {
      res.push(sortedArray[i]);
    }
    seen = sortedArray[i];
  }
  return res;
}

在线运行 / 预览地址

28 图片懒加载

图片懒加载就是鼠标滑动到哪里,图片加载到哪里。总的来说,一般页面打开,会同时加载页面所有的图片,如果页面的图片请求太多会造成很卡很慢的现象,为了避免这一现象,利用懒加载图片的方法,提高性能

let imgList = [...document.querySelectorAll("img")];
let length = imgList.length;

const imgLazyLoad = (function () {
  let count = 0;

  return function () {
    let deleteIndexList = [];
    imgList.forEach((img, index) => {
      let rect = img.getBoundingClientRect();
      if (rect.top < window.innerHeight) {
        img.src = img.dataset.src;
        deleteIndexList.push(index);
        count++;
        if (count === length) {
          document.removeEventListener("scroll", imgLazyLoad);
        }
      }
    });
    imgList = imgList.filter((img, index) => !deleteIndexList.includes(index));
  };
})();

// 加上防抖处理
document.addEventListener("scroll", debounce(imgLazyLoad, 200));

29 洗牌算法

打乱数组开发场景中也是很常用

function shuffle(arr) {
  for (leti = 0; i < arr.length; i++) {
    let randomIndex = i + Math.floor(Math.random() * (arr.length - i));
    [arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
  }
  return arr;
}

在线运行 / 预览地址

30手写JSONP

众所周知,后端一般给前端返回json数据,而使用Jsonp的特殊之处就在于前端会传递一个callback参数(key)给后端,接着后端返回数据时会将这个callback参数的值(value)作为函数名来包裹住json数据,最终返给前端的就是一段js代码了,这样就巧妙地解决了跨域的问题,优点是兼容性好,但是只能用于 GET 请求,而且同时需要服务端支持。

const jsonp = ({ url, params, callbackName }) => {
  const generateUrl = () => {
    let dataSrc = "";
    for (let key in params) {
      if (params.hasOwnProperty(key)) {
        dataSrc += `${key}=${params[key]}&`;
      }
    }
    dataSrc += `callback=${callbackName}`;
    return `${url}?${dataSrc}`;
  };
  return new Promise((resolve, reject) => {
    const scriptEle = document.createElement("script");
    scriptEle.src = generateUrl();
    document.body.appendChild(scriptEle);
    window[callbackName] = (data) => {
      resolve(data);
      document.removeChild(scriptEle);
    };
  });
};

注: 由于字数限制,剩余内容在后续进行总结。

你可能感兴趣的:(javascript,前端,开发语言,typescript,ecmascript)