2025面试题大全(5)

169 阅读1小时+

1. escape、encodeURI、encodeURIComponent 有什么区别?

escapeencodeURIencodeURIComponent 都是 JavaScript 中用于编码 URI 的函数,但它们在编码范围和用途上有所不同:

1. escape()

  • 历史escape 是早期的 JavaScript 版本中用于编码 URI 的函数。
  • 编码范围:它编码除字母、数字和符号(@*-._)以外的所有字符。
  • 用途:由于它的编码范围有限,且不适用于编码 URI 的组件(如查询字符串),因此现在不推荐使用。
  • 注意escape 函数已经被弃用,不应在新代码中使用。

2. encodeURI()

  • 编码范围encodeURI 编码除字母、数字和以下符号以外的所有字符:!()*-._~
  • 用途:用于编码整个 URI,但不编码 URI 中的特殊字符(如冒号、斜杠、问号等),因此它适用于编码 URI 的路径部分。
  • 示例:对整个 URL 进行编码,如 http://example.com/测试?空格=值

3. encodeURIComponent()

  • 编码范围encodeURIComponent 编码除字母、数字和以下符号以外的所有字符:!()*-._~
  • 用途:用于编码 URI 的单个组件,如查询字符串中的参数。它会编码 URI 中的特殊字符,包括冒号、斜杠、问号等。
  • 示例:对 URL 的查询字符串中的参数进行编码,如 空格=值

区别总结

  • 编码范围escape 的编码范围最窄,encodeURIencodeURIComponent 的编码范围相似,但 encodeURIComponent 会编码更多的特殊字符。
  • 用途escape 已被弃用;encodeURI 用于编码整个 URI;encodeURIComponent 用于编码 URI 的单个组件。
  • 安全性encodeURIencodeURIComponent 更安全,因为它们遵循 URI 编码的规范。

示例对比

console.log(escape("测试?空格=值")); // %u6D4B%u8BD5%3F%BF%D5%3D%D6%BE
console.log(encodeURI("测试?空格=值")); // %E6%B5%8B%E8%AF%95%3F%E7%A9%BA%E6%A0%BC%3D%E5%80%BC
console.log(encodeURIComponent("测试?空格=值")); // %E6%B5%8B%E8%AF%95%3F%E7%A9%BA%E6%A0%BC%3D%E5%80%BC

在上述示例中,escape 不推荐使用,encodeURI 保留了问号,而 encodeURIComponent 对所有特殊字符进行了编码。

使用场景

  • 当你需要编码整个 URI 时,使用 encodeURI
  • 当你需要编码 URI 的某个组件(如查询参数)时,使用 encodeURIComponent
  • 不要使用 escape,因为它已被弃用且不安全。 了解这些区别有助于正确地编码 URI,避免潜在的问题,如字符编码错误或安全漏洞。

2. use strict是什么,有什么用?

"use strict"; 是 JavaScript 中的一种指令,用于指示 JavaScript 引擎以更严格的方式解析和执行代码。这种模式被称为“严格模式”(Strict Mode)。

严格模式的作用

  1. 消除了一些不规范的语法:在严格模式下,一些过去被允许但不规范的语法将不再被接受,从而减少了一些潜在的错误。
  2. 改变了某些行为的语义:严格模式改变了某些 JavaScript 语句的执行结果,以减少意外行为。
  3. 增加了一些有用的错误提示:在严格模式下,一些在非严格模式下可能默默失败的操作会抛出错误,从而更容易发现和修复问题。
  4. 禁用了某些未来可能会定义的语法:为了确保未来的 JavaScript 版本能够顺利引入新特性,严格模式禁用了一些可能在未来被定义的语法。

具体变化

  • 禁止使用未声明的变量:在严格模式下,引用未声明的变量会抛出错误,而不是创建一个全局变量。
  • 禁止删除不可删除的属性:尝试删除不可删除的属性会抛出错误。
  • 禁止函数中的 with 语句with 语句在严格模式下会被禁用,因为它可能导致代码执行结果不明确。
  • 禁止使用八进制字面量:如 010 将被视为十进制的 10,而不是八进制的 8
  • 禁止使用 evalarguments 作为变量名:这些名称在严格模式下被保留。
  • 改变了 this 的行为:在严格模式下,函数中的 this 如果不是由对象调用,则默认为 undefined,而不是全局对象。
  • 禁止使用重复的参数名称:在函数定义中,使用重复的参数名称会抛出错误。

如何使用严格模式

严格模式可以通过在脚本或函数的开头添加 "use strict"; 指令来启用。例如:

// 整个脚本都使用严格模式
"use strict";
var v = "这是严格模式";
function f() {
  // 这个函数也使用严格模式
  "use strict";
  return "这也是严格模式";
}

注意事项

  • 兼容性:严格模式是 ECMAScript 5 引入的,因此旧版本的浏览器可能不支持。
  • 逐文件启用:严格模式是逐文件启用的,不会影响其他未明确指定严格模式的文件。
  • 团队协作:在使用严格模式时,需要确保整个团队都了解并遵守这一约定。 通过使用严格模式,可以编写更清晰、更健壮的 JavaScript 代码,同时减少一些常见的错误和问题。

3. 解释性语言和编译型语言有什么区别?

解释性语言和编译型语言是两种不同的编程语言执行方式,它们在代码的翻译和执行过程中有所区别。以下是它们的主要区别:

解释性语言

  1. 翻译过程
    • 解释性语言的代码在运行时由解释器逐行翻译和执行。
    • 没有生成独立的可执行文件。
  2. 执行速度
    • 通常比编译型语言慢,因为每执行一次都需要重新翻译。
  3. 跨平台性
    • 通常具有良好的跨平台性,因为解释器负责处理平台特定的细节。
  4. 开发周期
    • 开发和调试周期可能更短,因为不需要等待编译过程。
  5. 示例语言
    • Python、Ruby、JavaScript、PHP等。

编译型语言

  1. 翻译过程
    • 编译型语言的代码在运行前通过编译器翻译成机器代码,生成可执行文件。
    • 这个过程称为编译。
  2. 执行速度
    • 通常比解释性语言快,因为代码已经预先翻译成机器代码。
  3. 跨平台性
    • 可能需要为不同平台编译不同的可执行文件。
  4. 开发周期
    • 开发和调试周期可能更长,因为需要编译过程。
  5. 示例语言
    • C、C++、Java(Java是一种混合型语言,既有编译也有解释的过程)、Go等。

混合型语言

一些语言结合了编译型和解释型的特点,例如:

  • Java:Java代码首先被编译成字节码,然后在Java虚拟机(JVM)上解释执行或通过即时编译(JIT)进一步编译成机器代码。
  • C#:C#代码被编译成中间语言(IL),然后在.NET运行时环境中执行,也可以通过即时编译优化。

总结

  • 解释性语言:逐行翻译和执行,通常更灵活,但执行速度较慢。
  • 编译型语言:预先编译成机器代码,执行速度快,但可能缺乏跨平台性。 选择使用解释性语言还是编译型语言取决于项目的具体需求,包括性能要求、开发速度、跨平台需求等。

4. 深度遍历与广度遍历有什么区别?

深度遍历(Depth-First Search,DFS)和广度遍历(Breadth-First Search,BFS)是图和树数据结构中常用的两种遍历算法,它们在遍历顺序和实现方式上有所区别:

深度遍历(DFS)

  1. 遍历顺序
    • DFS会优先深入遍历图或树的分支,直到到达叶子节点或无法进一步深入,然后回溯并探索其他分支。
  2. 实现方式
    • 通常使用栈(递归实现)或显式栈来跟踪待访问的节点。
  3. 特点
    • 可能会更早地到达深层节点。
    • 在某些情况下,如果目标节点较深,DFS可能更快找到目标。
    • 可能需要更多的内存来存储递归调用栈。
  4. 应用场景
    • 寻找路径。
    • 拓扑排序。
    • 解决迷宫问题。

广度遍历(BFS)

  1. 遍历顺序
    • BFS按层次顺序遍历图或树的节点,先访问所有邻近的节点,然后移动到下一层。
  2. 实现方式
    • 使用队列来跟踪待访问的节点。
  3. 特点
    • 按层次顺序遍历,确保先访问浅层节点。
    • 在寻找最短路径问题时,BFS通常更有效。
    • 内存消耗相对稳定,取决于队列的大小。
  4. 应用场景
    • 寻找最短路径。
    • 层次遍历。
    • 在无权图中找到两点之间的最短路径。

区别总结

  • 遍历顺序:DFS是深度优先,BFS是广度优先。
  • 数据结构:DFS通常使用栈,BFS使用队列。
  • 内存消耗:DFS的内存消耗可能因递归深度而变化,BFS的内存消耗相对稳定。
  • 应用场景:DFS适合深度搜索,BFS适合寻找最短路径和层次遍历。 选择使用DFS还是BFS取决于具体问题的需求和数据结构的特点。在实际应用中,根据问题的性质和目标来选择合适的遍历策略。

5. JS 中的数组和函数在内存中是如何存储的?

在JavaScript中,数组和函数的内存存储方式有所不同,但它们都是基于堆内存(heap)进行存储的。下面分别介绍数组和函数的存储方式:

数组的存储

  1. 基本概念
    • JavaScript中的数组是一种特殊对象,其属性是从0开始递增的整数,称为索引。
    • 数组可以存储不同类型的元素,因为它们实际上是通过对象来实现的。
  2. 内存分配
    • 当创建一个数组时,JavaScript引擎会在堆内存中分配一块连续的内存空间来存储数组元素。
    • 数组的长度是动态的,可以根据需要增长或缩小。
  3. 存储方式
    • 数组的元素存储在分配的内存空间中,每个元素根据其类型占用不同的内存大小。
    • 对于稀疏数组(即索引不连续的数组),JavaScript引擎会使用更复杂的数据结构来存储,以节省内存。

函数的存储

  1. 基本概念
    • 在JavaScript中,函数是一等公民,意味着它们可以像其他变量一样被赋值、传递和返回。
    • 函数实际上是对象,具有属性和方法,如namelengthcall等。
  2. 内存分配
    • 当定义一个函数时,JavaScript引擎会在堆内存中为该函数分配一块内存空间。
    • 函数的代码本身(即函数体)作为字符串存储在内存中。
  3. 存储方式
    • 函数对象包含指向其代码的引用、作用域链(用于访问外部变量)以及可能的其他属性。
    • 每次调用函数时,都会在栈内存中创建一个新的执行上下文(execution context),用于存储函数的局部变量、参数、返回值和执行状态。

共同点与区别

  • 共同点
    • 数组和函数都存储在堆内存中。
    • 它们都可以动态地增长或缩小(对于数组是元素数量,对于函数是作用域链和执行上下文)。
  • 区别
    • 数组主要存储数据元素,而函数存储代码和执行相关的信息。
    • 数组的内存布局通常是连续的(对于密集数组),而函数的内存布局更复杂,包括代码引用、作用域链等。
    • 函数在调用时会在栈内存中创建执行上下文,而数组不会。

注意事项

  • JavaScript的内存管理是自动的,开发者不需要手动分配或释放内存。
  • 但是,理解内存存储方式有助于优化性能和避免内存泄漏。 总之,JavaScript中的数组和函数在内存中的存储方式反映了它们各自的数据结构和用途。数组侧重于数据存储,而函数侧重于代码执行和作用域管理。

6. 手写实现一个缓存函数 memoize

memoize 是一种优化技术,用于提高函数的执行效率,特别是对于那些计算成本高且结果可重复使用的函数。通过缓存函数的输入和输出,memoize 可以避免在相同的输入上重复计算。 下面是一个简单的 memoize 函数的实现:

function memoize(func) {
  const cache = new Map();
  return function(...args) {
    // 将输入参数转换为字符串,作为缓存的键
    const key = JSON.stringify(args);
    // 检查缓存中是否已有该键对应的值
    if (cache.has(key)) {
      // 如果有,直接返回缓存的结果
      return cache.get(key);
    }
    // 如果没有,调用原函数计算结果
    const result = func(...args);
    // 将结果存入缓存
    cache.set(key, result);
    // 返回结果
    return result;
  };
}
// 示例:使用 memoize 来缓存一个计算密集型的函数
function expensiveFunction(a, b) {
  console.log('Calculating...');
  return a + b;
}
const memoizedExpensiveFunction = memoize(expensiveFunction);
console.log(memoizedExpensiveFunction(2, 3)); // Calculating... 然后 输出 5
console.log(memoizedExpensiveFunction(2, 3)); // 直接输出 5,没有计算

解释:

  1. 缓存存储
    • 使用了一个 Map 对象 cache 来存储缓存数据,其中键是输入参数的字符串表示,值是函数的返回结果。
  2. 键的生成
    • 使用 JSON.stringify(args) 将输入参数数组转换为字符串,作为缓存的键。这种方法简单但可能不适用于所有情况,特别是当输入包含循环引用或无法序列化的对象时。
  3. 缓存查找
    • 在函数执行前,首先检查缓存中是否已经有了对应的键。如果有,直接返回缓存的结果。
  4. 缓存更新
    • 如果缓存中没有对应的结果,执行原函数并将结果存入缓存。

注意事项:

  • 键的生成:上述实现中使用 JSON.stringify 来生成键,这可能不是最高效或最安全的方法。对于复杂的对象或大数组,这可能会导致性能问题。
  • 缓存失效:这个简单的实现没有缓存失效策略。在实际应用中,可能需要实现缓存大小限制或过期时间等策略。
  • 副作用:如果被缓存的函数有副作用(如修改外部状态),使用 memoize 可能会导致意外的行为。 这个基本的 memoize 实现适用于许多场景,但根据具体需求,可能需要进一步的定制和优化。

7. 普通函数动态参数 和 箭头函数的动态参数有什么区别?

在JavaScript中,普通函数和箭头函数都可以接受动态参数,即使用 rest 参数(...)来表示不确定数量的参数。它们在语法上非常相似,但在一些细节上有所区别:

