# 前端 JavaScript 面试题 by 爽爽学编程

本文作者:爽爽学编程 (opens new window)

本站地址:https://code-wss.com (opens new window)

# JavaScript 有哪些数据类型?它们的区别是什么?

JavaScript 是一种弱类型语言,这意味着变量的类型在声明时不必指定,类型会在代码执行过程中自动确定。JavaScript 支持以下几种基本数据类型:

  1. Undefined:当变量被声明了但未被赋值时,它就是 undefined 类型。例如,let x; 此时 x 的值就是 undefined
  2. Null:表示故意赋予变量的空值,用来表示一个变量没有指向任何对象。例如,let y = null;
  3. Boolean:逻辑类型,只有两个值:truefalse。用于逻辑判断和条件控制。
  4. Number:表示整数或浮点数。JavaScript 中的 Number 类型是基于 IEEE 754 标准的双精度 64 位二进制格式。
  5. BigInt:为了能够表示大于 2^53 - 1 的整数,JavaScript 引入了 BigInt 类型。使用 BigInt 需要在数值后面添加 n 后缀。
  6. String:表示文本数据,可以使用双引号 "..." 或单引号 '...' 来创建字符串。
  7. Symbol:ES6 引入的新类型,用于创建唯一的不可变的代号。通常用于对象属性名的创建,确保不会与其他属性名冲突。
  8. Object:包括普通对象、数组、函数等,是复合类型。对象可以包含多种类型的属性,包括其他对象。

它们之间的区别主要体现在用途和行为上:

  • Undefined 和 Nullundefined 表示变量声明了但没有被赋值,而 null 表示一个变量被赋予了“空”的值。在类型转换时,undefined 会被转换为 NaN,而 null 会被转换为 0
  • Boolean:只有两个值,用于逻辑判断。
  • Number:可以表示整数和浮点数,支持多种数学运算。
  • BigInt:用于表示大于 2^53 - 1 的整数,解决 Number 类型在处理大整数时的精度问题。
  • String:用于表示文本数据,支持多种字符串操作方法。
  • Symbol:用于创建唯一的属性名,防止属性名冲突。
  • Object:可以包含多种类型的属性,是 JavaScript 中最复杂的数据类型,包括数组、函数等都是对象。

# 如何判断 JavaScript 变量是数组?

在JavaScript中,有几种方法可以判断一个变量是否是数组类型:

  1. 使用 Array.isArray() 方法: 这是最简单也是最推荐的方法。Array.isArray() 方法会检查传递给它的参数是否是一个数组。如果是数组,返回 true;如果不是,返回 false

    let arr = [1, 2, 3];
    console.log(Array.isArray(arr)); // 输出:true
    
    let notArr = {length: 3};
    console.log(Array.isArray(notArr)); // 输出:false
    
    1
    2
    3
    4
    5
  2. 使用 instanceof 操作符instanceof 操作符可以用来检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

    let arr = [1, 2, 3];
    console.log(arr instanceof Array); // 输出:true
    
    let notArr = "Hello, world!";
    console.log(notArr instanceof Array); // 输出:false
    
    1
    2
    3
    4
    5
  3. 检查 Object.prototype.toString.call() 的返回值: 这个方法可以检查任何对象的内部 [[Class]] 属性,并且对于数组来说,它会返回字符串 "[object Array]"

    let arr = [1, 2, 3];
    console.log(Object.prototype.toString.call(arr) === '[object Array]'); // 输出:true
    
    let notArr = {length: 3};
    console.log(Object.prototype.toString.call(notArr) === '[object Array]'); // 输出:false
    
    1
    2
    3
    4
    5
  4. 检查 constructor 属性: 每个数组实例都有一个 constructor 属性,指向创建它的构造函数。对于数组来说,这个属性指向 Array 函数。

    let arr = [1, 2, 3];
    console.log(arr.constructor === Array); // 输出:true
    
    let notArr = "Hello, world!";
    console.log(notArr.constructor === Array); // 输出:false
    
    1
    2
    3
    4
    5

    注意:constructor 属性可能会被修改或覆盖,因此这种方法不是最可靠的。

  5. 检查 length 属性: 数组有一个 length 属性,表示数组中元素的数量。但是,这个方法并不可靠,因为其他对象也可能有 length 属性。

    let arr = [1, 2, 3];
    console.log(typeof arr.length === 'number' && arr.length >= 0); // 输出:true
    
    let notArr = {length: 3};
    console.log(typeof notArr.length === 'number' && notArr.length >= 0); // 输出:true
    
    1
    2
    3
    4
    5

    这种方法可能会误判,因为任何具有数字 length 属性的对象都会被判断为数组。

# JavaScript 中 null 和 undefined 的区别是什么?

在JavaScript中,nullundefined 是两种不同的数据类型,它们各自有不同的用途和含义:

  1. Undefined
    • 表示一个变量声明了但没有被赋值。
    • 当你尝试读取一个未声明的变量时,会得到 undefined
    • 函数没有返回值时,默认返回 undefined
    • undefined 是一个原始值,而不是一个对象。
  2. Null
    • 表示一个变量被赋予了“空”的值,即没有指向任何对象。
    • 它是一个表示“空”的对象,即 nullObject 的一个实例。
    • 通常用于表示空值或不存在的值。
    • null 用于表示一个变量应该指向一个对象,但现在没有指向任何对象。

区别

  • 类型

    • undefined 是一个原始值。
    • null 是一个对象(typeof null 返回 "object",这是一个历史遗留问题)。
  • 用途

    • undefined 用于表示变量被声明了但没有被赋值。
    • null 用于表示变量应该指向一个对象,但现在没有指向任何对象。
  • 转换

    • undefined 被转换为数字时,它变为 NaN
    • null 被转换为数字时,它变为 0
  • 检查

    • 检查一个变量是否未定义或未声明,可以使用typeof操作符:

      let x;
      if (typeof x === 'undefined') {
        console.log('x is undefined');
      }
      
      1
      2
      3
      4
    • 检查一个变量是否为null,可以直接使用===操作符:

      let y = null;
      if (y === null) {
        console.log('y is null');
      }
      
      1
      2
      3
      4

示例

let a; // a 是 undefined
let b = null; // b 是 null

console.log(typeof a); // 输出:"undefined"
console.log(typeof b); // 输出:"object"

console.log(a === undefined); // 输出:true
console.log(b === null); // 输出:true

console.log(a === null); // 输出:false
console.log(a == null); // 输出:true (使用 == 时,undefined 和 null 被视为相等)

// 转换为数字
console.log(Number(a)); // 输出:NaN
console.log(Number(b)); // 输出:0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# typeof null 的结果是什么?为什么?

typeof null 的结果是 "object"

这个结果是一个历史遗留问题,源于JavaScript的早期实现。在JavaScript的原型中,null 被设计为一个特殊的值,用来表示“没有对象”。在早期的JavaScript引擎中,typeof 操作符对于一个非空的对象会返回 "object"。然而,null 虽然表示“没有对象”,但它在技术上被分类为一个“空的对象引用”,因此 typeof null 错误地返回了 "object"

这个设计决策在后来的JavaScript版本中被保留下来,以保持向后兼容性。尽管这个行为在逻辑上可能看起来有些不一致,但它已经成为JavaScript语言的一个标准特性。

因此,当你在JavaScript中使用 typeof 操作符检查 null 时,你会得到 "object" 作为结果。为了避免混淆,当你需要区分 null 和其他对象时,最好直接使用 === 操作符来与 null 进行比较:

if (value === null) {
  // 处理 value 为 null 的情况
}
1
2
3

这样的比较方式可以确保你准确地判断一个变量是否为 null

# typeof 和 instanceof 有什么区别?

typeofinstanceof 是JavaScript中用于类型检查的两个不同的操作符,它们各自有不同的用途和行为:

  1. typeof

    • typeof 是一个一元操作符,用于返回一个值的数据类型。

    • 它可以返回以下值:"undefined""object""boolean""number""bigint""string""symbol""function"

    • typeof 用于基本数据类型的检查,但不能用来区分对象的具体类型或实例。

    • 对于 nulltypeof 返回 "object",这是一个历史遗留问题。

    • 示例:

      typeof undefined; // "undefined"
      typeof 123;       // "number"
      typeof 'abc';     // "string"
      typeof true;      // "boolean"
      typeof {a: 1};    // "object"
      typeof null;      // "object" (注意:这是一个特例)
      typeof [1, 2, 3];  // "object"
      typeof function() {}; // "function"
      
      1
      2
      3
      4
      5
      6
      7
      8
  2. instanceof

    • instanceof 是一个二元操作符,用于检查一个对象是否是某个构造函数的实例。

    • 它用来检测对象的原型链上是否存在某个构造函数的 prototype 属性。

    • instanceof 可以用来区分对象的具体类型或实例。

    • 示例:

      const obj = {};
      const arr = [1, 2, 3];
      const date = new Date();
      
      obj instanceof Object; // true
      arr instanceof Array;   // true
      date instanceof Date;   // true
      arr instanceof Object; // true (因为数组是对象的一种)
      
      1
      2
      3
      4
      5
      6
      7
      8

区别

  • 用途
    • typeof 用于检查基本数据类型和函数。
    • instanceof 用于检查对象是否是某个构造函数的实例。
  • 返回值
    • typeof 返回一个字符串,表示数据类型。
    • instanceof 返回一个布尔值,表示是否是某个构造函数的实例。
  • 行为
    • typeof 对于 null 返回 "object",这是一个特例。
    • instanceof 检查的是对象的原型链,因此可以用来区分继承关系。
  • 使用场景
    • 当你需要检查一个变量是否是基本数据类型时,使用 typeof
    • 当你需要检查一个对象是否是某个特定的类或构造函数的实例时,使用 instanceof

示例

let a = 123;
let b = "hello";
let c = {name: "Kimi"};
let d = [1, 2, 3];

console.log(typeof a); // "number"
console.log(typeof b); // "string"
console.log(typeof c); // "object"
console.log(typeof d); // "object"

console.log(c instanceof Object); // true
console.log(d instanceof Array);   // true
console.log(c instanceof Array);   // false
1
2
3
4
5
6
7
8
9
10
11
12
13

# 为什么 JavaScript 中 0.1 + 0.2 !== 0.3,如何让其相等?

在JavaScript中,0.1 + 0.2 不等于 0.3 的原因在于浮点数的表示方式。JavaScript 使用 IEEE 754 标准来表示浮点数,这种表示方式是基于二进制的,而某些小数在二进制表示中是无限循环的,因此不能精确表示。

例如,0.10.2 在二进制中是无限循环小数,它们不能被精确地表示为二进制浮点数。当你执行 0.1 + 0.2 时,实际上你得到的是一个近似值,而不是精确的 0.3。这个近似值与 0.3 非常接近,但在二进制浮点数表示中略有不同,因此当你使用严格等于 (===) 比较时,它们不相等。

如何使其相等?

  1. 使用 Number 构造函数转换: 使用 Number 构造函数可以将字符串形式的数字转换为数字,这样可以避免直接的浮点数运算误差。

    console.log(Number(0.1 + 0.2) === 0.3); // true
    
    1
  2. 使用 parseFloat 函数parseFloat 函数可以将字符串转换为浮点数,同样可以避免直接的浮点数运算误差。

    console.log(parseFloat((0.1 + 0.2).toFixed(1)) === 0.3); // true
    
    1
  3. 使用 toFixed 方法toFixed 方法可以将数字转换为指定小数位数的字符串,然后你可以将字符串转换回数字。

    console.log(parseFloat((0.1 + 0.2).toFixed(1)) === 0.3); // true
    
    1
  4. 使用 Math.round 或其他数学函数: 你可以使用 Math.round 或其他数学函数来四舍五入结果。

    console.log(Math.round(0.1 + 0.2) === 0.3); // true
    
    1
  5. 使用 BigInt: 如果你需要处理非常大的整数,可以使用 BigInt,但它不适用于浮点数。

  6. 避免直接比较浮点数: 在实际开发中,通常建议避免直接比较浮点数,而是设置一个误差范围来判断两个浮点数是否“相等”。

    function areEqual(a, b, epsilon = 0.0001) {
        return Math.abs(a - b) < epsilon;
    }
    
    console.log(areEqual(0.1 + 0.2, 0.3)); // true
    
    1
    2
    3
    4
    5

示例代码

// 直接比较
console.log(0.1 + 0.2 === 0.3); // false

// 使用 Number 构造函数
console.log(Number(0.1 + 0.2) === 0.3); // true

// 使用 parseFloat 和 toFixed
console.log(parseFloat((0.1 + 0.2).toFixed(1)) === 0.3); // true

// 使用 Math.round
console.log(Math.round(0.1 + 0.2) === 0.3); // true

// 使用 epsilon 检查
function areEqual(a, b, epsilon = 0.0001) {
  return Math.abs(a - b) < epsilon;
}

console.log(areEqual(0.1 + 0.2, 0.3)); // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 如何获取安全的 undefined 值?

在JavaScript中,undefined 是一个特殊的值,表示变量已声明但未初始化。然而,由于各种原因,例如全局变量污染或误操作,undefined 可能被覆盖或改变。为了确保你获取到的是一个安全的 undefined 值,你可以采取以下措施:

  1. 使用严格模式(Strict Mode): 在严格模式下,未声明的变量赋值将抛出一个错误,这有助于防止全局变量污染。

    'use strict';
    
    let x; // 未声明的变量赋值将抛出错误
    x = 5; // ReferenceError: Assignment to undefined variable
    
    1
    2
    3
    4
  2. 使用 void 操作符void 操作符可以返回 undefined,无论其后的表达式返回什么值。

    let safeUndefined = void 0;
    console.log(safeUndefined); // 输出:undefined
    
    1
    2
  3. 使用 typeof 操作符typeof 操作符对于未声明的变量返回 "undefined"

    let safeUndefined = typeof somethingThatDoesNotExist;
    console.log(safeUndefined); // 输出:"undefined"
    
    1
    2
  4. 使用 try...catch 语句: 通过尝试访问一个未声明的变量,并在 catch 块中捕获错误,你可以安全地获取 undefined

    let safeUndefined;
    try {
      safeUndefined = someUndefinedVariable;
    } catch (e) {
      safeUndefined = undefined;
    }
    console.log(safeUndefined); // 输出:undefined
    
    1
    2
    3
    4
    5
    6
    7
  5. 使用 Object.getOwnPropertyDescriptor: 如果你想要检查一个对象上是否存在某个属性,并且该属性的值为 undefined,你可以使用 Object.getOwnPropertyDescriptor 方法。

    let obj = { a: 1 };
    let descriptor = Object.getOwnPropertyDescriptor(obj, 'b');
    let safeUndefined = descriptor ? descriptor.value : undefined;
    console.log(safeUndefined); // 输出:undefined
    
    1
    2
    3
    4
  6. 使用 Reflect.get: 类似于 Object.getOwnPropertyDescriptorReflect.get 可以用来安全地获取对象属性的值。

    let obj = { a: 1 };
    let safeUndefined = Reflect.get(obj, 'b');
    console.log(safeUndefined); // 输出:undefined
    
    1
    2
    3
  7. 避免全局变量: 避免使用全局变量,尽量使用模块化或闭包来封装变量,这样可以减少 undefined 被覆盖的风险。

  8. 使用 const 声明变量: 使用 const 声明变量可以确保变量不会被重新赋值,从而避免 undefined 被覆盖。

    const x = undefined;
    
    1

# typeof NaN 的结果是什么?

typeof NaN 的结果是 "number"

尽管 NaN 代表“不是一个数字”(Not-a-Number),它实际上是 JavaScript 中 Number 类型的一个特殊值,用于表示某些未定义或不可表示的数值操作的结果。例如,当你尝试将一个字符串转换为数字但该字符串不是有效的数字时,或者当你执行某些数学运算导致无法得到有效数值时,就会得到 NaN

以下是一些产生 NaN 的例子:

typeof NaN; // "number"
typeof 0 / 0; // "number",因为 0 / 0 产生 NaN
typeof "abc" - 1; // "number",因为字符串 "abc" 转换为数字时是 NaN,然后执行减法运算
typeof Math.sqrt(-1); // "number",因为对负数开平方根会产生 NaN
1
2
3
4

在进行类型检查时,这一点很重要,因为 typeof NaN 不会返回 "undefined""object",而是 "number"。如果你想检查一个值是否是 NaN,不能使用 typeof,而应该使用 isNaN 函数或者更现代的 Number.isNaN 方法:

isNaN(NaN); // true
Number.isNaN(NaN); // true

isNaN("Hello"); // true,因为字符串转换为数字时是 NaN
Number.isNaN("Hello"); // false,只有当值严格是 NaN 时,Number.isNaN 才返回 true
1
2
3
4
5

Number.isNaN 方法比 isNaN 函数更严格,因为它只检查值是否严格等于 NaN,而 isNaN 函数会先将它的参数转换为数字,然后再检查是否为 NaN,这可能导致一些意想不到的结果。

# isNaN 和 Number.isNaN 函数有什么区别?

isNaNNumber.isNaN 都是用来检查一个值是否是 NaN(Not-a-Number),但它们在处理参数和返回结果时有一些关键的区别:

  1. 全局 isNaN 函数

    • isNaN 是全局对象(在浏览器中是 window 对象)的一个方法。
    • 当你传入一个参数给 isNaN,它会先将这个参数转换为数字,然后再检查是否是 NaN
    • 这意味着如果传入的参数不是数字,isNaN 可能会返回 true,即使这个值实际上并不是 NaN
    isNaN(NaN); // true
    isNaN("NaN"); // true,因为 "NaN" 被转换为数字时是 NaN
    isNaN("Hello"); // true,因为 "Hello" 被转换为数字时是 NaN
    isNaN(0); // false
    
    1
    2
    3
    4
  2. Number.isNaN 方法

    • Number.isNaNNumber 对象的一个静态方法。
    • 它不会将参数转换为数字,而是直接检查参数是否严格等于 NaN
    • 这意味着只有当传入的参数严格是 NaN 时,Number.isNaN 才会返回 true
    Number.isNaN(NaN); // true
    Number.isNaN("NaN"); // false,因为 "NaN" 不是 NaN
    Number.isNaN("Hello"); // false,因为 "Hello" 不是 NaN
    Number.isNaN(0); // false
    
    1
    2
    3
    4

区别总结

  • 参数转换
    • isNaN 会将参数转换为数字,然后检查是否是 NaN
    • Number.isNaN 不会转换参数,只检查参数是否严格等于 NaN
  • 返回结果
    • isNaN 可能对非数字值返回 true,因为这些值在转换为数字时可能产生 NaN
    • Number.isNaN 只对严格等于 NaN 的值返回 true
  • 使用场景
    • 如果你想要检查一个值是否是 NaN,并且这个值可能不是数字,使用 Number.isNaN 更安全。
    • 如果你确定传入的值已经是一个数字,或者你希望 isNaN 能够处理非数字值(例如字符串),那么使用 isNaN 也是可以的。

示例代码

// 使用 isNaN
console.log(isNaN(NaN)); // true
console.log(isNaN("NaN")); // true
console.log(isNaN("Hello")); // true
console.log(isNaN(0)); // false

// 使用 Number.isNaN
console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN("NaN")); // false
console.log(Number.isNaN("Hello")); // false
console.log(Number.isNaN(0)); // false
1
2
3
4
5
6
7
8
9
10
11

# == 操作符的强制类型转换规则是什么?

在JavaScript中,== 操作符用于比较两个值是否相等,但它在比较过程中会进行类型转换(也称为强制类型转换或隐式类型转换)。以下是 == 操作符的强制类型转换规则:

  1. 字符串与数字的比较

    • 如果一个操作数是字符串,另一个是数字,则字符串将被转换成数字。
    • 字符串转换为数字时,会尝试从字符串的开头解析数字,直到遇到非数字字符。
    "123" == 123; // true,因为字符串 "123" 被转换为数字 123
    "123abc" == 123; // false,因为字符串 "123abc" 转换为数字时是 NaN
    
    1
    2
  2. 布尔值与数字或字符串的比较

    • 布尔值 true 被转换为数字 1,布尔值 false 被转换为数字 0
    true == 1; // true
    false == 0; // true
    true == "1"; // true
    false == "0"; // true
    
    1
    2
    3
    4
  3. 对象与原始值的比较

    • 如果一个操作数是对象,另一个是原始值,则对象将被转换为原始值。
    • 对象的转换规则是调用对象的 valueOf() 方法,如果返回的对象还是不能与原始值比较,则调用 toString() 方法。
    [1, 2] == 2; // true,因为数组被转换为字符串 "1,2",然后与数字 2 比较
    {toString: () => "2"} == 2; // true,因为对象的 toString 方法返回字符串 "2",然后与数字 2 比较
    
    1
    2
  4. nullundefined 的比较

    • nullundefined 被认为是相等的。
    null == undefined; // true
    
    1
  5. NaN 的比较

    • NaN 与任何值(包括它自己)都不相等。
    NaN == NaN; // false
    
    1
  6. null 与数字或字符串的比较

    • null 与数字 0 或字符串 "0" 被认为是相等的。
    null == 0; // true
    null == "0"; // true
    
    1
    2
  7. undefined 与数字或字符串的比较

    • undefined 与数字 NaN 或字符串 "undefined" 被认为是相等的。
    undefined == NaN; // true
    undefined == "undefined"; // true
    
    1
    2

示例代码

console.log("123" == 123); // true
console.log(true == 1); // true
console.log([1, 2] == 2); // true
console.log(null == undefined); // true
console.log(NaN == NaN); // false
console.log(null == 0); // true
console.log(undefined == NaN); // true
1
2
3
4
5
6
7

# JavaScript 其他值到字符串的转换规则是什么?

在JavaScript中,将其他类型的值转换为字符串类型遵循一系列的规则。这些转换可以通过显式调用方法(如 .toString())或在需要字符串的上下文中隐式进行(例如在字符串拼接、JSON.stringifyconsole.log 等)。以下是不同类型到字符串的转换规则:

  1. Undefined

    • undefined 转换为字符串是 "undefined"
    String(undefined); // "undefined"
    undefined.toString(); // "undefined"
    
    1
    2
  2. Null

    • null 转换为字符串是 "null"
    String(null); // "null"
    null.toString(); // "null"
    
    1
    2
  3. Boolean

    • true 转换为字符串是 "true"
    • false 转换为字符串是 "false"
    String(true); // "true"
    String(false); // "false"
    true.toString(); // "true"
    false.toString(); // "false"
    
    1
    2
    3
    4
  4. Number

    • 数字转换为字符串时,会将其转换为有效的字符串表示形式。
    String(123); // "123"
    123.toString(); // "123"
    
    1
    2
  5. BigInt

    • BigInt 转换为字符串时,会将其转换为字符串表示,包括 n 后缀。
    String(123n); // "123"
    123n.toString(); // "123"
    
    1
    2
  6. Symbol

    • 符号(Symbol)不能直接转换为字符串,尝试这样做会抛出错误。
    String(Symbol.iterator); // TypeError: Cannot convert a Symbol value to a string
    Symbol.iterator.toString(); // TypeError: Cannot convert a Symbol value to a string
    
    1
    2
  7. Object

    • 对象转换为字符串时,会调用对象的 toString() 方法,如果 toString() 方法不在原型链上,则会调用 Object.prototype.toString(),后者会返回 [object Object]
    const obj = {toString: () => "custom string"};
    String(obj); // "custom string"
    obj.toString(); // "custom string"
    
    1
    2
    3
  8. Array

    • 数组转换为字符串时,会调用数组的 join() 方法,将数组元素连接成一个由逗号分隔的字符串。
    String([1, 2, 3]); // "1,2,3"
    [1, 2, 3].toString(); // "1,2,3"
    
    1
    2
  9. Function

    • 函数转换为字符串时,会得到函数的源代码字符串。
    String(function() {}); // "function() {}"
    function() {}.toString(); // "function() {}"
    
    1
    2
  10. Date

    • 日期对象转换为字符串时,会调用日期对象的 toString() 方法,返回一个代表该日期的易读字符串。
    const date = new Date();
    String(date); // "Tue Apr 01 2024 14:00:00 GMT-0700 (Pacific Daylight Time)"
    date.toString(); // "Tue Apr 01 2024 14:00:00 GMT-0700 (Pacific Daylight Time)"
    
    1
    2
    3

# JavaScript 其他值到数字值的转换规则是什么?

在JavaScript中,将其他类型的值转换为数字遵循一套详细的规则。这些转换可以是隐式的,比如在算术运算中,也可以是显式的,通过使用 Number() 函数或一元加号(+)操作符。以下是不同类型到数字的转换规则:

  1. Undefined

    • undefined 转换为数字是 NaN(Not-a-Number)。
    Number(undefined); // NaN
    +undefined; // NaN
    
    1
    2
  2. Null

    • null 转换为数字也是 NaN
    Number(null); // NaN
    +null; // NaN
    
    1
    2
  3. Boolean

    • true 转换为数字是 1
    • false 转换为数字是 0
    Number(true); // 1
    +true; // 1
    Number(false); // 0
    +false; // 0
    
    1
    2
    3
    4
  4. String

    • 空字符串 "" 或字符串 " "(仅包含空格)转换为数字是 0
    • 只包含有效数字字符的字符串转换为相应的数字。
    • 包含非数字字符的字符串(除非这些非数字字符在字符串开头,且后面跟随数字字符,这种情况下会解析到第一个非数字字符为止),转换为 NaN
    Number(""); // 0
    Number("123"); // 123
    Number(" 123"); // 123
    Number("123abc"); // NaN
    Number("abc"); // NaN
    +"123"; // 123
    +"123abc"; // NaN
    
    1
    2
    3
    4
    5
    6
    7
  5. Symbol

    • 符号(Symbol)不能直接转换为数字,尝试这样做会抛出错误。
    Number(Symbol.iterator); // TypeError: Cannot convert a Symbol value to a number
    
    1
  6. Object

    • 对象转换为数字时,会调用对象的 valueOf() 方法,然后对返回的结果进行数字转换。
    • 如果 valueOf() 方法返回的对象不是原始值,则会尝试调用该对象的 toString() 方法,并转换其返回的字符串。
    const obj = {valueOf: () => 123};
    Number(obj); // 123
    const obj2 = {toString: () => "123"};
    Number(obj2); // 123
    
    1
    2
    3
    4
  7. Array

    • 空数组 [] 转换为数字是 0
    • 包含一个元素的数组,该元素会被转换为数字。
    • 包含多个元素的数组转换为 NaN
    Number([]); // 0
    Number([10]); // 10
    Number([10, 20]); // NaN
    
    1
    2
    3
  8. Function

    • 函数对象转换为数字时,会调用函数的 toString() 方法,然后转换其返回的字符串。
    const func = function() { return 123; };
    Number(func); // NaN,因为函数的 toString() 返回的是函数的源代码字符串
    
    1
    2
  9. Date

    • 日期对象转换为数字时,会调用日期对象的 getTime() 方法,返回自1970年1月1日以来的毫秒数。
    const date = new Date();
    Number(date); // date.getTime()
    
    1
    2

# JavaScript 其他值到布尔值的转换规则是什么?

在JavaScript中,将其他类型的值转换为布尔值(truefalse)遵循一套特定的规则。这些转换通常在需要布尔上下文(例如条件语句、循环、逻辑操作符等)时隐式进行。以下是不同类型到布尔值的转换规则:

  1. Undefined

    • undefined 转换为布尔值是 false
    !!undefined; // false
    
    1
  2. Null

    • null 转换为布尔值也是 false
    !!null; // false
    
    1
  3. Boolean

    • true 保持为 true
    • false 保持为 false
    !!true;  // true
    !!false; // false
    
    1
    2
  4. Number

    • 数字 0-0NaN 以及 Infinity-Infinity 转换为布尔值是 false
    • 所有其他数值转换为布尔值是 true
    !!0;       // false
    !!NaN;     // false
    !!Infinity; // true
    !!-Infinity; // true
    !!1;       // true
    
    1
    2
    3
    4
    5
  5. String

    • 空字符串 "" 转换为布尔值是 false
    • 字符串 "0""false" 以及其他非空字符串转换为布尔值是 true
    !!"";      // false
    !!"0";     // true
    !!"false"; // true
    !!"hello"; // true
    
    1
    2
    3
    4
  6. Object

    • 所有对象(包括数组和函数)转换为布尔值是 true,即使它们的内容可能是空的或包含假值。
    !!{};      // true
    !![];      // true
    !!function() {}; // true
    
    1
    2
    3
  7. Symbol

    • 符号(Symbol)转换为布尔值是 true
    !!Symbol.iterator; // true
    
    1
  8. BigInt

    • 所有 BigInt 值转换为布尔值是 true
    !!123n; // true
    
    1

示例代码

console.log(!!undefined); // false
console.log(!!null); // false
console.log(!!true); // true
console.log(!!0); // false
console.log(!!""); // false
console.log(!!{}); // true
console.log(!![]); // true
console.log(!!Symbol.iterator); // true
console.log(!!123n); // true
1
2
3
4
5
6
7
8
9

# JavaScript 中 || 和 && 操作符的返回值是什么?

