春招面试万字整理,全程拷打,干货满满(2)

665 阅读27分钟

最近有几次面试,总结了一下面试遇到的题目。不说废话,就直接上干货了。

2222.jpg

vue 生命周期钩子

Vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是Vue的⽣命周期。

  1. beforeCreate(创建前) :数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据。

  2. created (创建后) :实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 $el 属性。

  3. beforeMount(挂载前) :在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。此时还没有挂载html到页面上。

  4. mounted(挂载后) :在el被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html 页面中。此过程中进行ajax交互。

  5. beforeUpdate(更新前) :响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染。

  6. updated(更新后) :在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。

  7. beforeDestroy(vue2)/BeforeUnmount(vue3)(销毁前) :实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例。

  8. destroyed(vue2)/Unmounted(vue3)(销毁后) :实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。

  9. onErrorCaptured: 当捕获到一个从后代组件传来的错误时被调用。

另外还有 keep-alive 独有的生命周期,分别为 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated 钩子函数。

tailwindcss

Tailwind CSS 是一个高度可定制的低级 CSS 框架,它提供了许多实用工具类(utility-first),使得你可以通过组合这些类来构建自定义的设计。与传统的预设组件库不同,Tailwind CSS 不提供预构建的组件,而是允许你通过组合不同的实用工具类来创建独特的设计。

  • 原子类
  • 几乎不需要写css,适配也方便
  • tailwindcss 语义和 AIGC 生成前端代码 更搭

JS/TS数据类型

JS基本数据类型和引用数据类型

基本数据类型: Undefined、Null、Boolean、Number、String、Symbol、BigInt。 引用数据类型:Object

其中 Symbol 和 BigInt 是ES6 中新增的数据类型:

  • Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

TS的数据类型

TypeScript 是 JavaScript 的一个超集,它为 JavaScript 添加了可选的静态类型和基于类的面向对象编程特性。TS的类型包括了JS的所有数据类型,并且 还引入了一些额外的静态类型和特性,以提供更强的类型检查能力和更好的开发体验。

元组(Tuple)

  • 在 JavaScript 中,数组的元素可以是不同类型,但你无法在语言级别上强制规定每个位置上的具体类型。
  • TypeScript 的元组允许你定义固定长度且各位置具有特定类型的数组。
let tupleType: [string, number];
tupleType = ["hello", 10]; // 正确
// tupleType = [10, "hello"]; // 错误

 枚举(Enum)

  • JavaScript 没有内置的枚举概念。
  • TypeScript 提供了枚举类型,使得一组命名常量更容易管理和使用。
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
console.log(Color[1]); // 输出 'Green'

 任意类型(Any)

  • 尽管 JavaScript 本身没有这种明确的概念,但在 TypeScript 中 any 类型用于表示变量可以持有任何类型的值,当你不确定或者希望绕过类型检查时很有用。
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // 可以自由改变类型

未知类型(Unknown)

  • 尽管 JavaScript 本身没有这种明确的概念,但在 TypeScript 中 unknown 类型用于表示变量可以持有任何类型的值,但与 any 不同的是,在对 unknown 类型的值执行大多数操作之前,必须进行某种形式的类型检查或类型断言。这使得 unknown 成为一种更安全的选择,当你不确定变量的具体类型但希望避免绕过类型系统时非常有用。
let mightBe: unknown = 4;
console.log(mightBe); // 可以持有任何类型的值

mightBe = "maybe a string instead"; // 类型可以自由改变
mightBe = false; // 同样支持布尔类型

// 以下操作会报错,TypeScript 编译器无法确定类型
// console.log(mightBe.toUpperCase());

// 需要进行类型断言或者类型保护
if (typeof mightBe === "string") {
    console.log(mightBe.toUpperCase()); // 现在可以安全调用字符串的方法
}

// 或者使用类型断言
(console.log((mightBe as string).toUpperCase())); // 使用类型断言来调用方法

空值(Void)

  • JavaScript 中函数不返回值时,默认返回 undefined,但没有专门的类型来表示这一点。
  • 在 TypeScript 中,void 表示没有可用类型的值,通常用于描述不会返回值的函数。
function warnUser(): void {
    console.log("This is my warning message");
}

Never

  • never 类型表示那些永不存在的值的类型。例如,函数总是抛出异常或根本没有返回值的情况。
function error(message: string): never {
    throw new Error(message);
}

联合类型(Union Types)

  • 允许变量被设置为多种类型中的一种,这在 JavaScript 中是没有直接对应的特性的。
let id: string | number;
id = 10; // 正确
id = "10"; // 正确
// id = true; // 错误,因为 boolean 不是 string | number 的一部分

交叉类型(Intersection Types)

  • 结合多个类型为一个类型,即该类型必须满足所有结合类型的条件。
