中高级前端面试题

475 阅读28分钟

中高级前端面试题

事件冒泡、事件捕获、事件穿透是什么?

  • 事件捕获: 事件从不具体的元素向目标元素传播,事件捕获先执行
  • 事件冒泡:事件从子元素传递到最上级父元素
  • 事件穿透:点击元素的时候 元素会穿透到下面的元素
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Event Capturing</title>
</head>
<body>
  <div id="parent" style="padding: 50px; border: 1px solid black;">
    Parent
    <div id="child" style="padding: 50px; border: 1px solid red;">
      Child
    </div>
  </div>
  <script>
    document.getElementById('parent').addEventListener('click', function() {
      console.log('Parent clicked - capturing');
    }, true); // 捕获阶段
​
    document.getElementById('child').addEventListener('click', function() {
      console.log('Child clicked - capturing');
    }, true); // 捕获阶段
​
    document.getElementById('parent').addEventListener('click', function() {
      console.log('Parent clicked - bubbling');
    }, false); // 冒泡阶段
​
    document.getElementById('child').addEventListener('click', function() {
      console.log('Child clicked - bubbling');
    }, false); // 冒泡阶段
  </script>
</body>
</html>

如何处理前端跨域问题

  • 代理: 通过设置一个代理服务器,中转前端请求,代理服务器再向目标服务器发起请求。由于浏览器和代理服务器在同一域中,不会触发跨域问题
  • CORS(跨域资源共享): 服务器设置适当的CORS响应头,允许跨域请求。浏览器会根据这些头决定是否允许前端进行跨域请求。
  • Nginx反向代理 将跨域请求代理到目标服务器

解释JavaScript的原型链(prototype chain),并说明它在继承中的作用。

  1. 对象的原型

    • 每个JavaScript对象(除 null 外)都有一个关联的对象称为原型(prototype),可以通过对象的 __proto__ 来获取 或者 通过 Object.getPrototypeOf(obj) 来访问这个原型

      //obj 和 obj1 默认的原型是 Object.prototype。当你直接修改 obj.__proto__ 时,实际上你修改的是 Object.prototype,因此任何继承自 Object.prototype 的对象都会受影响const obj = {};
      obj.__proto__.getobjData = function () {
        console.log('getobjData');
      }
      ​
      const obj1 = {
        a: 2
      }
      ​
      console.log(Object.getPrototypeOf(obj1), 'getPrototypeOf');
      obj1.__proto__.getobjData();  // getobjData// 通过 Object.create() 创建的对象 不会受到影响 Object.create 会创建一个新的对象 该对象的原型链 指向创建的对象本身
      ​
      ​
      ​
      
  2. 构造函数的原型链

    • 当创建一个对象时,通过构造函数的 prototype 属性可以设置这个对象的原型

      function Person(name) {
          this.name = name;
      }
      Person.prototype.sayHello = function() {
          console.log(`Hello, my name is ${this.name}`);
      };
      ​
      const alice = new Person('Alice');
      alice.sayHello(); // 输出: Hello, my name is Alice
      ​
      ​
      
  3. 原型链

    • 当访问一个对象的属性或方法时,JavaScript首先在对象自身的属性中查找。如果没有找到,它会沿着原型链向上查找,直到找到这个属性或方法或者达到原型链的顶端(即 null)。这就是原型链的机制。

        const obj = {
          a : 1
        }
      ​
        obj.__proto__.data = {
          b : 2
        }
      ​
        console.log(obj.data); // {b:2}
      
  4. 继承

    • 通过原型链,JavaScript实现了继承机制。一个对象可以继承另一个对象的属性和方法。这使得代码的复用性和可维护性得到提高。

          function getData (name) {
           this.name = name
          }
      ​
          getData.prototype.getName = function () {
            console.log('this.name',this.name);
            return this.name
          }
      ​
          function getData1 (name) {
           this.name = name
          }
      ​
          getData1.prototype = Object.create(getData.prototype)
      ​
          console.log(getData1.prototype.constructor === getData, 'constructor');
      ​
           const p =  new getData1('12')
           console.log(p.getName());  // 12
      
    • 每个对象都有一个原型,形成一个链条,即原型链。
    • 原型链在继承中起到重要作用,允许对象共享属性和方法。
    • 通过原型链,JavaScript实现了基于原型的继承机制。

什么是闭包(closure),请解释其原理并举一个实际使用的例子

  • 闭包是指在一个函数内部定义的函数可以访问外部函数的作用域变量。即使外部函数已经执行结束,这个内部函数仍然可以访问和修改外部函数的局部变量。这种机制称为闭包。

  • 闭包依赖于函数作用域链和 JavaScript 的垃圾回收机制。当一个内部函数被返回并在外部调用时,它依然保留对其外部函数作用域中变量的引用,这使得这些变量不会被垃圾回收机制回收,从而实现闭包。

        function add () {
            let count = 1
          return function () {
            console.log(count);
            return count++
          }
        }
    ​
        const a = add()
        console.log(a, 'a');
        a() // 1
        a() // 2
    
    应用场景
    • 数据封装:闭包可以用来创建私有变量,防止外部直接访问和修改。

    • 函数柯里化:通过闭包可以实现函数柯里化(Currying),即把接受多个参数的函数转换成接受单一参数的函数,并返回接受余下参数的新函数。

      • 函数柯里化(Currying)是一个函数式编程中的概念,它指的是将一个接受多个参数的函数转换成一系列接受单一参数的函数的过程。简单来说,柯里化让你可以分步调用一个函数,每次只传递一个参数,直到传递了所有参数,返回最终结果。

         function add (x) {
             return function (y) {
                 return x+y
             }
         }
        add(3)(5) // 8
        ​
        ​
        ​
        ​
        ​
        function curry(func) {
          return function curried(...args) {
            if (args.length >= func.length) {
              return func.apply(this, args);
            } else {
              return function(...args2) {
                return curried.apply(this, args.concat(args2));
              }
            }
          };
        }
        ​
        // 示例使用
        function multiply(a, b, c) {
          return a * b * c;
        }
        ​
        const curriedMultiply = curry(multiply);
        ​
        console.log(curriedMultiply(2)(3)(4)); // 输出: 24
        console.log(curriedMultiply(2, 3)(4)); // 输出: 24
        console.log(curriedMultiply(2)(3, 4)); // 输出: 24
    • 事件处理:闭包常用于事件处理程序中,保存函数的状态。

      // 数据封装 
          function add (name) {
              let _name = name
            return {
               getname() {
                return _name
               },
               setname (n) {
                _name = n
               }
            }
          }
      ​
          const p = add('张三')
          p.setname('李四')
          console.log(p.getname())  // 李四
          console.log(_name)  //  _name is not defined