在JavaScript中,||(逻辑或)和&&(逻辑与)操作符用于布尔上下文中,但它们有一个特别的行为:它们会返回它们操作数中的一个。具体来说:

  1. 逻辑或 (||)

    • 如果第一个操作数可以被转换为 true,则 || 操作符返回第一个操作数。
    • 如果第一个操作数可以被转换为 false,则返回第二个操作数。

    这意味着如果第一个操作数是任意“真值”(在JavaScript中,除了 false0""(空字符串)、nullundefinedNaN 之外的所有值都被认为是“真值”),那么 || 操作符将返回这个操作数本身,而不会评估第二个操作数。

    const a = 0;
    const b = "Hello";
    console.log(a || b); // 输出 "Hello",因为 0 转换为 false,所以返回第二个操作数
    
    1
    2
    3
  2. 逻辑与 (&&)

    • 如果第一个操作数可以被转换为 false,则 && 操作符返回第一个操作数。
    • 如果第一个操作数可以被转换为 true,则返回第二个操作数。

    这意味着如果第一个操作数是任意“假值”,那么 && 操作符将返回这个操作数本身,而不会评估第二个操作数。

    const c = "Hello";
    const d = 123;
    console.log(c && d); // 输出 123,因为 "Hello" 转换为 true,所以返回第二个操作数
    
    1
    2
    3

返回值特点

  • 短路求值&&|| 都采用短路求值。这意味着如果第一个操作数可以确定整个表达式的布尔值,那么第二个操作数将不会被评估。
  • 非布尔值返回:这些操作符返回操作数中的一个,而不是仅仅 truefalse。这在很多情况下非常有用,比如在条件表达式中返回某个特定的值。

示例

let x = 0;
let y = "text";
let z = true;

console.log(x || y); // 输出 "text"
console.log(z || y); // 输出 true,因为 true 是一个真值,所以返回第一个操作数
console.log(x && y); // 输出 0,因为 0 是一个假值,所以返回第一个操作数
console.log(z && y); // 输出 "text",因为 true 是一个真值,所以返回第二个操作数
1
2
3
4
5
6
7
8

这种特性使得 ||&& 不仅用于逻辑运算,也常用于简化代码,特别是在需要提供默认值的情况下。例如,使用 || 操作符可以很容易地实现一个变量的默认值设置:

const name = someValue || "Default Name";
1

这里,如果 someValue 是一个假值,name 将被设置为 "Default Name"

# Object.is() 与比较操作符 == 和 === 的区别是什么?

Object.is() 是 JavaScript ES6 引入的一个方法,它提供了一种更精确的方式来比较两个值是否相等。与 ===== 比较操作符相比,Object.is() 在处理一些特殊情况时会有不同的行为。

== 操作符

  • == 是一个比较操作符,它执行类型转换(也称为强制类型转换或隐式类型转换)。
  • 如果两个操作数的类型不同,JavaScript 引擎会尝试将它们转换为相同的类型,然后再进行比较。
  • 这可能导致一些非直观的结果,例如 "" == 0 会返回 true,因为空字符串被转换为数字 0。

=== 操作符

  • === 也是一个比较操作符,但它执行严格比较。
  • 这意味着如果两个操作数的类型不同,=== 直接返回 false,不会进行类型转换。
  • === 在比较时不会进行任何类型转换,因此它通常被认为是更安全的比较方式。

Object.is() 方法

  • Object.is() 是一个方法,它提供了一种更精确的方式来比较两个值。
  • 它在比较时不会进行任何类型转换,与 === 类似。
  • 但是,Object.is() 在处理一些特殊情况时与 === 有所不同:
    1. Object.is(+0, -0) 返回 false,而 +0 === -0 也返回 false,这是因为 +0-0 在 JavaScript 中是不同的值,但它们在数学上是相等的。
    2. Object.is(NaN, NaN) 返回 true,而 NaN === NaN 返回 false。这是因为 NaN 是一个特殊的值,它与任何值(包括它自己)都不相等,但 Object.is() 认为两个 NaN 是相等的。

示例

// 使用 == 操作符
console.log("" == 0); // true,因为空字符串被转换为数字 0
console.log(1 == "1"); // true,因为字符串 "1" 被转换为数字 1

// 使用 === 操作符
console.log(+0 === -0); // false,因为 +0 和 -0 是不同的值
console.log(NaN === NaN); // false,因为 NaN 不等于任何值,包括它自己

// 使用 Object.is()
console.log(Object.is(+0, -0)); // false
console.log(Object.is(NaN, NaN)); // true
1
2
3
4
5
6
7
8
9
10
11

总结

  • == 进行类型转换,可能导致非直观的结果。
  • === 进行严格比较,不进行类型转换,更安全。
  • Object.is() 提供了一种更精确的比较方式,特别是在处理 +0-0NaN 这些特殊值时。

# 什么是 JavaScript 中的包装类型?

在JavaScript中,包装类型(Wrapper Types)是指用于基本数据类型(如字符串、数字、布尔值)的对象类型。这些包装类型允许你将基本数据类型当作对象来使用,从而可以使用一些对象的方法和属性。JavaScript中的包装类型包括:

  1. String:用于包装基本数据类型字符串。
  2. Number:用于包装基本数据类型数字。
  3. Boolean:用于包装基本数据类型布尔值。

包装类型的创建

当你对一个基本数据类型的值调用对象方法时,JavaScript引擎会自动创建一个对应的包装对象。例如:

let s = 'hello';
let strObj = s.toUpperCase(); // "HELLO"

let n = 10;
let numObj = n.toFixed(2); // "10.00"

let b = true;
let boolObj = b.toString(); // "true"
1
2
3
4
5
6
7
8

在上面的例子中,toUpperCase()toFixed()toString()StringNumberBoolean 包装对象的方法。

包装类型的自动装箱和拆箱

JavaScript引擎会自动在基本类型和包装对象之间进行转换,这个过程称为“装箱”(将基本类型转换为对象)和“拆箱”(将对象转换回基本类型)。

  • 装箱:当你尝试对基本类型使用对象的方法时,JavaScript引擎会临时创建一个包装对象,使用完毕后,这个对象会被销毁。
  • 拆箱:当你将包装对象用于需要基本类型的上下文中时,JavaScript引擎会自动将对象转换回基本类型。

包装类型的缺点

虽然包装类型提供了方便的方法来处理基本数据类型,但它们也有一些缺点:

  1. 性能问题:每次调用包装类型的方法时,都会创建一个新的对象,这可能会导致不必要的内存分配和垃圾回收,从而影响性能。
  2. 不可变性:尽管字符串是不可变的,但使用 String 包装对象的方法(如 charAtslice 等)会返回一个新的字符串,而不是修改原始字符串。

示例代码

let num = 42;

// 装箱:创建 Number 对象
let numObj = new Number(42);

// 拆箱:将 Number 对象转换为数字
let numAgain = numObj.valueOf();

console.log(typeof num); // "number"
console.log(typeof numObj); // "object"
console.log(typeof numAgain); // "number"

// 自动装箱和拆箱
let str = '123';
let strObj = str.toUpperCase(); // 自动装箱,调用方法
console.log(strObj); // "123".toUpperCase() -> "123"

let bool = false;
let boolObj = new Boolean(bool);
let boolAgain = boolObj.valueOf(); // 自动拆箱
console.log(boolAgain); // false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# JavaScript 中如何进行隐式类型转换?

在JavaScript中,隐式类型转换是指在某些操作或表达式中,JavaScript引擎自动将一种数据类型转换为另一种数据类型,而不需要程序员显式地进行类型转换。这种转换通常发生在算术运算、比较运算、逻辑运算以及一些内置函数中。以下是一些常见的隐式类型转换场景:

1. 算术运算

在算术运算中,非数字类型的操作数会被转换为数字类型。

"10" + 2 * 3; // "103",因为字符串 "10" 与数字 2 相加时,2 被转换成字符串 "2",然后进行字符串拼接
"10" - 2;     // 8,因为字符串 "10" 被转换成数字 10,然后执行减法运算
"5" / "2";    // 2.5,两个字符串都被转换成数字,然后执行除法运算
1
2
3

2. 比较运算

在比较运算中,不同类型的操作数会尝试被转换成相同的类型,通常是数字或字符串。

"5" < 3;       // false,字符串 "5" 与数字 3 比较时,字符串会被转换成数字
"10" == 10;    // true,一个字符串和一个数字比较时,字符串被转换成数字
"abc" > "ab";  // true,字符串按照字典顺序进行比较
1
2
3

3. 逻辑运算

逻辑运算符 &&|| 在处理非布尔值时,会将操作数转换为布尔值。

"" || 0;      // 0,空字符串转换为 false,然后转换为数字 0
true || "yes"; // true,字符串 "yes" 不会被评估,因为 true 已经是一个真值
1
2

4. 条件运算(三元运算符)

条件运算符 ? : 也会进行隐式类型转换。

"10" ? "yes" : "no"; // "yes",因为字符串 "10" 转换为数字 10,是一个真值
0 ? "yes" : "no";     // "no",数字 0 转换为 false
1
2

5. 内置函数

一些内置函数,如 parseInt()parseFloat(),会尝试将它们的参数转换为数字。

parseInt("10.98 apples"); // 10,字符串被转换为数字,直到遇到非数字字符
parseFloat("3.14 apples"); // 3.14,字符串被转换为数字,直到遇到非数字字符
1
2

6. 字符串拼接

任何非字符串类型的值在与字符串进行拼接时,都会被转换为字符串。

"Hello, " + 123; // "Hello, 123",数字 123 被转换成字符串 "123"
1

7. 使用 == 操作符

使用 == 操作符进行比较时,如果操作数类型不同,会自动进行类型转换。

"5" == 5;       // true,字符串 "5" 被转换成数字 5
0 == false;     // true,数字 0 被转换成 false
"" == false;    // true,空字符串转换为 false
1
2
3

# JavaScript 中 + 操作符什么时候用于字符串的拼接?

在JavaScript中,+ 操作符具有多种用途,其中之一就是用于字符串的拼接。当+ 操作符用于两个或多个操作数时,JavaScript引擎会根据操作数的类型和上下文来决定是执行数学加法还是字符串拼接。以下是+ 操作符用于字符串拼接的一些情况:

  1. 两个操作数都是字符串: 当两个操作数都是字符串时,+ 操作符会将它们拼接在一起。

    let greeting = "Hello, " + "world!";
    console.log(greeting); // 输出:Hello, world!
    
    1
    2
  2. 至少一个操作数是字符串: 如果至少有一个操作数是字符串,而另一个操作数是任何其他类型(如数字、布尔值、对象等),那么非字符串的操作数会被转换成字符串,然后进行拼接。

    let message = "The number is " + 42;
    console.log(message); // 输出:The number is 42
    
    let booleanValue = "Checking " + true;
    console.log(booleanValue); // 输出:Checking true
    
    1
    2
    3
    4
    5
  3. 操作数之一是对象: 如果操作数之一是对象,那么对象会通过调用其toString() 方法转换为字符串,然后进行拼接。如果对象没有toString() 方法,或者toString() 方法返回undefined,则会尝试调用valueOf() 方法,如果valueOf() 方法也没有返回有效值,则结果为"[object Object]"

    let date = new Date();
    let dateStr = date + " was the date of the event.";
    console.log(dateStr); // 输出当前日期,转换为字符串,然后拼接
    
    let obj = { name: "Kimi" };
    let greeting = "Hello, " + obj + "!";
    console.log(greeting); // 输出:Hello, [object Object]!
    
    1
    2
    3
    4
    5
    6
    7
  4. 操作数之一是nullundefined: 如果操作数之一是nullundefined,它们会被转换为空字符串"",然后进行拼接。

    let text1 = "This is " + undefined + " example.";
    console.log(text1); // 输出:This is  example.
    
    let text2 = "This is " + null + " a test.";
    console.log(text2); // 输出:This is  a test.
    
    1
    2
    3
    4
    5
  5. 在模板字符串中: 模板字符串(使用反引号```)内部也可以使用+ 操作符进行字符串拼接。

    let name = "Kimi";
    let introduction = `My name is ${name} and I love coding.`;
    console.log(introduction);
    // 输出:My name is Kimi and I love coding.
    
    1
    2
    3
    4

# JavaScript 中为什么会有 BigInt 的提案?

JavaScript 中的 BigInt 是一种新的内置对象,它提供了一种方法来表示大于 2^53 - 1 的整数。这是 JavaScript 中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数,它在某些方面类似于 Number,但是也有几个关键的不同点:不能用于 Math 对象中的方法;不能和任何 Number 实例混合运算,两者必须转换成同一种类型。在两种类型来回转换时要小心,因为 BigInt 变量在转换成 Number 变量时可能会丢失精度。

BigInt 的使用场景包括但不限于:

  1. 金融科技:金融技术领域经常需要使用大整数来表示金额,以确保计算的精确性。
  2. 大整数ID:例如,Twitter 使用64位整数作为推文的ID,在 JavaScript 中,这些ID以前不得不以字符串形式存储。
  3. 高精度时间戳BigInt 可以用于更高精度的时间戳,例如纳秒级。

创建 BigInt 的方式有两种:

  1. 在整数字面量后面添加 n 后缀,例如:10n
  2. 使用 BigInt() 函数,并传递一个整数值或字符串值,例如:BigInt("10")

BigInt 支持大多数常见的操作符,包括 +-***%。位操作符也支持,除了无符号右移操作符 >>>,因为 BigInt 都是有符号的。

在比较 BigIntNumber 时,BigIntNumber 不是严格相等的,但是宽松相等的。例如,0n === 0 返回 false,但 0n == 0 返回 true

由于 BigInt 的引入,开发者不再需要使用字符串来表示大整数,也不需要依赖第三方库来处理大整数的运算。这使得 JavaScript 能够更安全、更高效地处理大整数的场景。

# Object.assign 和对象扩展运算符有什么区别?是深拷贝还是浅拷贝?

Object.assign 方法和对象扩展运算符(...)都可以用来合并对象,但它们之间有一些区别:

  1. 语法
    • Object.assign 是一个方法,需要明确地调用。
    • 对象扩展运算符是 ES6 引入的语法糖,用于在字面量对象和函数调用中展开对象。
  2. 返回值
    • Object.assign 返回合并后的对象。
    • 对象扩展运算符返回一个新的对象实例。
  3. 参数
    • Object.assign 接受多个参数,第一个参数是目标对象,其余参数都是源对象。
    • 对象扩展运算符在对象字面量中使用,可以连续展开多个对象。
  4. 继承
    • Object.assign 会复制源对象上的可枚举属性到目标对象,包括原型链上的属性。
    • 对象扩展运算符只会复制对象自身的可枚举属性,不会复制原型链上的属性。
  5. 特殊处理
    • Object.assignnullundefined 作为源对象或目标对象有特殊处理,会抛出错误。
    • 对象扩展运算符不允许 nullundefined 作为展开对象,否则会抛出错误。
  6. 性能
    • 在某些情况下,对象扩展运算符可能比 Object.assign 性能更好,因为它通常有更少的抽象操作。

两者都不是深拷贝,它们都是浅拷贝。这意味着它们只会复制对象的顶层属性,如果属性值是引用类型,则复制的是引用,而不是引用的对象本身。如果需要深拷贝,需要使用其他方法,如递归复制或使用库函数(例如 lodashcloneDeep 方法)。

示例

使用 Object.assign

const obj1 = { a: 1 };
const obj2 = { b: 2 };
const obj3 = { c: 3 };

const merged = Object.assign({}, obj1, obj2, obj3);
console.log(merged); // { a: 1, b: 2, c: 3 }
1
2
3
4
5
6

使用对象扩展运算符:

const obj1 = { a: 1 };
const obj2 = { b: 2 };
const obj3 = { c: 3 };

const merged = { ...obj1, ...obj2, ...obj3 };
console.log(merged); // { a: 1, b: 2, c: 3 }
1
2
3
4
5
6

# JavaScript 中 Map 和 Object 的区别是什么?

在JavaScript中,MapObject 都是用于存储键值对的数据结构,但它们之间存在一些关键的区别:

  1. 键的类型
    • Object 的键(属性名)通常是字符串或符号(Symbol),但也可以是任何值,包括对象和函数,但在实践中,非字符串的键可能会在某些操作中遇到问题。
    • Map 的键可以是任意值,包括函数、对象、任意原始类型等。
  2. 迭代顺序
    • Map 对象保持键值对的插入顺序,这意味着迭代操作(如 forEach)将按照元素被添加的顺序进行。
    • Object 的属性遍历顺序在ES6之前是不确定的,ES6规范规定了一种遍历顺序,但这种顺序并不是按照属性添加的顺序。
  3. 内置方法
    • Map 有一些内置的方法,如 Map.prototype.get()Map.prototype.set()Map.prototype.has()Map.prototype.delete()Map.prototype.clear(),这些方法提供了对集合的直观操作。
    • Object 没有这样的内置方法,操作对象通常使用普通的属性访问语法(点或方括号)。
  4. 性能
    • 对于频繁增删键值对的场景,Map 通常提供更好的性能。
    • 对于具有大量属性且变动不频繁的对象,Object 可能在某些引擎优化下表现更好。
  5. 可迭代性
    • Map 是可迭代的,这意味着它可以直接用在 for...of 循环中,或者用在任何接受迭代器的函数中。
    • Object 也可以被迭代,但默认情况下,它会迭代其所有可枚举的属性,包括原型链上的属性。
  6. 键值对的数量
    • Map 对存储的键值对数量没有限制。
    • Object 的属性数量受限于JavaScript引擎的内部限制,虽然这个限制通常非常高。
  7. 原型链
    • Object 继承自 Object.prototype,这意味着它可能继承了一些不想要的方法,如 toStringhasOwnProperty 等。
    • Map 是一个纯粹的数据结构,没有原型链,不继承任何方法。
  8. 序列化
    • Map 对象不能直接被 JSON.stringify() 序列化。
    • Object 可以被 JSON.stringify() 序列化,但需要确保键是字符串。
  9. 用途
    • Map 更适合用作通用的键值对集合,特别是当键的类型多样或者需要保持插入顺序时。
    • Object 更适合用作表示实体或具有属性的数据结构。

示例

使用 Map

let map = new Map();
map.set('key1', 'value1');
map.set(123, 'value2');
map.set(true, 'value3');

console.log(map.get('key1')); // 'value1'
console.log(map.get(123)); // 'value2'
console.log(map.has(true)); // true
1
2
3
4
5
6
7
8

使用 Object

let obj = {};
obj['key1'] = 'value1';
obj[123] = 'value2';
obj[true] = 'value3';

console.log(obj.key1); // 'value1'
console.log(obj[123]); // 'value2'
console.log('true' in obj); // true
1
2
3
4
5
6
7
8

# JavaScript 中判断数据类型的方式有哪些?

在JavaScript中,有多种方式可以用来判断变量的数据类型:

  1. typeof 操作符

    • 返回一个字符串,表示未经计算的值的数据类型。
    • 对于基本数据类型,如 undefinednullstringnumberbooleansymbol(ES6引入),以及 functiontypeof 提供了直接的类型信息。
    • 对于对象和数组,typeof 返回 "object",包括 null
    typeof "text"; // "string"
    typeof 123;    // "number"
    typeof true;   // "boolean"
    typeof undefined; // "undefined"
    typeof function() {}; // "function"
    typeof {name: "Kimi"}; // "object"
    typeof [1, 2, 3]; // "object"
    typeof null; // "object"
    
    1
    2
    3
    4
    5
    6
    7
    8
  2. instanceof 操作符

    • 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
    • 返回一个布尔值,表示一个对象是否是某个构造函数的实例。
    const arr = [1, 2, 3];
    arr instanceof Array; // true
    
    1
    2
  3. Object.prototype.toString.call() 方法

    • 返回一个字符串,表示对象的内部类[[class]]属性。
    • 这是确定对象类型的最准确方式,因为它不会受到对象属性的影响。
    Object.prototype.toString.call("text"); // "[object String]"
    Object.prototype.toString.call(123); // "[object Number]"
    Object.prototype.toString.call(true); // "[object Boolean]"
    Object.prototype.toString.call([1, 2, 3]); // "[object Array]"
    Object.prototype.toString.call(null); // "[object Null]"
    Object.prototype.toString.call(undefined); // "[object Undefined]"
    
    1
    2
    3
    4
    5
    6
  4. Array.isArray() 方法

    • 用于确定传递的值是否是一个数组。
    • 返回一个布尔值。
    Array.isArray([1, 2, 3]); // true
    Array.isArray({}); // false
    
    1
    2
  5. constructor 属性

    • 每个对象都有一个 constructor 属性,指向创建该对象的构造函数。
    • 可以用来检查一个对象的类型,但不是很可靠,因为 constructor 属性可以被改写。
    const arr = [1, 2, 3];
    arr.constructor === Array; // true
    
    1
    2
  6. Object.getPrototypeOf() 方法

    • 可以用来获取一个对象的原型对象,进而判断对象的类型。
    const obj = {};
    Object.getPrototypeOf(obj) === Object.prototype; // true
    
    1
    2
  7. typeof 与直接调用函数

    • 对于函数,typeof 会返回 "function",但直接调用函数会执行函数。
    typeof function() {} === "function"; // true
    
    1
  8. isNaN() 函数

    • 用于判断一个值是否是 NaN
    • 返回一个布尔值。
    isNaN(NaN); // true
    isNaN(123); // false
    
    1
    2
  9. Number.isNaN() 方法(ES6引入):

    • 用于判断一个值是否是 NaN,与 isNaN() 不同,它不会进行类型转换。
    Number.isNaN(NaN); // true
    Number.isNaN(123); // false
    Number.isNaN("NaN"); // false
    
    1
    2
    3
  10. Symbol.toStringTag 属性

    • 可以通过 Object.prototype.toString.call() 方法访问,或者通过 Object.prototype[Symbol.toStringTag] 属性访问。
    • 许多内置类型和一些内置对象在 Symbol.toStringTag 属性上都有定义,可以用来判断类型。
    Object.prototype.toString.call(new Map()); // "[object Map]"
    Object[Symbol.toStringTag]; // "Object"
    
    1
    2

# JavaScript 有哪些内置对象?

JavaScript 中有许多内置对象,它们提供了语言的核心功能和操作数据的能力。以下是一些常见的 JavaScript 内置对象:

  1. Global Objects
    • globalThis:指向全局对象(在浏览器中是 window,在Node.js中是 global)。
  2. Function Constructors
    • Function:创建一个新的函数。
  3. Fundamental Objects
    • Object:创建新的对象。
    • Array:表示一个数组,用于存储多个元素。
    • String:表示一个字符串值。
    • Number:表示一个数字值。
    • Boolean:表示一个布尔值(truefalse)。
    • Symbol:表示一个唯一的、不可变的符号。
  4. Collections
    • Map:存储键值对的集合,可以记住键的原始插入顺序。
    • Set:存储唯一值的集合。
    • WeakMap:存储键值对的集合,键必须是对象,且不允许垃圾回收。
    • WeakSet:存储对象的集合,不允许垃圾回收。
  5. ES6+ Data Structures
    • Promise:用于异步计算和事件链。
    • Generator:用于创建可以暂停和恢复的函数。
    • AsyncFunction:异步函数,使用 asyncawait 关键字。
  6. Text Processing
    • RegExp:表示正则表达式的对象。
    • String.prototype:提供字符串操作方法。
    • Array.prototype:提供数组操作方法。
  7. Mathematical and Date Objects
    • Math:提供数学常数和函数。
    • Date:用于处理日期和时间。
  8. Structured Data
    • ArrayBuffer:表示通用的、固定长度的原始二进制数据缓冲区。
    • SharedArrayBuffer:表示可以在多个共享器之间共享的固定长度的原始二进制数据缓冲区。
    • TypedArray:表示原始二进制数据的类型化数组,例如 Int8ArrayUint8ArrayFloat32Array 等。
    • DataView:提供了一个接口,用于读写多种数值类型的二进制数据。
  9. Control Abstraction Objects
    • Error:创建错误对象。
    • EvalError:用于表示 eval() 函数中的错误。
    • RangeError:表示数值变量或参数超出其有效范围的错误。
    • ReferenceError:表示引用未定义的变量时的错误。
    • SyntaxError:表示语法错误。
    • TypeError:表示类型不匹配的错误。
    • URIError:表示全局 encodeURI()decodeURI()encodeURIComponent()decodeURIComponent() 函数中的错误。
  10. Reflect API
    • Reflect:提供拦截JavaScript对象属性访问的API。
  11. JSON
    • JSON:用于处理JSON数据的解析和字符串化。
  12. DOM and BOM (Browser Only)
    • Document:表示整个HTML或XML文档。
    • HTMLElement:表示所有的HTML元素。
    • Window:表示浏览器中的一个窗口。
  13. Node.js Specific Objects (Server Only)
    • Buffer:用于处理二进制数据流。
    • process:提供关于当前Node.js进程的信息。

# JavaScript 中常用的正则表达式有哪些?

在JavaScript中,正则表达式是一种强大的文本模式匹配工具,它们可以用来搜索、替换、提取或校验字符串。以下是一些常用的正则表达式示例,以及它们各自的用途:

  1. 匹配电子邮件地址

    /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
    
    1

    用于验证电子邮件地址的格式。

  2. 匹配电话号码

    /^\+?\d{10,13}$/;
    
    1

    用于验证电话号码的长度和数字。

  3. 匹配URL

    /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([\/\w .-]*)*\/?$/;
    
    1

    用于验证URL的格式。

  4. 匹配日期格式(YYYY-MM-DD)

    /^\d{4}-\d{2}-\d{2}$/;
    
    1

    用于验证日期格式是否正确。

  5. 匹配十六进制颜色值

    /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
    
    1

    用于验证十六进制颜色值。

  6. 匹配CSS选择器

    /^(?:#([\w-]+)|\.([\w-]+))$/;
    
    1

    用于验证CSS选择器的格式。

  7. 匹配空白字符

    /\s+/;
    
    1

    用于匹配空格、制表符、换行符等空白字符。

  8. 匹配HTML标签

    /<([a-z]+)([^>]*)>.*?<\/\1>/gi;
    
    1

    用于匹配HTML标签。

  9. 匹配IP地址

    /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
    
    1

    用于验证IPv4地址的格式。

  10. 匹配字符串开始和结束

    ^abc 末尾:abc$
    
    1

    用于匹配字符串开始和结束的特定文本。

  11. 匹配特定字符出现次数

    /^(a+)+$/;
    
    1

    用于匹配字符a出现一次或多次。

  12. 匹配特定模式的字符串

    /\d{3}-\d{2}-\d{4}/;
    
    1

    用于匹配类似“123-45-6789”这样的格式。

  13. 匹配货币金额

    /^\$?\d+(?:\.\d{2})?$/;
    
    1

    用于验证货币金额,包括可选的小数部分。

  14. 匹配特定文件扩展名

    /\.(jpg|jpeg|png|gif)$/i;
    
    1

    用于验证文件名是否以特定的图像扩展名结束。

  15. 匹配单词边界

    \bword\b;
    
    1

    用于匹配完整的单词。

# 说说你对 JSON 的理解?

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,它基于文本,易于人阅读和编写,同时也易于机器解析和生成。JSON 是 JavaScript 的一个子集,但尽管它的名字中有“JavaScript”,它实际上是一种独立于语言的格式,许多编程语言都支持生成和解析 JSON 数据。

以下是我对 JSON 的一些理解:

  1. 结构
    • JSON 数据格式支持两种结构:对象(在 JavaScript 中表示为对象字面量)和数组。
    • 对象由一系列键值对组成,键必须是字符串,并且使用大括号 {} 包围。
    • 数组是值的有序序列,使用方括号 [] 包围。
  2. 键值对
    • 对象中的每个键值对由键和值组成,键和值之间用冒号 : 分隔。
    • 键必须是双引号包围的字符串。
  3. 数据类型
    • JSON 支持几种数据类型:字符串(必须用双引号包围)、数字、对象、数组、布尔值(truefalse)以及 null
    • JSON 中的字符串表示可以包括常见的转义字符,如 \"\\\/ 等。
  4. 用途
    • JSON 主要用于在网络上传输数据,特别是在 Web 应用程序和服务器之间。
    • 它也常用于配置文件和数据存储。
  5. 解析和序列化
    • 在 JavaScript 中,可以使用 JSON.parse() 方法将 JSON 字符串解析为 JavaScript 对象。
    • 使用 JSON.stringify() 方法可以将 JavaScript 对象序列化为 JSON 字符串。
  6. 跨语言支持
    • 许多编程语言提供了解析和生成 JSON 的库,如 Python 的 json 模块,Java 的 org.json 库等。
  7. 与 XML 的比较
    • JSON 通常比 XML 更简洁,因为它不需要结束标签,也不需要定义标签。
    • JSON 结构简单,易于解析,因此在现代 Web 开发中比 XML 更受欢迎。
  8. 局限性
    • JSON 不支持注释,这在某些情况下可能会限制其表达能力。
    • JSON 没有提供数据验证或类型定义的功能,这通常需要额外的机制来实现。
  9. 安全性
    • 在处理来自不可信源的 JSON 数据时,需要谨慎,因为恶意构造的 JSON 可以导致安全问题,如注入攻击。
  10. 应用实例
    • Web APIs(如 RESTful API)通常使用 JSON 作为数据交换格式。
    • 跨域资源共享(CORS)中,JSONP 利用 <script> 标签来绕过同源策略限制。

# JavaScript 脚本延迟加载的方式有哪些?

在Web开发中,延迟加载JavaScript脚本是一种优化页面加载时间和性能的常用技术。以下是一些常见的JavaScript延迟加载方法:

  1. defer 属性

    • <script>标签中使用defer属性可以延迟脚本的加载和执行,直到HTML文档解析完成。
    • 多个带有defer属性的脚本按顺序执行,不会阻塞页面渲染。
    <script src="script1.js" defer></script>
    <script src="script2.js" defer></script>
    
    1
    2
  2. async 属性

    • 使用async属性的<script>标签会异步加载脚本,但一旦加载完成就会立即执行,可能会阻塞页面渲染。
    • 适用于那些独立于其他脚本运行的代码。
    <script src="script.js" async></script>
    
    1
  3. 动态创建脚本

    • 通过JavaScript动态创建<script>元素并将其添加到DOM中,可以控制脚本的加载时机。
    • 这种方法可以进一步控制脚本的加载顺序和执行时机。
    function loadScript(url, callback) {
      var script = document.createElement('script');
      script.type = 'text/javascript';
      script.src = url;
      script.onload = function() {
        if (typeof callback === 'function') {
          callback();
        }
      };
      script.onerror = function() {
        console.error('Script load error: ' + url);
      };
      document.head.appendChild(script);
    }
    
    loadScript('script1.js', function() {
      // script1.js 加载完成后的回调函数
      loadScript('script2.js');
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
  4. 事件监听器

    • 将脚本的加载与某些事件(如DOMContentLoadedload或自定义事件)关联起来,可以延迟脚本的加载。
    window.addEventListener('DOMContentLoaded', (event) => {
      loadScript('script.js');
    });
    
    1
    2
    3
  5. 页面底部加载

    • <script>标签放置在HTML文档的底部,紧接在</body>标签之前,可以减少脚本加载对页面渲染的阻塞。
    <script src="script.js"></script>
    </body>
    
    1
    2
  6. 使用Promises和Fetch API

    • 结合现代JavaScript的fetchPromise,可以更灵活地控制脚本的加载和执行。
    fetch('script.js')
      .then(response => response.text())
      .then(script => {
        const scriptElement = document.createElement('script');
        scriptElement.textContent = script;
        document.body.appendChild(scriptElement);
      })
      .catch(error => console.error('Error loading the script:', error));
    
    1
    2
    3
    4
    5
    6
    7
    8
  7. 服务端渲染

    • 在服务端预先加载和执行必要的JavaScript脚本,然后将结果直接嵌入到HTML中,可以减少客户端的加载时间。
  8. 代码分割和模块懒加载

    • 使用模块打包工具(如Webpack、Rollup)的代码分割功能,可以实现模块的懒加载,即只有当某个模块被实际需要时才加载。

# JavaScript 脚本异步加载如何实现?各有什么区别?

JavaScript 脚本的异步加载可以通过几种不同的技术实现,每种技术都有其特点和适用场景。以下是几种常见的异步加载方法及其区别:

  1. async 属性

    • <script> 标签中使用 async 属性可以让脚本异步加载,但加载完成后会立即执行,这可能会阻塞页面的渲染。
    • async 脚本不会保证加载的顺序,它们是乱序执行的。
    <script src="script1.js" async></script>
    <script src="script2.js" async></script>
    
    1
    2
  2. defer 属性

    • 使用 defer 属性的 <script> 标签会延迟脚本的加载和执行,直到整个页面解析完成后,按照它们在文档中出现的顺序执行。
    • defer 脚本不会阻塞页面渲染,但会阻塞页面的解析。
    <script src="script1.js" defer></script>
    <script src="script2.js" defer></script>
    
    1
    2
  3. 动态创建脚本

    • 通过 JavaScript 动态创建 <script> 元素并添加到文档中,可以更灵活地控制脚本的加载和执行。
    • 可以使用 onload 事件监听器来知道何时加载完成,并且可以控制执行顺序。
    function loadScript(url) {
      const script = document.createElement('script');
      script.src = url;
      script.async = true; // 可以设置为 false 来保证顺序执行
      script.onload = function() {
        console.log('Script loaded and executed.');
      };
      script.onerror = function() {
        console.error('Script load error.');
      };
      document.head.appendChild(script);
    }
    
    loadScript('script1.js');
    loadScript('script2.js');
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  4. Fetch API

    • 使用 Fetch API 可以更细粒度地控制脚本的加载和执行,并且可以在加载完成后立即执行脚本。
    • 这种方法允许你使用现代的 Promise 和 async/await 语法。
    fetch('script.js')
      .then(response => {
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        return response.text();
      })
      .then(script => {
        const scriptElement = document.createElement('script');
        scriptElement.textContent = script;
        document.body.appendChild(scriptElement);
      })
      .catch(error => console.error('Error loading the script:', error));
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  5. 模块动态导入

    • 使用 ES6 模块的动态导入 import() 函数,可以实现模块的异步加载。
    • 动态导入返回一个 Promise,可以在加载完成后执行特定的代码。
    import('module.js')
      .then(module => {
        module.default();
      })
      .catch(error => console.error('Module load error:', error));
    
    1
    2
    3
    4
    5

区别:

  • 加载时机
    • async 脚本在加载过程中会阻塞页面解析,但不会阻塞页面渲染。
    • defer 脚本不会阻塞页面解析,但会阻塞页面渲染直到脚本执行完成。
    • 动态创建脚本和 Fetch API 可以在不影响页面解析和渲染的情况下加载和执行。
  • 执行顺序
    • async 脚本按照加载完成的顺序执行,不保证与文档中出现的顺序一致。
    • defer 脚本按照它们在文档中出现的顺序执行。
    • 动态创建脚本和 Fetch API 允许你精确控制执行顺序。
  • 兼容性
    • asyncdefer 属性在现代浏览器中广泛支持。
    • 动态创建脚本和 Fetch API 需要更现代的JavaScript环境,但它们提供了更多的灵活性。
  • 使用场景
    • 当你需要尽快执行脚本,并且该脚本不依赖于其他脚本时,可以使用 async
    • 当你需要确保脚本按顺序执行,并且等到页面解析完成后再执行时,可以使用 defer
    • 当你需要完全控制脚本的加载和执行,或者需要加载模块时,可以使用动态创建脚本、Fetch API 或模块动态导入。

# 什么是 JavaScript 的类数组对象?如何转化为数组?

在JavaScript中,类数组对象(Array-like objects)是指那些看起来像数组,但实际上不是数组的对象。这些对象通常具有以下特征:

  1. 有数字索引的属性(例如,object[0]object[1] 等)。
  2. 有一个表示数组长度的 length 属性。

常见的类数组对象包括:

  • 函数的 arguments 对象。
  • NodeList,由 document.getElementsByTagNamedocument.querySelectorAll 等DOM方法返回。
  • 某些Web API返回的对象,如 HTMLCollection

如何转化为数组?

要将类数组对象转换为真正的数组,可以使用以下几种方法:

  1. 使用 Array.from() 方法

    • Array.from() 方法可以从类数组对象或可迭代对象中创建一个新的数组实例。
    let arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
    let arr = Array.from(arrayLike);
    console.log(arr); // ["a", "b", "c"]
    
    1
    2
    3
  2. 使用扩展运算符(spread operator)

    • 扩展运算符 ... 可以用来将类数组对象展开成数组的元素。
    let arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
    let arr = [...arrayLike];
    console.log(arr); // ["a", "b", "c"]
    
    1
    2
    3
  3. 使用 Array.prototype.slice() 方法

    • slice() 方法可以复制数组的一部分,将类数组对象作为其参数传入,可以创建一个新的数组。
    let arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
    let arr = Array.prototype.slice.call(arrayLike);
    console.log(arr); // ["a", "b", "c"]
    
    1
    2
    3
  4. 使用 Array.prototype.concat() 方法

    • concat() 方法可以将两个或多个数组合并成一个新数组,也可以接受非数组参数。
    let arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
    let arr = Array.prototype.concat.call(arrayLike);
    console.log(arr); // ["a", "b", "c"]
    
    1
    2
    3
  5. 使用 Object 方法结合 map

    • 这种方法较少使用,但可以通过 Object.keys()Object.values()map() 结合来转换。
    let arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
    let arr = Object.keys(arrayLike).map(key => arrayLike[key]);
    console.log(arr); // ["a", "b", "c"]
    
    1
    2
    3

# JavaScript 的数组有哪些原生方法?

JavaScript 数组是对象的一种特殊类型,它提供了许多内置方法来执行常见的数组操作。以下是一些常用的数组原生方法:

  1. 数组实例方法
    • concat():合并两个或多个数组。
    • join():将数组的所有元素连接成一个字符串。
    • slice():提取原数组的一部分,返回一个新数组。
    • toString():将数组转换为字符串,并返回结果。
    • toLocaleString():将数组转换为本地字符串表示,按照本地的数字和日期格式。
    • push():向数组的末尾添加一个或多个元素,并返回新的长度。
    • pop():移除数组的最后一个元素,并返回该元素。
    • shift():移除数组的第一个元素,并返回该元素。
    • unshift():向数组的开头添加一个或多个元素,并返回新的长度。
    • splice():添加、删除或替换数组的元素。
    • sort():对数组元素进行排序。
    • reverse():颠倒数组中元素的顺序。
  2. 迭代方法
    • every():对数组中的所有元素执行一个函数,直到找到不满足条件的元素为止。
    • some():对数组中的所有元素执行一个函数,直到找到满足条件的元素为止。
    • forEach():对数组中的每个元素执行一个函数。
    • map():创建一个新数组,其元素是调用一次提供的函数后的返回值。
    • filter():创建一个新数组,包含通过测试的所有元素。
    • reduce():对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
    • reduceRight():同 reduce(),但从数组的末尾开始应用函数。
  3. 搜索和位置方法
    • indexOf():返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。
    • lastIndexOf():返回在数组中可以找到一个给定元素的最后一个索引,如果不存在,则返回-1。
    • find():返回数组中满足提供的测试函数的第一个元素的值。
    • findIndex():返回数组中满足提供的测试函数的第一个元素的索引。
    • includes():确定一个数组是否包含一个特定的值。
  4. 数组特定方法
    • flat():创建一个新数组,其中所有嵌套数组都被“扁平化”。
    • flatMap():首先使用映射函数映射每个元素,然后将结果“扁平化”。
    • entries():返回一个新的数组迭代器对象,该对象包含数组中每个索引的键/值对。
    • keys():返回一个新的迭代器对象,该对象包含数组中每个索引的键。
    • values():返回一个新的迭代器对象,该对象包含数组中每个索引的值。
  5. ES2020+ 新增方法
    • flatMap():对数组的每个元素执行一个映射函数,并用结果数组替换原数组的每个元素。
    • toReversed():返回一个新的数组,它是原数组的倒序副本。
    • toLocaleString():返回一个使用本地特定格式的数字格式化数组元素的字符串。
    • toString():返回一个由数组中的所有元素转换成字符串,并通过逗号连接而成的字符串。

# 为什么 JavaScript 函数的 arguments 参数是类数组而不是数组?如何遍历类数组?

在JavaScript中,arguments 参数代表一个类数组对象,它提供了访问函数调用时传入的参数的途径。尽管它看起来和数组很像,具有数字索引和length属性,但它并不是一个真正的数组。arguments对象是类数组的原因包括:

  1. 历史原因:在JavaScript的早期版本中,arguments对象被设计为一个类数组对象,而不是一个真正的数组,这是出于性能和设计的考虑。
  2. 性能考虑:在早期的JavaScript引擎中,类数组对象可能比真正的数组在某些操作上更高效,因为它们不需要实现数组的所有功能。
  3. 功能限制arguments对象不支持数组的一些方法,如pushpopslice等。这是因为在ES6之前,arguments对象是基于内建Arguments对象的,而不是Array对象。

遍历类数组 arguments

由于arguments是一个类数组对象,你不能直接使用数组的方法来遍历它。但是,你可以使用以下几种方法来遍历arguments

  1. 使用for循环

    function example() {
      for (let i = 0; i < arguments.length; i++) {
        console.log(arguments[i]);
      }
    }
    example(1, 2, 3); // 打印 1, 2, 3
    
    1
    2
    3
    4
    5
    6
  2. 使用Array.from()方法(ES6+):

    function example() {
      Array.from(arguments).forEach((arg) => {
        console.log(arg);
      });
    }
    example(1, 2, 3); // 打印 1, 2, 3
    
    1
    2
    3
    4
    5
    6
  3. 使用展开运算符(...(ES6+):

    function example() {
      (...arguments).forEach((arg) => {
        console.log(arg);
      });
    }
    example(1, 2, 3); // 打印 1, 2, 3
    
    1
    2
    3
    4
    5
    6
  4. 使用Array.prototype.forEach.call()方法

    function example() {
      Array.prototype.forEach.call(arguments, (arg) => {
        console.log(arg);
      });
    }
    example(1, 2, 3); // 打印 1, 2, 3
    
    1
    2
    3
    4
    5
    6
  5. 使用for...of循环(ES6+,如果将arguments转换为迭代器):

    function example() {
      for (let arg of Array.from(arguments)) {
        console.log(arg);
      }
    }
    example(1, 2, 3); // 打印 1, 2, 3
    
    1
    2
    3
    4
    5
    6

在ES6及更高版本的JavaScript中,由于箭头函数和let/const等新特性的引入,arguments对象的使用已经大大减少,因为这些新特性提供了更好的作用域和参数处理方式。此外,如果你需要一个真正的数组,可以使用rest参数语法来代替arguments

function example(...args) {
  args.forEach((arg) => {
    console.log(arg);
  });
}
example(1, 2, 3); // 打印 1, 2, 3
1
2
3
4
5
6

# 什么是 DOM 和 BOM?

DOM(Document Object Model)和BOM(Browser Object Model)是Web开发中的两个重要概念,它们都与浏览器中的对象模型有关,但各自代表不同的功能和用途。

DOM(文档对象模型)

DOM 是一种编程接口,它将HTML或XML文档呈现为一个由节点和对象(通常称为元素)组成的树结构。这使得开发者可以通过脚本来操作和访问页面的内容、结构和样式。

DOM的主要特点包括:

  1. 树结构:DOM将文档表示为一个节点树,每个节点可以是元素、属性或文本。
  2. 元素访问:可以通过DOM API访问和修改文档中的元素。
  3. 事件处理:可以为DOM元素添加事件处理器,响应用户操作如点击、滚动等。
  4. 属性和样式修改:可以读取和修改元素的属性和样式。
  5. 遍历和操作:可以遍历DOM树,创建、删除或修改节点。

BOM(浏览器对象模型)

BOM 是指浏览器提供的对象和属性的集合,它允许开发者与浏览器窗口进行交互。BOM 包括了与浏览器窗口、框架、书签、导航和浏览器的GUI元素相关的对象和方法。

BOM的主要特点包括:

  1. 窗口对象window 对象是BOM的核心,代表浏览器窗口,并提供与浏览器交互的方法和属性。
  2. 导航:提供与浏览器导航相关的功能,如window.locationwindow.history等。
  3. 屏幕和视口:提供关于用户屏幕和浏览器视口的信息,如window.screenwindow.innerWidth等。
  4. 定时器:允许使用setTimeoutsetInterval等函数设置定时任务。
  5. 对话框:提供显示消息框、警告框和确认框的方法,如window.alertwindow.confirm等。
  6. 浏览器扩展:提供与浏览器插件和扩展交互的接口。

区别

  • 作用域:DOM 专注于文档的结构和内容,而BOM 专注于浏览器窗口和用户界面。
  • 功能:DOM 提供了操作HTML和XML文档的方法,BOM 提供了与浏览器交互的方法。
  • 对象类型:DOM 主要处理文档节点和元素,BOM 处理的是浏览器功能和窗口对象。

# escape、encodeURI、encodeURIComponent 的区别是什么?

escapeencodeURIencodeURIComponent 是JavaScript中用于编码字符串的函数,它们各自有不同的用途和编码规则:

  1. escape()

    • escape() 函数会对字符串中的字符进行编码,以便将其用作URL的一部分。
    • 它编码的字符包括:"@""/""?""::""#""[""]"" "(空格转换为"+")、"<"">""%""."","";"":"")""!""(""$""'""*""+""=""&""_""-"`。
    • 需要注意的是,escape() 函数不是ECMAScript标准的一部分,且在现代浏览器中不推荐使用,它已经被encodeURIComponent所取代。
    escape("Hello, world!"); // "Hello%2C%20world%21"
    
    1
  2. encodeURI()

    • encodeURI() 函数用于编码完整的URI,它不会编码URI中本来就应该存在的特殊字符,如冒号(":")、正斜杠("/")、问号("?")和井号("#")等。
    • 它编码的字符主要是除了上述特殊字符之外的非字母数字字符。
    encodeURI("http://example.com/?query=hello world"); // "http://example.com/?query=hello%20world"
    
    1
  3. encodeURIComponent()

    • encodeURIComponent() 函数用于编码URI的组成部分,如查询字符串的键或值。
    • 它编码的字符包括所有非字母数字字符,除了"-""_"".""~",这些字符在URI中是安全的。
    • 这个函数非常适合用于编码URL的查询字符串参数。
    encodeURIComponent("Hello, world!"); // "Hello%2C%20world%21"
    encodeURIComponent("http://example.com/?query=hello world"); // "http%3A%2F%2Fexample.com%2F%3Fquery%3Dhello%20world"
    
    1
    2

区别总结:

  • escape() 是一个较老的函数,它编码的字符集比encodeURIComponent()更少,且不推荐在新的代码中使用。
  • encodeURI() 用于编码整个URI,不会编码URI中本来就应该存在的特殊字符。
  • encodeURIComponent() 用于编码URI的组成部分,如查询参数,编码的字符集更全面,包括空格(" ")被编码为"%20"

在实际开发中,通常推荐使用encodeURIComponent()来处理URL的参数编码,因为它能够确保参数值在URI中的正确传输。而encodeURI()则用于编码整个URI,当你需要确保URI的结构不被破坏时使用。

# 什么是 AJAX?如何实现一个 AJAX 请求?

AJAX(Asynchronous JavaScript and XML)是一种在无需重新加载整个页面的情况下,能够更新部分网页的技术。它通过在后台与服务器交换数据,然后使用JavaScript和DOM来更新部分网页内容,从而实现了网页的异步更新。

什么是AJAX?

AJAX的核心是XMLHttpRequest对象(在现代浏览器中也支持fetch API),它允许你:

  1. 从JavaScript代码中发起HTTP请求。
  2. 接收服务器响应。
  3. 在不刷新页面的情况下,使用响应数据更新网页。

如何实现一个AJAX请求?

使用 XMLHttpRequest 对象(传统方式):

// 创建一个新的 XMLHttpRequest 对象
var xhr = new XMLHttpRequest();

// 配置请求类型(GET 或 POST),URL 以及是否异步
xhr.open('GET', 'your-api-url', true);

// 设置请求完成的回调函数
xhr.onload = function () {
  if (xhr.status >= 200 && xhr.status < 300) {
    // 请求成功,可以使用响应数据 xhr.responseText
    console.log(xhr.responseText);
  } else {
    // 请求出错
    console.error(xhr.statusText);
  }
};

// 设置请求失败的回调函数
xhr.onerror = function () {
  console.error("请求失败");
};

// 发送请求
xhr.send();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

使用 fetch API(现代方式):

fetch API提供了一个更简洁和强大的方式来发起网络请求,它支持Promise,使得异步操作更加方便。

fetch('your-api-url')
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok ' + response.statusText);
    }
    return response.json(); // 如果响应内容是JSON
  })
  .then(data => {
    console.log(data); // 请求成功的处理逻辑
  })
  .catch(error => {
    console.error('There has been a problem with your fetch operation: ', error);
  });