普通函数的动态参数:

function ordinaryFunction(...args) {
  // args 是一个数组,包含了所有传递给函数的参数
}

箭头函数的动态参数:

const arrowFunction = (...args) => {
  // args 是一个数组,包含了所有传递给函数的参数
};

区别:

  1. 语法风格
    • 普通函数使用 function 关键字定义。
    • 箭头函数使用箭头表达式 => 定义,通常更简洁。
  2. this 的绑定
    • 普通函数的 this 值取决于调用方式,可以是全局对象、某个对象或者 undefined(在严格模式下)。
    • 箭头函数不绑定自己的 this,它捕获其所在上下文的 this 值作为自己的 this 值。
  3. 构造函数
    • 普通函数可以用作构造函数,使用 new 关键字实例化对象。
    • 箭头函数不能用作构造函数,使用 new 会抛出错误。
  4. 原型
    • 普通函数有 prototype 属性,可以添加方法到其原型上。
    • 箭头函数没有 prototype 属性。
  5. arguments 对象
    • 普通函数内部有 arguments 对象,包含了所有传递给函数的参数。
    • 箭头函数没有自己的 arguments 对象,但可以访问外围函数的 arguments
  6. 命名
    • 普通函数可以有自己的名称。
    • 箭头函数通常是匿名的,但可以赋值给一个变量。

动态参数的使用:

无论是普通函数还是箭头函数,使用 rest 参数来处理动态参数的方式是相同的:

// 普通函数
function sum(...numbers) {
  return numbers.reduce((total, number) => total + number, 0);
}
// 箭头函数
const sum = (...numbers) => numbers.reduce((total, number) => total + number, 0);

在这两个例子中,...numbers 都会收集所有传递给函数的参数 into 一个数组,然后可以使用数组的方法进行处理。 总的来说,普通函数和箭头函数在处理动态参数方面没有本质区别,选择使用哪种函数形式更多是基于其他因素,如 this 绑定、构造函数需求或个人/团队的编码风格。

8. 函数声明与函数表达式有什么区别

函数声明和函数表达式是JavaScript中定义函数的两种主要方式,它们在语法、解析时机和特性上有所区别:

函数声明(Function Declaration):

function functionName() {
  // 函数体
}

特点

  1. 解析时机:函数声明会在代码执行前被解析,即提升(Hoisting)。这意味着你可以在函数声明之前调用该函数。
    functionName(); // 可以正常调用
    function functionName() {
      // 函数体
    }
    
  2. 有名称:函数声明必须有名称。
  3. 不能被匿名:函数声明不能是匿名的。

函数表达式(Function Expression):

const functionName = function() {
  // 函数体
};

特点

  1. 解析时机:函数表达式在代码执行到该行时才会被解析。如果你在定义函数表达式之前调用它,会报错。
    functionName(); // 会报错:functionName is not a function
    const functionName = function() {
      // 函数体
    };
    
  2. 可以匿名:函数表达式可以是没有名称的匿名函数。
    const functionName = function() {
      // 函数体
    };
    
  3. 可以命名:函数表达式也可以有名称,但这个名称只在函数体内有效,外部无法访问。
    const functionName = function namedFunction() {
      // 函数体
    };
    
  4. 灵活性:函数表达式可以用于创建立即执行函数表达式(IIFE)、回调函数等。

主要区别:

  • 提升:函数声明会被提升,而函数表达式不会。
  • 命名:函数声明必须有名称,而函数表达式可以匿名或命名。
  • 使用场景:函数声明通常用于定义主要的函数,而函数表达式更灵活,适用于各种场景,如回调、立即执行等。

示例:

函数声明

sayHello(); // 可以正常调用
function sayHello() {
  console.log('Hello!');
}

函数表达式

// sayHello(); // 会报错:sayHello is not a function
const sayHello = function() {
  console.log('Hello!');
};
sayHello(); // 正常调用

在选择使用函数声明还是函数表达式时,需要根据具体的需求和场景来决定。函数声明提供了更好的提升特性,而函数表达式则提供了更大的灵活性和匿名性。

9. JS 创建对象的方式有哪些?

在JavaScript中,创建对象有多种方式,以下是几种常见的方法:

1. 对象字面量(Object Literal)

const obj = {};

2. 构造函数(Constructor)

function Person(name, age) {
  this.name = name;
  this.age = age;
}
const person1 = new Person('Alice', 30);

3. Object.create() 方法

const proto = { greet: function() { console.log('Hello!'); } };
const obj = Object.create(proto);

4. 类(Class)

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  greet() {
    console.log(`Hello, my name is ${this.name}!`);
  }
}
const person1 = new Person('Bob', 25);

5. 工厂函数(Factory Function)

function createPerson(name, age) {
  return {
    name: name,
    age: age,
    greet: function() {
      console.log(`Hello, my name is ${this.name}!`);
    }
  };
}
const person1 = createPerson('Charlie', 28);

6. new Object() 语法

const obj = new Object();
obj.name = 'David';
obj.age = 32;

7. Object.assign() 方法

const defaults = { greet: function() { console.log('Hello!'); } };
const obj = Object.assign({}, defaults, { name: 'Eve', age: 27 });

8. ... 扩展运算符

const defaults = { greet: function() { console.log('Hello!'); } };
const obj = { ...defaults, name: 'Frank', age: 35 };

9. Object.setPrototypeOf() 方法

const proto = { greet: function() { console.log('Hello!'); } };
const obj = { name: 'Grace', age: 22 };
Object.setPrototypeOf(obj, proto);

10. Reflect.construct() 方法

function Person(name, age) {
  this.name = name;
  this.age = age;
}
const person1 = Reflect.construct(Person, ['Hank', 40]);

这些方法各有特点,适用于不同的场景。例如,对象字面量适合创建简单的对象,而类和构造函数适合创建具有特定结构和行为的对象。Object.create() 方法适用于基于现有对象创建新对象,而工厂函数则提供了一种灵活的方式来创建对象。选择哪种方法取决于具体的需求和代码风格。

10. hasOwnProperty 与 instanceof 有什么区别

hasOwnPropertyinstanceof 是JavaScript中两个非常常用的方法,但它们的功能和用途截然不同:

hasOwnProperty

  • 用途:用于检查一个对象是否包含特定的自身属性(即不是继承自原型链上的属性)。
  • 语法obj.hasOwnProperty(prop)
  • 返回值:如果对象包含指定的属性,则为true;否则为false
  • 示例
    const obj = { name: 'Alice' };
    console.log(obj.hasOwnProperty('name')); // true
    console.log(obj.hasOwnProperty('toString')); // false,因为toString是继承自Object原型的
    

instanceof

  • 用途:用于检查一个对象是否是一个类的实例。
  • 语法object instanceof constructor
  • 返回值:如果对象是构造函数的实例,则为true;否则为false
  • 示例
    function Person(name) {
      this.name = name;
    }
    const alice = new Person('Alice');
    console.log(alice instanceof Person); // true
    console.log(alice instanceof Object); // true,因为所有对象都是Object的实例
    

区别

  1. 检查的内容不同
    • hasOwnProperty 检查的是对象是否具有某个特定的自身属性。
    • instanceof 检查的是对象是否是某个构造函数的实例。
  2. 使用场景不同
    • hasOwnProperty 通常用于避免在遍历对象属性时受到原型链上属性的影响。
    • instanceof 通常用于类型检查,确定一个对象是否属于某个特定的类或构造函数。
  3. 实现原理不同
    • hasOwnProperty 是直接在对象上调用,检查属性是否存在于对象自身的[[OwnPropertyNames]]中。
    • instanceof 检查的是对象的[[Prototype]]链是否包含构造函数的prototype属性。
  4. 性能考虑
    • hasOwnProperty 通常性能较高,因为它只检查对象自身的属性。
    • instanceof 可能性能较低,因为它需要遍历对象的整个原型链。

注意事项

  • 使用hasOwnProperty时,要注意如果对象本身没有hasOwnProperty方法(例如,对象是从其他对象如null继承的),则需要通过Object.prototype.hasOwnProperty.call(obj, prop)来调用。
  • instanceof在处理多窗口(iframe)或多个全局环境时可能会出现问题,因为不同的全局环境中的构造函数的prototype属性不同。 理解这两个方法的区别和用途,可以帮助你更准确地使用它们来满足不同的编程需求。

11. 原型链的终点是什么?

在JavaScript中,原型链的终点是null。具体来说,任何对象都有一个内部属性[[Prototype]](在ES5中通常通过__proto__访问,但这是一个非标准属性),这个属性指向该对象的原型。当你沿着原型链向上追溯时,最终会到达一个对象,它的[[Prototype]]属性值为null。 这意味着null是原型链的顶端,它没有原型,因此不再有[[Prototype]]属性。在JavaScript中,只有Object.prototype的对象原型是null,其他所有对象都直接或间接地继承自Object.prototype。 以下是原型链的一个简单示例:

const myObject = {};
// myObject 的原型是 Object.prototype
console.log(myObject.__proto__ === Object.prototype); // true
// Object.prototype 的原型是 null
console.log(Object.prototype.__proto__ === null); // true

在这个例子中,myObject的原型是Object.prototype,而Object.prototype的原型是null,这就是原型链的终点。 需要注意的是,__proto__属性是非标准的,并且不应该在正式代码中使用。标准的做法是使用Object.getPrototypeOf()方法来获取对象的原型:

console.log(Object.getPrototypeOf(myObject) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype) === null); // true

理解原型链的终点是null有助于深入理解JavaScript中的继承和对象模型。

12. JS里的类就是构造函数的语法糖,这个说法是否正确?

这个说法是正确的。 在JavaScript中,类(Class)确实可以被视为构造函数的语法糖。类为创建对象提供了一种更清晰和更简洁的语法,但它背后的实现原理仍然是基于原型和构造函数。 以下是几点说明:

  1. 语法糖:类只是构造函数的一种更优雅的写法,它并没有引入新的对象创建机制。当你定义一个类时,JavaScript会将其转换为一个构造函数。
  2. 构造函数:在类定义中,constructor方法是一个特殊的函数,它用于创建和初始化对象。当你使用new关键字实例化一个类时,实际上调用的是这个constructor方法。
  3. 原型链:类定义中的所有方法都会被添加到类的原型对象上。这意味着类实例可以继承这些方法,这和传统的基于原型的继承是一样的。
  4. 兼容性:由于类只是构造函数的语法糖,所以它们可以与现有的基于构造函数的代码无缝协作。 下面是一个简单的类与构造函数的对比示例: 使用构造函数
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

使用类

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    console.log(`Hello, my name is ${this.name}`);
  }
}
const alice = new Person('Alice');
alice.sayHello(); // 输出:Hello, my name is Alice

在这两个示例中,Person无论是作为构造函数还是类,其行为和结果都是相同的。类语法只是让代码看起来更清晰、更易于理解。 然而,类也引入了一些新的特性,如静态方法、私有属性和方法等,这些在传统的构造函数语法中不是那么直观。但总的来说,将类视为构造函数的语法糖是一个准确的理解。

13. 手写实现 Object.create

Object.create 是 ECMAScript 5 中引入的一个方法,它用于创建一个新对象,并将该对象的原型指向一个指定的对象。如果想要在环境不支持 Object.create 的场景下实现类似的功能,可以通过以下方式手动实现:

function createObject(proto) {
  function F() {} // 创建一个临时构造函数
  F.prototype = proto; // 将临时构造函数的原型指向传入的对象
  return new F(); // 返回临时构造函数的实例,该实例的原型是传入的对象
}
// 使用示例
var person = {
  greet: function() {
    return "Hello!";
  }
};
var personChild = createObject(person);
console.log(personChild.greet()); // 输出:Hello!

在这个实现中,我们定义了一个名为 createObject 的函数,它接受一个参数 proto,这是新对象应该继承的原型。函数内部定义了一个空的构造函数 F,然后将 F 的原型设置为 proto。最后,通过 new F() 创建了一个新对象,这个新对象的原型就是 proto。 这种实现方式利用了 JavaScript 的原型继承机制。当通过 new F() 创建对象时,新对象会继承构造函数 F 的原型,即我们传入的 proto 对象。 需要注意的是,这个简单的实现没有处理 Object.create 的第二个参数,即属性描述符。如果需要完全模拟 Object.create 的行为,包括属性描述符,实现会更加复杂。但上述代码足以模拟 Object.create 的基本功能。

14. 实现数组的flat方法,支持深度层级参数

数组的 flat 方法用于将嵌套数组扁平化,可以指定一个深度参数来决定扁平化的层级。下面是一个手动实现 flat 方法的示例,包括支持深度层级参数:

function flatArray(arr, depth) {
  // 如果没有指定深度,默认为1
  depth = depth === undefined ? 1 : depth;
  // 递归扁平化函数
  function flatten(input, depth) {
    return input.reduce((acc, val) => {
      if (Array.isArray(val) && depth > 0) {
        // 如果是数组并且深度大于0,递归调用
        return acc.concat(flatten(val, depth - 1));
      } else {
        // 否则直接添加到结果数组
        return acc.concat(val);
      }
    }, []);
  }
  // 调用递归函数并返回结果
  return flatten(arr, depth);
}
// 使用示例
const nestedArray = [1, [2, [3, [4]], 5]];
console.log(flatArray(nestedArray, 0)); // [1, [2, [3, [4]], 5]]
console.log(flatArray(nestedArray, 1)); // [1, 2, [3, [4]], 5]
console.log(flatArray(nestedArray, 2)); // [1, 2, 3, [4], 5]
console.log(flatArray(nestedArray, 3)); // [1, 2, 3, 4, 5]
console.log(flatArray(nestedArray));    // [1, 2, [3, [4]], 5] (默认深度为1)