interface A { a: number }
interface B { b: string }
type AB = A & B;
let ab: AB = {a: 1, b: "test"}; // 必须同时包含 A 和 B 的属性

存储方式(堆和栈)

这些数据可以分为原始数据类型和引用数据类型:

  • 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
  • 堆:引用数据类型(对象、数组和函数)

两种类型的区别在于存储位置的不同:

  • 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
  • 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

数据结构和操作系统中的堆和栈

堆和栈的概念存在于数据结构和操作系统内存中

数据结构中:

  • 在数据结构中,栈中数据的存取方式为先进后出。

  • 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。

操作系统中,内存被分为栈区和堆区:

  • 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区内存一般由开发者分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。

什么时候需要使用到状态管理库

随着应用规模的增长和复杂性的增加,使用状态管理库可以帮助你更好地管理和组织状态逻辑,从而提高开发效率和应用的可维护性。

1. 全局状态管理需求

当你的应用中存在多个组件需要共享和访问相同的数据(如用户登录状态、主题设置等),通过 props 和事件机制在嵌套较深的组件树之间传递这些数据会变得繁琐且难以维护。状态管理库提供了一个中心化的地方来存储和管理这种全局状态。

2. 复杂的状态逻辑

如果你的应用涉及复杂的业务逻辑,例如需要根据不同的用户交互或外部事件更新多个相互关联的状态,直接在 Vue 组件中处理可能会导致代码难以理解和测试。状态管理库提供了更清晰的模式来组织这类逻辑。

3. 非父子组件通信

在没有直接父子关系的组件之间进行通信时,直接使用事件总线或依赖注入可能会使代码结构变得混乱。状态管理库提供了一种更加结构化的方法来实现这种跨层级的组件通信。

4. 提升开发效率和可维护性

使用状态管理库可以帮助团队成员更容易理解项目中的状态流,减少因为状态分散而导致的错误。此外,良好的状态管理模式有助于提高代码的可复用性和可测试性。

数组上的方法

数组和字符串转换方法

  • toString :将数组或字符串转换为字符串形式。对于数组,会将每个元素转换为字符串并以逗号连接。
    const array1 = [1, 2, 'a', 'b']; 
    console.log(array1.toString()); // 输出 "1,2,a,b"
    
  • toLocaleString :类似于 toString(),但会使用本地特定的格式规则来转换数组中的每一项。
    const prices = [
        new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(50), 
        new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(100)
    ];
    console.log(prices.toLocaleString()); // 输出 "$50.00,$100.00"(取决于本地设置)
    
  • join :将数组的所有元素连接成一个字符串,并返回这个字符串。可以指定一个分隔符(默认为逗号)。
    const array1 = ['Wind', 'Rain', 'Fire']; 
    console.log(array1.join(' / ')); // 输出 "Wind / Rain / Fire"
    

数组尾部和首部操作方法

  • push :在数组末尾添加一个或多个元素,并返回新数组的长度。
    const animals = ['pigs', 'goats', 'sheep']; 
    const count = animals.push('cows'); 
    console.log(count); // 输出 4 
    console.log(animals); // 输出 ["pigs", "goats", "sheep", "cows"]
    
  • pop :移除数组的最后一个元素,并返回该元素。
    const plants = ['broccoli', 'cauliflower', 'cabbage', 'kale', 'tomato']; 
    console.log(plants.pop()); // 输出 "tomato" 
    console.log(plants); // 输出 ["broccoli", "cauliflower", "cabbage", "kale"]
    
  • shift :移除数组的第一个元素,并返回该元素。此操作会改变数组长度。
    const fruits = ['strawberry', 'peach', 'lemon', 'orange']; 
    console.log(fruits.shift()); // 输出 "strawberry" 
    console.log(fruits); // 输出 ["peach", "lemon", "orange"]
    
  • unshift :在数组开头添加一个或多个元素,并返回新数组的长度。
    const vegetables = ['parsnip', 'potato']; 
    vegetables.unshift('celery', 'turnip'); 
    console.log(vegetables); // 输出 ["celery", "turnip", "parsnip", "potato"]
    

重排序数组的方法

  • reverse :颠倒数组中元素的位置。直接修改原数组。
    const a = ['one', 'two', 'three'];
    a.reverse(); 
    console.log(a); // 输出 ["three", "two", "one"]
    
  • sort :按字母顺序对数组元素进行排序;如果提供了比较函数,则根据函数的结果进行排序。比较函数接收两个参数,如果返回值大于0,则交换这两个参数的位置。
    const numbers = [4, 2, 5, 1, 3]; 
    numbers.sort((a, b) => a - b); 
    console.log(numbers); // 输出 [1, 2, 3, 4, 5]
    