1
2
3
4
5
6
7
8
9
10
11
12
13

AJAX的优势:

  1. 提高用户体验:通过异步更新,用户无需等待整个页面重新加载就能看到新数据。
  2. 减轻服务器负担:只需传输必要的数据,而不是整个页面。
  3. 前后端分离:AJAX促进了前后端分离的架构,提高了开发效率和可维护性。

注意事项:

  • 安全性:确保通过AJAX发送的数据是安全的,使用HTTPS,避免跨站请求伪造(CSRF)等安全问题。
  • 兼容性:虽然现代浏览器都支持XMLHttpRequestfetch,但在老旧浏览器中可能需要回退方案或使用polyfill。
  • 错误处理:合理处理网络错误和服务器返回的错误状态码。

# 常见的 DOM 操作有哪些?

DOM(文档对象模型)操作是Web开发中的核心任务之一,涉及对HTML文档的结构和内容进行修改。以下是一些常见的DOM操作:

  1. 创建元素

    • 使用 document.createElement() 方法创建新的HTML元素。
    var newDiv = document.createElement('div');
    
    1
  2. 获取元素

    • 使用 document.getElementById() 获取具有特定ID的元素。
    • 使用 document.getElementsByTagName() 获取具有特定标签名的所有元素。
    • 使用 document.getElementsByClassName() 获取具有特定类名的所有元素。
    • 使用 document.querySelector() 获取匹配特定CSS选择器的第一个元素。
    • 使用 document.querySelectorAll() 获取匹配特定CSS选择器的所有元素。
    var header = document.getElementById('header');
    var buttons = document.getElementsByTagName('button');
    var items = document.getElementsByClassName('item');
    var firstParagraph = document.querySelector('p');
    var allListItems = document.querySelectorAll('ul li');
    
    1
    2
    3
    4
    5
  3. 修改元素内容

    • 使用 element.innerHTML 属性获取或设置元素的HTML内容。
    • 使用 element.textContent 属性获取或设置元素的文本内容。
    var contentDiv = document.getElementById('content');
    contentDiv.innerHTML = '<strong>新内容</strong>';
    contentDiv.textContent = '新文本内容';
    
    1
    2
    3
  4. 修改元素属性

    • 直接通过属性访问或修改元素的属性。
    var link = document.getElementById('myLink');
    link.href = 'https://example.com';
    link.target = '_blank';
    
    1
    2
    3
  5. 添加、删除和替换元素

    • 使用 element.appendChild() 添加子元素。
    • 使用 element.removeChild() 删除子元素。
    • 使用 element.replaceChild() 替换子元素。
    var parentDiv = document.getElementById('parent');
    var newElement = document.createElement('p');
    parentDiv.appendChild(newElement);
    parentDiv.removeChild(newElement);
    var oldElement = parentDiv.firstChild;
    var newElement = document.createElement('p');
    parentDiv.replaceChild(newElement, oldElement);
    
    1
    2
    3
    4
    5
    6
    7
  6. 插入元素

    • 使用 element.insertBefore() 将新元素插入到现有元素之前。
    var parentDiv = document.getElementById('parent');
    var newElement = document.createElement('p');
    var referenceElement = parentDiv.firstChild;
    parentDiv.insertBefore(newElement, referenceElement);
    
    1
    2
    3
    4
  7. 克隆元素

    • 使用 element.cloneNode() 方法克隆一个元素。
    var originalElement = document.getElementById('original');
    var cloneElement = originalElement.cloneNode(true); // true表示深克隆,包括子节点
    
    1
    2
  8. 设置元素样式

    • 通过 element.style 属性直接设置样式。
    var myDiv = document.getElementById('myDiv');
    myDiv.style.color = 'red';
    myDiv.style.fontSize = '16px';
    
    1
    2
    3
  9. 监听事件

    • 使用 element.addEventListener() 方法为元素添加事件监听器。
    var button = document.getElementById('myButton');
    button.addEventListener('click', function() {
      alert('按钮被点击了!');
    });
    
    1
    2
    3
    4
  10. 移除事件监听器

    • 使用 element.removeEventListener() 方法移除事件监听器。
    button.removeEventListener('click', handleClick);
    
    1
  11. 获取和设置元素的尺寸和位置

    • 使用 element.offsetWidthelement.offsetHeight 获取元素的尺寸。
    • 使用 element.getBoundingClientRect() 获取元素相对于视口的位置。
    var myDiv = document.getElementById('myDiv');
    console.log(myDiv.offsetWidth);
    console.log(myDiv.offsetHeight);
    console.log(myDiv.getBoundingClientRect());
    
    1
    2
    3
    4

# use strict 是什么意思?使用它有什么区别?

use strict 是一种在 JavaScript 代码中使用的指令,它是一种声明,告诉 JavaScript 引擎以更严格的模式来运行代码。这个指令可以写在脚本的开始部分,或者函数的开始部分,以限制代码的行为,从而避免一些常见的错误。

什么是 use strict

  • 全局严格模式:当你在脚本的顶部写上 "use strict";,它会影响整个脚本文件。
  • 函数严格模式:当你在函数的开始写上 "use strict";,它只会影响那个特定的函数。

使用 use strict 的区别:

  1. 消除隐式全局变量:在非严格模式下,如果一个变量没有声明就直接赋值,它会成为全局变量。在严格模式下,这将抛出一个错误。
  2. 禁止使用 with 语句with 语句在严格模式下是不允许的,因为它会引入作用域链的混乱。
  3. 禁止重复的函数参数名:在严格模式下,函数参数名不能重复。
  4. 禁止八进制字面量:在非严格模式下,以 0 开头的数字会被解析为八进制数。在严格模式下,这将抛出一个错误。
  5. 捕获一些常见的类型错误:例如,尝试将一个非函数对象当作函数来调用时,会抛出错误。
  6. 禁止删除不可配置的属性:在非严格模式下,尝试删除一个不可配置的属性会静默失败。在严格模式下,这将抛出一个错误。
  7. 禁止对只读属性赋值:例如,尝试修改一个对象的只读属性会抛出错误。
  8. 禁止赋值给 undefined:在严格模式下,尝试将 undefined 赋值给一个变量会抛出错误。
  9. 禁止赋值给 null:在严格模式下,尝试将 null 赋值给一个变量会抛出错误。
  10. 禁止 evalarguments 的某些用法:在严格模式下,evalarguments 的一些用法会受到限制。

# JavaScript 如何判断一个对象是否属于某个类?

在 JavaScript 中,判断一个对象是否属于某个类(或构造函数)可以通过几种不同的方法来实现。以下是一些常用的方法:

  1. instanceof 操作符instanceof 是最常用的方法来检测一个对象是否是某个构造函数的实例。它不仅检查构造函数本身,还检查原型链。

    function MyClass() {}
    var obj = new MyClass();
    console.log(obj instanceof MyClass); // true
    
    1
    2
    3
  2. constructor 属性: 每个对象都有一个 constructor 属性,指向创建该对象的构造函数。但是,这个属性可以被改写,因此它不是最可靠的方法。

    function MyClass() {}
    var obj = new MyClass();
    console.log(obj.constructor === MyClass); // true
    
    1
    2
    3
  3. Object.prototype.toString 方法: 这个方法返回一个表示该对象类型的字符串。你可以用它来检查对象的确切类型。

    function MyClass() {}
    var obj = new MyClass();
    console.log(Object.prototype.toString.call(obj) === '[object MyClass]'); // true
    
    1
    2
    3
  4. typeof 操作符: 对于函数构造的实例,typeof 可以用来检测对象是否为某个特定的类型。

    function MyClass() {}
    var obj = new MyClass();
    console.log(typeof obj === 'object'); // true
    
    1
    2
    3
  5. Array.isArray() 方法: 如果你要检查的对象是数组,可以使用 Array.isArray() 方法。

    var arr = [];
    console.log(Array.isArray(arr)); // true
    
    1
    2
  6. 自定义类的 Symbol.hasInstance 方法: 在 ES2015 及更高版本中,你可以在类定义中使用 Symbol.hasInstance 方法来自定义 instanceof 的行为。

    class MyClass {
      [Symbol.hasInstance](instance) {
        return instance instanceof MyClass;
      }
    }
    var obj = new MyClass();
    console.log(obj instanceof MyClass); // true
    
    1
    2
    3
    4
    5
    6
    7

# ajax、axios、fetch 的区别是什么?

AJAXaxiosfetch 都是用于在浏览器中进行异步网络请求的技术,但它们在实现方式、使用场景和特性上有所不同。

  1. AJAX (Asynchronous JavaScript and XML)
    • AJAX 是一种在无需重新加载整个页面的情况下,能够更新部分网页的技术。
    • 它通常使用 XMLHttpRequest 对象来发送请求和接收响应。
    • AJAX 可以发送同步或异步请求,但在现代开发中,异步请求更为常见。
    • 它支持多种数据格式,包括 HTML、XML 和 JSON。
    • 由于 XMLHttpRequest 是较早的技术,它的 API 可能不如现代技术那样简洁。
  2. axios
    • axios 是一个基于 Promise 的 HTTP 客户端,用于浏览器和 node.js。
    • 它提供了一个更简洁和强大的 API 来发送异步请求。
    • axios 支持请求和响应的拦截、转换数据格式、自动转换 JSON 数据等。
    • 它返回的是一个 Promise 对象,使得异步处理更加方便。
    • axios 可以很容易地进行扩展和维护。
  3. Fetch API
    • fetch 是现代浏览器提供的一个原生 API,用于进行网络请求。
    • 它同样基于 Promise,提供了一个简洁的 API 来处理异步请求。
    • fetch 支持流式传输,可以处理大文件上传和下载。
    • 它默认只支持 JSON 格式的响应,如果需要其他格式,需要手动处理。
    • fetch 不支持 IE,如果需要在旧版浏览器上使用,可能需要 polyfill。