在这个实现中,flatArray 函数接受两个参数:要扁平化的数组 arr 和扁平化的深度 depth。如果没有提供 depth 参数,默认值为 1。 函数内部定义了一个名为 flatten 的递归函数,它使用 reduce 方法来遍历数组。如果当前元素是数组并且 depth 大于 0,它会递归地调用自身,并将 depth 减 1。如果当前元素不是数组或者 depth 为 0,它将直接将元素添加到累积器数组 acc 中。 最后,flatArray 函数调用 flatten 函数并返回结果。 这个实现支持任意深度的嵌套数组,并且可以处理非数组元素。如果需要处理非常深的嵌套或者非常大的数组,可能需要考虑性能优化或者避免栈溢出的问题。

15. 斐波拉契数列是什么,用 JS 实现,用尾调优化斐波拉契数列

斐波那契数列(Fibonacci sequence)是一种著名的数列,其中每个数字是前两个数字的总和。数列的前两个数字是0和1。斐波那契数列的前几个数字是:0, 1, 1, 2, 3, 5, 8, 13, 21, 34等。 在JavaScript中,可以使用递归方式来实现斐波那契数列,但普通的递归方法在计算较大的斐波那契数时会导致大量的重复计算,效率较低。为了优化这个问题,可以使用尾调用优化(Tail Call Optimization,TCO)。 尾调用优化是一种优化递归函数的技术,通过确保递归调用是函数体中的最后一个操作,并且结果不需要进一步处理,从而允许编译器或解释器重用堆栈帧而不是创建新的堆栈帧。这样,递归就不会导致堆栈溢出,并且可以有效地计算。 以下是使用尾调用优化实现的斐波那契数列的JavaScript代码:

function fibonacci(n, a = 0, b = 1) {
  if (n === 0) return a;
  if (n === 1) return b;
  return fibonacci(n - 1, b, a + b);
}
// 使用示例
console.log(fibonacci(0));  // 输出: 0
console.log(fibonacci(1));  // 输出: 1
console.log(fibonacci(2));  // 输出: 1
console.log(fibonacci(3));  // 输出: 2
console.log(fibonacci(4));  // 输出: 3
console.log(fibonacci(5));  // 输出: 5
console.log(fibonacci(10)); // 输出: 55

在这个实现中,fibonacci 函数接受三个参数:n 是要计算的斐波那契数列的索引,ab 是两个连续的斐波那契数,默认值为0和1。函数通过递归调用自身来计算斐波那契数,每次递归时更新ab的值,并将n减1。由于递归调用是函数的最后一个操作,并且结果不需要进一步处理,这个实现是尾调用优化的。 请注意,尽管这个实现是尾调用优化的,但并不是所有的JavaScript引擎都支持尾调用优化。如果引擎不支持,即使使用了尾调用优化的写法,仍然可能遇到堆栈溢出的问题。在这种情况下,可以考虑使用迭代方法或其他优化技术来计算斐波那契数列。

16. react 中,数组用useState做状态管理的时候,使用push,pop,splice等直接更改数组对象,会引起页面渲染吗?

在React中,使用useState进行状态管理时,直接使用pushpopsplice等数组方法来修改数组对象不会触发页面重新渲染。这是因为useState以及React的不可变性原则(immutability)。

不可变性原则

React基于不可变性原则来设计,意味着状态在更新时应该创建一个新的对象或数组,而不是直接修改现有的对象或数组。这样做的好处包括:

  • 简化状态更新:使得状态更新更加可预测。
  • 优化性能:React可以通过比较新旧状态的引用来快速确定是否需要重新渲染。

直接修改数组的问题

使用pushpopsplice等直接修改数组的方法会改变原数组,但不会改变数组的引用。因此,React无法检测到这种变化,也就不会触发重新渲染。

如何正确更新数组状态

要正确更新数组状态并触发重新渲染,应该使用新的数组来替换旧数组。例如:

const [items, setItems] = useState([]);
// 添加元素
setItems([...items, newItem]);
// 删除元素
setItems(items.filter(item => item !== itemToRemove));
// 修改元素
const newItems = [...items];
newItems[index] = updatedItem;
setItems(newItems);

在这些示例中,我们使用了扩展运算符(...)来创建数组的副本,并对其进行修改,然后使用setItems来更新状态。这样,setItems会接收一个新数组,其引用与旧数组不同,从而触发React的重新渲染机制。

总结

  • 直接使用pushpopsplice等修改数组不会触发React重新渲染。
  • 应该通过创建新数组并使用setItems来更新状态,以符合React的不可变性原则并触发重新渲染。 遵循这些原则可以确保你的React应用状态管理正确且高效。

17. react 中,在什么场景下需要使用 useContext?

在React中,useContext Hook用于访问React的Context对象,它允许你跨越组件树传递数据,而不必在每个层级手动传递props。以下是一些常见的场景, where using useContext can be beneficial:

  1. 全局状态管理
    • 当你有一个全局状态(如用户认证信息、主题设置、语言选择等)需要在不同组件间共享时,可以使用Context来避免通过层层组件传递props。
  2. 避免 Prop Drilling
    • 在组件层级很深的情况下,为了避免将props从顶层组件手动传递到每一个子组件(即prop drilling),可以使用Context来简化数据传递。
  3. 主题和样式配置
    • 如果你的应用支持主题切换或样式配置,可以使用Context来存储当前主题或样式设置,并在需要的地方访问和应用这些设置。
  4. 权限控制
    • 在需要根据用户权限展示不同内容或功能的情况下,可以使用Context来存储用户权限信息,并在组件中根据这些信息进行条件渲染。
  5. 数据共享
    • 当多个组件需要共享相同的数据源时,可以使用Context来提供一个统一的数据存储和更新机制。
  6. 配置和设置
    • 对于应用级别的配置和设置,如API端点、环境变量等,可以使用Context来集中管理和访问。
  7. 路由和导航
    • 在使用React Router等路由库时,可以使用Context来存储路由状态或自定义导航逻辑。
  8. 表单管理
    • 对于复杂表单,可以使用Context来管理表单状态,如表单数据、验证状态等。
  9. 性能优化
    • 通过Context配合React.memoshouldComponentUpdate,可以减少不必要的渲染,提高性能。
  10. 跨组件通信
    • 当需要在不同组件间进行通信,而这些组件并不存在直接的父子关系时,Context提供了一种便捷的通信方式。

使用示例

首先,创建一个Context:

const ThemeContext = React.createContext('light');

然后,在顶层组件提供Context值:

<ThemeContext.Provider value="dark">
  <App />
</ThemeContext.Provider>

在需要使用Context的组件中,使用useContext Hook来访问值:

const theme = useContext(ThemeContext);

这样,任何 consumer of ThemeContext can access the theme value without passing it through each level of the component tree.

注意事项

  • 过度使用:避免在不需要的时候使用Context,因为每个Context消费都会导致组件重新渲染。
  • 性能考虑:如果Context值频繁变化,可能会导致不必要的渲染。可以使用React.memoshouldComponentUpdate来优化。
  • 结构清晰:保持Context的清晰和逻辑性,避免创建过多的Context导致管理困难。 合理使用useContext可以大大简化React应用的状态管理和数据传递,提高开发效率和应用性能。

18. package.json 里面 sideEffects 属性的作用是什么?

package.json 文件中的 sideEffects 属性是用于告知打包工具(如 Webpack、Rollup 或.Parcel等)该模块是否有副作用。副作用是指模块执行时除了导出内容之外还做了其他操作,比如修改了全局变量、有样式文件导入、或者有 console.log 输出等。 sideEffects 属性的主要作用如下:

  1. 优化打包过程
    • 当你设置 sideEffectsfalse 时,打包工具会假设该模块没有副作用,从而可以在tree shaking过程中更积极地移除未使用的代码。这可以减少打包后的文件大小,提高应用性能。
  2. 标识有副作用的模块
    • 如果你的模块确实有副作用(比如模块执行了某些初始化操作,或者导入了样式文件),你可以将 sideEffects 设置为 true 或者提供一个数组,列出有副作用的文件路径。这样打包工具在处理这些文件时会更谨慎,不会错误地移除必要的代码。
  3. 处理样式文件
    • 在某些情况下,模块可能会导出JavaScript代码同时导入样式文件。由于样式文件本身不导出任何JavaScript值,但又有副作用(影响页面样式),这时可以设置 sideEffectstrue 或者在数组中列出样式文件的路径,以确保样式文件不会被tree shaking移除。

示例

// 告知打包工具该模块没有副作用,可以安全地进行tree shaking
{
  "name": "my-package",
  "sideEffects": false
}
// 告知打包工具只有特定的文件有副作用
{
  "name": "my-package",
  "sideEffects": ["./src/sideEffectFile.js", "*.css"]
}
// 默认情况下,所有文件都被视为可能有副作用
{
  "name": "my-package"
  // "sideEffects" 属性未设置或设置为 true
}

注意事项

  • 谨慎使用:如果错误地将有副作用的模块标记为无副作用,可能会导致应用运行不正常。
  • 兼容性:确保你使用的打包工具支持 sideEffects 属性。现代的打包工具通常都支持这一特性。
  • 副作用定义:理解什么是副作用很重要,任何除了导出模块内容之外的行为都应该被视为副作用。 通过合理使用 sideEffects 属性,可以有效地优化打包输出,同时确保应用的正常运行。

19. 使用 async/await 时,是否有必要加 try catch?

使用 async/await 时,加 try/catch 是非常有必要的,尤其是在你需要处理可能出现的错误时。

为什么需要 try/catch

  1. 错误处理
    • async/await 使得异步代码看起来像同步代码,但异步操作仍然可能失败。使用 try/catch 可以捕获这些异步操作中抛出的错误。
  2. 防止未捕获的异常
    • 如果不使用 try/catch,异步函数中抛出的错误可能会成为未捕获的异常,这可能导致程序崩溃或出现其他不可预见的行为。
  3. 更好的控制流
    • try/catch 允许你在捕获错误后执行特定的代码,比如清理资源、记录错误或提供备用逻辑。
  4. 更清晰的代码结构
    • try/catch 块使得错误处理逻辑与正常执行逻辑分离,使得代码更易于阅读和维护。

示例

async function fetchData() {
  try {
    let data = await fetch('https://api.example.com/data');
    return data.json();
  } catch (error) {
    console.error('Failed to fetch data:', error);
    // 可以在这里处理错误,比如返回默认数据、抛出错误等
    throw error; // 或者可以选择重新抛出错误
  }
}

注意事项

  • 不是所有情况都需要 try/catch:如果你确定某个异步操作不会失败,或者失败的情况可以忽略,那么可以不使用 try/catch。但这种情况很少见,通常建议总是考虑错误处理。
  • 错误传播:如果你在某个异步函数中捕获了错误,但不想在那里处理它,可以重新抛出错误,让调用者来处理。
  • 全局错误处理:即使使用了 try/catch,也应该有全局的错误处理机制,以捕获那些可能遗漏的错误。

结论

在使用 async/await 时,加 try/catch 是一种良好的实践,它可以提高代码的健壮性、可维护性和可读性。当然,具体是否使用还需要根据实际情况和需求来决定。

20. 如何搭建一套灰度系统?

搭建一套灰度系统(也称为金丝雀发布、渐进式发布或蓝绿部署系统)是为了在发布新功能或更新时,能够逐步、可控地将变更推送给一部分用户,从而降低风险并收集反馈。以下是搭建灰度系统的一般步骤:

1. 需求分析

  • 确定目标:明确灰度发布的目的,比如测试新功能、评估性能或收集用户反馈。
  • 用户分组:决定如何划分用户群体,可以是按比例、按地区、按用户属性等。

2. 技术选型

  • 灰度策略:选择合适的灰度策略,如基于流量、基于用户ID、基于版本等。
  • 工具和框架:选择或开发适合的灰度工具和框架,如OpenResty、Nginx、Spring Cloud等。

3. 系统设计

  • 架构设计:设计系统架构,包括灰度决策层、数据收集层、控制台等。
  • 接口设计:设计用于控制灰度的API接口,如开启/关闭灰度、调整灰度比例等。

4. 开发实现

  • 灰度决策逻辑:实现灰度决策逻辑,根据预设规则决定是否将请求路由到灰度环境。
  • 数据收集:实现数据收集机制,用于监控灰度环境的性能和用户行为。
  • 控制台:开发一个控制台,用于配置灰度规则、查看灰度状态和收集的数据。

5. 部署实施

  • 环境准备:准备灰度环境,确保与生产环境隔离。
  • 逐步发布:逐步将变更推送给灰度用户,监控并收集反馈。

6. 监控与反馈

  • 实时监控:实时监控灰度环境的各项指标,如响应时间、错误率等。
  • 用户反馈:收集用户反馈,评估灰度效果。

7. 决策与调整

  • 数据分析:分析监控数据和用户反馈,决定是否扩大灰度范围或回滚。
  • 调整策略:根据分析结果调整灰度策略。

8. 全量发布

  • 确认无误:在灰度环境确认无误后,逐步扩大到全量发布。
  • 清理灰度:全量发布后,清理灰度环境和相关配置。

9. 文档与培训

  • 编写文档:编写灰度系统的使用文档和操作手册。
  • 培训团队:培训相关团队成员,确保他们了解如何使用灰度系统。

10. 持续优化

  • 优化策略:根据实际使用情况,不断优化灰度策略和系统。
  • 技术更新:跟踪新技术,更新灰度系统的技术栈。

工具和框架推荐

  • Nginx:可以通过配置实现简单的灰度路由。
  • OpenResty:基于Nginx的扩展,提供了更强大的灰度控制能力。
  • Spring Cloud:微服务架构下的灰度发布解决方案。
  • Kubernetes:通过服务网格(如Istio)实现灰度发布。
  • Apollo:携程开源的配置中心,可以用于灰度配置管理。 搭建灰度系统是一个复杂的过程,需要综合考虑技术、产品、运营等多个方面。根据实际情况选择合适的策略和工具,可以有效地降低发布风险,提高产品质量。

21. script 标签上有那些属性,作用分别是什么?

