面试之JS相关问题

269 阅读26分钟

1. 闭包

1.1 闭包是什么

闭包是一种编程概念,它是指在一个函数内部定义另一个函数,并且内部函数可以访问外部函数的变量。闭包可以捕获和维持自己创建时所在的外部环境,即使外部环境已经不存在了。通过将函数内部的变量和函数绑定在一起,闭包可以创建私有变量和实现高级功能。

1.2 闭包通常在以下情况下使用

保护变量:闭包可以创建私有变量,这些变量无法通过外部访问或修改。这种方式可以增强代码的安全性,并且只允许通过特定的函数来操作这些变量。 保存状态:闭包可以保存函数创建时的状态,即使该函数已经执行完毕。这对于需要记住先前操作或状态的功能非常有用,例如计数器或迭代器。 实现回调和事件处理程序:当一个函数作为参数传递给另一个函数时,闭包可以用于捕获外部函数的作用域,并在稍后的时间执行。这使得可以在特定条件下执行回调函数或处理事件。

1.3 闭包的优缺点

优点:

1、信息隐藏:闭包可以隐藏外部函数的变量,只暴露内部函数的接口。这种封装机制可以防止对变量的意外修改,并提高程序的可靠性。 2、保留状态:闭包可以记录外部函数的状态。当外部函数执行完毕后,内部函数仍然可以访问和修改外部函数的变量。这对于某些应用场景(如事件处理函数)非常有用。 3、动态性:闭包在每次调用外部函数时都创建一个新的内部函数,所以在不同的调用中可以产生不同的内部函数。这种动态性可以提供更灵活的编程方式。

缺点:

1、内存占用:闭包会占用更多的内存,因为它需要同时保存外部函数和内部函数的状态。如果闭包的使用不当,可能会导致内存泄漏等问题。 2、性能损耗:由于闭包需要在每次调用外部函数时重新创建内部函数,所以在频繁调用的情况下可能会导致性能下降。 难以理解和调试:闭包的嵌套结构可能会增加代码的复杂性,使得代码更难以理解和调试。尤其是在多级闭包的情况下,代码可能会变得非常晦涩。

1.4 使用示例

function outerFunction(x) {
  return function innerFunction(y) {
    return x + y;
  };
}

var closure = outerFunction(5);
var result = closure(3);
console.log(result); // 输出 8

在这个示例中,outerFunction 是外部函数,它接受一个参数 x。内部函数 innerFunction 定义在外部函数内部,并且可以访问外部函数的变量 x。外部函数返回内部函数 innerFunction,形成闭包。

在主程序中,我们调用 outerFunction 并将返回的内部函数赋值给变量 closure。然后,我们调用 closure 并传递参数 y。内部函数会将参数 y 与外部函数的变量 x 相加并返回结果。

通过使用闭包,我们可以创建一个固定的上下文环境,使内部函数可以访问外部函数的变量,即使外部函数已经执行完毕。在这个例子中,闭包使得内部函数 innerFunction 可以记住外部函数 outerFunction 的参数值,即变量 x 的值为 5

闭包的一个重要特性是它能够保留状态并形成私有变量。通过将内部函数返回并赋值给变量,我们可以将外部函数的状态封装起来,只暴露内部函数的接口。这种封装机制可以防止对变量的意外修改,并提高程序的可靠性。在这个例子中,closure 变量持有了 innerFunction 的引用,可以多次调用它,并保留了它访问 x 的能力。

2. 原型和原型链

在 JavaScript 中,每个对象都有一个原型(prototype),它是一个包含属性和方法的对象。原型充当了对象的模板,提供了共享的属性和方法,可以被对象实例访问和使用。

当我们创建一个对象时,JavaScript 会自动为该对象关联一个原型。对象可以通过 __proto__ 属性来访问它的原型。如果我们调用对象的某个属性或方法,但该对象本身没有这个属性或方法,JavaScript 就会在对象的原型上查找该属性或方法。

原型链(prototype chain)是一个对象之间的链接,每个对象都有一个指向其原型的引用。当我们访问对象的属性或方法时,JavaScript 会按照原型链依次查找,直到找到该属性或方法或者到达原型链的末尾(即 Object.prototype)。

这种通过原型链实现属性和方法继承的机制使得 JavaScript 中的对象具有了很高的灵活性和可扩展性。如果一个对象的原型上没有找到所需的属性或方法,它会在原型链上继续向上查找,直到找到为止。这意味着我们可以通过修改对象的原型来动态地扩展对象的功能。

当我们创建一个新的对象实例时,它会继承其构造函数的原型。这意味着该对象实例可以访问其构造函数原型上定义的属性和方法。如果我们对原型进行修改,新创建的对象实例也会受到影响。

原型和原型链是 JavaScript 面向对象编程的重要概念。它们使得对象的属性和方法可以共享和继承,并且为对象之间的关系提供了灵活的机制。理解原型和原型链对于编写可维护、可扩展的 JavaScript 代码非常重要。

3. 事件代理(事件委托)

3.1 什么是事件代理(事件委托)

JavaScript 事件代理(事件委托)是一种利用事件冒泡机制的技术,通过将事件处理程序绑定到它们共同的父元素上,来统一管理子元素上的事件。当事件发生时,事件会逐级冒泡至父元素,并通过判断事件的目标元素来执行相应的处理逻辑。

3.2 优点

减少事件处理程序的数量: 通过使用事件委托,我们只需要将事件处理程序绑定到共同的父元素上,而不是将事件处理程序分别绑定到每个子元素上。这样就可以减少事件处理程序的数量,使代码更简洁。 动态监听新添加的元素: 当有新的元素被添加到父元素下时,使用事件委托可以自动对这些新元素进行事件的监听,而无需手动重新绑定事件处理程序。这样可以大大简化代码,并且减少了对动态元素的额外处理逻辑。 提高性能和内存利用: 使用事件委托可以减少事件处理程序的数量,从而降低内存的使用量。此外,事件委托利用了事件冒泡机制,在事件触发时将冒泡至父元素,而不是逐个检查子元素。这样可以提高性能,特别是对于大型的 DOM 结构和有大量子元素的情况。 简化代码维护: 通过使用事件委托,我们可以将相似的逻辑放在一个事件处理程序中,而不是分散在多个处理程序中。这样可以提高代码的可读性、可维护性和可重用性。 方便处理事件委托: 通过事件委托,可以将事件处理程序绑定到父元素上,而不是各个子元素上。这意味着父元素上的处理程序可以获得更全面的上下文信息,例如事件的目标元素、事件的属性和其他相关信息。这样更便于处理事件委托,并且可以动态地为目标元素提供不同的交互体验。

3.3 缺点

监听的事件类型限制: 事件委托只适用于能够冒泡的事件类型。一些事件,如 focus 和 blur,没有冒泡机制,所以无法使用事件委托处理这些事件。 可能影响性能: 当事件触发时,事件委托需要遍历整个 DOM 树来确定事件的目标元素。如果 DOM 树非常庞大,可能会影响性能。因此,在处理大型 DOM 树或需要高性能的情况下,事件委托可能不是最佳选择。 难以确定事件目标: 在事件处理程序中,需要通过事件对象的 target 属性来确定实际触发事件的元素。有时候,如果目标元素过于复杂或嵌套层级过深,确定目标元素可能会比较困难,需要更复杂的逻辑判断。 处理事件代理的代码复杂性: 虽然事件委托可以减少事件处理程序的数量,但在父元素的事件处理程序中,需要根据事件目标元素的不同来执行相应的逻辑操作。这可能会导致处理事件委托的代码更加复杂和难以维护。 可能导致误操作: 由于事件委托将事件处理程序绑定到父元素上,如果没有适当地阻止事件冒泡或其他处理机制,有可能会导致意外的事件触发或误操作。

3.4 示例

<ul id="parentList">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>

<script>
  // 获取父元素
  var parentList = document.getElementById('parentList');

  // 绑定事件代理
  parentList.addEventListener('click', function(event) {
    // 判断目标元素是否为 <li> 元素
    if (event.target && event.target.nodeName === 'LI') {
      // 在控制台输出点击的文本内容
      console.log('You clicked on item: ' + event.target.textContent);
    }
  });
</script>

在这个例子中,ul 元素的父元素 parentList 上绑定了一个点击事件代理。当用户点击列表项 li 元素时,事件会冒泡至父元素,然后通过判断事件的目标元素来执行指定的操作。

事件代理通过判断事件对象的 target 属性来确定实际触发事件的元素。在这个例子中,我们通过比较 event.target 的 nodeName 是否为 'LI' 来判断点击的元素是否为列表项。

当用户点击列表项时,事件代理会在控制台输出相应的提示信息,以便演示。注意,事件代理使得我们可以在实际元素变化时不需要重新绑定事件处理程序,从而提供了更好的灵活性和可维护性。

4. 如何判断对象的真实类型

在 JavaScript 中,可以使用 typeof 运算符来判断一个对象的基本类型,如 "string""number""boolean""undefined""symbol" 和 "function"。但是,typeof 运算符对于复杂类型的对象(如数组、日期、正则表达式等)并不准确。

对于复杂类型的对象,可以使用以下方法来判断其真实类型:

使用 instanceof 运算符instanceof 运算符可以用来检查对象是否是某个特定类型的实例。例如,obj instanceof Array 可以用来判断 obj 是否为数组类型的实例。 使用 Object.prototype.toString.call():这是一个较为可靠的方法,通过调用对象的 toString 方法来获取其内部属性 [[Class]] 的值,从而判断其真实类型。例如,使用Object.prototype.toString.call(obj)方法 可以返回 "[object Array]" 来判断 obj 是否为数组类型。

以下是一些示例:

var obj = [1, 2, 3];

console.log(typeof obj); // 输出 "object"
console.log(obj instanceof Array); // 输出 true
console.log(Object.prototype.toString.call(obj)); // 输出 "[object Array]"

