前端面试总结

1.  es6基础

for in: 遍历对象,循环的是key。也能遍历数组,循环的是下标,但是是字符串不是数值。会遍历原型链上的属性,可以使用hasOwnProperty解决。

for of: 遍历数组和实现了迭代器功能的结构,不能遍历对象。

迭代器iterator: Array、Set、Map、函数argument对象内部已经实现了iterator接口,可以使用for of变量。注意Map可以普通对象不可以,因为Map实现了迭代器

generate函数返回一个迭代器对象,通过next方法一步步执行函数体内代码,它的本质是一个指针对象:

  • 初始时用该next方法,移动指针使得指针指向数据的第一个元素
  • 然后每调用一次next方法,指针就指向数据结构里的下一个元素,直到指向最后一个元素
  • 不断调用next方法就可以实现遍历元素的效果了

async函数就是generate函数的语法糖,await对应于yield,用于解决嵌套地调用回调。async基于promise封装的。

自定义object内部迭代器,遍历object内部数组

const obj = {
  name: 'abc',
  cities:[ 'London', 'New York', 'Tokyo'],
  [Symbol.iterator](){
    let index = 0;
    let _self = this;
    return {      
      next: function(){
        if(index >= _self.cities.length){
          return {value:undefined,done:true}
        }else{
          const result = {value: _self.cities[index], done:false}
          index++
          return result
        }
      }
    }
  }
}
for (const v of obj) {
  console.log('v=',v)
}

// v= London
// v= New York
// v= Tokyo

使用for of,可以在循环体内部使用await来保证异步代码顺序执行,就是基于迭代器实现的。

普通函数和箭头函数区别:箭头函数不能new,this指向问题,不能访问arguments

apply和call的区别:apply第二个传数组,call一个个传,超过3个参数call更快,因为和内部[[call]]方法参数格式一致。

this指向问题

ts: 规定变量,参数和返回值类型,无法推断类型时用类型断言。泛型允许您创建可与各种类型一起使用的可重用组件或函数,保证使用不同数据类型的灵活性。Partial<Person>使得Person里面所有类型可选。Pick<Person, 'name' | 'age'>。Omit<Person, 'city'>。Exclude<Color, 'green' | 'blue'>从联合类型中排除。keyof获取接口的key,typeof获取变量的值的类型。Person写两个同名的会合并。type和interface区别:type使用交叉类型&来继承,不可重复声明,可声明为基本类型。interface可以extends,只能定义对象,可重复声明相当于交叉类型。

Array.from,forEach,for of遍历类数组。Array.toString()可以将二维数组拍平。

2.继承 

原型链继承:缺点是prototype是属于函数的而不是实例,prototype被所有new出来的实例共享,修改了会相互影响。

apply方法继承:不能继承原型上的属性

组合继承:构造函数要执行两次

原型式继承:创建一个function,传参是对象obj。内容new一个实例F,F.prototype = obj,返回这个实例F。 Object.create将此方法规范化,第一个参数是一样的,把传参对象挂到新的对象的prototype上,第二个参数定义额外的属性。

寄生式继承:原型式继承+自己定义的属性。现根据原型式继承创建一个对象obj,在往里面加属性obj.xxx = "xxx";

原型寄生组合继承

class继承:通过super函数继承父类属性方法。

3.深复制和浅复制

深复制是新的对象修改操作不影响老的对象。使用JSON.parse(JSON.stringify(obj))或者Object.assign(obj)或者递归遍历赋值。

4. http三次握手

SYN:同步序列编号,建立连接时使用的握手信号。建立连接时,客户端首先发出一个SYN消息。

(FIN表示申请结束连接)

seq:序号。是报文段发送的数据组的第一个字节的序号。若报文段序号是300,数据部分有100个字节,下一个报文段序号是400。

ACK:确认值,为1表示确认号字段有效,建立连接。ACK为1,报文才能传输。

ack:确认值,下一个期待收到的字节序号,表明该序号之前的数据都收到了。ACK为1时有效。建立连接时,SYN报文的ACK标志为0。

x表示第一个数据字节的序号,最开始为0。客户端第一次握手就发送的是seq=x=0(表示客户端自己的初始序号0),SYN=1。服务器接受后发送ack=x+1=1,表示期望收到的数据序号(客户端的序号)为1,SYN=1,ACK=1,seq=y=0(y表示服务器自己的初始序号,从0开始)。

第二次握手:客户端收到数据后,发送ACK=1,自己发送数据的序号seq=x+1,期望接受的数据序号ack=y+1。

这里的x+1和y+1不是固定+1,取决于每次发送了多少字节的数据,一个字节就+1。

seq和ack都是报文序号,seq是自己发的报文序号,ack是对方的报文序号

三次握手为了防止失效的报文又发给服务端。第一次是客户端请求连接,第三次是客户端同意连接。seq的初始值也叫ISN,是随机生成的,防止旧的连接和恶意链接。半连接可以防止洪水攻击。

http2多路复用:一个tcp连接里并行请求。http1.x浏览器使用个5-7个tcp连接去并行请求。

5. 闭包

外部访问函数内部作用域变量。可以合理的使用闭包,优点是防止变量全局污染,缺点是不当的使用可能会造成内存泄漏:

function fn() {
      const arr = [1];
      function fn1() {
        console.log(arr[0]);
      }
      return fn1
    }
    let test = fn()
    test()