连接与截取数组的方法

  • concat(...arrays) :合并两个或更多数组而不改变原数组,返回合并后的新数组。
    const array1 = ['a', 'b', 'c']; 
    const array2 = ['d', 'e', 'f']; 
    const newArray = array1.concat(array2); 
    console.log(newArray); // 输出 ["a", "b", "c", "d", "e", "f"]
    
  • slice(start, end) :提取从 start 开始至 end(不包括 end)的数组元素部分,不影响原数组。
    const animals = ['ant', 'bison', 'camel', 'duck', 'elephant']; 
    console.log(animals.slice(2)); // 输出 ["camel", "duck", "elephant"] 
    console.log(animals.slice(2, 4)); // 输出 ["camel", "duck"]
    

插入和查找方法

  • splice(index, howMany, ...items) :从 index 开始删除 howMany 个元素,并插入 items 中的所有元素。影响原数组。

    const months = ['Jan', 'March', 'April', 'June']; 
    months.splice(1, 0, 'Feb'); // 插入 "Feb" 在索引 1 处 
    console.log(months); // 输出 ["Jan", "Feb", "March", "April", "June"] 
    months.splice(4, 1, 'May'); // 替换 "June" 为 "May" 
    console.log(months); // 输出 ["Jan", "Feb", "March", "April", "May"]
    
  • indexOf(searchElement[, fromIndex]) :返回找到的第一个匹配项的索引,如果没有找到则返回 -1

    const beasts = ['ant', 'bison', 'camel', 'duck', 'bison']; 
    console.log(beasts.indexOf('bison')); // 输出 1 
    console.log(beasts.indexOf('giraffe')); // 输出 -1
    
  • lastIndexOf(searchElement[, fromIndex]) :从数组的末尾开始查找,其它与 indexOf 类似。

    const beasts = ['ant', 'bison', 'camel', 'duck', 'bison']; 
    console.log(beasts.lastIndexOf('bison')); // 输出 4
    
  • includes:判断一个数组是否包含一个指定的值,如果包含则返回 true,否则返回 false。第二个参数可以设置从哪个下标开始。

    const array1 = [1, 2, 3];
    console.log(array1.includes(2)); // 输出 true
    
  • find:返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined

    const array1 = [5, 12, 8, 130, 44];
    const found = array1.find(element => element > 10);
    console.log(found); // 输出 12
    
  • findIndex:返回数组中满足提供的测试函数的第一个元素的索引。否则返回 -1

    const index = array1.findIndex(element => element > 10);
    console.log(index); // 输出 1
    
迭代方法

这些方法的第二个参数都是用于指定在回调函数中用作 this 的值

  • every :测试数组的所有元素是否都通过了测试函数的测试。
    const isBelowThreshold = (currentValue) => currentValue < 40; 
    const array1 = [1, 30, 39, 29, 10, 13]; 
    console.log(array1.every(isBelowThreshold)); // 输出 true
    
  • some :测试数组中是否有至少一个元素通过了测试函数的测试。
    const array = [1, 2, 3, 4, 5]; 
    const even = (element) => element % 2 === 0; 
    console.log(array.some(even)); // 输出 true
    
  • filter :创建一个新数组,包含所有通过测试函数的元素。
    const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present']; 
    const result = words.filter(word => word.length > 6); 
    console.log(result); // 输出 ["exuberant", "destruction", "present"]
    
  • map :创建一个新数组,其结果是对调用数组的每一个元素应用一个函数后的返回值。
    const array1 = [1, 4, 9, 16];
    const map1 = array1.map(x => x * 2); 
    console.log(map1); // 输出 [2, 8, 18, 32]
    
  • forEach :对数组的每一个元素执行一次提供的函数。
    const array1 = ['a', 'b', 'c']; 
    array1.forEach(element => console.log(element)); 
    // 输出: 
    // a 
    // b 
    // c
    

归并方法

第二个参数设置初始值

  • reduce :对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个值。
    const array1 = [1, 2, 3, 4]; 
    const reducer = (accumulator, currentValue) => accumulator + currentValue; 
    console.log(array1.reduce(reducer)); // 输出 10 
    console.log(array1.reduce(reducer, 5)); // 输出 15
    
  • reduceRight :与 reduce 类似,但处理方向是从右向左。
    const array1 = ['1', '2', '3', '4']; 
    const reducer = (accumulator, currentValue) => accumulator + currentValue; 
    console.log(array1.reduceRight(reducer)); // 输出 "4321"
    

填充和清空方法

  • fill:用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。

    const array1 = [1, 2, 3, 4];
    console.log(array1.fill(0, 2, 4)); // 输出 [1, 2, 0, 0]
    
  • copyWithin:浅复制数组的一部分到同一数组中的另一个位置,并返回它,不会改变原数组的长度。

    const array1 = ['a', 'b', 'c', 'd', 'e'];
    console.log(array1.copyWithin(0, 3, 4)); // 输出 ['d', 'b', 'c', 'd', 'e']
    