注意,以上方法也存在一定的局限性。比如,通过 instanceof 运算符无法准确判断对象是否为某个特定类型的子类实例,因为它只检查原型链中的构造函数。而使用 Object.prototype.toString.call() 方法会返回 [object Object],无法区分自定义的对象类型。

5. 深拷贝和浅拷贝

5.1 什么是深拷贝和浅拷贝

在 JavaScript 中,拷贝对象或数组有两种方式:深拷贝和浅拷贝。

浅拷贝是指创建一个新的对象或数组,并将原始对象或数组的引用复制给新对象或数组。换句话说,新的对象或数组与原始对象或数组共享同一份数据。如果原始对象或数组的属性值发生改变,那么共享的对象或数组也会受到影响。

深拷贝则是创建一个全新的对象或数组,并将原始对象或数组中的每个属性值逐一复制给新对象或数组。深拷贝创建了一份完全独立的拷贝,新对象或数组与原始对象或数组之间没有任何关联。

5.2 深拷贝和浅拷贝的区别

浅拷贝:

  • 创建一个新对象或数组,但内部的元素仍然是原始对象或数组的引用。
  • 原始对象或数组的改变会影响到拷贝的对象或数组。
  • 浅拷贝通常只拷贝第一层的属性或元素。

深拷贝:

  • 创建一个新对象或数组,并将原始对象或数组的每个属性或元素完全复制到新对象或数组中。
  • 原始对象或数组的改变不会影响到拷贝的对象或数组。
  • 深拷贝会递归地拷贝嵌套的属性或元素,保证所有层级的数据都是独立的。

5.3 深拷贝和浅拷贝实现方法

浅拷贝的实现方法:

  • 对于数组:可以使用 Array.prototype.slice() 或扩展运算符 ... 来创建一个浅拷贝。
  • 对于对象:可以使用 Object.assign() 或扩展运算符 {...} 来创建一个浅拷贝。
var originalArray = [1, 2, 3];
var shallowCopyArray = originalArray.slice(); // 或者使用 shallowCopyArray = [...originalArray];

var originalObject = { a: 1, b: 2 };
var shallowCopyObject = Object.assign({}, originalObject); // 或者使用 shallowCopyObject = { ...originalObject };

深拷贝的实现方法:

  • 可以使用递归来逐一复制对象或数组的属性或元素。这样可以处理嵌套的对象或数组。
  • 使用 JSON 序列化和反序列化,将对象或数组转换成字符串,然后再转换回来。这样创建的对象将是原始对象的完全独立拷贝。但是,这种方法有一些限制,例如无法处理循环引用和函数等非 JSON 可序列化的属性。

以下是一个简单的深拷贝示例(使用递归实现):

function deepCopy(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }

  let copy;
  if (Array.isArray(obj)) {
    copy = [];
  } else {
    copy = {};
  }

  for (let key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      copy[key] = deepCopy(obj[key]);
    }
  }

  return copy;
}

这个 deepCopy 函数接受一个对象或数组作为输入,并返回一个深拷贝后的对象或数组。它会对原始对象或数组的每个属性值进行递归调用,以创建完全独立的拷贝。

下面是一个示例,展示了如何使用 deepCopy 函数进行数据的深拷贝:

let originalArray = [1, 2, { a: 1, b: 2 }];
let deepCopyArray = deepCopy(originalArray);

deepCopyArray[2].a = 10;

console.log(originalArray); // 输出 [1, 2, { a: 1, b: 2 }]
console.log(deepCopyArray); // 输出 [1, 2, { a: 10, b: 2 }]

在这个示例中,我们对深拷贝后的数组 deepCopyArray 进行修改,并发现原始数组 originalArray 保持不变,因为它们是独立的拷贝。

请注意,上述示例是比较基本的实现,对于复杂的数据结构和特殊情况,可能需要更完善的处理。另外,有一些第三方库(如 lodash、jQuery)也提供了深拷贝的函数。

使用第三方库实现数据的深拷贝是一种更简便和可靠的方式,因为这些库通常已经考虑了许多边界情况和特殊对象的处理。

一个流行的第三方库是 lodash。lodash 提供了 cloneDeep 函数来实现深拷贝。下面是使用 lodash 的示例:

首先,使用 npm 或 yarn 安装 lodash:

npm install lodash

然后在你的代码中引入 lodash,并使用 cloneDeep 函数进行深拷贝:

const _ = require('lodash');

let originalArray = [1, 2, { a: 1, b: 2 }];
let deepCopyArray = _.cloneDeep(originalArray);

deepCopyArray[2].a = 10;

console.log(originalArray); // 输出 [1, 2, { a: 1, b: 2 }]
console.log(deepCopyArray); // 输出 [1, 2, { a: 10, b: 2 }]

在这个示例中,我们使用 _.cloneDeep 函数进行深拷贝,并对 deepCopyArray 进行修改,而不影响原始数组 originalArray

除了 lodash,还有其他一些第三方库也提供了深拷贝的功能,如 jQuery 的 $.extend(true, {}, obj)、ramda 的 R.clone(obj) 等。根据你的具体需求和项目中使用的库,选择适合的第三方库进行深拷贝会更加方便和可靠。

6. js中声明变量的方法

在 JavaScript 中,有三种常用的声明变量的方法:varlet 和 const,它们有一些区别。

6.1 var

var:在较早的 JavaScript 版本中使用的关键字,用于声明变量。var 声明的变量具有以下特点:

  • 全局作用域或函数作用域:var 声明的变量将在声明所在的函数或全局作用域中生效。
  • 变量提升(Hoisting):在函数作用域中,无论变量声明的位置在哪里,都会被提升到函数的顶部。这意味着可以在变量声明之前使用该变量,但其值为 undefined
  • 可重复声明:可以重复使用 var 对同一个变量进行声明,而且不会引发错误。
  • 可修改:可以重新赋值给同一个变量,或修改其值。

6.2 let

let:在 ECMAScript 6(ES6)中引入的关键字,用于声明块级作用域的变量。let 声明的变量具有以下特点:

  • 块级作用域:let 声明的变量只在声明所在的块级作用域中有效。
  • 暂时性死区(Temporal Dead Zone,TDZ):在块级作用域中,使用 let 声明的变量在声明之前是不可访问的。这可以防止变量提升导致的意外行为。
  • 不可重复声明:在同一作用域中重复使用 let 对同一个变量进行声明会导致错误。
  • 可修改:可以重新赋值给同一个变量,或修改其值。

6.3 const

const:也是在 ES6 中引入的关键字,用于声明块级作用域的常量。const 声明的变量具有以下特点:

  • 块级作用域:const 声明的变量只在声明所在的块级作用域中有效。
  • 不可修改:const 声明的变量必须在声明时进行初始化,并且不能再次赋值。该变量的值一旦被设置,就不能被修改。
  • 不可重复声明:在同一作用域中重复使用 const 对同一个变量进行声明会导致错误。

7. const相关

7.1 const中声明的变量可以修改么

在 JavaScript 中,使用 const 声明的变量被认为是常量,旨在创建一个不可修改的值。一旦使用 const 声明变量并初始化,就不能再对该变量进行赋值或修改。

以下是一个示例,展示了使用 const 声明的变量不能被修改的行为:

const x = 10;
x = 20; // TypeError: Assignment to constant variable.

在这个示例中,使用 const 声明了变量 x 并初始化为 10,然后尝试将其改为 20,结果会抛出一个 TypeError 错误。这是因为 const 声明的变量是只读的,不能再次赋予新的值。

然而,请注意,const 声明的是变量引用的地址是不可修改的,而不是变量引用的内容是不可修改的。对于引用类型(如对象和数组),虽然 const 声明的变量本身不能被重新赋值,但是可以修改对象的属性或数组的元素。

以下是一个示例,展示了使用 const 声明的变量引用的对象属性是可以修改的:

const person = {
  name: "John",
  age: 30
};
person.age = 40;

console.log(person); // 输出 { name: "John", age: 40 }

在这个示例中,使用 const 声明了变量 person,并修改了对象的 age 属性。这是允许的,因为 const 只限制重新赋值变量本身,而不限制修改对象属性。

总结起来,使用 const 声明的变量在初始化后不能被重新赋值,但对于引用类型的变量,其属性或元素仍然可以被修改。

7.2 const 定义引用类型也不能改变它的值该怎么做

7.2.1 深度冻结

要使使用 const 定义的引用类型数据不能修改,可以使用一种称为深度冻结(deep freeze)的技术。深度冻结将递归地冻结对象及其嵌套的所有属性

可以编写一个 deepFreeze 函数来实现深度冻结,如下所示:

function deepFreeze(obj) {
  // 首先冻结对象本身
  Object.freeze(obj);

  // 递归冻结对象的属性
  for (let prop in obj) {
    if (obj.hasOwnProperty(prop) && typeof obj[prop] === 'object') {
      deepFreeze(obj[prop]);
    }
  }

  return obj;
}

使用 deepFreeze 函数可以确保使用 const 定义的引用类型数据及其嵌套属性不可修改。以下是一个示例:

const person = deepFreeze({
  name: "John",
  age: 30
});

person.age = 40; // 无效,属性不会被修改,静默失败

console.log(person); // 输出 { name: "John", age: 30 }

在这个示例中,我们将 person 对象传递给 deepFreeze 函数,它会递归地冻结对象及其属性,确保其不可修改。

需要注意的是,深度冻结是一种强制性的保护机制,确保对象及其嵌套属性的不可修改性。然而,这可能会带来一些性能开销,因为每次访问属性时都需要进行额外的检查。因此,深度冻结适用于对数据的绝对不可变性要求较高的情况,但如果不需要绝对的不可变性,其他方法(如浅冻结)可能更适合。

7.2.2 浅冻结

浅冻结(shallow freeze)是相对于深度冻结而言的一种冻结对象的方式。它只冻结对象本身,而不会递归地冻结其嵌套属性

要对对象进行浅冻结,可以使用 Object.freeze() 方法。以下是一个示例:

const obj = {
  prop1: "value1",
  prop2: "value2"
};

Object.freeze(obj);

obj.prop1 = "newValue"; // 无效,属性不会被修改,静默失败

