前端知识体系~持续更新

287 阅读9分钟

jolin27144/blog: blog (github.com)

HTML

CSS

JavaScirpt

柯里化

1.写一个currying,要求 add(1)(2)(3)(4) 打印10。

function add(...args) {
  return args.reduce((acc, item) => acc + item);
}

function currying(fn) {
  let args = [];

  function _c(...newArgs) {
    if (newArgs.length) {
      // 合并参数
      args = [...args, ...newArgs];
      // 继续返回函数
      return _c;
    } else {
      // 返回执行结果
      return fn.apply(null, args);
    }
  }

  /**
   * 追加的
   * =====================================
   * 利用console.log()打印函会调用fn.去掉最后的()
   */
  //_c.toString = () => {
  //  return fn.apply(null, args);
  //};

  return _c;
}
let addCurry = currying(add);
console.log(addCurry(1)(2)(3)(4)());

// 追加的
// console.log(addCurry(1)(2)(3)(4));

大数相加

V8

图解V8笔记

1-1. V8是如何执行一段JavaScript代码的

因为计算机只能识别二进制指令,所以要让计算机执行一段高级语言通常有两种手段,第一种是将高级代码转换为二进制代码,再让计算机去执行;另外一种方式是在计算机安装一个解释器,并由解释器来解释执行(当然最后CPU跑的还是二进制指令。只不过解析器抽象了一层。还是那句话,解决不了就再加一层。同样这也是较慢的原因)。

解释执行和编译执行都有各自的优缺点,解释执行启动速度快,但是执行时速度慢,而编译执行启动速度慢,但是执行速度快。为了充分地利用解释执行和编译执行的优点,规避其缺点,V8 采用了一种权衡策略,在启动过程中采用了解释执行的策略,但是如果某段代码的执行频率超过一个值,那么 V8 就会采用优化编译器将其编译成执行效率更加高效的机器代码。

理解了这一点,我们就可以来深入分析 V8 执行一段 JavaScript 代码所经历的主要流程了,这包括了:

  • 初始化基础环境;解析源码生成 AST 和作用域;
  • 依据 AST 和作用域生成字节码;解释执行字节码;
  • 监听热点代码;优化热点代码为二进制的机器代码;
  • 反优化生成的二进制机器代码。

这里需要注意的是,JavaScript 是一门动态语言,在运行过程中,某些被优化的结构可能会被 V8 动态修改了,这会导致之前被优化的代码失效,如果某块优化之后的代码失效了,那么编译器需要执行反优化操作。

内存回收机制

1. let obj = new Vue()。这个obj什么时候会回收。直接new Vue呢

Node.js

跨端

小程序

react-native

前端工程化

webpack

1.loader和plugin的区别是什么?

2.webpack打包优化

网络

Http、http2、http3

1. options请求是什么?有什么作用?

安全

1.xss和csrf

浏览器

浏览器工作原理

常用数据结构TS实现

链表

单向链表:用next指针将节点连接在一起。

实现思路:节点Node类+ 链表LinkedList类。

Node类

image.png

LinkedList类

image.png

双向链表:单向链表的基础上多维护一个prev指针

实现思路:继承Node类,增加一个prev指针。继承单向链表,重写与单向链表不同的相关函数

DoublyNode类 继承 Node类 image.png

DoublyLinkedList类 继承 LinkedList类 image.png

循环链表:单向链表基础上,尾节点指向头节点。

完整代码在这: blog/src/data-structure/一维数据结构/链表 at master · jolin27144/blog (github.com)

将[1,2,5,12,7,17,25,19,36,99,22,24,46,92]这一组数,看成一个完全二叉树。

调整后,若所有父节点比子节点小,则为小根堆。

调整后,所所有父节点比子节点大,则为大根堆。

这种数据结构称为堆。

小根堆

/**
 *
 * 树的性质
 * 对于每个节点有NUM_CHILDREN个子节点,而不是2个,则公式为:
 *                 root at 0                  root at 1
 * Left child    index*NUM_CHILDREN + 1     index*NUM_CHILDREN
 * Right child   index* NUM_CHILDREN + 2    index*NUM_CHILDREN + 1
 * Parent        (index-1)/NUM_CHILDREN     index/NUM_CHILDREN
 *
 */

/**
 * 构建小根堆
 */
function buildMinHeap(arr) {
  // 向下取整
  for (let i = parseInt(arr.length / 2); i >= 0; i--) {
    sink(arr, i);
  }
}