test没有及时回收,过段时间内存还占用空间。可以test = null或者fn()(),早期可能外面在包一层函数自调用。未清除定时器和DOM事件监听器会导致内存泄漏,需要remove监听器,clear定时器。

垃圾回收机制:最初是引用计数法,每个对象内部标记引用它的总个数,如果为0,为垃圾对象,如果两个对象相互引用,断开对象的引用后他们不是垃圾对象,因此无法回收互相引用的对象。

标记清除法:从window开始找,所有引用的对象,标记为使用中。若没有标记为使用中的都是垃圾对象。打上标记和去除标记的时机是进入执行环境和离开执行环境,比如执行一个函数,执行完后即使里面还存在引用关系,但是函数的执行已经结束了,这个时候就可以回收函数内部变量。解决了循环引用问题,因为从window找引用的对象过一会是null:

var o1 = {};
var o2 = {};
o1.a = o2; // o1 引用 o2
o2.a = o1; // o2 引用 o1
o1 = null
o2 = null

主动回收:变量=null 

6. 浏览器进程

四个:主进程负责协调,渲染进程处理网页呈现和交互,插件进程负责浏览器插件,GPU进程负责图像和视频。

渲染进程:GUI线程,JS引擎线程执行js代码,事件触发线程处理点击等事件,定时器触发线程,异步Http请求线程。

事件循环又叫消息循环,是浏览器渲染主线程的工作方式。主线程先执行同步代码,异步代码从主线程提出放在任务队列(消息队列),执行完后在任务队列放入一个事件(回调函数),主线程执行完同步代码后,再去读取任务队列并执行。

主线程的同步代码形成一个执行栈(代码的执行顺序),是一个同步任务,执行完以后执行微任务,然后从宏任务队列取回调按顺序执行。然后检查渲染,GUI线程接管渲染,渲染时遇到js,js线程继续接管,执行下一个事件循环。

浏览器环境微任务有promise,Mutation Observer API,queueMicrotask。宏任务是setTimeout定时器,requestAnimationFrame,网络请求,事件回调函数onload(DOM事件),ui渲染。

nodejs环境会有区别。微任务有nextTick队列和microTask队列。宏任务队列放在6个阶段:

timer阶段是定时器,pending callback是系统错误回调,tcp连接错误和dns解析异常等。idle prepare内部使用的,poll阶段是轮询阶段主要是io操作,check是setImmediate,close 处理关闭的回调如socket.close()。

两个事件循环差异在于环境不同,API不同,node主要是文件操作,网络请求等io操作,数据库访问。

回流:改变大小。重绘:改变样式。回流一定重绘,重绘不一定回流。优化:一次性修改样式,最好使用class,不要一行一行操作修改。transfrom和opacity不会重绘,他们使用GPU硬件加速,本质是合成阶段开启了新的图层。

js线程和GUI线程互斥,即请求HTM然后解析DOM树和生成CSS规则树的过程中,遇到js文件就会阻塞,所以很多js放在body的最后,也可以使用js异步加载,即script加上defer或者async,async会在加载完后立即执行阻塞html解析,defer不会。解析css不会阻塞html解析,但是阻塞html渲染,css和html解析都和js互斥。

7.原型和原型链

显式原型简称原型,即prototype。隐式原型__proto__。

Function.__proto__ == Object.prototype

A.__proto__ = Function.prototype

8.协商缓存和强缓存

用的比较多的是cacheControl: max-age=36800。他表示超过时间走协商缓存,否则走强缓存。协商缓存时请求带上Last-Midify或者Etag值,让后端决定是否更新。所以打包文件都会带contenthash。使用contenthash的话,可以有利于减少上线白屏,如果不刷新页面都走旧的js文件访问可以避免白屏。

9.排序算法