console.log(obj); // 输出 { prop1: "value1", prop2: "value2" }

在这个示例中,我们使用 Object.freeze() 方法将 obj 对象进行浅冻结。这样,obj 对象本身不可修改,但它的属性可以继续修改。

需要注意的是,浅冻结只会冻结对象的第一层属性,嵌套在对象中的引用类型属性仍然可以被修改。

例如:

const obj = {
  prop1: "value1",
  prop2: {
    nestedProp: "nestedValue"
  }
};

Object.freeze(obj);

obj.prop2.nestedProp = "newNestedValue"; // 有效,属性可以被修改

console.log(obj); // 输出 { prop1: "value1", prop2: { nestedProp: "newNestedValue" } }

在这个示例中,虽然使用了浅冻结,但是可以修改 obj.prop2.nestedProp 的值,因为只有 obj 对象本身被冻结了,而嵌套在其内部的对象没有被冻结。

8. 数组相关

8.1 常用的改变原数组和不改变原数组方法

在 JavaScript 中,数组有一些方法可以改变原数组,而其他方法不会改变原数组。下面列出了一些常见的数组方法,并说明它们是属于哪一类。

能够改变原数组的方法(原地改变):

  • push():向数组末尾添加一个或多个元素,并返回新的长度。
  • pop():删除并返回数组的最后一个元素。
  • shift():删除并返回数组的第一个元素。
  • unshift():向数组开头添加一个或多个元素,并返回新的长度。
  • splice():删除、替换或添加元素,并返回被删除的元素。
  • reverse():颠倒数组中元素的顺序。
  • sort():对数组元素进行排序。

不改变原数组的方法(返回新数组):

  • concat():连接两个或多个数组,并返回一个新数组。
  • slice():返回指定位置的片段或浅拷贝数组。
  • filter():通过传入的函数创建一个新数组,其中包含符合条件的元素。
  • map():通过传入的函数对数组中的每个元素进行处理,然后返回新数组。
  • reduce():通过传入的函数对数组元素进行累积计算,将数组减少为单个值。
  • forEach():对数组中的每个元素执行提供的函数。

需要注意的是,虽然不改变原数组的方法返回一个新数组,但它们可能返回了原数组的浅拷贝,其中引用类型元素仍然与原数组共享。如果需要对新数组进行修改而不影响原数组,应该注意避免修改这些共享的引用类型元素。

另外,还有一些方法,如 indexOf()includes()some()every() 等,它们虽然不改变数组,但也不返回一个新数组。它们用于在数组中进行搜索或检查条件,并返回布尔值或索引位置。

8.2 some 和 every 区别

some() 和 every() 是 JavaScript 数组的两个方法,用于在数组中进行条件判断。它们的区别在于返回值和操作行为

some() 方法用于检查数组中是否至少有一个元素满足指定的条件。如果数组中有一个元素满足条件,则 some() 方法返回 true,否则返回 false

以下是 some() 方法的示例:

const numbers = [1, 2, 3, 4, 5];
const hasEvenNumber = numbers.some((num) => num % 2 === 0);

console.log(hasEvenNumber); // 输出 true

在这个示例中,numbers 数组中有一个偶数(2)满足条件,所以 some() 方法返回了 true

every() 方法用于检查数组中的每个元素是否都满足指定的条件。如果数组中的所有元素都满足条件,则 every() 方法返回 true,否则返回 false

以下是 every() 方法的示例:

const numbers = [1, 2, 3, 4, 5];
const allPositive = numbers.every((num) => num > 0);

console.log(allPositive); // 输出 true

在这个示例中,numbers 数组中的所有元素都大于 0,所以 every() 方法返回了 true

需要注意的是,当数组为空时,some() 方法将返回 false,而 every() 方法将返回 true 这是因为 some() 方法至少需要一个元素满足条件,而 every() 方法要求所有元素都满足条件,而空数组无法满足这两个条件。

总结起来,some() 方法用于检查数组中是否至少有一个元素满足条件,而 every() 方法用于检查数组中的每个元素是否都满足条件。它们的返回值不同,分别是 true 或 false

8.3 常用的数组去重方法

8.3.1 Set

利用 Set 数据结构的特性,可以轻松地去除数组中的重复元素。

const numbers = [1, 2, 3, 3, 4, 5, 5];
const uniqueNumbers = [...new Set(numbers)];

console.log(uniqueNumbers); // 输出 [1, 2, 3, 4, 5]

在这个示例中,通过将数组转换为 Set,再将 Set 转换回数组的方式,实现了去重的效果。

8.3.2 filter

使用 Array 的 filter 方法,遍历数组并返回不重复的元素。

const numbers = [1, 2, 3, 3, 4, 5, 5];
const uniqueNumbers = numbers.filter((value, index, self) => {
  return self.indexOf(value) === index;
});

console.log(uniqueNumbers); // 输出 [1, 2, 3, 4, 5]

在这个示例中,通过 filter 方法遍历数组,并使用 indexOf 方法判断当前元素在数组中的第一个索引位置,从而过滤掉重复元素。

8.3.3 reduce

使用 Array 的 reduce 方法,将数组中的元素添加到结果数组中,仅当元素不在结果数组中时才添加。

const numbers = [1, 2, 3, 3, 4, 5, 5];
const uniqueNumbers = numbers.reduce((result, current) => {
  if (!result.includes(current)) {
    result.push(current);
  }
  return result;
}, []);

console.log(uniqueNumbers); // 输出 [1, 2, 3, 4, 5]

在这个示例中,通过 reduce 方法遍历数组,将非重复的元素添加到结果数组中。

8.3.4. 双重遍历循环

const numbers = [1, 2, 3, 3, 4, 5, 5];
const uniqueNumbers = [];

for (let i = 0; i < numbers.length; i++) {
  let isDuplicate = false;

  for (let j = 0; j < uniqueNumbers.length; j++) {
    if (numbers[i] === uniqueNumbers[j]) {
      isDuplicate = true;
      break;
    }
  }

  if (!isDuplicate) {
    uniqueNumbers.push(numbers[i]);
  }
}

console.log(uniqueNumbers); // 输出 [1, 2, 3, 4, 5]

在这个示例中,使用两个嵌套的 for 循环进行双重遍历。外层循环遍历原始数组 numbers,内层循环遍历用于存储唯一元素的数组 uniqueNumbers

在每次迭代中,通过比较当前元素与 uniqueNumbers 数组中已存在的元素来判断是否为重复元素。如果找到重复元素,将 isDuplicate 标志设置为 true,并立即跳出内层循环。如果未找到重复元素,将当前元素添加到 uniqueNumbers 数组中。

最终,uniqueNumbers 数组将包含原数组中的唯一元素。

请注意,使用双重遍历循环的方法在面对大型数组时可能会变得很慢,因为它的时间复杂度为 O(n^2)。因此,当处理较大的数组时,推荐使用其他更高效的去重方法,如使用 Set 或使用 filter 和 reduce 方法。

8.3.5 数组内置方法加单层遍历去重( indexOf、includes )

const numbers = [1, 2, 3, 3, 4, 5, 5];
const uniqueNumbers = [];

for (let i = 0; i < numbers.length; i++) {
  if (uniqueNumbers.indexOf(numbers[i]) === -1) {
    uniqueNumbers.push(numbers[i]);
  }
}

console.log(uniqueNumbers); // 输出 [1, 2, 3, 4, 5]

在这个示例中,使用 indexOf() 方法检查当前元素在 uniqueNumbers 数组中的索引位置。如果索引为 -1,则说明该元素在 uniqueNumbers 数组中不存在,因此将其添加到 uniqueNumbers 数组。

另一种方法是使用 includes() 方法进行相同的判断,如下所示:

const numbers = [1, 2, 3, 3, 4, 5, 5];
const uniqueNumbers = [];

for (let i = 0; i < numbers.length; i++) {
  if (!uniqueNumbers.includes(numbers[i])) {
    uniqueNumbers.push(numbers[i]);
  }
}

console.log(uniqueNumbers); // 输出 [1, 2, 3, 4, 5]

在这个示例中,使用 includes() 方法判断当前元素是否包含在 uniqueNumbers 数组中,如果不包含,则将其添加到 uniqueNumbers 数组。

这种方法只需进行一次遍历,通过内置方法的快速查找,实现了数组去重的效果。

需要注意的是,使用 indexOf() 和 includes() 方法进行元素查找的时间复杂度为 O(n),因此对于大型数组,建议使用 Set 或其他高效的方法进行去重。

8.3.6 Map 和单层遍历

const numbers = [1, 2, 3, 3, 4, 5, 5];
const uniqueNumbers = [];
const numberMap = new Map();

for (let i = 0; i < numbers.length; i++) {
  const number = numbers[i];

  if (!numberMap.has(number)) { // 检查 Map 中是否已经存在该键
    numberMap.set(number, true); // 将元素添加至 Map 中
    uniqueNumbers.push(number); // 将元素添加至结果数组
  }
}

console.log(uniqueNumbers); // 输出 [1, 2, 3, 4, 5]

在这个示例中,我们首先创建一个空的 Map 对象 numberMap 和一个空的数组 uniqueNumbers。然后,我们进行单层遍历,逐个检查数组中的元素。

对于每个元素,我们使用 has() 方法来检查 numberMap 中是否已经存在相同的键。如果该键不存在,我们使用 set() 方法将该元素添加到 numberMap 中,并将其添加到 uniqueNumbers 数组中。

由于 Map 只允许唯一的键,因此最终的 uniqueNumbers 数组将包含除去重复元素之外的所有元素。

利用 Map 和单层遍历进行数组去重可以达到较高的性能,并且可以保持原始数组的顺序。这种方法的时间复杂度为 O(n),因为 Map 的查找操作几乎是常数时间复杂度。

8.3.7 空 Object 实现和单层遍历

const numbers = [1, 2, 3, 3, 4, 5, 5];
const uniqueNumbers = [];
const numberObj = {};