以下是一些它们之间的主要区别:

  • 兼容性
    • AJAX:通过 XMLHttpRequest 实现,几乎所有浏览器都支持。
    • axios:需要额外引入库,但支持现代浏览器和 node.js。
    • Fetch:现代浏览器支持,不兼容 IE,可能需要 polyfill。
  • 基于 Promise
    • AJAX:不直接基于 Promise,但可以通过回调函数和 Promise 封装来实现。
    • axios:完全基于 Promise。
    • Fetch:完全基于 Promise。
  • API 设计
    • AJAX:API 较为复杂,需要手动处理很多事情。
    • axios:提供了丰富的配置选项和简洁的 API。
    • Fetch:API 设计简洁,易于理解和使用。
  • 数据格式
    • AJAX:支持多种数据格式。
    • axios:支持多种数据格式,并且可以自定义转换函数。
    • Fetch:默认只支持 JSON,需要手动处理其他格式。
  • 使用场景
    • AJAX:适用于需要与旧系统或旧浏览器兼容的场景。
    • axios:适用于需要强大功能和灵活性的现代 Web 应用。
    • Fetch:适用于现代 Web 应用,特别是需要简洁 API 和基于 Promise 的异步处理的场景。

# JavaScript 数组的遍历方法有哪些?

在 JavaScript 中,有多种方法可以遍历数组。以下是一些常用的数组遍历方法:

  1. for 循环: 最基础的遍历方法,适用于所有浏览器。

    var array = [1, 2, 3, 4, 5];
    for (var i = 0; i < array.length; i++) {
      console.log(array[i]);
    }
    
    1
    2
    3
    4
  2. for...of 循环 (ES6+): ES6 引入的新语法,可以直接遍历数组中的元素。

    var array = [1, 2, 3, 4, 5];
    for (const value of array) {
      console.log(value);
    }
    
    1
    2
    3
    4
  3. forEach 方法: 数组的 forEach 方法对数组的每个元素执行一次提供的函数。

    var array = [1, 2, 3, 4, 5];
    array.forEach(function(value, index) {
      console.log(value);
    });
    
    1
    2
    3
    4
  4. map 方法map 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。

    var array = [1, 2, 3, 4, 5];
    var squared = array.map(function(value) {
      return value * value;
    });
    console.log(squared); // [1, 4, 9, 16, 25]
    
    1
    2
    3
    4
    5
  5. filter 方法filter 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。

    var array = [1, 2, 3, 4, 5];
    var evenNumbers = array.filter(function(value) {
      return value % 2 === 0;
    });
    console.log(evenNumbers); // [2, 4]
    
    1
    2
    3
    4
    5
  6. reduce 方法reduce 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。

    var array = [1, 2, 3, 4, 5];
    var sum = array.reduce(function(accumulator, value) {
      return accumulator + value;
    }, 0);
    console.log(sum); // 15
    
    1
    2
    3
    4
    5
  7. every 方法every 方法测试数组的所有元素是否都通过了由提供的函数实现的测试。

    var array = [1, 2, 3, 4, 5];
    var allAreNumbers = array.every(function(value) {
      return typeof value === 'number';
    });
    console.log(allAreNumbers); // true
    
    1
    2
    3
    4
    5
  8. some 方法some 方法测试数组中的某些元素是否至少通过了由提供的函数实现的测试。

    var array = [1, 2, 3, 4, 5];
    var hasNumbers = array.some(function(value) {
      return value > 10;
    });
    console.log(hasNumbers); // false
    
    1
    2
    3
    4
    5
  9. find 方法 (ES6+): find 方法返回数组中满足提供的测试函数的第一个元素的值。

    var array = [1, 2, 3, 4, 5];
    var found = array.find(function(value) {
      return value > 3;
    });
    console.log(found); // 4
    
    1
    2
    3
    4
    5
  10. findIndex 方法 (ES6+): findIndex 方法返回数组中满足提供的测试函数的第一个元素的索引。

    var array = [1, 2, 3, 4, 5];
    var index = array.findIndex(function(value) {
      return value > 3;
    });
    console.log(index); // 3
    
    1
    2
    3
    4
    5

# JavaScript 的 forEach 和 map 方法有什么区别?

JavaScript 中的 forEachmap 方法都是用于数组遍历的方法,但它们的目的和返回值有所不同。

forEach 方法

  • forEach 方法为数组中的每个元素执行一次提供的函数。
  • 它没有返回值,或者说它总是返回 undefined
  • 它不能被中断,即使在回调函数中抛出一个错误,forEach 也会继续执行数组的其余部分。
  • 它不创建新数组,而是直接在原数组上进行操作。
let numbers = [1, 2, 3, 4, 5];
numbers.forEach(function(number) {
  console.log(number); // 打印每个元素
});
// 没有返回值
1
2
3
4
5

map 方法

  • map 方法创建一个新数组,其结果是原数组中的每个元素调用提供的函数后的返回值。
  • 它返回一个新数组,原数组不会被修改。
  • 它也可以被中断,如果在回调函数中抛出错误,map 将停止执行。
  • 它常用于对数组中的每个元素进行某种操作,并返回一个包含操作结果的新数组。
let numbers = [1, 2, 3, 4, 5];
let squared = numbers.map(function(number) {
  return number * number; // 对每个元素进行平方操作
});
console.log(squared); // 输出: [1, 4, 9, 16, 25]
1
2
3
4
5

主要区别

  1. 返回值
    • forEach 不返回任何值。
    • map 返回一个新数组。
  2. 目的
    • forEach 通常用于执行副作用,如打印每个元素。
    • map 用于转换数组中的每个元素并返回一个新数组。
  3. 中断执行
    • forEach 即使在回调函数中抛出错误也会继续执行。
    • map 在回调函数中抛出错误时会停止执行。
  4. 修改原数组
    • forEach 不修改原数组。
    • map 不修改原数组,但返回一个新数组。
  5. 用途
    • forEach 用于遍历数组并执行操作,但不需要返回值。
    • map 用于遍历数组并返回一个新数组,其中包含原数组每个元素经过函数处理后的结果。

# mouseover 和 mouseenter 事件的区别是什么?

mouseovermouseenter 都是与鼠标指针移动相关的事件,但它们在触发行为和冒泡方式上有所不同:

  1. mouseover 事件
    • 当鼠标指针移动到元素上时触发。
    • 当鼠标指针移动到子元素上时,也会触发父元素的 mouseover 事件。
    • 它是一个冒泡事件,意味着如果鼠标指针移动到子元素上,父元素也会接收到事件。
    • 如果鼠标指针在元素的不同子元素之间移动,父元素会多次触发 mouseover 事件。
  2. mouseenter 事件
    • 当鼠标指针移动到元素上时触发,但与 mouseover 不同的是,即使鼠标指针移动到子元素上,也不会再次触发父元素的 mouseenter 事件。
    • 它不冒泡,即父元素不会因子元素的鼠标指针移动而触发 mouseenter 事件。
    • 一旦鼠标指针进入元素边界,无论在元素内部如何移动,父元素只会触发一次 mouseenter 事件。

示例说明:

假设有一个父元素 div 包含两个子元素 span

<div id="parent">
  <span>Child 1</span>
  <span>Child 2</span>
</div>
1
2
3
4
  • 使用 mouseover
    • 当鼠标指针从 Child 1 移动到 Child 2 时,parent 元素会触发两次 mouseover 事件(一次进入 Child 1,一次进入 Child 2)。
  • 使用 mouseenter
    • 当鼠标指针从 Child 1 移动到 Child 2 时,parent 元素只会触发一次 mouseenter 事件(当指针首次进入 parent 边界时)。

代码示例:

// 为父元素添加 mouseover 事件监听器
document.getElementById('parent').addEventListener('mouseover', function() {
  console.log('mouseover on parent');
});

// 为父元素添加 mouseenter 事件监听器
document.getElementById('parent').addEventListener('mouseenter', function() {
  console.log('mouseenter on parent');
});
1
2
3
4
5
6
7
8
9
  • 当使用 mouseover 时,无论鼠标指针在 parent 或其子元素之间如何移动,都会在每次移动时触发事件。
  • 当使用 mouseenter 时,只有在鼠标指针首次进入 parent 边界时触发一次事件,之后无论在内部如何移动,都不会再次触发。

总结:

  • mouseover:冒泡,多次触发。
  • mouseenter:不冒泡,只触发一次。

# JavaScript 的 == 和 === 有什么区别?

在 JavaScript 中,===== 是用于比较两个值的运算符,但它们在比较时的行为有所不同:

  1. ==(等于运算符)

    • == 是所谓的“宽松相等”(loose equality)运算符。
    • 当使用 == 进行比较时,如果两个值的类型不同,JavaScript 会尝试将它们转换为相同的类型,然后进行比较。
    • 这意味着即使两个值在不同的类型,它们也可能被认为是相等的,如果它们的转换后的值相等。
    3 == '3'  // true,因为字符串 '3' 被转换成了数字 3
    0 == false  // true,因为 false 被转换成了数字 0
    ' ' == 0  // true,因为空字符串被转换成了数字 0
    
    1
    2
    3
  2. ===(严格等于运算符)

    • === 是所谓的“严格相等”(strict equality)运算符。
    • 当使用 === 进行比较时,如果两个值的类型不同,比较的结果将直接是 false,不会进行类型转换。
    • 这意味着只有当两个值的类型和值都相同时,=== 的比较结果才会是 true
    3 === '3'  // false,因为一个是数字,一个是字符串
    0 === false  // false,因为一个是数字,一个是布尔值
    ' ' === 0  // false,因为一个是字符串,一个是数字
    
    1
    2
    3

推荐使用 ===

在 JavaScript 编程中,通常推荐使用 === 而不是 ==,因为 === 提供了更明确和可预测的比较行为,减少了由于隐式类型转换导致的潜在错误。使用 === 可以避免许多常见的陷阱,使得代码更加健壮和易于维护。

例外情况

尽管 === 是首选,但在某些特定情况下,使用 == 可能是有用的,尤其是在你需要进行类型转换的时候。例如,当你想要检查一个变量是否为 nullundefined 时,使用 == 可以同时检查两者:

var a = null;
if (a == null) {
  // 当 a 为 null 或 undefined 时,这个条件为 true
}
1
2
3
4

这是因为 nullundefined 与彼此使用 == 比较时被认为是相等的,但它们与任何其他值(包括它们自己的类型)使用 == 比较时都被认为是不相等的。不过,即使在这种情况下,使用 ===Object.is 函数结合也是一个更安全的选择:

var a = null;
if (Object.is(a, null)) {
  // 只有当 a 为 null 时,这个条件为 true
}
1
2
3
4

Object.is 会检查两个值是否为同一个值,对于 nullundefined 的比较,它提供了与 == 相同的结果,但不会进行隐式的类型转换。

# JavaScript 中 substring 和 substr 函数的区别是什么

在 JavaScript 中,substring()substr() 方法都可以用来从字符串中提取子字符串,但它们在参数和一些行为上有所不同。

substring() 方法

  • substring() 方法返回一个字符串中从指定位置开始到另一个指定位置结束的子字符串。
  • 它接受两个参数:起始索引和结束索引。
  • 起始索引是包含在内的,而结束索引是不包含的。
  • 如果结束索引小于起始索引,这两个参数的位置会自动交换。
let str = 'Hello, world!';
let sub = str.substring(0, 5); // 返回 'Hello'
1
2

substr() 方法

  • substr() 方法返回一个字符串中从指定位置开始的指定数量的字符。
  • 它接受两个参数:起始索引和要返回的字符数。
  • 起始索引是包含在内的。
  • 如果省略第二个参数,substr() 会从起始索引开始返回剩余的所有字符。
let str = 'Hello, world!';
let sub = str.substr(7, 5); // 返回 'world'
1
2

主要区别

  1. 参数
    • substring() 需要两个参数:起始索引和结束索引。
    • substr() 需要一个或两个参数:起始索引和(可选的)字符数。
  2. 行为
    • substring() 在处理负数或超出字符串长度的索引时表现更一致,它会自动调整这些值。
    • substr() 在处理负数索引时可能会返回空字符串,或者在某些浏览器中返回整个字符串。
  3. 兼容性
    • substring() 是 ECMAScript 标准的一部分,所有现代浏览器都支持。
    • substr() 在一些老版本的浏览器中可能不被支持,尽管它在现代浏览器中通常也是可用的。
  4. 推荐使用
    • 由于 substring() 的行为更加一致和可预测,通常推荐使用 substring() 而不是 substr()

示例

  • 使用 substring()
let str = 'Hello, world!';
let sub1 = str.substring(7); // 'world!'
let sub2 = str.substring(0, 5); // 'Hello'
let sub3 = str.substring(10, 25); // '!',因为结束索引超出了字符串长度
let sub4 = str.substring(-3, 2); // 'He',负数索引会被调整为 0
1
2
3
4
5
  • 使用 substr()
let str = 'Hello, world!';
let sub1 = str.substr(7); // 'world!'
let sub2 = str.substr(0, 5); // 'Hello'
let sub3 = str.substr(10, 15); // '!',因为第二个参数指定了要返回的字符数
let sub4 = str.substr(-3, 2); // '' 或 'lo',取决于浏览器,负数索引的行为不一致
1
2
3
4
5

总的来说,substring() 是更标准和更安全的选择,而 substr() 虽然在大多数现代浏览器中可用,但在一些情况下可能表现不一致。

# JavaScript 数组的 map 和 forEach 函数中能否通过 break 等语法结束循环?

在 JavaScript 中,mapforEach 函数用于遍历数组,但它们在处理循环控制语句时的行为有所不同。

map 函数

  • map 函数不会返回一个可以被 breakcontinue 语句中断的循环。
  • 它总是遍历数组的每个元素,并为每个元素调用一次提供的函数。
  • 即使在回调函数中使用 return 语句,它也只会结束当前的回调函数调用,而不是整个 map 操作。

forEach 函数

  • forEach 函数同样不会返回一个可以被 breakcontinue 语句中断的循环。
  • 它也会遍历数组的每个元素,并为每个元素调用一次提供的函数。
  • map 类似,即使在回调函数中使用 return 语句,它也只会结束当前的回调函数调用,而不是整个 forEach 操作。

示例

  • 使用 map
let numbers = [1, 2, 3, 4, 5];
let doubled = numbers.map(function(number) {
  if (number === 3) {
    return; // 这只会结束当前的回调函数调用,不会中断 map 操作
  }
  return number * 2;
});

console.log(doubled); // 输出: [2, 4, 6, 8, 10]
1
2
3
4
5
6
7
8
9
  • 使用 forEach
let numbers = [1, 2, 3, 4, 5];
numbers.forEach(function(number) {
  if (number === 3) {
    return; // 这只会结束当前的回调函数调用,不会中断 forEach 操作
  }
  console.log(number);
});

// 即使在条件满足时使用了 return,forEach 操作仍然会继续执行
1
2
3
4
5
6
7
8
9

如何中断循环

如果你需要在满足特定条件时中断遍历,可以考虑使用传统的 for 循环或 for...of 循环,这些循环可以使用 break 语句来中断。

  • 使用 for...of 循环:
let numbers = [1, 2, 3, 4, 5];
for (const number of numbers) {
  if (number === 3) {
    break; // 这会中断整个循环
  }
  console.log(number);
}

// 输出:
// 1
// 2
1
2
3
4
5
6
7
8
9
10
11

总结

  • mapforEach 函数中的 return 语句不会中断整个遍历操作,只会结束当前的回调函数调用。
  • 如果需要在满足特定条件时中断遍历,可以使用 for 循环或 for...of 循环,并使用 break 语句。

# JavaScript 中如何合并对象?

在 JavaScript 中,合并对象可以通过多种方式实现,以下是一些常用的方法:

1. 使用 Object.assign()

Object.assign() 方法可以用来复制一个或多个源对象的所有可枚举的自身属性到目标对象中,然后返回目标对象。这个方法会覆盖目标对象中与源对象属性名相同的属性。

const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const mergedObj = Object.assign({}, obj1, obj2);
console.log(mergedObj); // { a: 1, b: 3, c: 4 }
1
2
3
4

2. 使用 Spread 操作符 (...)

ES6 引入的 Spread 操作符可以用来在字面量对象中合并多个对象。

const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const mergedObj = { ...obj1, ...obj2 };
console.log(mergedObj); // { a: 1, b: 3, c: 4 }
1
2
3
4

3. 使用 for...in 循环

可以使用传统的 for...in 循环遍历对象的属性并将它们添加到新对象中。

const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const mergedObj = {};

for (const key in obj1) {
  mergedObj[key] = obj1[key];
}

for (const key in obj2) {
  mergedObj[key] = obj2[key];
}

console.log(mergedObj); // { a: 1, b: 3, c: 4 }
1
2
3
4
5
6
7
8
9
10
11
12
13

4. 使用递归合并 (深合并)

如果你需要进行深合并(即合并嵌套对象),你可以使用递归方法来实现。

function deepMerge(target, ...sources) {
  if (!sources.length) return target;
  const source = sources.shift();

  if (isObject(target) && isObject(source)) {
    for (const key in source) {
      if (isObject(source[key])) {
        if (!target[key]) Object.assign(target, { [key]: {} });
        deepMerge(target[key], source[key]);
      } else {
        Object.assign(target, { [key]: source[key] });
      }
    }
  }

  return deepMerge(target, ...sources);
}

const obj1 = { a: 1, b: { c: 3 } };
const obj2 = { b: { c: 4, d: 5 }, e: 6 };
const mergedObj = deepMerge({}, obj1, obj2);
console.log(mergedObj); // { a: 1, b: { c: 4, d: 5 }, e: 6 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

5. 使用 Lodash 库

如果你在项目中使用了 Lodash 库,可以使用 _.merge() 方法来合并对象。

const _ = require('lodash');

const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const mergedObj = _.merge({}, obj1, obj2);
console.log(mergedObj); // { a: 1, b: 3, c: 4 }
1
2
3
4
5
6

总结

  • Object.assign() 和 Spread 操作符是最简单的浅合并方法。
  • 如果需要深合并,可以使用递归方法或 Lodash 库的 _.merge() 方法。
  • 根据你的具体需求选择合适的方法来合并对象。

# JavaScript 如何判断一个对象是不是空对象?

在 JavaScript 中,判断一个对象是否为空对象(即没有任何可枚举的自身属性)可以通过几种不同的方法来实现。以下是一些常用的方法:

  1. 使用 Object.keys()

Object.keys() 方法会返回一个由给定对象的自身可枚举属性组成的数组。如果这个数组的长度为 0,那么对象就是空的。

function isEmptyObject(obj) {
  return Object.keys(obj).length === 0;
}

const emptyObj = {};
const nonEmptyObj = { a: 1 };

console.log(isEmptyObject(emptyObj)); // true
console.log(isEmptyObject(nonEmptyObj)); // false
1
2
3
4
5
6
7
8
9

2. 使用 for...in 循环

for...in 循环可以遍历对象的所有可枚举属性(包括原型链上的属性)。如果循环没有执行,说明对象没有可枚举的自身属性。

function isEmptyObject(obj) {
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      return false;
    }
  }
  return true;
}

const emptyObj = {};
const nonEmptyObj = { a: 1 };

console.log(isEmptyObject(emptyObj)); // true
console.log(isEmptyObject(nonEmptyObj)); // false
1
2
3
4
5
6
7
8
9
10
11
12
13
14

3. 使用 JSON.stringify()

将对象转换为 JSON 字符串,如果字符串是 {}, 则说明对象是空的。这种方法简单但效率较低,因为它涉及到字符串操作。

function isEmptyObject(obj) {
  return JSON.stringify(obj) === '{}';
}

const emptyObj = {};
const nonEmptyObj = { a: 1 };

console.log(isEmptyObject(emptyObj)); // true
console.log(isEmptyObject(nonEmptyObj)); // false
1
2
3
4
5
6
7
8
9

4. 使用 Object.entries()

Object.entries() 方法返回一个给定对象自身可枚举属性的键值对数组。如果这个数组的长度为 0,那么对象就是空的。

function isEmptyObject(obj) {
  return Object.entries(obj).length === 0;
}

const emptyObj = {};
const nonEmptyObj = { a: 1 };

console.log(isEmptyObject(emptyObj)); // true
console.log(isEmptyObject(nonEmptyObj)); // false
1
2
3
4
5
6
7
8
9

5. 使用 Reflect.ownKeys()

Reflect.ownKeys() 方法返回一个由目标对象自身的属性键组成的数组。这包括不可枚举的属性和符号属性。

function isEmptyObject(obj) {
  return Reflect.ownKeys(obj).length === 0;
}

const emptyObj = {};
const nonEmptyObj = { a: 1 };

console.log(isEmptyObject(emptyObj)); // true
console.log(isEmptyObject(nonEmptyObj)); // false
1
2
3
4
5
6
7
8
9

总结

  • Object.keys()Object.entries() 是最常用的方法,因为它们直接返回对象自身的可枚举属性。
  • for...in 循环 可以检查所有可枚举属性,包括原型链上的属性。
  • JSON.stringify()Reflect.ownKeys() 是其他可选方法,但它们可能在某些情况下效率较低或不适用。

# JavaScript 的 splice 和 slice 函数会改变原数组吗?

在 JavaScript 中,splice()slice() 是两个用于操作数组的常用方法,但它们对原数组的影响是不同的。

splice() 方法

  • splice() 方法可以用于添加、删除或替换数组中的元素。
  • 它直接修改原数组。
  • splice() 可以接收多个参数,其中前两个参数是必需的:开始操作的索引和删除的元素数量。
  • 如果添加了新元素,这些新元素会被插入到数组中,替换或添加到被删除的元素的位置。

示例

let fruits = ["Apple", "Banana", "Mango", "Orange"];
fruits.splice(1, 1); // 删除索引为1的元素,即 "Banana"
console.log(fruits); // ["Apple", "Mango", "Orange"]

fruits.splice(1, 0, "Banana"); // 在索引为1的位置添加 "Banana"
console.log(fruits); // ["Apple", "Banana", "Mango", "Orange"]
1
2
3
4
5
6

slice() 方法

  • slice() 方法用于提取原数组的一部分,并返回一个新数组,原数组不会被修改。
  • 它接受两个参数:开始索引和结束索引(不包含)。
  • 返回的新数组包含了从开始索引到结束索引之间的所有元素。

示例

let fruits = ["Apple", "Banana", "Mango", "Orange"];
let fruitsSlice = fruits.slice(1, 3);
console.log(fruitsSlice); // ["Banana", "Mango"]
console.log(fruits); // ["Apple", "Banana", "Mango", "Orange"],原数组未被修改
1
2
3
4

总结

  • splice() 会改变原数组,因为它是用来直接在数组上进行添加、删除或替换操作的。
  • slice() 不会改变原数组,它只是用来提取原数组的一部分并返回一个新数组。

# JavaScript 中怎么删除数组最后一个元素?

在 JavaScript 中,删除数组最后一个元素通常使用 pop() 方法。这个方法会移除数组的最后一个元素,并返回被移除的元素。如果数组为空,pop() 不会改变数组,并返回 undefined

使用 pop() 方法

let fruits = ["Apple", "Banana", "Mango", "Orange"];
let lastElement = fruits.pop(); // 移除并返回数组最后一个元素 "Orange"
console.log(fruits); // ["Apple", "Banana", "Mango"]
console.log(lastElement); // "Orange"
1
2
3
4

其他方法

除了 pop(),还有其他几种方法可以用来删除数组的最后一个元素,尽管它们不是专门为此目的设计的:

  1. 使用 splice() 方法splice() 方法可以用于添加、删除或替换数组中的元素。要删除最后一个元素,可以指定从最后一个元素开始删除一个元素。

    let fruits = ["Apple", "Banana", "Mango", "Orange"];
    fruits.splice(-1, 1); // 从最后一个元素开始删除一个元素
    console.log(fruits); // ["Apple", "Banana", "Mango"]
    
    1
    2
    3
  2. 使用 length 属性: 通过将 length 属性设置为比当前长度小 1,可以移除数组的最后一个元素。

    let fruits = ["Apple", "Banana", "Mango", "Orange"];
    fruits.length = fruits.length - 1; // 将数组长度减 1
    console.log(fruits); // ["Apple", "Banana", "Mango"]
    
    1
    2
    3

总结

  • pop() 是最简单和最直接的方法来删除数组的最后一个元素。
  • splice() 提供了更多的灵活性,可以用于更复杂的删除操作。
  • 修改 length 属性 是一种直接但较少使用的方法,它通过改变数组的长度来移除最后一个元素。

# 如何判断网页元素是否到达可视区域?

