前端面试宝典总结4-手搓代码JavaScript(基础篇)

前端面试宝典总结4之手写代码JavaScript(基础篇)

本文章 对各大学习技术论坛知识点,进行总结、归纳自用学习,共勉🙏

上一篇👉: 前端面试宝典总结4-手搓代码JavaScript(数据处理)

1.手写 instanceof 方法

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

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

  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;

    proto = Object.getPrototypeOf(proto);
  }
}

2.手写类型判断函数

function getType(value) {
  // 判断数据是 null 的情况
  if (value === null) {
    return value + "";
  }
  // 判断数据是引用类型的情况
  if (typeof value === "object") {
    let valueClass = Object.prototype.toString.call(value),
      type = valueClass.split(" ")[1].split("");
    type.pop();
    return type.join("").toLowerCase();
  } else {
    // 判断数据是基本数据类型的情况和函数的情况
    return typeof value;
  }
}

3.手写 call 函数

call 函数的实现步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window ,(在Web浏览器中是window,Node.js中是globalglobalThis)。
  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;
};

4. 手写 apply 函数

apply 函数的实现步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window ,(在Web浏览器中是window,Node.js中是globalglobalThis)。
  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;
};

5.手写 bind 函数

bind 函数的实现步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 保存当前函数的引用,获取其余传入参数值。
  3. 创建一个函数返回
  4. 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 thisapply 调用,其余情况都传入指定的上下文对象。
Function.prototype.customBind = function(context, ...argsBefore) {
    // 步骤1: 判断调用对象是否为函数
    if (typeof this !== 'function') {
        throw new TypeError('The customBind method can only be called on a function');
    }

    // 步骤2: 保存当前函数的引用,获取其余传入参数值
    const func = this;
    
    // 步骤3: 创建一个函数返回
    return function(...argsAfter) {
        // 函数内部使用 apply 来绑定函数调用
        // 判断是否为构造函数调用,通过 new.target 判断
        const finalContext = new.target ? this : context;
        return func.apply(finalContext, argsBefore.concat(argsAfter));
    };
};

// 示例使用
function greet(greeting) {
    return greeting + ', ' + this.name;
}

const user = {name: 'Alice'};
const boundGreet = greet.customBind(user, 'Hello');

console.log(boundGreet()); // 输出: Hello, Alice

6. 手写防抖函数

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

/**
 * 函数防抖 (Debounce) 实现
 * 
 * @param {Function} fn 要防抖的函数
 * @param {Number} wait 防抖时间,单位毫秒
 * @returns {Function} 返回防抖后的函数
 */
function debounce(fn, wait) {
  let timer = null; // 用来保存定时器的标识

  // 返回的防抖函数会在每次调用时执行
  return function debouncedFunction() {
    // 保存当前的上下文和参数,因为在setTimeout中需要正确地应用它们
    let context = this;
    let args = arguments;

    // 如果定时器存在,说明前一次的调用还在延迟中,需要取消前一次的调用
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    // 重新设置定时器,延迟wait毫秒后执行fn
    timer = setTimeout(() => {
      // 使用apply来确保fn在正确的上下文中执行,并且传入参数
      fn.apply(context, args);
    }, wait);
  };
}

// 示例使用
document.getElementById('someButton').addEventListener('click', debounce(function(event) {
  console.log('点击事件触发,但是我只会在你停止点击1秒后响应!');
}, 1000)

7. 手写节流函数

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

// 函数节流的实现;
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);
    }
  };
}

8. 手写 Object.create

思路:将传入的对象作为原型

function create(obj) {
  // 创建一个临时构造函数F
  function F() {}

  // 将F的prototype设置为传入的对象obj,这样任何通过F构造出来的实例都会将obj作为它们的原型
  F.prototype = obj;

  // 使用new操作符调用F构造函数,创建一个新的对象实例,该实例的原型自动指向F.prototype,即obj
  return new F();
}

let prototypeObj = {
  hello: function() {
    console.log('Hello, world!');
  }
};

let newObj = create(prototypeObj);

// 现在 newObj 可以访问原型上的 hello 方法
newObj.hello(); // 输出: Hello, world!

// 验证 newObj 的原型是否为 prototypeObj
console.log(Object.getPrototypeOf(newObj) === prototypeObj); // 输出: true

9. 手写 new 操作符

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

  1. 首先创建了一个新的空对象。
  2. 设置原型,将对象的原型设置为函数的 prototype 对象。
  3. 让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)。
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
function objectFactory() {
  // 从arguments中移除构造函数并保存,同时修改arguments数组内容
  const constructor = Array.prototype.shift.call(arguments);

  // 确保传入的是一个函数,否则抛出错误
  if (typeof constructor !== 'function') {
    throw new TypeError('Argument must be a function');
  }

  // 创建一个新对象,其原型指向构造函数的prototype
  const newObject = Object.create(constructor.prototype);

  // 调用构造函数,将其this绑定到新对象上,并传入剩余的arguments作为构造函数的参数
  const result = constructor.apply(newObject, arguments);

  // 检查构造函数的返回值是否为一个对象(或函数,因为函数也是对象),如果不是则忽略返回值,直接返回新创建的对象
  // 注意:此处逻辑应确保result不为null或undefined,而不仅仅是对象或函数,因此修正条件检查
  return result instanceof Object ? result : newObject;
}