迭代器方法

  • entries:返回一个新的数组迭代器对象,该对象包含数组中每个索引的键/值对。

    const array1 = ['a', 'b', 'c'];
    const iterator1 = array1.entries();
    console.log(iterator1.next().value); // 输出 [0, 'a']
    
  • keys:返回一个新的数组迭代器对象,该对象包含数组中每个索引的键。

    const array1 = ['a', 'b', 'c'];
    const iterator = array1.keys();
    for (const key of iterator) {
      console.log(key); // 输出 0, 1, 2
    }
    
  • values:返回一个新的数组迭代器对象,该对象包含数组中每个索引的值。

    const array1 = ['a', 'b', 'c'];
    const iterator = array1.values();
    for (const value of iterator) {
      console.log(value); // 输出 'a', 'b', 'c'
    }
    

其他方法

  • flat:按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。默认深度为 1。

    const array1 = [1, 2, [3, 4]];
    console.log(array1.flat()); // 输出 [1, 2, 3, 4]
    
  • flatMap:首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。它等价于 map()flat() 的组合,但 flatMap() 只会压平一层数组。

    const array1 = [1, 2, 3, 4];
    console.log(array1.flatMap(x => [x * 2])); // 输出 [2, 4, 6, 8]
    

返回新数组 与 修改原数组

返回新数组

  • concat
  • slice
  • filter
  • map
  • flat
  • flatMap
  • entries
  • keys
  • values

修改原数组

  • push
  • pop
  • shift
  • unshift
  • reverse
  • sort
  • splice
  • fill
  • copyWithin

字符串的方法

字符串和数组转换方法

  • split(separator, limit):将一个字符串分割成子字符串数组,并返回这个数组。
    const str = "How are you doing today?";
    const words = str.split(' ');
    console.log(words); // 输出 ["How", "are", "you", "doing", "today?"]
    
    const byLimit = str.split(' ', 3);
    console.log(byLimit); // 输出 ["How", "are", "you"]
    

查找方法

  • indexOf(searchvalue, start):返回指定值在字符串中首次出现的位置,如果没有找到则返回 -1
    const sentence = 'The quick brown fox jumps over the lazy dog.';
    const idx = sentence.indexOf('fox');
    console.log(idx); // 输出 16
    
  • lastIndexOf(searchvalue, start):从后往前搜索字符串,并返回指定值最后一次出现的位置,如果未找到则返回 -1
    const sentence = 'The quick brown fox jumps over the lazy fox.';
    const idx = sentence.lastIndexOf('fox');
    console.log(idx); // 输出 40
    
  • includes(searchvalue, start):判断一个字符串是否包含在另一个字符串中,根据情况返回 truefalse
    const str = 'Hello world';
    console.log(str.includes('world')); // 输出 true
    console.log(str.includes('World')); // 输出 false
    

提取方法

  • slice(start, end):提取字符串的一部分并返回新字符串(不包括结束位置的字符)。
    const str = "The quick brown fox jumps over the lazy dog.";
    console.log(str.slice(31)); // 输出 "the lazy dog."
    console.log(str.slice(4, 19)); // 输出 "quick brown fox"
    
  • substring(start, end):类似于 slice(),但不能接受负数作为参数。
    const str = "Mozilla";
    console.log(str.substring(1, 3)); // 输出 "oz"
    

修改方法

  • replace(searchFor, replaceWith):用一些其他字符替换匹配到的字符或正则表达式。只替换第一个匹配项。
    const greeting = 'Hello world!';
    console.log(greeting.replace('world', 'Earth')); // 输出 "Hello Earth!"
    
  • replaceAll(searchFor, replaceWith):用一些其他字符替换所有匹配到的字符或正则表达式。注意,该方法需要确保目标字符串不是全局匹配模式的一部分。
    const p = 'The quick brown fox jumps over the lazy dog. If the dog is happy, then fox will be too.';
    console.log(p.replaceAll('dog', 'ferret')); // 输出 "The quick brown fox jumps over the lazy ferret. If the ferret is happy, then fox will be too."
    
  • toLowerCase()toUpperCase():分别将整个字符串转换为小写或大写。
    const original = 'Hello World!';
    console.log(original.toLowerCase()); // 输出 "hello world!"
    console.log(original.toUpperCase()); // 输出 "HELLO WORLD!"
    

填充方法

  • padStart(targetLength, padString):在当前字符串开头填充指定的字符串,直到达到目标长度。
    const str = '5';
    console.log(str.padStart(4, '0')); // 输出 "0005"
    
  • padEnd(targetLength, padString):在当前字符串结尾填充指定的字符串,直到达到目标长度。
    const str = '5';
    console.log(str.padEnd(4, '0')); // 输出 "5000"
    

TCP和UDP区别

