面试遇到的问题
1. 实现 async
第一次遇到,只知道是生成器相关,其他的不知道。
看了下掘金的别人的实现,自己也大概写一下:
// 测试用的获取数据用异步函数
function getData(num) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(num);
resolve();
}, 1000);
}
);
}
// 测试用的要被 async 转成异步函数的生成器函数
function* myTestFunction() {
console.log("0");
yield getData(1);
console.log("2");
yield getData(3);
console.log("4");
yield getData(5);
return 6;
}
// async 的实现
function myAsync(func) {
return function () {
let gen = func.apply(this, arguments);
return new Promise(
(resolve, reject) => {
function step() {
const { value, done } = gen.next();
if (done) {
// 生成器返回的值要被作为函数返回值
// resolve(value) 返回值为 Promise.resolve(value)
return resolve(value);
} else {
// done 之前 value 为 promise
return value.then(() => { step(); });
}
}
step();
}
);
}
}
// 测试
let asyncMyTestFunction = myAsync(myTestFunction);
asyncMyTestFunction().then((val) => { console.log(val); });
// 输出
// 0
// 一秒后
// 1
// 2
// 一秒后
// 3
// 4
// 一秒后
// 5
// 6
2. 判断数据类型
最准确的方式是使用 Object.prototype.toString.call()
let a = [1,2,3];
Object.prototype.toString.call(a); // 返回 "[Object Array]"
3. 幂等性
http 请求的类型有 GET,POST,HEAD,PUT,DELETE。
其中只有 POST 不是幂等的。
幂等指的是请求一次和请求多次的结果是一样的,服务器状态不变。
- 像是 GET,DELETE 这些请求一次和多次,服务器的状态是一样的。(GET 不改变服务器状态,DELETE 第一次改变之后由于已经删除了,再次请求不会重复删除,只会返回错误状态码)(DELETE 第二次之后的返回状态码可能和第一次不一样,但是幂等表示的是服务器的状态,和状态码无关)
- 而 POST 是不幂等的,每次调用都会增加新的数据。
4. transform translate
从来没用过。
垂直居中的时候可能会用到如下代码:
div {
position: absolute;
top: 50%;
transform: translate(0, -50%);
}
translate 是 2d 动作,表示向右和向下移动多少。而且 translate 里的 % 是相对于自身元素宽高度的,因此 -50% 为向上移动自身元素的一半,再加上 top:50%,就完成了垂直居中。
5. const 可以改变吗,为什么
一般而言不能改变,但是如果是对象可以改变其属性和方法。
因为 const 的变量储存在栈之中,栈中的值是不能改变的。而如果是对象,那么变量地址虽然在栈中,但是属性和方法在堆之中,因此可以改变属性和方法。
6. 基线
6.1 字体的各种线,及其与行高之间的关系
6.1.1 字体
- 文字设计的时候具有很多个线,基线 baseline 就是其中一条重要的线。基线并不是文字最下面的线,如果是英文,可以理解为字母 x 的最下部。
- 文字的最上部的线叫做顶线 text-top,最下部的叫做底线 text-bottom。这两者之间就是字体大小 font-size,同时也是 em-box(em 等于 font-size)。
- x-height,字母 x 的高度。 1ex 指的就是1个 x 的高度。
- verticle-align 里有一个值叫做 middle,指的就是 baseline 往上 0.5ex 的高度。
- 字体里还有上基线 super 和下基线 sub,具体什么意思不知道,反正一个在基线上,一个在下。
6.1.2 行高
- 行高和行距要一起来说,css 里只能控制行高,也就是 line-height,配合 font-size 也就知道了行距。行距 = 行高 - font-size。图里的是半行距,也就是行距/2。(至于为什么是半行距,因为第一行的下面半个行距,加上第二行上面的半个行距,就是一个行距了)
- 顶线上面半个行距叫做 top,底线下面半个行距叫做 bottom。(注意区分于顶线和底线,顶线和底线都是对于字体而言的,top和bottom超出了字体的范围)。这两者之间叫做 content-area。也就是实际上盒模型的那个 content。top 和 bottom 之间的距离就是行高 line-height。还有种说法 line-height 为两行之间的基线距离,其实是一样的,换个说法而已。
6.1 inline 元素的 baseline
就是文字的 baseline
6.2 inline-block 元素的 baseline
最后一行 inline 元素的 baseline。
6.3 block 元素
独占一行,不需要基线
7. 为什么 React 要在 16 版本抛弃很多生命周期函数
首先这些功能完全可以通过其他生命周期函数覆盖,而这些 will 开头的函数经常被滥用导致各种问题。React 想限制开发者写出好的代码。
此外,由于 fiber 的推出,render 函数之前的生命周期函数可能会被多次调用,如果开发者不小心犯错就可能造成一些问题,因此也被抛弃。而render 之后的生命周期函数因为已经渲染好了,自然不会有问题。
至于 fiber,是 React 16 版本对之前的内部算法的大修改。优化了渲染机制,是一种高并发渲染。
8. 链式调用的实现
链式调用有很多种,这里只写一个简单的加法的链式调用实现:
function add(num){
let res = num;
function _add(num2){
res+=num2;
return _add;
}
_add.valueOf = () => {return res;}
return _add;
}
add(3)(1)(1000).valueOf(); // 1004
此外,像是构造函数,类,对象的链式调用,可以通过在方法中返回 this 对象来完成。
当然也要具体问题具体分析,比如说 promise 的 then 返回的就是一个新的 promise。
9. 自执行函数
来自这篇文章。
- 为什么自执行函数要用括号括起来,比如说这个
function a(){console.log("1");}() // 语法报错 (function a(){console.log("1");}()); // 正常运行输出 1 (function a(){console.log("1");})(); // 也是正常运行输出 1- 因为实际上第一行代码,解释器把前面的
function a(){console.log("1");}当做一个函数声明,所以其实整个代码会被这样解释:
function a(){console.log("1");}; // 函数声明 (); // 语法报错- 而后面两行代码,函数声明被包裹进括号里,而括号里的语法会被当做表达式来处理,因此括号里的内容相当于创造了一个匿名函数然后运行:
虽然有给匿名函数加名字,但它依然还是匿名函数,这个名字可以用于像递归这样的作用,但是其他地方并不能引用这个名字:(function a() {console.log("1");})(); // a 是匿名函数 a(); // 报错,a is not defined(function a(num){if (num === 0) {return;}; console.log(num); return a(num-1)})(3); // 输出 // 3 // 2 // 1 - 因为实际上第一行代码,解释器把前面的
- 各种歪瓜裂枣的立即执行的匿名函数用法。
- 除了括号,只要是能把函数声明当做表达式来看待的语法,都可以实现匿名函数的立即执行用法。
- 如:
- 布尔运算符:
!function a(){console.log(1);}(),由于!让解释器把后面的语法当做表达式处理,自然也不需要加括号了,其实相当于!(function a(){console.log(1);}())。该语法在执行完匿名函数后,还会返回 true。因为感叹号后面的表达式执行完后输出 undefined,而!undefined返回 true &&,||,,也可以- 甚至
=也可以,比如说:let a = function foo() {console.log(1); return "你是谁"}; let b = function foo() {console.log(1); return "你是谁"}(); // 输出 1 console.log(a); // a 为函数 console.log(b); // b 为字符串:"你是谁" foo(); // 报错 // 输出 // 1 // function foo() {console.log(1); return "你是谁"}。 // "你是谁" // 报错,foo is not defined
- 布尔运算符:
10. npm 的依赖冲突解决方案
- 首先如果有一个包 A,依赖 C 包,C 包的版本为 1.0,A 和 C 会同时放在项目的 node_modules 文件夹下。
- 如果之后要安装包 B,然而 B 的依赖中有一个依赖也是 C,但是版本为 2.0。此时 B 的模块文件夹下还会有一个 node_modules 文件夹,用于存放 2.0 版本的 C 包。
- 这种方式,有时候会导致多个相同版本的包,可以用 npm dedupe 来去除,顺便一说 yarn 会自动 dedupe。
11. React Hooks 的原理
核心是 memorizedState 的链表,会记录每次 useState,useEffect 等 hook 函数的状态。
- 以 useState 为例,每到一个新的hook 就会调用
hook.nexthook()从上一个 hook 到达这个 memorizedState,调用 hook.memorizedState 可以知道该 hook 的状态。链表的每个位置记录对应的状态,这也是为什么 hook 函数不能在 if 等语句里,因为执行顺序改变了就会到达错误的 memorizedState,从而导致状态错乱。(想想看,有一个 hook 突然被 if 跳过了,使用下一个 hook,但是调用的 nexthook 依然是跳过的那个 hook 的 memorizedState,就会出大问题) - useEffect 也差不多,一样是调用
hook.nexthook()获取 memorizedState, memorizedState 记录的是第二个参数要监听的变量,比较一下这次和之前有没有变化,有就执行函数,没有就不执行。
12. create-react-app 为什么修改 webpack,babel 配置这么麻烦?
CRA 设计的时候刻意减少生成项目的依赖,webpack,babel这些配置虽然有,但不会显示在 package.json 里(所以 eject 是反编译,把这些配置文件全都暴露出来),全都集成在 CRA 里,降低了新手的使用难度。CRA 初衷也是给中小型项目使用的,这些项目并不需要多复杂的配置。
13. 浏览器缓存手段
浏览器缓存,也叫 http 缓存。
- 以下缓存都没有命中的情况,才会发送请求
- 强缓存用的是 mamory cache 和 disk cache,cache-control 里可以控制是否被缓存。当然,memory cache 是不管这个的。
13.1 service worker 服务工作者线程
可以在 js 里手动控制缓存,可以缓存响应,让浏览器在没有网络的情况下仍然可以使用网站。
13.2 memory cache 内存缓存
内存可以短期缓存一些资源,不过当关闭 tab 时候就会释放资源。
内存缓存并不关心 cache-control 头部的内容。
13.3 disk cache 硬盘缓存
放在硬盘里比内存保存的时间更长,能存的数据也大。只是读取慢一点。
会根据 http 头部判断是否缓存。
13.4 push cache 推送缓存
http/2.0 的缓存。缓存服务端推送过来的资源。连接关闭的时候会被释放。
13.5 对用户行为的不同做法
- 直接在地址栏输入 url:会去找 disk cache 有没有缓存(因为不可能有 memory cache)
- 刷新页面:先去找 memory cache,再去找 disk cache
- ctrl+F5,强制不使用缓存。