// 使用方法示例
function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`);
};

const person = objectFactory(Person, 'Alice');
person.sayHello(); // 输出: Hello, my name is Alice

10. 手写 Promise

const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";

function MyPromise(fn) {
  // 保存初始化状态
  var self = this;

  // 初始化状态
  this.state = PENDING;

  // 用于保存 resolve 或者 rejected 传入的值
  this.value = null;

  // 用于保存 resolve 的回调函数
  this.resolvedCallbacks = [];

  // 用于保存 reject 的回调函数
  this.rejectedCallbacks = [];

  // 状态转变为 resolved 方法
  function resolve(value) {
    // 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变
    if (value instanceof MyPromise) {
      return value.then(resolve, reject);
    }

    // 保证代码的执行顺序为本轮事件循环的末尾
    setTimeout(() => {
      // 只有状态为 pending 时才能转变,
      if (self.state === PENDING) {
        // 修改状态
        self.state = RESOLVED;

        // 设置传入的值
        self.value = value;

        // 执行回调函数
        self.resolvedCallbacks.forEach(callback => {
          callback(value);
        });
      }
    }, 0);
  }

  // 状态转变为 rejected 方法
  function reject(value) {
    // 保证代码的执行顺序为本轮事件循环的末尾
    setTimeout(() => {
      // 只有状态为 pending 时才能转变
      if (self.state === PENDING) {
        // 修改状态
        self.state = REJECTED;

        // 设置传入的值
        self.value = value;

        // 执行回调函数
        self.rejectedCallbacks.forEach(callback => {
          callback(value);
        });
      }
    }, 0);
  }

  // 将两个方法传入函数执行
  try {
    fn(resolve, reject);
  } catch (e) {
    // 遇到错误时,捕获错误,执行 reject 函数
    reject(e);
  }
}

MyPromise.prototype.then = function(onResolved, onRejected) {
  // 首先判断两个参数是否为函数类型,因为这两个参数是可选参数
  onResolved =
    typeof onResolved === "function"
      ? onResolved
      : function(value) {
          return value;
        };

  onRejected =
    typeof onRejected === "function"
      ? onRejected
      : function(error) {
          throw error;
        };

  // 如果是等待状态,则将函数加入对应列表中
  if (this.state === PENDING) {
    this.resolvedCallbacks.push(onResolved);
    this.rejectedCallbacks.push(onRejected);
  }

  // 如果状态已经凝固,则直接执行对应状态的函数

  if (this.state === RESOLVED) {
    onResolved(this.value);
  }

  if (this.state === REJECTED) {
    onRejected(this.value);
  }
};

11. 手写 Promise.then

then 方法返回一个新的 promise 实例,为了在 promise 状态发生变化时(resolve / reject 被调用时)再执行 then 里的函数,我们使用一个 callbacks 数组先把传给then的函数暂存起来,等状态改变时再调用。
那么,保证后一个 then 里的方法在前一个 then(可能是异步)结束之后再执行?
可以将传给 then 的函数和新 promiseresolve 一起 push 到前一个 promisecallbacks 数组中,达到承前启后的效果:

  • 承前:当前一个 promise 完成后,调用其 resolve 变更状态,在这个 resolve 里会依次调用 callbacks 里的回调,这样就执行了 then 里的方法了
  • 启后:上一步中,当 then 里的方法执行完成后,返回一个结果,如果这个结果是个简单的值,就直接调用新 promise 的 resolve,让其状态变更,这又会依次调用新 promise 的 callbacks 数组里的方法,循环往复。如果返回的结果是个 promise,则需要等它完成之后再触发新 promiseresolve,所以可以在其结果的 then 里调用新 promiseresolve
then(onFulfilled, onReject){
    // 保存前一个promise的this
    const self = this; 
    return new MyPromise((resolve, reject) => {
      // 封装前一个promise成功时执行的函数
      let fulfilled = () => {
        try{
          const result = onFulfilled(self.value); // 承前
          return result instanceof MyPromise? result.then(resolve, reject) : resolve(result); //启后
        }catch(err){
          reject(err)
        }
      }
      // 封装前一个promise失败时执行的函数
      let rejected = () => {
        try{
          const result = onReject(self.reason);
          return result instanceof MyPromise? result.then(resolve, reject) : reject(result);
        }catch(err){
          reject(err)
        }
      }
      switch(self.status){
        case PENDING: 
          self.onFulfilledCallbacks.push(fulfilled);
          self.onRejectedCallbacks.push(rejected);
          break;
        case FULFILLED:
          fulfilled();
          break;
        case REJECT:
          rejected();
          break;
      }
    })
   }

12. 手写 Promise.all

思路:

  1. 接收一个 Promise 实例的数组或具有 Iterator 接口的对象作为参数
  2. 这个方法返回一个新的 promise 对象,
  3. 遍历传入的参数,用Promise.resolve()将参数"包一层",使其变成一个promise对象
  4. 参数所有回调成功才是成功,返回值数组与参数顺序一致
  5. 参数数组其中一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息作为 Promise.all 的错误信息。

一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,一旦其中一个接口请求失败,多个请求也就失败了 整个就给噶了,页面可能无法展示内容,页面能否展现出一定的健壮性,部分取决于其对各个数据组件相互依赖的紧密程度耦合性

function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      throw new TypeError('argument must be an array');
    }

    const promiseNum = promises.length;
    const resolvedResult = new Array(promiseNum);
    let resolvedCounter = 0;
    let hasRejected = false;

    function handleResolve(index, value) {
      resolvedResult[index] = value;
      resolvedCounter++;
      if (resolvedCounter === promiseNum && !hasRejected) {
        resolve(resolvedResult);
      }
    }

    function handleReject(reason) {
      if (!hasRejected) {
        hasRejected = true;
        reject(reason);
      }
    }

    promises.forEach((p, index) => {
      Promise.resolve(p).then(value => handleResolve(index, value), reject => handleReject(reject));
    });
  });
}

// Test
let p1 = new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000));
let p2 = new Promise((resolve, reject) => setTimeout(() => resolve(2), 2000));
let p3 = new Promise((resolve, reject) => setTimeout(() => reject('Error'), 500));

promiseAll([p3, p1, p2])
  .then(res => console.log(res))
  .catch(err => console.error(err)); // Will catch the first rejection with 'Error'

13. 手写 Promise.race

该方法的参数是 Promise 实例数组, 然后其 then 注册的回调方法是数组中的某一个 Promise 的状态变为 fulfilled 的时候就执行. 因为 Promise 的状态只能改变一次, 那么我们只需要把 Promise.race 中产生的 Promise 对象的 resolve 方法, 注入到数组中的每一个 Promise 实例中的回调函数中即可。

// 假设这是自定义Promise构造函数或者正在扩展原生Promise
// 这里直接使用原生Promise进行演示
if (!Promise.race) {
  Promise.race = function (promises) {
    // 确保传入的是一个可迭代对象
    if (!Array.isArray(promises)) {
      return Promise.reject(new TypeError('Promise.race requires an array of promises'));
    }

    return new Promise((resolve, reject) => {
      // 遍历传入的Promise数组
      for (let i = 0; i < promises.length; i++) {
        // 对每个Promise实例注册回调
        promises[i].then(resolve, reject);
      }
    });
  };
}
}

// 示例使用
const p1 = new Promise((resolve, reject) => setTimeout(() => resolve('Promise 1 resolved'), 1000));
const p2 = new Promise((resolve, reject) => setTimeout(() => reject('Promise 2 rejected'), 500));
const p3 = new Promise((resolve, reject) => setTimeout(() => resolve('Promise 3 resolved'), 3000));

Promise.race([p1, p2, p3])
  .then(value => console.log('Resolved:', value))
  .catch(reason => console.error('Rejected:', reason));

这段代码首先检查了传入Promise.race的参数是否为一个数组,确保了类型安全。然后,对于数组中的每个Promise实例,分别注册了resolvereject回调,这样无论哪个Promise先改变状态(无论是fulfilled还是rejected),Promise.race返回的Promise都会立即采用这个状态,反映出“赛跑”中的“胜利者”。

14.实现深拷贝

深拷贝是指创建一个新对象,其内容是原对象的完全拷贝,包括子对象也进行同样的拷贝,而不是引用。

function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
  let cloneObj = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key]);
    }
  }
  }
  return cloneObj;
}

15.实现斐波那契数列

斐波那契数列是这样一个数列:每个数字是前两个数字的和,前两个数字是0和1。

function fibonacci(n) {
  if (n <= 0) return 0;
  if (n === 1) return 1;
  return fibonacci(n - 1) + fibonacci(n - 2);
}
// 或者使用动态规划减少重复计算
function fibonacciDP(n) {
  let fib = [0, 1];
  for (let i = 2; i <= n; i++) {
    fib[i] = fib[i - 1] + fib[i - 2];
  }
  return fib[n];
}

下一篇👉: 前端面试宝典总结4-手搓代码JavaScript(场景篇)

  • 17
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水煮白菜王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值