在 JavaScript 中,判断一个元素是否到达可视区域可以通过以下几种方法实现:

  1. 使用 getBoundingClientRect 方法: 这个方法返回一个 DOMRect 对象,包含元素的大小以及其相对于视口的位置。可以通过比较 toprightbottomleft 属性与视口的宽度和高度来判断元素是否在可视区域内。

    function isInViewPort(element) {
      const viewWidth = window.innerWidth || document.documentElement.clientWidth;
      const viewHeight = window.innerHeight || document.documentElement.clientHeight;
      const { top, right, bottom, left } = element.getBoundingClientRect();
      return (
        top >= 0 &&
        left >= 0 &&
        right <= viewWidth &&
        bottom <= viewHeight
      );
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  2. 使用 Intersection Observer API: 这是一个现代浏览器提供的 API,专门用于异步观察目标元素与祖先元素或顶级文档视窗(viewport)的交叉状态。当目标元素进入或离开视窗时,Intersection Observer 可以自动触发回调函数。

    let observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          console.log('元素在视口中');
        } else {
          console.log('元素不在视口中');
        }
      });
    });
    
    let element = document.querySelector('#myElement'); // 替换为你的元素选择器
    observer.observe(element);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  3. 使用 scroll 事件和 offsetTop 属性: 通过监听 scroll 事件,结合元素的 offsetTop 属性和文档的 scrollTop 属性来判断元素是否在可视区域内。

    window.addEventListener('scroll', function() {
      let element = document.querySelector('#myElement');
      let elementTop = element.offsetTop;
      let scrollTop = window.pageYOffset || document.documentElement.scrollTop;
      let visible = elementTop >= scrollTop &&
                     elementTop < (scrollTop + window.innerHeight);
      if (visible) {
        console.log('元素在视口中');
      } else {
        console.log('元素不在视口中');
      }
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

以上方法中,Intersection Observer API 是最现代和高效的方式,它不会导致性能问题,因为它不需要频繁地轮询元素的位置。而 getBoundingClientRectscroll 事件结合使用的方法可能会因为频繁的计算而导致性能问题,特别是在滚动时。

# JavaScript 操作数组元素的方法有哪些?

JavaScript 中操作数组元素的方法非常多,这些方法可以帮助你修改数组、访问数组元素、搜索数组元素、合并数组等。以下是一些常用的数组操作方法:

修改数组

  1. push():向数组的末尾添加一个或多个元素,并返回新的长度。
  2. pop():移除数组的最后一个元素,并返回被移除的元素。
  3. unshift():在数组的开头添加一个或多个元素,并返回新的长度。
  4. shift():移除数组的第一个元素,并返回被移除的元素。
  5. splice():添加、删除或替换数组中的元素。
  6. fill():用一个固定值填充数组中从起始索引到终止索引内的所有元素(终止索引不包括)。

访问数组元素

  1. indexOf():返回数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。
  2. lastIndexOf():返回数组中可以找到一个给定元素的最后一个索引,如果不存在,则返回-1。
  3. includes() (ES7):用来判断数组是否包含某个元素,如果是返回 true,否则 false

搜索数组元素

  1. find() (ES6):返回数组中满足提供的测试函数的第一个元素的值。如果没有符合条件的元素则返回 undefined
  2. findIndex() (ES6):返回数组中满足提供的测试函数的第一个元素的索引。如果没有符合条件的元素则返回-1。
  3. filter():创建一个新数组,包含通过所提供函数实现的测试的所有元素。
  4. map():创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。

合并数组

  1. concat():用于合并两个或多个数组。此方法不会改变现有数组,而是返回一个新数组。
  2. slice():返回一个新数组对象,该对象是一个由 begin 到 end(不包括 end)的原数组的浅拷贝。

排序数组

  1. sort():对数组的元素进行排序,并返回对数组的引用。
  2. reverse():颠倒数组中元素的顺序。

迭代数组

  1. forEach():对数组的每个元素执行一次提供的函数。
  2. every():检测数组中所有元素是否都通过了测试函数的测试。
  3. some():检测数组中是否至少有一个元素通过了测试函数的测试。

归并数组

  1. reduce():对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
  2. reduceRight():同 reduce(),但是以倒序执行。

# JavaScript 中 for...in 和 for...of 的区别是什么?

for...infor...of 是 JavaScript 中用于遍历迭代的两种不同的循环结构,它们在用法和用途上有一些区别:

for...in 循环

  • for...in 循环用于遍历一个对象的所有可枚举属性(包括原型链上的属性)。
  • 它适用于遍历对象的键(key),不适用于数组或类数组对象的遍历。
  • for...in 循环会枚举对象的所有可枚举的属性,包括它从原型继承的属性。

示例

const obj = { a: 1, b: 2 };
for (const key in obj) {
  if (obj.hasOwnProperty(key)) { // 检查属性是否是对象自身的属性,而不是原型上的
    console.log(key); // 'a', 'b'
  }
}
1
2
3
4
5
6

**for...of 循环**

  • for...of 循环是 ES6 引入的,用于遍历可迭代对象(如数组、字符串、Map、Set 等)的值。
  • 它适用于遍历数组或任何符合迭代协议(有 [Symbol.iterator] 属性)的对象。
  • for...of 循环不会遍历对象的原型链上的属性。

示例

const array = [3, 5, 7];
for (const value of array) {
  console.log(value); // 3, 5, 7
}

const str = 'hello';
for (const char of str) {
  console.log(char); // 'h', 'e', 'l', 'l', 'o'
}
1
2
3
4
5
6
7
8
9

主要区别

  1. 遍历对象 vs 遍历可迭代对象
    • for...in 用于遍历对象的键。
    • for...of 用于遍历可迭代对象的值。
  2. 原型链
    • for...in 会遍历对象自身的属性以及它从原型链上继承的可枚举属性。
    • for...of 不会遍历原型链,只遍历当前对象的属性。
  3. 适用性
    • for...in 更适合于需要获取对象键的场景。
    • for...of 更适合于需要获取数组或字符串等可迭代对象的值的场景。
  4. 语法
    • for...in 使用 in 关键字。
    • for...of 使用 of 关键字。

# JavaScript 如何使用 for...of 遍历对象?

在 JavaScript 中,for...of 循环通常用于遍历数组或其他可迭代对象(如字符串、Map、Set 等)。然而,直接使用 for...of 来遍历一个普通对象的属性是不可行的,因为普通对象不是可迭代的。

不过,你可以通过几种方法来使用 for...of 遍历对象的属性:

1. 使用 Object.keys()

Object.keys() 方法会返回一个包含对象所有自身可枚举属性名的数组,这个数组是可迭代的。

const obj = { a: 1, b: 2, c: 3 };
for (const key of Object.keys(obj)) {
  console.log(key, obj[key]); // 'a' 1, 'b' 2, 'c' 3
}
1
2
3
4

2. 使用 Object.values()

Object.values() 方法会返回一个包含对象所有自身可枚举属性值的数组。

const obj = { a: 1, b: 2, c: 3 };
for (const value of Object.values(obj)) {
  console.log(value); // 1, 2, 3
}
1
2
3
4

3. 使用 Object.entries()

Object.entries() 方法会返回一个给定对象自身可枚举属性的键值对数组,这个数组也是可迭代的。

const obj = { a: 1, b: 2, c: 3 };
for (const [key, value] of Object.entries(obj)) {
  console.log(key, value); // 'a' 1, 'b' 2, 'c' 3
}
1
2
3
4

4. 使用 Reflect.ownKeys()

Reflect.ownKeys() 方法返回一个由目标对象自身的属性键组成的数组,包括不可枚举的属性和符号属性。

const obj = { a: 1, b: 2, c: 3 };
for (const key of Reflect.ownKeys(obj)) {
  console.log(key, obj[key]); // 'a' 1, 'b' 2, 'c' 3
}
1
2
3
4

# const 对象的属性可以修改吗?

在 JavaScript 中,使用 const 声明的变量是用来保存一个不可变的引用,而不是使引用的对象本身不可变。这意味着你不能重新分配 const 声明的变量,但是你可以修改该变量引用的对象中的属性。

例如:

const obj = { a: 1, b: 2 };

// 尝试重新赋值会报错
// obj = { c: 3 }; // TypeError: Assignment to constant variable.

// 修改对象的属性是允许的
obj.a = 4;
obj.b = 5;
obj.c = 6; // 添加新属性也是允许的

console.log(obj); // { a: 4, b: 5, c: 6 }
1
2
3
4
5
6
7
8
9
10
11

在这个例子中,对象 obj 是通过 const 声明的,你不能将 obj 重新指向另一个对象,但是你可以修改 obj 的属性(包括添加新属性)。

总结如下:

  1. 不可重新赋值const 关键字声明的变量不能被重新赋值。
  2. 可修改属性:可以修改 const 变量引用的对象的属性。
  3. 可添加属性:可以向 const 变量引用的对象添加新属性。
  4. 可删除属性:可以删除 const 变量引用的对象的属性(使用 delete 操作符)。

这种行为适用于数组和其他可变数据结构,因为数组也是对象的一种类型。例如:

const arr = [1, 2, 3];

// 尝试重新赋值会报错
// arr = [4, 5, 6]; // TypeError: Assignment to constant variable.

// 修改数组的内容是允许的
arr[0] = 10;
arr.push(4); // 添加元素

console.log(arr); // [10, 2, 3, 4]
1
2
3
4
5
6
7
8
9
10

因此,使用 const 时,重要的是要理解它是保护变量不被重新赋值,而不是保护变量指向的数据结构不被修改。

# 说说你对 fetch 的理解,它有哪些优点和不足?

fetch 是一个现代 JavaScript API,用于在浏览器中进行 HTTP 请求。它返回一个 Promise,使得异步请求处理更加简洁和强大。fetch 可以用于获取资源,或者向服务器发送请求并接收响应。

优点:

  1. 基于 Promisefetch 返回一个 Promise 对象,使得异步操作更加方便,可以避免回调地狱,并且易于链式调用。
  2. 支持 ES6+fetch 是 ES6 的一部分,因此它支持现代 JavaScript 开发。
  3. 流式传输fetch 支持流式传输,这意味着可以处理大量数据而不需要等待整个数据加载完成。
  4. 支持 JSONfetch 内置了对 JSON 的支持,可以直接将响应转换为 JSON 对象。
  5. 灵活的请求和响应fetch 允许你自定义请求方法(GET、POST、PUT 等)、请求头、请求体以及响应类型。
  6. 原生支持:大多数现代浏览器都原生支持 fetch,不需要额外的库或工具。
  7. 易于使用fetch 的 API 设计简洁,易于理解和使用。

不足:

  1. 不兼容旧浏览器fetch 不支持 IE 浏览器,如果需要在旧版浏览器上使用,可能需要引入 polyfill。
  2. 默认不支持 cookies:在跨域请求中,默认情况下 fetch 不会发送或接收 cookies。如果需要,必须在请求中明确设置 credentials: 'include'
  3. 不自动转换 JSON:虽然 fetch 支持 JSON,但它不会自动将响应转换为 JSON 对象,需要手动调用 json() 方法。
  4. 没有全局错误处理fetch 不会抛出网络错误(例如断网),除非网络错误导致请求无法发出。如果请求成功发出但服务器响应错误(如 404 或 500),fetch 仍然会将响应解析为成功的 Promise,需要手动检查响应状态码。
  5. 请求方法大小写敏感fetch 对请求方法的大小写敏感,例如 GETget 被视为不同的方法。
  6. 不支持超时处理fetch 本身不支持超时设置,需要通过 Promise 的 race 方法或第三方库来实现。
  7. 复杂的请求和响应选项:虽然 fetch 允许自定义请求和响应,但这些选项可能比某些其他库(如 Axios)更复杂和繁琐。

# JavaScript 中 Object.keys 的返回值是无序的吗?

Object.keys() 在 JavaScript 中返回的是一个包含对象自身可枚举属性名的数组,这个数组是有序的。它按照创建属性时的顺序返回属性名,但是注意,这个顺序可能因不同的 JavaScript 引擎而异。

在 ECMAScript 2015(ES6)之前,对象属性的顺序并不是标准化的,不同的 JavaScript 引擎可能会有不同的实现。但是,从 ES6 开始,对象属性的枚举顺序被明确定义为按照属性被添加到对象中的顺序进行。

例如:

const obj = {
  a: 1,
  b: 2,
  c: 3
};

// ES6 及更高版本的 JavaScript 引擎会按照属性添加的顺序返回
console.log(Object.keys(obj)); // ["a", "b", "c"]
1
2
3
4
5
6
7
8

在上面的例子中,Object.keys(obj) 将返回一个数组 ["a", "b", "c"],这与属性被添加到对象中的顺序相匹配。

需要注意的是,Object.keys() 只考虑对象自身的可枚举属性,不会包含继承的属性,也不会包含不可枚举的属性。如果需要考虑不可枚举的属性,可以使用 Object.getOwnPropertyNames() 方法。如果需要包括符号作为键的属性,可以使用 Object.getOwnPropertySymbols() 方法。

# JavaScript 的 BigInt 和 Number 类型有什么区别?

JavaScript 中的 Number 类型和 BigInt 类型都用于表示数值,但它们之间有几个关键的区别:

  1. 表示范围
    • Number 类型是基于 IEEE 754 标准的双精度 64 位二进制格式,这意味着它能够精确表示的整数范围是从 -2^53 + 1 到 2^53 - 1(即 -9,007,199,254,740,991 到 9,007,199,254,740,991),对于大于这个范围的整数,JavaScript 无法精确表示。
    • BigInt 类型则可以表示任意大的整数。它是为了解决 Number 类型在表示大整数时的精度问题而引入的。
  2. 语法
    • Number 类型的字面量直接书写数字即可,例如:1234.56
    • BigInt 类型的字面量需要在数字后面添加 n 后缀,例如:123n456n
  3. 精度
    • Number 类型在极端的大数值或小数值时可能会失去精度,因为它使用的是浮点数表示法。
    • BigInt 类型提供了精确的整数算术,无论数值多大。
  4. 运算
    • 当使用 BigInt 类型进行运算时,必须确保所有参与运算的数都是 BigInt,否则会触发类型转换,将 BigInt 转换为 Number,这可能会导致精度丢失或错误。
    • Number 类型的运算则遵循 IEEE 754 标准。
  5. 内置函数和方法
    • 一些内置的数学函数和数组方法可能不直接支持 BigInt 类型,因为它们原本设计时只考虑了 Number 类型。
  6. JSON 和数据存储
    • BigInt 类型的值不能直接存储为 JSON 格式,因为 JSON 规范只支持数字类型,而不支持 BigInt。在需要将 BigInt 转换为 JSON 时,通常需要将其转换为字符串。
    • 在数据库或其他数据存储系统中,对 BigInt 的支持也取决于具体的存储系统。
  7. 兼容性
    • BigInt 是一个相对较新的 JavaScript 特性,可能在一些旧的 JavaScript 环境中不被支持。

使用 BigInt 的示例:

let bigNumber = 123456789012345678901234567890n;
let anotherBigNumber = 987654321098765432109876543210n;
let sum = bigNumber + anotherBigNumber; // 也是一个 BigInt
console.log(sum); // 输出: 123456789012345678901234567890n + 987654321098765432109876543210n
1
2
3
4

在处理非常大的整数时,BigInt 提供了一种有效且安全的方法,但需要注意它与 Number 类型的不同之处。

# 什么是 JavaScript 的尾调用?使用尾调用有什么好处?

在 JavaScript 中,尾调用(Tail Call)是指在函数的最后一步调用另一个函数的情况。也就是说,在函数执行过程中,最后执行的是一个函数调用,且这个调用没有其他的表达式或语句在它之后执行。

尾调用的例子:

function add(a, b) {
  return b === 0 ? a : add(a + 1, b - 1);
}

console.log(add(5, 0)); // 5
1
2
3
4
5

在这个例子中,add 函数在最后一步调用了自己。如果 b 不等于 0,它会递归地调用 add 函数,这是尾调用,因为这是函数执行的最后操作。

尾调用优化(Tail Call Optimization, TCO)

尾调用优化是一种编译技术,它允许编译器或者解释器优化尾调用,避免增加新的调用堆栈。在没有尾调用优化的情况下,每次函数调用都会在调用堆栈上增加一个新的条目。如果递归调用非常深,这可能会导致堆栈溢出。尾调用优化通过重用当前函数的堆栈帧来调用下一个函数,从而避免了这个问题。

使用尾调用的好处:

  1. 避免堆栈溢出:尾调用优化可以显著减少内存消耗,因为它避免了为每个递归调用创建新的堆栈帧。
  2. 提高性能:由于减少了堆栈的使用,尾调用优化可以提高递归函数的性能。
  3. 支持无限递归:在理论上,使用尾调用优化的递归函数可以无限递归,只要有足够的内存,而不会因为堆栈溢出而失败。

# JavaScript 为什么要进行变量提升?它导致了什么问题?

在 JavaScript 中,变量提升(Hoisting)是指变量和函数声明在代码执行前被提升到当前作用域的顶部的过程。这是 JavaScript 引擎在代码执行前进行的一个编译阶段的行为。

为什么要进行变量提升?

变量提升的存在主要是由于 JavaScript 引擎的运行机制。在代码执行之前,JavaScript 引擎会进行两个重要的步骤:

  1. 创建阶段:在这个阶段,内存中会为变量和函数声明创建空间。变量会被初始化为 undefined(对于 letconst),或者直接赋予初始值(对于 var)。
  2. 执行阶段:在这个阶段,代码从上到下依次执行。在执行过程中,变量和函数的值会被实际分配。

变量提升允许 JavaScript 引擎在代码执行前就了解所有变量和函数的存在,这样在执行代码时就可以确保对任何变量和函数的引用都是有效的。

变量提升导致的问题:

  1. 覆盖变量:由于 var 声明的变量会被提升并初始化为 undefined,如果在同一个作用域内有多个相同的 var 声明,后面的声明可能会覆盖前面的声明。

    var x = 5;
    var x = 10;
    console.log(x); // 输出 10
    
    1
    2
    3
  2. 暂时性死区:对于 letconst,它们在被声明之前是不可访问的,这被称为暂时性死区。如果在声明之前尝试访问这些变量,会导致运行时错误。

    // 尝试在声明之前访问 let 变量
    console.log(x); // ReferenceError: x is not defined
    let x = 5;
    
    1
    2
    3
  3. 函数表达式提升:只有函数声明会被提升,函数表达式不会。这可能会导致一些混淆。

    var myFunction = function() {
      console.log("Hello");
    };
    myFunction(); // 正常工作,因为函数声明被提升
    
    var anotherFunction = function() {
      console.log("Hello");
    };
    anotherFunction(); // 不会工作,因为 anotherFunction 是一个未被提升的赋值表达式
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  4. 全局变量:如果在一个函数内部使用 var 声明变量而没有指定 var,这个变量会被错误地提升为全局变量。

    function test() {
      x = 5;
    }
    test();
    console.log(x); // 输出 5,因为 x 被错误地提升为全局变量
    
    1
    2
    3
    4
    5

# 使用 let 全局声明变量,能通过 window 对象取到吗?

在 JavaScript 中,使用 letconst 声明的变量具有块级作用域,而不是全局作用域。这意味着它们不会被添加到全局对象(如 window 在浏览器环境中或 global 在 Node.js 环境中)上。

当你在全局作用域(例如,在任何函数之外)使用 letconst 声明变量时,这个变量只能在它被声明的代码块内被访问。你不能通过 window 对象直接访问这些变量。

例如:

let globalVar = 'I am a global variable';

// 这会在浏览器中输出 undefined,因为在全局作用域中使用 let 声明的变量不会成为 window 对象的属性
console.log(window.globalVar); // undefined
console.log(globalVar); // 'I am a global variable'
1
2
3
4
5

然而,如果你使用 var 声明全局变量,那么这个变量将会成为 window 对象的一个属性,可以通过 window 对象访问:

var globalVar = 'I am a global variable';

// 使用 var 声明的全局变量可以通过 window 对象访问
console.log(window.globalVar); // 'I am a global variable'
1
2
3
4

这是一个重要的差异,因为 var 声明的全局变量可能会导致意外的全局污染,而 letconst 提供了更严格的作用域控制,有助于避免这种情况。

总结:

  • 使用 letconst 在全局作用域声明的变量不会成为 window 对象的属性。
  • 使用 var 在全局作用域声明的变量会成为 window 对象的属性。
  • 推荐使用 letconst 来声明变量,以利用它们提供的块级作用域特性。

# let、const 和 var 的区别是什么?

letconstvar 是 JavaScript 中用于声明变量的三个关键字,它们之间有几个关键的区别:

  1. 作用域
    • var 声明的变量具有函数作用域,如果在函数外部使用 var 声明变量,它将具有全局作用域。
    • letconst 声明的变量具有块级作用域,这意味着它们只在包含它们的代码块(如 if 语句、for 循环、函数等)内部可见。
  2. 变量提升
    • var 声明的变量会被提升到它们所在作用域的顶部,但它们的赋值不会提升。
    • letconst 声明的变量也会被提升,但是它们处于“暂时性死区”(Temporal Dead Zone, TDZ)中,直到它们被初始化。在声明之前访问它们会导致一个 ReferenceError
  3. 重复声明
    • var 允许在相同作用域内多次声明同一个变量。
    • letconst 不允许在相同作用域内重复声明同一个变量,尝试这样做会抛出一个 SyntaxError
  4. 赋值和重新赋值
    • varlet 声明的变量可以被重新赋值。
    • const 声明的变量必须在声明时初始化,且之后不能被重新赋值。这意味着 const 绑定的是不可变的,但如果你将对象或数组赋值给 const 变量,对象或数组的内容是可以改变的。
  5. 全局对象属性
    • 使用 var 声明的全局变量会成为全局对象(在浏览器中是 window 对象)的属性。
    • 使用 letconst 声明的全局变量不会成为全局对象的属性。
  6. 循环中的使用
    • for 循环中,使用 var 声明的变量在循环外也是可访问的。
    • 使用 letconstfor 循环中声明的变量只能在循环内部访问。

以下是一些示例来说明这些区别:

// 函数作用域 vs 块级作用域
function testVar() {
  var varVariable = 'I am a var variable';
  let letVariable = 'I am a let variable';
  const constVariable = 'I am a const variable';

  if (true) {
    var varVariable = 'Redeclaration of var';
    let letVariable = 'Redeclaration of let'; // SyntaxError
    const constVariable = 'Redeclaration of const'; // SyntaxError
  }
}

// 变量提升
console.log(typeof varVariable); // 'function'
console.log(typeof letVariable); // 'undefined'
console.log(typeof constVariable); // 'undefined'

// 重复声明
var variable = 'I am a var variable';
var variable = 'Redeclaration of var';

let anotherVariable = 'I am a let variable';
let anotherVariable = 'Redeclaration of let'; // SyntaxError

// 全局对象属性
var globalVar = 'I am a global var';
console.log(window.globalVar); // 'I am a global var'
let globalLetVar = 'I am a global let var';
console.log(window.globalLetVar); // undefined
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

# 说说你对 JS 作用域的理解?

在 JavaScript 中,作用域(Scope)是指变量、函数以及其他类型在代码中可见的区域。作用域决定了如何访问和如何查找变量。JavaScript 有两种主要的作用域类型:全局作用域和局部作用域。

全局作用域

全局作用域是在整个脚本中都可以访问的作用域。在全局作用域中声明的变量和函数可以在脚本的任何地方被访问。全局变量是作为 window 对象的属性存在的(在浏览器环境中)。

var globalVar = 'I am global';
console.log(globalVar); // 可以在这里访问
function test() {
  console.log(globalVar); // 也可以在这里访问
}
1
2
3
4
5

局部作用域

局部作用域是在一个函数内部创建的作用域。在这个作用域中声明的变量和函数只能在该函数内部被访问。JavaScript 的局部作用域是通过函数创建的,而不是通过块(如 if 语句或 for 循环)创建的,直到 ES6 引入了 letconst 声明。

function myFunction() {
  var localVar = 'I am local';
  console.log(localObject); // 可以在这里访问
}
console.log(localObject); // ReferenceError: localObject is not defined
myFunction();
1
2
3
4
5
6

块级作用域

ES6 引入了 letconst 关键字,它们提供了块级作用域,这意味着变量的作用域被限制在了它们被声明的代码块内,如 if 语句、for 循环、while 循环等。

if (true) {
  let blockScopedVar = 'I am block scoped';
  console.log(blockScopedVar); // 可以在这里访问
}
console.log(blockScopedVar); // ReferenceError: blockScopedVar is not defined
1
2
3
4
5

作用域链

当在某个作用域内访问一个变量时,JavaScript 引擎会首先在当前作用域查找该变量。如果在当前作用域找不到,它会向上查找父作用域,这个过程会一直持续到全局作用域。如果全局作用域也没有找到该变量,那么就会抛出一个 ReferenceError 错误。这个从当前作用域到全局作用域的链被称为作用域链。

闭包

闭包是一个函数和其周围的状态(词法环境)的组合。闭包允许函数访问其定义时的作用域链,即使在其定义的作用域外执行。这使得闭包可以记忆并访问它们创建时的作用域中的变量,即使那个作用域已经消失。

function createFunction() {
  var message = "Hello";
  return function() {
    console.log(message);
  };
}

var myFunction = createFunction();
myFunction(); // 输出 "Hello",即使 createFunction 已经执行完毕
1
2
3
4
5
6
7
8
9

在这个例子中,myFunction 是一个闭包,它记住了 createFunction 函数内的作用域中的 message 变量。

# 什么是 JavaScript 的临时性死区?

在 JavaScript 中,临时性死区(Temporal Dead Zone,简称 TDZ)是指变量在声明之前到声明时的一段代码区域,在这段时间内,尝试访问变量会导致错误。这个概念是在 ES6 引入 letconst 声明关键字时出现的,目的是为了解决变量提升(hoisting)可能带来的问题。

变量提升的问题

在 ES6 之前,使用 var 关键字声明的变量会被提升到它们所在作用域的顶部,但是它们的赋值不会提升。这意味着在变量声明之前,你就可以访问这些变量,它们会被初始化为 undefined。这可能会导致一些难以理解的代码行为。

临时性死区的引入

ES6 引入了 letconst,它们提供了块级作用域,而不是 var 的函数作用域。与 var 不同,letconst 声明的变量不会在代码执行前被提升。相反,它们会在代码块的开始处被创建,但在声明之前,它们处于 TDZ 中。

临时性死区的行为

  • 在变量声明之前,尝试访问 letconst 声明的变量会导致 ReferenceError
  • 在变量声明之后,你可以正常访问这些变量。
  • 如果在变量声明之前对变量进行赋值,也会抛出错误,因为赋值操作实际上试图在 TDZ 中访问变量。

示例

console.log(x); // ReferenceError: x is not defined (在 let 声明之前访问)
let x = 5;
console.log(x); // 5 (在 let 声明之后访问)

if (true) {
  console.log(y); // ReferenceError: y is not defined (在 let 声明之前访问)
  let y = 10;
  console.log(y); // 10 (在 let 声明之后访问)
}

for (let i = 0; i < 5; i++) {
  console.log(i); // 0, 1, 2, 3, 4 (在循环内访问)
}
console.log(i); // ReferenceError: i is not defined (在循环外访问)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

为什么需要临时性死区

TDZ 的引入主要是为了解决以下问题:

  • 避免在代码块开始执行前就访问变量,这有助于编写更清晰和更可预测的代码。
  • 避免由于变量提升导致的意外行为,特别是在循环和条件语句中。
  • 确保变量的声明和初始化顺序与代码中的顺序一致,这有助于开发者更好地理解和维护代码。

# JavaScript 事件冒泡和捕获的区别是什么?默认是冒泡还是捕获?

事件冒泡和事件捕获是事件传播的两个阶段。当一个元素触发了某个事件,这个事件会经历两个阶段:事件捕获阶段和事件冒泡阶段。

事件捕获(Event Capturing)

事件捕获是从顶层节点(如 document)开始,逐步向下传播到目标节点的过程。在这个阶段,事件从父节点向子节点传递,直到到达事件的实际目标节点。捕获阶段允许父节点在事件到达目标节点之前先对事件进行处理。

事件冒泡(Event Bubbling)

事件冒泡是事件从目标节点开始,逐步向上传播到顶层节点的过程。在这个阶段,事件从子节点向父节点传递,直到到达 document 对象。冒泡阶段允许父节点在事件处理完成后对事件进行进一步的处理。

区别

  • 传播方向:事件捕获是从外向内,事件冒泡是从内向外。
  • 处理时机:捕获阶段在事件到达目标元素之前处理,冒泡阶段在事件离开目标元素之后处理。
  • 使用场景:捕获阶段常用于需要在事件到达目标元素之前进行处理的场景,如表单验证;冒泡阶段常用于需要在事件处理完成后进行额外处理的场景,如事件委托。

默认行为

在 DOM 事件模型中,默认的事件流包括三个阶段:事件捕获、目标元素事件处理、事件冒泡。但是,大多数现代浏览器的默认行为是事件冒泡。这意味着如果你没有特别指定事件的捕获,那么事件处理程序将监听冒泡阶段。

如何指定捕获

在现代浏览器中,你可以通过在事件处理函数中设置 useCapture 标志为 true 来指定事件捕获。例如:

element.addEventListener('click', function(event) {
  // 事件处理代码
}, true); // 第三个参数为 true 表示使用事件捕获
1
2
3

事件委托

事件委托是一种利用事件冒泡原理的技术,通过在父元素上设置一个事件处理程序来管理子元素的事件。这样做的好处是可以减少事件处理程序的数量,提高性能,并且可以动态管理子元素的事件。

示例

// 冒泡阶段的事件处理
document.getElementById('parent').addEventListener('click', function(event) {
  console.log('Parent bubble');
});

// 捕获阶段的事件处理
document.getElementById('child').addEventListener('click', function(event) {
  console.log('Child capture');
}, true);

// 目标元素的事件处理
document.getElementById('child').addEventListener('click', function(event) {
  console.log('Child target');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在这个例子中,如果 child 元素被点击,事件处理程序的调用顺序将取决于浏览器的实现和事件类型的默认行为。对于大多数现代浏览器,如果事件默认行为是冒泡,那么调用顺序可能是:Child capture -> Child target -> Parent bubble

# 什么是 JavaScript 的事件代理?

事件代理(Event Delegation)是一种利用事件冒泡原理的技术,它意味着你可以在父元素上设置一个事件处理程序,来监听在它的子元素上发生的事件。这种方法特别适用于处理多个子元素的事件,而这些子元素可能在页面加载时还不存在(例如,通过动态交互添加到 DOM 中的元素)。

事件代理的工作原理

当一个事件在子元素上触发时,它会冒泡到父元素,直到达到根节点。在冒泡过程中,父元素上的事件处理程序可以捕获这个事件,并根据事件的目标(event.target)来判断触发事件的确切子元素。

事件代理的优点

  1. 减少事件处理程序的数量:你不需要为每个子元素单独设置事件处理程序,这可以减少内存消耗和提高性能。
  2. 动态元素管理:对于动态添加到 DOM 的元素,你不需要为它们单独绑定事件处理程序,因为它们的事件会冒泡到父元素。
  3. 简化代码:你可以用一个通用的处理程序来管理多个子元素的事件,这使得代码更加简洁和易于维护。

事件代理的示例

假设你有一个列表,列表项是动态生成的,你想要为每个列表项添加点击事件:

<ul id="myList">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <!-- 更多列表项 -->
</ul>
1
2
3
4
5
6

而不是为每个 <li> 元素添加事件监听器,你可以这样做:

document.getElementById('myList').addEventListener('click', function(event) {
  if (event.target.tagName === 'LI') {
    console.log('You clicked on:', event.target);
  }
});
1
2
3
4
5

在这个例子中,无论列表项如何变化,你只需要在 #myList 元素上设置一个事件处理程序。当用户点击任何 <li> 元素时,事件会冒泡到 #myList,然后事件处理程序会检查事件的目标是否为 <li> 元素。

注意事项

  • 事件代理可能会稍微增加事件处理程序的复杂性,因为你需要在处理程序中判断事件的实际目标。
  • 有些事件不冒泡,例如 focusblurloadunload 等,这些事件不能使用事件代理。

# 什么是 JavaScript 的事件流?

事件流是指事件从发生到被处理的过程中所经历的路径。在 JavaScript 中,事件流描述了事件在 DOM(文档对象模型)中的传播方式,主要包括三个阶段:事件捕获阶段、目标阶段和事件冒泡阶段。

事件捕获阶段(Event Capturing Phase)

在这个阶段,事件从文档的根节点(document 对象)开始,逐步向下传播到目标元素。这个过程允许在事件到达目标元素之前捕获它。在这个阶段,你可以在父元素上设置事件处理程序,以便在事件到达子元素之前进行处理。

目标阶段(Target Phase)

当事件到达目标元素时,就进入了目标阶段。在这个阶段,事件会在目标元素上触发,执行绑定在目标元素上的事件处理程序。

事件冒泡阶段(Bubbling Phase)

事件冒泡是事件从目标元素向上传播到文档根节点的过程。在这个阶段,事件处理程序可以在事件离开目标元素后进行处理。冒泡允许父元素在事件处理完成后对事件进行进一步的处理。

事件流的示例

假设你有一个 HTML 结构如下:

<div>
  <p>Click me!</p>
</div>
1
2
3

如果用户点击了 <p> 元素,事件流将如下:

  1. 事件捕获阶段:事件从 document 开始向下传播,经过 div 元素,直到 <p> 元素。如果在 divdocument 上设置了捕获阶段的事件处理程序,它们将在这里被调用。
  2. 目标阶段:事件到达 <p> 元素,执行绑定在 <p> 元素上的事件处理程序。
  3. 事件冒泡阶段:事件从 <p> 元素向上冒泡,经过 div 元素,直到 document。如果在 divdocument 上设置了冒泡阶段的事件处理程序,它们将在这里被调用。

事件处理的两种模式

  1. 冒泡型事件(Bubbling Events):大多数 DOM 事件都是冒泡事件,如 clickmousedownkeyup 等。这些事件遵循完整的事件流,即从捕获阶段到目标阶段,再到冒泡阶段。
  2. 非冒泡型事件(Non-bubbling Events):有些事件不会冒泡,如 focusblurloadunload 等。这些事件只会在目标阶段触发。

事件流的控制

  • 事件冒泡:可以通过 event.stopPropagation() 方法阻止事件继续冒泡。
  • 事件捕获:可以通过 event.stopImmediatePropagation() 方法阻止同一元素上的其他事件处理程序被调用。
  • 默认行为:可以通过 event.preventDefault() 方法阻止事件的默认行为。

# 什么是 JavaScript 的事件轮询机制?

事件轮询机制(Event Loop)是 JavaScript 运行时的一种机制,它允许 JavaScript 引擎在单线程环境中处理异步操作。事件轮询是 JavaScript 非阻塞 I/O 和异步编程的核心概念。

事件轮询的工作原理

JavaScript 引擎使用一个主线程来执行代码,这个线程会按照顺序执行任务队列(也称为消息队列)中的回调函数。当主线程空闲时,它会检查任务队列,如果队列中有待处理的任务,它就会执行这些任务。这个过程会不断重复,直到任务队列为空。

事件轮询的阶段

  1. 调用阶段:执行同步代码,如函数调用、表达式求值等。
  2. 事件处理阶段:执行与特定事件(如用户点击、网络响应等)相关的回调函数。
  3. 回调阶段:执行异步操作完成后的回调函数,如定时器、网络请求、文件读写等。
  4. 渲染阶段:浏览器会将计算结果渲染到屏幕上。

事件轮询的示例

假设你有一个定时器和一个异步操作(如 setTimeoutPromise):

console.log('Script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('Script end');
1
2
3
4
5
6
7
8
9
10
11
12
13

在这个例子中,事件轮询可能会按照以下顺序执行:

  1. 执行同步代码:Script startScript end
  2. 检查任务队列,执行 Promise 的第一个回调:promise1
  3. 再次检查任务队列,执行 Promise 的第二个回调:promise2
  4. 检查定时器,如果定时器到期,将 setTimeout 的回调添加到任务队列。
  5. 再次检查任务队列,执行 setTimeout 的回调:setTimeout

事件轮询的重要性

事件轮询机制使得 JavaScript 能够在不阻塞主线程的情况下处理大量异步操作。这对于创建高性能、响应迅速的 Web 应用程序至关重要。

微任务和宏任务

在 JavaScript 中,任务队列进一步分为微任务(microtasks)和宏任务(macrotasks):

  • 微任务:包括 Promise 回调、MutationObserver 等。
  • 宏任务:包括 setTimeoutsetIntervalrequestAnimationFrame、I/O、UI 渲染等。

当主线程执行完当前的同步代码后,它会首先清空微任务队列,然后才处理宏任务队列。这意味着微任务会优先于宏任务执行。

# 什么是 JavaScript 的原型链?

原型链是一种实现继承和属性共享的机制。每个 JavaScript 对象都有一个内部属性,称为 [[Prototype]](在代码中通常通过 __proto__ 属性或 Object.getPrototypeOf() 方法来访问),它指向该对象的原型。原型本身也是一个对象,这意味着每个对象都可能有自己的原型,形成了一个链式结构,这就是所谓的原型链。

原型链的工作原理

当你尝试访问一个对象的属性或方法时,JavaScript 引擎首先会在该对象本身查找。如果找不到,它会沿着原型链向上查找,直到找到属性或到达原型链的末端。原型链的末端通常是 Object.prototype,其原型是 null

原型链的例子

function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}.`);
};

const person1 = new Person('Alice');
person1.sayHello(); // 输出:Hello, my name is Alice.

// 原型链示例
console.log(person1.hasOwnProperty('name')); // true,因为 'name' 是 person1 的属性
console.log(person1.hasOwnProperty('sayHello')); // false,因为 'sayHello' 是原型上的属性

// 查找 'sayHello' 方法的过程:
// 1. person1 对象上没有找到 'sayHello'。
// 2. 查找 person1 的原型(即 Person.prototype)。
// 3. 在 Person.prototype 上找到 'sayHello'。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

在这个例子中,person1 对象没有自己的 sayHello 方法,但是通过原型链,它继承了 Person.prototype 上定义的 sayHello 方法。

原型链的重要性

  1. 继承:原型链使得对象可以继承另一个对象的属性和方法。
  2. 属性查找:JavaScript 引擎使用原型链来查找对象的属性和方法。
  3. 性能优化:原型链允许多个对象共享同一个方法,减少了内存消耗。

原型链的注意事项

  1. 原型污染:如果原型对象被不当修改,可能会影响所有继承该原型的对象。
  2. 继承的属性不可枚举:通过原型链继承的属性不会出现在 for-in 循环中,但可以通过 Object.getOwnPropertyNames() 方法获取。
  3. 构造函数指向:对象的 constructor 属性默认指向其构造函数,但这个属性可以通过原型链被改变。

原型链和类的关系

在 ES6 中,JavaScript 引入了 class 关键字,它提供了一种更接近传统面向对象编程语言的语法来定义类和实例。然而,底层仍然是基于原型链的。

class Person {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

const person1 = new Person('Alice');
person1.sayHello(); // 输出:Hello, my name is Alice.

// 类的原型链
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在这个例子中,Person 类的实例 person1 通过原型链继承了 Person.prototype 上定义的 sayHello 方法。

# 什么是 JavaScript 的原型修改、原型重写?

JavaScript 的原型修改和原型重写是两种不同的操作,它们都涉及到原型对象的变更,但是方式和影响有所不同。

原型修改

原型修改是指在现有的原型对象上添加新的属性或方法。这种方式不会改变原型对象本身,而是在原型对象上扩展新的功能。例如,给 Person 构造函数的原型添加一个 getName 方法:

function Person(name) {
  this.name = name;
}
// 修改原型
Person.prototype.getName = function() {
  console.log(this.name);
};
1
2
3
4
5
6
7

在这种方式下,所有已经创建的 Person 实例都能访问到新添加的方法,因为它们共享同一个原型对象。

原型重写

原型重写是指用一个新的对象来替换现有的原型对象。这通常通过直接将一个新的对象赋值给构造函数的 prototype 属性来实现。例如:

Person.prototype = {
  getName: function() {
    console.log(this.name);
  },
  newName: "Alice"
};
1
2
3
4
5
6

这种方式会替换掉原有的原型对象,所有新的 Person 实例将继承这个新的原型对象。但是,已经创建的实例不会自动更新它们的原型指针,它们仍然指向重写前的原型对象。

修改和重写原型的影响

  • 原型修改:对现有原型对象的修改会影响到所有已经存在的实例,因为它们共享同一个原型。
  • 原型重写:重写原型会影响到重写之后创建的所有新实例。重写原型时,如果忘记将 constructor 属性指向原来的构造函数,那么新实例的 constructor 将指向 Object,因为新对象默认由 Object 构造。

原型链指向

在 JavaScript 中,每个对象都有一个原型,这个原型本身也是一个对象,这样就形成了一个链式结构,即原型链。当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到匹配的属性或方法,或者到达原型链的末端(Object.prototype),最终到达 null

# JavaScript 的原型链指向什么?

每个对象都有一个特殊的隐藏属性 [[Prototype]](在代码中通常通过 __proto__ 属性或 Object.getPrototypeOf() 方法来访问),这个属性指向该对象的原型。原型链就是由这些原型属性构成的链式结构。

当你尝试访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或方法或者到达原型链的末端。原型链的末端通常是 Object.prototype,它的原型是 null

原型链的指向如下:

  1. 自定义对象的原型:当你创建一个自定义对象时,这个对象的原型通常会指向 Object.prototype

    const myObject = {};
    console.log(Object.getPrototypeOf(myObject) === Object.prototype); // true
    
    1
    2
  2. 构造函数创建的对象:如果你使用构造函数创建了一个对象,这个对象的原型会指向该构造函数的 prototype 属性。

    function Person(name) {
      this.name = name;
    }
    const person = new Person('Alice');
    console.log(Object.getPrototypeOf(person) === Person.prototype); // true
    
    1
    2
    3
    4
    5
  3. 内置对象的原型:内置对象(如 ArrayFunctionDate 等)也有自己的原型,它们通常会指向 Object.prototype,但会提供一些额外的方法和属性。

    const myArray = [];
    console.log(Object.getPrototypeOf(myArray) === Array.prototype); // true
    
    1
    2
  4. 原型链的顶端:所有的原型链最终都会指向 Object.prototype,它的原型是 null

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

原型链的重要性

原型链是 JavaScript 中实现继承和属性查找的核心机制。它允许对象继承和共享属性和方法,从而减少内存消耗并提高性能。通过原型链,对象可以访问在它们自身上没有定义的属性和方法,这些属性和方法被定义在它们的原型或原型的原型上。

原型链的示例

假设你有以下对象和原型:

function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}.`);
};

const person1 = new Person('Alice');
1
2
3
4
5
6
7
8
9

在这个例子中,person1 对象的原型链如下:

person1 ---> Person.prototype ---> Object.prototype ---> null
1

当你调用 person1.sayHello() 时,JavaScript 引擎会在 person1 上查找 sayHello 方法,找不到后会沿着原型链向上查找,在 Person.prototype 上找到该方法并执行。

# JavaScript 原型链的终点是什么?如何打印出原型链的终点?

原型链的终点是 null。每个对象的原型链最终都会指向 null,这意味着当查找一个属性或方法时,如果沿着原型链一直上溯都没有找到,那么就会到达终点 null,此时查找操作将停止,并返回 undefined

打印出原型链的终点

要打印出原型链的终点,你可以使用 Object.getPrototypeOf() 方法来不断获取一个对象的原型,直到原型为 null。这里是一个示例代码:

let obj = { a: 1 };

// 不断获取原型,直到原型为 null
while (Object.getPrototypeOf(obj) !== null) {
  obj = Object.getPrototypeOf(obj);
}

console.log(obj); // 输出:null
1
2
3
4
5
6
7
8

在这个例子中,我们从一个自定义对象开始,不断获取它的原型,直到原型为 null。每次循环,我们都将 obj 更新为它的原型,直到 Object.getPrototypeOf(obj) 返回 null

理解原型链的终点

原型链的终点是 null,这是因为 JavaScript 需要一个明确的标志来表示属性或方法查找的结束。在原型链的每个环节,如果当前对象没有找到所需的属性或方法,就会去它的原型对象中查找。如果原型对象也没有,就继续去原型的原型查找,直到达到 null,这意味着没有更多的原型可以查找了。

为什么终点是 null 而不是其他值?

选择 null 作为终点是因为 null 在 JavaScript 中是一个特殊的值,它表示“没有对象”。在原型链中使用 null 作为终点可以清晰地表示“没有更多原型可以查找”的状态。此外,null 在 JavaScript 中是一个常见的值,它不会与其他对象的属性或方法冲突。

# JavaScript 如何获得对象非原型链上的属性?

要获取对象自身的属性(即非原型链上的属性),可以使用以下几种方法:

1. hasOwnProperty 方法

hasOwnProperty 是 Object 的一个内置方法,用于检查对象上是否直接含有指定的属性,而不查找原型链。

const obj = {
  a: 1,
  b: 2
};

console.log(obj.hasOwnProperty('a')); // true
console.log(obj.hasOwnProperty('b')); // true
console.log(obj.hasOwnProperty('toString')); // false,toString 是原型链上的属性
1
2
3
4
5
6
7
8

2. Object.hasOwn 方法(ES2022)

Object.hasOwn 是 ES2022 新增的一个方法,它提供了与 hasOwnProperty 类似的功能,但是它是一个静态方法,可以直接使用,不需要调用对象实例。

const obj = {
  a: 1,
  b: 2
};

console.log(Object.hasOwn(obj, 'a')); // true
console.log(Object.hasOwn(obj, 'b')); // true
console.log(Object.hasOwn(obj, 'toString')); // false
1
2
3
4
5
6
7
8

3. 使用 Object.getOwnPropertyNames 方法

Object.getOwnPropertyNames 方法返回一个包含对象所有自有属性(无论是否可枚举)的数组,不包括原型链上的属性。

const obj = {
  a: 1,
  b: 2
};

Object.getOwnPropertyNames(obj); // ["a", "b"]
1
2
3
4
5
6

4. 使用 Object.keys 方法

Object.keys 方法返回一个包含对象所有可枚举自有属性的数组,不包括原型链上的属性。

const obj = {
  a: 1,
  b: 2
};

Object.keys(obj); // ["a", "b"]
1
2
3
4
5
6

5. 使用 Object.getOwnPropertyDescriptor 方法

Object.getOwnPropertyDescriptor 方法可以用来获取对象指定属性的描述符,如果该属性是自有属性,则返回其描述符,否则返回 undefined

const obj = {
  a: 1,
  b: 2
};

console.log(Object.getOwnPropertyDescriptor(obj, 'a')); // {value: 1, writable: true, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(obj, 'toString')); // undefined,因为 toString 是原型链上的属性
1
2
3
4
5
6
7

# 什么是 JavaScript 的闭包?有什么作用和使用场景?

闭包(Closure)是指一个函数能够访问其定义时的作用域链,即使在其定义的作用域外执行。这意味着闭包可以记忆并访问它们创建时的作用域中的变量,即使那个作用域已经消失。

闭包的工作原理

当一个函数在另一个函数的内部定义时,内部函数可以访问外部函数的变量。当外部函数执行完毕后,其作用域链并不立即消失,而是被内部函数“记住”。因此,即使内部函数在外部函数之外被调用,它仍然可以访问外部函数的变量。

闭包的作用

  1. 数据封装和私有变量:闭包可以用来创建私有变量,这些变量对外部函数不可见,从而实现数据的封装和隐藏。
  2. 创建函数模板:闭包可以用来创建具有不同行为的函数,这些函数可以操作同一组变量。
  3. 延长变量的生命周期:在某些情况下,闭包可以用来延长变量的生命周期,防止变量被垃圾回收。

闭包的使用场景

  1. 模块化:通过闭包实现模块化编程,每个模块可以有自己的私有变量和公共接口。
  2. 事件处理器:在事件驱动编程中,闭包可以用来存储事件处理器的状态。
  3. 循环和计数器:闭包可以用来在循环中创建计数器,每个计数器都有自己的独立状态。
  4. 函数工厂:闭包可以用来创建具有特定功能的函数,这些函数可以操作特定的数据集。

闭包的示例

function createCounter() {
  let count = 0;
  return function() {
    count += 1;
    console.log(count);
  };
}

const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3

// 每次调用 counter 时,它都能记住上一次的 count 值
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在这个例子中,createCounter 函数创建了一个闭包。闭包能够访问并修改 createCounter 函数内的 count 变量。即使 createCounter 函数已经执行完毕,闭包仍然可以访问并修改 count 变量的值。

注意事项

  • 内存泄漏:由于闭包会保持其作用域链,这可能会导致内存泄漏,特别是当闭包在大量短生命周期的元素(如 DOM 元素)上使用时。
  • 性能考虑:创建过多的闭包可能会影响性能,因为它们会占用更多的内存。

# 说说 JavaScript 闭包的实现原理?

JavaScript 闭包的实现原理与 JavaScript 的作用域链和垃圾回收机制密切相关。以下是闭包实现的几个关键点:

1. 作用域链

在 JavaScript 中,函数创建时会生成一个作用域链,这个作用域链包含了函数本身的作用域以及它能够访问的外部作用域。当函数执行时,这个作用域链会被用来查找变量。即使函数执行完毕后,如果函数的引用仍然存在,它的作用域链也会被保留。

2. 函数是一等公民

在 JavaScript 中,函数是一等公民(first-class citizen),这意味着函数可以作为变量存储,作为参数传递,也可以作为其他函数的返回结果。因此,函数可以捕获定义它们时的作用域链。

3. 变量的生命周期

在 JavaScript 中,变量的生命周期通常由其作用域决定。局部变量在函数执行完毕后通常会被销毁,但如果一个函数返回了一个内部函数(这个内部函数形成了闭包),那么内部函数可以访问外部函数的局部变量,即使外部函数已经执行完毕。

4. 垃圾回收

JavaScript 使用垃圾回收机制来管理内存。当一个对象没有任何引用指向它时,它将成为垃圾回收的候选对象。然而,如果一个闭包持续引用一个局部变量,那么这个局部变量将不会被垃圾回收,因为它仍然是一个活跃的引用。

闭包实现原理的示例

function outerFunction() {
  var outerVariable = 'I am outer';

  function innerFunction() {
    console.log(outerVariable);
  }

  return innerFunction;
}

var theInnerFunction = outerFunction(); // 这里返回的是 innerFunction 函数的引用
theInnerFunction(); // 输出:I am outer
1
2
3
4
5
6
7
8
9
10
11
12

在这个例子中,innerFunction 函数形成了一个闭包。它被 outerFunction 返回,并且即使 outerFunction 已经执行完毕,innerFunction 仍然可以访问 outerVariable。这是因为 innerFunction 捕获了 outerFunction 的作用域链。

# 说说你对 JavaScript 作用域、作用域链的理解?

在 JavaScript 中,作用域(Scope)是指变量、函数以及其他表达式在代码中可见的区域。作用域决定了如何访问和如何查找变量。理解作用域和作用域链对于编写有效的 JavaScript 代码至关重要。

作用域的类型

  1. 全局作用域
    • 全局作用域是在整个脚本中都可以访问的作用域。
    • 在全局作用域中声明的变量和函数可以在脚本的任何地方被访问。
    • 浏览器环境中,全局作用域由 window 对象提供,Node.js 环境中由 global 对象提供。
  2. 函数作用域
    • 函数作用域是只在函数内部可以访问的作用域。
    • 在函数内部声明的变量和函数只能在该函数内部被访问。
    • 函数作用域可以嵌套,即一个函数内部可以包含另一个函数。
  3. 块级作用域
    • 块级作用域是由大括号 {} 定义的作用域,如 if 语句、for 循环、while 循环等。
    • 在 ES6 之前,JavaScript 没有真正的块级作用域,只有全局和函数作用域。
    • ES6 引入了 letconst 关键字,它们声明的变量具有块级作用域。

作用域链

作用域链(Scope Chain)是 JavaScript 在执行过程中用来查找变量的一系列作用域的集合。当代码试图访问一个变量时,它首先会在当前作用域中查找,如果没有找到,就会沿着作用域链向上查找,直到找到该变量或者到达全局作用域。

作用域链的工作原理

  1. 查找变量
    • 如果在当前作用域中找到了变量,就使用该变量。
    • 如果没有找到,就沿着作用域链向上查找,直到找到变量或者到达全局作用域。
  2. 函数调用
    • 当一个函数被调用时,它会创建一个新的作用域,这个作用域包含了函数的参数和在函数内部声明的变量。
    • 这个新的作用域会形成一个作用域链,其中包含了函数的父作用域(即调用该函数的作用域)。
  3. 闭包
    • 闭包是函数和其关联的作用域链的组合。
    • 闭包允许函数访问并操作定义它们时的作用域中的变量,即使那些变量的外部函数已经执行完毕。

作用域链的例子

var globalVar = 'I am global';

function outerFunction() {
  var outerVar = 'I am outer';
  function innerFunction() {
    var innerVar = 'I am inner';
    console.log(globalVar); // 访问全局变量
    console.log(outerVar); // 访问外部函数的变量
    console.log(innerVar); // 访问当前函数的变量
  }
  innerFunction();
}

outerFunction();
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在这个例子中,innerFunction 可以访问 globalVarouterVarinnerVar,因为它的作用域链包含了全局作用域、outerFunction 的作用域和它自己的局部作用域。

# 什么是 JavaScript 的执行上下文?

执行上下文(Execution Context)是指 JavaScript 代码执行的环境,它包含了代码执行所需的变量、函数、对象等信息。执行上下文定义了变量或函数的查找范围,也就是它们的作用域。每个执行上下文都有自己的作用域链,用于确定变量和函数的访问权限。

执行上下文的类型

JavaScript 中主要有两种执行上下文:

  1. 全局执行上下文(Global Execution Context):
    • 最外层的代码运行时创建的执行上下文,是所有其他执行上下文的父上下文。
    • 它包含了全局对象(在浏览器中是 window,在 Node.js 中是 global),以及在全局作用域中定义的所有变量和函数。
  2. 函数执行上下文(Function Execution Context):
    • 每当一个函数被调用时,就会创建一个新的执行上下文。
    • 它包含了函数的参数、函数内部定义的变量、函数内部定义的函数(闭包)等。

执行上下文的生命周期

执行上下文的生命周期通常包括以下几个阶段:

  1. 创建阶段(Creation Phase):
    • 在这个阶段,会创建一个执行上下文的实例,初始化 this 变量,确定函数的参数值,以及在函数内部声明的变量和函数。
    • 全局执行上下文的创建阶段还会初始化全局对象。
  2. 执行阶段(Execution Phase):
    • 在这个阶段,会执行代码。对于全局执行上下文,这意味着执行全局作用域中的代码;对于函数执行上下文,这意味着执行函数体内的代码。
    • 在这个阶段,会按照顺序执行代码,遇到函数调用时,会创建新的执行上下文,并将其压入调用栈。
  3. 销毁阶段(Destruction Phase):
    • 当执行上下文中的代码执行完毕后,这个执行上下文会从调用栈中弹出,不再存在。
    • 局部变量和函数会随着执行上下文的销毁而被清除,除非它们被闭包引用。

执行上下文的例子

var globalVar = 'I am global';

function myFunction() {
  var localVar = 'I am local';
  console.log localVar;
}

myFunction();
1
2
3
4
5
6
7
8

在这个例子中,全局执行上下文包含了 globalVar 变量,而 myFunction 的函数执行上下文包含了 localVar 变量和 myFunction 函数本身。

调用栈(Call Stack)

JavaScript 引擎使用调用栈来管理执行上下文。调用栈是一种后进先出(LIFO)的数据结构,每次函数调用都会创建一个新的执行上下文,并将其压入调用栈。当函数执行完毕后,它的执行上下文会从调用栈中弹出。

# 说说你对 JavaScript 中 this 的理解?指向什么?

this 是一个特殊的关键字,它指向当前执行上下文的环境对象。this 的值取决于函数的调用方式,而不是在哪里定义函数。因此,this 的指向在不同的场景下会有所不同。

this 的常见指向场景:

  1. 全局执行上下文

    • 如果函数在全局执行上下文中被调用(即不在任何对象的方法中调用),则 this 指向全局对象,浏览器中是 window 对象,Node.js 中是 global 对象。
    function showThis() {
      console.log(this);
    }
    showThis(); // 输出:window (在浏览器中)
    
    1
    2
    3
    4
  2. 作为对象的方法调用

    • 当函数作为某个对象的方法被调用时,this 指向该对象。
    const obj = {
      name: 'Kimi',
      showThis: function() {
        console.log(this.name);
      }
    };
    obj.showThis(); // 输出:Kimi
    
    1
    2
    3
    4
    5
    6
    7
  3. 使用函数调用

    • 使用 .call().apply().bind() 方法显式指定 this 值时,this 指向指定的对象。
    function showThis() {
      console.log(this.name);
    }
    const obj = { name: 'Kimi' };
    showThis.call(obj); // 输出:Kimi
    
    1
    2
    3
    4
    5
  4. 构造函数调用

    • 在构造函数中,this 指向新创建的对象。
    function Person(name) {
      this.name = name;
    }
    const person = new Person('Kimi');
    console.log(person.name); // 输出:Kimi
    
    1
    2
    3
    4
    5
  5. 事件处理器中

    • 在事件处理器中,this 指向触发事件的对象。
    const button = document.getElementById('myButton');
    button.addEventListener('click', function() {
      console.log(this); // 输出:[object HTMLButtonElement]
    });
    
    1
    2
    3
    4
  6. 箭头函数中

    • 箭头函数不绑定自己的 this,它会捕获其所在上下文的 this 值,作为自己的 this 值。
    const obj = {
      name: 'Kimi',
      showThis: () => {
        console.log(this.name);
      }
    };
    obj.showThis(); // 输出:undefined (箭头函数不绑定 this)
    
    1
    2
    3
    4
    5
    6
    7

this 的注意事项:

  • this 的值在函数定义时无法确定,只有在函数调用时才能确定。
  • this 的指向可能会因为使用了 .call().apply().bind() 或者箭头函数而改变。
  • 在严格模式('use strict')下,全局执行上下文中的 this 指向 undefined 而不是全局对象。

# JavaScript 中 call、apply 及 bind 函数有什么区别?

callapplybind 都是用来控制函数的 this 指向的方法,但它们在使用方式和应用场景上有所不同。

call 函数

call 方法可以在任何对象上调用一个函数,该方法接受一系列的参数,这些参数被传递给调用的函数。

function greet() {
  console.log('Hello, ' + this.name);
}

const person = { name: 'Kimi' };
greet.call(person); // 输出:Hello, Kimi
1
2
3
4
5
6

call 方法的参数是直接传递给函数的。

apply 函数

apply 方法和 call 类似,也可以在任何对象上调用一个函数,但它接受一个参数数组,这个数组的元素被传递给调用的函数。

function greet(greeting, punctuation) {
  console.log(greeting + ', ' + this.name + punctuation);
}

const person = { name: 'Kimi' };
greet.apply(person, ['Hello', '!']); // 输出:Hello, Kimi!
1
2
3
4
5
6

apply 方法的第二个参数是一个数组,数组中的元素作为参数传递给函数。

bind 函数

bind 方法创建一个新的函数,称为绑定函数,当调用这个绑定函数时,它将一个指定的 this 值和提供的参数序列调用原始函数。

function greet(greeting, punctuation) {
  console.log(greeting + ', ' + this.name + punctuation);
}

const person = { name: 'Kimi' };
const greetKimi = greet.bind(person, 'Hello');
greetKimi('!'); // 输出:Hello, Kimi!
1
2
3
4
5
6
7

bind 方法返回一个新的函数,这个新函数可以稍后被调用,并且它的 this 值被永久地绑定到提供的值。

主要区别

  1. 参数传递方式
    • call 直接传递参数列表。
    • apply 传递一个参数数组。
    • bind 可以传递参数,也可以稍后调用时传递,并且可以返回一个新函数。
  2. 返回值
    • callapply 都立即执行函数并返回函数的返回值。
    • bind 返回一个新的函数,不会立即执行。
  3. 使用场景
    • call 适用于当你知道需要传递确切数量的参数时。
    • apply 适用于参数数量不确定时,尤其是当参数以数组形式已经存在时。
    • bind 适用于当你需要创建一个新的函数,并且希望固定其 this 值时,或者你需要延迟执行函数。

# 如何实现 call、apply 及 bind 函数?

callapplybind 是内置的函数,它们允许你控制函数的 this 值。如果你想自己实现这些函数,可以通过以下方式来模拟它们的行为:

实现 call 函数

Function.prototype.myCall = function(context) {
  // 若没有传入上下文或者传入的上下文为 null,则默认上下文为 window
  context = context || window;

  // 给 context 添加一个属性,值为这个函数
  const fnSymbol = Symbol(); // 使用 Symbol 保证不会重复
  context[fnSymbol] = this;

  // 调用函数,传入除了第一个参数之外的所有参数
  const result = context[fnSymbol](...arguments);

  // 删除刚才添加的属性
  delete context[fnSymbol];

  // 返回函数执行结果
  return result;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

实现 apply 函数

Function.prototype.myApply = function(context, arr) {
  context = context || window;
  const fnSymbol = Symbol();
  context[fnSymbol] = this;

  let result;
  if (Array.isArray(arr)) {
    result = context[fnSymbol](...arr);
  } else {
    result = context[fnSymbol]();
  }

  delete context[fnSymbol];
  return result;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

实现 bind 函数

Function.prototype.myBind = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError("Bind must be called on a function");
  }

  // 存储原函数以及预置参数
  const self = this;
  const args = Array.prototype.slice.call(arguments, 1);

  // 创建一个空函数用作中转
  const nop = function() {};
  
  // 绑定函数
  const bound = function() {
    const finalArgs = args.concat(Array.prototype.slice.call(arguments));
    return self.apply(
      // 当作为构造函数时,this 应该指向实例,此时不绑定 context
      this instanceof nop ? this : context,
      finalArgs
    );
  };

  // 中转的原型指向原函数的原型
  nop.prototype = this.prototype;
  // 修改返回函数的原型,使其实例能够访问到原函数的原型
  bound.prototype = new nop();

  return bound;
};
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

这些实现提供了 callapplybind 的基本功能。它们不是完全符合 ECMAScript 规范的实现,但可以作为理解这些内置函数工作原理的参考。

# JavaScript 中连续多次调用 bind 函数,最终 this 指向什么?

连续多次调用 bind 函数会返回一个新的函数,每个函数都会记住其被 bind 时的 this 值。最终 this 指向的是最后一次调用 bind 时传入的值。

举个例子:

function identify() {
  return this.name;
}

const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };

const bound1 = identify.bind(obj1);
const bound2 = bound1.bind(obj2);

console.log(bound2()); // 输出:'obj2'
1
2
3
4
5
6
7
8
9
10
11

在这个例子中,identify 函数首先被 bind 到了 obj1,然后又被 bind 到了 obj2。最终,调用 bound2 时,this 指向的是 obj2,因此输出的是 'obj2'

需要注意的是,bind 返回的新函数并不会立即执行,而是在之后被调用时才执行。每次调用 bind 都会创建一个新的函数,并且只有最后一次调用 bind 时传入的对象会影响最终的 this 指向。

此外,如果 bind 被用在一个已经通过 bind 创建的函数上,那么之前创建的函数不会受到影响,this 指向的依然是最初 bind 时传入的对象。每次调用 bind 都是独立的,它们之间不会相互影响。

# promise.all 和 promise.allsettled 函数有什么区别?

Promise.allPromise.allSettled 都是 JavaScript 中用于处理多个 Promise 对象的函数,但它们在处理 Promise 结果和拒绝行为时有所不同。

Promise.all

Promise.all 接收一个 Promise 对象的数组作为输入,并返回一个新的 Promise 对象。这个新的 Promise 对象会在所有输入的 Promise 对象都成功解决(即 fulfilled)时解决,并且会按顺序返回每个 Promise 的结果构成的数组。如果任何一个输入的 Promise 被拒绝(即 rejected),返回的 Promise 将立即被拒绝,并且拒绝的原因会是第一个被拒绝的 Promise 的原因。

Promise.all([promise1, promise2, promise3])
  .then(results => {
    // 如果所有的 promise1, promise2, promise3 都成功解决,这里会收到一个数组 [result1, result2, result3]
  })
  .catch(error => {
    // 如果至少有一个 Promise 被拒绝,这里会收到那个 Promise 的拒绝原因
  });
1
2
3
4
5
6
7

Promise.allSettled

Promise.allSettled 也接收一个 Promise 对象的数组作为输入,并返回一个新的 Promise 对象。与 Promise.all 不同的是,Promise.allSettled 会等待所有输入的 Promise 对象都达到最终状态(无论是解决还是拒绝),然后解决返回的 Promise。返回的结果是一个对象数组,每个对象都包含一个 status 属性(值为 fulfilledrejected),以及 valuereason 属性,分别对应解决的值或拒绝的原因。

Promise.allSettled([promise1, promise2, promise3])
  .then(results => {
    // 这里会收到一个对象数组,每个对象包含 status, value 和 reason 属性
    // 例如:[{ status: "fulfilled", value: result1 }, { status: "rejected", reason: error1 }]
  });
1
2
3
4
5

主要区别

  1. 处理拒绝的方式
    • Promise.all 只要有一个 Promise 被拒绝,就会立即拒绝。
    • Promise.allSettled 会忽略拒绝的 Promise,继续等待其他 Promise 完成,然后返回每个 Promise 的最终状态。
  2. 结果对象
    • Promise.all 返回的是一个包含每个 Promise 结果的数组。
    • Promise.allSettled 返回的是一个包含每个 Promise 状态和结果(或拒绝原因)的对象数组。
  3. 用途
    • Promise.all 适用于所有 Promise 都必须成功的场景,任何一个失败都会导致整个操作失败。
    • Promise.allSettled 适用于你想了解所有 Promise 的最终状态,即使有些失败了,你仍然对它们的失败原因感兴趣。

Promise.allSettled 是 ES2020 新增的,它提供了一种更细致地控制异步操作结果的方式,特别是在你需要确保所有异步操作都被处理,无论它们是成功还是失败时。

# 说说你对 Promise 的理解?

Promise 是 JavaScript 中用于异步编程的一种对象,它代表了未来某个将要完成的事件的结果。Promise 对象可以处于三种状态之一:

  1. Pending(进行中):初始状态,既不是成功,也不是失败。
  2. Fulfilled(已成功):意味着操作成功完成。
  3. Rejected(已失败):意味着操作失败。

Promise 的特性

  • 异步执行Promise 允许你将异步操作以同步操作的方式表达,避免回调地狱(callback hell)。
  • 状态不可变:一旦 Promise 的状态从 Pending 变为 FulfilledRejected,它就不能再被改变。
  • 微任务Promise 的回调函数是在当前任务队列清空后,下一个任务队列开始时执行,属于微任务。
  • 链式调用Promise 可以通过链式调用 .then().catch() 方法来处理异步结果和错误。

Promise 的使用

创建一个新的 Promise 对象,你需要提供一个执行器函数(executor function),它将在 Promise 创建后立即执行:

const myPromise = new Promise((resolve, reject) => {
  // 异步操作
  if (/* 异步操作成功 */) {
    resolve(value); // 操作成功,调用 resolve
  } else {
    reject(error); // 操作失败,调用 reject
  }
});
1
2
3
4
5
6
7
8

resolvereject 是两个函数,分别用于将 Promise 状态从 Pending 变为 FulfilledRejected

Promise 的方法

  • .then():用于指定在 Promise 成功后要执行的回调函数。
  • .catch():用于指定在 Promise 失败后要执行的回调函数。
  • .finally():无论 Promise 最终状态如何,都会执行的回调函数。
  • .resolve():用于手动将 Promise 状态变为 Fulfilled
  • .reject():用于手动将 Promise 状态变为 Rejected

Promise 的应用场景

  • 异步请求:如 fetchXMLHttpRequest 等。
  • 定时操作:如 setTimeoutsetInterval 等。
  • 流控制:如并发控制、任务队列等。

Promise 的链式调用

myPromise
  .then(result => {
    // 处理 myPromise 的成功结果
    return anotherPromise; // 可以返回另一个 Promise,进行链式调用
  })
  .then(result => {
    // 处理上一个 then 的结果或 anotherPromise 的成功结果
  })
  .catch(error => {
    // 处理 myPromise 或另一个 Promise 的失败
  });
1
2
3
4
5
6
7
8
9
10
11

注意事项

  • Promise 一旦被拒绝,它不会将错误传播到下一个 .then(),除非使用 .catch() 捕获错误。
  • 如果 .then().catch() 中的回调函数返回一个 Promise,那么链式调用的下一个 .then() 会等待这个 Promise 完成后才会执行。
  • 如果 .then().catch() 中的回调函数抛出错误,那么这个错误会被传递到下一个 .catch()

# JavaScript 中异步编程的实现方式有哪些?

在JavaScript中,异步编程是一种处理非阻塞操作的方式,允许代码在等待某些操作(如网络请求、文件读写等)完成时继续执行。以下是一些常见的异步编程实现方式:

  1. 回调函数(Callbacks)
    • 这是最传统的异步编程方式,通过将函数作为参数传递给另一个函数来处理异步操作完成后的逻辑。
  2. Promises
    • Promises 提供了一种更好的异步操作管理方式,它代表了未来某个时间点完成的操作的结果。Promises 有三种状态:pending(等待中)、fulfilled(已成功)和 rejected(已失败)。
  3. async/await
    • 基于 Promises,asyncawait 关键字使得异步代码可以以同步的方式书写,提高了代码的可读性和维护性。async 函数会返回一个 Promise,而 await 用于等待一个 Promise 解决。
  4. 事件监听(Event Listeners)
    • 通过监听和响应事件来处理异步操作,这在处理 DOM 事件时非常常见。
  5. RxJS(Reactive Extensions for JavaScript)
    • RxJS 是一个库,它使用 Observables 来处理异步数据流。它提供了丰富的操作符来处理复杂的异步逻辑。
  6. Worker Threads
    • 通过使用 Web Workers,可以在浏览器中创建后台线程来执行脚本,从而避免阻塞主线程。
  7. Node.js 特定的异步模式
    • 在 Node.js 中,几乎所有的 I/O 操作都是异步的,通常会使用回调函数或者 Promises 来处理。
  8. Generators 和迭代器
    • Generators 允许函数在执行过程中暂停和恢复,这可以用来控制异步流的执行。
  9. Streams API
    • 在 Node.js 和现代浏览器中,Streams API 允许你处理可读和可写的数据流,这对于处理大量数据或文件非常有用。

# setTimeout、Promise、Async/Await 有什么区别?

setTimeoutPromiseasync/await是JavaScript中处理异步操作的不同方式,它们各自有不同的用途和特点:

  1. setTimeout

    • setTimeout是一个定时器函数,它允许你设定一个时间延迟,然后在这个延迟之后执行一个函数(称为回调函数)。

    • 它不是基于Promise的,也不返回一个Promise对象。

    • setTimeout通常用于延迟执行某些操作,或者在特定时间间隔后重复执行某些操作。

    • 示例代码:

      setTimeout(() => {
        console.log('这条消息将在1秒后输出');
      }, 1000);
      
      1
      2
      3
  2. Promise

    • Promise是一个代表了异步操作最终完成或失败的对象。

    • 它有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

    • Promise允许你通过.then().catch()方法来处理异步操作的成功和失败情况。

    • Promise可以链式调用,使得异步代码的流程更加清晰。

    • 示例代码:

      const myPromise = new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve('操作成功');
        }, 1000);
      });
      
      myPromise.then((value) => {
        console.log(value); // 输出:操作成功
      }).catch((error) => {
        console.error(error);
      });
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
  3. async/await

    • async/await是基于Promise的语法糖,它使得异步代码可以像同步代码一样书写和理解。

    • async函数会返回一个Promise,而await关键字用于等待一个Promise的结果。

    • await只能在async函数内部使用。

    • async/await提供了更好的错误处理方式,可以使用try/catch语句来捕获错误。

    • 示例代码:

      async function asyncFunction() {
        try {
          const result = await new Promise((resolve, reject) => {
            setTimeout(() => {
              resolve('操作成功');
            }, 1000);
          });
          console.log(result); // 输出:操作成功
        } catch (error) {
          console.error(error);
        }
      }
      
      asyncFunction();
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14