UDPTCP
无连接面向连接
不可靠传输,不使用流量控制和拥塞控制可靠传输(数据顺序和正确性),使用流量控制和拥塞控制
支持一对一,一对多,多对一和多对多交互通信只能是一对一通信
面向报文面向字节流
首部开销小,仅8字节首部最小20字节,最大60字节
适用于实时应用,例如视频会议、直播适用于要求可靠传输的应用,例如文件传输

ref 和 reactive 区别

特性/对比项refreactive
适用类型可以将基本数据类型(如 number, string, boolean)和对象、数组等变成响应式。主要用于对象(包括数组),使其成为响应式。
访问值的方式需要通过 .value 访问或修改其值。直接访问和修改属性,无需额外操作。
解构与扩展运算符影响解构时会失去响应式,除非使用 toRefs() 转换。解构时也会失去响应式,但可以通过 toRefs() 来保持响应性。
性能对于基本类型,由于每次都需要通过 .value 来访问,可能会稍微影响性能。性能上可能更优,尤其是处理复杂对象结构时。
嵌套深度的响应性自动为嵌套的对象提供响应性。自动为整个对象结构提供响应性,包括嵌套的对象和数组。
类型支持(TypeScript)对于 TypeScript 用户,需要显式地定义类型,特别是对于复杂的数据结构。更加直观地支持复杂类型的定义,简化了类型声明。

ref设置为null 还可以获取DOM元素

当你想要获取一个 DOM 元素的引用时,可以在元素上设置 ref 属性,并在 <script setup>setup() 函数中定义相应的 ref 变量。Vue 会自动将这个变量与指定的 DOM 元素关联起来。

<template>
  <div ref="root">这是一个根 div 元素</div>
  <button @click="handleClick">点击我</button>
</template>

<script setup>
import { ref, onMounted } from 'vue';

// 创建一个 ref 来存放对 DOM 元素的引用
const root = ref(null);

// 在生命周期钩子中使用 DOM 引用
onMounted(() => {
  console.log(root.value); // 输出: <div>这是一个根 div 元素</div>
});

// 定义方法来操作 DOM
const handleClick = () => {
  if (root.value) {
    root.value.textContent = '文本内容已更改';
  }
};
</script>

vue中的 $route 和 $router 的区别

$route - 路由信息对象

$route 是一个“路由信息对象”,它包含了当前激活的路由的状态信息。每当 URL 发生变化时,一个新的 route 对象就会被创建并暴露给组件内的 $route 属性。这个对象包含了很多有用的属性来获取当前路径、参数等信息:

  • path: 当前路径字符串。
  • params: 包含动态片段和全匹配通配符的键值对。
  • query: 查询参数(即 URL 中的键值对)。
  • hash: 当前 URL 的 hash 值(带#号的部分),如果没有则为空字符串。
  • fullPath: 完整的 URL,包括查询参数和 hash。
  • matched: 一个数组,包含了当前路由的所有嵌套路径片段对应的配置参数对象(即与当前路由匹配的所有路由记录)。
  • name: 当前路由的名称,如果有的话。

$router - 路由实例对象

$router 指向的是 Vue Router 实例本身,它是用来进行编程式导航的工具。通过 $router,你可以执行跳转到不同的 URL,替换当前页面,前进或后退等操作。主要方法包括但不限于:

  • push(location) : 导航到新位置。调用此方法不会向 history 添加新纪录,而是替换当前的 history 记录。
  • replace(location) : 不会向 history 添加新记录,而是替换当前的 history 记录。
  • go(n) : 在 history 记录中向前或者后退多少步。
  • back() : 等同于 go(-1),后退一步。
  • forward() : 等同于 go(1),前进一步。

TypeScript

泛型

泛型程序设计是一种编程风格或编程范式,它允许在程序中定义形式类型参数,然后在泛型实例化时使用实际类型参数来替换形式类型参数,这一过程有些类似于函数形参在被函数调用时传入的实参替换

形参的名称为合法的变量标识符,通常约定首字母大写表示它是类型,一般名称使用简写 T,U,V,W 依次递增。

  • 灵活性和重用性:泛型允许你在定义时不指定具体的数据类型,而是将这种类型的确定推迟到创建实例的时候。
  • 类型安全:尽管泛型提供了极大的灵活性,但它依然保留了类型检查的优点,确保你的代码在编译阶段就能捕获尽可能多的错误。

下面是一个简单的例子,展示了如何定义一个带有泛型的函数,该函数可以返回传入参数本身:

function identity<T>(arg: T): T {
    return arg;
}

let output = identity<string>("myString"); // 输出 "myString"
console.log(output);

在这个例子中,T 是一个类型变量,代表任意类型。当调用 identity 函数时,我们通过 <string> 明确指定了 T 的类型为 string

使用泛型约束 extends

有时候,你可能希望对泛型进行一些限制,使其只能是实现了某些属性或方法的类型。这可以通过“泛型约束”来实现:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length); // 现在我们知道这个参数一定有 length 属性
    return arg;
}