<script> 标签是 HTML 中用于嵌入或引用 JavaScript 代码的元素。它具有多个属性,每个属性都有其特定的作用。以下是一些常见的 script 标签属性及其作用:

  1. src
    • 作用:指定外部 JavaScript 文件的 URL。
    • 示例:<script src="path/to/script.js"></script>
  2. type
    • 作用:指定脚本的语言类型。默认值为 "text/javascript"。
    • 示例:<script type="text/javascript"></script>
  3. async
    • 作用:指示浏览器异步加载外部脚本文件,不阻塞 DOM 解析。
    • 示例:<script src="path/to/script.js" async></script>
  4. defer
    • 作用:指示浏览器延迟执行脚本,直到整个页面解析完毕后再运行。
    • 示例:<script src="path/to/script.js" defer></script>
  5. charset
    • 作用:指定外部脚本文件的字符集。较少使用,因为通常由 src 属性指定的文件决定。
    • 示例:<script src="path/to/script.js" charset="UTF-8"></script>
  6. crossorigin
    • 作用:配置跨域请求的 CORS(跨源资源共享)设置。当使用外部脚本且需要发送 CORS 请求时使用。
    • 示例:<script src="path/to/script.js" crossorigin="anonymous"></script>
  7. integrity
    • 作用:包含用于验证已加载资源完整性的哈希值。用于防止资源被篡改。
    • 示例:<script src="path/to/script.js" integrity="sha384-Base64EncodedHash"></script>
  8. nonce
    • 作用:包含一个加密的随机数,用于防止 XSS(跨站脚本)攻击。与 Content Security Policy (CSP) 配合使用。
    • 示例:<script nonce="randomValue"></script>
  9. referrerpolicy
    • 作用:指定在获取外部脚本时使用的referrer信息。用于控制referrer头的发送。
    • 示例:<script src="path/to/script.js" referrerpolicy="no-referrer"></script>
  10. nomodule
    • 作用:指示脚本不适用于 ES6 模块加载器。用于兼容旧版浏览器。
    • 示例:<script src="path/to/old-script.js" nomodule></script>
  11. language(已弃用)
    • 作用:指定脚本使用的语言。由于 type 属性的广泛使用,此属性已不推荐使用。
  12. eventfor(已弃用)
    • 作用:这些属性用于将脚本与特定的事件或元素关联。由于 HTML 4.01 中不再推荐使用这些属性,应使用更现代的方法(如内联事件处理器或 JavaScript 中的事件监听器)。 在使用这些属性时,应注意以下几点:
  • asyncdefer 属性不能同时使用。
  • integrity 属性需要与 src 属性一起使用。
  • nonce 属性需要与 CSP 配合使用,以提供额外的安全性。 了解这些属性及其作用有助于更有效地使用 <script> 标签来管理 JavaScript 代码的加载和执行。

22. 如何冻结一个 JS 对象?

在 JavaScript 中,冻结一个对象意味着阻止该对象被修改,包括添加新属性、删除现有属性以及修改属性的值。这可以通过使用 Object.freeze() 方法来实现。

使用 Object.freeze()

const obj = {
  property1: 'value1',
  property2: 'value2'
};
Object.freeze(obj);
// 尝试修改对象
obj.property1 = 'new value'; // 这将不起作用
obj.property3 = 'value3';    // 这将不起作用
console.log(obj); // { property1: 'value1', property2: 'value2' }

冻结对象的特点

  • 不可扩展:不能添加新属性。
  • 属性不可配置:不能重新配置属性(例如,不能将属性从数据属性更改为访问器属性)。
  • 属性不可写:不能修改属性的值。

检查对象是否被冻结

可以使用 Object.isFrozen() 方法来检查一个对象是否被冻结:

console.log(Object.isFrozen(obj)); // true

注意事项

  • 浅冻结Object.freeze() 只会冻结对象本身,不会冻结对象属性引用的其他对象。如果需要深度冻结,需要递归地冻结每个属性引用的对象。
  • 已有属性:冻结对象后,对象已有的可写属性仍然可以修改,直到它们被单独冻结或定义为不可写。

示例:深度冻结对象

function deepFreeze(obj) {
  Object.freeze(obj);
  Object.getOwnPropertyNames(obj).forEach(prop => {
    if (obj[prop] !== null && typeof obj[prop] === 'object' && !Object.isFrozen(obj[prop])) {
      deepFreeze(obj[prop]);
    }
  });
  return obj;
}
const obj = {
  innerObj: {
    property: 'value'
  }
};
deepFreeze(obj);
obj.innerObj.property = 'new value'; // 这将不起作用
console.log(obj.innerObj.property); // 'value'

通过使用 Object.freeze()deepFreeze() 函数,可以有效地冻结 JavaScript 对象,防止它们被意外修改。这在创建不可变数据结构或确保对象在多线程环境中保持稳定时非常有用。

23. Vue 项目中,你做过哪些性能优化?

在Vue项目中,性能优化是一个持续且重要的过程。以下是一些常见的性能优化措施,包括我在项目中可能实施过的:

1. 减少组件重新渲染

  • 使用v-once:对于不需要改变的数据,使用v-once可以避免不必要的重新渲染。
  • 使用shouldComponentUpdateVue.memo:在Vue 3中,可以使用memo来防止组件在接收到相同的props时重新渲染。
  • 利用计算属性和侦听器:合理使用计算属性和侦听器,避免在模板中进行复杂的表达式计算。

2. 代码分割和懒加载

  • 路由懒加载:使用动态导入(import())来实现路由组件的懒加载,减少初始加载时间。
  • 组件懒加载:对于非首屏组件,可以实现懒加载。
  • 异步组件:使用Vue异步组件功能,按需加载组件。

3. 优化资源加载

  • 压缩图片和文件:使用工具压缩图片和文件,减少资源大小。
  • 使用CDN:将静态资源部署到CDN,提高资源加载速度。
  • 缓存策略:设置合理的HTTP缓存策略,减少重复资源的下载。

4. 优化打包输出

  • tree shaking:利用tree shaking去除未使用的代码。
  • 代码压缩:使用UglifyJSTerser等工具压缩代码。
  • 分割Vendor代码:将第三方库代码分割到单独的文件中。

5. 优化DOM操作

  • 避免频繁的DOM操作:尽量减少DOM操作,使用Vue的虚拟DOM来更新视图。
  • 使用v-for时的key:在v-for循环中使用唯一的key,帮助Vue更快地识别节点。
  • 避免过度使用全局事件总线:减少全局事件总线的使用,避免不必要的组件通信。

6. 使用Vue性能分析工具

  • Vue Devtools:使用Vue Devtools进行性能分析,找出性能瓶颈。
  • Chrome Performance Tab:使用Chrome浏览器的Performance标签分析组件渲染和脚本执行时间。

7. 服务器端渲染(SSR)

  • Nuxt.js:使用Nuxt.js实现服务器端渲染,提高首屏加载速度。

8. WebSocket和轮询

  • WebSocket:对于实时数据更新,使用WebSocket代替轮询,减少HTTP请求。
  • 节流和防抖:对于频繁触发的事件(如滚动、窗口调整大小),使用节流和防抖技术。

9. 优化组件设计

  • 函数式组件:对于无状态、无实例的组件,使用函数式组件提高性能。
  • 拆分大组件:将大组件拆分成更小的子组件,提高组件的复用性和可维护性。

10. 使用Vue 3

  • Vue 3:如果可能,升级到Vue 3,它提供了更好的性能和更小的打包体积。 这些优化措施可以根据项目的具体情况进行选择和实施。性能优化是一个持续的过程,需要不断地监控、评估和调整。

24. 如何优化 DOM 树解析过程?

优化DOM树解析过程是提高网页加载速度和性能的关键环节。以下是一些有效的优化策略:

1. 减少HTML文件大小

  • 压缩HTML:移除不必要的空格、换行符和注释。
  • 避免内联样式和脚本:将样式和脚本放在外部文件中,利用浏览器缓存。

2. 优化文档结构

  • 减少嵌套层级:尽量减少DOM的嵌套层级,简化结构。
  • 使用语义化标签:合理使用HTML5的语义化标签,有助于浏览器更快地理解文档结构。

3. 优化CSS和JavaScript

  • 避免阻塞渲染的CSS和JavaScript:将CSS放在<head>中,JavaScript放在<body>底部。
  • 使用媒体查询分离关键CSS:将首屏需要的CSS内联,其他通过媒体查询加载。
  • 异步或延迟加载JavaScript:使用asyncdefer属性。

4. 利用浏览器缓存

  • 设置合理的缓存策略:通过HTTP缓存头控制资源缓存。
  • 使用Service Workers:缓存关键资源,即使在离线状态下也能快速加载。

5. 优化资源加载

  • 减少资源数量:合并文件,减少HTTP请求。
  • 压缩资源:压缩CSS、JavaScript和图片文件。
  • 使用CDN:分发资源,减少加载时间。

6. 使用预解析技术

  • DNS预解析:使用<link rel="dns-prefetch">预解析域名。
  • 预连接:使用<link rel="preconnect">预建立连接。
  • 预加载:使用<link rel="preload">预加载关键资源。

7. 优化脚本执行

  • 避免长时间运行的脚本:优化JavaScript代码,避免长时间阻塞主线程。
  • 使用Web Workers:将复杂计算移至后台线程。

8. 利用浏览器渲染优化

  • 避免重排和重绘:减少DOM操作,批量更新DOM。
  • 使用虚拟DOM:如Vue、React等框架,减少直接操作DOM的次数。

9. 监控和分析

  • 使用性能分析工具:如Chrome DevTools的Performance标签,分析解析和渲染时间。
  • 监控真实用户性能:使用Real User Monitoring (RUM)收集实际用户加载时间数据。

10. 服务器端优化

  • 启用GZIP/Brotli压缩:压缩传输的HTML、CSS和JavaScript文件。
  • 优化服务器响应时间:提高服务器性能,减少TTFB(Time to First Byte)。 通过实施这些策略,可以显著提高DOM树解析的效率,从而提升网页的整体性能和用户体验。需要注意的是,不同的网站和应用程序可能需要不同的优化方法,因此最好根据具体情况进行分析和调整。

25. 实现管道函数

在JavaScript中,实现管道函数(Pipeline)通常意味着创建一个函数,该函数接收一系列函数作为参数,然后依次对某个值进行执行这些这些函数;这些函数执行执行某个为10cm;5号足球:直径20cm;这些函数足球为21.5cm;6号直径为直径23cm;这些直径为直径25cm;这些直径为直径28cm;这些直径为直径30cm;这些直径为直径33cm;这些直径为直径35cm;这些直径为直径37cm;这些直径为直径40cm;这些直径为直径42cm;这些直径为直径45cm;这些直径为直径47cm;这些直径为直径50cm;这些直径为直径52cm;这些直径为直径55cm;这些直径为直径57cm;这些直径为直径60cm;这些直径为直径62cm;这些直径为直径65cm;这些直径为直径67cm;这些直径为直径70cm;这些直径为直径72cm;这些直径为直径75cm;这些直径为直径77cm;这些直径为直径80cm;这些直径为直径82cm;这些直径为直径85cm;这些直径为直径87cm;这些直径为直径90cm;这些直径为直径92cm;这些直径为直径95cm;这些直径为直径97cm;这些直径为直径100cm。 为了简化,我们假设每个函数都接受一个参数并返回一个结果,这样它们就可以被链式调用。以下是一个简单的管道函数实现:

function pipeline(value, ...funcs) {
  return funcs.reduce((currentValue, func) => func(currentValue), value);
}
// 示例使用:
function add1(x) { return x + 1; }
function multiply2(x) { return x * 2; }
function subtract3(x) { return x - 3; }
const result = pipeline(5, add1, multiply2, subtract3);
console.log(result); // 输出:7

在这个例子中,pipeline函数接受一个初始值和一系列函数。它使用reduce方法来遍历函数数组,将每个函数应用于累积值(初始值为value),并将结果传递给下一个函数。 这种管道函数的实现方式非常灵活,可以用于任何单参数函数的链式调用。如果需要处理多个参数的函数,可以使用柯里化或者修改函数来适应这种模式。 如果你想要一个更通用的管道函数,可以处理函数具有多个参数的情况,你可以这样实现:

function pipeline(value, ...funcs) {
  return funcs.reduce((currentValue, func) => {
    // 如果函数需要的参数多于1个,可以在这里进行处理
    // 例如,可以使用剩余参数来传递额外的参数
    return func(...currentValue);
  }, [value]);
}
// 示例使用:
function add(x, y) { return x + y; }
function multiply(x, y) { return x * y; }
const result = pipeline(5, add, multiply); // 这将不会按预期工作,需要传递额外的参数
console.log(result); // 输出:错误,因为add和multiply需要两个参数

在这个更通用的版本中,初始值被设置为一个数组,这样就可以使用剩余参数语法...来传递多个参数给每个函数。但是,这个例子中的addmultiply函数都需要两个参数,所以你需要确保在调用pipeline时传递了足够的参数。 请注意,这个通用版本需要你根据实际情况进行调整,以确保每个函数都能接收到正确数量的参数。

26. 为什么 SPA 应用都会提供一个 hash 路由,好处是什么?

SPA(单页应用)通常提供一个hash路由(也称为hashbang或hash-based routing)的原因和好处包括:

原因:

  1. 历史管理
    • 在早期的Web应用中,浏览器的历史记录是通过页面的加载来管理的。SPA为了在单页面上实现多视图的切换,需要一种方式来管理应用的状态和历史,而hash路由提供了一种简单的方法来实现这一点。
  2. 兼容性
    • Hash路由可以在所有现代浏览器中工作,不需要服务器端的特殊配置。它利用了URL中的hash部分(#后面的内容),这部分内容不会被发送到服务器,因此可以用来在客户端控制路由。
  3. 无刷新导航
    • Hash路由允许用户在应用中进行导航,而不会触发页面的重新加载。这样可以提供更流畅的用户体验。

好处:

  1. 用户体验
    • 无刷新导航提高了应用的响应速度,减少了加载时间,从而提升了用户体验。
  2. 前端控制
    • Hash路由使得前端开发者可以完全控制路由逻辑,而不需要依赖服务器端的配置。
  3. 状态保持
    • 在SPA中,用户的状态(如表单数据、滚动位置等)可以在不同视图之间保持,而不会因为页面刷新而丢失。
  4. 书签和分享
    • Hash路由允许用户对特定的应用状态进行书签标记或分享URL,因为hash值可以代表应用的特定状态。
  5. SEO优化
    • 虽然hash路由对SEO不太友好(因为搜索引擎可能不会索引hash后面的内容),但现代的SPA框架和库通常提供了服务器端渲染(SSR)或静态站点生成(SSG)的解决方案来改善SEO。
  6. 路由嵌套
    • Hash路由可以很容易地实现路由的嵌套,使得应用的结构更加清晰。
  7. 缓存策略
    • Hash路由可以与浏览器的缓存策略结合,实现更高效的资源加载。
  8. 跨域问题
    • 由于hash值不会被发送到服务器,因此可以避免一些跨域问题。 尽管hash路由有这些好处,但它也有一些局限性,比如对SEO的不友好和对URL长度的限制。因此,现代SPA框架通常还支持另一种路由方式,即HTML5的History API,它提供了更自然的URL结构和更好的SEO支持,但需要服务器端的配置来处理所有的路由请求。

27. 原生 js 如何进行监听路由的变化?

在原生JavaScript中,监听路由变化通常有两种方法:一种是使用window对象的hashchange事件来监听hash路由的变化;另一种是使用HTML5 History API的popstate事件来监听历史记录的变化。下面分别介绍这两种方法:

1. 使用hashchange事件监听hash路由变化

当URL的hash部分发生变化时,会触发hashchange事件。你可以为这个事件添加事件监听器来响应路由变化:

window.addEventListener('hashchange', function(event) {
  var newURL = event.newURL; // 变化后的新URL
  var oldURL = event.oldURL; // 变化前的旧URL
  var hash = window.location.hash; // 当前hash值
  // 根据hash值进行相应的路由处理
  console.log('Hash changed from ' + oldURL + ' to ' + newURL);
  // ...其他路由处理逻辑
});

2. 使用popstate事件监听历史记录变化

当浏览器的历史记录发生变化时(例如用户点击前进或后退按钮),会触发popstate事件。你可以为这个事件添加事件监听器来响应路由变化:

window.addEventListener('popstate', function(event) {
  var state = event.state; // state对象,可以是你在pushState或replaceState时提供的
  // 根据state或当前URL进行相应的路由处理
  console.log('State changed:', state);
  // ...其他路由处理逻辑
});

需要注意的是,popstate事件只在浏览器的历史记录发生变化时触发,而不会在调用pushStatereplaceState方法时触发。因此,如果你使用HTML5 History API来改变状态,你需要手动处理这些情况:

// 使用pushState或replaceState改变状态时
history.pushState({page: 1}, 'title 1', '?page=1');
// 需要手动添加逻辑来处理这种状态变化

结合使用

在实际的SPA应用中,你可能会结合使用这两种方法来处理不同的路由情况。例如,你可以在页面加载时根据当前URL初始化路由,然后监听hashchangepopstate事件来响应路由变化:

// 页面加载时初始化路由
initRouter();
function initRouter() {
  // 根据当前URL进行路由处理
  var path = window.location.pathname;
  console.log('Initial route:', path);
  // ...其他初始化逻辑
}
// 监听hashchange事件
window.addEventListener('hashchange', handleRouteChange);
// 监听popstate事件
window.addEventListener('popstate', handleRouteChange);
function handleRouteChange(event) {
  // 路由变化处理逻辑
  var path = window.location.pathname;
  console.log('Route changed to:', path);
  // ...其他路由处理逻辑
}

这样,你就可以在原生JavaScript中实现基本的路由监听和响应了。对于更复杂的路由需求,你可能需要使用或实现更完整的路由管理库或框架。

28. onpopstate 可以监听到 pushstate 的事件吗?

onpopstate事件监听器不能监听到pushState方法触发的事件onpopstate事件只会在浏览器的历史记录发生变化时触发,例如用户点击前进或后退按钮。而pushState方法是用来向历史记录中添加新条目的,它不会触发popstate事件。 如果你需要在调用pushState后执行某些操作,你可以在调用pushState的方法后面直接添加这些操作。例如:

history.pushState({page: 1}, 'title 1', '?page=1');
// 在这里执行你需要在pushState后执行的操作
console.log('State pushed');
// ...其他逻辑

如果你想要在pushState后也能有一个统一的事件处理机制,你可以封装一个函数来同时处理pushStatepopstate事件:

function updateState(state, title, url) {
  history.pushState(state, title, url);
  handleStateChange(state);
}
window.onpopstate = function(event) {
  handleStateChange(event.state);
};
function handleStateChange(state) {
  // 根据state进行相应的处理
  console.log('State changed:', state);
  // ...其他逻辑
}
// 使用封装的函数来更新状态
updateState({page: 1}, 'title 1', '?page=1');

这样,无论是通过pushState添加新状态,还是通过用户操作触发popstate事件,handleStateChange函数都会被调用,从而实现统一的状态变化处理。

29. 前端项目里,一般对请求 request 做哪些统一的封装?

在前端项目中,对请求进行统一封装是常见的做法,这样可以提高代码的可维护性、可读性和复用性。以下是一些常见的封装方式:

1. 封装请求函数

创建一个统一的请求函数,用于发送各种类型的HTTP请求(如GET、POST等)。

async function request(url, options) {
  const response = await fetch(url, {
    method: options.method || 'GET',
    headers: {
      'Content-Type': 'application/json',
      ...options.headers,
    },
    body: JSON.stringify(options.body),
    ...options,
  });
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return response.json();
}

2. 错误处理

统一处理请求错误,例如网络错误、服务器错误等。

async function requestWithErrorHandler(url, options) {
  try {
    const data = await request(url, options);
    return data;
  } catch (error) {
    console.error('Request failed:', error);
    // 可以在这里处理错误,例如显示错误消息
    throw error;
  }
}

3. 请求拦截器

在请求发送前进行拦截,可以添加通用 headers、认证信息等。

async function requestWithInterceptor(url, options) {
  const token = localStorage.getItem('authToken'); // 假设认证信息存储在localStorage
  const headers = {
    ...options.headers,
    Authorization: `Bearer ${token}`,
  };
  return request(url, { ...options, headers });
}

4. 响应拦截器

在接收到响应后进行拦截,可以处理通用响应逻辑,如刷新认证信息。

async function requestWithResponseInterceptor(url, options) {
  const data = await requestWithInterceptor(url, options);
  if (data.statusCode === 401) {
    // 处理认证失败的情况,例如跳转到登录页面
  }
  return data;
}

5. 拦截重复请求

防止短时间内重复发送相同的请求。

const ongoingRequests = new Map();
async function requestWithDuplicateInterceptor(url, options) {
  if (ongoingRequests.has(url)) {
    return ongoingRequests.get(url);
  }
  const promise = requestWithResponseInterceptor(url, options).finally(() => {
    ongoingRequests.delete(url);
  });
  ongoingRequests.set(url, promise);
  return promise;
}

6. 超时处理

为请求设置超时时间,超时后取消请求。

async function requestWithTimeout(url, options, timeout = 5000) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);
  try {
    const data = await request(url, { ...options, signal: controller.signal });
    return data;
  } finally {
    clearTimeout(id);
  }
}

7. 缓存处理

对某些请求结果进行缓存,避免重复请求。

const cache = new Map();
async function requestWithCache(url, options) {
  if (cache.has(url)) {
    return cache.get(url);
  }
  const data = await requestWithTimeout(url, options);
  cache.set(url, data);
  return data;
}

8. 统一配置

将请求的基地址、默认 headers 等配置统一管理。

const config = {
  baseUrl: 'https://api.example.com',
  defaultHeaders: {
    'Content-Type': 'application/json',
  },
};
async function requestWithConfig(url, options) {
  return request(`${config.baseUrl}${url}`, {
    ...config.defaultHeaders,
    ...options,
  });
}

组合使用

在实际项目中,通常会根据需要组合使用上述封装方式,以创建一个强大且灵活的请求库。

async function customRequest(url, options) {
  return requestWithCache(
    url,
    requestWithDuplicateInterceptor(
      url,
      requestWithTimeout(
        url,
        requestWithResponseInterceptor(
          url,
          requestWithInterceptor(url, options)
        ),
        5000
      )
    )
  );
}

通过这种方式,你可以根据项目的具体需求来定制请求封装,提高开发效率和代码质量。

30. 如何封装一个请求,让其多次调用的时候,实际只发起一个请求的时候,返回同一份结果?

要实现一个请求在多次调用时只实际发起一次,并且返回同一份结果,可以使用以下几种方法:

方法一:使用全局变量或缓存对象

const requestCache = {};
async function fetchOnce(url, options) {
  if (requestCache[url]) {
    return requestCache[url];
  }
  const promise = fetch(url, options).then(response => response.json());
  requestCache[url] = promise;
  try {
    const data = await promise;
    return data;
  } catch (error) {
    // 如果请求失败,从缓存中移除
    delete requestCache[url];
    throw error;
  }
}

方法二:使用 Map 对象

const ongoingRequests = new Map();
async function fetchOnceWithMap(url, options) {
  if (ongoingRequests.has(url)) {
    return ongoingRequests.get(url);
  }
  const promise = fetch(url, options).then(response => response.json()).finally(() => {
    ongoingRequests.delete(url);
  });
  ongoingRequests.set(url, promise);
  return promise;
}

方法三:使用闭包

function createFetchOnce() {
  const cache = {};
  return async function fetchOnce(url, options) {
    if (cache[url]) {
      return cache[url];
    }
    const promise = fetch(url, options).then(response => response.json());
    cache[url] = promise;
    try {
      const data = await promise;
      return data;
    } catch (error) {
      // 如果请求失败,从缓存中移除
      delete cache[url];
      throw error;
    }
  };
}
const fetchOnce = createFetchOnce();

方法四:使用 Promise

const fetchOncePromise = (function() {
  const cache = {};
  return function(url, options) {
    if (!cache[url]) {
      cache[url] = new Promise((resolve, reject) => {
        fetch(url, options)
          .then(response => response.json())
          .then(data => {
            resolve(data);
          })
          .catch(error => {
            delete cache[url];
            reject(error);
          });
      });
    }
    return cache[url];
  };
})();
async function fetchOnce(url, options) {
  return fetchOncePromise(url, options);
}

使用示例

async function getData() {
  const url = 'https://api.example.com/data';
  const options = {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
    },
  };
  try {
    const data = await fetchOnce(url, options);
    console.log(data);
  } catch (error) {
    console.error('Request failed:', error);
  }
}
// 多次调用 getData(),实际只发起一次请求
getData();
getData();
getData();

在上述示例中,无论你调用 getData() 多少次,实际只会向服务器发起一次请求,后续的调用会直接返回第一次请求的结果。这样可以避免不必要的网络请求,提高应用性能。 选择哪种方法取决于你的具体需求和偏好。第一种和第二种方法较为简单直观,第三种和第四种方法则提供了更好的封装和隔离。

31. 为什么 webpack 可以通过文件打包,让浏览器可以支持 CommonJS 规范?

Webpack 可以通过文件打包让浏览器支持 CommonJS 规范的原因在于它的模块打包和转换能力。以下是具体的解释:

  1. 模块化规范差异
    • CommonJS 是 Node.js 中的模块化规范,它允许模块同步导入其他模块,并使用 module.exportsrequire 来导出和导入模块。
    • 浏览器 原生支持的是 ES6 模块规范(ESM),使用 importexport 语句来进行模块的导入和导出。
  2. Webpack 的模块打包
    • Webpack 是一个模块打包器,它可以将多个模块打包成一个或多个 bundles,这些 bundles 可以在浏览器中运行。
    • 在打包过程中,Webpack 会分析模块之间的依赖关系,并构建一个依赖图。
  3. 转换 CommonJS 到浏览器可执行代码
    • Webpack 在打包时会将 CommonJS 模块转换为浏览器可以执行的代码。
    • 具体来说,Webpack 会将 require 函数替换为内部实现的模块加载函数,这个函数会负责在浏览器中动态加载和执行模块。
    • module.exports 也会被转换为模块的局部变量,以确保模块之间的作用域隔离。
  4. 模拟 Node.js 环境的部分功能
    • Webpack 可以通过插件或加载器来模拟 Node.js 环境的部分功能,如 fspath 等 Node.js 内置模块,使得在浏览器中也可以使用这些模块。
  5. 代码分割和懒加载
    • Webpack 支持代码分割和懒加载,可以将代码分割成多个 chunks,按需加载,这有助于提高应用的性能。
  6. 兼容性处理
    • Webpack 通过 Polyfill 或 Babel 等工具来处理浏览器兼容性问题,确保打包后的代码可以在不同浏览器中运行。 通过这些机制,Webpack 能够将使用 CommonJS 规范编写的代码转换为浏览器可以执行的代码,从而让浏览器支持 CommonJS 模块。这样,开发者可以自由地使用 CommonJS 模块编写代码,而不用担心浏览器兼容性问题。

32. 如何判断一个单向链表是否是循环链表?

判断一个单向链表是否是循环链表(也称为环形链表)通常使用“快慢指针”方法,也称为弗洛伊德的循环检测算法。以下是具体的步骤:

  1. 初始化两个指针
    • slow 指针:每次移动一步。
    • fast 指针:每次移动两步。
  2. 遍历链表
    • 同时移动 slowfast 指针,直到 fast 指针到达链表末尾(fastfast.nextnull)或者 slowfast 相遇。
  3. 判断是否有环
    • 如果 fast 指针到达链表末尾,则链表不是循环链表。
    • 如果 slowfast 相遇,则链表是循环链表。 下面是使用 Python 实现的代码示例:
class ListNode:
    def __init__(self, value=0, next=None):
        self.value = value
        self.next = next
def has_cycle(head):
    slow = head
    fast = head
    
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        
        if slow == fast:
            return True
    
    return False
# 示例使用
# 创建一个循环链表
node1 = ListNode(1)
node2 = ListNode(2)
node3 = ListNode(3)
node1.next = node2
node2.next = node3
node3.next = node1  # 这里创建了一个循环
# 检测链表是否有循环
print(has_cycle(node1))  # 应该输出 True

在这个例子中,has_cycle 函数会正确地检测出链表是否有循环。如果链表是循环的,那么 slowfast 指针最终会相遇;如果链表不是循环的,那么 fast 指针会先到达链表的末尾。

33. React 中的 hooks 和 memorizedState 是什么关系?

在 React 中,hooks 是一种允许你在函数组件中使用 state 和其他 React 特性的函数。而 memorizedState 是 React 在内部用来存储和管理 hooks 状态的一种数据结构。它们之间的关系可以从以下几个方面来理解:

  1. 存储状态
    • 当你使用像 useState 这样的 hooks 来创建状态时,React 会在组件的内部数据结构中为这个状态分配一个位置,这个位置就是 memorizedState
    • memorizedState 数组中的每个元素都对应一个 hook 的状态,例如,第一个 useState 的状态存储在 memorizedState[0],第二个 useState 的状态存储在 memorizedState[1],依此类推。
  2. 保持状态
    • memorizedState 的主要作用是确保组件在重新渲染时能够保持之前的状态。即使组件的函数被重新执行,React 也能通过 memorizedState 恢复之前的状态。
  3. hooks 的顺序
    • React 依赖 hooks 的调用顺序来正确地关联 memorizedState 和对应的 hook。这就是为什么 hooks 不能在条件语句或循环中调用,因为这样会破坏调用顺序,导致状态关联错误。
  4. 更新状态
    • 当你调用状态更新函数(如 setState)时,React 会更新对应的 memorizedState 中的值,并可能触发组件的重新渲染。
  5. 性能优化
    • memorizedState 还与性能优化相关,例如 useMemouseCallback 可以利用 memorizedState 来缓存计算结果和函数,以避免不必要的重新计算和渲染。 总的来说,memorizedState 是 React 实现 hooks 状态管理的底层机制,它确保了 hooks 的状态可以在组件的多次渲染之间保持一致性和稳定性。而 hooks 则是提供给开发者使用的抽象,使得在函数组件中管理状态和副作用变得更加方便和直观。

34. React 中,怎么给 children 添加额外的属性?

在 React 中,如果你想要给 children 添加额外的属性,你可以使用 React 的 cloneElement 方法或者直接在渲染时添加属性。以下是两种常见的方法:

方法一:使用 React.cloneElement

React.cloneElement 可以用来克隆并返回一个新的 React 元素,同时可以添加额外的属性。

import React from 'react';
function ParentComponent() {
  const childWithExtraProps = React.cloneElement(
    <ChildComponent />,
    { extraProp: 'value' } // 这里添加额外的属性
  );
  return <div>{childWithExtraProps}</div>;
}
function ChildComponent(props) {
  // 这里可以访问到 extraProp
  return <div>{props.extraProp}</div>;
}

如果你有多个 children,你可以遍历它们并给每个子元素添加属性:

import React from 'react';
function ParentComponent({ children }) {
  const childrenWithExtraProps = React.Children.map(children, (child) =>
    React.cloneElement(child, { extraProp: 'value' })
  );
  return <div>{childrenWithExtraProps}</div>;
}
// 使用时
<ParentComponent>
  <ChildComponent />
  <AnotherChildComponent />
</ParentComponent>

方法二:直接在渲染时添加属性

如果你直接在渲染 children 时添加属性,可以这样做:

import React from 'react';
function ParentComponent({ children }) {
  return (
    <div>
      {React.Children.map(children, (child) => (
        <child.type {...child.props} extraProp="value" />
      ))}
    </div>
  );
}
// 使用时
<ParentComponent>
  <ChildComponent />
  <AnotherChildComponent />
</ParentComponent>

在这个例子中,我们使用了 React.Children.map 来遍历 children,并且为每个子元素创建了一个新的元素,同时添加了 extraProp 属性。

注意事项

  • 性能考虑:频繁地使用 cloneElement 或者在渲染时添加属性可能会导致性能问题,因为它们会创建新的元素。如果性能成为问题,可以考虑使用 React.memo 或其他优化技术。
  • 属性覆盖:使用这些方法添加属性时,如果子组件已经定义了相同的属性,这些属性会被覆盖。
  • 类型安全:确保你添加的属性是子组件期望的,否则可能会导致类型错误。 选择哪种方法取决于你的具体需求和代码风格。在一些情况下,你可能还需要考虑组件的封装性和可重用性。

35. never 是什么类型,详细讲一下

never 类型是 TypeScript 中的一个特殊类型,它表示永远不会发生的类型。具体来说,never 类型用于以下几种情况:

  1. 函数返回类型
    • 当一个函数没有返回值时(即不会正常结束,比如抛出错误或无限循环),可以使用 never 类型作为其返回类型。
    • 例如:
      function error(message: string): never {
        throw new Error(message);
      }
      
      这个函数永远不会返回,因为它总是抛出错误。
  2. 类型断言
    • 在类型断言中,可以使用 never 来表示某些不可能的情况。
    • 例如,在一个联合类型中,如果所有可能的类型都已经检查过,剩下的情况就可以使用 never 来表示:
      type Foo = string | number | boolean;
      function controlFlowAnalysisWithNever(foo: Foo) {
        if (typeof foo === "string") {
          // do something
        } else if (typeof foo === "number") {
          // do something
        } else if (typeof foo === "boolean") {
          // do something
        } else {
          // foo 的类型此时为 never
          const check: never = foo;
        }
      }
      
      在这个例子中,如果 foo 的类型不是 stringnumberboolean,那么它的类型就是 never。这通常用于确保代码的完整性,即所有可能的类型都已经处理过。
  3. 排除类型
    • never 类型可以用于排除其他类型,从而得到一个更具体的类型。
    • 例如,使用 TypeScript 的条件类型和 never 来排除某些类型:
      type Exclude<T, U> = T extends U ? never : T;
      type Result = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
      
      在这个例子中,Result 的类型是 "c",因为 "a""b" 都被排除了。
  4. 映射类型
    • 在映射类型中,可以使用 never 来过滤掉某些属性。
    • 例如:
      type RemoveReadOnly<T> = {
        [P in keyof T as T[P] extends readonly any[] ? never : P]: T[P]
      };
      type Result = RemoveReadOnly<{ readonly a: number; b: string[] }>; // { b: string[] }
      
      在这个例子中,Result 的类型只包含 b 属性,因为 a 是只读的,被 never 过滤掉了。 总结never 类型在 TypeScript 中用于表示不可能的情况或永远不会发生的类型。它通常用于函数返回类型、类型断言、排除类型和映射类型等场景,以增强代码的健壮性和类型安全性。

36. unknown 是什么类型?

unknown 是 TypeScript 中的一种顶级类型,用于表示未知或不确定的类型。它与 any 类型有些相似,但更安全,因为它不允许在没有类型检查或类型断言的情况下进行任意操作。 以下是 unknown 类型的一些关键特点:

  1. 任何类型都可以赋值给 unknown
    • unknown 类型可以接受任何类型的值,类似于 any 类型。
    • 例如:
      let value: unknown;
      value = 42; // OK
      value = "Hello"; // OK
      value = true; // OK
      
  2. unknown 类型不能直接赋值给其他类型
    • any 不同,unknown 类型不能直接赋值给其他类型,除非进行类型检查或类型断言。
    • 例如:
      let value: unknown;
      let num: number = value; // Error: Type 'unknown' is not assignable to type 'number'.
      
  3. 需要进行类型检查或类型断言
    • 在使用 unknown 类型的值之前,通常需要进行类型检查或类型断言,以确保类型安全。
    • 例如:
      let value: unknown;
      if (typeof value === "number") {
        let num: number = value; // OK
      }
      // 或者使用类型断言
      let str: string = value as string; // OK,但可能不安全
      
  4. 不允许进行操作
    • unknown 类型不允许进行除了赋值和比较之外的操作,如调用函数、访问属性等。
    • 例如:
      let value: unknown;
      value.toString(); // Error: Object is of type 'unknown'.
      
  5. 用于函数返回类型
    • 当函数的返回类型不确定时,可以使用 unknown
    • 例如:
      function fetch_data(url: string): unknown {
        // Fetch data from URL and return it
      }
      
  6. 与联合类型一起使用
    • unknown 可以与联合类型一起使用,表示可能返回多种类型之一。
    • 例如:
      function get_value(): unknown {
        return Math.random() > 0.5 ? 42 : "Hello";
      }
      

总结unknown 类型是 TypeScript 中的一种安全类型,用于表示未知或不确定的类型。它要求在使用前进行类型检查或类型断言,从而避免了 any 类型可能带来的类型安全问题。unknown 类型适用于那些类型无法预先确定的场景,如从外部数据源获取数据的情况。

37. 联合类型是什么?

联合类型(Union Types)是 TypeScript 中的一种类型机制,允许开发者定义一个变量可以具有多种类型之一。使用联合类型,你可以让一个变量在多个指定的类型之间进行选择。联合类型使用竖线(|)来分隔每个类型,表示“可以是这个类型,也可以是那个类型”。 以下是联合类型的一些关键特点和示例:

关键特点:

  1. 多类型选择
    • 联合类型允许变量在定义时指定多种可能的类型。
  2. 类型安全性
    • TypeScript 会在编译时检查联合类型变量的使用,确保其操作符合其可能的类型之一。
  3. 灵活性
    • 联合类型提供了一种灵活的方式来处理不同类型的数据,而不需要使用 any 类型,从而保持类型安全。

示例:

  1. 基本使用
    let age: number | string;
    age = 30; // OK
    age = "三十"; // OK
    age = true; // Error: Type 'boolean' is not assignable to type 'string | number'.
    
  2. 函数返回类型
    • 函数可以返回联合类型,表示可能返回多种类型之一。
    function getAge(): number | string {
      return Math.random() > 0.5 ? 30 : "三十";
    }
    
  3. 类型保护
    • 使用类型保护(如 typeof 检查)来区分联合类型中的不同类型。
    function printAge(age: number | string) {
      if (typeof age === "number") {
        console.log(`Age is ${age} years old`); // age 被视为 number 类型
      } else {
        console.log(`Age is ${age}`); // age 被视为 string 类型
      }
    }
    
  4. 数组元素类型
    • 数组可以包含联合类型,表示数组元素可以是多种类型之一。
    let values: (number | string)[];
    values = [1, "two", 3, "four"]; // OK
    values = [1, true, 3, "four"]; // Error: Type 'boolean' is not assignable to type 'string | number'.
    
  5. 与类型别名一起使用
    • 可以使用类型别名来简化联合类型的定义。
    type Age = number | string;
    let age: Age;
    age = 30; // OK
    age = "三十"; // OK
    

总结:

联合类型是 TypeScript 中一种强大的类型机制,它提供了一种安全且灵活的方式来处理可能具有多种类型的变量。通过使用联合类型,开发者可以在保持类型安全的同时,处理更复杂的数据类型场景。联合类型常用于函数参数、返回值、变量声明等地方,并且通常与类型保护一起使用,以区分和处理不同的类型情况。

38. extends 条件类型怎么定义?

在 TypeScript 中,extends 关键字用于条件类型(Conditional Types),它允许你根据一个类型是否扩展了另一个类型来决定最终的类型。条件类型是 TypeScript 2.8 及以上版本引入的,它提供了一种强大的方式来创建基于类型关系的类型。 条件类型的语法类似于三元运算符,形式如下:

T extends U ? X : Y

这里的意思是,如果类型 T 可以赋值给类型 U(即 TU 的子类型或与 U 相同),那么条件类型的结果就是 X,否则是 Y

示例:

  1. 基本使用
type isString<T> = T extends string ? true : false;
type A = isString<string>; // true
type B = isString<number>; // false

在这个例子中,isString 类型根据传入的类型 T 是否为 string 来返回 truefalse。 2. 泛型约束

type Extract<T, U> = T extends U ? T : never;
type OnlyStrings = Extract<'a' | 'b' | 1 | 2, string>;
// OnlyStrings 类型为 'a' | 'b',因为只有这两个是字符串类型

Extract 类型会从联合类型 T 中提取出可以赋值给 U 的类型。 3. 复杂条件

type Flatten<T> = T extends Array<infer U> ? U : T;
type FlattenResult = Flatten<number[]>; // number
type FlattenResult2 = Flatten<string>; // string

在这个例子中,Flatten 类型会检查 T 是否是一个数组类型,如果是,就使用 infer 关键字来提取数组元素的类型,否则直接返回 T。 4. 与映射类型结合

type Optional<T> = {
  [P in keyof T]?: T[P] extends Function ? T[P] : T[P] | undefined;
};
interface Example {
  a: number;
  b: () => void;
}
type OptionalExample = Optional<Example>;
// OptionalExample 类型为 { a?: number | undefined; b?: () => void; }

在这个例子中,Optional 类型会遍历 T 的所有属性,并将非函数类型的属性变为可选并允许为 undefined

总结:

extends 条件类型是 TypeScript 中一种非常强大的类型操作工具,它可以用来创建基于类型关系的复杂类型。通过使用 extends,你可以编写出更加灵活和可重用的类型定义。条件类型常用于类型守卫、类型提取、类型映射等场景,是高级 TypeScript 编程中的重要组成部分。

39. infer 关键字是什么?

infer 关键字是 TypeScript 中用于条件类型(Conditional Types)的一个特殊语法,它允许你在条件类型中推断类型。infer 用于在条件类型的 extends 子句中声明一个类型变量,这个类型变量可以在条件类型的真(true)分支中被引用。