/**
 * @description 下沉(这里是构建小根堆,大的父节点下沉)
 * @param {number[]} arr 堆
 * @param {number} parentIndex 需要判断是否需要下沉的节点索引
 * @returns arr
 */
function sink(arr, parentIndex) {
  // 先将最小节点的索引指向父节点
  let minIndex = parentIndex;
  // 左节点索引
  let leftNodeIndex = 2 * parentIndex + 1;
  // 右节点索引
  let rightNodeIndex = 2 * parentIndex + 2;
  // 判断是否有左节点
  const hasLeftNode = () => arr[leftNodeIndex] !== undefined;
  // 判断是否有右节点
  const hasRightNode = () => arr[rightNodeIndex] !== undefined;
  // 根据完全二叉树性质,没有左子节点,则已经没有子节点,不需要判断下沉。
  if (!hasLeftNode) {
    return;
  }
  // 如果有左节点且比父节点小。
  if (hasLeftNode && arr[parentIndex] > arr[leftNodeIndex]) {
    // 最小节点的索引指向左节点
    minIndex = leftNodeIndex;
  }
  // 如果有右节点且比父节点小。
  if (hasRightNode && arr[minIndex] > arr[rightNodeIndex]) {
    // 最小节点的索引指向右节点
    minIndex = rightNodeIndex;
  }
  // 如果父节点不是最小的节点。
  if (minIndex !== parentIndex) {
    // 父节点与最小的节点交换
    [arr[parentIndex], arr[minIndex]] = [arr[minIndex], arr[parentIndex]];
    // 交换后,以与父节点交换的子节点的索引,作为新的父节点,继续判断是否需要下沉
    return sink(arr, minIndex);
  }
  return arr;
}

/**
 * @description 上浮
 * @param {*} arr
 * @param {*} index
 * @returns
 */
function swim(arr, index) {
  // 父节点索引
  const parentIndex = parseInt((index - 1) / 2);
  // 是否有父节点
  const hasParent = () => arr[parentIndex] !== undefined;
  // 如果有父节点,并且大于自己
  if (hasParent && arr[parentIndex] > arr[index]) {
    // 和父节点交换
    [arr[parentIndex], arr[index]] = [arr[index], arr[parentIndex]];
    return swim(arr, parentIndex);
  }
  return arr;
}

插入
/**
 * 插入
 * 尾部新增一个节点
 */
function insert(arr, el) {
  arr.push(el);
  // 插入叶子节点要进行上浮处理
  swim(arr, arr.length - 1);
}
删除
/**
 * 删除
 * 删除堆顶元素
 */
function deleteLast(arr) {
  // 先交换堆顶元素,和最后一个元素。
  [arr[0], arr[arr.length - 1]] = [arr[arr.length - 1], arr[0]];
  // 此时删除的是堆顶元素
  arr.pop();
  // 堆最后一个元素,被移到了堆顶,需要重新构建堆
  sink(arr, 0);
}
排序
/**
 * 堆排序
 */
function heapSort(heap) {
  // 排序后的元素
  let sort = [];
  // 最小堆
  let minHeap = [...heap];
  while (minHeap.length !== 0) {
    // 先交换堆顶元素,和最后一个元素。
    [minHeap[0], minHeap[minHeap.length - 1]] = [
      minHeap[minHeap.length - 1],
      minHeap[0]
    ];
    // 此时删除的是堆顶元素
    sort.push(minHeap.pop());

    // 堆最后一个元素,被移到了堆顶,需要重新构建堆
    sink(minHeap, 0);
    // [2, 5, 3, 7, 19, 17, 46, 12, 99, 22, 25, 28, 36, 92]
    // console.log(heap);
  }
  return sort;
}

大根堆

实现思路一样,只是下沉的是小节点。上浮的是大节点。

数据结构和算法

数组

1.二分查找

function search(nums: number[], target: number): number {
  let left = 0;
  let right = nums.length - 1;
  while (left <= right) {
    const mid = (left + right) >> 1;
    if (nums[mid] === target) {
      return mid;
    }
    if (nums[mid] > target) {
      right = mid - 1;
    } else {
      left = mid + 1;
    }
  }
  return -1;
}

export { search };

2.移除元素

function removeElement(nums: number[], val: number): number {
  let slowIndex = 0;
  let fastIndex = 0;
  for (; fastIndex < nums.length; fastIndex++) {
    if (val !== nums[fastIndex]) {
      nums[slowIndex] = nums[fastIndex];
      slowIndex++;
    }
  }
  return slowIndex;
}

export {removeElement}

3.有序数组的平方

