前端面试八股文(必备)

1. 数组去重

在这里插入图片描述

2. 有关 let const var 区别

  1. 使用 var 声明的变量,其作用域为该语句所在的函数内,且存在变量提升现象。
  2. 使用 let 声明的变量,其作用域为该语句所在的代码块内,不存在变量提升。
  3. 使用 const 声明的是常量,在后面出现的代码中不能再修改该常量的值。

3. for…of 和 for…in 的区别

可枚举 VS 可迭代

  • for...in 用于可枚举(enumerable)数据,如对象、数组、字符串,得到 key
  • for...of 用于可迭代(next)数据,如数组、字符串、Map、Set,得到 value

注:对于数组来说,for...of 返回的就是数组中的元素值。for...in 返回的是key,这里就是指数组中每个元素的索引值(遍历顺序有可能不是按照实际数组的内部顺序)。在实际开发中我们遍历对象一般用for...in更合适,遍历数组一般用for...of更合适

4. 事件循环 - 宏任务和微任务

javascript的异步任务:宏任务和微任务

宏任务有Event Table、 Event Queue,微任务有Event Queue

  1. 宏任务:包括整体代码script,setTimeout,setinterval,I/O, UI 交互事件,setlmmediate(Node.js 环境)。
  2. 微任务:Promise,MutaionObserver,process.nextTick(Node.js 环境)

注:new Promise中的代码会立即执行,then函数分发到微任务队列,process.nextTick分发到微任务队列Event Queue。settimeout 这种异步操作的回调,只有主线程中没有执行任何同步代码的前提下,才会执行异步回调,而 settimeout(fun,0)表示立刻执行,也就是用来改变任务的执行顺序,要求浏览器尽可能快的进行回调

JS是单线程,防止代码阻塞,我们把代码(任务)分为:同步和异步(同步代码给js引擎执行,异步代码交给宿主环境)。 执行过程:任务进入执行栈->同步任务还是异步任务?->同步的进入主线程,异步的进入Event Table并注册函数。当指定的事情完成时,Event Table 会将这个函数移入Event Queue ->主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。反复循环查看执行,这个过程就是常说的事件循环(Event Loop)。

在这里插入图片描述
在这里插入图片描述
练习测试网址:https://www.jsv9000.app

5. 跨域

同源指的是两个 URL 的协议、域名、端口一致,反之,则是跨域
出现跨域的根本原因:浏览器的同源策略不允许非同源的 URL 之间进行资源的交互。

浏览器对跨域请求的拦截:

在这里插入图片描述

如何实现跨域数据请求:
现如今,实现跨域数据请求,最主要的两种解决方案,分别是 JSONPCORS

  • JSONP:出现的早,兼容性好(兼容低版本E)。是前端程序员为了解决跨域问题,被迫想出来的一种临时解决方案。缺点是只支持 GET 请求,不支持 POST 请求。
  • CORS:出现的较晚,它是 W3C 标准,属于跨域 Ajax 请求的根本解决方案。支持GET 和 POST 请求。缺点是不兼容某些低版本的浏览器。

6. V8引擎

官方对V8引擎的定义:

  • V8是用C++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等。
  • 它实现 ECMAScriptWebAssembly,并在Windows 7或更高版本,macOS 10.12+和使用x64,IA-32, ARM或MIPS处理器的Linux系统上运行。
  • V8可以独立运行,也可以嵌入到任何C++应用程序中。

在这里插入图片描述

V8引擎本身的源码非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道它是如何对JavaScript执行的:

Parse 模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码

Ignition 是一个解释器,会将AST转换成ByteCode(字节码)

  • 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
  • 如果函数只调用一次,Ignition会执行解释执行ByteCode;
  • Ignition的V8官方文档:https://v8.dev/blog/ignition-interpreter