总结区别:

  • setTimeout是用于设置延迟执行的函数,不直接处理异步操作的结果。
  • Promise提供了一种处理异步操作结果的方式,支持链式调用,但代码可能变得复杂。
  • async/await提供了一种更简洁、更易于理解的方式来处理异步操作,使得异步代码的读写更加直观。

# Promise 有哪些基本用法?

Promise 是 JavaScript 中处理异步操作的一种机制,它代表了一个异步操作的最终完成(或失败)及其结果值。以下是 Promise 的一些基本用法:

  1. 创建 Promise

    • 通过 new Promise 构造函数创建一个新的 Promise 对象。构造函数接受一个执行器函数(executor function),它带有两个参数:resolvereject
    • resolve 用于异步操作成功时调用,并将 Promise 状态变为 fulfilled
    • reject 用于异步操作失败时调用,并将 Promise 状态变为 rejected
    const myPromise = new Promise((resolve, reject) => {
      if (/* 异步操作成功 */) {
        resolve('成功');
      } else {
        reject('失败');
      }
    });
    
    1
    2
    3
    4
    5
    6
    7
  2. 使用 then 方法

    • .then() 方法用于指定 Promise 状态变为 fulfilled 时的回调函数。
    • 可以链式调用多个 .then() 方法,每个回调函数都会接收前一个回调函数的返回值作为参数。
    myPromise.then((value) => {
      console.log(value); // '成功'
    });
    
    1
    2
    3
  3. 使用 catch 方法

    • .catch() 方法用于指定 Promise 状态变为 rejected 时的回调函数。
    • 它可以用来捕获前面 .then() 方法中抛出的错误。
    myPromise.then((value) => {
      console.log(value);
    }).catch((error) => {
      console.error(error); // '失败'
    });
    
    1
    2
    3
    4
    5
  4. 使用 finally 方法

    • .finally() 方法用于指定无论 Promise 最终状态是 fulfilled 还是 rejected 都会执行的回调函数。
    • 通常用于清理操作,如关闭文件流、释放资源等。
    myPromise.then((value) => {
      console.log(value);
    }).catch((error) => {
      console.error(error);
    }).finally(() => {
      console.log('无论成功还是失败都会执行');
    });
    
    1
    2
    3
    4
    5
    6
    7
  5. Promise 链

    • 可以串联多个 Promise 操作,每个 Promiseresolve 可以返回另一个 Promise,从而形成链式调用。
    Promise.resolve('第一步')
      .then((value) => {
        console.log(value);
        return '第二步';
      })
      .then((value) => {
        console.log(value);
        return '第三步';
      })
      .catch((error) => {
        console.error(error);
      });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  6. Promise.all

    • 当需要等待多个 Promise 同时进行,并且它们都完成后再继续下一步操作时,可以使用 Promise.all
    • 它接受一个 Promise 数组作为参数,只有当所有的 Promise 都成功解决时,才会调用 .then() 的回调函数。
    Promise.all([promise1, promise2, promise3]).then((values) => {
      console.log(values); // [结果1, 结果2, 结果3]
    }).catch((error) => {
      console.error(error);
    });
    
    1
    2
    3
    4
    5
  7. Promise.race

    • Promise.racePromise.all 类似,但它是只要数组中的任意一个 Promise 完成(无论是解决还是拒绝),就立即调用 .then().catch()
    Promise.race([promise1, promise2, promise3]).then((value) => {
      console.log(value); // 第一个完成的 Promise 的结果
    }).catch((error) => {
      console.error(error);
    });
    
    1
    2
    3
    4
    5
  8. 静态方法

    • Promise 还有一些静态方法,如 Promise.resolve(value)Promise.reject(reason),它们分别用于创建已解决和已拒绝的 Promise
    const resolvedPromise = Promise.resolve('已解决');
    const rejectedPromise = Promise.reject('已拒绝');
    
    1
    2

