基础手写算法

153 阅读8分钟

1、最常见的两种攻击方式

XSS攻击:

XSS攻击是指攻击者在网站上注入恶意的客户端代码,通过恶意脚本对客户端网页进行篡改,

1、HttpOnly Cookie: 使用HttpOnly标志来设置Cookie,以防止JavaScript访问它们。这样,即使攻击者成功注入恶意脚本,也无法访问或窃取用户的Cookie信息。

2、用户的输入检查: 对用户输入的数据进行验证和过滤,以确保只接受预期的数据格式。这可以防止XSS攻击者尝试在输入中注入恶意脚本。

3、JS/HTML 转义: 在将用户输入嵌入到 HTML 页面时,使用合适的 HTML 转义方法,将特殊字符转换为实体编码。比如将 < 转义为 &lt;> 转义为 &gt;

CSRF攻击:

攻击者试图在受害者不知情的情况下以受害者身份执行未经授权的操作。这种攻击发生在受害者已经登录了某个网站的情况下,攻击者利用受害者的身份在该网站上执行恶意操作。

1、验证码: 要求用户在执行敏感操作之前输入验证码,这可以确保请求是由真正的用户发起的,而不是通过CSRF攻击。

2、Referer Check: 在服务端检查请求的Referer头,确保请求来自预期的来源。但请注意,Referer头并不总是可靠,因为它可以被伪造。

3、Token验证: 为每个用户会话生成一个独特的CSRF令牌,并将其与用户的请求一起发送。服务端在接收请求时验证令牌的有效性,以确保请求是合法的

2、优化相关

定位:

NetWork、Performance、webpack-bundle-analyzer、JSCP、抓包

方法:

webpack的tree shaking、slit chunks

Nginx配置的gzip压缩

图片(压缩、分割、雪碧图、懒加载、格式webp、svg、小图base64)

cdn

代码算法降低复杂度:节流防抖、v-if、v-for、路由懒加载、打包去掉map文件、UI库懒加载

web worker创造多线程环境、允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时, Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程

利用缓存:浏览器缓存、CDN、本地缓存、分布式缓存、数据库缓存。

SSR

HTTP2:解析速度快、多路复用、首部压缩

3、基本的算法题

防抖

    function debounce(func, delay) {
      let timeout
      return function (...args) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          func.apply(this, args)
        }, delay)
      }
    }

节流

    function throttle(func, delay) {
      let lastTime = 0;
      return function (...args) {
        const currentTime = Date.now();
        if (currentTime - lastTime >= delay) {
          func.apply(this, args);
          lastTime = currentTime;
        }
      };
    }

深拷贝

    function deepClone(obj) {
      if (obj === null || typeof obj !== "object") {
        return obj
      }
      if (Array.isArray(obj)) {
        const cloneArr = []
        for (let i = 0; i < obj.length; i++) {
          cloneArr[i] = deepClone(obj[i])
        }
      }
      if (typeof obj === 'obj') {
        cloneObj = {}
        for (const key in obj) {
          if (obj.hasOwnProperty(key)) {
            cloneObj[key] = cloneObj(obj[key])
          }
        }
      }
    }

promise.race

    function myPromiseRace(promsieList) {
      return new Promise((resolve, reject) => {
        list.forEach((promise, index) => {
          promise
            .then(value => resolve(value))
            .catch(err => reject(err));
        });
      });
    }

promise.all

    function myPromiseAll(promsieList) {
      return new Promise((resolve, reject) => {
        let result = []
        let sum = 0
        promiseList.forEach((promise, index) => {
          promise.then((value) => {
            result[index] = value
            sum++
            if (sum === promiseList.length) {
              resolve(result)
            }
          })
            .catch((err) => {
              reject(err)
            })
        })
      })
    }

new

    function _new(fn, ...args) {
      let obj = {};
      // 将这个对象的 __proto__ 属性指向构造函数的原型
      obj.__proto__ = fn.prototype;
      // 调用这个构造函数
      const result = fn.call(obj, ...args);
      // 如果构造函数返回了一个对象,则返回这个对象
      return result instanceof Object ? result : obj;
    };

快速排序函数

    function quickSort(arr) {
      if (arr.length <= 1) {
        return arr;
      }
      // 选择基准元素,中间元素
      const pivot = arr[Math.floor(arr.length / 2)];
      // 将数组分为左侧和右侧两个子数组
      const left = arr.filter(element => element < pivot);
      const middle = arr.filter(element => element === pivot);
      const right = arr.filter(element => element > pivot);
      // 递归地对左侧和右侧子数组进行快速排序,并将结果与中间元素连接起来
      return quickSort(left).concat(middle, quickSort(right));
    }