function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}
// 冒泡
// 可以利用一个标志位进行优化,每轮遍历开始的时候flag为0,如果第i轮没有进行交换,则说明数组已经有序不需要在遍历
function Bubble(arr) {
    for (var i = 0; i < arr.length; i++) {
        for (var j = 0; j < arr.length - i - 1; j++) {
            var temp = arr[j];
            if (temp > arr[j + 1]) {
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}
// 选择排序,不稳定 如 5 5 2,第一次交换后5之间的顺序变了
function Choose(arr) {
    for (var i = 0; i < arr.length; i++) {
        var min = i;
        for (var j = i; j < arr.length - 1; j++) {
            if (arr[j + 1] < arr[min]) {
                min = j + 1;
            }
        }
        var temp = arr[i];
        arr[i] = arr[min];
        arr[min] = temp;
    }
}
// 快速排序
function quickSort(arr, _left, _right) {
    var left = _left; // 左边
    var right = _right - 1; // 右边
    var temp = arr[left]; // 基准值,一般是第一个数,比基准值大的数都在基准值右边,比基准值小的数都在基准值左边。
    if (left >= right) {
        return;
    }
    while(left != right) {
        while(arr[right] >= temp && left < right) {
            right--;
        }
        arr[left] = arr[right]; // 从最右边找到第一个比基准值小的数,赋值到前面去。留出空位arr[right](虽然是空位,但是实际还有值,只不过该位置的值待填入)
        while(arr[left] <= temp && left < right) {
            left++;
        }
        arr[right] = arr[left]; // 从最左边开始找第一个比基准值大的数,放入到上一步留出的空位,然后留出空位arr[left]。
    }
    // 考虑right = left + 1时,假设left是空位,right上的数据小于temp,arr[right]填到arr[left]上去,left++,此时left和right都指向right。最后把temp填到right上
    // 考虑right = left + 1时,假设left是空位,right上的数据大于temp,则right--,空位仍然是left。最后再把temp填到left上
    // 此时left = right
    arr[left] = temp; // 这样一趟就遍历完了,每趟都会遍历n次
    // 递归,最多递归n-1次,此时时间复杂度是n + n - 1 + ... + 1。最少递归logn次,此时时间复杂度是nlogn。
    if (_left < left - 1) {
        quickSort(arr, _left, left - 1);
    }
    if (left + 1 < _right) {
        quickSort(arr, left + 1, _right);
    }
}
// 选择第一个作为基准值,从第二个数开始每次遍历将小的数跟前面大的数交换(或者自己跟自己交换),并且使用index=left+1开始进行计数用于记录小的数的数量
// 遍历结束以后将第一个基准值和最后一个小的数即index位置的数交换,这样基准值右边的都是大的数,左边都是小的数
function quickSort2 (arr, _left, _right) {
    var left = _left;
    var right = _right;
    if (left >= right) {
        return;
    }
    var index = left + 1;
    var temp = arr[left]; // 基准值
    for (var i = index;i < _right;i++) {
        if (arr[i] < temp) {
            swap(arr, index, i); // 将小的数放在前面,使用index记录最后一个小的数的位置
            index++;
        }
    }
    swap(arr, left, index - 1); // 基准值和最后一个小的数交换,此时基准值在中间位置
    if (left < right) {
        quickSort2(arr, _left, index - 2); // 将左边的小的数排序
        quickSort2(arr, index, _right); // 将右边的大的数排序
    }
}

var arr1 = [5, 3, 3, 36, 24, 19, 1, 92];
var arr2 = [5, 3, 1];

// Bubble(arr1);
// console.log(arr1);
// Choose(arr1);
// console.log("选择排序", arr1);
// quickSort2(arr1, 0, arr1.length);
// quickSort2(arr2, 0, arr2.length);
// console.log("快速排序", arr1);
// console.log("快速排序", arr2);

// 归并排序
function mergeSort(arr, left, right) {
    const mid = Math.floor((left + right) / 2);
    if (left < right) {
        mergeSort(arr, left, mid);
        mergeSort(arr, mid+1, right);
        merge(arr, left, mid, right);
    }
}

function merge(arr,left, mid, right) {
    const temp = [];
    let i = left; // 左侧
    let j = mid + 1; // 右侧
    let k = 0;
    while(i <= mid && j <= right) {
        if (arr[i] <= arr[j]) {
            temp[k] = arr[i];
            k++;
            i++;
        } else {
            temp[k] = arr[j];
            k++;
            j++;
        }
    }
    while (i <= mid) {
        temp[k++] = arr[i++];
    }
    while (j <= right) {
        temp[k++] = arr[j++];
    }
    for(let s = 0;s < temp.length;s++) {
        arr[left++] = temp[s];
    }
}

mergeSort(arr1, 0, arr1.length - 1);
console.log(arr1);

10. 防抖和节流

防抖和节流都是为了限制短时间内多次触发事件,区别在于防抖短时间内触发会重置定时器导致函数一直推迟执行(非立即执行和立即执行都会一直推迟),而节流就是按照时间间隔去执行(立即或非立即)。

防抖也是可以立即执行的,立即执行的防抖就是时间间隔内最初就开始执行然后时间间隔内不再执行,频繁点击时会一直推迟状态的翻转,停下来以后也不再执行函数。当下一次触发时又会立即执行。

useDebounce优化了lodash的防抖函数,做了那些优化,或者说普通的防抖函数有什么问题:

这个问题有好几家都问了,大概意思是用了lodash的防抖函数包一层,函数里拿不到最新的state值,而ahooks的useDebounce可以解决。当时觉得这个问题应该是个很常见的问题,我怎么没遇到过?我好菜啊,后来看了一下ahooks以及结合百度到的问题,发现场景就是输入框要实时输入内容去搜,但是请求需要做防抖操作,一开始这样写的:

Button上面加的没问题,但是Input上不能给整个change事件加防抖,因为要受控。这个时候就发现问题了,search方法会执行多次: 

究其原因,无非就是search重复赋值,debounce执行了4次,创建了4个定时器执行了4遍,所以需要useCallback来避免重复声明:

const search = useCallback(debounce((v) => {
    console.log(v);
  }, 1000), []);

然后这样是肯定不能加input输入得内容的依赖,不然重复声明又挂了,所以面试官开始搞事情了, 他直接问使用debounce会拿不到新的state值怎么办,你当然可以回答使用useDebounce,他的原理是声明一个自定义hook,将value值作为props传进去:

function useDebounce<T>(value: T, options?: DebounceOptions) {
  const [debounced, setDebounced] = useState(value);

  const { run } = useDebounceFn(() => {
    setDebounced(value);
  }, options);

  useEffect(() => {
    run();
  }, [value]);

  return debounced;
}

useDebounceFn方法本质上就是使用useMemo做缓存,useDebounce这样的一个hook在执行时从props拿的value,当然能拿到最新的state,然后监听返回的debounced值变化,这样又得加useEffect去发送api请求,但这样很冗余,对于输入框的场景,你可以直接告诉面试官,e.target.value就是最新的,干嘛还这么麻烦?search(e.target.value)不行吗??因为debounce返回的是个匿名函数,里面的执行是fn.apply(this, args);。注意这个args,他就是传过去的值。或者我把state记在useRef里也是一种方法,非得问甲骨文。

11. newWorker开启多进程

12. 函数式编程

13. instanceof

用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

14 new干了哪些事

创建空对象,实例对象的隐式原型__proto__等于构造函数的原型,改变this指向,执行构造函数。

判断return,返回的引用类型,new就不起作用了。

手写new

function myNew(fn, ...args) {
var obj = {};
Object.setPrototypeOf(obj, fn.prototype);
var result = fn.apply(obj, args);
return result instanceof Object ? result : obj;
}

15 手写promise,发布订阅

16 性能优化

开启多线程打包TerserPlugin插件设置parallel: true,webpack4推荐thread-loader,js和ts编译都开启多线程。

检查代码中给组件加key

本地考虑使用vite,热更新。

使用runtimeChunk提取manifest部分代码(管理所有模块的交互代码),生成的文件里有个manifest.js文件。runtime是程序运行时连接模块所需的加载和解析逻辑,是用来连接模块化程序用到的所有代码。manifest是打包时产生的,记录连接模块的详细要点,import和require语法转化成__webpack_require__方法获取到模块,runtime通过manifest的代码找到标识符对应的模块。

Tree Shaking删除import但是没有使用的文件代码,但是会有副作用,如引入一个js文件,文件里设置了document.title='xxx',如果删掉则无法触发修改title。因此不建议使用tree-shaking,否则出现问题难以排查。sideEffects可以帮我们排查副作用的文件,sideEffects的值是个数组,数组里的文件没有使用也可以保证不被删除。

stats: "errors-only",只显示错误信息,删除进度条和其它信息。

IgnorePlugin

noParse处理jquery、lodash或者一些min.js文件,这些文件也没有其它依赖,加到noParse列表里让webpack不去解析,提高打包效率。

devtool: 'source-map'方便定位源文件中的错误。收集错误使用addEventListener("error", callback)

拆包过程需要注意将node_modules拆的更细致一点:

const splitConfig = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 30000, // 大于30kb的才会被分割
      maxSize: 0, // 最大没有限制
      minChunks: 3, // 要提取的chunk最少被引用1次
      maxAsyncRequests: 5, // 按需加载时并行加载的文件的最大数量
      maxInitialRequests: 3, // 入口js文件最大并行请求数量
      automaticNameDelimiter: '~', // 名称链接符
      name: true, //可以使用命名规则
      cacheGroups: { // 分割chunk的组
        // node_modules文件会被打包到vendors组的chunk中。--> vendors~xxx.js
        antd: {
          name: 'antd',
          test: /[\\/]node_modules[\\/](antd)(.*)/,
          chunks: 'all',
          priority: 10,
          minChunks: 1,
          reuseExistingChunk: true
        },
        react: { 
          name: "react",
          test: /[\\/]node_modules[\\/]react(.*)?[\\/]/,
          chunks: "all",
          minChunks: 1,
          priority: 30,//打包权重
          reuseExistingChunk: true

        },
        moment: { //减小代码体积-reac-dom react-router-dom一起打包成单独文件
          name: "moment",
          test: /[\\/]node_modules[\\/]moment(.*)?[\\/]/,
          chunks: "all",
          minChunks: 1,
          priority: 50,//打包权重
          reuseExistingChunk: true

        },
        design: {
          name: 'ant_design',
          test: /[\\/]node_modules[\\/](@ant-design)(.*)?[\\/]/,
          chunks: 'all',
          priority: 20,
          minChunks: 1,
          reuseExistingChunk: true

        },
        vendors: {
					name: "vendor",
					test: /[\\/]node_modules[\\/]/,
					priority: -20,
				},
      }
    },
    minimize: true,
    minimizer: [new TerserPlugin({
      parallel: true,
				cache: true,

      terserOptions: {
        compress: {
          // drop_console: true,
        },
        output: {
          comments: true,
        },
      },
    }),
    new OptimizeCSSAssetsPlugin({}),
    ],
    runtimeChunk: {
      name: "manifest", // 模块信息清单
    },
  },
};