for (let i = 0; i < numbers.length; i++) {
  const number = numbers[i];

  if (!numberObj[number]) { // 检查对象中是否已经存在该属性
    numberObj[number] = true; // 将属性添加至对象中
    uniqueNumbers.push(number); // 将元素添加至结果数组
  }
}

console.log(uniqueNumbers); // 输出 [1, 2, 3, 4, 5]

在这个示例中,我们首先创建一个空的对象 numberObj 和一个空的数组 uniqueNumbers。然后,我们进行单层遍历,逐个检查数组中的元素。

对于每个元素,我们使用 !numberObj[number] 来检查对象 numberObj 中是否已经存在相同的属性名。如果该属性不存在,我们将属性名设置为 true,将该元素添加到 uniqueNumbers 数组中。

由于对象的属性名是唯一的,因此重复的元素将被覆盖,从而实现了数组去重的效果。

利用空对象和单层遍历进行数组去重是一种简单且高效的方法,可以保持原始数组的顺序。这种方法的时间复杂度为 O(n),因为在对象中进行属性查找操作几乎是常数时间复杂度。

8.4 for 循环和 forEach 的性能哪个更好一点

在 JavaScript 中,for 循环和 forEach 方法都可以用于遍历数组,但它们在性能上有所差异。

通常情况下,使用简单的 for 循环性能更好,因为它的执行速度相对较快。这是因为 for 循环是一种底层的控制结构,而 forEach 是一个高级的数组方法,它需要更多的内部处理。

另外,与 forEach 方法相比,for 循环可以更好地与其他优化技术(如循环展开)结合使用,以提高执行效率。

以下是一个简单的性能对比示例:

    
javascript
插入代码复制代码
const numbers = [1, 2, 3, 4, 5];
const iterations = 1000000;

console.time('for loop');
for (let i = 0; i < iterations; i++) {
  for (let j = 0; j < numbers.length; j++) {
    const value = numbers[j];
    // ...
  }
}
console.timeEnd('for loop');

console.time('forEach');
for (let i = 0; i < iterations; i++) {
  numbers.forEach((value) => {
    // ...
  });
}
console.timeEnd('forEach');

    

在上面的示例中,我们对包含 5 个元素的数组进行了 1000000 次遍历,并使用 console.time 和 console.timeEnd 测量了两种方法的执行时间。

根据测试结果,for 循环往往比 forEach 方法更快,因为它在执行上没有额外的开销。但是,这只是一种一般情况,并不意味着 for 循环始终比 forEach 性能更好。实际性能还可能受到其他因素的影响,如具体的代码逻辑、数据规模等。

以下情况forEach的性能更好

在某些情况下,forEach 可能比 for 循环具有更好的性能,尤其是在某些特定的优化情况下。以下是一些可能导致 forEach 在性能上优于 for 循环的情况:

1、并行执行:在多线程或多处理器环境中,使用 forEach 可以更容易地实现并行执行,从而提高执行速度。这是因为 forEach 方法可以自动处理迭代器和回调函数的调度问题。 2、内部优化:某些 JavaScript 引擎/运行时环境可以对 forEach 进行内部优化,以提高执行效率。这些优化可能包括针对回调函数的优化、数组遍历的优化等。具体取决于 JavaScript 引擎的实现。 3、代码可读性与维护性:forEach 的语法更简洁,更具可读性,可以提高代码的可维护性。尤其是在编写复杂的逻辑或使用回调函数时,使用 forEach 可以减少编写的代码量,提高代码的清晰度和可读性。

值得注意的是,以上情况并不一定适用于所有 JavaScript 引擎和运行时环境。不同的引擎可能在执行 for 循环和 forEach 方法时有不同的优化策略。此外,具体的性能可能受到代码逻辑、数据规模和运行环境等因素的影响。

下面是一个示例,演示了在某些情况下 forEach 方法可能优于 for 循环的性能:

const numbers = [1, 2, 3, 4, 5];
let sum = 0;

console.time('for loop');
for (let i = 0; i < numbers.length; i++) {
  sum += numbers[i];
}
console.timeEnd('for loop');

sum = 0;

console.time('forEach');
numbers.forEach((value) => {
  sum += value;
});
console.timeEnd('forEach');

console.log('Sum:', sum);

在上面的示例中,我们计算了数组 numbers 中所有元素的和。通过使用 console.time 和 console.timeEnd,我们可以比较 for 循环和 forEach 方法的执行时间。

在某些 JavaScript 引擎中,在该示例中,forEach 方法可能性能更好,因为引擎可以优化回调函数的执行和数组遍历过程。

8.5 sort 是按照什么方式排序的

sort() 方法是 JavaScript 中的数组方法,用于对数组元素进行排序。默认情况下,sort() 方法使用字符串的 Unicode 值进行排序。

具体来说,sort() 方法会对数组进行原位排序,即直接修改原始数组,而不会创建新的排序后的数组。它会按照以下方式进行排序:

  • 如果数组元素是字符串,则按照每个字符的 Unicode 值进行排序。例如,['apple', 'banana', 'cherry'] 将以字母顺序排序为 ['apple', 'banana', 'cherry']
  • 如果数组元素是数字,则按照数字的大小进行排序。例如,[3, 1, 2] 将排序为 [1, 2, 3]
  • 如果数组元素是混合类型的值,则先将它们转换为字符串,然后按照字符串的排序规则进行排序。例如,[10, '2', 'a', '30'] 将排序为 ['10', '2', '30', 'a']

需要注意的是,由于 sort() 方法使用字符排序规则而不是数值排序规则,因此对于数字排序时,可能会得到与预期不同的结果,例如:[1, 10, 2] 排序后为 [1, 10, 2]

如果想要进行数值排序,可以传递一个比较函数给 sort() 方法,该函数会定义排序的逻辑。比较函数应接受两个参数,然后根据需要返回一个负数、零或正数,以确定元素之间的顺序。例如:

const numbers = [3, 1, 2];
numbers.sort((a, b) => a - b);
console.log(numbers); // 输出 [1, 2, 3]

在上面的示例中,比较函数 (a, b) => a - b 用于进行数值排序。a - b 返回一个负值、零或正值,取决于 a 与 b 的大小关系,以确定它们之间的顺序。

8.6 多维数组转换为一维数组

JavaScript 中有几种方法可以将多维数组转换为一维数组。以下是几种常用的方法:

8.6.1 flat

flat() 方法用于将多维嵌套数组平铺为一个新的一维数组。它接受一个可选的参数 depth,用于指定展开的层数。如果不提供 depth 参数,则默认展开所有层级。

const multiDimensionalArray = [1, [2, [3, 4]]];
const flattenedArray = multiDimensionalArray.flat();
console.log(flattenedArray); // 输出 [1, 2, 3, 4]

8.6.2 reduce

reduce() 方法可以使用递归功能将多维数组转换为一维数组。可以通过将初始值设置为空数组 [],并在每次迭代中将元素连接到结果数组中来实现。