loggingIdentity("hello world"); // 正确,因为字符串有 length 属性
// loggingIdentity(10); // 错误,数字没有 length 属性

在这个例子中,T extends Lengthwise 表示 T 必须是包含 length 属性的类型。可以说类型参数 T 的基约束为类型 Lengthwise。如果没有设置extends,则类型参数 T 的基约束为"{}"

泛型接口和类

除了函数,你还可以在接口和类中使用泛型:

  • 泛型接口
interface Pair<T> {
    first: T;
    second: T;
}
  • 泛型类
class Stack<T> {
    private elements: T[] = [];
    
    push(element: T): void {
        this.elements.push(element);
    }
    
    pop(): T | undefined {
        return this.elements.pop();
    }
}

断言

类型断言

类型断言允许你手动指定一个值的类型,通常是在 TypeScript 的类型推断可能不准确或无法推断出具体类型的情况下使用。你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。它没有运行时的影响,只是在编译阶段起作用。

  1. 使用尖括号 <Type> 语法(当不在 JSX 中时),<>尖括号语法与JSX冲突。
    let someValue: any = "this is a string";
    let strLength: number = (<string>someValue).length;
    
  2. 使用as语法
    let someValue: any = "this is a string";
    let strLength: number = (someValue as string).length;
    
非空断言

非空断言通过后缀 ! 来表示,用来告诉 TypeScript 编译器你非常确定某个值不会是 nullundefined,从而绕过编译器对这些值的检查。具体而言,x! 将从 x 值域中排除 null 和 undefined 。

function myFunc(maybeString: string | undefined | null) {
  // Type 'string | null | undefined' is not assignable to type 'string'.
  // Type 'undefined' is not assignable to type 'string'. 
  const onlyString: string = maybeString; // Error
  const ignoreUndefinedAndNull: string = maybeString!; // 使用非空断言!,排除了null和undefined
}
确认赋值断言

当你确信某个变量在首次使用之前一定会被赋值,但由于某些原因TypeScript无法自动推断出这一点时,可以使用确认赋值断言。

let x!: number;     // 变量 x 在赋值前被使用了,要解决该问题,使用!确认赋值断言
initialize();
console.log(2 * x); // Ok
function initialize() {
  x = 10;
}

确认赋值断言也常使用在class类中

class MyClass { 
    // 使用确认赋值断言 
    private name!: string; 
    constructor() { 
        // 假设这里有一些复杂的初始化逻辑, 
        // 在构造函数的某处会为 `name` 赋值。 
        setTimeout(() => { 
            this.name = "My Class"; 
        }, 1000); 
    }
}

类型守卫

类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。 类型保护帮助开发者编写更安全、更明确的代码,确保在不同分支中处理的数据类型是预期的类型。

typeof

使用 typeof 运算符可以实现简单的类型保护。这对于原始类型的检查非常有用,比如 string, number, boolean, 和 symbol

需要注意的是typeof会把null识别为object

function printId(id: number | string) {
    if (typeof id === 'string') {
        // 在这里,id 被确认为 string 类型
        console.log('Your ID is: ' + id.toUpperCase());
    } else {
        // 在这里,id 被确认为 number 类型
        console.log('Your ID is: ' + id);
    }
}

typeof 类型保护只支持两种形式:typeof v === "typename" 和 typeof v !== typename

instanceof

对于类或构造函数创建的对象,可以使用 instanceof 来判断对象是否是指定的类型。

class Animal { /* ... */ }
class Bird extends Animal { /* ... */ }
function detectAnimal(animal: Animal) {
    if (animal instanceof Bird) {
        // animal 被确认为 Bird 类型
        animal; // 类型为 Bird
    }
}
in

in 操作符可以用来检查对象中是否存在指定属性,这对于区分具有不同属性的对象非常有用,尤其是在处理联合类型时。

interface Fish {
    swim: () => void;
}

interface Bird {
    fly: () => void;
}

function move(animal: Fish | Bird) {
    if ("swim" in animal) {
        // 在这个分支中,animal 被确定为 Fish 类型
        animal.swim();
    } else {
        // 在这个分支中,animal 被确定为 Bird 类型
        animal.fly();
    }
}
自定义类型保护的类型谓词

一个自定义类型保护函数通常具有如下结构:

function isType(arg: any): arg is Type {
    // 返回一个布尔值,表明 arg 是否为 Type 类型
}

这里的 arg is Type 是类型谓词,它告诉编译器如果函数返回 true,那么参数 arg 应被视为 Type 类型。

interface Cat {
    meow(): void;
}

interface Dog {
    bark(): void;
}

// 自定义类型谓词  如果存在bark方法,animal就是Dog
function isDog(animal: Cat | Dog): animal is Dog {
    return (animal as Dog).bark !== undefined;
}