解释事件循环(event loop)及其在 JavaScript 中的作用。

  • JavaScript 是一种单线程的语言,它通过事件循环(Event Loop)来处理异步操作。这意味着它在一个线程中执行代码,避免了多线程带来的复杂性,但同时也需要一种机制来处理异步任务。事件循环就是这种机制。

  • 同步任务

    • 同步任务是立即执行的任务,它们在主线程上按顺序执行。只有当前的同步任务执行完毕后,JavaScript 引擎才能继续执行下一个任务。
  • 异步任务

    • 异步任务不会立即执行,而是被放入任务队列中,等待主线程空闲时再执行。异步任务分为两类:宏任务(Macro Task)和微任务(Micro Task)。

    • 宏任务包括整体代码脚本、setTimeoutsetIntervalI/O 操作等。它们被放入宏任务队列中,按顺序执行

    • 微任务包括 Promisethen 方法、MutationObserver 等。它们被放入微任务队列中,优先于宏任务执行。

    • 同步任务先执行 然后执行微任务 最后 执行宏任务

    • promise在.then 之前都是同步任务

      console.log('script start'); // 同步任务setTimeout(function() {
        console.log('setTimeout'); // 宏任务
      }, 0);
      ​
      Promise.resolve().then(function() {
        console.log('promise1'); // 同步任务
      }).then(function() {
        console.log('promise2'); // 微任务
      });
      ​
      console.log('script end'); // 同步任务
      
    Web Workers
    • js 虽然是单线程但通过使用 Web Workers、Shared Workers、Service Workers 以及 Node.js 中的 worker_threads 模块,可以实现类似于多线程的并行执行。
    • Web Workers是HTML5引入的一种在后台线程中运行JavaScript的机制,允许你在主线程(UI线程)之外运行脚本操作。它们可以用于执行耗时的计算任务,而不阻塞用户界面
    • Web Workers非常适合处理那些可能会阻塞主线程的计算密集型或高延迟的任务。以下是一些常见的应用场景:

      // main.js
      const worker = new Worker('dataFetcher.js');
      ​
      worker.postMessage('startFetching');worker.onmessage = function(event) {
        console.log('Fetched data:', event.data);
      };
      ​
      // dataFetcher.js
      self.onmessage = function() {
        fetch('https://api.example.com/data')
          .then(response => response.json())
          .then(data => self.postMessage(data));
      };
      

请解释JavaScript的作用域(Scope)及其类型,并说明它们的区别

  • 全局作用域

    • 任何在全局作用域中声明的变量和函数都可以在整个JavaScript程序中访问。在浏览器环境中,window对象代表全局作用域。

          let a = false 
      ​
          if(a) {
            var c = 4
          } else {
            var c = 8
          }
      ​
          console.log(c, 'cc');  // 8
      
  • 块级作用域

    • 使用letconst声明的变量在块级作用域(如ifforwhile等代码块中)内是局部变量,只能在该块中访问。

          let a = false 
      ​
          if(a) {
            let c = 4
          } else {
            let c = 8
          }
      ​
          console.log(c, 'cc');  // c is not defined
      
  • 函数作用域

    • 在函数内部声明的变量和函数只能在该函数内部访问,这种作用域称为函数作用域。

    • vue的data为什么是个函数也是这个

          let a = false 
        function add () {
          if(a) {
            var c = 4
          } else {
            var c = 8
          }
          console.log(c, 'function'); 
      ​
        }
          add() // 8
          console.log(c, 'cc'); // c is not defined
  • 作用域链接

    • 当JavaScript在查找变量时,会从当前作用域开始查找,如果找不到就会向上一级作用域查找,直到全局作用域,这种机制称为作用域链。

请解释JavaScript中的this关键字及其在不同上下文中的指向

  • this 关键字在JavaScript中是动态绑定的,其指向取决于函数的调用方式,而不是函数的声明方式。具体来说,this 的指向可以分为以下几种情况:

    1. 在全局作用域或函数中,this 指向全局对象。在浏览器中,全局对象是 window

    2. 当方法作为对象的属性调用时,this 指向该对象。

      const obj = {
        name: 'Alice',
        getName: function() {
          console.log(this.name);
        }
      };
      ​
      obj.getName(); // 输出: Alice
      
    3. 当使用 new 关键字调用构造函数时,this 指向新创建的实例对象。

      • 构造函数是一种用于创建和初始化对象的特殊函数。它通常与 new 关键字一起使用。构造函数的名称通常首字母大写,以示区别

      • 创建一个新对象: 一个新的空对象被创建并分配给 this

        设置原型: 新对象的 __proto__ 被设置为构造函数的 prototype 属性,从而实现原型继承。

        执行构造函数: 构造函数内部的代码执行,对 this 进行初始化。

        返回新对象: 如果构造函数没有显式返回对象,则默认返回 this

      ​
      function Person(name) {
        this.name = name;
      }
      ​
      const person = new Person('Bob');
      console.log(person.name); // 输出: Bob
      
    4. 通过 callapplybind 方法,可以显式地指定 this 的指向。

      function showName() {
        console.log(this.name);
      }
      ​
      const obj1 = { name: 'Charlie' };
      const obj2 = { name: 'Dave' };
      ​
      showName.call(obj1); // 输出: Charlie
      showName.apply(obj2); // 输出: Dave
      ​
      const boundShowName = showName.bind(obj1);
      boundShowName(); // 输出: Charlie
      ​
      ​
      
    5. 箭头函数不会创建自己的 this,它会捕获上下文中的 this 值,即箭头函数所在作用域的 this。这使得箭头函数非常适合在回调中使用,以确保 this 的指向正确。

    6. dom 的this 指向的是事件