冒泡排序

    function bubbleSort(arr) {
      const len = arr.length
      for (let i = 0; i < len; i++) {
        for (let j = 0; j < len - 1; j++) {
          if (arr[j] > arr[j + 1]) {
            [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
          }
        }
      }
      return arr
    }

数组扁平化

    function flatten(arr) {
      let result = [];
      for (let i = 0; i < arr.length; i++) {
        if (Array.isArray(arr[i])) {
          result = result.concat(flatten(arr[i]));
        } else {
          result = result.concat(arr[i]);
        }
      }
      return result;
    }

寄生组合继承

    function Parent(name) {
      this.name = name;
      this.colors = ['red', 'green', 'blue'];
    }
    // sayName 方法被添加到 Parent 原型上,使所有由 Parent 创建的实例共享此方法。
      Parent.prototype.sayName = function () {
        console.log(this.name);
      };
      // Child 构造函数使用 Parent.call(this, name) 调用了 Parent 构造函数。这被称为"构造函数借用",允许 Child 实例继承 Parent 的属性。
      function Child(name, age) {
       Parent.call(this, name);
        this.age = age;
      }
     // 此行建立了原型链。它创建了一个从 Parent.prototype 继承的新对象,并将其分配给 Child.prototype。这确保对 Child.prototype 的更改不会影响 Parent.prototype,并建立了继承所需的原型链
     Child.prototype = Object.create(Parent.prototype);
     // 此时的构造函数为父类的 需要指回自己
     Child.prototype.constructor = Child;
    // 在 Child.prototype 上添加了一个新的方法 sayAge,这个方法是特定于 Child 构造函数的。
     Child.prototype.sayAge = function () {
       console.log(this.age);
     };

输入url后,浏览器内部会发生什么(主要讲浏览器渲染)

当用户在浏览器中输入内容时,浏览器进程的UI线程负责捕捉输入。对于网址,UI线程启动网络线程进行DNS解析和服务器连接,获取数据。对于搜索内容,浏览器使用默认搜索引擎进行查询。

网络线程在获取数据后,通过安全检测系统(如Google的safeBrowsing)检查站点是否安全。通过安全校验后,网络线程通知UI线程数据准备就绪。此时,UI进程创建一个渲染器进程,该进程将负责将数据渲染成用户可交互的web页面。

在渲染器进程中,主线程开始解析HTML,构建DOM树,并形成文档对象模型(DOM)。加载的资源,如图片和CSS,可以异步进行,而JavaScript的加载可能会阻塞HTML解析,因此需要谨慎处理其位置或使用defer和async属性。

解析后,主线程生成DOM树和CSS树,开始进行布局,计算节点的坐标和占用的区域。生成的布局树(layout tree)记录了节点的位置和边框尺寸。接着,主线程遍历布局树,生成一张绘制记录表,包括节点的绘制顺序和绘制命令。

这些绘制记录表通过IPC管道传递给合成器线程。合成器线程将记录表切分为图块,传递给栅格线程。栅格线程完成后,将渲染结果传递给GPU。合成器线程记录了一个合成器帧,包含图块在内存中的位置等信息。帧通过IPC传递给浏览器进程。

浏览器进程将合成器帧传送到GPU,由GPU进行最终的渲染。整个流程实现了高效的渲染和用户交互体验,涉及输入捕捉、网络请求、安全检测、渲染器创建、DOM构建、布局、绘制、合成和GPU渲染等多个环节,协同工作以实现顺畅的网页浏览。

讲讲V8引擎

V8是一个用于执行JavaScript代码的C++程序,广泛应用于多种操作系统,包括谷歌浏览器、Node.js和Electron等。其主要职责涵盖了编译和执行JavaScript代码、处理调用栈、进行内存分配和垃圾回收等方面。

工作流程如下:首先,JavaScript源代码通过解析器(parser)转换成抽象语法树(AST),接着Ignition(基准解释器)将AST转化为字节码(bytecode)。一旦字节码生成完成,AST就被清除,以释放内存。然后解释器执行字节码。

在代码运行的过程中,解释器收集了大量的可优化信息,例如变量的类型和高频执行的函数。这些信息会被传递给新的优化编译器TurboFan,该编译器可以生成新的机器代码。V8的优化编译器可以根据以下情况进行优化:

  1. 函数只被声明而未调用时,不会生成AST。
  2. 函数只被调用一次,其字节码会被解释执行。
  3. 函数被多次调用,可能被标记为热点函数,最终被编译成机器代码。
  4. 优化后的机器代码也可能被反编译成字节码,例如变量类型的优化,最初可能是数字,但在标记为热点函数后可能默认为数组类型。这时传入字符串时可能发生反编译。

继承的方式,优缺点

1. 原型链继承:

实现方式:

function Parent() {
  this.property = 'value';
}

function Child() {
  // 子类构造函数
}

Child.prototype = new Parent();

优点:

  • 简单易懂,易于实现。

缺点:

  • 引用类型属性会被所有实例共享,可能导致意外修改。
  • 无法向父类传递参数。

2. 借用构造函数继承:

实现方式:

javascriptCopy code
function Parent() {
  this.property = 'value';
}

function Child() {
  Parent.call(this); // 借用构造函数
}

优点:

  • 避免了引用类型属性共享的问题。
  • 可以向父类传递参数。

缺点:

  • 方法无法复用,每个实例都有一份副本。
  • 无法访问父类原型上的方法。

3. 组合继承:

实现方式:

javascriptCopy code
function Parent() {
  this.property = 'value';
}

function Child() {
  Parent.call(this); // 借用构造函数
}

Child.prototype = new Parent(); // 原型链继承

Child.prototype.constructor = Child; // 修复构造函数指针

优点:

  • 既能避免引用类型属性共享,又能实现原型链继承的方法共享。

缺点:

  • 父类构造函数会被调用两次,可能导致性能问题。

4. 寄生组合继承:

实现方式:

javascriptCopy code
function Parent() {
  this.property = 'value';
}

function Child() {
  Parent.call(this); // 借用构造函数
}

Child.prototype = Object.create(Parent.prototype); // 原型链继承

Child.prototype.constructor = Child; // 修复构造函数指针

优点:

  • 避免了父类构造函数被调用两次的问题,性能较好。