TurboFan 是一个编译器,可以将字节码编译为CPU可以直接执行的机器码

  • 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;
  • 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
  • TurboFan的V8官方文档:https://v8.dev/blog/turbofan-jit
    在这里插入图片描述
    V8执行的细节
    那么我们的JavaScript源码是如何被解析(Parse过程)的呢?
  • Blink将源码交给V8引擎,Stream获取到源码并且进行编码转换;
  • Scanner会进行词法分析(lexical analysis),词法分析会将代码转换成tokens;
  • 接下来tokens会被转换成AST树,经过Parser和PreParser:
    • Parser就是直接将tokens转成AST树架构;
    • PreParser称之为预解析,为什么需要预解析呢?
      • 这是因为并不是所有的JavaScript代码,在一开始时就会被执行。那么对所有的JavaScript代码进行解析,必然会影响网页的运行效率;
      • 所以V8引擎就实现了Lazy Parsing(延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析 是在函数被调用时才会进行;
      • 比如我们在一个函数outer内部定义了另外一个函数inner,那么inner函数就会进行预解析;
  • 生成AST树后,会被Ignition转成字节码(bytecode),之后的过程就是代码的执行过程;

7. 如何一次渲染10W条数据?

  /**
   * @description: 初级方案 使用 分页 + setTimeout
   * @return {*}
   */
  const renderList = async () => {
    console.time('list渲染时长');
    const list = new Array();
    for (let i = 0; i < 10 * 10000; i++) {
      list.push({ text: '测试文本' + i });
    }
    const total = list.length;
    const page = 0;
    const limit = 200;
    const totalPage = Math.ceil(total / limit);
    const render = (page) => {
      if (page >= totalPage) return;
      setTimeout(() => {
        for (let i = page * limit; i < page * limit + limit; i++) {
          const item = list[i];
          const div = document.createElement('div');
          div.innerHTML = `<div>${item.text}</div>`;
          document.getElementById('container')?.appendChild(div);
        }
      }, 0);
      render(page + 1);
    };
    render(page);
    console.timeEnd('list渲染时长');
  };
  renderList();

  /**
   * @description: 优化方案==>使用 分页 + requestAnimationFrame
   * 请求动画帧,它是一个浏览器的宏任务
   * 代替 setTimeout **多次拼接变成一次重排 以减少浏览器重排**
   * @return {*}
   */
  const renderList = async () => {
    console.time('list渲染时长');
    const list = new Array();
    for (let i = 0; i < 10 * 10000; i++) {
      list.push({ text: '测试文本' + i });
    }
    const total = list.length;
    const page = 0;
    const limit = 200;
    const totalPage = Math.ceil(total / limit);
    const render = (page) => {
      if (page >= totalPage) return;
      requestAnimationFrame(() => {
        for (let i = page * limit; i < page * limit + limit; i++) {
          const item = list[i];
          const div = document.createElement('div');
          div.innerHTML = `<div>${item.text}</div>`;
          document.getElementById('container')?.appendChild(div);
        }
      });
      render(page + 1);
    };
    render(page);
    console.timeEnd('list渲染时长');
  };
  renderList();

  /**
   * @description: 终极方案==> createDocumentFragment 文档碎片(vue底层也有使用) 
   * dom节点 非主dom树节点
   * @return {*}
   */
  const renderList = async () => {
    console.time('list渲染时长');
    const list = new Array();
    for (let i = 0; i < 10 * 10000; i++) {
      list.push({ text: '测试文本' + i });
    }
    const total = list.length;
    const page = 0;
    const limit = 200;
    const totalPage = Math.ceil(total / limit);
    const render = (page) => {
      if (page >= totalPage) return;
      // 创建一个文档碎片
      const fragment = document.createDocumentFragment();
      requestAnimationFrame(() => {
        for (let i = page * limit; i < page * limit + limit; i++) {
          const item = list[i];
          const div = document.createElement('div');
          div.innerHTML = `<div>${item.text}</div>`;
          fragment.appendChild(div);
        }
        // 一次性 appendChild
        document.getElementById('container')?.appendChild(fragment);
      });
      render(page + 1);
    };
    render(page);
    console.timeEnd('list渲染时长');
  };
  renderList();

8. JS中深拷贝和浅拷贝区别?

浅拷贝 是说只拷贝了对象的引用,这是对指针的拷贝,拷贝后两个指针指向的是同一份内存同一份数据。这意味着当原对象发生变化的时候,拷贝对象也会跟着发生变化。
深拷贝 不但对指针进行了拷贝,而且还对指针指向的内容也进行了拷贝,也就是另外申请了一块空间内存,内容和原对象一致,但是,是两份独立的数据。更改原对象,拷贝对象是不会发生变化的。

浅拷贝的五种实现方式

  1. Object.assign 是 Object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。
    注意点:
    • 它不会拷贝对象的继承属性;
    • 它不会拷贝对象的不可枚举的属性;
    • 可以拷贝 Symbol 类型的属性。
  2. 扩展运算符 …
  3. Array.prototype.concat
  4. Array.prototype.slice
  5. 使用第三方库 比如lodash就提供了clone方法供用户进行浅拷贝

手写浅拷贝

const clone = (target) => {
  // 如果是引用类型
  if (typeof target === "object" && target !== null) {
    // 判断是数据还是对象,为其初始化一个数据
    const cloneTarget = Array.isArray(target) ? [] : {};
    
    // for in 可以遍历数组 对象
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {
        cloneTarget[prop] = target[prop];
      }
    }
 
    return cloneTarget;
  } else {
    // 基础类型 直接返回
    return target;
  }
};