请解释事件委托(event delegation),并举一个实际应用的例子。

  • 事件委托是一种将事件处理程序添加到一个父级元素上,而不是每个子元素上,通过事件冒泡(event bubbling)机制来处理子元素的事件。这样可以显著减少事件处理程序的数量,提升性能和简化代码管理。

  • 在React中,事件委托的概念被内置在其合成事件系统(Synthetic Event System)中。React通过在最顶层的根元素上监听所有的事件,而不是在每个组件实例中单独监听事件。这样可以提高性能和效率。

    <html>
    <head>
        <title>事件委托示例</title>
    </head>
    <body>
        <ul id="parent">
            <li class="child">Item 1</li>
            <li class="child">Item 2</li>
            <li class="child">Item 3</li>
            <li class="child">Item 4</li>
        </ul>
    ​
        <script>
            document.getElementById('parent').addEventListener('click', function(event) {
                if (event.target && event.target.matches('li.child')) {
                    console.log('Clicked:', event.target.textContent);
                }
            });
        </script>
    </body>
    </html>
    

请解释Promise的概念及其在JavaScript中的作用,并举例说明如何使用Promise链进行异步操作。

  • Promise是JavaScript中的一种用于处理异步操作的对象。它代表一个未来可能完成(或失败)的操作及其结果值。Promise提供了一种更优雅的方式来处理异步操作,使代码更加简洁和可读。

  • Promise 的状态有三种 分别是

    • pending (进行中) 初始状态,既不是成功也不是失败。
    • fulfilled( 已成功 ) 表示操作成功完成 。
    • rejected( 失败 ) 表示操作失败。
  • 你可以使用Promise构造函数来创建一个Promise对象,并提供一个执行器函数(executor function),该函数接收两个参数:resolverejectresolve 用于标记操作成功并返回结果,reject 用于标记操作失败并返回错误。

  • then方法用于处理成功结果,catch方法用于处理失败结果。

    const fetchData = (url) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          const success = true; // 模拟异步操作结果
          if (success) {
            resolve(`Fetched data from ${url}`);
          } else {
            reject('Fetching failed');
          }
        }, 1000);
      });
    };
    ​
    // 使用Promise链进行多个异步操作
    fetchData('https://api.example.com/data1')
      .then(result => {
        console.log(result); // 输出: Fetched data from https://api.example.com/data1
        return fetchData('https://api.example.com/data2'); // 返回新的Promise
      })
      .then(result => {
        console.log(result); // 输出: Fetched data from https://api.example.com/data2
        return fetchData('https://api.example.com/data3'); // 返回新的Promise
      })
      .then(result => {
        console.log(result); // 输出: Fetched data from https://api.example.com/data3
      })
      .catch(error => {
        console.error(error); // 处理任何一个Promise的错误
      });
    ​
    

请解释ES6中的箭头函数(arrow function)及其与传统函数的区别。

  • 箭头函数是ES6引入的一种新的函数定义语法,它比传统函数更加简洁,特别适用于简单的回调函数和内联函数。箭头函数没有自己的 this 绑定,它会捕获其定义时的上下文中的 this 值。

  • 箭头函数没有自己的 arguments 对象。如果需要访问参数列表,可以使用 rest 参数语法

    ​
          const add = (...args)=> {
            console.log(args, '');
          }
          add(1,2,3,5,6)
    ​
    
  • 箭头函数不能使用 new 关键字调用,因为它们没有自己的 this 绑定和原型链。

    const Person = (name) => {
      this.name = name;
    };
    ​
    // 会抛出错误
    const john = new Person('John'); // TypeError: Person is not a constructor
    
  • 箭头函数没有 prototype 属性,因此它们不能用于创建具有方法的对象。

    const arrowFunction = () => {};
    console.log(arrowFunction.prototype); // 输出: undefinedfunction traditionalFunction() {}
    console.log(traditionalFunction.prototype); // 输出: {}
    
  • 箭头函数特别适合在回调函数和内联函数中使用,例如数组方法的回调、事件处理程序和Promise链。

请解释JavaScript的模块化(modularization)及其在ES6中的实现方式。

  • 模块化是软件开发中的一种设计和编程技术,它将代码组织成独立的、可重用的单元(模块),每个模块封装特定的功能。通过模块化,可以提高代码的可维护性、可读性和重用性。模块化的目标是减少代码之间的依赖性,使得每个模块可以独立开发、测试和使用。

  • 1. ES6 模块(ESM)

    ES6引入了标准的模块系统,使用 importexport 关键字来导入和导出模块。

  • 2. CommonJS 模块

    CommonJS 是Node.js采用的模块系统,使用 requiremodule.exports

    组件库:创建独立的UI组件,每个组件作为一个模块。

    工具库:封装常用的工具函数和方法,形成工具库模块。

    业务逻辑分离:将不同业务逻辑分离成独立的模块,提高代码的可维护性和测试性。

请解释什么是深拷贝(deep copy)和浅拷贝(shallow copy),并说明它们之间的区别。

  • js数据类型分为 基本数据类型和引用数据类型
  • 基本数据类型(Primitive Types)在赋值时拷贝的是值,存储在栈内存中
  • 引用数据类型(Reference Types)在赋值时拷贝的是地址,存储在堆内存中。