基本用法:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

在这个例子中,ReturnType 类型用于获取函数类型 T 的返回类型。infer R 声明了一个类型变量 R,它表示 T 的返回类型。如果 T 是一个函数类型,那么 ReturnType<T> 就会返回 R,即函数的返回类型;否则,返回 any

示例:

  1. 获取函数返回类型
type Func = () => number;
type Return = ReturnType<Func>; // number
  1. 提取数组元素类型
type UnpackArray<T> = T extends Array<infer U> ? U : T;
type Element = UnpackArray<number[]>; // number

在这个例子中,UnpackArray 类型用于提取数组类型 T 的元素类型。如果 T 是一个数组类型,那么 UnpackArray<T> 就会返回数组元素的类型;否则,直接返回 T。 3. 提取Promise类型

type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
type Resolved = UnpackPromise<Promise<string>>; // string

在这个例子中,UnpackPromise 类型用于提取 Promise 类型 T 的解析值类型。如果 T 是一个 Promise 类型,那么 UnpackPromise<T> 就会返回 Promise 的解析值类型;否则,直接返回 T

注意事项:

  • infer 关键字只能在条件类型的 extends 子句中使用。
  • infer 声明的类型变量只能在条件类型的真分支中被引用。
  • infer 可以用于推断多种类型,如函数参数类型、返回类型、数组元素类型、泛型类型参数等。

总结:

infer 关键字为 TypeScript 的类型系统提供了强大的类型推断能力,使得开发者可以更灵活地操作和变换类型。它是高级 TypeScript 编程中的重要工具,常用于编写通用类型工具和类型守卫。通过 infer,你可以编写出更加抽象和可重用的类型定义。

40. in 运算符作用是什么?

in 运算符在 JavaScript 中用于检查一个属性是否存在于一个对象中。它返回一个布尔值,指示指定的属性是否为对象的自有属性(即不是继承的属性)。

语法:

prop in object
  • prop:一个字符串或符号(Symbol),表示要检查的属性名。
  • object:要检查的对象。

返回值:

  • 如果 object 中存在名为 prop 的自有属性,返回 true
  • 否则,返回 false

示例:

const obj = { a: 1, b: 2, c: 3 };
console.log('a' in obj); // true,因为 obj 有一个名为 'a' 的自有属性
console.log('b' in obj); // true,因为 obj 有一个名为 'b' 的自有属性
console.log('d' in obj); // false,因为 obj 没有名为 'd' 的自有属性
const arr = [1, 2, 3];
console.log(0 in arr); // true,因为数组索引 0 对应的元素存在
console.log(3 in arr); // false,因为数组没有索引为 3 的元素

注意事项:

  • in 运算符会检查对象的自有属性,包括不可枚举属性。
  • 对于数组,in 运算符可以用来检查数组索引是否存在。
  • in 运算符不会检查对象的原型链上的属性。

hasOwnProperty 的区别:

  • in 运算符会检查对象的自有属性和继承的属性。
  • hasOwnProperty 方法只会检查对象的自有属性,不会检查继承的属性。
const obj = { a: 1 };
console.log('a' in obj); // true
console.log(obj.hasOwnProperty('a')); // true
console.log('toString' in obj); // true,因为 toString 是继承的属性
console.log(obj.hasOwnProperty('toString')); // false,因为 toString 不是自有属性

总结:

in 运算符是 JavaScript 中检查对象属性存在性的常用工具,它简单易用,但在使用时需要注意它也会检查继承的属性。在需要严格检查自有属性时,可以使用 hasOwnProperty 方法。

41. link 标签有哪些属性,分别有什么作用?

<link> 标签用于定义文档与外部资源之间的关系,最常见的用途是链接样式表。它包含在文档的 <head> 部分中。以下是 <link> 标签的一些常见属性及其作用:

  1. href
    • 作用:指定链接资源的位置。
    • 示例:<link href="styles.css" rel="stylesheet">
  2. rel(relation):
    • 作用:定义当前文档与链接资源之间的关系。
    • 常见值:
      • stylesheet:链接一个CSS样式表。
      • alternate:链接一个替代版本(如打印版或翻译版)。
      • icon:链接一个图标(如favicon)。
      • preload:预加载资源。
      • dns-prefetch:预解析DNS。
      • preconnect:预连接到服务器。
      • prefetch:预获取资源。
      • prerender:预渲染页面。
    • 示例:<link rel="stylesheet" href="styles.css">
  3. type
    • 作用:指定链接资源的MIME类型。
    • 示例:<link rel="stylesheet" type="text/css" href="styles.css">
  4. sizes(仅用于rel="icon"):
    • 作用:指定图标的大小。
    • 示例:<link rel="icon" href="favicon.ico" sizes="16x16">
  5. media
    • 作用:指定样式表应用于哪种媒体类型。
    • 示例:<link rel="stylesheet" href="print.css" media="print">
  6. hreflang
    • 作用:指定链接资源的语言。
    • 示例:<link rel="alternate" href="doc.en.html" hreflang="en">
  7. title
    • 作用:为链接资源指定标题,尤其用于样式表时,可以定义首选样式表。
    • 示例:<link rel="stylesheet" href="styles.css" title="Default Styles">
  8. crossorigin(跨域):
    • 作用:配置跨域请求的CORS(跨源资源共享)设置。
    • 常见值:
      • anonymous:跨域请求时不发送凭证信息(如cookies或认证信息)。
      • use-credentials:跨域请求时发送凭证信息。
    • 示例:<link rel="stylesheet" href="https://example.com/styles.css" crossorigin="anonymous">
  9. as(仅用于rel="preload"):
    • 作用:指定预加载资源的类型。
    • 示例:<link rel="preload" href="image.png" as="image">
  10. referrerpolicy
    • 作用:指定在获取资源时如何发送referrer信息。
    • 常见值:
      • no-referrer:不发送referrer信息。
      • origin:只发送源信息(不包含路径)。
      • unsafe-url:发送完整的URL(可能不安全)。
    • 示例:<link rel="stylesheet" href="styles.css" referrerpolicy="no-referrer">
  11. integrity
    • 作用:包含用于验证已加载资源完整性的哈希值。
    • 示例:<link rel="stylesheet" href="styles.css" integrity="sha384-..."> 这些属性可以根据需要组合使用,以实现不同的功能,如链接样式表、预加载资源、设置图标等。在实际应用中,根据具体需求和场景选择合适的属性和值。

42. 衡量页面性能的指标有哪些?

衡量页面性能的指标有很多,它们可以帮助开发者了解网页的加载速度、响应性、稳定性以及用户体验。以下是一些常见的页面性能指标:

  1. 加载性能指标
    • 首次内容绘制(FCP):测量从页面开始加载到主要内容首次在屏幕上呈现的时间。
    • 首次有效绘制(FMP):测量从页面开始加载到主要内容被渲染的时间。
    • 速度指标(Speed Index):衡量页面内容填充速度的指标,数值越低表示页面加载越快。
    • 最大内容绘制(LCP):测量视口中最大元素呈现的时间,是衡量页面主要内容加载速度的重要指标。
    • 可交互时间(TTI):测量页面变得完全可交互的时间点。
    • 加载时间(Load Time):从请求页面到页面完全加载完成的时间。
  2. 渲染性能指标
    • 布局偏移(CLS):衡量页面布局稳定性的指标,检测元素位置是否发生意外移动。
    • 帧率(FPS):衡量页面动画或交互的流畅度,通常希望达到60fps。
    • 重绘和重排次数:重绘和重排会影响页面性能,减少这些操作可以提升渲染效率。
  3. 响应性能指标
    • 首次输入延迟(FID):测量从用户首次与页面交互(如点击按钮)到浏览器响应的时间。
    • 点击响应时间:从用户点击到应用响应的时间。
  4. 资源加载指标
    • 请求次数:页面加载过程中发出的HTTP请求总数。
    • 资源大小:页面加载的所有资源总大小。
    • 缓存利用:衡量页面是否有效利用浏览器缓存。
  5. 服务器性能指标
    • 服务器响应时间(TTFB):从请求发出到收到服务器第一个字节的时间。
    • 错误率:请求失败的比率,如404或500错误。
  6. 用户体验指标
    • 页面可见性API:衡量页面是否对用户可见。
    • 用户满意度调查:通过用户反馈来评估页面性能。
  7. 移动性能指标
    • 优化移动体验:确保页面在移动设备上也有良好的性能表现。
  8. SEO相关指标
    • 页面速度分数:如Google PageSpeed Insights提供的分数,影响搜索引擎排名。
  9. 网络性能指标
    • 网络延迟:数据从用户设备到服务器再返回的耗时。
    • 带宽:数据传输速率。
  10. 综合性能指标
    • 性能评分:如Lighthouse提供的综合性能评分,考虑了多个性能指标。 这些指标可以通过各种工具来测量,如Google Chrome的DevTools、Lighthouse、WebPageTest、GTmetrix等。开发者可以根据这些指标来优化页面性能,提升用户体验。

43. 怎么统计页面的性能指标?

统计页面性能指标通常涉及使用各种工具和技术的组合。以下是一些常见的方法和工具,用于统计和分析页面性能指标:

1. 浏览器开发者工具

  • Chrome DevTools:Chrome浏览器内置的开发者工具,提供了性能标签页,可以录制页面加载和运行时的性能,分析帧率、CPU使用情况、网络活动等。
  • Firefox Developer Tools:Firefox浏览器的开发者工具,同样提供了性能分析功能。

2. 性能分析工具

  • Lighthouse:Google开发的一个开源的自动化工具,用于改进网络应用的质量。它可以运行在各种网页上,提供性能、可访问性、渐进式Web应用、SEO等方面的审计。
  • WebPageTest:一个在线工具,可以提供详细的性能测试报告,包括加载时间、瀑布图、速度指数等。
  • GTmetrix:另一个在线性能测试工具,结合了Google PageSpeed Insights和WebPageTest的结果,提供详细的优化建议。

3. 实时监控和日志分析

  • New RelicDatadogSentry等:这些服务可以实时监控应用的性能,提供详细的性能指标和错误追踪。
  • 服务器日志分析:通过分析服务器日志,可以获取请求时间、响应时间、错误率等指标。

4. JavaScript库和API

  • Performance API:现代浏览器提供的API,可以获取到Navigation Timing、Resource Timing、User Timing等详细的性能数据。
  • RAIL模型:一种性能评估模型,包括Response、Animation、Idle、Load四个方面,用于指导性能优化。
  • Web Vitals:Google提出的一组核心性能指标,包括LCP、FID、CLS等,可以通过JavaScript库进行测量。

5. 网络分析

  • 网络瀑布图:通过浏览器开发者工具或WebPageTest等工具获取,显示每个资源的请求和响应时间。
  • HTTP/HTTPS请求分析:分析请求的数量、大小、缓存策略等。

6. 用户行为分析

  • Real User Monitoring (RUM):通过在用户设备上收集性能数据,了解真实用户环境下的性能表现。
  • Google Analytics:可以设置自定义指标,追踪页面加载时间等性能数据。

7. 自动化测试和持续集成

  • 自动化测试脚本:使用Selenium等工具编写自动化测试脚本,定期运行以监控性能变化。
  • 持续集成/持续部署 (CI/CD):在CI/CD流程中集成性能测试,确保每次部署都不会影响性能。

8. 移动应用性能测试

  • Android Studio ProfilerXcode Instruments:用于测试移动应用的性能指标。

统计步骤:

  1. 确定指标:首先确定要统计的性能指标,如加载时间、TTI、FPS等。
  2. 选择工具:根据需要选择合适的工具或组合多种工具。
  3. 收集数据:使用选定的工具收集性能数据。
  4. 分析数据:分析收集到的数据,识别性能瓶颈。
  5. 优化和迭代:根据分析结果进行性能优化,并持续监控优化效果。 通过这些方法和工具,可以全面地统计和分析页面性能指标,从而做出有针对性的优化措施。

44. link 标签的 rel 属性中,preload 和 prefetch 这两个值的作用是什么?

<link> 标签的 rel 属性用于定义当前文档与被链接资源之间的关系。preloadprefetchrel 属性的两个值,它们都用于资源预加载,但它们的用途和加载时机有所不同:

preload

preload 是一种声明式的资源预加载方式,它告诉浏览器当前页面必定会用到某个资源,所以浏览器会提前加载这个资源。这样做的目的是为了提高页面加载性能,减少延迟。

  • 作用:确保资源在页面加载时就已经可用,避免在解析到需要该资源时才去加载,从而减少延迟。
  • 加载时机:在页面加载时立即开始加载指定的资源。
  • 使用场景:当你确定某个资源会在当前页面中使用时,可以使用 preload
<link rel="preload" href="style.css" as="style">
<link rel="preload" href="script.js" as="script">
<link rel="preload" href="image.png" as="image">

在上面的例子中,浏览器会提前加载 CSS 文件、JavaScript 文件和图片。

prefetch

prefetch 是一种预取资源的方式,它告诉浏览器当前页面可能在未来会用到某个资源,所以浏览器可以在空闲时提前加载这个资源。

  • 作用:提高未来可能访问的页面的加载速度,因为相关资源已经被预取。
  • 加载时机:在浏览器空闲时加载,且不会影响当前页面的加载。
  • 使用场景:当你预测用户可能会访问某个页面或使用某个资源时,可以使用 prefetch
<link rel="prefetch" href="next-page.css">
<link rel="prefetch" href="next-page.js">

在上面的例子中,浏览器会在空闲时预取下一个页面可能需要的 CSS 和 JavaScript 文件。

区别总结

  • 确定性preload 是确定会使用的资源,而 prefetch 是可能会使用的资源。
  • 加载优先级preload 的加载优先级较高,会立即开始加载;prefetch 的加载优先级较低,会在浏览器空闲时进行。
  • 使用时机preload 用于当前页面,prefetch 用于未来可能访问的页面。 合理使用 preloadprefetch 可以有效提高页面加载性能和用户体验,但也要注意不要过度使用,以免造成资源浪费和带宽占用。