主要比较大的是antd,@ant-design,moment和lodash这几个库,即使拆出来,单个的文件也还是比较大,这时候可以考虑cdn,第一次先打包出来这几个第三方库, 放在静态资源服务器上,webpack添加externals选项,这样以后打包就不打包这几个库,运行时在直接加载这几个库。当然打包出来的文件还可以在压缩一下:npm i compression-webpack-plugin@5.0.1。注意compression-webpack-plugin的版本。

然后还需要nginx配合设置一下:

项目性能优化之用compression-webpack-plugin插件开启gzip压缩,以vue为例 - 知乎

使用babel-loader时,可以使用@babel/plugin-transform-runtime:

antd4和之前版本在.babelrc使用babel-plugin-import按需加载antd中的less或css,antd5使用cssinjs内置了。

17 算法nodejs版

一般有两个变量i和j同时控制循环或者循环的值i经常具有跳跃性,如i从1变到3,又从3变到2就用

while循环,常规的遍历才用for循环。

表达式求值:

const rl = require("readline").createInterface({ input: process.stdin });
var iter = rl[Symbol.asyncIterator]();
const readline = async () => (await iter.next()).value;

// 表达式求值顺序:先乘除,后加减,先左边,后右边,先括号内,后括号外
// 使用两个栈的方式,一个寄存运算符称为A,一个寄存数据称为B。
// ch不是运算符,则压入B,读入下一个字符。
// ch是运算符,比较A中栈顶元素和ch优先级,ch优先级高的话,ch压入A栈,继续遍历。栈顶优先级高的话,就取出栈顶和B中两个数据计算,得到的结果压入B栈。
// 然后继续循环从A栈顶去运算符和ch比较。
// 当ch为)时且遇到栈顶为(,则从A中弹出左括号,ch也不做操作,相当于(x)换成x,这一步是存在的,因为ch为)时,优先级总是最小的,所以会把()里面的都处理完。
// 即在上一步的循环中就会循环到A栈顶为(的情况。
// 按照数据结构上描述,当遍历整个表达式结束以后,此时还没计算结束,但此时右边的优先级都高于左边,所以需要while循环重复从栈里取数字和运算符,直到运算符栈为空
// 而为了统一,需要在一开始就给表达式加上前缀(后缀),尤其是末尾加上)作用很明显,因为可能到最后了右边都是高优先级,加上),它们都比)优先级高就可以一直循环直到运算符栈为空
// 例如2+3*5,+比*优先级低,优先级函数priority不好复用,改成(2+3*5),遍历到最后的)时,*比)高,计算得(2+15),此时+又比)优先级高计算得(17),然后(出栈,运算符栈为空计算结束。
// 这就不难理解课本的循环是while(ch != "#" || getTop(OPTR) != "#"),当然课本上前后加了#也是为了在末尾加一个低优先级的。
// 当然可以for循环表达式,给表达式加上(),for循环到最后一个字符)时,通过while循环把剩下的计算处理了。
// 运算符栈为空时,此时数据栈的数值就是计算结果。
void async function () {
    // Write your code here
    while (line = await readline()) {
        evalExpression(line);
    }
}()
// 传入表达式
function evalExpression(str) {
    let numbers = []; // 存数字
    let opts = []; // 存运算符
    let newStr = str;
    if(str[0] != "(") {
        newStr = "(" + str + ")";
    }
    opts.push(newStr[0]); // 第一个左括号直接入栈,且优先级最低
    let flag = false; // 表示是否符号是否是正负号,如-1*(-1-1)。出现一次运算符以后置为true,下一次还是运算符的话看做是正负号,走处理数字的分支。
    for (let i = 0; i < newStr.length; i++) {
        let data = newStr[i];

        if (/^\d+$/.test(data) || flag) {
            let j = i + 1;

            while(/^\d+$/.test(newStr[j])) {
                data += newStr[j];
                j++;
            };
            numbers.push(data); // 数字入栈
            i = j - 1;
            flag = false;
        } else {
            // 获取栈顶,和当前字符比较优先级,考虑2*(3+4*5),会循环两次,第一次计算4*5得到2*(3+20),这时候3+20优先级高于),在计算一次得2*(23)
            while(priority(opts.slice(-1)[0], data)) {
                compute(numbers, opts); // 优先级较高就先计算前面的,如果是2*3+1,只会循环一次得6+1,但不会去计算得到7,因为1后面可能是左括号或者*/。
            }
            if (opts.slice(-1)[0] == "(" && data == ")") {
                opts.pop(); // 去掉运算符栈顶的(,继续遍历,如3*(2+1)计算得到3*(3)以后,左括号出栈,有括号也没必要入栈,继续遍历。
            } else {
                opts.push(data); // 如2+3*4,*优先级更高,不能计算2+3,因此*入栈即可,然后继续遍历
            }
            if (data == "(") {
                flag = true;
            }
        }
    }
    console.log(numbers[0]);
}