浅拷贝是指只拷贝对象的第一层属性,对于嵌套的对象依旧是引用。常见的浅拷贝方法有 Object.assign 和展开运算符(...)。
深拷贝是指完全拷贝对象及其嵌套对象的所有属性,拷贝后的对象与原对象完全独立。实现深拷贝的常见方法有手动递归拷贝和使用第三方库如 lodash_.cloneDeep

请解释JavaScript中的执行上下文(Execution Context)及其工作原理。

  • 执行上下文是JavaScript代码在运行时所处的环境,包含了代码执行所需的所有信息。每当JavaScript代码执行时,都会创建一个执行上下文。

  • 全局执行上下文

    • 当JavaScript代码第一次运行时,默认创建一个全局执行上下文。全局执行上下文会在整个JavaScript应用程序的生命周期内存在。
    • 全局执行上下文会创建一个全局对象(在浏览器中是 window 对象)和一个特殊变量 this,指向全局对象。
  • 函数执行上下文

    • 每当一个函数被调用时,都会创建一个新的函数执行上下文。每个函数都有自己的执行上下文。
    • 函数执行上下文包含函数的参数、局部变量、内部函数以及一个特殊变量 this
  • 执行上下文工作分为创建阶段和执行阶段

    • 创建阶段

      • 变量对象(Variable Object,VO) :创建变量对象,包含函数的所有参数、局部变量和函数声明。
      • 作用域链(Scope Chain) :创建作用域链,用于解析变量。
      • this绑定:确定 this 的值。
    • 在执行阶段,执行上下文会执行代码,变量和函数会被赋值,并执行具体的操作。

  • JavaScript使用执行上下文栈(Execution Context Stack,也称为调用栈)来管理执行上下文。执行上下文栈遵循先进后出(First In Last Out,FILO)的原则。

    • 当JavaScript引擎第一次运行代码时,会创建一个全局执行上下文,并将其压入执行上下文栈。
    • 每当一个函数被调用时,会创建一个新的函数执行上下文,并将其压入栈顶。
    • 当函数执行完毕后,函数执行上下文会从栈顶弹出,控制权交还给上一个执行上下文。

请解释JavaScript中的迭代器(Iterator)和生成器(Generator),并举例说明它们的用法。

  • 具有Symbol.iterator属性的对象,即为可迭代对象。在 ES6 中,所有集合对象,包括数组字符串Set集合Map 集合

  • Symbol.iterator 为每一个对象定义了默认的迭代器。该迭代器可以被 for...of 循环使用

    const myIterable = {};
    myIterable[Symbol.iterator] = function* () {
      yield 1;
      yield 2;
      yield 3;
    };
    [...myIterable]; // [1, 2, 3]
    
迭代器(Iterator)
  • 迭代器是一种对象,提供了一个 next 方法,返回序列中的下一个值。每次调用 next 方法都会返回一个包含 valuedone 属性的对象:

    • value 是当前迭代的值

    • done 表示迭代是否完成

      // 设计一个迭代器
      ​
      function createIterator (v) {
              let i = 0
              return {
                next: function () {
                if (i >= v.length) {
                  return { value: undefined, done: true };
                } else {
                  i++;
                  return { value: i, done: false };
                }
              },
              }
            };
      ​
            const IteratorData = createIterator([1, 2, 3, 4, 5]);
            console.log(IteratorData);
            console.log(IteratorData.next());
            console.log(IteratorData.next());
             console.log(IteratorData.next());
             console.log(IteratorData.next());
             console.log(IteratorData.next());
             console.log(IteratorData.next());
      ​
      
生成器(Generator)
  • 生成器是一个可以返回迭代器的函数,定义使用 function* 语法。生成器函数可以使用 yield 关键字逐步返回值,生成器返回的迭代器可以通过调用 next 方法按需生成值。
function* generatorFunction() {
  yield 1;
  yield 2;
  yield 3;
}
​
const generator = generatorFunction();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }
  • yieldnext 的工作机制

    • yield:用于在生成器函数中暂停执行,并返回一个值。
    • next:用于恢复生成器函数的执行,并接收一个可选的参数,该参数会作为 yield 表达式的返回值。
    function* interactiveGenerator() {
      const first = yield 'First yield';
      console.log(first); // 输出: 10
      const second = yield 'Second yield';
      console.log(second); // 输出: 20
      return 'Done';
    }
    ​
    const gen = interactiveGenerator();
    console.log(gen.next()); // { value: 'First yield', done: false }
    console.log(gen.next(10)); // { value: 'Second yield', done: false }
    console.log(gen.next(20)); // { value: 'Done', done: true }
    

在 React 中,什么是 Hook?请解释 useState 和 useEffect 的用法。

  • Hook 是 React 16.8 引入的一项新功能,它允许你在不编写类组件的情况下使用状态和其他 React 特性。
  • useState 是一个 Hook,它允许你在函数组件中添加状态。
  • useEffect 是一个 Hook,它允许你在函数组件中执行副作用操作。它的功能类似于类组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个生命周期函数的组合。

请解释 Vue 中的响应式数据(reactive data)系统是如何工作的?

  • Vue 2 的响应式系统使得数据变化能够自动更新 DOM,从而保持视图和数据的一致性。

  • Object.defineProperty 劫持数据

    • Vue 2 使用 Object.defineProperty 方法劫持对象属性的读取和写入操作。通过在属性上定义 getter 和 setter,可以在数据变化时执行特定的逻辑。
    • 当一个对象被传递给 Vue 实例的 data 选项时,Vue 会遍历这个对象的所有属性,并使用 Object.defineProperty 将这些属性转换为 getter 和 setter
  • 依赖收集

    • 当组件渲染时,getter 会被调用,Vue 将当前组件的渲染函数作为依赖,记录在属性的依赖列表中。
    • 这使得 Vue 能够在属性变化时,通知这些依赖,触发重新渲染。
  • Watcher

    • Watcher 是 Vue 中负责跟踪数据变化的类。当依赖的数据变化时,Watcher 会被通知,从而执行更新操作。
    • 每个组件实例都会有一个 Watcher 实例,用于追踪组件的渲染。
  • 发布订阅模式

    • Vue 的响应式系统可以看作是发布订阅模式的实现。数据变化时,相关的 Watcher 会被通知并触发更新。