# Promise 解决了什么问题?

Promise 解决了 JavaScript 异步编程中的几个关键问题,特别是在处理回调函数时遇到的一些常见问题。以下是 Promise 带来的主要改进:

  1. 回调地狱(Callback Hell)
    • 在没有 Promise 的情况下,异步操作通常通过嵌套回调函数来实现,这会导致代码难以阅读和维护,这种现象被称为“回调地狱”。Promise 通过链式调用 .then() 方法,使得异步代码的书写更加直观和结构化。
  2. 错误处理
    • 传统的回调函数错误处理通常需要在每个回调中单独进行,这使得错误处理变得繁琐且容易出错。Promise 提供了 .catch() 方法,允许集中处理整个链中的错误。
  3. 代码可读性和维护性
    • Promise 使得异步代码更接近同步代码的形式,提高了代码的可读性和易于维护。
  4. 并行异步操作
    • Promise.all 方法允许同时执行多个异步操作,并在所有操作都完成时才继续执行后续代码,这简化了并行异步操作的管理。
  5. 状态管理
    • Promise 有明确的状态(pending、fulfilled、rejected),这使得状态管理更加清晰和一致。
  6. 更好的调试和测试
    • Promise 的链式调用和状态管理使得异步代码的调试和测试更加容易。
  7. 资源清理
    • Promise.finally() 方法允许在异步操作完成后执行清理工作,无论操作是成功还是失败,这有助于资源的管理和释放。
  8. 性能优化
    • 由于 Promise 的状态一旦改变就不会再变,这使得某些性能优化成为可能,例如避免不必要的回调函数调用。
  9. 更好的集成
    • Promise 成为了现代 JavaScript 异步编程的标准,许多现代库和框架都基于 Promise 或与之兼容,这使得不同技术之间的集成更加容易。
  10. 异步/等待(async/await)
    • Promiseasync/await 语法提供了基础,后者进一步简化了异步代码的书写,使得异步代码看起来就像是同步代码。

# Promise.all 和 Promise.race 分别有哪些使用场景?有什么区别?

Promise.allPromise.race 都是处理多个 Promise 对象的静态方法,但它们的使用场景和行为有所不同。

Promise.all 使用场景:

  1. 并行执行多个异步操作:当你需要同时启动多个异步操作,并且只有在所有操作都成功完成时才继续执行后续代码时,可以使用 Promise.all
  2. 聚合多个数据源:例如,当你需要从多个API端点获取数据并将它们合并为一个结果时。
  3. 依赖多个资源加载:在Web开发中,可能需要加载多个资源(如图片、脚本文件等),只有当所有资源都加载完成后,才执行某些操作。

Promise.race 使用场景:

  1. 竞态条件:当你有多个异步操作,但只关心最先完成的那个操作的结果时,可以使用 Promise.race
  2. 超时处理:通过 Promise.race,你可以创建一个在特定时间后解决的 Promise(作为超时),与其他异步操作的 Promise 进行竞态,以实现超时逻辑。
  3. 优先级处理:如果有多个异步操作,但某些操作的结果比其他的更紧急或更重要,可以使用 Promise.race 来优先处理。

区别:

  1. 结果处理
    • Promise.all 只有在所有传入的 Promise 对象都成功解决(fulfilled)时,才会解决,返回一个包含所有结果的数组。
    • Promise.race 会立即解决(或拒绝),一旦任何一个传入的 Promise 完成(无论是解决还是拒绝),它就会以那个 Promise 的结果(解决值或拒绝理由)来解决。
  2. 错误处理
    • Promise.all 中,如果任何一个 Promise 被拒绝,Promise.all 会立即拒绝,并且它的拒绝理由是第一个被拒绝的 Promise 的理由。
    • Promise.race 中,如果有 Promise 被拒绝,Promise.race 也会被拒绝,并且它的拒绝理由是第一个被拒绝的 Promise 的理由。
  3. 使用意图
    • Promise.all 通常用于确保所有异步操作都完成后再继续,适用于所有操作都成功才有意义的场景。
    • Promise.race 用于处理最先完成的异步操作,适用于只需要关心最快完成的操作的场景。

示例:

  • 使用 Promise.all 获取多个API数据:

    Promise.all([fetch(url1), fetch(url2), fetch(url3)])
      .then((responses) => Promise.all(responses.map(res => res.json())))
      .then((data) => {
        console.log(data); // 所有API数据都已获取
      })
      .catch((error) => {
        console.error('至少一个请求失败', error);
      });
    
    1
    2
    3
    4
    5
    6
    7
    8
  • 使用 Promise.race 实现超时逻辑:

    const timeoutPromise = new Promise((resolve, reject) => {
      setTimeout(() => reject(new Error('请求超时')), 5000);
    });
    
    Promise.race([fetch(url), timeoutPromise])
      .then((response) => response.json())
      .then((data) => {
        console.log(data); // 请求成功,获取数据
      })
      .catch((error) => {
        console.error(error.message); // 请求失败,处理超时或其他错误
      });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

总结来说,Promise.all 用于所有异步操作都必须成功的场景,而 Promise.race 用于只需要关注最快完成的异步操作的场景。

# 说说你对 async/await 的理解?

async/await 是 JavaScript 中处理异步操作的一种语法糖,它建立在 Promise 之上,使得异步代码的书写和理解更加直观和简洁。以下是对 async/await 的一些理解:

  1. 基于 Promise
    • async/await 是构建在 Promise 之上的,async 函数总是返回一个 Promise。如果函数正常执行结束,则 Promise 会被成功解决(fulfilled);如果函数中抛出错误,则 Promise 会被拒绝(rejected)。
  2. 书写异步代码像同步代码
    • 使用 async/await,你可以暂停函数执行,等待异步操作完成,然后再继续执行后续代码,这使得异步代码的逻辑更加清晰,类似于同步代码的流程。
  3. 错误处理
    • await 表达式会等待 Promise 解决,如果 Promise 被拒绝,它会抛出拒绝的原因,可以用 try/catch 语句来捕获这些错误,这与同步代码的错误处理方式一致。
  4. 提高代码可读性
    • async/await 使得异步代码的逻辑更加直观,代码结构更接近于人们习惯的顺序执行方式,提高了代码的可读性和可维护性。
  5. 避免回调地狱
    • 在没有 async/await 之前,异步操作通常需要嵌套多层回调函数,这被称为“回调地狱”。async/await 通过同步的代码风格避免了这个问题。
  6. 控制异步流程
    • await 表达式可以用来控制异步操作的执行顺序,使得代码的执行流程更加明确。
  7. 并发控制
    • 虽然 async/await 让代码看起来是顺序执行的,但它并不阻塞代码的执行,JavaScript 引擎仍然可以在等待异步操作时执行其他任务。
  8. 与 Promise 无缝集成
    • async/await 可以与现有的 Promise 代码无缝集成,使得在现有代码基础上进行异步操作的改进变得更加容易。
  9. 性能考虑
    • 在某些情况下,使用 async/await 可能会导致额外的函数调用和上下文切换,但通常这些性能开销是可以接受的,并且代码的可读性和可维护性带来的收益远大于这些开销。

示例代码:

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error('网络请求失败');
    }
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('请求数据时出错:', error);
  }
}

fetchData();
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在这个例子中,fetchData 函数使用 async 关键字声明,内部使用 await 等待 fetch 请求的结果。如果请求成功,它会解析 JSON 数据并打印出来;如果请求失败或解析出错,它会捕获错误并打印错误信息。

# async/await 是否会阻塞代码的执行?

async/await 不会阻塞 JavaScript 的执行,但它确实会控制代码的执行顺序。这是通过以下方式实现的:

  1. 非阻塞异步
    • 在 JavaScript(特别是在浏览器和 Node.js 环境中)中,async/await 允许异步操作在不阻塞主线程的情况下执行。这意味着,尽管 await 关键字使得 JavaScript 代码看起来像是在等待异步操作完成,但实际上它不会阻塞其他 JavaScript 代码的执行。
  2. 事件循环和任务队列
    • JavaScript 运行在单线程环境中,通过事件循环(event loop)和任务队列(task queue)来处理异步操作。当 await 表达式遇到一个 Promise 时,它会将剩余的函数执行放入任务队列中,并暂停当前 async 函数的执行。事件循环将继续执行其他任务,直到 Promise 解决,此时事件循环会将暂停的函数重新放入任务队列中继续执行。
  3. 控制执行顺序
    • 尽管 async/await 不会阻塞代码执行,但它确实控制了异步操作的执行顺序。await 表达式会等待 Promise 解决,然后才继续执行后面的代码。这意味着,你可以按照同步代码的逻辑来编写异步操作,使得代码逻辑更加清晰。
  4. 并发控制
    • 使用 async/await 时,你可以控制哪些异步操作是并发执行的,哪些是顺序执行的。例如,通过在 async 函数中顺序使用 await,你可以确保异步操作按顺序执行。如果你希望并发执行多个异步操作,可以同时启动它们,然后在 Promise.all 或其他方法中等待它们完成。

示例说明:

async function sequential() {
  console.log('开始执行 sequential');
  await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟异步操作
  console.log('sequential 完成');
}

async function concurrent() {
  console.log('开始执行 concurrent');
  const promise1 = new Promise(resolve => setTimeout(resolve, 1000));
  const promise2 = new Promise(resolve => setTimeout(resolve, 3000));
  
  await Promise.all([promise1, promise2]); // 并发执行
  console.log('concurrent 完成');
}

console.log('主程序开始');
sequential();
concurrent();
console.log('主程序结束');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

在这个例子中:

  • sequential 函数中的 await 确保了打印 "sequential 完成" 之前等待 2 秒。
  • concurrent 函数中的 Promise.all 确保了两个异步操作并发执行,但 await 确保了在两个操作都完成后才打印 "concurrent 完成"。
  • 主程序的 "主程序结束" 会在启动异步函数后立即打印,显示 async/await 不会阻塞其他代码的执行。

总结来说,async/await 通过事件循环和任务队列机制,实现了非阻塞的异步代码执行,同时提供了一种控制异步操作顺序的直观方式。

# await 到底在等待什么?

await 关键字在 JavaScript 中用于等待一个 Promise 对象的结果。具体来说,await 到底在等待以下几件事情:

  1. 等待 Promise 解决(Resolve)
    • await 后面跟随的 Promise 成功解决(即异步操作成功完成),await 会等待这个 Promise 变为解决状态,然后继续执行后面的代码,并返回解决的值。
  2. 等待 Promise 拒绝(Reject)
    • 如果 Promise 被拒绝(即异步操作失败),await 同样会等待这个 Promise 变为拒绝状态,然后抛出拒绝的原因,可以通过 try/catch 语句捕获这个错误。
  3. 等待异步操作完成
    • async 函数中,await 用于等待任何返回 Promise 的异步操作完成。这包括但不限于网络请求、文件读写、定时器等。
  4. 等待同步转换
    • await 也可以用于等待一个非 Promise 的值,如同步值或通过 Promise.resolve() 包装的值。在这种情况下,await 相当于一个同步转换,它确保了值在被后续代码使用之前已经可用。
  5. 等待执行顺序
    • async 函数中,await 确保了代码的执行顺序。即使多个异步操作可以并发执行,使用 await 可以强制代码按顺序执行,直到 await 后面的 Promise 解决。

示例说明:

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data'); // 等待网络请求完成
    if (!response.ok) {
      throw new Error('网络请求失败');
    }
    const data = await response.json(); // 等待响应体转为 JSON
    console.log(data);
  } catch (error) {
    console.error('请求数据时出错:', error);
  }
}

async function handleAsyncOperations() {
  const promise1 = new Promise((resolve, reject) => {
    setTimeout(() => resolve('操作1完成'), 1000);
  });
  const promise2 = new Promise((resolve, reject) => {
    setTimeout(() => resolve('操作2完成'), 2000);
  });

  console.log('开始执行');
  const result1 = await promise1; // 等待 promise1 解决
  console.log(result1);
  const result2 = await promise2; // 等待 promise2 解决
  console.log(result2);
  console.log('所有操作完成');
}

console.log('主程序开始');
fetchData();
handleAsyncOperations();
console.log('主程序结束');
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
31
32
33

在这个例子中:

  • fetchData 函数中的 await fetch(...) 等待网络请求完成并获取响应。
  • handleAsyncOperations 函数中的 await promise1await promise2 分别等待两个 Promise 解决。
  • await 确保了 console.log 打印的顺序,即使 promise2promise1 花费更多时间。

# async/await 对比 Promise 的优势是什么?

async/await 是基于 Promise 的一种语法糖,它提供了一种更直观、更简洁的方式来处理 JavaScript 中的异步操作。以下是 async/await 相对于传统 Promise 链的一些优势:

  1. 代码可读性
    • async/await 允许开发者以近乎同步的方式编写异步代码,这使得代码逻辑更加直观和易于理解,尤其是对于那些不熟悉回调和 Promise 链的开发者。
  2. 错误处理
    • 使用 async/await,你可以使用传统的 try/catch 语句来处理错误,这比 Promise.catch() 方法更符合开发者的习惯,也使得错误处理更加集中和一致。
  3. 调试友好
    • 调试 async/await 代码时,你可以设置断点和步进执行,就像调试同步代码一样。这比调试嵌套的 Promise 回调或 .then() 链更简单。
  4. 减少回调嵌套
    • async/await 避免了所谓的“回调地狱”或深层嵌套的 .then() 链,这使得代码更加扁平化,更易于维护。
  5. 更好的控制流
    • await 表达式强制代码在等待异步操作完成时暂停,这提供了更清晰的控制流,使得代码的执行顺序更加明确。
  6. 并发控制
    • 通过 async/await,你可以更轻松地控制哪些异步操作应该并发执行,哪些应该顺序执行,而不需要复杂的 Promise.all 或其他并发控制逻辑。
  7. 更自然的编码风格
    • async/await 允许开发者以更自然的方式表达逻辑,比如使用 for 循环来处理异步迭代,而不是使用递归或 .then() 链。
  8. 代码简洁性
    • 通常,使用 async/await 编写的代码比使用 Promise 链的代码更简洁,需要的代码行数更少。
  9. 更好的集成
    • async/await 可以很容易地与现有的 Promise 代码集成,因为 async 函数总是返回一个 Promise
  10. 避免不必要的 Promise 创建
    • 在某些情况下,使用 async/await 可以避免创建不必要的 Promise 对象,比如在不需要链式调用 .then() 的场景下。

示例对比:

使用 Promise:

function fetchUserData(userId) {
  return fetch(`/api/users/${userId}`)
    .then(response => response.json())
    .then(user => fetch(`/api/posts?userId=${user.id}`))
    .then(response => response.json())
    .then(posts => ({ user, posts }))
    .catch(error => console.error('Error:', error));
}
1
2
3
4
5
6
7
8

使用 async/await:

async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    const user = await response.json();
    const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
    const posts = await postsResponse.json();
    return { user, posts };
  } catch (error) {
    console.error('Error:', error);
  }
}
1
2
3
4
5
6
7
8
9
10
11

在这个对比中,使用 async/await 的版本更加清晰和简洁,错误处理也更加直观。

# async/await 如何捕获异常?

在 JavaScript 中使用 async/await 时,捕获异常主要依靠 try/catch 语句。这种方式与同步代码中的错误处理非常相似,使得异步代码的错误处理更加直观和易于管理。以下是如何使用 try/catch 来捕获 async/await 中的异常:

基本用法

  1. try 块
    • try 块中,你可以放置使用 await 的异步代码。如果 await 表达式中的 Promise 被拒绝(即异步操作失败),则会抛出一个错误。
  2. catch 块
    • catch 块用于捕获 try 块中抛出的任何错误。这包括由 await 表达式引发的异常,以及 try 块中的同步错误。
  3. finally 块(可选):
    • finally 块在 try/catch 结构中是可选的,它将在尝试和捕获错误之后执行,无论是否发生错误。这通常用于清理资源,如关闭文件或数据库连接。

示例代码

下面是一个使用 async/await 并结合 try/catch 来捕获异常的示例:

async function fetchData(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error('网络响应不是 OK');
    }
    const data = await response.json();
    console.log(data);
    return data;
  } catch (error) {
    console.error('请求数据时出错:', error);
  } finally {
    console.log('请求完成,无论成功或失败');
  }
}

// 调用函数
fetchData('https://api.example.com/data');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

在这个例子中:

  • try 块中的 await fetch(url) 尝试发起一个网络请求。
  • 如果请求失败(例如网络问题或服务器错误),fetch 会抛出一个错误,这个错误会在 catch 块中被捕获。
  • 如果响应状态不是 ok(即 HTTP 状态码不是 200-299),代码会手动抛出一个错误,这个错误也会被 catch 块捕获。
  • finally 块中的代码无论请求成功还是失败都会执行,通常用于执行一些清理工作。

错误传播

async 函数中,任何未被捕获的异常都会自动被拒绝(reject),并且可以通过返回的 Promise 来捕获:

fetchData('https://api.example.com/data')
  .then(data => console.log('数据获取成功:', data))
  .catch(error => console.error('处理请求时出错:', error));
1
2
3

这种方式使得错误处理更加灵活,你可以在调用 async 函数时选择如何处理错误。

# 什么是回调函数?回调函数有什么缺点?