function makeSound(animal: Cat | Dog) {
    if (isDog(animal)) {
        // 在这里,TypeScript 知道 animal 是 Dog 类型
        animal.bark();
    } else {
        // 在这里,TypeScript 推断 animal 可能是 Cat 类型
        animal.meow();
    }
}

联合类型和类型别名

联合类型

联合类型允许你将多个类型组合成一个类型。这意味着一个变量可以拥有多种类型的值中的一种。使用竖线 | 来分隔每个类型。

let id: string | number;
id = 100; // 正确
id = 'abc'; // 正确
// id = true; // 错误,因为布尔类型不是指定的联合类型的一部分
可辨识联合

可辨识联合(Discriminated Unions)类型,也称为代数数据类型或标签联合类型。它包含 3 个要点:可辨识、联合类型和类型守卫。

如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。

假设我们有一个应用需要处理不同类型的几何形状(如圆形、矩形),我们可以使用可辨识联合来表示这些形状及其特有的属性。

  1. 可辨识

    定义一些具有共同可辨识属性的接口

    interface Circle {
        kind: "circle"; // 可辨识属性
        radius: number;
    }
    
    interface Square {
        kind: "square"; // 可辨识属性
        sideLength: number;
    }
    
    interface Triangle {
        kind: "triangle"; // 可辨识属性
        base: number;
        height: number;
    }
    
  2. 联合类型

    我们将这些接口组合成一个联合类型

    type Shape = Circle | Square | Triangle;
    
  3. 类型守卫

    当我们需要对 Shape 类型的变量进行操作时,可以通过检查 kind 属性来确定具体的类型,并执行相应的逻辑

    function getArea(shape: Shape) {
        switch (shape.kind) {
            case "circle":
                return Math.PI * shape.radius ** 2;
            case "square":
                return shape.sideLength ** 2;
            case "triangle":
                return 0.5 * shape.base * shape.height;
            default:
                const _exhaustiveCheck: never = shape;
                throw new Error(`Unknown shape kind: ${shape.kind}`);
        }
    }
    
类型别名

类型别名让你为现有的类型创建一个新的名字。这可以使你的代码更加易读,并且当你需要重复使用相同的类型时,它也能够减少重复。使用 type 关键字来定义类型别名。

type ID = string | number;

let userId: ID;
userId = 200; // 正确
userId = 'user123'; // 正确
// userId = false; // 错误

这里,我们定义了一个名为 ID 的类型别名,它可以是字符串或数字。然后我们可以用 ID 这个名称代替 string | number 在声明变量时使用。

shallowRef 和 shallowReactive

shallowRef 创建一个浅层的 ref 对象。只有这个 ref 对象本身是响应式的,即它的值改变时会触发视图更新;但是它内部嵌套的对象或数组不会被转换成响应式。这意味着如果你修改了嵌套对象或数组中的属性,Vue 将不会检测到这些变化,并且视图也不会自动更新。

import { shallowRef } from 'vue';
const state = shallowRef({ count: 1 });
// 修改state.value为一个新的对象时,会触发视图更新
state.value = { count: 2 }; // 视图更新
// 直接修改嵌套对象的属性,不会触发视图更新
state.value.count = 3; // 不会触发视图更新

shallowReactive 创建一个浅层的响应式对象。与 reactive 不同的是,它只将对象的第一层级转换为响应式,而嵌套的对象或数组不会被转换为响应式。这意味着对于深层级的数据结构,你需要手动处理其响应性。

import { shallowReactive } from 'vue';
const state = shallowReactive({
  nested: { count: 1 }
});
// 修改state.nested为一个新的对象时,会触发视图更新
state.nested = { count: 2 }; // 视图更新
// 直接修改嵌套对象的属性,不会触发视图更新
state.nested.count = 3; // 不会触发视图更新

使用场景

  • 性能优化:当你有一个深层数据结构,并且你知道你只会修改最外层的引用,而不需要对内部嵌套对象进行响应式追踪时,可以使用 shallowRef 或 shallowReactive 来避免不必要的性能开销。
  • 集成现有数据:当你需要将现有的数据结构整合进 Vue 的响应式系统中,但又不希望整个数据结构都被 Vue 转换为响应式时,这些浅层的工具就显得特别有用。

移动端适配

rem

在移动端适配中使用 rem 单位是一种非常有效的手段,它允许你根据屏幕宽度动态调整页面元素的大小,从而实现响应式设计。通过设置根元素(即 <html> 标签)的字体大小,并基于这个值来定义其他元素的尺寸,可以确保你的页面在不同设备上都有良好的显示效果。

动态计算rem

// 阻塞 来动态设置 html font-size
// 立即执行函数
(function(){
    // console.log('立即执行函数');
    function calc(){
        // document.documentElement 获得<html>标签
        // clientWidth 获得当前可视区宽度
        const w=document.documentElement.clientWidth;
        console.log(w);
        document.documentElement.style.fontSize
            =75*(w/750)+'px';   // 根据需求配置
    }
    calc()
    // 监听屏幕变化 (横屏竖屏变换)
    window.onresize=function(){
        // console.log('resize');
        calc();
    }
})()