// 假设有一个对象
let data = {
  message: 'Hello Vue!'
};
​
// Vue 劫持数据
Object.defineProperty(data, 'message', {
  get() {
    // 依赖收集
    console.log('getter 被调用');
    return message;
  },
  set(newValue) {
    // 数据变化
    console.log('setter 被调用');
    message = newValue;
    // 通知依赖,触发更新
  }
});
​
// 当我们访问 data.message 时,会触发 getter
console.log(data.message); // 'getter 被调用'// 当我们设置 data.message 时,会触发 setter
data.message = 'Hello World!'; // 'setter 被调用'

请解释 Webpack 中的热模块替换(Hot Module Replacement, HMR)是什么以及它的工作原理?

  • 热模块替换是 Webpack 提供的一项功能,允许在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。这对于开发过程中提升效率非常有帮助。
工作原理
  1. 监听文件变化

    • Webpack 监听项目文件的变化。当文件内容发生改变时,Webpack 重新编译这些变化的模块。
  2. 通知 HMR 服务器

    • 重新编译完成后,Webpack 通知 HMR 服务器,生成新的模块包并通知客户端。
  3. 客户端接收更新

    • 客户端通过 WebSocket 与 HMR 服务器通信,接收到更新通知后,客户端会请求新的模块代码。
  4. 替换模块

    • 客户端接收到新的模块代码后,使用 module.hot API 替换旧的模块,而不刷新页面。
    • 在模块内部,可以使用 module.hot.accept 接受更新的模块,并定义模块更新后的处理逻辑。

请解释什么是虚拟 DOM,以及它在 React 和 Vue 中的作用是什么?

  • 虚拟 DOM(Virtual DOM)是对真实 DOM 的一种抽象表示。它使用 JavaScript 对象来描述 DOM 树的结构,从而使得对 DOM 的操作变得更加高效。
工作原理
  1. 初始渲染

    • 应用程序的 UI 首次渲染时,框架(如 React 或 Vue)会创建一个虚拟 DOM 树,这个树是用 JavaScript 对象表示的。
  2. 状态变化

    • 当应用程序的状态发生变化时,新的虚拟 DOM 树会被创建。
  3. 差异计算

    • 框架会将新的虚拟 DOM 树与旧的虚拟 DOM 树进行比较,找出两者之间的差异(diff)。
  4. 更新真实 DOM

    • 框架会根据差异计算的结果,最小化地更新真实 DOM,只更新必要的部分,从而提高性能。
虚拟 DOM 的作用
  1. 性能优化

    • 通过对比新旧虚拟 DOM 树,框架可以最小化地更新真实 DOM,从而减少不必要的 DOM 操作,提高渲染性能。
  2. 跨平台

    • 虚拟 DOM 是一个抽象层,使得框架可以在不同的平台上使用相同的逻辑进行 UI 渲染。例如,React 可以在浏览器、服务器(React Server-side Rendering)以及移动端(React Native)使用相同的虚拟 DOM 技术。
  3. 简化编程模型

    • 开发者只需要描述应用的状态和视图之间的映射关系,而不需要手动操作 DOM。这种声明式编程方式使得代码更加简洁和易于维护。

      Vue 和 React 都使用虚拟 DOM 和 diff 算法来优化更新,但它们的实现方式和策略有所不同。
      React
    • React 使用的 diff 算法称为

      reconciliation

      。它主要基于两个假设:

      1. 不同类型的元素会生成不同的树。
      2. 开发者可以通过 key 属性来显式指定哪些子元素在不同渲染中保持稳定。
    • React 会将新旧虚拟 DOM 树进行逐层比较,当发现不同类型的节点时,会直接销毁旧节点及其子节点,并创建新的节点。

    • React 的 diff 算法时间复杂度为 O(n)。

    Vue

    • Vue 采用了一种双端比较的 diff 算法。
    • Vue 的 diff 算法更注重实际应用中的性能优化,针对静态节点和动态节点进行了优化处理。
    • Vue 的 diff 算法也是逐层比较,但在比较过程中,会尝试通过最小的操作来更新 DOM。
性能优化
  • React 引入 Fiber 架构和批量处理更新来优化性能。
  • Vue 通过静态节点优化和异步更新队列来提升性能。

请解释 Webpack 中的代码拆分(Code Splitting),以及它的作用和优点。

  • 代码拆分是一种前端优化技术,它允许将代码库拆分成更小的块,并在需要时按需加载这些块。Webpack 提供了多种方式来实现代码拆分,从而提高应用的性能和用户体验。

  • 代码拆分的作用

    1. 减少初始加载时间

      • 将应用的代码拆分成多个小的代码块(chunk),仅在需要时加载这些代码块,从而减少初始页面的加载时间。
    2. 按需加载

      • 通过按需加载,可以在用户访问特定功能或页面时,动态加载相关的代码,而不是在应用初始化时加载所有代码。
    3. 提高性能

      • 代码拆分可以减少主 bundle 的大小,使得浏览器能够更快地解析和执行代码,从而提高性能。
  • Vue 和 React 都支持代码拆分,并且实现起来相对简单。你可以使用动态导入(import())和框架提供的工具(如 Vue Router 和 React Router)来实现按需加载,提高应用性能和用户体验。