const multiDimensionalArray = [1, [2, [3, 4]]];
const flattenedArray = multiDimensionalArray.reduce((acc, cur) => {
  return acc.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, []);
console.log(flattenedArray); // 输出 [1, 2, 3, 4]

8.6.3 递归函数

递归函数:可以使用自定义的递归函数来执行多维数组的平铺操作。递归函数遍历每个元素,如果元素是数组,则递归调用自身处理子数组,否则将元素添加到结果数组中。

function flatten(arr) {
  return arr.reduce((acc, cur) => {
    return acc.concat(Array.isArray(cur) ? flatten(cur) : cur);
  }, []);
}

const multiDimensionalArray = [1, [2, [3, 4]]];
const flattenedArray = flatten(multiDimensionalArray);
console.log(flattenedArray); // 输出 [1, 2, 3, 4]

这些方法都可以将多维数组转换为一维数组,你可以根据具体的需求和喜好选择其中的一种方法。

8.6.4 join 和 split

function flatten2DArray(array2D) {
  return array2D.join().split(',').map(Number);
}

const array2D = [[1, 2], [3, 4], [5, 6]];
const flattenedArray = flatten2DArray(array2D);
console.log(flattenedArray); // 输出 [1, 2, 3, 4, 5, 6]

在上述示例中,flatten2DArray() 函数接受一个二维数组 array2D 作为参数。首先,我们使用 join() 方法将二维数组转换为一个逗号分隔的字符串,将子数组连接起来。然后,我们使用 split(',') 方法将字符串拆分为字符串数组,其中的逗号作为分隔符。最后,我们使用 map(Number) 将字符串数组中的每个元素转换为数字类型的一维数组。

需要注意的是,这种方法假设二维数组中的元素都是可转换为数字的。如果二维数组包含其他类型的元素,例如字符串或对象,将会导致类型不一致的问题。

此外,这种实现方式依赖于使用逗号分隔元素的字符串表示。如果二维数组中的元素本身包含逗号,那么这种实现方式可能会失效。

8.6.5 toString 和 split

toString() 和 split() 方法来将二维数组转换为一维数组。下面是一种使用这两个方法的实现方式:

function flatten2DArray(array2D) {
  const string = array2D.toString();
  const flattenedArray = string.split(',').map(Number);
  return flattenedArray;
}

const array2D = [[1, 2], [3, 4], [5, 6]];
const flattenedArray = flatten2DArray(array2D);
console.log(flattenedArray); // 输出 [1, 2, 3, 4, 5, 6]

在上面的示例中,flatten2DArray() 函数接受一个二维数组 array2D 作为参数。首先,我们使用 toString() 方法将二维数组转换为一个逗号分隔的字符串。然后,我们使用 split(',') 方法将字符串拆分为字符串数组,其中的逗号作为分隔符。最后,我们使用 map(Number) 将字符串数组中的每个元素转换为数字类型的一维数组。

需要注意的是,这种方法假设二维数组中的元素都是可转换为数字的。如果二维数组包含其他类型的元素,例如字符串或对象,将会导致类型不一致的问题。

此外,这种实现方式依赖于使用逗号分隔元素的字符串表示。如果二维数组中的元素本身包含逗号,那么这种实现方式可能会失效。

8.6.6 广度优先遍历

8.6.6.1 广度优先遍历是什么

广度优先遍历(BFS)是一种遍历图或树的算法,它按照层级遍历节点,从起始节点开始,逐层遍历至最后一层。

8.6.6.2 广度优先遍历实现二维数组转一维数组步骤

要使用广度优先遍历将二维数组转换为一维数组,你可以按照以下步骤实现:

创建一个队列(queue)并将初始二维数组的所有元素添加到队列中。

创建一个空数组(flattenedArray)来存储遍历的结果。

当队列不为空时,循环执行以下步骤:

  • 从队列中取出队首元素(current)。
  • 如果 current 是一个数组,则将它的所有元素添加到队列的末尾。
  • 否则,将 current 添加到 flattenedArray 中。

返回 flattenedArray,它即为转换后的一维数组。

8.6.6.3 示例
function flattenArrayBFS(array2D) {
  const queue = [...array2D];
  const flattenedArray = [];

  while (queue.length) {
    const current = queue.shift();

    if (Array.isArray(current)) {
      queue.push(...current);
    } else {
      flattenedArray.push(current);
    }
  }

  return flattenedArray;
}

const array2D = [[1, 2], [3, [4, 5]], [6]];
const flattenedArray = flattenArrayBFS(array2D);
console.log(flattenedArray); // 输出 [1, 2, 3, 4, 5, 6]

在上面的示例中,flattenArrayBFS() 函数接受一个二维数组 array2D 作为参数。使用广度优先遍历的方法,我们按照步骤描述中的逻辑将二维数组转换为一维数组。遍历过程中,我们将每个遍历的元素添加到 flattenedArray 中,并根据元素的类型分别添加到队列或结果数组中。

最终,函数返回 flattenedArray,其中包含转换后的一维数组。

需要注意的是,广度优先遍历保证按照层级遍历节点,因此转换后的一维数组的顺序与原始二维数组中节点的顺序保持一致。

8.6.7 深度优先遍历

8.6.7.1 深度优先遍历是什么

深度优先遍历(DFS)是一种遍历图或树的算法,它通过优先访问节点的子节点,直到子节点都被遍历完才回溯到上一层节点继续遍历。

8.6.7.2 深度优先遍历实现实维数组转一维数组步骤

创建一个空数组(flattenedArray)来存储遍历的结果。

创建一个递归函数(traverseArray),接受一个二维数组(arr)作为参数。

在递归函数中,遍历二维数组的每个元素:

  • 如果元素是数组,则递归地调用 traverseArray 函数来遍历该子数组。
  • 否则,将元素添加到 flattenedArray 中。

调用递归函数 traverseArray,将初始二维数组作为参数传入。

返回 flattenedArray,它即为转换后的一维数组。

8.6.7.3 示例
function flattenArrayDFS(array2D) {
  const flattenedArray = [];

  function traverseArray(arr) {
    for (let i = 0; i < arr.length; i++) {
      if (Array.isArray(arr[i])) {
        traverseArray(arr[i]);
      } else {
        flattenedArray.push(arr[i]);
      }
    }
  }

  traverseArray(array2D);
  return flattenedArray;
}

const array2D = [[1, 2], [3, [4, 5]], [6]];
const flattenedArray = flattenArrayDFS(array2D);
console.log(flattenedArray); // 输出 [1, 2, 3, 4, 5, 6]

在上面的示例中,flattenArrayDFS() 函数接受一个二维数组 array2D 作为参数。通过使用深度优先遍历的方式,我们在 traverseArray 函数中递归地遍历二维数组的每个元素,并将它们添加到 flattenedArray 中。

最后,函数返回 flattenedArray,其中包含转换后的一维数组。

需要注意的是,深度优先遍历会优先访问节点的子节点,因此转换后的一维数组的顺序可能与原始二维数组中节点的顺序不同。在示例中,我们从左到右按深度优先的顺序遍历子数组的元素,并依次添加到结果数组中

8.7 for...in和for...of的比较

8.7.1 相同点

尽管 for...in 和 for...of 是两种不同的循环语句,但它们也有一些相同点:

迭代可迭代对象:for...in 和 for...of 都用于迭代可迭代对象,例如数组、字符串、Set 和 Map。

迭代顺序:for...in 和 for...of 都按照被迭代对象的顺序执行迭代。具体顺序取决于被迭代对象的迭代规则。

迭代数组: 虽然 for...of 是更常用的迭代数组的方式,但 for...in 同样可以用于迭代数组,并返回数组的索引。

不迭代原型链上的属性:for...in 和 for...of 都不会迭代对象原型链上的属性或方法,只遍历对象自身的属性或数组自身的元素。

尽管 for...in 和 for...of 有一些相似之处,但它们的用途和行为仍有明显的区别。在实际编程中,你需要根据具体的需求选择适当的循环语句来完成相应的迭代任务。

8.7.2 不同点

for...in 和 for...of 是 JavaScript 中两种不同的循环语句,用于迭代对象或数组的元素。它们之间有以下区别:

  • for...in 循环用于迭代对象的可枚举属性,它遍历对象的键(key)。在每次迭代中,for...in 返回当前属性的键名。
const obj = { a: 1, b: 2, c: 3 };

for (let key in obj) {
  console.log(key); // 输出 "a", "b", "c"
}
  • for...of 循环用于迭代可迭代对象(如数组、字符串、Set、Map 等),它遍历对象的值(value)。在每次迭代中,for...of 返回当前对象的元素的值。
const arr = [1, 2, 3];

for (let value of arr) {
  console.log(value); // 输出 1, 2, 3
}

需要注意以下几点:

  • for...in 循环也可以用于迭代数组,但它返回索引(字符串类型)而不是实际的值。
  • for...of 循环不支持普通对象,因为普通对象不是可迭代的。但你可以使用 for...in 循环遍历普通对象的属性。
  • for...of 循环可以与 breakcontinue 和 return 语句一起使用,而 for...in 循环不支持这些语句。

总结来说,for...in 循环用于迭代对象的属性(键),而 for...of 循环用于迭代可迭代对象的值(元素)。根据具体的使用场景和需求,你可以选择使用适合的循环语句。

9. promise

9.1. promise基础

9.1.1 promise是什么

Promise 是 JavaScript 中用于处理异步操作的对象,它提供了一种更优雅和便捷的方式来处理异步代码。Promise 支持链式的操作,简化了回调函数嵌套的问题,使得异步代码更易于理解和维护。

9.1.2 promise的状态

一个 Promise 对象代表了一个异步操作的最终结果。它有以下三种状态:

Pending(进行中): 初始状态,表示异步操作尚未完成。 Fulfilled(已成功): 表示异步操作已经成功完成,可以获取到最终的结果。 Rejected(已失败): 表示异步操作遇到错误或失败。

9.1.3 promise处理异步的步骤

使用 Promise 可以按照以下步骤来处理异步操作:

  1. 创建一个 Promise 对象,传入一个执行器函数(executor function),该函数带有两个参数:resolve 和 reject
  2. 在执行器函数中执行异步操作。如果异步操作成功完成,则调用 resolve 函数,并传递最终的结果;如果异步操作失败,则调用 reject 函数,并传递失败的原因。
  3. 使用 then() 方法来处理异步操作的成功情况,该方法接受一个回调函数作为参数,在异步操作成功时被调用,并接收最终的结果作为参数。
  4. 可以使用 catch() 方法来处理异步操作的失败情况,该方法接受一个回调函数作为参数,在异步操作失败时被调用,并接收失败的原因作为参数。

9.2 promise的基本用法

const promise = new Promise((resolve, reject) => {
  // 模拟异步操作
  setTimeout(() => {
    const randomNumber = Math.random();
    if (randomNumber < 0.5) {
      resolve(randomNumber);
    } else {
      reject('操作失败');
    }
  }, 1000);
});

promise
  .then((result) => {
    console.log('操作成功:', result);
  })
  .catch((error) => {
    console.log('操作失败:', error);
  });

在上面的示例中,我们创建了一个 Promise 对象,并在执行器函数中使用 setTimeout 模拟了一个异步操作。根据生成的随机数,我们通过调用 resolve 或 reject 来表示操作的成功或失败。

在 then() 方法中,我们传入了一个回调函数,在异步操作成功后会被调用,并接收最终的结果作为参数。在 catch() 方法中,我们传入了一个回调函数,在异步操作失败时会被调用,并接收失败的原因作为参数。

通过 Promise 的链式调用,我们可以在代码中更清晰地表达异步操作的顺序和逻辑,避免了回调函数嵌套的问题,使得代码更易于阅读和维护。

除了 then() 和 catch() 方法外,Promise 还提供了其他方法如 finally() 和 Promise.all() 等,用于处理更复杂的异步操作场景。

9.3 使用promise实现sleep函数(延迟函数)

使用 Promise 可以很容易地实现一个 sleep 函数(延迟函数),可以暂停代码的执行一段时间。下面是一个使用 Promise 实现的 sleep 函数的示例:

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 使用 sleep 函数暂停 1 秒钟
sleep(1000).then(() => {
  console.log('延迟 1 秒钟后执行的代码');
});

在上面的示例中,我们定义了一个 sleep 函数,它接受一个参数 ms,代表要延迟的毫秒数。该函数内部返回一个 Promise 对象,在一定的延迟时间之后解析该 Promise

通过调用 setTimeout 函数,我们将 resolve 函数作为回调函数传递给 setTimeout,从而在延迟时间到达后触发解析 Promise 的操作。

最后,我们可以通过调用 sleep 函数并使用 then() 方法来执行延迟后的代码。

例如,在示例中我们延迟了 1 秒钟,然后在 then() 方法中打印了一条消息。

使用 sleep 函数可以轻松地在异步操作中添加延迟,例如等待 API 请求响应、模拟动画效果等。

9.4 promise中的异步和同步

在 Promise 中,构造函数是同步的,而 thencatch 和 finally 方法是异步的。

构造函数是同步的:当创建一个 Promise 对象时,构造函数会立即执行。它接受一个执行器函数作为参数,并在创建对象时立即执行该函数。因此,构造函数中的代码会立即执行,不会等待异步操作的完成。

then 方法是异步的then 方法用于处理 Promise 对象的成功情况。当 Promise 对象的状态变为 Fulfilled(已成功)时,then 方法中的回调函数会被添加到异步任务队列中,等待当前执行栈为空时才会被调用。

catch 方法是异步的:catch 方法用于处理 Promise 对象的失败情况。当 Promise 对象的状态变为 Rejected(已失败)时,catch 方法中的回调函数会被添加到异步任务队列中,等待当前执行栈为空时才会被调用。

finally 方法是异步的:finally 方法用于添加在 Promise 完成之后执行的回调函数,无论 Promise 是成功还是失败,该回调函数都会执行。finally 方法中的回调函数会被添加到异步任务队列中,等待当前执行栈为空时才会被调用。

总结来说,Promise 构造函数是同步的,而 thencatch 和 finally 方法是异步的。这是因为 thencatch 和 finally 方法中的回调函数是在 Promise 对象的状态改变后异步执行的,以确保它们在合适的时机被调用,并且不会阻塞主线程的执行。

9.5 如何取消一个promise

取消一个 Promise 在原生的 Promise API 中是不直接支持的。通常,Promise 只有两种可能的状态,即已解决(resolved)或已拒绝(rejected),一旦进入这些状态,就不能直接取消它们。

但是,你可以实现自定义的取消机制来处理 Promise。一种常用的方法是使用一个标志变量来判断是否应该取消 Promise 的执行。下面是一个示例:

let isCanceled = false;

const promise = new Promise((resolve, reject) => {
  // 异步操作
  // 在每一步操作之前都检查 isCanceled 变量
  if (isCanceled) {
    reject(new Error('操作被取消'));
    return;
  }

  // 执行异步操作
  // 当异步操作成功完成时,调用 resolve()
  // 当异步操作失败时,调用 reject()
});

// 取消 Promise 的执行
isCanceled = true;

在上面的示例中,我们使用一个名为 isCanceled 的变量作为标志来判断是否应该取消 Promise 的执行。在每一步具有可能被取消的异步操作之前,我们都检查 isCanceled 变量的值。如果 isCanceled 为 true,我们就立即将 Promise 的状态设置为拒绝,并传递一个包含取消原因的错误。

请注意,这种自定义的取消机制需要在异步操作的每一步进行检查,以确保在取消时及时中断正在执行的操作,如果没有及时中断,异步操作可能会继续执行,直到成功或失败。

这只是一种手动实现的取消机制示例,实际中的取消机制可能需要根据具体的异步操作类型和需求进行相应的定制。

9.6 多个promise如何获取第一个成功的promise

你可以使用 Promise.race() 方法来获取多个 Promise 中最先解决(即成功)的 Promise 的结果。Promise.race() 接受一个 Promise 数组作为参数,并返回一个新的 Promise,该 Promise 的状态和第一个解决的 Promise 的状态相同。

下面是一个示例来演示如何使用 Promise.race() 来获取多个 Promise 中最先解决的 Promise 的结果:

const promises = [promise1, promise2, promise3];

const firstResolvedPromise = Promise.race(promises);

firstResolvedPromise
  .then(result => {
    console.log('第一个成功的 Promise:', result);
  })
  .catch(error => {
    console.log('所有 Promise 都被拒绝:', error);
  });

在上面的示例中,我们有一个包含多个 Promise 的数组 promises。我们使用 Promise.race() 方法将这些 Promise 传递给它,并创建一个新的 Promise firstResolvedPromise

firstResolvedPromise 会在数组中的 Promise 中最先解决的一个 Promise 成功时解决,并传递这个 Promise 的结果。如果所有的 Promise 都被拒绝(失败),那么 firstResolvedPromise 会被拒绝,并传递一个包含拒绝原因的错误。

最后,我们可以通过在 then() 方法中处理 firstResolvedPromise 的解决回调函数来获取第一个成功的 Promise 的结果。在 catch() 方法中处理拒绝的情况,如果所有的 Promise 都被拒绝,会捕获到错误。

使用 Promise.race() 方法,你可以同时执行多个 Promise,并获取最先解决的 Promise 的结果,从而有效地处理并发异步操作的结果。

9.7 获取多个 Promise 所有的返回结果

如果你想要获取多个 Promise 所有的返回结果,无论是成功还是失败,你可以使用 Promise.all() 方法。Promise.all() 方法接受一个 Promise 数组作为参数,并返回一个新的 Promise,该 Promise 的状态会在所有的 Promise 都解决(成功或失败)后解决,并将所有 Promise 的结果作为一个数组传递。

下面是一个示例来演示如何使用 Promise.all() 来获取多个 Promise 的所有返回结果:

const promises = [promise1, promise2, promise3];

const allPromises = Promise.all(promises);

allPromises
  .then(results => {
    console.log('所有 Promise 的结果:', results);
  })
  .catch(error => {
    console.log('至少有一个 Promise 被拒绝:', error);
  });

在上面的示例中,我们有一个包含多个 Promise 的数组 promises。我们使用 Promise.all() 方法将这些 Promise 传递给它,并创建一个新的 Promise allPromises

allPromises 会在所有的 Promise 都解决后解决,并将所有 Promise 的解决结果作为一个数组传递。如果其中至少有一个 Promise 被拒绝(失败),那么 allPromises 会被拒绝,并传递一个包含拒绝原因的错误。

最后,我们可以通过在 then() 方法中处理 allPromises 的解决回调函数来获取包含所有 Promise 结果的数组。在 catch() 方法中处理拒绝的情况,如果至少有一个 Promise 被拒绝,会捕获到错误。

使用 Promise.all() 方法,你可以同时执行多个 Promise,并等待它们全部解决,然后获取所有 Promise 的结果。

9.8 promise的静态方法有哪些

Promise 的静态方法是直接在 Promise 构造函数上定义的方法,可以通过 Promise 对象本身调用,而不是通过实例调用。下面列出了一些常用的 Promise 静态方法:

Promise.all(iterable):接受一个可迭代对象,并返回一个新的 Promise,只有当可迭代对象中的所有 Promise 都解决时,该 Promise 才会解决。如果可迭代对象中的任何 Promise 被拒绝,该 Promise 会被拒绝,并传递该 Promise 的拒绝原因。

Promise.race(iterable):接受一个可迭代对象,并返回一个新的 Promise,该 Promise 的状态将由可迭代对象中最先解决的 Promise 决定。如果最先解决的 Promise 是解决的,该 Promise 也会解决;如果最先解决的 Promise 是拒绝的,该 Promise 也会被拒绝。

Promise.resolve(value):返回一个已解决的 Promise,以给定的值 value 进行解决。如果 value 是一个 Promise,则返回该 Promise。

Promise.reject(reason):返回一个已拒绝的 Promise,以给定的拒绝原因 reason 进行拒绝。

Promise.allSettled(iterable):接受一个可迭代对象,并返回一个新的 Promise,该 Promise 在可迭代对象中的所有 Promise 都解决或拒绝后解决。返回的 Promise 会包含一个数组,其中包含着所有 Promise 的解决结果对象,每个对象包含了 status 和 value 或 reason 属性。

Promise.any(iterable):接受一个可迭代对象,并返回一个新的 Promise,只要可迭代对象中的某个 Promise 解决,该 Promise 就会解决。如果可迭代对象中的所有 Promise 都被拒绝,该 Promise 会被拒绝,并传递一个包含所有拒绝原因的数组。

这些是一些常见的 Promise 静态方法,它们提供了对多个 Promise 进行组合和处理的便捷功能。

9.9 Promise.then 的第二个是什么、 .catch 有什么区别

Promise.then() 方法有两个参数:第一个参数是处理 Promise 解决(成功)情况的回调函数,第二个参数是处理 Promise 拒绝(失败)情况的回调函数。

promise.then(onResolved, onRejected);
  • onResolved:是一个可选的回调函数,在 Promise 解决时被调用。它接收 Promise 的解决值作为参数,并处理该值。如果不提供 onResolved,则解决值会被传递给后续的 then 方法,并最终传递到 Promise 链的最终解决处理函数。
  • onRejected:也是一个可选的回调函数,在 Promise 被拒绝时被调用。它接收 Promise 的拒绝原因作为参数,并处理该原因。如果不提供 onRejected,则拒绝原因会被传递给 Promise 链中最近的 catch 方法来处理。

区别:

  1. 触发时机:then 方法的第二个参数 onRejected 只有在 Promise 被拒绝时才会被调用,而 catch 方法是 then(undefined, onRejected) 的一种简写形式,用于捕获 Promise 链中的拒绝情况。
  2. 错误传递:使用多个 then 方法串联 Promise 时,如果中间某个 then 方法的回调函数抛出了异常(或返回了拒绝的 Promise),后续的 then 方法将会直接跳过,而不会被调用;但如果使用 catch 方法捕获错误,后续的 then 方法会继续执行,可以处理错误或继续操作。

综上所述,除非你有特殊的处理需求,否则使用 catch 方法来处理 Promise 的拒绝情况会更加简洁和直观。

9.10 如果.then中的参数不是函数会怎样

如果在 Promise.then() 方法中的参数不是函数,那么 JavaScript 引擎会忽略这个非函数参数,并将控制权传递给 Promise 链中的下一个 then 方法。

下面是一个示例来说明这种情况:

promise.then('Not a function')
  .then(value => {
    console.log(value);
  })
  .catch(error => {
    console.log('捕获错误:', error);
  });

在上面的示例中,将一个非函数作为参数传递给了第一个 then 方法。由于这个参数不是函数,JavaScript 引擎会忽略它,并继续执行 Promise 链中的下一个 then 方法。

如果在下一个 then 方法中,前一个 Promise 的解决值被成功传递过来,它会被输出。如果在之前的任何一个 Promise 被拒绝,控制权将被传递到 catch 方法中,以捕获错误。

总结来说,如果 Promise.then() 方法中的参数不是函数,它会被忽略,并继续执行 Promise 链中的下一个方法。这可能会导致意外的行为,因此最好确保在 then 方法中传递的参数是一个函数。

9.11 .finally后面继续跟了个.then,那么这个then里面的值是什么?

在 Promise 中,.finally() 方法返回一个新的 Promise,该 Promise 在前一个 Promise 解决或被拒绝后都会执行指定的回调函数。.finally() 方法的回调函数不接收任何参数,也不会改变前一个 Promise 的解决值或拒绝原因。

如果在 .finally() 后面继续跟一个 .then() 方法,那么这个 .then() 方法将会接收到前一个 Promise 的解决值或拒绝原因,并在 .finally() 方法完成后执行。

下面是一个示例来演示这种情况:

promise
  .finally(() => {
    console.log('Finally 执行');
  })
  .then(value => {
    console.log('解决值:', value);
  })
  .catch(error => {
    console.log('拒绝原因:', error);
  });

在上面的示例中,.finally() 方法中指定的回调函数会在前一个 Promise 解决或被拒绝后执行,打印出 "Finally 执行"。然后,.then() 方法将接收前一个 Promise 的解决值,并打印出 "解决值"。如果前一个 Promise 被拒绝,catch 方法将会执行,并打印出 "拒绝原因"。

总结来说,.finally() 方法后面跟着的 .then() 方法将接收前一个 Promise 的解决值,并在 .finally() 方法完成后执行。

9.12 .all和.race在传入的数组有第一个抛出异常的时候,其他异步任务还会继续执行吗

在 Promise 中,无论是 Promise.all() 还是 Promise.race(),如果传入的 Promise 数组中的任何一个 Promise 抛出异常(即拒绝并抛出错误),其他尚未完成的异步任务也会立即被中止,不会继续执行。

具体来说:

  • Promise.all():如果传入的 Promise 数组中的任何一个 Promise 被拒绝并抛出异常,Promise.all() 返回的 Promise 会立即被拒绝,并传递这个异常。其他尚未完成的异步任务将会被中止,不会继续执行。
  • Promise.race():如果传入的 Promise 数组中的第一个解决(成功)的 Promise 触发了异常,并抛出错误,Promise.race() 返回的 Promise 会立即被拒绝,并传递这个异常。其他尚未完成的异步任务将会被中止,不会继续执行。

以下是示例来说明这个行为:

const promises = [
  Promise.resolve(1),
  Promise.reject(new Error('出错了')),
  Promise.resolve(3)
];

Promise.all(promises)
  .then(results => {
    console.log('全部解决:', results);
  })
  .catch(error => {
    console.log('至少有一个 Promise 被拒绝:', error);
  });

在上述示例中,传入的 Promise 数组中的第二个 Promise 被拒绝并抛出了一个错误。由于其中的一个 Promise 被拒绝,Promise.all() 返回的 Promise 会立即被拒绝,并传递这个错误。因此,在 .catch() 方法中错误会被捕获,输出 "至少有一个 Promise 被拒绝: Error: 出错了"。

总结来说,在 Promise.all() 和 Promise.race() 中,如果传入的 Promise 数组有任何一个 Promise 抛出异常,其他尚未完成的异步任务会被中止,不会继续执行。

10. JS中的this

JavaScript 中的 this 关键字表示当前执行代码的对象。具体来说,this 的值取决于调用函数的方式,可以根据不同的情况分为四种:

全局上下文中的 this:当代码在全局作用域中执行时,this 指向全局对象(浏览器环境中指向 window 对象)。

函数调用中的 this:当函数作为一个独立的函数调用时,this 指向全局对象或者 undefined(严格模式下)。

对象方法中的 this:当函数作为某个对象的方法调用时,this 指向调用该方法的对象。

构造函数中的 this:当使用 new 关键字调用函数时,this 指向新创建的实例对象。

需要注意的是,this 的值在函数定义时是无法确定的,只有在函数被调用时才能确定,并且每次函数调用时 this 的值可能会不同。

此外,可以使用 call()apply() 或者 bind() 方法来改变函数中 this 的指向。这些方法可以显式地将一个对象绑定到函数中的 this

总结:JavaScript 中的 this 关键字是一个特殊的指针,表示当前执行代码的对象。它的值取决于函数的调用方式,可以通过四种不同的方式来确定 this 的值。

11. call、apply、bind

11.1 相同

callapply 和 bind 都是 JavaScript 中用来改变函数中的 this 指向的方法

11.2 不同

参数传递方式:call 方法的参数是逐个传入,而 apply 方法的参数是以数组或类数组对象的形式传入。例如:

function example(arg1, arg2) {
  console.log(arg1, arg2);
}

example.call(null, 'arg1', 'arg2'); // 输出: arg1 arg2
example.apply(null, ['arg1', 'arg2']); // 输出: arg1 arg2

返回值:call 和 apply 方法会立即调用函数并返回执行结果,而 bind 方法则返回一个新的函数而不会立即执行。

11.3 主要区别

callcall 方法通过指定的参数列表直接调用函数。第一个参数是要绑定给 this 的对象,后面的参数是传递给函数的参数列表。示例:func.call(obj, arg1, arg2, ...) applyapply 方法和 call 类似,区别在于参数的传递方式。第一个参数也是要绑定给 this 的对象,但是第二个参数是一个数组或类数组对象,其中的元素会作为参数传递给函数。示例:func.apply(obj, [arg1, arg2, ...]) bindbind 方法不会立即执行函数,而是创建一个新函数,并将原函数中的 this 绑定到指定的对象。bind 方法可以传递参数给函数,这些参数会作为绑定函数的预置参数,之后调用绑定函数时会自动传递其他参数。示例:const boundFunc = func.bind(obj, arg1, arg2, ...),然后可以通过 boundFunc(arg3, arg4, ...) 调用新函数。.

11.4 使用场景

  • call: 适合于已知参数数量且需要立即调用的场景,可以指定 this 和参数列表直接调用函数。
  • apply: 适合于已知参数数量且需要立即调用的场景,可以指定 this 和参数数组直接调用函数。
  • bind: 适合于需要提前绑定 this 和部分参数,生成一个新函数以便稍后调用的场景。常用于事件处理程序、回调函数等场景。

下面是一些具体的使用示例:

// 使用 call 方法改变函数中的 this 指向
const obj1 = { name: 'Alice' };
function greet() {
  console.log(`Hello, ${this.name}!`);
}
greet.call(obj1);  // 输出: Hello, Alice!

// 使用 apply 方法改变函数中的 this 指向
const obj2 = { name: 'Bob' };
function farewell(message) {
  console.log(`${message}, ${this.name}!`);
}
farewell.apply(obj2, ['Goodbye']); // 输出: Goodbye, Bob!

// 使用 bind 方法改变函数中的 this 指向,并预设参数
const obj3 = { name: 'Carol' };
function introduce(age, occupation) {
  console.log(`I'm ${this.name}, ${age} years old, working as a ${occupation}.`);
}
const boundIntroduce = introduce.bind(obj3, 25); // 预设 age 参数
boundIntroduce('developer'); // 输出: I'm Carol, 25 years old, working as a developer.

在上面的示例中,我们使用了 callapply 和 bind 方法来改变函数中的 this 指向。根据不同的场景和需求,选择合适的方法可以更灵活地使用函数。

11.5 原生实现

11.5.1 实现 call 方法

Function.prototype.myCall = function(context, ...args) {
  context = context || window;
  const fn = Symbol('fn');
  context[fn] = this;
  const result = context[fn](...args);
  delete context[fn];
  return result;
};

在这个实现中,我们给 Function.prototype 添加了一个名为 myCall 的方法,该方法接收一个 context 参数,表示要绑定的 this 对象,以及一系列 args 参数作为传递给函数的参数列表。在方法内部,我们首先判断 context 是否为 null 或 undefined,如果是则使用全局对象 window 作为 this 对象。接着,我们创建一个独一无二的 Symbol 类型的属性 fn,将当前函数 this 绑定到 context[fn] 上,并调用该函数,将结果保存到 result 变量中。最后,我们删除 context[fn] 属性并返回结果。

11.5.2 实现 apply 方法

Function.prototype.myApply = function(context, args) {
  context = context || window;
  const fn = Symbol('fn');
  context[fn] = this;
  const result = context[fn](...args);
  delete context[fn];
  return result;
};

在这个实现中,我们和 myCall 方法类似,只是 args 参数是以数组或类数组对象的形式传入,我们直接将其传递给函数即可。

11.5.3 实现 bind 方法

Function.prototype.myBind = function(context, ...args) {
  const self = this;
  return function(...restArgs) {
    return self.apply(context, args.concat(restArgs));
  };
};

在这个实现中,我们同样给 Function.prototype 添加了一个名为 myBind 的方法,该方法接收一个 context 参数,表示要绑定的 this 对象,以及一系列 args 参数作为预置参数。在方法内部,我们首先将当前函数 this 绑定到 self 变量上,并返回一个新函数。在新函数中,我们调用原函数 self,并传递来自 args 和后续调用时传入的参数 restArgs。这里使用了 concat 方法将两个数组合并成一个新数组。

这些实现并不完整,真正的 callapply 和 bind 方法还需要处理更多的情况,比如在绑定函数使用 new 运算符创建新实例时,this 的优先级问题。但这些实现可以满足一般的使用场景。

12. 函数防抖和函数节流

节流(throttling)用于限制某个函数在一定时间间隔内的执行次数

防抖(debouncing)用于限制某个函数在一定时间间隔内的连续执行次数。

具体使用以及使用js实现

13. js的精度丢失

JavaScript中的精度丢失问题是由于浮点数的内部表示方式引起的。由于计算机内部使用二进制表示数字,而浮点数在二进制中无法精确表示,因此在进行浮点数运算时可能会出现精度丢失的情况。

例如,下面的代码将会导致精度丢失问题:

var x = 0.1 + 0.2;
console.log(x); // 输出结果为 0.30000000000000004

在这个例子中,0.1和0.2都无法精确表示为二进制小数,因此它们的相加结果也无法精确表示。

解决JavaScript中的精度丢失问题可以采用以下几种方法:

使用整数进行计算: 将浮点数转换为整数,进行计算后再转换回浮点数。例如,可以将小数放大一定倍数,进行整数运算后再除以倍数得到结果。这种方法可以避免浮点数运算带来的精度问题。

使用第三方库: 一些第三方库,如Big.js和Decimal.js,提供了更精确的数学运算功能,可以避免JavaScript浮点数的精度问题。这些库通常提供了高精度的数学运算方法,可以处理大数和小数的运算。

使用toFixed方法: JavaScript提供了toFixed方法,可以将浮点数转换为指定精度的字符串表示。例如,可以使用toFixed(2)将浮点数保留两位小数。这种方法适用于只需要显示结果而不需要进行进一步计算的情况。

14. js的内存泄漏

JavaScript内存泄漏是指在程序中不再使用的内存空间没有被正确释放,导致内存占用不断增加,最终可能导致程序崩溃或变慢。以下是可能导致内存泄漏的常见情况和解决方法。

全局变量: 全局变量在脚本执行完后仍然存在于内存中。如果过多地使用全局变量,可能会导致大量内存泄漏。解决方法是在使用完毕后将全局变量设置为null或使用模块化方式避免全局变量的过多使用。

事件监听器: 如果页面中存在大量的事件监听器,没有正确地移除这些监听器,会导致内存泄漏。解决方法是在页面销毁前,手动移除所有事件监听器。

定时器: 使用setInterval或setTimeout设置的定时器,如果不正确地清理,会导致内存泄漏。解决方法是在不需要定时器时,调用clearInterval或clearTimeout手动清除定时器。

闭包: 闭包是指函数内部的函数可以访问外部函数的变量。如果闭包函数持有外部函数的引用,就会导致外部函数的内存无法释放。解决方法是在不再使用闭包时,手动解除对外部函数的引用。

循环引用: 如果两个对象相互引用,而且没有及时解除引用,就会导致内存泄漏。例如,一个对象持有另一个对象的引用,而后者又持有前者的引用。解决方法是在不再需要引用时,手动解除对象之间的引用。

DOM操作: 在页面中频繁进行DOM操作,但未正确地删除或重用DOM元素,会导致内存泄漏。解决方法是在不需要使用DOM元素时,手动删除或重用DOM元素。 未释放的资源:例如未关闭的文件、未释放的网络请求或数据库连接等都可能导致内存泄漏。解决方法是在不再需要使用这些资源时,正确地关闭或释放它们。

第三方库问题: 某些第三方库可能存在内存泄漏的问题,因此需要确保使用的第三方库是稳定和可靠的。

综上所述,内存泄漏是由于程序中未正确释放不再使用的内存空间而导致的。解决内存泄漏问题的关键在于正确管理和释放不再使用的资源,注意及时解除引用,并尽量避免在全局范围内创建变量

15. ajax的原理以及实现

15.1 原理

Ajax(Asynchronous JavaScript and XML)是一种用于在Web页面中异步地发送和接收数据的技术。它基于 Web 标准,使用 JavaScript 和 XML(也可以使用其他数据格式)来实现数据的异步通信。

Ajax 的原理是通过在Web页面中使用JavaScript代码,向服务器发送HTTP请求并接收响应,而无需刷新整个页面。它使用了浏览器提供的XMLHttpRequest对象来发送和接收数据。具体的实现步骤如下:

  1. 创建XMLHttpRequest对象:通过调用new XMLHttpRequest()来创建一个XMLHttpRequest对象。
  2. 设置回调函数:使用onreadystatechange属性设置一个回调函数,该函数在请求状态发生变化时会被调用。
  3. 发送请求:调用open()方法设置请求方法(GET或POST)、URL和是否异步等参数,然后调用send()方法发送请求。
  4. 处理响应:在回调函数中,可以通过readyState属性来获取请求的当前状态。当readyState值变为4,即请求已完成时,可以通过status属性来获取响应的状态码。通常,2xx表示成功,4xx或5xx表示出现错误。通过responseTextresponseXML属性,可以获取到服务器端返回的数据。
  5. 更新页面:根据服务器返回的数据,使用JavaScript代码将数据更新到页面的特定部分,而无需刷新整个页面。

需要注意的是,由于Ajax是通过JavaScript在后台与服务器进行通信,因此需要在服务器端提供对应的接口来处理请求并返回数据。

虽然Ajax的名字中包含XML,但实际上并不仅限于XML数据格式,也可以使用其他格式如JSON等。

15.2 实现步骤

当使用JavaScript实现Ajax时,可以使用XMLHttpRequest对象来发送和接收数据。以下是基本的步骤:

  1. 创建XMLHttpRequest对象:
var xhr = new XMLHttpRequest();
  1. 设置回调函数:
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    // 处理响应数据
    var response = xhr.responseText;
    console.log(response);
  }
};