function sortedSquares(nums: number[]): number[] {
  const result: number[] = new Array(nums.length).fill(0);
  let numsLeft = 0;
  let numsRight = nums.length - 1;
  let resultRight = nums.length - 1;

  // numsLeft和numsRight相等时,是最后一次处理
  while (numsLeft <= numsRight) {
    if (Math.pow(nums[numsLeft], 2) > Math.pow(nums[numsRight], 2)) {
      result[resultRight] = Math.pow(nums[numsLeft], 2);
      numsLeft++;
    } else {
      result[resultRight] = Math.pow(nums[numsRight], 2);
      numsRight--;
    }
    resultRight--;
  }
  return result;
}

export { sortedSquares };

4.长度最小的子数组

function minSubArrayLen(target: number, nums: number[]): number {
  let result: number, fastIndex: number, slowIndex: number, sum: number;

  fastIndex = slowIndex = sum = result = 0;

  for (; fastIndex < nums.length; fastIndex++) {
    sum += nums[fastIndex];

    while (sum >= target) {
      const subLength = fastIndex - slowIndex + 1;
      if (result === 0) {
        result = subLength;
      } else {
        result = Math.min(subLength, result);
      }
      sum -= nums[slowIndex++];
    }
  }

  return result;
}

export { minSubArrayLen };

5.螺旋矩阵 II

function generateMatrix(n: number): number[][] {
  let matrix: number[][] = Array.from({ length: n }).map(() => new Array(n));
  let left = 0;
  let top = 0;
  let right = n - 1;
  let bottom = n - 1;
  let num = 1;

  while (num <= n * n) {
    for (let i = left; i <= right; i++) {
      matrix[top][i] = num++;
    }
    top++;

    for (let i = top; i <= bottom; i++) {
      matrix[i][right] = num++;
    }
    right--;

    for (let i = right; i >= left; i--) {
      matrix[bottom][i] = num++;
    }
    bottom--;

    for (let i = bottom; i >= top; i--) {
      matrix[i][left] = num++;
    }
    left++;
  }

  return matrix;
}

export { generateMatrix };

链表

1.移除链表元素

function removeElements(head: ListNode | null, val: number): ListNode | null {
  if (head === null) {
    return null;
  }

  let current: ListNode | null = head;

  while (current) {
    // 头节点的情况
    if (current.val === val) {
      head = current.next;
      current = head;
      continue;
    } else if (current.next && current.next.val === val) {
      current.next = current.next.next;
      continue;
    }
    current = current.next;
  }

  return head;
}

2.设计链表

// 链表
class MyLinkedList {
  // 链表长度
  private size: number;
  private head: LinkedNode | null;

  constructor() {
    this.head = null;
    this.size = 0;
  }

  protected validateIndex(index: number): boolean {
    return index >= 0 && index <= this.size - 1;
  }

  // 此方法,方便插入、删除节点
  getNodeAtIndex(index: number): LinkedNode | null {
    if (!this.validateIndex(index)) {
      return null;
    }

    let i = 0;
    let current = this.head as LinkedNode;
    while (i !== index) {
      current = current.next as LinkedNode;
      i++;
    }
    return current;
  }

  get(index: number): number {
    const node = this.getNodeAtIndex(index);
    if (node instanceof LinkedNode) {
      return node.val;
    }
    return -1;
  }

  // FIXME
  addAtHead(val: number): void {
    const myLinkNode: LinkedNode = new LinkedNode(val);
    const head = this.head;
    
    // TODO
    // const dummyHead = new LinkedNode(0)
    // dummyHead.next = this.head
    
    // 空链表
    if (!head) {
      this.head = myLinkNode;
    } else {
      // 非空
      myLinkNode.next = head;
      this.head = myLinkNode;
    }
    this.size++;
  }

  addAtTail(val: number): void {
    const myLinkNode: LinkedNode = new LinkedNode(val);
    const head = this.head;

    // 空链表
    if (!head) {
      this.head = myLinkNode;
    } else {
      // 非空
      let current = head;
      while (current.next) {
        current = current.next;
      }
      current.next = myLinkNode;
    }

    this.size++;
  }

  addAtIndex(index: number, val: number): void {
    if (index <= 0) {
      // 如果index小于等于0,则在头部插入节点。
      this.addAtHead(val);
      return;
    } else if (index === this.size) {
      // 如果 index 等于链表的长度,则该节点将附加到链表的末尾
      return this.addAtTail(val);
    } else if (index > this.size) {
      // 如果 index 大于链表长度,则不会插入节点
      return;
    }

    const myLinkNode: LinkedNode = new LinkedNode(val);
    // index 是有效的, 因此一定能返回节点
    const pre = this.getNodeAtIndex(index - 1) as LinkedNode;
    myLinkNode.next = pre.next;
    pre!.next = myLinkNode;
    this.size++;
  }