请解释 React 中的上下文(Context)是什么,以及它在状态管理中的作用。

  • Context 是 React 提供的一种方式,用于在组件树中传递数据,而无需显式地通过每一层组件的 props 进行传递。

  • 主要用途

    • 在组件树的多个层级之间共享数据,如当前的主题、语言设置、认证信息等。
    • 避免了通过每一层组件手动传递 props 的繁琐过程,尤其是在深层嵌套的组件结构中。
    import React, { createContext, useContext } from 'react';
    ​
    // 创建 Context
    const ThemeContext = createContext('light');
    ​
    function ThemedComponent() {
      const theme = useContext(ThemeContext);
      return <div>Current theme: {theme}</div>;
    }
    ​
    function App() {
      return (
        <ThemeContext.Provider value="dark">
          <ThemedComponent />
        </ThemeContext.Provider>
      );
    }
    

请解释 Webpack 中的插件(Plugins)和加载器(Loaders)的区别及其作用。

  • 插件是用来扩展 Webpack 功能的工具。它们可以在构建过程中执行各种任务,如打包优化、资源管理、环境变量注入等。

  • 作用

    • 打包优化:如压缩 JavaScript 文件、分离 CSS 文件、删除无用代码等。
    • 资源管理:如自动生成 HTML 文件、复制文件到输出目录、处理环境变量等。
    • 增强功能:如启用热模块替换(HMR)、定义全局变量等。

    常用插件

    • HtmlWebpackPlugin:自动生成 HTML 文件,并将打包后的 JavaScript 和 CSS 文件插入其中。
    • MiniCssExtractPlugin:将 CSS 从 JavaScript 文件中提取到单独的 CSS 文件中。
    • TerserWebpackPlugin:压缩和优化 JavaScript 文件。
  • 加载器是用于转换模块的工具。它们可以在 importrequire 模块时,对模块的内容进行预处理。例如,将 ES6 转换为 ES5、将 SCSS 编译为 CSS 等。

    作用

    • 文件转换:如将 TypeScript 转换为 JavaScript、将 JSX 转换为 JavaScript 等。
    • 预处理:如将 SCSS 编译为 CSS、将图片转换为 base64 等。

    常用加载器

    • babel-loader:使用 Babel 将 ES6+ 转换为 ES5。
    • css-loader:解析 CSS 文件中的 @importurl() 语法。
    • sass-loader:将 SCSS 编译为 CSS。
    • file-loader:处理文件导入(如图片、字体等),并将它们复制到输出目录。
  • 区别

    • Plugins(插件):用于扩展 Webpack 功能,可以在构建过程的各个阶段执行任务。插件更灵活,可以处理更广泛的任务,如打包优化、资源管理、增强功能等。
    • Loaders(加载器):用于转换模块,主要处理模块内容的预处理和转换,如将 ES6 转换为 ES5、将 SCSS 编译为 CSS 等。加载器只在模块导入时生效。

请解释在 CSS 中,盒模型(Box Model)是什么,以及它的各个组成部分。

  • CSS 盒模型是所有 HTML 元素的基础布局模型。它描述了一个元素在页面上的占位方式,包括内容、内边距(padding)、边框(border)和外边距(margin)。
标准盒模型(content-box)与替代盒模型(border-box)
  1. 标准盒模型(content-box)

    • box-sizing: content-box;
    • 在标准盒模型中,widthheight 只包括内容区域的大小,不包括内边距、边框和外边距。
    • 计算元素的总宽度和高度时,需要将内边距和边框的大小加上内容的宽度和高度。
  2. 替代盒模型(border-box)

    • 可以通过设置 box-sizing: border-box; 来启用替代盒模型。
    • 在替代盒模型中,widthheight 包括内容、内边距和边框的大小,不包括外边距。
    • 计算元素的总宽度和高度时,只需考虑外边距。
    • 盒模型组成:标准盒模型包括内容(Content)、内边距(Padding)、边框(Border)和外边距(Margin)。
    • 标准盒模型与替代盒模型:标准盒模型(content-box)计算 widthheight 时不包括内边距和边框,而替代盒模型(border-box)则包括内边距和边框。

请解释什么是单页应用程序(Single Page Application,SPA),以及它与多页应用程序(Multi Page Application,MPA)的区别。

  • 单页应用程序是一种网络应用程序或网站,用户与应用程序交互时,页面不会整体刷新,而是通过动态加载数据和页面部分内容来更新视图。

  • 主要特点

    1. 单个 HTML 页面:整个应用通常只有一个 HTML 页面,通过 JavaScript 动态加载和渲染内容。
    2. 路由管理:使用前端路由(如 React Router、Vue Router)来管理 URL 和视图的变化,避免页面刷新。
    3. 快速响应:由于不需要整体刷新页面,用户操作后的响应速度更快,用户体验更好。

    优点

    1. 用户体验好:页面切换流畅,无需整体刷新,提高用户体验。
    2. 高效的前端开发:可以使用现代前端框架和工具(如 React、Vue、Angular)进行开发,提升开发效率。
    3. 更容易实现复杂交互:由于整个应用运行在客户端,可以更容易实现复杂的用户交互和动态效果。

    缺点

    1. SEO 难度大:由于内容是通过 JavaScript 动态加载的,搜索引擎爬虫可能无法抓取和索引页面内容,影响 SEO。
    2. 初始加载时间较长:SPA 通常需要加载大量的 JavaScript 文件,初始加载时间可能较长。
    3. 浏览器兼容性问题:某些老旧浏览器可能不支持 SPA 所需的所有功能,需要额外处理兼容性问题。