// 定义运算符优先级,m是栈顶元素,n是取出来的字符ch。上一个运算符优先级高的话,就要先计算,这也符合实际场景。
function priority(m, n) {
    // n="("时,优先级总是大于m。m="("时,优先级又总是最低的。
    if (m == "(") {
        return false;
    } else if (m == "+" || m == "-") {
        if (n == "*" || n == "/" || n == "(") {
            return false;
        }
    } else if (m == "*" || m == "/") {
        if (n == "(") {
            return false;
        }
    }
    // n为空时,遍历结束。这时认为是m优先级更高,执行计算的操作。直到m也是空。
    return true;
}
// 使用数组模拟栈,计算时从arr1取出两个数字,从arr2取出运算符,计算结果添加到arr1中
function compute(arr1, arr2) {
    const a2 = Number(arr1.pop());
    const a1 = Number(arr1.pop());
    const operate = arr2.pop();
    let result;
    if (operate == "+") {
        result = a1 + a2;
    }
    if (operate == "-") {
        result = a1 - a2;
    }
    if (operate == "*") {
        result = a1 * a2;
    }
    if (operate == "/") {
        result = a1 / a2;
    }
    arr1.push(result);
}

连续的公共子序列:

const rl = require("readline").createInterface({
    input: process.stdin,
    output: process.stdout
});

let arr = [];
let str1,str2;
rl.on('line', function (line) {
    arr.push(line);
    if (arr.length == 2) {
        str1 = arr[0];
        str2 = arr[1];
        commonStr(str1, str2);
    }
});
// y a a x  和 z a x y 