百分比适配

在 CSS 中盒子的宽度可以设置为一个百分比值,表示根据父级宽度的百分比来计算宽度。因此我们可以通过百分比的方式让一个盒子在任何设备中宽度占比都是一样的。

VW,VM适配

  • VW (Viewport Width) : 1vw 等于视口宽度的 1%。这意味着如果你设置一个元素的宽度为 50vw,它将占据视口宽度的一半。
  • VH (Viewport Height) : 类似地,1vh 等于视口高度的 1%。一个高度设置为 50vh 的元素将会占据视口高度的一半。

虽然 vwvh 非常适合创建基于视口大小的全屏或部分屏幕布局,但它们并不总是完美的解决方案。例如,文本大小如果完全依赖于 vw,可能会在大屏幕上变得难以阅读。因此,结合使用 vwvh 与其他单位(如 remem)可以提供更加灵活的设计方案。

html {
    font-size: 16px;
}

@media (max-width: 600px) {
    html {
        font-size: calc(14px + 0.5vw); /* 根据视口宽度动态调整字体大小 */
    }
}

为了更精确地控制元素大小,你可以使用 CSS 的 calc() 函数结合百分比、vw/vh 以及其他单位。这使你可以创建更复杂的计算公式,以满足特定的设计需求。

.container {
    width: calc(50% + 10vw); /* 宽度是父容器的一半加上视口宽度的10% */
}

watch 和 watchEffect

  • watch 监听响应式数据的变化 执行复杂的逻辑 请求
    • 复杂业务(异步,检查库存)
    • 手动指定依赖
    • 如果要立即执行,需要指定immediate: true
  • watchEffect 立即执行一次 自动处理依赖 数据修改后的副作用
    • 自动收集依赖
    • 立即执行一次

v-model的实现原理

vue中v-model可以实现数据的双向绑定,但是为什么这个指令就可以实现数据的双向绑定呢?其实v-model是vue的一个语法糖。即利用v-model绑定数据后,既绑定了数据,又添加了一个input事件监听。

实现原理:

  • v-bind绑定响应数据
  • v-on 监听 触发input事件并传递数据
<input v-model="text"></input>

// 等价于:
<input :value="text" @input="text = $event.target.value"></input>

// 组件中使用:
<custom-input :value="text" @input="$event"></custom-input>

// 根据v-model原理使用原生JS模拟:
<input type="text" id="ipt1">
<input type="text" id="ipt2">
<script>
    var ipt1=document.getElementById('ipt1');
    var ipt2=document.getElementById('ipt2');
    ipt1.addEventListener("input",function(){
        ipt2.value=ipt1.value;
    })
</script>

npm 和 pnpm 区别

特性npmpnpm
安装机制使用嵌套的 node_modules 结构。使用扁平化的存储结构和符号链接。
磁盘空间使用可能占用更多磁盘空间。通过共享依赖包来节省磁盘空间。
安装速度在大型项目中可能较慢。通常更快,尤其是对于重复依赖。
锁定文件使用 package-lock.json 锁定版本。使用 pnpm-lock.yaml 锁定版本。
性能优化提供缓存提高性能。利用内容寻址存储进一步优化性能。

pnpm 解决 npm 的幻影依赖问题

在npm(Node Package Manager)生态系统中,“幻影依赖”指的是那些并非直接在你的package.json文件中声明,但却出现在你的node_modules目录中的包。这种情况可能会引发一些潜在的问题,包括但不限于模块冲突、构建不稳定以及安全隐患。

image.png

幻影依赖的产生原因

  1. 间接依赖:这是最常见的原因。当你安装一个npm包时,这个包可能自身又依赖于其他包。这些间接依赖会被npm自动安装到你的node_modules目录中,但不会出现在你的package.json中。
  2. 版本冲突解决:当不同包需要同一依赖的不同版本时,npm会尝试解决这种冲突。有时这会导致额外的依赖被安装,以满足所有包的需求。
  3. 遗留依赖:随着项目的演化,某些依赖可能不再需要,但由于它们存在于node_modules中而没有被移除。例如,在升级或修改依赖关系后,旧的依赖可能仍然存在。
  4. 开发环境与生产环境差异:有时候,开发者环境和生产环境之间的依赖管理不一致也会导致幻影依赖的存在。

pnpm完美解决了幻影依赖问题

pnpm 的理念是把所有的包,存到一个仓库文件夹里,然后再 node_modules 里使用正常的树形结构来表达我们的包依赖,那你说这样子不是有重复项了吗?其实并没有,因为这里它使用的是链接的方式也就是说树形结构并不占空间,而只是指向仓库里的一个个链接。 image.png