前端面试手撕代码(拼多多)

1,336 阅读3分钟

前端笔试题(拼多多)

前言

今天面试了拼多多的前端岗位,考了四道笔试题。笔试有点慌,答得一般。分享给同行看看,希望你遇到了能全过!

实现Promise串行执行任务

实现Promise串行执行,并在失败时重试指定次数的函数。如果所有重试都失败,则整个序列会停止执行。

function promiseSeries(tasks, retryTimes) {
  // 内部函数,用于执行单个任务,并重试指定次数
  function runTask(task, retryCount) {
    return new Promise((resolve, reject) => {
      task()
        .then(resolve)
        .catch((error) => {
          if (retryCount > 0) {
            console.log(`任务失败,正在重试... 剩余重试次数:${retryCount - 1}`);
            runTask(task, retryCount - 1).then(resolve).catch(reject);
          } else {
            reject(error);
          }
        });
    });
  }
  // 串行执行任务
  let result = Promise.resolve();
  tasks.forEach((task) => {
    result = result.then(() => runTask(task, retryTimes));
  });
  return result;
}
// 示例使用
const tasks = [
  () => new Promise((resolve, reject) => setTimeout(resolve, 1000, '任务1完成')),
  () => new Promise((resolve, reject) => setTimeout(reject, 1000, '任务2失败')),
  () => new Promise((resolve, reject) => setTimeout(resolve, 1000, '任务3完成'))
];
promiseSeries(tasks, 2)
  .then(() => console.log('所有任务完成'))
  .catch((error) => console.log(`执行失败:${error}`));

在这个例子中,tasks 是一个包含Promise任务的数组,每个任务都是一个返回Promise的函数。retryTimes 是每个任务失败时重试的次数。如果任务失败并且重试次数用尽,整个序列会停止,并返回一个拒绝的Promise。如果所有任务都成功完成,最终会返回一个解决的Promise。

实现一个compile 函数

实现一个compile函数,支持多级引用变量以及数组,能够解析嵌套的属性和数组索引。下面是一个compile函数实现:

function compile(str, obj) {
  const exp = /\$\{([^}]+)\}/g;

  function matchFn(match, p1) {
    let curObj = obj;
    let value;

    p1.split(".").forEach((part) => {
      const arrExp = /(\w+)\[(\d+)\]/;
      const matchRes = part.match(arrExp);

      if (matchRes) {
        const [_, name, index] = matchRes;
        value = curObj[name] && curObj[name][index];

        if (value && typeof value !== "object") {
          return value;
        }

        if (!value) throw new Error(`Property not found: ${match}`);

        curObj = value;
      } else {
        value = curObj[part];

        if (value && typeof value !== "object") {
          return value;
        }

        if (!value) throw new Error(`Property not found: ${match}`);

        curObj = value;
      }
    });

    return value;
  }

  return str.replace(exp, matchFn);
}


const template =
  "Hello, ${user.name}! Your balance is${user.balance}. You have ${user.items[0]} in your cart. and${user.items[2].kk}";
const exprObj = {
  user: {
    name: "Alice",
    balance: 100.5,
    items: ["Item1", "Item2", { kk: 1 }],
  },
};
const compiledString = compile(template, exprObj);
console.log(compiledString);

在这个实现中,我们使用了正则表达式来匹配形如${obj.prop}${arr[0]}的插值表达式。在replaceMatch函数中,我们首先将变量路径(如user.nameuser.items[0])分割成多个部分,然后遍历这些部分,逐步解析出最终的值。 注意,这个实现假设表达式对象exprObj的结构是固定的,并且所有的路径都是有效的。如果路径中的某个属性或索引不存在,函数将返回原始的插值表达式。此外,这个实现不处理循环引用或复杂的对象结构。如果需要处理更复杂的情况,可能需要一个更健壮的路径解析器。

法二:使用with函数实现