JSON.parse(JSON.stringify ()) 实现深拷贝时有什么缺点?

  1. obj 里面有 new Date() ,深拷贝后,时间会变成字符串的形式。而不是时间对象;
  2. obj 里有 RegExpError 对象,则序列化的结果会变成空对象{}
  3. obj 里有 functionundefined,则序列化的结果会把 functionundefined 丢失;
  4. obj 里有 NaNInfinity-Infinity,则序列化的结果会变成null;
  5. JSON.stringify() 只能序列化对象的可枚举的自有属性,如果 obj 中的对象是由构造函数生成的实例对象, 深拷贝后,会丢弃对象的constructor

手写深拷贝

 /**
   * @description: 手写深拷贝
   * @param {*} source 拷贝数据源
   * @return {*}
   */
  const deepClone = (source) => {
    const targetObj = source.constructor === Array ? [] : {};
    for (let keys in source) {
      if (source.hasOwnProperty(keys)) {
        // 引用数据类型
        if (source[keys] && typeof source[keys] == 'object') {
          targetObj[keys] = deepClone(source[keys]); // 递归调用
        } else {
          // 基本数据类型 直接赋值
          targetObj[keys] = source[keys];
        }
      }
    }
  };

9. 判断数据类型方法?

  1. typeof 主要用来判断基本数据类型(7种):字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、BigIntSymbol
  2. instanceof 运算符返回一个布尔值,表示对象是否为某个构造函数的实例
    • instanceof 是检测整个原型链,所以一个对象可能对多个构造函数返回 true
    • 但是有一种情况,左边为null,或者左边对象的原型链上只有null对象时,这时,instanceof会判断失真
    • instanceof 还有一种用法用于判断值的类型,但只能用于对象,不能用于基本数据类型的值(基本数据类型则返回false)。
    • 特殊情况instanceof只有同一个全局window才会返回true,如出现嵌套页面的多个window则不能正确判断。
var obj = Object.create(null);
typeof obj // "object"
obj instanceof Object // false
var x = [1, 2, 3];
var y = {};
console.log( x instanceof Array )// true
console.log(y instanceof Object)  // true
  1. Object.prototype.toString.call() 最准确的判断数据类型的方法,返回字符串 如[object Array]强烈推荐!!

扩展:

  • BigIntES11引入的新的基本数据类型。BigInt数据类型的目的是比Number数据类型支持的范围更大的整数值,以任意精度表示整数。使用 BigInt解决了之前Number整数溢出的问题。
  • Symbol 是原始值,可用于设置创建需要特殊的名字:
// Symbol(字符串) 返回一个具有唯一的标识符,标识符是原始值
const sym1 = Symbol('hello')
const sym2 = Symbol('hello')
console.log(sym1===sym2);//false
// const sym3 = new Symbol('hello') //用new调用Symbol会报错
const sym4 = Symbol() 
const sym5 = Symbol()
const obj = {
      x:1,
      y:2,
      [sym4]:'hello',
      [sym5]:'你好'
    }
console.log(obj[sym4]); //hello
console.log(obj[sym5]); //你好

10. 从哪些点做性能优化?

加载层面

  1. 减少http请求(精灵图,文件的合并)
  2. 减小文件大小(资源压缩,图片压缩,代码压缩)
  3. CDN(第三方库,大文件,大图)
  4. SSR服务端渲染,预渲染
  5. 懒加载
  6. 分包(小程序)

渲染层面

  1. 减少dom操作
  2. 避免回流
  3. 文档碎片

性能分析

  1. 页面加载性能(加载时间,用户体验)
  2. 动画与操作性能(是否流畅无卡顿)
  3. 内存占用(内存占用过大,浏览器崩掉等)
  4. 电量消耗(游戏方面,可以暂不考虑)

在这里插入图片描述

11. 改变this指向的三种方法?

call(), apply(), bind() 区别:

  • bind() 不调用 只改变this指向
  • callapply() 改变this指向后 并且执行一次调用
  • apply 主要作用于 []
  • call 主要作用于 {}

谁最近调用 this 就指向谁

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值