在这个回调函数中,我们检查xhr对象的readyState属性,当它的值为4时表示请求已完成。然后,我们检查xhr对象的status属性,200表示请求成功。在这个条件下,我们可以处理来自服务器的响应数据。

  1. 发送请求:
xhr.open('GET', 'example.com/api/data', true);  // 设置请求方法、URL和是否异步
xhr.send();

在这个示例中,我们使用GET方法发送请求到example.com/api/data 将true作为第三个参数传递给open()方法,以启用异步请求。

如果需要发送POST请求,可以使用xhr.setRequestHeader()方法设置请求头,并在send()方法中传递要发送的数据。

  1. 处理响应: 在回调函数中,我们可以通过xhr对象的responseTextresponseXML属性来获取来自服务器的响应数据。

上述就是一个简单的Ajax实现的步骤。需要注意的是,不同浏览器可能对Ajax支持有所不同。为了更好地兼容各种浏览器,可以使用现代的JavaScript库(如jQuery)来处理Ajax请求。

15.3 示例

15.3.1. GET请求的Ajax方法示例:

// 创建XMLHttpRequest对象
var xhr = new XMLHttpRequest();

// 设置回调函数
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    // 处理响应数据
    var response = JSON.parse(xhr.responseText);
    console.log(response);
  }
};