  deleteAtIndex(index: number): void {
    // 0开始。无效索引返回
    if (!this.validateIndex(index)) {
      return;
    }
    if (index === 0) {
      // 删除头节点
      this.head = this.head!.next;
    } else if (index === this.size - 1) {
      // 删除尾节点, 处理倒数第二个节点
      const pre = this.getNodeAtIndex(index - 1);
      pre!.next = null;
    } else {
      // 其他情况,一定会有pre.next.next;例如删除倒数第二个节点,取倒数第三个节点,则有pre.next.next
      const pre = this.getNodeAtIndex(index - 1);
      pre!.next = pre!.next!.next;
    }
    this.size--;
  }
}

3.反转链表

/*
 * 1、双指针解法(我个人理解这实际上是3个指针)
 * 图示意
 *       head->node1->node2->null
 * null<-head<-node1<-node2
 *
 *  双指针(我个人理解这实际上是3个指针)思路:A跟B 交换一次,那么A,B就反转了。如果不保存C,跟C就断了。因此要先取到C。
 *  因此这实际上是取三个节点指针,反转前两个节点。然后往后一位,重复。直到后一位是空
 */
// function reverseList(head: LinkedNode | null): LinkedNode | null {
//   // previousNode 指向空(假设是第一个节点)
//   let previousNode = null;
//   // currentNode  指向头(当作第二个节点)
//   let currentNode = head;
//   while (currentNode) {
//     // temp指向 第三个节点
//     const temp = currentNode.next;
//
//     // 反转, 令第二个节点指向第一个节点
//     currentNode.next = previousNode;
//
//     // 后移一位
//     previousNode = currentNode;
//     currentNode = temp;
//   }
//
//   return previousNode;
// }

/**
 * 2、递归解法
 * 跟循环原理一样。只是把循环改成递归。看注释
 */
function reverse(
  pre: LinkedNode | null,
  cur: LinkedNode | null
): LinkedNode | null {
  // 结束条件,cur指向null了,返回pre
  if (!(cur instanceof LinkedNode)) {
    return pre;
  }
  // const temp = cur.next;
  cur.next = pre;

  // 递归就是在做这个事件
  // pre = cur;
  // cur = temp;
  return reverse(cur, cur.next);
}

function reverseList(head: LinkedNode | null): LinkedNode | null {
  // 双指针开始调用
  return reverse(null, head);
}

4.两两交换链表中的节点

/**
 *   cur->1->2->3->4->5
 *
 *   step1: cur.next => 2
 *   step2: 2.next   => 1
 *   step3:1.next   => 3
 *
 *   一组交换完,cur向后移动两位
 *
 */
function swapPairs(head: ListNode | null): ListNode | null {
  // 构造虚拟头节点
  const dummyNode: ListNode = new ListNode(0, head);
  let cur = dummyNode;

  // 目的是将后两个节点交换,所以终止条件为后两个节点其中之一为空.
  while (cur.next !== null && cur.next.next !== null) {
    const temp1 = cur.next;
    const temp2 = cur.next.next;
    const temp3 = cur.next.next.next;

    cur.next = temp2;
    cur.next.next = temp1;
    cur.next.next.next = temp3;

    cur = cur.next.next;
  }

  return dummyNode.next;
}

5.删除链表的倒数第 N 个结点

哈希表

1.两数之和

/**
 * @description  循环的时候,通过hash表找前面有无符合的元素(思想:缓存),有则返回,没有则添加到hash
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
 function twoSum(nums, target) {
    const hashMap = new Map()
    for (let i = 0; i < nums.length; i++) {
        const item = nums[i]
        const key = target - item
        if (!hashMap.has(key)) {
            hashMap.set(item, i)
            continue;
        }
        return [hashMap.get(key), i]
    }
};


let nums = [2, 7, 11, 15], target = 9
const result = twoSum(nums, target)
console.log(result)
// 输入:nums = [2,7,11,15], target = 9
// 输出:[0,1]
// 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

滑动窗口

1. 无重复字符的最长子串

/**
 * @description 最长无重复子串。
 *              双指针,右一直加,加到有重复,就左一直加,直到无重复。(至于这个规律怎么推出来的,不知道)
 *              记录每个字串长度,找出最长的。
 * @param {string} s
 * @return {number}
 */