多页应用程序(Multi Page Application,MPA)
  • 多页应用程序是一种传统的 web 应用,每次用户与应用交互时,都会从服务器加载一个新的 HTML 页面。

  • 主要特点

    1. 多个 HTML 页面:每个页面都有单独的 HTML 文件,用户访问不同页面时会触发整体页面刷新。
    2. 后端渲染:页面内容由服务器生成和渲染,用户请求新页面时,服务器返回完整的 HTML。
    3. 自然的 SEO 友好:由于每个页面都是独立的 HTML 文件,搜索引擎爬虫可以轻松抓取和索引页面内容。
  • 优点

    1. SEO 友好:每个页面都是独立的 HTML 文件,易于搜索引擎抓取和索引。
    2. 简单的架构:传统的服务器端渲染架构,技术成熟,易于维护和扩展。
    3. 较少的前端依赖:可以使用基本的 HTML、CSS 和 JavaScript 开发,不需要复杂的前端工具链。
  • 动态改变 meta 标签的局限性

    虽然可以使用 JavaScript 动态改变 标签(如meta 等)以改进 SEO,但这仅解决了部分问题:

    1. 动态更新的延迟

      • 搜索引擎爬虫可能在 JavaScript 执行之前就完成了对页面的抓取,导致爬虫无法看到动态更新的 meta标签。
    2. 初始内容不足

      • 即使动态更新了 meta 标签,初始加载的 HTML 内容仍然不足以让搜索引擎全面理解页面内容。

请解释 Webpack 中的代码拆分(Code Splitting)是什么,以及它的作用和优点。

  • 代码拆分(Code Splitting)是 Webpack 提供的一种强大的优化技术,通过将应用程序代码拆分成多个更小的块,可以显著减少初始加载时间,按需加载模块,提高应用的性能和用户体验。Webpack 提供了多种实现代码拆分的方法,包括多入口文件、动态导入和内置的 SplitChunksPlugin。

  • 代码拆分的作用和优点

    1. 减少初始加载时间

      • 通过将应用程序代码拆分成多个块,只加载当前页面需要的部分,减少了初始加载的 JavaScript 文件大小,从而加快页面的加载速度。
    2. 按需加载

      • 代码拆分允许在用户访问特定功能或页面时才加载相应的代码块。这样可以显著减少不必要的代码加载,提高应用的响应速度。
    3. 提高性能

      • 由于减少了初始加载时间和资源占用,代码拆分有助于提高整体性能,特别是在大型应用中效果显著。
    4. 更好的缓存策略

      • 代码拆分后,可以更好地利用浏览器缓存。当某个代码块内容未改变时,浏览器可以直接从缓存中加载,避免重复下载。

请解释 CSS Grid 和 Flexbox 的区别,并举例说明它们的使用场景。

  • CSS Grid 是一种二维的布局系统,可以同时处理行和列。它允许我们在网格中精确地放置元素,适合复杂的布局需求。

  • 主要特点

    1. 二维布局:CSS Grid 能够同时处理水平和垂直方向的布局。
    2. 显式网格:可以定义明确的行和列,并将元素放置在网格的特定位置。
    3. 复杂布局:适合创建复杂的布局,如瀑布流布局、响应式网格布局等。

    使用场景

    • 复杂布局:如页面主体结构、瀑布流布局、图片画廊等。

    • 网格布局:需要在行和列中精确放置元素的布局。

          .gird_data {
            width: 100%;
            height: 100%;
            display: grid;
            grid-template-columns: repeat(5, 1fr);
            grid-template-rows: repeat(4, auto); /* 定义4行,每行高度自动 */
            grid-gap: 10px;
          }
          .item {
            width: 100%;
            height: 100px;
            background-color: red;
          }
      ​
      
  • Flexbox 是一种一维布局系统,可以在水平或垂直方向上排列子元素。它主要用于处理一维空间内的布局问题。

  • 主要特点

    1. 一维布局:Flexbox 只能处理一个方向的布局(水平或垂直)。
    2. 灵活对齐:可以轻松实现元素的对齐、分布和顺序控制。
    3. 动态布局:适合内容不定的布局,如导航栏、工具栏等。

    使用场景

    • 简单布局:如导航栏、按钮组、弹性盒模型等。
    • 单行或单列布局:需要在一个方向上排列元素的布局。

请解释什么是 React 的生命周期方法,并列举它们的用途

  • 在 React 17 及其更高版本中,函数组件的生命周期不同于类组件,但可以通过 Hooks 实现类似的功能。函数组件没有类组件那样明确的生命周期方法(如 componentDidMountcomponentDidUpdatecomponentWillUnmount),但通过 useEffect 和其他 Hooks,可以实现类似的生命周期行为。
  1. 初次渲染(Initial Render)

    • 当函数组件首次渲染时,会执行组件函数,并返回要渲染的 JSX。

      function MyComponent() {
        // 初次渲染时,useState 初始化状态
        const [count, setCount] = useState(0);
      ​
        // 初次渲染和依赖项变化时,useEffect 执行
        useEffect(() => {
          console.log('Component mounted or updated');
          // 返回的函数在组件卸载时执行
          return () => {
            console.log('Component will unmount');
          };
        }, [count]); // 依赖项
      ​
        // 返回要渲染的 JSX
        return (
          <div>
            <p>{count}</p>
            <button onClick={() => setCount(count + 1)}>Increment</button>
          </div>
        );
      }
      
    • 初次渲染步骤

      1. 执行组件函数,初始化状态和其他 Hooks。
      2. 返回 JSX 以生成虚拟 DOM。
      3. React 将虚拟 DOM 渲染为真实 DOM
  2. 后续渲染(Subsequent Renders)

    • 当组件的状态或属性发生变化时,会重新执行组件函数,并更新虚拟 DOM。

    • 后续渲染步骤

      1. 执行组件函数,更新状态和其他 Hooks。

      2. 返回新的 JSX 以生成更新后的虚拟 DOM。

      3. React 将更新后的虚拟 DOM 与当前的虚拟 DOM 进行比较(diff 算法)。

      4. React 更新实际的 DOM 以匹配新的虚拟 DOM。

      5. 在初次渲染时,React 会执行以下 Hooks:

        • useState:初始化状态。
        • useReducer:初始化状态和提供 dispatch 函数。
        • useEffect:处理副作用(在组件渲染完成后执行)。
        • useLayoutEffect:在 DOM 更新后同步执行。
        • useRef:初始化 ref 对象。
        • useMemo:记忆化计算结果。
        • useCallback:记忆化函数。

        通过这些 Hooks,React 函数组件能够实现复杂的状态管理、副作用处理和性能优化。