// 连续的最大公共子序列,两个片段的末尾不相等,则认为是0,如果相等则dp[i][j] = dp[i-1][j-1]+1。最后计算二维数组中最大值。
function commonStr(m, n) {
    // 当i或j为0时,i-1或j-1为-1,所以构造的二维数组加1列和1行,初始值记为0。子序列的比较从[1,1]到[n+1,m+1]算。
    const dp = [];
    let maxLength = 0;
    // 初始化数组记录
    for (let i = 0; i <= m.length; i++) {
        dp[i] = [0];
        if (i == 0) {
            for (let j = 1; j <= n.length; j++) {
                dp[i].push(0);
            }
        }
    }
    for (let i = 1; i <= m.length; i++) {
        for (let j = 1; j <= n.length; j++) {
            if (m[i - 1] == n[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
                if (dp[i][j] > maxLength) {
                    maxLength = dp[i][j];
                }
            } else {
                dp[i][j] = 0;
            }
        }
    }
    console.log(maxLength);
}

深度和广度搜索:

其中深度搜索可以直接递归,即先访问数据,然后在循环遍历孩子节点,循环里进行递归。

也可以借助栈

//          1
//       2   3   4
//     5    6 7   8
//   9    10
const tree = {
    data: 1,
    next: [{
        data: 2,
        next: [{
            data: 5,
            next: [{
                data: 9,
                next: null
            }]
        }]
    },
    {
        data: 3,
        next: [{
            data: 6,
            next: [{
                data: 10,
                next: null
            }],
        }, {
            data: 7,
            next: null
        }]
    },
    {
        data: 4,
        next: [null, {
            data: 8,
            next: null
        }]
    }
    ]
}
// 深度优先搜索,将节点放在一个栈里。因为是先一直往下找,所以同一层次的节点从右到左的入栈,然后栈顶出栈访问并压入它的子节点。
// 对于图而言,还需要构造一个visited数组,这样一个节点如果有多个父节点,只会访问一次。
function dfs(tree) {
    const stack = [];
    stack.push(tree); // 根节点入栈
    while(stack.length) {
        const currentNode = stack.pop(); // 提取数组最后一个
        console.log(currentNode.data);
        if (currentNode.next && currentNode.next.length) {
            let i = currentNode.next.length;
            // 倒序入栈,不要currentNode.next.reverse().forEach,这样会改变原有tree的结构。
            while(i) {
                currentNode.next[i-1] && stack.push(currentNode.next[i-1]);
                i--;
            }
        }
    }
}
console.log("---深度搜索---");
dfs(tree);
// 广度搜索,构造一个队列,将子节点按从左到右入队,然后出队访问
function bfs(tree) {
    const stack = [];
    stack.push(tree); // 根节点入栈
    while(stack.length) {
        const currentNode = stack.shift(); // 提取数组第一个
        console.log(currentNode.data);
        if (currentNode.next && currentNode.next.length) {
            currentNode.next.forEach(item => {
                if(item) { // 非空节点
                    stack.push(item);
                }
            });
        }
    }
}
console.log("---广度搜索---");
bfs(tree);

KMP算法:

// BMP算法
// 主串 ababcabcacbab
// 子串 abcac
//       abcac 不行,因为和abcac和abcab不等。取出主串的abca和子串的abc比较,由于匹配到第5个字符不等,前面4个字符相等的,
//             所以可以看做是子串abca部分和去掉末尾以后的abc对比,因为abca同时是主串和子串序列。这样转化的好处是需要往前移到哪个字符对比只和子串自身有关。

//       abca 
//        abc 不行,因为c不等于a,这里就已经有规律了,即往前匹配时,先要保证找到的字符等于末尾,即a的前面一个a
//          a 此时只有一个字符a,这样就匹配上了1个字符,避免了还去从第4个字符开始重新匹配。实际上假设前面还有字符,想要匹配上只有可能是c,即ca去和abca的最后两位匹配。
//      这样的例子很容易推导出: 子串等于cabcax,主串为cabcabcax。第一次匹配卡在了主串的第6个字符b时,则前5个字符cabca和ca匹配上。这样由于第4,5位是ca对上了
//      所以下一次匹配是从子串的第三个字符b和主串的第6个字符b对比,当然如果主串的第6个字符不是b,那实际上就从ca开始往前找了,由于c不等于a,找不到相同的部分
//      所以这时就只能从第一个字符c和主串的第6个字符比(例:子串是cabcax,主串是cabcadcabcax,此时主串第6个字符不是b,要从ca往前找)。

// 将子串需要往前移的index下标放在next数组里记录,如abaabc就定义next是6维数组,next[6]表示第6个字符c匹配不上时,要返回的位置。
// 这时候考虑abaab,和ab匹配上,所以next[6]= 3,表示ab不用匹配了,从第三个字符a开始。又比如next[1]表示第一个字符就匹配不上,那只能拿子串的第一个字符重头开始匹配主串的下一个字符。
// next[j]计算思路方式很简单,如果用T表示子串,那么就是往前找到第一个和T[j-1]相等的位置k,然后还需要保证k之前的k-1个字符都和T[j-2]之前的k-1个字符相等。
// 因此next[j+1]=next[101]=k=10,其含义表示:1-10个字和91到100个字相等,即T[j-1]=T[k-1]。求next[j+1]是一个递归的过程,设next[j]=k。如果T[j-1]=T[k],则带上前面k-1个数就相等。
// 此时next[j+1]= next[j]+1。如果S中第j个数不等于第k个数,就去前面找匹配的字符,这时候去找next[k]= t,这时候如果第j个数T[j-1]= T[t-1],说明前面t-1个字带上第t个字T[t-1],一共t个字符是匹配的
// 此时next[j+1] = t + 1。

const str1 = "abaabcac";
// const str = "aba";
function get_next(T) {
    let i = 2;
    let next = [];
    next[1] = 0; // 第一个匹配不上,应该拿子串的第一个字符去匹配主串的下一个字符了。
    j = 0; // 保存next[i]的值。
    while (i <= T.length) {
        if (j == 0) {
            next[i] = j + 1; // 第一次进来的时候next[2] = 1; 子串的第二个字符对不上,表示下一次拿子串的第一个去匹配主串的当前字符。
            i++;
            j++;
        } else if(T[i-2] == T[j-1]) {
            // 求next[i]时,考虑第i-1个数和第next[i-1]=k个数是否相等
            next[i] = j + 1;
            j++;
            i++;
        } else {
            j = next[j];
        }
    }
    // next.shift(); // 去掉第一个即可。不建议去,因为j = next[j],j=1时就是死循环,而j需要从1变成0。
    return next;
}
console.log(get_next(str1)); // [undefined, 0, 1, 1, 2, 2, 3, 1, 2 ]

const S = "ababcabcacbab";
const T = "abcac";

function Index_KMP(S, T, _pos = 0) {
    let j = 0;
    let i = _pos;
    const next = get_next(T); // 获取next的计算值
    console.log("next:", next);
    while(i < S.length && j < T.length) { // 匹配没结束
        if (S[i] == T[j]) {
            // j = 0意味着也要重新开始匹配
            i++;
            j++;
        } else if(j == 0) {
            i++;
        } else {
            j = next[j];
        }

    }
    if(j == T.length) {
        return i - j + 1;
    } else return -1;
}
console.log(Index_KMP(S,T));

零钱兑换问题(动态规划):

// 给定一个零钱兑换的例子,其中有不同面额的零钱,如1元,5元,10元,
// 求解如何用这些零钱兑换一个给定的金额,并打印出所有可能的兑换方案。
const coins = [1, 2, 5]; // 面额
const amount = 5; // 总额
// 类似于数结构
const arr = [];
function dfs(amount, depth = []) {
    if(amount == 0) {
        arr.push(depth);
    } else if(amount > 0) {
        for(let coin of coins){
            if(amount - coin >= 0) {
                dfs(amount - coin, [...depth, coin]);
            }
        }
    }
}
dfs(amount);
console.log(arr);

18 Diff算法

主要是对同级节点进行复用,通常加了key值以后,React Diff算法根据相同的key去做patch修改,如果有相同的key的两个组件类型(如div)也一样,就直接复用,此时组件内如果改了文本,直接执行修改文本操作。因此如果有4个div,key值给的是索引,里面的文本分别是1,2,3,4。删除第2个div,然后遍历[1,3,4]去展示,展示的分别是1,3,4依然能保证正确。第2个节点进行了复用,但是文本从2->3,第3个节点也会复用,从3->4,但是如果是input框里的输入会显示错误(因为这里真的是复用了value)。key只是告诉react我们期望的复用组件,react还会根据这个key去对比实际内容。

        比较过程:如旧的是ABCD,新的是BACD。B在旧的里在第二个位置,这时候就会把第二个位置前面的都移到B后面(index和newIndex取最大值就是这个意思),因此如果是ABCD改成DABC,那么就会把旧的ABC依次放在D后面执行3次移动,而不是D移到ABC执行一次,这就是diff的缺点。如果面试官问你diff怎么优化,就回答可以先求出最长公共子串,然后把将剩下的进行移动,如ABCD和DABC的公共子串是ABC,那么这三个的顺序一致可以复用就不动,仅移动D。

19 react常见问题

高阶组件,无状态组件,受控组件和非受控组件的概念。执行上下文context通过一层层往下传props解决子组件通信问题。apply和call的区别以及第一个参数通常是实例对象obj。this的指向问题包括执行普通函数Fun(),此时谁调用this就指向谁,和执行let a = new Fun(),this指向实例对象a。使用propsType定义组件props的类型。ts中的常见用法以及泛型。

react redux工作流程如react-thunk中间件判断组件里发出的actions是对象还是函数,对象的话直接dispatch,函数的话通常是请求数据然后去dispatch,这些dispatch先经过store,再到reducer计算state的值,然后调用getState()得到新的state值做为props去更新组件。class继承的关键在于执行super()方法。

延迟加载defer和async的区别:相对于HTML的解析,他们的加载都是和html的解析(GUI渲染线程执行html解析,和js线程互斥)并行的,区别在于执行。defer中的js代码在dom加载完后才会执行不会阻塞html解析,async加载完就会执行js,并且执行js时会阻塞html解析。

预加载和预获取:prefetch的作用是该文件要等浏览器空闲时下载,而preload是正常跟父包一起下载,但两者都不会阻塞渲染,其中prefetch使用率更高。

点击需要显示一个组件,组件有用到第三方库,可以动态引入这个组件,通过注释的方式加上prefetch预获取。

在写一个按钮,点击显示Com组件,

浏览器从缓存里获取文件:

tsconfig配置的坑:

{
    "compilerOptions": {
        "outDir": "./dist/",
        "noImplicitAny": true,
        "module": "esnext",
        "target": "es5",
        "jsx": "react",
        "allowJs": true,
        "moduleResolution": "node",
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,
    },
    "include": [
        "src/**/*"
    ],
    "exclude": [
        "node_modules"
    ]
}

1.module是esnext而不是es6,如果你想使用动态import的话,即import("lodash").then()这种。

2. "allowSyntheticDefaultImports": true, "esModuleInterop": true,帮助你可以import _ from "lodash";否则只能import * as  _ from "lodash";

20 nodejs面试

nodejs全局变量:global变量,process进程变量,console,几个定时器。

内置模块:os模块,path(resolve相对路径转绝对,__dirname获取绝对路径),url,http模块,fs模块(readDir,stat判断是否是文件,readFile,append追加,unlink删除)。

require('')的过程:先计算绝对路径,然后判断是否在缓存里,没有的话判断是否是内置模块、npm安装的模块、自己写的模块,然后实例化模块放入缓存。然后加载模块,根据扩展名用不同的解析函数解析,export出来。commonjs暴露出来的值类型是深复制的,es暴露出来的值类型是同一个引用。

面试常见问题:

hook和class组件的区别

为什么不能在if里面使用hook?

state是同步的还是异步的?生命周期内同步,函数内异步。

使用哪些hook: useEffect,useCallback(返回函数), useMemo(返回函数执行结果)。避免子组件重复渲染:使用pureComponent或者useMemo。

自适应屏幕:width:100%, 100vw,flex布局。注意flex的三个意思。

qiankun微服务原理

第三方sdk的使用:打印构造函数名称,查看隐式原型__proto__,里面有所有的方法。

优化方案:

优化postcss计算rem方案,统一转化为750px二倍稿,项目里使用rem代替px。

webpack5官方给出的常用的代码分离方法有三种:

  • 入口起点:使用 entry 配置手动地分离代码。配置较多·,隐患多。
  • 防止重复:使用 入口依赖 或者 SplitChunksPlugin 去重和分离 chunk。
  • 动态导入:通过模块的内联函数调用分离代码。

闭包:外部作用域通过内部作用域返回的函数,访问内部作用域的闭包变量,优点是避免全局污染,缺点是造成内存泄漏,闭包访问的内部变量不能及时回收。内存泄漏只要是定时器未清除,DOM引用未清除,全局变量一直在内存中,console.log的使用,闭包用的太多造成的泄漏。

const add = function (a, b) {
   
  return a + b;
};

function addOne(num) {
   
  const one = 1;
  return function inner() {
   
    return add(num, one);
  };
}

const result= addOne(2);

console.log(result());

写过哪些组件: 早期的上拉刷新组件,swiper组件,表单+列表组件。弹幕组件,维护公司组件库。定制上传组件,验证码组件,antdesign+主题色方案。

强缓存协商缓存:

第一次请求获取cache-control,Last-modify、Etag等字段。

cache-control设置max-age=设置强缓存时长(s)。表示有效时间内走强缓存,超过时间走协商缓存,拿着第一次返回的Last-modify的值作为If-Modify-Since的值(简单理解就是换个名字)传给服务器,服务器拿If-Modify-Since和资源最后修改的时间对比,没变化返回304且不返回资源,继续走浏览器缓存(内部缓存或者磁盘缓存)。

ETag/If-None-Match与Last-modify/If-Modify-Since类似,ETag是文件唯一标识符,文件资源发生变化ETag就会变化,ETag弥补了上面last-modified可能出现文件内容没有变化但是last-modified发生了变化出现重新向服务器请求资源情况。这个值也是服务器返回的。

dist/index.html:强缓存
css,js,图片:协商缓存,文件带上contenthash

从0-1设计一个react项目:

1.选择webpack,配置里面的loader和plugin根据package.json里的命令区分环境,可使用cross.env

2.本地使用devserver在内存中运行文件,有哪些文件可以在控制台network里请求。

3. babel-loader编译es6语法,具体配置在babel.config.json(.babelrc)里,支持react的话也需要在里面加上。

4. ts-loader编译ts,具体配置在tsconfig.json,如ts里需要动态引入的话,就是在这个文件加两个属性。

5. react-router的配置

6.网络请求的封装如axios

7.组件库的选择如antdesign

8.如不用自己webpack配置,可以使用umi搭建spa项目,nextjs搭建ssr项目。

http状态码:301,302重定向。400前端语法错误,401未授权,403拒绝访问。404请求不到资源。500后端出错。304与协商缓存有关。

useState传函数递增和传值递增的区别:useState是可以批量合并的(在promise或者定时器里使用的话,还跟react版本有关,据说17和之前这里不会合并),传值的话,实际效果相当于只加一次,传函数可以在更新state队列时将上一次的当作参数获取到,做到真正的递增。state在settimeout有闭包问题,这时候useState要传函数。所以useState到底是同步还是异步的,主要看执行环境,一般情况下在一个点击事件里写是异步的,拿的是旧值,但是如果是

setState(v => v+1);
setState(v => v+1);

第二行的参数v就是+1以后的值,此时在函数体v=> v+1内可以认为是同步的。如果是useEffect和useCallback加了依赖也可以认为是同步的,另外传给一个子组件作为他的props也是同步的。如果想把异步变成同步,可以用useRef记一份值。

antd5:使用cssinjs动态加载css

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

heine162

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

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

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

打赏作者

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

抵扣说明:

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

余额充值