• 前端面试题整理


    1.沙箱隔离

    前端沙箱隔离(Frontend sandbox isolation)是一种安全机制,用于将前端代码与主机环境隔离开来,以保护系统的安全性和稳定性。

    在Web开发中,前端代码通常由JavaScript编写,而JavaScript是一种强大且灵活的语言,但它也可能存在一些安全风险。例如,恶意用户可能会通过前端代码执行跨站脚本攻击(XSS)或跨站请求伪造(CSRF)等攻击。

    为了解决这些安全问题,前端沙箱隔离提供了一种隔离机制,使得前端代码不能直接访问和修改主机环境。它使用了一些技术来限制前端代码的权限,并提供一个受限的执行环境。

    常见的前端沙箱隔离技术包括:

    1. 同源策略(Same Origin Policy):浏览器采用同源策略,限制来自不同源的页面之间的交互。这样,前端代码只能与同一源(域名、协议和端口号相同)的页面进行通信,防止跨域攻击。

    2. 沙箱环境(Sandboxed Environment):一些现代浏览器提供了沙箱环境,即在一个受限制的执行环境中运行前端代码。这意味着代码被隔离在一个受控制的环境中,无法访问敏感资源或执行危险操作。

    3. 内容安全策略(Content Security Policy):内容安全策略是一种通过HTTP头部或HTML标签来定义规则的机制,用于限制页面中可以加载和执行的资源。它可以阻止不信任的脚本或外部资源的加载,从而减少潜在的安全风险。

    4. Web Worker:Web Worker 是一种在后台运行的 JavaScript 线程,它与主线程相互独立,可以在没有对 UI 的影响下执行复杂的计算任务。通过将耗时的计算任务放在 Web Worker 中执行,可以避免前端代码阻塞页面渲染,提高页面的响应性和安全性。

    通过使用这些前端沙箱隔离技术,开发人员可以增加应用程序的安全性,使其更具可靠性和稳定性。同时,用户可以更加放心地使用网页应用而不担心受到恶意攻击的影响。

    2.事件委托是将事件写在父级元素上

    是的,事件委托(Event delegation)是一种常用的前端开发技术,它将事件处理程序绑定到父级元素上,而不是直接绑定到每个子元素上。

    通过将事件处理程序绑定到父级元素上,可以利用事件冒泡(event bubbling)的原理来处理子元素的事件。当触发子元素上的事件时,事件将沿着DOM树向上传播到父级元素,而不仅仅是停留在子元素上。然后,父级元素可以根据事件的目标来确定要执行的操作。

    使用事件委托的好处包括:

    1. 减少事件处理程序的数量:通过将事件处理程序绑定到父级元素上,而不是每个子元素上,可以减少需要维护的事件处理程序的数量。这对于动态添加或删除子元素的情况尤其有用。

    2. 提高性能和内存效率:事件委托利用了事件冒泡的机制,将事件处理集中在父级元素上。这样可以避免给每个子元素添加事件处理程序,节省内存并提高性能。

    3. 处理动态元素:如果页面上有动态生成的元素,直接为每个元素绑定事件可能无法生效。而通过事件委托,可以确保动态生成的元素也能够被正确处理。

    使用事件委托时,需要在父级元素上监听相应事件,并在事件处理程序中判断事件的目标元素是否是我们所期望的子元素。如果是,则执行相应的操作;如果不是,则可以忽略该事件。

    总结而言,事件委托是一种利用事件冒泡机制的技术,将事件处理程序绑定到父级元素上以处理子元素的事件。它能够减少事件处理程序的数量、提高性能和内存效率,并且适用于处理动态生成的元素。

    3.对原型链的认识

    原型链(Prototype chain)是 JavaScript 中一个重要的概念,它实现了对象之间的继承关系。在 JavaScript 中,每个对象都有一个原型(prototype),它可以是另一个对象或者 null。当访问一个对象的属性或方法时,如果该对象本身没有定义该属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到对应的属性或方法或者到达原型链的顶端(即原型为 null)。

    以下是对原型链的一些认识:

    1. 原型对象(Prototype Object):每个 JavaScript 对象(除了 null)都有一个隐藏的内部属性[[Prototype]],指向其原型对象。可以通过 Object.getPrototypeOf(obj) 或者 obj.__proto__ 来访问对象的原型。

    2. proto 属性:__proto__ 是对象实例上的属性,它指向对象的原型。通过 obj.__proto__ 可以获取对象的原型,也可以用来设置对象的原型(不推荐使用)。例如,obj.__proto__ = proto 可以将 obj 的原型设置为 proto。

    3. 构造函数(Constructor):在 JavaScript 中,构造函数是用于创建对象的函数,通过 new 关键字调用。构造函数自身也是一个对象,它有一个 prototype 属性,指向构造函数的原型对象。构造函数通过 this 关键字可以给新创建的对象添加属性和方法。

    4. 原型继承:当访问一个对象的属性或方法时,如果该对象本身没有定义,JavaScript 引擎会沿着原型链向上查找。这种机制实现了对象之间的继承关系,可以让子对象共享父对象的属性和方法。

    5. 原型链的终点:原型链的终点是 Object.prototype,它是所有 JavaScript 对象的最顶层原型对象。Object.prototype 的原型为 null。

    6. 使用原型链的好处:原型链实现了对象之间的继承关系,通过共享原型对象的属性和方法,可以节省内存,并且使代码更加简洁和易于维护。

    需要注意的是,在 JavaScript 中,不推荐直接修改 __proto__ 属性或者使用 obj.__proto__ 来设置对象的原型。而应该使用 Object.create() 方法或者构造函数来创建具有指定原型的新对象。

    总结而言,原型链是 JavaScript 中实现对象之间继承关系的机制,每个对象都有一个原型,通过原型链可以访问和共享原型对象的属性和方法。原型链的终点是 Object.prototype,它是所有对象的最顶层原型对象。原型链的概念在 JavaScript 中非常重要,对于理解和使用 JavaScript 的面向对象特性至关重要。

    4.数据类型

    JavaScript 中有一些常见的数据类型,包括:

    1. 基本数据类型(Primitive data types):

      • 数字(Number):整数或浮点数,如 103.14
      • 字符串(String):一串文本字符,用引号(单引号或双引号)括起来,如 'Hello'"World"
      • 布尔值(Boolean):表示真(true)或假(false)的值。
      • null:表示一个空值或不存在的对象。
      • undefined:表示未定义的值。
      • Symbol(符号):表示唯一的标识符,用于创建对象的唯一属性键。
    2. 引用数据类型(Reference data types):

      • 对象(Object):复合值,可以包含多个属性和方法。对象可以是内置对象(如数组、日期、正则表达式等)、宿主对象(由宿主环境提供的对象,如浏览器的 DOM 对象)或自定义对象。
      • 数组(Array):一组有序的值,可以通过索引访问。数组可以包含不同类型的数据。
      • 函数(Function):可执行的代码块,接收参数并返回一个值。函数也是对象的一种,可以包含属性和方法。
      • 日期(Date):表示日期和时间的对象。
      • 正则表达式(Regular Expression):描述一种字符串匹配规则的对象。

    JavaScript 的数据类型是动态的,变量可以在不同的时间保存不同类型的值。通过 typeof 关键字可以获取一个值的类型。

    例如:

    let num = 10;
    let str = "Hello";
    let bool = true;
    let n = null;
    let u = undefined;
    let sym = Symbol("foo");
    
    console.log(typeof num);  // 输出: "number"
    console.log(typeof str);  // 输出: "string"
    console.log(typeof bool); // 输出: "boolean"
    console.log(typeof n);    // 输出: "object" (typeof null 的结果是 "object" 是由于历史原因)
    console.log(typeof u);    // 输出: "undefined"
    console.log(typeof sym);  // 输出: "symbol"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    需要注意的是,JavaScript 是一种动态类型语言,变量的类型是在运行时确定的,可以随时改变。因此,对于 JavaScript 开发来说,了解和正确使用不同的数据类型是至关重要的。

    5.Symbol(符号)

    Symbol 是 JavaScript 中的一种数据类型,引入于 ECMAScript 6(ES6)标准。它表示一个唯一且不可变的数据类型,用于创建对象的唯一属性键。

    Symbol 值通过 Symbol 函数调用来创建,可以传入一个可选的描述字符串作为标识符的描述。每个通过 Symbol 函数创建的 Symbol 值都是唯一的,即使它们的描述相同。这意味着,可以将 Symbol 值用作对象的属性键,确保属性名的唯一性,避免命名冲突。

    以下是一些使用 Symbol 的示例:

    // 创建一个 Symbol
    const symbol1 = Symbol();
    console.log(symbol1);  // 输出: Symbol()
    
    // 使用描述字符串创建一个 Symbol
    const symbol2 = Symbol('foo');
    console.log(symbol2);  // 输出: Symbol(foo)
    
    // 作为对象的属性键
    const obj = {};
    const prop = Symbol('bar');
    obj[prop] = 'value';
    console.log(obj[prop]);  // 输出: value
    
    // 遍历对象的 Symbol 属性键
    for (let key in obj) {
      console.log(key);  // 不会输出任何内容,Symbol 属性键不会被遍历
    }
    console.log(Object.getOwnPropertySymbols(obj));  // 输出: [Symbol(bar)]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    值得注意的是,Symbol 属性键在 for…in 循环中不会被遍历,也不会出现在 Object.keys、Object.values、Object.entries 方法返回的结果中。如果需要获取对象的所有 Symbol 属性键,可以使用 Object.getOwnPropertySymbols 方法。

    Symbol 还提供了一些内置的属性,如 Symbol.iterator、Symbol.toStringTag 等,用于指定对象的默认迭代器或自定义对象的类型标记。可以通过这些内置属性扩展 JavaScript 的语言特性。

    Symbol 的主要作用是确保属性名的唯一性,尤其在对象的属性键比较复杂或存在命名冲突的情况下非常有用。它在 JavaScript 中广泛用于实现类似私有属性、符号常量等的概念。

    6.ES5,ES6如何实现继承?

    防抖(Debounce)和节流(Throttle)是在 JavaScript 中用于优化函数执行频率的两种常见技术。

    1. 防抖(Debounce):
      • 当一个事件被触发后,延迟一定时间再执行相应的操作。如果在这段延迟时间内再次触发该事件,则重新计时。
      • 主要用于处理频繁触发的事件,如窗口大小改变、输入框输入等,以减少函数的执行次数。
      • 常见应用场景包括搜索框自动完成、无限滚动加载数据等。

    下面是一个简单的防抖函数的实现示例:

    function debounce(func, delay) {
      let timerId;
    
      return function (...args) {
        clearTimeout(timerId);
        timerId = setTimeout(() => {
          func.apply(this, args);
        }, delay);
      };
    }
    
    // 使用防抖函数包装需要执行的函数
    const debouncedFn = debounce(() => {
      console.log('Debounced function executed');
    }, 200);
    
    // 在事件触发时调用防抖函数
    debouncedFn();  // 在 200ms 后执行
    debouncedFn();  // 重新计时,再次延迟 200ms 执行
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    1. 节流(Throttle):
      • 当一个事件被触发后,在固定时间间隔内只执行一次相应的操作。
      • 主要用于限制函数的执行频率,尤其是处理持续触发的事件,如滚动事件、鼠标移动事件等。
      • 常见应用场景包括按钮防重复点击、限制请求发送频率等。

    下面是一个简单的节流函数的实现示例:

    function throttle(func, delay) {
      let timerId;
      let lastExecutedTime = 0;
    
      return function (...args) {
        const currentTime = Date.now();
    
        if (currentTime - lastExecutedTime >= delay) {
          func.apply(this, args);
          lastExecutedTime = currentTime;
        } else {
          clearTimeout(timerId);
          timerId = setTimeout(() => {
            func.apply(this, args);
            lastExecutedTime = currentTime;
          }, delay - (currentTime - lastExecutedTime));
        }
      };
    }
    
    // 使用节流函数包装需要执行的函数
    const throttledFn = throttle(() => {
      console.log('Throttled function executed');
    }, 200);
    
    // 在事件触发时调用节流函数
    throttledFn();  // 立即执行
    throttledFn();  // 在 200ms 后执行
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    防抖和节流可以根据不同的需求和场景选择合适的技术,通过控制函数的执行次数,提升页面性能和用户体验。

    7.ES5,ES6如何实现继承?

    在 ES5 中,可以使用原型链继承、构造函数继承和组合继承等方式来实现继承。而在 ES6 中,引入了 class 和 extends 关键字,使得实现继承更加简洁和易读。

    以下是在 ES5 和 ES6 中实现继承的示例:

    ES5 实现继承

    1. 原型链继承:

      function Parent() {
        this.name = 'Parent';
      }
      
      Parent.prototype.sayHello = function() {
        console.log('Hello, I am ' + this.name);
      };
      
      function Child() {
        this.age = 10;
      }
      
      Child.prototype = new Parent();
      
      var child = new Child();
      child.sayHello();  // 输出: Hello, I am Child
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
    2. 构造函数继承:

      function Parent(name) {
        this.name = name || 'Parent';
        this.sayHello = function() {
          console.log('Hello, I am ' + this.name);
        };
      }
      
      function Child(name) {
        Parent.call(this, name);
        this.age = 10;
      }
      
      var child = new Child('Child');
      child.sayHello();  // 输出: Hello, I am Child
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
    3. 组合继承(原型链继承 + 构造函数继承):

      function Parent(name) {
        this.name = name || 'Parent';
      }
      
      Parent.prototype.sayHello = function() {
        console.log('Hello, I am ' + this.name);
      };
      
      function Child(name) {
        Parent.call(this, name);
        this.age = 10;
      }
      
      Child.prototype = new Parent();
      
      var child = new Child('Child');
      child.sayHello();  // 输出: Hello, I am Child
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17

    ES6 实现继承

    使用 class 和 extends 关键字可以更简洁地实现继承:

    class Parent {
      constructor() {
        this.name = 'Parent';
      }
    
      sayHello() {
        console.log('Hello, I am ' + this.name);
      }
    }
    
    class Child extends Parent {
      constructor() {
        super();
        this.age = 10;
      }
    }
    
    const child = new Child();
    child.sayHello();  // 输出: Hello, I am Child
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    ES6 中的 class 和 extends 可以更直观地表示类之间的继承关系,而不需要手动设置原型链或调用构造函数。同时,通过 super 关键字可以在子类中调用父类的构造函数和方法,更方便地进行属性和方法的继承。

    8.讲讲什么是作用域

    作用域(Scope)是指在程序中定义变量、函数和对象时,这些标识符(Identifier)的可访问范围。简而言之,作用域决定了在代码中的哪些部分可以访问到变量、函数和对象。

    在 JavaScript 中,有以下几种常见的作用域:

    1. 全局作用域(Global Scope):

      • 全局作用域是在整个程序中都可访问的最外层作用域。
      • 在全局作用域中声明的变量和函数可以被程序中的任何位置访问。
    2. 函数作用域(Function Scope):

      • 函数作用域是在函数内部定义的变量和函数所具有的作用域。
      • 在函数作用域中声明的变量和函数只能在函数内部访问,外部无法访问。
    3. 块级作用域(Block Scope):

      • 块级作用域是在块({ })内部定义的变量所具有的作用域。
      • 在 ES6 之前,JavaScript 中没有块级作用域,只有全局作用域和函数作用域。
      • 在 ES6 中,引入了 let 和 const 关键字,用于声明块级作用域的变量。

    作用域规定了变量的可访问范围和生命周期。当需要使用一个变量时,JavaScript 引擎会在当前作用域中查找变量,如果找到,则使用该变量;如果找不到,则会继续向上查找,直到找到全局作用域。这被称为作用域链(Scope Chain)。

    作用域的理解对于编写和调试 JavaScript 代码非常重要,它有助于避免变量冲突、提高代码的可维护性,并且影响着变量的访问和生命周期。

    9.块级作用域

    块级作用域(Block Scope)是在块(由一对花括号 {} 包围的代码段)内部定义的变量所具有的作用域。在块级作用域中声明的变量只能在当前块内部访问,外部作用域无法访问。

    在 ES6(ECMAScript 2015)之前,JavaScript 中只有全局作用域和函数作用域,没有块级作用域。这意味着使用 var 关键字声明的变量不受花括号的限制,仍然可以在外部作用域访问到。

    而在 ES6 中,引入了 let 和 const 关键字来声明块级作用域的变量。

    使用 let 声明的变量具有块级作用域:

    function example() {
      if (true) {
        let x = 10; // 块级作用域内的变量
        console.log(x); // 输出: 10
      }
      console.log(x); // 报错: x is not defined
    }
    
    example();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在上面的示例中,变量 x 使用 let 声明,在 if 块内部定义,只能在该块内部访问。在块外部访问变量 x,会导致 ReferenceError。

    使用 const 声明的变量也具有块级作用域,并且具有常量的特性:

    function example() {
      if (true) {
        const y = 20; // 块级作用域内的常量
        console.log(y); // 输出: 20
        y = 30; // 报错: Assignment to constant variable
      }
    }
    
    example();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在上面的示例中,变量 y 使用 const 声明,也只能在 if 块内部访问。由于是常量,所以不能修改其值,否则会导致 TypeError。

    块级作用域的引入使得 JavaScript 中变量的作用域更加清晰和可控,避免了变量污染和冲突问题,并且增加了对变量的细粒度控制。

    10.单页面应用是什么?优缺点?如何弥补缺点

    单页面应用(Single Page Application,SPA)是一种Web应用程序的架构模式,它通过使用动态加载内容和异步数据交互,使用户在一个单独的页面上与应用程序进行交互,而不需要每次跳转页面。

    优点:

    1. 用户体验好:由于只需要加载一次页面,之后的页面切换都是通过动态加载内容实现,减少了页面刷新的延迟,用户体验更流畅。
    2. 前后端分离:前端负责渲染视图,后端负责提供接口和数据,各自独立开发,提高了开发效率和团队协作。
    3. 减少服务器负载:因为只有一个页面需要加载,减少了服务器传输的数据量和频率,降低了服务器的压力。
    4. 跨平台:单页面应用通常使用 JavaScript 编写,可以在多个平台上运行,例如浏览器、移动设备等。

    缺点:

    1. 首次加载时间长:由于所有的资源都要一次性加载并缓存,首次加载时间会比较长,特别是当应用变得庞大时。
    2. SEO 不友好:搜索引擎爬虫难以获取到使用 JavaScript 动态生成的内容,对于SEO来说不友好。
    3. 内存占用较高:由于在切换页面时,之前加载的内容并不会被释放,容易导致内存占用过高。
    4. 浏览器兼容性:某些旧版本的浏览器对于一些 HTML5 特性支持不完善,可能导致兼容性问题。

    如何弥补缺点:

    1. 优化首次加载时间:可以通过代码分割(code splitting)和资源压缩等手段来减少首次加载时间,以及使用缓存机制。
    2. 对于 SEO 不友好的问题,可以使用预渲染(prerendering)或服务器端渲染(Server-side Rendering,SSR)等技术来改善搜索引擎的爬取和索引。
    3. 合理释放资源:在进行页面切换时,需要合理地释放之前加载的资源,避免内存占用过高,可以通过垃圾回收机制实现。
    4. 对于浏览器兼容性问题,可以使用polyfill或使用适配和降级策略来解决旧版本浏览器的兼容性。

    综合考虑,在开发和设计单页面应用时,需要权衡每个项目的需求和限制,并选择合适的技术和解决方案来弥补其缺点,以实现更好的用户体验和性能。

    11.vuex中 mutation和action的区别和使用?

    在Vuex中,Mutation和Action是两个核心概念,用于管理和修改应用程序的状态。

    1. Mutation(变更状态):

      • Mutation是用于修改Vuex中的状态(State)的方法。
      • Mutation必须是同步函数,用于保证状态的可追踪性和可维护性。
      • Mutation通过提交(commit)来调用,并且只能在Vuex的store中调用。
      • Mutation需要定义在Vuex的store内部的mutations对象中。每个mutation都有一个关联的字符串类型的事件类型(type),以及一个处理函数(handler)。
      // 在Vuex的store中定义一个mutation
      const store = new Vuex.Store({
        state: {
          count: 0
        },
        mutations: {
          increment(state) {
            state.count++
          }
        }
      })
      
      // 在组件中提交一个mutation
      store.commit('increment')
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
    2. Action(异步操作):

      • Action用于处理异步操作、封装业务逻辑以及触发Mutation来改变状态。
      • Action可以包含任意异步操作,例如请求API、定时器等。
      • Action通过派发(dispatch)来调用,并且可以在组件中使用this.$store.dispatch或映射辅助函数进行调用。
      • Action需要定义在Vuex的store内部的actions对象中。每个action也有一个关联的字符串类型的事件类型(type),以及一个处理函数(handler)。
      // 在Vuex的store中定义一个action
      const store = new Vuex.Store({
        state: {
          count: 0
        },
        mutations: {
          increment(state) {
            state.count++
          }
        },
        actions: {
          incrementAsync(context) {
            setTimeout(() => {
              context.commit('increment')
            }, 1000)
          }
        }
      })
      
      // 在组件中派发一个action
      store.dispatch('incrementAsync')
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21

    总结:

    • Mutation用于同步地改变状态,而Action用于处理异步操作和封装复杂逻辑。
    • Mutation通过提交(commit)来调用,Action通过派发(dispatch)来调用。
    • Mutation必须是同步函数,而Action可以是异步操作。
    • 在组件中使用store.commit来调用Mutation,在组件中使用store.dispatch来调用Action。

    在实际开发中,通常建议将异步操作放在Action中处理,确保状态管理的一致性,并保持Mutation的纯粹性。通过Action的封装和处理,可以使代码更加清晰和可维护。

    12.Vue的虚拟Dom是什么?谈一谈对vue diff算法的认识?key的作用

    Vue的虚拟DOM(Virtual DOM)是Vue框架用于提升性能和优化渲染的一种技术。

    虚拟DOM是通过JavaScript对象模拟真实DOM的层次结构和属性。当数据发生变化时,Vue会通过比较新旧虚拟DOM的差异并将更改重新应用到真实DOM上,以避免直接操作真实DOM引起的性能损耗。虚拟DOM具有以下特点:

    1. 轻量快速:由于虚拟DOM是JavaScript对象,操作它的成本相对较低,比直接操作真实DOM快速且高效。
    2. 跨平台:虚拟DOM可以在不同平台上运行,例如浏览器、移动设备等。
    3. 高效更新:虚拟DOM会根据数据的变化生成新的虚拟DOM,并通过diff算法找出新旧虚拟DOM的差异,只更新差异部分,减少了不必要的重渲染,提高了性能。

    Vue的diff算法是用于比较新旧虚拟DOM的差异,并仅更新变化的部分到真实DOM上。Vue的diff算法基于以下几个原则:

    1. 以深度优先遍历算法进行对比:从根节点开始,逐层对比子节点的差异。
    2. 比较相同层级节点:只比较同级别的节点,不进行跨级别的比较。
    3. 使用唯一的key标识节点:通过key属性,可以告诉Vue哪些节点是稳定的,哪些是需要重新创建的,优化了对比过程,提高效率。
    4. 相同组件类型的节点进行复用:如果新旧虚拟DOM的同一位置的节点类型相同,直接复用该节点,减少了节点的销毁和重建。

    key的作用:
    在虚拟DOM的diff算法中,key是用来标识虚拟DOM节点的唯一性和稳定性。使用key可以帮助Vue跟踪每个节点的身份,从而优化更新过程。具体作用如下:

    1. 提高性能:通过key,Vue可以对比新旧虚拟DOM中的节点,准确判断出哪些是新增的、删除的或移动的节点,避免不必要的更新操作,提高性能。
    2. 维护组件状态:当列表数据变化时,如果没有key,Vue会按照就地复用的原则,导致组件状态错乱。而通过设置唯一的key,可以确保复用正确的组件和维护组件的状态。

    需要注意的是,key应该是稳定且唯一的,通常可以使用数据的唯一标识作为key。同时,避免在同一层级的兄弟节点中使用相同的key,这可能导致渲染错误。

    总结:
    虚拟DOM是Vue用于提升性能和优化渲染的技术,在数据变化时通过diff算法对比新旧虚拟DOM的差异,并只更新变化的部分到真实DOM上。key作为唯一标识节点的属性,在diff算法中起到标记节点身份和优化更新的作用。合理使用key可以提高渲染性能和组件状态的正确性。

    13.异步操作放在created还是mouted?

    在Vue中,异步操作可以放在createdmounted钩子函数中,具体取决于你的需求和操作类型。

    1. created钩子函数:在组件实例被创建之后立即调用。适合执行一些初始化操作,如获取数据、初始化变量等。如果异步操作不依赖于DOM元素,而是更多地关联到组件的数据或状态,那么可以将异步操作放在created钩子函数中。

    示例:

    created() {
      // 执行异步操作
      fetchData()
        .then(data => {
          // 处理数据
        })
        .catch(error => {
          // 处理错误
        });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. mounted钩子函数:在组件挂载到DOM之后调用。适合执行需要访问真实DOM元素的操作,比如初始化图表库、绑定第三方插件等。如果异步操作需要依赖已经渲染的DOM元素,并且操作涉及到DOM操作或尺寸计算等,那么可以将异步操作放在mounted钩子函数中。

    示例:

    mounted() {
      // 调用第三方插件
      this.$nextTick(() => {
        initChart();
      });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    需要注意的是,无论选择在created还是mounted中执行异步操作,都应该处理好异步操作的错误和取消。以及在组件销毁时清理相关资源,避免内存泄漏。

    综上所述,如果异步操作不需要依赖DOM元素,可以放在created钩子函数中。如果涉及到DOM操作或需要访问已渲染的DOM元素,可以放在mounted钩子函数中。根据具体需求选择适当的钩子函数来执行异步操作。

    13.vue子组件的生命周期?子元素在什么时候挂载?

    Vue Router提供了多个钩子函数,可以在路由导航过程中执行相应的操作。以下是Vue Router中常用的钩子函数:

    1. 全局前置守卫:

      • beforeEach: 在每个路由导航之前执行,可以用来进行全局的权限验证、登录状态检查等操作。
    2. 全局解析守卫:

      • beforeResolve: 在每个路由导航解析之前执行,与beforeEach类似。
    3. 全局后置钩子:

      • afterEach: 在每个路由导航之后执行,通常用于记录页面浏览日志、页面滚动行为等操作。
    4. 路由独享的守卫:

      • beforeEnter: 针对某个特定路由配置的前置守卫,在进入该路由前执行。
    5. 组件内的守卫:

      • beforeRouteEnter: 在进入路由前,但还未进入该路由对应组件时执行,无法直接访问组件实例。
      • beforeRouteUpdate: 在当前路由改变,但仍然复用该组件时执行,可用于检测路由参数的变化。
      • beforeRouteLeave: 在离开当前路由时执行,可用于提示用户保存未保存的数据或执行其他操作。

    这些钩子函数可以通过在路由配置中的路由对象上定义,也可以通过全局配置进行定义。例如:

    const router = new VueRouter({
      routes: [
        {
          path: '/home',
          component: Home,
          beforeEnter(to, from, next) {
            // 路由独享的守卫
            // 检查用户权限
            if (checkPermission()) {
              next();
            } else {
              next('/login');
            }
          },
        },
        // ...
      ],
    });
    
    router.beforeEach((to, from, next) => {
      // 全局前置守卫
      // 检查登录状态、权限等
      if (isAuthenticated()) {
        next();
      } else {
        next('/login');
      }
    });
    
    export default router;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    通过使用这些钩子函数,你可以在路由导航过程中执行一些操作,如权限验证、重定向、记录日志等,以实现更灵活和可定制的路由控制逻辑。

    14.vue子组件的生命周期?子元素在什么时候挂载?

    在Vue中,子组件也具有自己的生命周期钩子函数。以下是子组件的生命周期钩子函数及其执行顺序:

    1. beforeCreate:在实例被创建之前调用,此时组件的数据观测和事件配置尚未初始化。

    2. created:在实例创建完成后调用,此时已经完成了数据观测、属性和方法的运算,但尚未挂载到真实的DOM。

    3. beforeMount:在挂载开始之前被调用,此时模板编译已完成,但尚未将编译结果替换到真实的DOM中。

    4. mounted:在挂载完成后被调用,此时组件已经被渲染到真实的DOM中,可以操作DOM元素。

    5. beforeUpdate:在数据更新之前被调用,发生在虚拟DOM重新渲染和打补丁之前。

    6. updated:在数据更新之后被调用,组件的更新已经同步到DOM中。

    7. activated:在使用keep-alive组件时,子组件被激活时调用。

    8. deactivated:在使用keep-alive组件时,子组件被停用时调用。

    9. beforeDestroy:在实例销毁之前调用,此时实例仍然可用。

    10. destroyed:在实例销毁之后调用,此时组件已经被销毁,清理工作已完成。

    子组件的挂载发生在父组件使用该子组件时。当父组件渲染时,会触发子组件的创建和挂载过程。

    具体来说,当父组件渲染时,会实例化子组件并调用子组件的生命周期钩子函数。子组件的beforeCreatecreated钩子函数会在父组件渲染过程中被调用。然后,子组件被插入到父组件的DOM中,进行挂载过程,依次调用beforeMountmounted钩子函数。

    需要注意的是,子组件的生命周期是相对独立的,与父组件的生命周期并不完全一致。父组件的更新不会直接触发子组件的更新,子组件的更新由其自身的数据驱动。

    综上所述,子组件的生命周期包括了创建、挂载、更新和销毁等阶段,子组件的挂载发生在父组件使用该子组件时。

    15.vue的import和node的require区别?

    importrequire 都是模块加载的方式,但是二者有以下几个主要的区别:

    1. 语法

      • import 是 ES6 引入的模块化语法,使用 import 来加载模块。
      • require 是 Node.js 中引入的模块化方式,使用 require 来加载模块。
    2. 功能

      • import 能够真正意义上实现静态导入,能够在编译阶段就确定模块之间的依赖关系,支持的功能更加强大,例如动态导入。
      • require 是动态加载模块的,在执行时才会去加载模块,只能加载 CommonJS 规范的模块。
    3. 变量提升

      • import 加载的模块只有被调用时才会执行,变量不会提升,因此需要在文件头部引入。
      • require 会将整个文件执行,模块中的变量会存在变量提升。
    4. 输出

      • import 导入的是模块的指定成员,可以通过解构的方式获取其中的成员。
      • require 导入的是模块的完整对象,需要通过对象属性或方法来获取成员。

    总体而言,import 更为灵活强大,逐渐取代了 require 的地位。在浏览器端,需要使用工具将 ES6 转换为 ES5 才能运行;在 Node.js 中,可以使用 import 代替 require,但需要在文件扩展名为 .mjs 时才支持。

    16.路由守卫的生命周期是怎样的

    在 Vue Router 中,路由守卫是一种机制,用于在导航过程中对路由进行控制和处理。路由守卫可以帮助我们在跳转到不同的路由之前、期间或之后执行特定的逻辑。

    Vue Router 提供了三种类型的路由守卫:全局守卫、路由独享的守卫和组件内的守卫。这些守卫函数都有特定的生命周期钩子函数,用于定义在特定阶段触发的逻辑。

    以下是路由守卫的生命周期及对应的钩子函数:

    1. 全局前置守卫

      • beforeEach(to, from, next):在跳转路由之前被调用,可以用来进行权限验证、登录状态检查等操作。
    2. 全局解析守卫

      • beforeResolve(to, from, next):在路由解析过程中被调用,此时异步组件已经被解析完毕。
    3. 全局后置钩子

      • afterEach(to, from):在导航完成之后被调用,常用于页面的统计和记录。
    4. 路由独享的守卫

      • beforeEnter(to, from, next):在进入某个路由之前被调用,只对当前路由有效。
    5. 组件内的守卫

      • beforeRouteEnter(to, from, next):在进入路由组件之前被调用,此时组件实例还未被创建,无法访问组件实例的 this。
      • beforeRouteUpdate(to, from, next):在当前路由复用的情况下,路由参数发生变化时被调用。
      • beforeRouteLeave(to, from, next):在离开当前路由组件之前被调用,常用于弹出提示框确认是否离开页面。

    这些钩子函数都可以接收三个参数:

    • to:即将进入的目标路由对象
    • from:当前导航正要离开的路由对象
    • next:用于跳转到下一个钩子函数的回调函数

    你可以在这些钩子函数中执行一些逻辑,如根据权限判断是否允许进入某个路由、记录日志等。通过调用 next() 方法,可以继续执行后续的钩子函数或导航到指定路由。

    17.vuex如何解决数据丢失?

    Vuex 是 Vue.js 的状态管理库,用于解决组件之间共享数据的问题。虽然 Vuex 本身并不能直接解决数据丢失的问题,但可以通过一些方法来确保数据在刷新或路由切换后不丢失。

    1. 持久化存储:使用插件如 vuex-persistedstate 将 Vuex 的数据持久化到本地存储(如 localStorage)中,在刷新或重新加载页面后可以从本地存储中恢复数据。这样确保了数据的持久性。

    2. 合理设计数据流:在设计应用程序的数据流时,遵循单向数据流的原则,确保数据的变化能够被正确保存和同步。通过定义好的 mutation 和 action 来修改和更新数据,可以更好地跟踪和管理数据的变化。

    3. 在合适的时机加载数据:在组件加载时,可以通过钩子函数(如 created、mounted)来触发对应的 action,从服务端获取数据并保存到 Vuex。这样可以保证每次组件加载时都能及时加载所需的数据。

    4. 路由导航守卫:可以利用路由的导航守卫钩子函数(如 beforeEach)来在路由切换前检查是否需要保存当前数据。在离开当前路由前,可以将需要保留的数据通过 mutation 存储到 Vuex 中,以便后续使用。

    5. 结合后端接口:在进行数据操作时,可以结合后端接口设计合适的数据保存和恢复机制。例如,在提交表单数据时,可以通过请求将数据保存到后端数据库,并在需要时从后端重新获取数据。

    总的来说,Vuex 本身并不能直接解决数据丢失问题,但可以通过持久化存储、合理设计数据流、加载数据时机、路由导航守卫等方法来确保数据在刷新或路由切换后不丢失。

    18.v先并行请求2个接口后,再请求第3个接口,如何处理?

    在处理并行请求后再发起第三个接口请求时,你可以使用 Promise.all() 方法将这两个并行请求包装为 Promise,并在两个请求都完成后再发起第三个请求。下面是一个示例代码:

    // 引入 axios 或其他 HTTP 请求库
    import axios from 'axios';
    
    // 并行请求的接口1
    const request1 = axios.get('接口1的URL');
    // 并行请求的接口2
    const request2 = axios.get('接口2的URL');
    
    // 使用 Promise.all() 包装并行请求
    Promise.all([request1, request2])
      .then((responses) => {
        // 并行请求都成功完成后执行的逻辑
        // responses 是一个数组,包含了两个请求的响应对象,顺序与请求的顺序一致
        // 可以通过 responses[0] 和 responses[1] 获取对应的响应数据
        const response1 = responses[0];
        const response2 = responses[1];
    
        // 处理第三个接口的请求
        return axios.get('第三个接口的URL');
      })
      .then((response3) => {
        // 第三个接口请求成功后的逻辑
        // response3 是第三个接口的响应对象,包含了响应数据
        console.log(response3.data);
      })
      .catch((error) => {
        // 错误处理
        console.error(error);
      });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在上述代码中,我们首先创建了两个并行请求 request1request2,然后使用 Promise.all() 将它们包装为一个 Promise。当这两个请求都成功完成后,then 方法中的回调函数会被执行,我们可以在其中处理这两个请求的响应数据,然后再发起第三个接口的请求。最后,通过 then 方法处理第三个接口请求成功后的逻辑,或通过 catch 方法捕获任何错误。

    19.说几个ES6新增的数组的方法

    ES6 引入了一些有用的数组方法,下面列举了其中的几个:

    1. Array.from():将类似数组或可迭代对象转换为真正的数组。
    const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
    const newArray = Array.from(arrayLike);
    console.log(newArray); // ['a', 'b', 'c']
    
    • 1
    • 2
    • 3
    1. Array.of():根据传入的参数创建一个新数组,无论参数的类型和数量。
    const newArray = Array.of(1, 2, 3, 'a', 'b');
    console.log(newArray); // [1, 2, 3, 'a', 'b']
    
    • 1
    • 2
    1. Array.prototype.find():返回数组中满足测试函数条件的第一个元素值。
    const numbers = [1, 2, 3, 4, 5];
    const found = numbers.find((element) => element > 3);
    console.log(found); // 4
    
    • 1
    • 2
    • 3
    1. Array.prototype.findIndex():返回数组中满足测试函数条件的第一个元素的索引。
    const numbers = [1, 2, 3, 4, 5];
    const foundIndex = numbers.findIndex((element) => element > 3);
    console.log(foundIndex); // 3
    
    • 1
    • 2
    • 3
    1. Array.prototype.includes():判断数组是否包含指定元素,返回布尔值。
    const numbers = [1, 2, 3, 4, 5];
    console.log(numbers.includes(3)); // true
    console.log(numbers.includes(6)); // false
    
    • 1
    • 2
    • 3

    这些是 ES6 中新增的一些有用的数组方法,它们提供了更加便捷和简洁的方式来操作和处理数组。这些方法可以提高开发效率并使代码更可读。

    20.vue2生命周期

    在 Vue 2 中,组件实例有以下生命周期钩子函数:

    1. beforeCreate:在实例初始化之后,数据观测 (data observer) 和事件/watcher 事件配置之前被调用。

    2. created:在实例创建完成后被立即调用。此时,实例已经完成数据观测 (data observer),属性和方法的运算,watch/event 事件回调。然而,挂载阶段还未开始,$el 属性尚不可用。

    3. beforeMount:在挂载开始之前被调用。相关的 render 函数首次被调用。

    4. mounted:实例被挂载后调用。此时,el 已经关联到实例的 vm. e l ,并进行 D O M 渲染。如果组件使用了 ‘ t e m p l a t e ‘ 选项,则只有在 ‘ m o u n t e d ‘ 钩子被调用后,整个模板才会在 ‘ el,并进行 DOM 渲染。如果组件使用了`template`选项,则只有在`mounted`钩子被调用后,整个模板才会在 ` el,并进行DOM渲染。如果组件使用了template选项,则只有在mounted钩子被调用后,整个模板才会在el` 内完全渲染。

    5. beforeUpdate:数据更新时调用,但是在 DOM 更新之前。

    6. updated:在数据更改导致虚拟 DOM 重新渲染和打补丁之后调用。

    7. beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。

    8. destroyed:实例销毁之后调用。当该钩子被调用时,Vue 实例的所有指令、过滤器、事件监听器等都已被解绑,子实例也被销毁。

    此外,还有两个和异步任务相关的钩子函数:

    • beforeMountmounted 钩子函数中可以使用 vm.$nextTick() 方法,在下次 DOM 更新循环结束之后执行一个回调。
    • created 钩子函数中可以使用 vm.$watch() 监听一个属性的变化,当监测到变化时执行回调。

    这些生命周期钩子函数为我们提供了在组件不同阶段执行代码的机会,可以用于初始化数据、与外部 API 交互、在 DOM 更新后执行操作、清理内存等。

    21.vue2生命周期 有几个

    Vue 2 中有8个生命周期钩子函数,它们按照组件创建、挂载、更新和销毁的不同阶段进行调用。这些钩子函数分别是:

    1. beforeCreate:实例刚在内存中创建,此阶段无法访问到组件中的 datamethods
    2. created:实例已经创建完成,可以访问到 datamethods,但此时尚未挂载到 DOM 上。
    3. beforeMount:在挂载开始之前调用,此时模板编译已完成,即将开始渲染组件。
    4. mounted:实例已经挂载到 DOM 上,此时可以进行 DOM 操作,如获取元素、绑定事件等。
    5. beforeUpdate:数据更新时调用,但是在 DOM 更新之前的阶段。
    6. updated:在数据变化导致的虚拟 DOM 重新渲染和打补丁之后调用。
    7. beforeDestroy:在组件销毁之前调用,可以进行善后工作,如清除计时器、解绑全局事件等。
    8. destroyed:组件销毁后调用,此时组件实例及其相关对象都会被销毁。

    这些生命周期钩子函数为我们提供了对组件不同阶段进行操作的机会,可以在适当的时候执行特定的代码,以满足业务需求或进行资源清理。

    22.vuex如何解决数据丢失?

    Vuex 是 Vue.js 的状态管理库,用于集中管理组件之间共享的状态。对于数据丢失的问题,Vuex 本身并没有提供特定的解决方案,但可以通过一些策略来减少或避免数据丢失的风险。

    以下是一些常见的方法和建议:

    1. 合理设计数据结构:在使用 Vuex 存储数据时,确保数据结构合理。遵循单一数据源的原则,将不同的模块、组件状态分开管理,并定义清晰的数据结构以避免混乱和数据丢失。使用对象、数组等数据类型时,确保正确的引用和拷贝,避免直接修改状态数据。

    2. 使用持久化插件:Vuex 的持久化插件(例如 vuex-persistedstate)可以将 Vuex 中的状态持久化到本地存储,如 localStorage 或 sessionStorage。通过将状态存储到本地,即使页面刷新或关闭再打开,数据也可以得到恢复,并避免数据丢失。

    3. 避免异步操作时的数据冲突:当多个组件同时进行异步操作,对于某个共享状态的修改,可能会导致数据冲突或丢失。在这种情况下,可以利用 Vuex 的 action 和 mutation 进行同步处理,避免同时对同一状态进行修改。

    4. 合理使用组件生命周期钩子:在组件的生命周期钩子函数中,可以通过订阅 store 中的状态变化,在特定时机保存数据到后端或本地存储,以确保数据的持久性。

    5. 错误处理和容错机制:在异步操作中要注意错误处理,捕获异常并采取相应的措施,例如回退到上一个有效的状态或提供用户友好的错误提示。

    总之,Vuex 本身不能完全解决数据丢失的问题,但通过合理设计数据结构、使用持久化插件、避免冲突、利用生命周期钩子等方法,可以最大限度地减少数据丢失的风险,并确保应用的状态管理更加可靠。

    23.宏任务和微任务有哪些?执行顺序?

    宏任务和微任务是指在 JavaScript 引擎中执行的两类任务,它们的执行顺序有一定的规律。

    宏任务是一些较为耗时的任务,例如输入、网络通信、计时器等。在每个宏任务执行完成后,JavaScript 引擎会检查是否有微任务需要执行。如果有,会依次执行所有微任务直到清空微任务队列,然后再进行下一个宏任务。也就是说,在一个宏任务中产生的所有微任务都会在这个宏任务结束之前执行完毕。

    而微任务则是一些轻量级的任务,例如 Promise 的回调函数、MutationObserver 等。它们的执行优先级高于宏任务。因此,当发生宏任务和微任务同时存在的情况时,JavaScript 引擎会先执行所有微任务,再执行下一个宏任务,即先处理微任务再处理宏任务。

    例如,在一段异步代码中,当异步操作成功返回时,Promise 的 then 回调将被放入微任务队列中,等待执行。而在当前宏任务结束后,JavaScript 就会去检查微任务队列,如果有待执行的微任务,就逐一执行它们。如果微任务队列为空,那么 JavaScript 就会取出下一个宏任务,继续执行。

    总的来说,宏任务和微任务的执行顺序可以概括为:

    1. 执行当前宏任务;
    2. 检查微任务队列,依次执行所有微任务;
    3. 取出下一个宏任务,重复执行上述步骤。

    需要注意的是,在每个宏任务中,只有当所有同步任务执行完毕,JavaScript 引擎才会考虑执行微任务,因此如果当前宏任务中存在循环或递归等耗时任务,可能会导致微任务无法及时执行,从而延迟了数据的变化和其他异步操作的执行。

    24解决首次加载白屏

    白屏问题通常是由于页面加载较慢或资源下载阻塞导致的。以下是一些常见的解决方案,可以帮助解决首次加载时的白屏问题:

    1. 优化代码和资源:对代码和资源进行优化,减少文件大小、请求次数和网络传输时间,以提高页面加载速度。可以压缩和合并 JavaScript 和 CSS 文件,使用图片压缩技术,延迟加载非关键资源等。

    2. 使用浏览器缓存:通过设置适当的缓存策略,让浏览器缓存静态资源,从而减少重复的网络请求。可以通过设置 HTTP 响应头中的 Cache-ControlExpires 字段来控制缓存策略。

    3. 异步加载脚本:将页面中的一些 JavaScript 脚本标记为异步加载,这样可以让浏览器在加载其他资源时并行下载脚本文件。可以使用