字节前端面试题

字节前端面试题

国庆前夕面试了字节的前端,面试官很nice,在这次面试中也学到了不少知识点。那时候靠着记忆记下了这些题

一、name、setName编程题,考察箭头函数中this的指向;

二、https的工作原理;

三、组件库的 polyfill的处理;

四、promise打印顺序问题;

五、JS中的原始值为什么能调用方法

六、React 中的 加上 if 判断后的 useState;

七、编程题tree,name,家谱问题;

八、三个数组,求交集,要求函数能重载; 先记录下,回头把这些问题都攻克攻克

一、箭头函数 this 指向问题

let name = "x";

let people = {
  name: "y",
  setName: () => {
    return () => {
      console.log(this.name);
    };
  },
};

let getName = people.setName();

console.log(getName());
console.log(people.name);

答案: x、y

箭头函数中没有 this,它的 this 指向外部词法环境,与谁调用无关。而且词法环境是在初始化的时候就确定,也就是说当代码中有箭头函数时,代码一开始还没执行时就确定了它的 this

在这代题目中,people 中的 setName 是箭头函数,它的 this 永远绑定在 window 上,所以 getName 无论怎么执行,它的this 都指向 window,所以let getName = people.setName()console.log(getName()) 中的 this 都指向 window,people.name 的值是 y,window 的name 是 x,所以打印 x、y

所谓外部词法环境

如果再思考一下上述代码,会觉得奇怪,setName 不是在 people 对象中吗,为什么它的 this 不是 people。其实 people 是它当前的词法环境,而非外部词法环境,也就是说在它这层环境的外层,那外层就是 window 了

我们从 this关键字 中拿出箭头函数的例子再做说明

var people = {
  name: "eliane",
  age: 28,
  sayName: () => console.log(this.name, this),
  sayName2: function () {
    console.log(this.name, this);
  },
};
people.sayName(); //  '', Window
people.sayName2(); // elaine, {name: 'eliane', age: 28}

看到没,sayName是箭头函数,所以它的this 要从外部词法环境中找,而 sayName2 是普通函数,谁调用它,this 指向谁

至于外部词法环境,简单来说就是要形成作用域(全局作用域、函数作用域和块级作用域),以下例子也是个典型的寻找外部词法环境的例子

var foo = {
  bar: {
    a: () => console.log(this),
  },
};
foo.bar.a(); // window

foo 是对象,bar 也是对象,都没有形成作用域,所以箭头函数a 的 this 只能找全局的 window

三、组件库的 polyfill的处理

主要是对组件库的理解

组件库主题有哪些?

组件库给别人用的时候,输出的是编译后的文件还是编译前的文件

  • 用的是编译后的产物给消费者

  • 如果你们做的组件的兼容目标和使用方不一样怎么办,你们会把一些 polyfill 编译进去吗

    • 规定了浏览器的兼容目标
    • 包体积会变大吗
    • 组件引入 polyfill,业务又引入了 polyfill。这样就两份 polyfill
  • 实现的方式是什么

    • 我的回答:从依赖项的做处理
    • 编译时检测,写一个babel插件,检测如果依赖项中已经有了,就不注入,如果没有则注入
    • 还有别的思路吗?你的思路有一个怪的点,一般babel处理文件的时候不会处理 node_modules 下的文件,它只会对我开发的代码进行检测,你这种方法,不仅要对自己写的代码进行检测处理,还要对你引用的依赖性也就是 node_modules 处理,这样babel编译的时候时间就会超慢,一般默认情况下 babel 不会做相关的检测,思路是正确的,只是怪
    • 另一个回答,把 polyfill 进行拆包,单独引用
      • 方案也可以,但问题是 polyfill 是动态的
      • 如果是你这种情况下,就要规定语法检测,要约定俗成一下

四、Promise 的编程题

setTimeout(() => {
  console.log("a");
});

new Promise((resolve) => {
  console.log("b");
  for (let i = 0; i < 10000; i++) {
    if (i === 1) console.log("c");
    if (i === 9999) resolve();
  }
}).then(() => {
  console.log("d");
});

console.log("e");

答案: b、c、e、d、a

衍生

let p0 = new Promise((resolve) => {
  resolve();
})
  .then(() => {
    console.log("0");
  })
  .then(() => {
    console.log("1");
  });

let p1 = new Promise((resolve) => {
  resolve();
}).then(() => {
  console.log("2");
});

p0.then(() => {
  console.log("3");
});

答案: 0、2、1、3

分析:Promise 的事件机制分为注册函数和执行函数,当我们执行到第三行的时候,我们会把 console(‘0’) 注册,因为第二行已经 resolve 了,所以会把第四行中的回调函数放入微任务队列中,执行第五行的时候,是个同步任务,会注册 console.log(‘1’),但注册完了并不会扔到微任务队列上,因为 console.log(‘1’) 是否扔到微任务队列上,取决于 console.log(‘0’) 的执行结果,如果这里报错了,就会走到 catch 里

此时微任务队列中有 0,但没有 1,因为 console.log(0) 还没执行

相同的道理,执行到11行,我们是将console.log(2)扔到微任务队列里去

同样的道理,执行到15行,console.log(3) 取决于 console.log(1) 的结果,console.log(3)只是注册了但是没有扔到微任务队列上

同步任务执行完后,微任务队列上有两个回调函数(0和2),当console.log(0) 执行完后,再把 console.log(1) 扔到console.log(2) 的后面,当 console.log(1) 执行完了,再把console.log(3) 扔在console.log(1) 的后面

五、JS中的原始值为什么能调用方法

为什么 js 中的 原始值能调用方法,比如 str.split()

关键字:原始值包装对象(Primitive Wrapper Objects)

在调用方法的时候会把它包装成一个对象,之后把这个对象置成null。它一开始就是个原始值,就是简单类型的值,只是在运行时做某一些处理,这种设计允许开发者像使用对象一样使用原始值

总结

后续的问题我没有什么记忆了,只是能在面试中感受到这个面试官的友好