请解释什么是 TypeScript 中的接口(Interface),并举例说明如何使用它们。

  • 接口用于定义对象的结构,可以包含属性和方法的签名。它们并不会在 JavaScript 中编译成实际的代码,只是用来帮助在开发过程中进行类型检查。

  • 主要特点

    1. 类型检查:接口用于类型检查,确保对象符合特定的结构。
    2. 可选属性:接口中的属性可以是可选的。
    3. 只读属性:接口中的属性可以设置为只读,防止修改。
    4. 方法签名:接口可以定义方法的签名,但不能包含具体实现。
    interface Person {
      name: string;
      age: number;
      sayHello(): void;
    }
    ​
    const person: Person = {
      name: 'John',
      age: 30,
      sayHello() {
        console.log(`Hello, my name is ${this.name}`);
      }
    };
    ​
    person.sayHello(); // 输出:Hello, my name is John
    
  • 接口不是类:接口用于定义对象的结构,可以包含属性和方法的签名,但没有具体实现。它们与类不同,不会在编译后生成实际的 JavaScript 代码。

    类型检查:接口用于类型检查,确保对象符合特定的结构。

    可选属性和只读属性:接口可以包含可选属性和只读属性。

    接口扩展和类实现:接口可以被扩展(类似于继承),类可以实现接口。

请解释什么是 WebAssembly,以及它的作用和使用场景。

  • WebAssembly(简称 Wasm)是一种二进制指令格式,作为一种可移植的、大小和加载时间优化的低级编程语言,用于在现代 Web 浏览器中执行。它是一种新的编码方式,使得在浏览器中运行的代码接近原生性能。

  • 主要特点

    1. 高性能:WebAssembly 代码可以接近原生性能,特别适用于计算密集型任务。
    2. 跨平台:WebAssembly 是一种平台无关的二进制格式,可以在不同的硬件和操作系统上运行。
    3. 与 JavaScript 互操作:WebAssembly 可以与 JavaScript 代码互操作,能够在 Web 应用中无缝集成。
  • WebAssembly 的作用

    1. 提升性能:通过将性能关键的代码用 WebAssembly 编写,可以显著提升 Web 应用的性能。
    2. 支持多种编程语言:WebAssembly 支持多种编程语言的编译输出,如 C、C++、Rust 等,使得开发者可以使用熟悉的编程语言进行 Web 开发。
    3. 丰富的应用场景:WebAssembly 适用于多种高性能应用场景,如游戏、视频编辑、虚拟现实(VR)、增强现实(AR)等。
  • 使用场景

    1. 游戏开发:利用 WebAssembly 提升游戏性能,使得复杂的 3D 图形和物理计算可以在浏览器中流畅运行。
    2. 视频处理:通过 WebAssembly 加速视频编码、解码和实时处理,提升视频应用的性能。
    3. 科学计算:利用 WebAssembly 加速数值计算和数据处理,适用于数据分析和机器学习等领域。
    4. 跨平台应用:通过 WebAssembly 开发跨平台应用,使得同一份代码可以在不同平台上运行。
  • 为什么通过 fetch 请求并转换为 ArrayBuffer

    1. 获取 WebAssembly 文件

      • WebAssembly 文件(.wasm)是二进制格式,需要通过 HTTP 请求获取。在浏览器中,最常用的方法是使用 fetch API。
    2. 转换为 ArrayBuffer

      • ArrayBuffer 是一个通用的二进制数据缓冲区。WebAssembly 编译和实例化过程需要一个包含二进制数据的 ArrayBuffer,因此需要将获取的 WebAssembly 文件转换为 ArrayBuffer

HTTP 和 HTTPS 区别

  • HTTP(HyperText Transfer Protocol)和HTTPS(HyperText Transfer Protocol Secure)是用于从Web服务器传输数据到浏览器的两种协议

  • 安全性

    • HTTP:数据以纯文本形式传输,没有加密,容易被中间人攻击(如窃听、篡改)。
    • HTTPS:数据在传输前经过SSL/TLS协议加密,确保数据在传输过程中不被窃听和篡改,提高了安全性和数据隐私
  • 端口

    • HTTP:使用端口80。
    • HTTPS:使用端口443。
  • 证书

    • HTTP:不需要证书。
    • HTTPS:需要由受信任的证书颁发机构(CA)颁发的SSL/TLS证书,以验证服务器的身份并加密通信。
  • 性能

    • HTTP:由于不进行加密和解密,速度相对较快,但不安全。
    • HTTPS:由于需要加密和解密处理,性能稍慢一些,但现代硬件的性能影响已经非常小。HTTPS还可以启用HTTP/2,进一步提升性能。

请解释 Webpack 的 Tree Shaking 功能是什么,以及它是如何工作的。

  • Tree Shaking 是一种用于删除 JavaScript 中未引用代码(dead code)的优化技术。它通过静态分析模块的依赖关系,移除未使用的导出内容,从而减小打包后的文件大小,提高应用的加载性能。

  • Tree Shaking 的工作原理

    1. ES6 模块化:Tree Shaking 依赖于 ES6 的模块系统(importexport)。ES6 模块的静态结构使得 Tree Shaking 可以在编译时确定哪些模块和函数是未使用的。
    2. 静态分析:Webpack 在构建过程中对模块进行静态分析,构建模块依赖树,确定哪些模块和导出内容是实际使用的。
    3. 删除未使用代码:通过分析模块依赖树,Webpack 移除那些未被引用的模块和导出内容,从而减小打包后的文件大小。
  • 使用 ES6 模块:确保代码使用 ES6 模块语法(importexport)。

  • 配置 Webpack:在生产模式下构建项目时,Webpack 默认启用 Tree Shaking。