function compile(template, context) {
  // 替换模板中的插值表达式为 JavaScript 模板字符串语法
  const compiledTemplate = template.replace(
    /{{(.*?)}}/g,
    (match, p1) => `\${${p1.trim()}}`
  );

  // 动态生成一个函数,用于替换模板中的插值表达式
  const compiledFunction = new Function(
    "context",
    `
    with (context) {
      return \`${compiledTemplate}\`;
    }
  `
  );

  // 调用函数并返回结果
  return compiledFunction(context);
}



const template =
  "Hello, ${user.name}! Your balance is${user.balance}. You have ${user.items[0]} in your cart. and${user.items[2].kk}";
const exprObj = {
  user: {
    name: "Alice",
    balance: 100.5,
    items: ["Item1", "Item2", { kk: 1 }],
  },
};
const compiledString = compile(template, exprObj);
console.log(compiledString);

使用with函数是非常取巧的,面试如果想要快速实现,可以使用第法二。

ES6 class关键字的实现

用ES6声明一个class,在es5怎么实现。

function inheritPrototype(subClass, superClass) {
  // 复制一份父类的原型
  var p = copy(superClass.prototype);
  // 修正构造函数
  p.constructor = subClass;
  // 设置子类原型
  subClass.prototype = p;
}

function Parent(name, id){
  this.id = id;
  this.name = name;
  this.list = ['a'];
  this.printName = function(){
    console.log(this.name);
  }
}
Parent.prototype.sayName = function(){
  console.log(this.name);
};
function Child(name, id){
  Parent.call(this, name, id);
  // Parent.apply(this, arguments);
}
inheritPrototype(Child, Parent);

分析打印结果

给出下列代码的打印结果,分析原因。

const temp = {}

class Test {
    a(){}
    b = () => { }
    c = 1;
    d = {};
    e = temp;
    static f = temp
}

const test1 = new Test();
const test2 = new Test();

console.log(test1.a === test2.a)
console.log(test1.b === test2.b)
console.log(test1.c === test2.c)
console.log(test1.d === test2.d)
console.log(test1.e === test2.e)
console.log(test1 === test2)
console.log(Test.f === Test.f)  

以下是给定代码的输出结果及其原因分析:

console.log(test1.a === test2.a); // true
console.log(test1.b === test2.b); // false
console.log(test1.c === test2.c); // true
console.log(test1.d === test2.d); // false
console.log(test1.e === test2.e); // true
console.log(test1 === test2);     // false
console.log(Test.f === Test.f);   // true

原因分析:

  1. console.log(test1.a === test2.a); // true
    • a 是一个在 Test 类原型上定义的方法。由于 test1test2 都是 Test 类的实例,它们会共享原型上的方法,因此 test1.atest2.a 指向同一个函数。
  2. console.log(test1.b === test2.b); // false
    • b 是一个通过赋值创建的箭头函数,它是实例自己的属性,而不是原型上的属性。因此,test1.btest2.b 是两个不同的函数实例。
  3. console.log(test1.c === test2.c); // true
    • c 是一个原始值(在这种情况下是数字),原始值是按值传递的,而不是按引用传递。但是,这里比较的是 test1.ctest2.c 的值,由于它们都被赋值为 1,所以比较结果是 true
  4. console.log(test1.d === test2.d); // false
    • d 是一个对象,它是通过赋值创建的实例自己的属性。test1.dtest2.d 是两个不同的对象实例。
  5. console.log(test1.e === test2.e); // true
    • e 被赋值为同一个外部对象 temp 的引用。因此,test1.etest2.e 指向同一个对象。
  6. console.log(test1 === test2); // false
    • test1test2Test 类的两个不同实例,因此它们不相等。
  7. console.log(Test.f === Test.f); // true
    • f 是一个静态属性,它属于 Test 类本身,而不是类的实例。静态属性在类定义时只会被创建一次,因此 Test.f 指向的是同一个对象。