// 发送GET请求
xhr.open('GET', 'example.com/api/data?id=123', true);
xhr.send();

    

15.3.2. POST请求的Ajax方法示例:

// 创建XMLHttpRequest对象
var xhr = new XMLHttpRequest();

// 设置回调函数
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    // 处理响应数据
    var response = JSON.parse(xhr.responseText);
    console.log(response);
  }
};

// 发送POST请求
xhr.open('POST', 'example.com/api/data', true);
xhr.setRequestHeader('Content-Type', 'application/json');  // 设置请求头
var data = {
  id: 123,
  name: 'John'
};
xhr.send(JSON.stringify(data));  // 将数据转换为JSON字符串并发送

需要注意的是,在实际开发中,可能需要对请求和响应进行错误处理,并根据需要处理其他请求头和参数。此外,为了更好地兼容各种浏览器,可以使用现代的JavaScript库(如jQuery)来处理Ajax请求。

16. Event Loop

Event LoopJavaScript的一种执行模型,用于处理异步操作。它是JavaScript单线程执行的基础,负责管理调度任务的执行顺序。

JavaScript中,存在两种类型的任务:同步任务异步任务。同步任务会按照代码的顺序依次执行,而异步任务则会被放入任务队列中等待执行。

Event Loop的主要作用是监听调用栈和任务队列,以确定何时执行异步任务。它由以下几个组件组成:

调用栈(Call Stack):用于存储执行上下文(函数调用)的栈结构。当执行一个函数时,会将其对应的执行上下文压入调用栈中,当函数执行完毕后,会将其执行上下文从调用栈中弹出。 任务队列(Task Queue):用于存储异步任务的队列结构。当异步任务完成后,会被放入任务队列中等待执行。 事件循环(Event Loop):负责监听调用栈和任务队列,以确定何时执行异步任务。当调用栈为空时,事件循环会从任务队列中取出一个任务,并将其对应的回调函数压入调用栈中执行。

事件循环的执行过程如下:

1、执行全局代码,将全局代码中的同步任务按照顺序压入调用栈中执行。 2、遇到异步任务时,将其回调函数交给对应的Web API处理,并继续执行后续的同步任务。 3、当异步任务完成后,会将其回调函数放入任务队列中。 4、当调用栈为空时,事件循环会从任务队列中取出一个任务,并将其对应的回调函数压入调用栈中执行。 5、重复步骤4,直到任务队列为空。

需要注意的是,事件循环是一个持续不断的过程,会不断地从任务队列中取出任务执行。这样就实现了JavaScript的异步执行机制,使得我们可以处理各种异步操作,如定时器、网络请求等。

总结起来,Event Loop是JavaScript的执行模型,通过监听调用栈和任务队列,实现了异步任务的执行顺序管理。它是JavaScript单线程执行的基础,使得我们可以编写出高效的异步代码

17. 把一个url拆解成origin、文件名、hash拆解成示例的格式

要将一个URL拆解为 origin、文件名和哈希,我们可以使用浏览器原生的URL对象来实现。下面是一个详细的步骤:

  1. 首先,创建一个URL对象,将URL作为参数传入。
const url = new URL("https://www.example.com/path/filename.html#hash");
  1. 接下来,可以通过URL对象的一些属性来获取拆解后的信息。
  • 获取 origin:可以使用URL对象的origin属性来获取URL的origin部分。
const origin = url.origin; // https://www.example.com
  • 获取文件名:可以使用URL对象的pathname属性来获取URL的路径部分,然后使用字符串的split方法来获取最后一个斜杠后的部分。
const pathname = url.pathname; // /path/filename.html
const fileName = pathname.split('/').pop(); // filename.html
  • 获取哈希:可以使用URL对象的hash属性来获取URL的哈希部分。注意,哈希部分包括#字符。
const hash = url.hash; // #hash