function lengthOfLongestSubstring(s) {
  if (!s) {
    return 0;
  }
  if (s.length === 1) {
    return 1;
  }
  let max = 0;
  const set = new Set();
  let left = 0,
    right = -1;
  for (; left < s.length; left++) {
    if (left !== 0) {
      set.delete(s[left - 1]);
    }

    while (right + 1 < s.length && !set.has(s[right + 1])) {
      set.add(s[right + 1]);
      ++right;
    }

    max = Math.max(set.size, max);
  }

  return max;
}

// 输入: s = "abcabcbb"
// 输出: 3 

1.数组中的第k大的元素(字节)

求第k大元素:

  1. 任取k个元素。
  2. 把k个元素构建成最小堆。
  3. 遍历剩下的元素,若比堆顶元素大,则替换堆顶的元素。堆顶元素下沉。
  4. 遍历完,等到k个元素构成的最小堆,堆顶元素即为第k大的元素。

举例:

求[1,2,3,4,5] 里面第3大的元素。

则先取[1,2,3]三个数,构成最小堆。

4比堆顶元素1大,则替换掉1,同时维护堆。变成[2,4,3]

5比堆顶元素2大,则替换掉2,同时维护堆。变成[3,4,5]

堆顶元素3,即为第三大的元素。

/**
 * @description 节点下沉(这里是构建小根堆,大的父节点下沉)
 * @param {array} arr
 * @param {number} index
 * @returns arr
 */
function sink(arr, index) {
  // 构建完成
  let notFinish = true;
  // 最小元素的索引
  let minIndex;
  // 有左节点并且未构建完成
  while (2 * index + 1 <= arr.length && notFinish) {
    // 父节点比左节点大
    if (arr[index] > arr[2 * index + 1]) {
      // 最小元素的索引为左节点
      minIndex = 2 * index + 1;
    } else {
      // 否则最小元素的索引为自己
      minIndex = index;
    }
    //  如果有右节点,则讨论右节点
    if (2 * index + 2 <= arr.length) {
      //左节点比右节点大
      if (arr[minIndex] > arr[2 * index + 2]) {
        //  最小元素的索引为右节点
        minIndex = 2 * index + 2;
      }
    }
    // 最小index不是自己,即有子节点比自己小,交换
    if (minIndex !== index) {
      // 交换
      [arr[index], arr[minIndex]] = [arr[minIndex], arr[index]];
      // 更新index为与自己交换的子节点,继续调整
      index = minIndex;
    } else {
      // 最小索引是自己,则当前节点比两个子节点都要小了。结束
      notFinish = false;
    }
  }
  // 返回小根堆
  return arr;
}

/**
 * 构建小根堆
 */
function buildMinHeap(arr) {
  // 向下取整
  for (let i = parseInt(arr.length / 2); i >= 0; i--) {
    sink(arr, i);
  }
}

/**
 * @description 找第k大的元素
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
function findKthLargest(nums, k) {
  // 取前k个数
  const arrleft = nums.slice(0, k);
  // 将其构建成小根堆
  buildMinHeap(arrleft);

  // 剩余的数,比堆顶大则替换,重新构建最小堆
  const arrRight = nums.slice(k);
  // 循环比较
  arrRight.forEach((item) => {
    // 若比堆顶元素大
    if (arrleft[0] < item) {
      // 替换
      arrleft[0] = item;
      // 下沉,重新构建最小堆
      sink(arrleft, 0);
    }
  });
  console.log(arrleft[0]);
  return arrleft[0];
}

其他

1.LRU缓存机制

/**
 * @description 实际上:通过顺序判断新鲜度即可,越靠后越新鲜
 * @param {number} capacity
 */

class LRUCache {
  capacity;
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map();
  }

  /**
   * @param {number} key
   * @return {number}
   */
  get(key) {
    if (!this.cache.has(key)) {
      return -1;
    }

    const value = this.cache.get(key);
    this.cache.delete(key);
    // 往最后插入
    this.cache.set(key, value);
    return value;
  }

  /**
   * @param {number} key
   * @param {number} value
   * @return {void}
   */
  put(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else {
      if (this.cache.size === this.capacity) {
        // 第一个,最不新鲜
        this.cache.delete(this.cache.keys().next().value);
      }
    }
    this.cache.set(key, value);
  }
}

const lRUCache = new LRUCache(2);

console.log(lRUCache);
/**
 * Your LRUCache object will be instantiated and called as such:
 * var obj = new LRUCache(capacity)
 * var param_1 = obj.get(key)
 * obj.put(key,value)
 */

项目类

1.编程式实例化并挂载Vue时,propData无法达到响应式的目的。