前端学习笔记(三十二)

176 阅读9分钟

面试遇到的问题

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 不是幂等的。
幂等指的是请求一次和请求多次的结果是一样的,服务器状态不变。

  1. 像是 GET,DELETE 这些请求一次和多次,服务器的状态是一样的。(GET 不改变服务器状态,DELETE 第一次改变之后由于已经删除了,再次请求不会重复删除,只会返回错误状态码)(DELETE 第二次之后的返回状态码可能和第一次不一样,但是幂等表示的是服务器的状态,和状态码无关)
  2. 而 POST 是不幂等的,每次调用都会增加新的数据。

4. transform translate

从来没用过。
垂直居中的时候可能会用到如下代码:

div {
    position: absolute;
    top: 50%;
    transform: translate(0, -50%);
}

translate 是 2d 动作,表示向右和向下移动多少。而且 translate 里的 % 是相对于自身元素宽高度的,因此 -50% 为向上移动自身元素的一半,再加上 top:50%,就完成了垂直居中。

5. const 可以改变吗,为什么

一般而言不能改变,但是如果是对象可以改变其属性和方法。
因为 const 的变量储存在栈之中,栈中的值是不能改变的。而如果是对象,那么变量地址虽然在栈中,但是属性和方法在堆之中,因此可以改变属性和方法。

6. 基线

image.png image.png

6.1 字体的各种线,及其与行高之间的关系

6.1.1 字体

  1. 文字设计的时候具有很多个线,基线 baseline 就是其中一条重要的线。基线并不是文字最下面的线,如果是英文,可以理解为字母 x 的最下部。
  2. 文字的最上部的线叫做顶线 text-top,最下部的叫做底线 text-bottom。这两者之间就是字体大小 font-size,同时也是 em-box(em 等于 font-size)。
  3. x-height,字母 x 的高度。 1ex 指的就是1个 x 的高度。
  4. verticle-align 里有一个值叫做 middle,指的就是 baseline 往上 0.5ex 的高度。
  5. 字体里还有上基线 super 和下基线 sub,具体什么意思不知道,反正一个在基线上,一个在下。

6.1.2 行高

  1. 行高和行距要一起来说,css 里只能控制行高,也就是 line-height,配合 font-size 也就知道了行距。行距 = 行高 - font-size。图里的是半行距,也就是行距/2。(至于为什么是半行距,因为第一行的下面半个行距,加上第二行上面的半个行距,就是一个行距了)
  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. 自执行函数

来自这篇文章

  1. 为什么自执行函数要用括号括起来,比如说这个
    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");}; // 函数声明
    (); // 语法报错
    
    image.png
    • 而后面两行代码,函数声明被包裹进括号里,而括号里的语法会被当做表达式来处理,因此括号里的内容相当于创造了一个匿名函数然后运行:
    (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
    
  2. 各种歪瓜裂枣的立即执行的匿名函数用法。
    • 除了括号,只要是能把函数声明当做表达式来看待的语法,都可以实现匿名函数的立即执行用法。
    • 如:
      1. 布尔运算符:!function a(){console.log(1);}(),由于!让解释器把后面的语法当做表达式处理,自然也不需要加括号了,其实相当于 !(function a(){console.log(1);}())。该语法在执行完匿名函数后,还会返回 true。因为感叹号后面的表达式执行完后输出 undefined,而 !undefined 返回 true
      2. &&,||,, 也可以
      3. 甚至 = 也可以,比如说:
        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 的依赖冲突解决方案

  1. 首先如果有一个包 A,依赖 C 包,C 包的版本为 1.0,A 和 C 会同时放在项目的 node_modules 文件夹下。
  2. 如果之后要安装包 B,然而 B 的依赖中有一个依赖也是 C,但是版本为 2.0。此时 B 的模块文件夹下还会有一个 node_modules 文件夹,用于存放 2.0 版本的 C 包。 image.png
  3. 这种方式,有时候会导致多个相同版本的包,可以用 npm dedupe 来去除,顺便一说 yarn 会自动 dedupe。

11. React Hooks 的原理

核心是 memorizedState 的链表,会记录每次 useState,useEffect 等 hook 函数的状态。

  1. 以 useState 为例,每到一个新的hook 就会调用 hook.nexthook() 从上一个 hook 到达这个 memorizedState,调用 hook.memorizedState 可以知道该 hook 的状态。链表的每个位置记录对应的状态,这也是为什么 hook 函数不能在 if 等语句里,因为执行顺序改变了就会到达错误的 memorizedState,从而导致状态错乱。(想想看,有一个 hook 突然被 if 跳过了,使用下一个 hook,但是调用的 nexthook 依然是跳过的那个 hook 的 memorizedState,就会出大问题)
  2. 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 对用户行为的不同做法

  1. 直接在地址栏输入 url:会去找 disk cache 有没有缓存(因为不可能有 memory cache)
  2. 刷新页面:先去找 memory cache,再去找 disk cache
  3. ctrl+F5,强制不使用缓存。