45. HTML 部分标签中的 crossorigin 属性,作用是什么?

crossorigin(跨源)属性用于控制跨源请求(即请求的资源与当前页面不是同一个源)时的 CORS(Cross-Origin Resource Sharing,跨源资源共享)行为。这个属性可以用于一些 HTML 标签,如 <img><video><audio><link>(当 rel 属性为 stylesheet 时)、<script><iframe>crossorigin 属性的主要作用如下:

  1. 允许跨源请求:当使用 crossorigin 属性时,浏览器会向服务器发送一个 CORS 请求,而不是简单的 HTTP 请求。这允许跨源资源被正确地加载和执行。
  2. 控制请求的凭据crossorigin 属性可以指定是否应包含用户凭据(如 cookies 或 HTTP 认证信息)。 crossorigin 属性有两个常见的值:
  • anonymous:表示请求时不会发送用户凭据。如果服务器响应中不包含 Access-Control-Allow-Origin 头或其值不包含当前源,则资源不会被加载。
  • use-credentials:表示请求时会发送用户凭据。服务器必须在响应中包含 Access-Control-Allow-Origin 头,并且其值必须是当前源,同时还要包含 Access-Control-Allow-Credentials 头,且其值为 true,否则资源不会被加载。 如果 crossorigin 属性被省略,那么默认行为是不发送 CORS 请求,也就是说,资源必须与页面同源,或者服务器必须返回适当的 CORS 头。

示例

<!-- 简单的跨源请求,不发送凭据 -->
<img src="https://example.com/image.png" crossorigin="anonymous">
<!-- 跨源请求,发送凭据 -->
<script src="https://example.com/script.js" crossorigin="use-credentials"></script>
<!-- 跨源加载 CSS -->
<link rel="stylesheet" href="https://example.com/style.css" crossorigin="anonymous">

注意事项

  • 使用 crossorigin 属性时,服务器必须配置正确的 CORS 头,否则资源可能无法被加载。
  • 对于 <script> 标签,如果脚本执行了 XMLHttpRequestFetch API,并且请求的是跨源资源,那么 crossorigin 属性是必需的,否则这些 API 将会因为同源策略而失败。
  • 对于 <img><video><audio> 标签,使用 crossorigin 属性可以允许跨源资源被正确地处理,例如在画布(<canvas>)上使用这些资源时。 合理使用 crossorigin 属性可以帮助开发者遵守同源策略,同时允许安全的跨源资源访问。

46. for...of、for...in、for 循环, 三者有什么区别?

for...offor...in 和普通的 for 循环是 JavaScript 中三种不同的循环方式,它们各自有不同的用途和特性:

1. for 循环

普通的 for 循环是最基本的循环结构,通常用于遍历数组或执行固定次数的迭代。

for (let i = 0; i < array.length; i++) {
  console.log(array[i]);
}
  • 用途:遍历数组或执行固定次数的迭代。
  • 索引:需要手动管理索引(例如 i)。
  • 性能:在遍历数组时,性能通常是最高的,因为直接通过索引访问元素。
  • 兼容性:所有 JavaScript 环境都支持。

2. for...in 循环

for...in 循环用于遍历对象的可枚举属性(包括原型链上的属性)。

for (let key in object) {
  console.log(key, object[key]);
}
  • 用途:遍历对象的所有可枚举属性。
  • :遍历的是对象的键(属性名),而不是值。
  • 性能:在遍历数组时,性能可能不如普通的 for 循环,因为需要检查对象的所有属性。
  • 兼容性:所有现代浏览器都支持。
  • 注意:不推荐用于遍历数组,因为会遍历数组对象的所有可枚举属性,包括原型链上的属性,这可能导致意外的结果。

3. for...of 循环

for...of 循环用于遍历可迭代对象(如数组、字符串、Map、Set 等)的元素值。

for (let value of iterable) {
  console.log(value);
}
  • 用途:遍历可迭代对象的所有元素。
  • :直接遍历的是元素的值,而不是索引或键。
  • 性能:在遍历数组和其他可迭代对象时,性能通常很好。
  • 兼容性:ECMAScript 2015(ES6)及更高版本支持。
  • 注意:不能用于遍历普通对象,因为普通对象不是可迭代对象。

总结

  • 使用普通的 for 循环 when 需要遍历数组或执行固定次数的迭代,且关心性能。
  • 使用 for...in 循环 when 需要遍历对象的所有可枚举属性,但避免用于数组。
  • 使用 for...of 循环 when 需要遍历可迭代对象(如数组、字符串、Map、Set 等)的元素值。 选择哪种循环取决于你的具体需求和对性能的考虑。在现代 JavaScript 开发中,for...of 循环因其简洁性和易用性而越来越受欢迎。

47. 实现一个可以用 for...of 遍历的对象

要实现一个可以用 for...of 遍历的对象,你需要确保该对象是可迭代的。在 JavaScript 中,这通常意味着对象需要实现一个名为 [Symbol.iterator] 的方法,该方法返回一个迭代器。 以下是一个简单的例子,展示如何创建一个可迭代的对象:

// 定义一个对象
const myIterable = {
  items: [1, 2, 3, 4, 5],
  [Symbol.iterator]: function() {
    let index = 0;
    return {
      next: () => {
        if (index < this.items.length) {
          return { value: this.items[index++], done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
};
// 使用 for...of 遍历对象
for (const item of myIterable) {
  console.log(item);
}

在这个例子中:

  • myIterable 是一个对象,它有一个 items 数组属性,存储了要遍历的元素。
  • [Symbol.iterator] 是一个特殊的方法,它返回一个迭代器对象。
  • 迭代器对象有一个 next 方法,每次调用 next 方法时,它返回一个包含 valuedone 属性的对象。value 是当前遍历的元素,done 是一个布尔值,表示是否还有更多元素可以遍历。 当你使用 for...of 循环遍历 myIterable 时,循环会自动调用 [Symbol.iterator] 方法来获取迭代器,并反复调用迭代器的 next 方法来获取每个元素,直到 done 属性为 true。 这种方法可以用于任何对象,使其变得可迭代,不仅限于数组。你可以根据需要自定义迭代逻辑,例如遍历对象属性、生成序列等。

48. webpack tree-shaking 在什么情况下会失效?

Webpack 的 tree-shaking 功能旨在移除 JavaScript 代码中的未使用代码(dead code),以减小打包后的文件大小。然而,在某些情况下,tree-shaking 可能会失效。以下是一些可能导致 tree-shaking 失效的情况:

  1. 代码副作用
    • 如果模块具有副作用(side effects),Webpack 可能无法安全地移除这些代码。你可以在 package.json 中设置 "sideEffects": false 来告诉 Webpack 你的代码没有副作用,除非明确指出某些文件有副作用。
  2. 动态导入
    • 使用动态导入(如 import())的代码无法在静态分析阶段被 tree-shaking 掉,因为 Webpack 无法确定哪些代码会在运行时被使用。
  3. babel-polyfill 或类似垫片
    • 一些垫片库(如 babel-polyfill)会修改全局对象,这被视为副作用,因此 Webpack 不会移除这些代码。
  4. 使用 CommonJS 模块
    • Webpack 的 tree-shaking 主要针对 ES6 模块(importexport)。如果你使用 CommonJS 模块(requiremodule.exports),tree-shaking 可能不那么有效,因为 CommonJS 的动态特性使得静态分析更加困难。
  5. 未使用 mode 配置
    • 在 Webpack 配置中未设置 modeproductiondevelopmentnone),或者设置为 development,可能会导致 tree-shaking 不那么积极。
  6. 错误的模块导出
    • 如果模块的导出方式不正确,例如导出的是一个对象,而不是具体的函数或变量,可能会导致 tree-shaking 失效。
  7. 使用第三方库
    • 一些第三方库可能没有被正确地标记为无副作用,或者它们的模块系统与 Webpack 的 tree-shaking 不兼容。
  8. 代码中的复杂引用
    • 如果代码中的引用关系非常复杂,Webpack 可能无法准确分析哪些代码是未使用的。
  9. 未开启优化
    • 在 Webpack 配置中,如果没有开启相关优化选项,如 optimization.usedExportsoptimization.sideEffects,tree-shaking 可能不会生效。
  10. 源代码压缩工具配置
    • 如果使用的源代码压缩工具(如 UglifyJS、Terser)没有正确配置,或者不支持 tree-shaking,那么最终的打包文件可能仍然包含未使用的代码。 要确保 tree-shaking 有效,可以采取以下措施:
  • 使用 ES6 模块语法。
  • package.json 中正确设置 sideEffects
  • 使用 Webpack 的 production 模式。
  • 确保所有依赖库都支持 tree-shaking。
  • 配置源代码压缩工具以支持 tree-shaking。
  • 避免在模块中创建不必要的副作用。 通过注意这些情况并采取相应的措施,可以最大限度地利用 Webpack 的 tree-shaking 功能来优化打包后的代码。

49. 引用类型有哪些,有什么特点

在JavaScript中,引用类型(Reference Types)是对象的一种,它们包括函数、数组、对象、日期、正则表达式等。引用类型的特点是它们可以存储复杂的数据结构,并且可以包含多个值和函数。以下是JavaScript中常见的引用类型及其特点:

  1. 对象(Object)
    • 对象是JavaScript中的基本数据结构,用于存储键值对。
    • 可以通过对象字面量 {} 创建,或者使用 new Object()
    • 对象的属性可以是任何数据类型,包括其他对象、数组、函数等。
  2. 数组(Array)
    • 数组是用于存储有序数据集合的对象。
    • 可以通过数组字面量 [] 创建,或者使用 new Array()
    • 数组的元素可以是任何数据类型,并且数组的大小是动态的。
  3. 函数(Function)
    • 函数是可执行的代码块,可以接受参数并返回结果。
    • 可以通过函数声明 function()、函数表达式 var func = function() 或使用 new Function() 创建。
    • 函数是第一类公民,意味着可以赋值给变量、作为参数传递、作为返回值等。
  4. 日期(Date)
    • 日期对象用于表示日期和时间。
    • 可以通过 new Date() 创建。
    • 日期对象提供了一系列方法来获取和设置日期和时间的各个部分。
  5. 正则表达式(RegExp)
    • 正则表达式用于匹配字符串中的模式。
    • 可以通过正则表达式字面量 /pattern/ 创建,或者使用 new RegExp('pattern')
    • 正则表达式提供了一系列方法来进行模式匹配和字符串操作。
  6. Map
    • Map对象保存键值对,并且能够记住键的原始插入顺序。
    • 可以通过 new Map() 创建。
    • Map的键可以是任何类型,而对象的键只能是字符串或符号。
  7. Set
    • Set对象是值的集合,每个值必须是唯一的。
    • 可以通过 new Set() 创建。
    • Set提供了方法来添加、删除和遍历元素。
  8. WeakMap 和 WeakSet
    • WeakMap 和 WeakSet 是Map和Set的弱引用版本,它们不会阻止垃圾回收器回收它们的键或值。
    • WeakMap的键必须是对象,而WeakSet的值必须是对象。
    • 这些类型不支持迭代,并且没有暴露出所有键或值的方法。 引用类型的特点包括:
  • 存储复杂结构:可以存储多个值,包括其他引用类型,形成复杂的数据结构。
  • 通过引用访问:引用类型的变量存储的是对对象的引用,而不是对象本身的数据。
  • 动态属性:可以动态地添加、修改和删除属性。
  • 共享和传递:可以通过引用共享和传递对象,而不是复制整个对象。
  • 原型链:引用类型通过原型链继承属性和方法。 理解引用类型的特点和用法对于有效地使用JavaScript进行开发至关重要。

50. 箭头函数解决了什么问题

箭头函数(Arrow Functions)是ES6(ECMAScript 2015)引入的一种新的函数表达式形式,它提供了一种更简洁、更直观的函数定义方式。箭头函数解决了或改善了以下几个方面的问题:

  1. 函数定义的简洁性
    • 传统函数表达式,特别是匿名函数,在语法上较为冗长。箭头函数通过省略function关键字和简化参数与函数体的语法,使得函数定义更加简洁。
  2. this的绑定问题
    • 在传统函数中,this的值取决于函数是如何被调用的(例如,作为对象的方法、单独调用、使用callapply等)。这常常导致this的值不是预期的,特别是在回调函数中。
    • 箭头函数不绑定自己的this,它会捕获其所在上下文的this值作为自己的this值。这使得在回调函数和闭包中更容易预测和控制this的值。
  3. 不绑定arguments对象
    • 传统函数内部有一个arguments对象,包含了所有传递给函数的参数。但在箭头函数中,没有arguments对象,这使得箭头函数不能直接访问函数的参数列表。这可以被视为一种改进,因为它鼓励使用剩余参数(...rest)来明确地处理函数参数。
  4. 不能用作构造函数
    • 箭头函数不能使用new关键字来创建实例,因为它们没有[[Construct]]方法。这避免了无意中将其用作构造函数的情况,从而减少了错误。
  5. 语法一致性
    • 箭头函数提供了一种更一致的函数定义语法,特别是在处理高阶函数和函数组合时,代码的可读性和可维护性得到了提升。
  6. 隐式返回
    • 对于单表达式函数,箭头函数允许省略大括号和return关键字,从而实现隐式返回。这使得编写简单的函数变得更加快速和直观。
  7. 更好的函数式编程支持
    • 箭头函数的简洁性和this的处理方式使得它们在函数式编程风格中非常有用,特别是在使用数组方法(如mapfilterreduce等)时。 总之,箭头函数通过提供更简洁的语法、改进this的绑定行为、去除arguments对象、禁止作为构造函数使用等特性,解决了传统函数表达式中的一些常见问题,并提高了代码的可读性和可维护性。然而,它们并不是传统函数的完全替代品,而是在特定场景下提供了一种更好的选择。