回调函数是一种以函数作为参数并在完成某个特定任务后被调用的函数。这种机制允许程序将代码(函数)作为数据传递,以便在适当的时机执行。在 JavaScript 中,回调函数广泛用于处理异步操作,如事件监听、网络请求、文件读写等。

回调函数的基本用法

function greeting(name) {
  console.log(`Hello, ${name}!`);
}

function processUserInput(callback) {
  const name = "Kimi";
  callback(name);
}

processUserInput(greeting);
1
2
3
4
5
6
7
8
9
10

在这个例子中,greeting 是一个回调函数,它作为参数传递给 processUserInput 函数,并在 processUserInput 函数内部被调用。

回调函数的缺点

尽管回调函数在 JavaScript 中非常有用,但它们也有一些缺点:

  1. 回调地狱(Callback Hell)
    • 当多个异步操作需要按顺序执行时,回调函数会导致代码嵌套多层,形成所谓的“回调地狱”。这使得代码难以阅读和维护。
  2. 错误处理困难
    • 在回调函数中,每个异步操作都需要单独处理错误,这可能导致错误处理代码重复和复杂。
  3. 调试困难
    • 由于回调函数可能导致代码执行流程变得复杂,调试起来可能比较困难。
  4. 代码可读性差
    • 嵌套的回调函数会使得代码结构变得混乱,影响代码的可读性。
  5. 资源清理问题
    • 在回调函数中,资源清理(如关闭文件流、数据库连接等)可能难以管理,因为资源清理代码需要放置在正确的回调位置。
  6. 并发控制困难
    • 管理多个并发的异步操作可能变得复杂,尤其是在需要等待所有操作完成时。

回调地狱示例

fs.readFile('file1.txt', 'utf-8', function(err, data1) {
  if (err) throw err;
  console.log(data1);
  fs.readFile('file2.txt', 'utf-8', function(err, data2) {
    if (err) throw err;
    console.log(data2);
    fs.readFile('file3.txt', 'utf-8', function(err, data3) {
      if (err) throw err;
      console.log(data3);
      // 更多的嵌套回调...
    });
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13

在这个例子中,每个 fs.readFile 调用都是一个异步操作,它们通过回调函数嵌套在一起。这种结构随着异步操作数量的增加而变得越来越复杂。

# JavaScript 中如何解决回调地域问题?

在 JavaScript 中,回调地狱(Callback Hell)是指由于多个异步操作需要按顺序执行,导致代码出现多层嵌套的现象,这使得代码难以阅读和维护。为了解决这个问题,JavaScript 社区引入了几种不同的方法:

  1. Promise:Promise 对象是 JavaScript 中用于异步编程的一种解决方案,它提供了一种更加清晰和结构化的方式来处理异步操作。通过链式调用 .then() 方法,可以顺序执行多个异步操作,并且可以使用 .catch() 方法来统一处理错误。Promise 允许你将异步操作以同步的方式表达,避免了深层嵌套的回调函数。
  2. async/await:基于 Promise,asyncawait 关键字使得异步代码可以以近乎同步的方式书写,提高了代码的可读性和可维护性。async 函数允许你使用 await 关键字等待一个 Promise 解决,这样可以让代码更加直观,并且错误处理也更加简单,因为可以使用传统的 try/catch 语句。
  3. 模块化和命名函数:将嵌套的回调函数拆分成独立的模块或命名函数,可以提高代码的可读性和可维护性。
  4. 异步控制流库:如 async 库,它提供了一系列的函数来帮助管理复杂的异步流程,例如 async.waterfall()async.parallel() 等。
  5. 避免过度使用 async/await:虽然 async/await 可以提高代码的可读性,但是过度使用也可能导致性能问题。例如,如果多个异步操作之间没有依赖关系,应该使用 Promise.all 来并行执行它们,而不是顺序执行。

# setTimeout、setInterval、requestAnimationFrame 各有什么特点?

setTimeoutsetIntervalrequestAnimationFrame 都是 JavaScript 中用于定时操作的函数,但它们各有特点和适用场景:

  1. setTimeout

    • setTimeout 用于在指定的毫秒数后执行一次函数。

    • 它接受两个参数:第一个是要延迟执行的函数,第二个是延迟的时间(以毫秒为单位)。

    • setTimeout 不能保证精确的执行时间,因为它的执行将会受到调用它的线程执行时间和宏任务队列中其他任务的影响。

    • 示例代码:

      setTimeout(() => {
        console.log('这条消息将在1秒后输出');
      }, 1000);
      
      1
      2
      3
  2. setInterval

    • setInterval 用于按照指定的时间间隔周期性地执行函数。

    • 它同样接受两个参数:第一个是要重复执行的函数,第二个是间隔时间(以毫秒为单位)。

    • setInterval 也不具备精确的执行保证,如果执行的函数耗时过长,可能会影响下一次执行的时间点。

    • 示例代码:

      setInterval(() => {
        console.log('这条消息将每隔1秒输出一次');
      }, 1000);
      
      1
      2
      3
  3. requestAnimationFrame

    • requestAnimationFrame 用于在下一次重绘之前执行动画更新的函数。

    • 它只接受一个参数,即要在下一次重绘前执行的函数。

    • requestAnimationFrame 通常用于创建平滑的动画效果,因为它的执行时间会与浏览器的刷新率同步,通常是每秒60次。

    • 它比 setTimeoutsetInterval 更适合动画,因为它可以在不需要动画帧的时候降低刷新率,从而节省资源。

    • 示例代码:

      let angle = 0;
      function animate() {
        // 执行动画更新
        console.log('动画帧');
        angle += 1;
        if (angle < 360) {
          requestAnimationFrame(animate);
        }
      }
      requestAnimationFrame(animate);
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

总结:

  • setTimeoutsetInterval 更适合不需要与浏览器渲染同步的定时操作。
  • requestAnimationFrame 更适合于动画效果,因为它可以提供更平滑的动画和更好的性能优化。
  • setTimeoutsetInterval 可能会受到执行时间的影响,而 requestAnimationFrame 则会自动适应浏览器的绘制过程,从而避免丢帧和性能问题。

# JavaScript 中 WeakMap 和 Map 的区别?

WeakMapMap 是 JavaScript 中两种不同的集合类型,它们在用途和特性上有一些关键的区别:

  1. 引用类型
    • Map 存储的键值对中的键可以是任何类型,包括对象、函数、字符串、数字等。
    • WeakMap 的键只能是对象引用,不能是其他类型的值。
  2. 垃圾回收
    • Map 中的对象引用是强引用,即使没有其他变量引用这些对象,它们也不会被垃圾回收机制回收。
    • WeakMap 中的键是弱引用,如果一个对象只被 WeakMap 引用,那么它可以被垃圾回收机制回收,这有助于防止内存泄漏。
  3. 性能
    • WeakMap 的性能通常比 Map 要低,因为它需要处理垃圾回收的额外逻辑。
  4. 迭代
    • Map 是可迭代的,你可以使用 for...of 循环或者其他迭代方法来遍历 Map
    • WeakMap 不可迭代,没有办法直接遍历 WeakMap 中的元素。
  5. API
    • Map 提供了丰富的 API,如 size 属性、clear()delete()forEach()get()has()set() 等方法。
    • WeakMap 的 API 较为有限,没有 size 属性,也没有 clear()forEach() 方法。
  6. 用途
    • Map 通常用于需要存储和检索键值对的场景,它是一个通用的集合类型。
    • WeakMap 通常用于缓存对象的额外信息,而不影响对象的垃圾回收,常用于实现私有数据或者缓存。
  7. 构造函数
    • Map 构造函数可以接受一个可迭代对象作为参数,用来初始化 Map
    • WeakMap 构造函数接受一个对象作为其键值对的数组,但这些对象只是作为键,而不是用来初始化 WeakMap

示例代码:

Map 的使用

let map = new Map();
map.set('key1', 'value1');
map.set('key2', 'value2');
console.log(map.get('key1')); // 输出:value1
for (let [key, value] of map) {
  console.log(key, value); // 输出:key1 value1 和 key2 value2
}
1
2
3
4
5
6
7

WeakMap 的使用

let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, 'value1');
console.log(weakMap.get(obj)); // 输出:value1
// 如果 obj 没有其他的引用,它和 weakMap 中的条目都可能被垃圾回收
1
2
3
4
5

总结来说,Map 是一个通用的键值对集合,适合大多数需要键值对存储的场景,而 WeakMap 则适用于需要缓存对象额外信息且不阻止对象被垃圾回收的场景。

# JavaScript 中对象创建的方式有哪些?

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

  1. 对象字面量

    • 最常见的创建单个对象的方式。
    let person = {
      name: 'Kimi',
      age: 25,
      greet: function() {
        console.log('Hello, my name is ' + this.name);
      }
    };
    
    1
    2
    3
    4
    5
    6
    7
  2. 构造函数

    • 使用 new 关键字和构造函数来创建对象实例。
    function Person(name, age) {
      this.name = name;
      this.age = age;
      this.greet = function() {
        console.log('Hello, my name is ' + this.name);
      };
    }
    let person = new Person('Kimi', 25);
    
    1
    2
    3
    4
    5
    6
    7
    8
  3. 工厂模式

    • 使用一个函数来封装对象创建过程,返回新对象。
    function createPerson(name, age) {
      return {
        name: name,
        age: age,
        greet: function() {
          console.log('Hello, my name is ' + this.name);
        }
      };
    }
    let person = createPerson('Kimi', 25);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  4. 原型链

    • 通过修改一个构造函数的 prototype 属性来让所有实例共享某些方法。
    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    Person.prototype.greet = function() {
      console.log('Hello, my name is ' + this.name);
    };
    let person = new Person('Kimi', 25);
    
    1
    2
    3
    4
    5
    6
    7
    8
  5. Object.create()

    • 使用 Object.create() 方法来创建一个新对象,使用现有对象来提供新创建的对象的__proto__
    let personProto = {
      greet: function() {
        console.log('Hello, my name is ' + this.name);
      }
    };
    let person = Object.create(personProto);
    person.name = 'Kimi';
    person.age = 25;
    
    1
    2
    3
    4
    5
    6
    7
    8
  6. 类(ES6)

    • 使用 class 关键字来定义类,然后通过 new 关键字创建实例。
    class Person {
      constructor(name, age) {
        this.name = name;
        this.age = age;
      }
      greet() {
        console.log('Hello, my name is ' + this.name);
      }
    }
    let person = new Person('Kimi', 25);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  7. 工厂模式与构造函数

    • 结合工厂模式和构造函数的优点,使用函数来创建对象,同时提供构造函数的灵活性。
    function Person(name, age) {
      let obj = new Object();
      obj.name = name;
      obj.age = age;
      obj.greet = function() {
        console.log('Hello, my name is ' + this.name);
      };
      return obj;
    }
    let person = Person('Kimi', 25);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  8. 解构赋值(ES6)

    • 当你需要创建一个对象来存储解构出来的值时。
    let { name, age } = { name: 'Kimi', age: 25 };
    let person = { name, age };
    
    1
    2
  9. Map 对象

    • 创建一个 Map 对象,它可以存储键值对,键可以是任意值。
    let map = new Map();
    map.set('name', 'Kimi');
    map.set('age', 25);
    let person = Object.fromEntries(map);
    
    1
    2
    3
    4

# JavaScript 中对象继承的方式有哪些?

在 JavaScript 中,对象继承是一种允许一个对象共享另一个对象的属性和方法的机制。以下是几种常见的实现继承的方式:

  1. 原型链继承

    • 通过将一个构造函数的 prototype 属性指向另一个构造函数的实例来实现继承。
    function Parent() {
      this.name = 'parent';
    }
    Parent.prototype.getName = function() {
      return this.name;
    };
    
    function Child() {
      this.age = 18;
    }
    
    // 继承 Parent
    Child.prototype = new Parent();
    var child = new Child();
    console.log(child.getName()); // 输出 'parent'
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  2. 构造函数继承

    • 通过在子类型的构造函数中调用超类型的构造函数来实现继承。
    function Parent(name) {
      this.name = name;
    }
    Parent.prototype.getName = function() {
      return this.name;
    };
    
    function Child(age) {
      Parent.call(this, 'parent');
      this.age = age;
    }
    
    var child = new Child(18);
    console.log(child.getName()); // 输出 'parent'
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  3. 组合继承(原型链 + 构造函数继承)

    • 结合原型链继承和构造函数继承,将两者的优点结合起来。
    function Parent(name) {
      this.name = name;
    }
    Parent.prototype.getName = function() {
      return this.name;
    };
    
    function Child(name, age) {
      Parent.call(this, name); // 继承属性
      this.age = age;
    }
    Child.prototype = new Parent(); // 继承方法
    Child.prototype.constructor = Child;
    
    var child = new Child('child', 18);
    console.log(child.getName()); // 输出 'child'
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  4. 原型式继承

    • 使用 Object 的 create 方法来实现继承。
    var parent = {
      name: 'parent',
      getName: function() {
        return this.name;
      }
    };
    
    var child = Object.create(parent, {
      age: {
        value: 18,
        writable: true,
        enumerable: true,
        configurable: true
      }
    });
    
    console.log(child.getName()); // 输出 'parent'
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  5. 寄生式继承

    • 创建一个增强对象的函数,然后返回这个对象。
    function createAnother(original) {
      var clone = Object.create(original); // 通过调用函数创建一个新对象
      clone.sayHi = function() { // 以某种方式来增强这个对象
        console.log('hi');
      };
      return clone; // 返回这个对象
    }
    
    var parent = {
      name: 'parent',
      getName: function() {
        return this.name;
      }
    };
    
    var child = createAnother(parent);
    child.sayHi(); // 输出 'hi'
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  6. 寄生组合式继承

    • 通过使用工厂函数来增强对象,然后通过原型链来继承方法。
    function inheritPrototype(childObj, parentObj) {
      var prototype = Object.create(parentObj.prototype);
      prototype.constructor = childObj;
      childObj.prototype = prototype;
    }
    
    function Parent(name) {
      this.name = name;
    }
    Parent.prototype.getName = function() {
      return this.name;
    };
    
    function Child(name, age) {
      Parent.call(this, name);
      this.age = age;
    }
    
    inheritPrototype(Child, Parent);
    
    var child = new Child('child', 18);
    console.log(child.getName()); // 输出 'child'
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
  7. ES6 类继承

    • 使用 class 关键字和 extends 来实现继承。
    class Parent {
      constructor(name) {
        this.name = name;
      }
      getName() {
        return this.name;
      }
    }
    
    class Child extends Parent {
      constructor(name, age) {
        super(name); // 调用父类的 constructor(name)
        this.age = age;
      }
    }
    
    var child = new Child('child', 18);
    console.log(child.getName()); // 输出 'child'
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

# JavaScript 中的 Proxy 与 Reflect 分别是什么?两者有什么关系?

Proxy

Proxy 是 ES6 引入的一个内置对象,它可以用来创建一个对象的代理,从而在访问对象的属性或方法时进行拦截和自定义操作。Proxy 对象可以用来定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等),这使得 Proxy 成为一种强大的工具,用于实现观察者模式、数据绑定、访问控制、日志记录、性能监控等。

Proxy 构造函数接受两个参数:

  1. target:这是要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括 JavaScript 的内置类型和函数)。
  2. handler:这是一个对象,其属性是当执行操作时定义自定义行为的方法,例如 getsethasapply 等。

Reflect

Reflect 也是 ES6 引入的内置对象,它提供了一种更直接的方式来操作对象属性。Reflect 对象的所有方法都与 Proxyhandler 对象的方法一一对应。这意味着,Reflect 的方法可以用来模拟默认的 Proxy 行为。

Reflect 的主要用途包括:

  1. 作为一个工具库,提供一些有用的函数,如 Reflect.get()Reflect.set()Reflect.has() 等,这些函数可以用来直接访问和修改对象的属性。
  2. 作为 Proxy 的补充,当你需要在 Proxyhandler 函数中调用默认的、内置的行为时,可以使用 Reflect 的相应方法。

Proxy 与 Reflect 的关系

ProxyReflect 之间存在密切的关系:

  1. 默认行为Reflect 的方法可以用来在 Proxyhandler 陷阱中调用默认操作。这确保了 Proxy 可以自定义行为,同时在需要时回退到默认行为。
  2. 兼容性Reflect 提供了一种标准化的方式来访问和修改对象属性,这使得在没有 Proxy 的环境中也可以使用类似的功能。
  3. 语义化Reflect 的方法命名通常更加语义化,直接反映了它们的功能,这有助于提高代码的可读性。

示例

下面是一个使用 ProxyReflect 的示例:

const target = {};
const handler = {
    get(target, property, receiver) {
        console.log(`Property ${property} has been read.`);
        return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
        console.log(`Property ${property} set to ${value}.`);
        return Reflect.set(target, property, value, receiver);
    }
};

const proxy = new Proxy(target, handler);

proxy.a = 'test'; // 输出: Property a set to test.
console.log(proxy.a); // 输出: Property a has been read. 然后输出: test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在这个例子中,我们创建了一个 Proxy 对象,它包装了一个空对象 target,并提供了自定义的 getset 陷阱。在这些陷阱中,我们使用 Reflect 的方法来执行默认的属性访问和赋值操作,并在操作前后打印日志信息。

# JavaScript 中如何实现寄生组合继承?

JavaScript 中的寄生组合继承是一种继承模式,它结合了构造函数继承和原型链继承的方法,旨在解决传统组合继承中的一些问题,如避免父类构造函数的多次调用和不必要的属性复制。

在寄生组合继承中,首先使用构造函数继承来继承父类的实例属性,然后通过寄生式继承来继承父类的原型方法。这种方法的核心在于创建一个函数,这个函数创建一个对象,以父类原型为原型,然后将其作为子类原型,从而继承父类的方法。这样做的好处是避免了父类构造函数的多次调用,并且确保了原型链的完整性。

具体实现步骤如下:

  1. 定义父类构造函数和子类构造函数,子类构造函数中通过 call 方法继承父类实例属性。
  2. 创建一个继承自父类原型的对象。
  3. 将子类原型指向这个新创建的对象,而不是直接指向父类原型。
  4. 修正子类原型的 constructor 属性,使其指向子类构造函数。

示例代码如下:

function Parent(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
Parent.prototype.sayName = function() {
  alert(this.name);
};

function Child(name, age) {
  Parent.call(this, name); // 继承实例属性
  this.age = age;
}

// 寄生式继承父类原型
function inherit(Child, Parent) {
  var prototype = Object.create(Parent.prototype);
  prototype.constructor = Child;
  Child.prototype = prototype;
}

inherit(Child, Parent);

Child.prototype.sayAge = function() {
  alert(this.age);
};

var child1 = new Child('Nicholas', 29);
child1.colors.push('black');
alert(child1.colors); // "red,blue,green,black"
child1.sayName(); // "Nicholas"
child1.sayAge(); // "29"
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
31

在这个例子中,inherit 函数是寄生组合继承的核心,它创建了一个新的对象,这个对象的原型是 Parent.prototype,然后将这个对象赋值给 Child.prototype。这样,Child 类的实例就可以访问 Parent 类的方法,同时避免了在 Child.prototype 上创建不必要的、多余的属性。

寄生组合继承是 JavaScript 中最理想的继承方式之一,它既保证了实例属性的私有性,又确保了方法的共享性,同时避免了父类构造函数的多次调用,是一种高效且优雅的继承模式。

# 说说 JavaScript 中 instanceof 的原理?

JavaScript 中,instanceof 操作符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。如果出现在原型链上,则表达式返回 true,否则返回 false

原理

当你使用 instanceof 检查一个对象是否为某个构造函数的实例时,JavaScript 引擎会执行以下步骤:

  1. 确定构造函数的 prototype 属性(即,构造函数原型上的可枚举属性)。
  2. 确定要检查的对象的内部 [[Prototype]] 属性(在 ES5 中称为 __proto__,在 ES6 中称为 Object.getPrototypeOf(obj))。
  3. 沿着对象的原型链向上遍历,直到找到与构造函数的 prototype 相同的对象。
  4. 如果找到,则 instanceof 返回 true;如果到达原型链的末端(即,Object.prototype)仍未找到,则返回 false

示例代码

function Person(name) {
  this.name = name;
}

Person.prototype.getName = function() {
  return this.name;
};

var person = new Person("Kimi");
console.log(person instanceof Person); // 输出:true
console.log(person instanceof Object); // 输出:true,因为Person.prototype最终指向Object.prototype
1
2
3
4
5
6
7
8
9
10
11

特殊情况

  • 当使用 instanceof 检查一个通过 new 操作符创建的对象时,它通常会返回 true,因为 new 操作符确保了构造函数的 prototype 属性出现在实例的原型链上。
  • 如果构造函数的 prototype 属性被修改或重新赋值,instanceof 的结果可能会与预期不同。
  • instanceof 操作符区分不同执行上下文(如不同的iframe或window)中的对象,即使对象的类型相同。

typeof 的区别

  • typeof 用于获取一个值的数据类型,如 typeof [] 返回 "object"typeof "Hello" 返回 "string"
  • instanceof 用于确定一个对象是否为某个特定构造函数的实例。

模拟实现 instanceof

虽然不推荐覆盖原生的 instanceof 操作符,但可以通过以下方式模拟实现 instanceof

function myInstanceof(left, right) {
  let proto = Object.getPrototypeOf(left);
  while (proto) {
    if (proto === right.prototype) {
      return true;
    }
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

console.log(myInstanceof(person, Person)); // 输出:true
1
2
3
4
5
6
7
8
9
10
11
12

在这个模拟实现中,我们使用 Object.getPrototypeOf 来获取对象的原型,并沿着原型链向上遍历,直到找到与构造函数的 prototype 相同的对象。

# 说说 JavaScript 中 new 的原理?

JavaScript 中,new 是一个操作符,用于创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型的实例。当你通过 new 调用一个函数时,实际上会发生以下几个步骤:

  1. 创建一个新的空对象
    • new 操作符会创建一个新的空对象。
  2. 设置原型
    • 这个新对象的原型会被设置为构造函数的 prototype 属性所指向的对象。这意味着新对象会继承构造函数原型上定义的所有属性和方法。
  3. 绑定构造函数的 this
    • 构造函数内部的 this 被赋值为新创建的对象。这意味着在构造函数中对 this 属性的任何引用和赋值都会影响新对象。
  4. 执行构造函数
    • 构造函数中的代码被执行,通常用于给新对象添加属性或方法。
  5. 返回对象
    • 如果构造函数返回了一个对象,则返回该对象;如果没有返回对象(或者返回了非对象类型的值,如 undefined),则返回步骤1中创建的新对象。

示例代码

function Person(name) {
  this.name = name;
  this.sayHello = function() {
    console.log(`Hello, my name is ${this.name}`);
  };
}

var person = new Person('Kimi');
console.log(person.name); // 输出:Kimi
person.sayHello(); // 输出:Hello, my name is Kimi
1
2
3
4
5
6
7
8
9
10

在这个例子中,Person 函数是一个构造函数,当我们通过 new 调用它时,它会创建一个新的 Person 对象,并设置 name 属性和 sayHello 方法。

模拟实现 new

虽然不推荐覆盖原生的 new 操作符,但可以通过以下方式模拟实现 new

function myNew(constructor, ...args) {
  let obj = {}; // 创建一个空对象
  obj.__proto__ = constructor.prototype; // 设置对象的原型
  let result = constructor.apply(obj, args); // 执行构造函数,并传入参数
  
  // 如果构造函数返回一个对象,则返回该对象;否则返回创建的对象
  return (typeof result === 'object' && result !== null) ? result : obj;
}

function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`);
};

var person = myNew(Person, 'Kimi');
person.sayHello(); // 输出:Hello, my name is Kimi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

在这个模拟实现中,我们手动创建了一个空对象,设置了它的原型,然后调用了构造函数并传入了参数。最后,我们检查构造函数的返回值,以确定是返回新创建的对象还是构造函数返回的对象。

# JavaScript 如何实现对象的深浅拷贝?

JavaScript 中,对象的拷贝可以是浅拷贝或深拷贝,这取决于拷贝过程中对象的属性值是直接复制(浅拷贝)还是递归复制(深拷贝)。

浅拷贝(Shallow Copy)

浅拷贝会创建一个新对象,其属性值与原始对象相同。如果属性值是基本类型(如字符串、数字、布尔值),则直接复制值;如果属性值是引用类型(如对象、数组),则复制的是引用,而不是引用的对象本身。

方法:

  1. Object.assign()

    • 可以用来合并多个源对象到目标对象中。对于源对象中的属性,如果是基本类型,则会进行值的复制;如果是引用类型,则会复制引用。
    const original = { a: 1, b: { c: 2 } };
    const copy = Object.assign({}, original);
    copy.b.c = 3; // 这会改变 original.b.c 的值
    
    1
    2
    3
  2. 展开运算符(Spread Operator)

    • 在数组和对象字面量中使用,对于对象,它也会复制属性的引用。
    const original = { a: 1, b: { c: 2 } };
    const copy = { ...original };
    copy.b.c = 3; // 这会改变 original.b.c 的值
    
    1
    2
    3
  3. Array.prototype.slice()

    • 用于数组,可以创建一个新数组,复制原数组的元素(如果是对象,则复制引用)。
    const original = [{ a: 1 }, { b: 2 }];
    const copy = original.slice();
    copy[0].a = 3; // 这会改变 original[0].a 的值
    
    1
    2
    3

深拷贝(Deep Copy)

深拷贝会创建一个新对象,然后递归地复制原始对象的所有属性和子属性,这样原始对象和拷贝对象在结构上完全独立。

方法:

  1. JSON 方法

    • 使用 JSON.stringify() 将对象转换为 JSON 字符串,然后使用 JSON.parse() 将字符串转换回对象。这种方法简单但有局限性,例如不能拷贝函数、undefined、循环引用等。
    const original = { a: 1, b: { c: 2 } };
    const copy = JSON.parse(JSON.stringify(original));
    copy.b.c = 3; // 这不会改变 original.b.c 的值
    
    1
    2
    3
  2. 递归拷贝

    • 自定义函数,递归地遍历对象的所有属性,对于每个属性,检查它是否是引用类型,如果是,则再次进行拷贝。
    function deepCopy(obj) {
      if (obj === null || typeof obj !== 'object') {
        return obj;
      }
      let copy = Array.isArray(obj) ? [] : {};
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          copy[key] = deepCopy(obj[key]);
        }
      }
      return copy;
    }
    const original = { a: 1, b: { c: 2 } };
    const copy = deepCopy(original);
    copy.b.c = 3; // 这不会改变 original.b.c 的值
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  3. 使用库

    • 使用如 Lodash 的 _.cloneDeep() 方法等第三方库来实现深拷贝。
    import _ from 'lodash';
    const original = { a: 1, b: { c: 2 } };
    const copy = _.cloneDeep(original);
    copy.b.c = 3; // 这不会改变 original.b.c 的值
    
    1
    2
    3
    4

在实际应用中,选择哪种拷贝方法取决于具体的需求和上下文。浅拷贝适用于简单对象或数组,而深拷贝适用于需要完全独立副本的复杂对象。

# JavaScript 数组 sort 函数的实现原理是什么?

JavaScript 中的 Array.prototype.sort() 方法用于对数组元素进行排序。默认情况下,sort() 方法会将数组元素转换为字符串,并按照字符串的Unicode码点进行排序。如果提供了一个比较函数,sort() 会根据该函数的返回值来决定元素的排序。

默认排序

当不提供比较函数时,sort() 会按照以下步骤进行排序:

  1. 将数组中的所有元素转换为字符串。
  2. 按照字符串的Unicode码点进行排序。
  3. 相邻的元素进行比较,如果第一个元素在Unicode码点上小于第二个元素,那么第一个元素会出现在排序后的数组的前面。

比较函数

如果提供了一个比较函数,sort() 方法会按照该函数的返回值来排序:

  • 如果比较函数返回值小于0,则将第一个参数(a)排在第二个参数(b)之前。
  • 如果比较函数返回值等于0,则保持 a 和 b 的相对位置不变。(尽管如此,并不是所有的 JavaScript 引擎都会这样做,因此不建议依赖于这个行为。)
  • 如果比较函数返回值大于0,则将第一个参数(a)排在后面。

实现原理

sort() 方法的实现通常基于一些高效的排序算法,如快速排序、归并排序或其他变体。具体的算法可能因浏览器和JavaScript引擎的不同而有所差异。例如,V8引擎(Chrome和Node.js)使用的是Timsort算法,这是一种结合了归并排序和插入排序的高效排序算法。

示例代码

// 默认排序
[10, 5, 45, 56, 1000].sort(); // [10, 1000, 45, 5, 56]

// 使用比较函数
[10, 5, 45, 56, 1000].sort((a, b) => a - b); // [5, 10, 45, 56, 1000]

// 对象数组排序
const items = [
  { name: 'Edward', value: 21 },
  { name: 'Sharpe', value: 37 },
  { name: 'And', value: 45 },
  { name: 'The', value: -12 },
  { name: 'Magnetic', value: 13 },
  { name: 'Zeros', value: 37 }
];
items.sort((a, b) => a.value - b.value); // 根据 value 属性排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

注意事项

  • sort() 方法会直接修改原数组,不会返回新数组。
  • 如果数组中含有 undefinednull 值,它们会被放在数组的开始位置。
  • sort() 方法是不稳定的排序,意味着相等元素的顺序可能会改变。
前端 JavaScript 面试题 by 爽爽学编程

爽爽学编程   |