JavaScript 面试问题

190+ JavaScript 问答,采用测验形式,由前 FAANG 面试官解答
由前面试官解答
涵盖关键主题

Tired of scrolling through low-quality JavaScript interview questions? You’ve found the right place!

Our JavaScript interview questions are crafted by experienced ex-FAANG senior / staff engineers, not random unverified sources or AI.

With over 190+ questions covering everything from core JavaScript concepts to advanced JavaScript features (async / await, promises, etc.), you’ll be fully prepared.

Each quiz question comes with:

  • Concise answers (TL;DR): Clear and to-the-point solutions to help you respond confidently during interviews.
  • Comprehensive explanations: In-depth insights to ensure you fully understand the concepts and can elaborate when required. Don’t waste time elsewhere—start practicing with the best!
如果您正在寻找 JavaScript 编码问题 -我们也会为您提供:
Javascript 编码
  • 280+ JavaScript 编码问题
  • 类似于真实面试环境的基于浏览器的编码工作区
  • 来自 Big Tech 前面试官的参考解决方案
  • 自动化测试用例
  • 立即预览您的 UI 问题代码
开始使用
加入 50,000+ 工程师

JavaScript 中 `==` 和 `===` 的区别是什么?

主题
JavaScript

TL;DR

== 是抽象相等运算符,而 === 是严格相等运算符。== 运算符会在进行任何必要的类型转换后比较是否相等。=== 运算符不会进行类型转换,因此如果两个值不是同一类型,=== 将直接返回 false

运算符=====
名称(宽松)相等运算符严格相等运算符
类型转换
比较值和类型

相等运算符 (==)

== 运算符检查两个值是否相等,但如果这些值的类型不同,则会执行类型转换。这意味着 JavaScript 将尝试在进行比较之前将这些值转换为通用类型。

console.log(42 == '42'); // true
console.log(0 == false); // true
console.log(null == undefined); // true
console.log([] == false); // true
console.log('' == false); // true

在这些示例中,JavaScript 在进行比较之前将操作数转换为相同的类型。例如,42 == '42' 为 true,因为字符串 '42' 在比较之前被转换为数字 42

但是,当使用 == 时,可能会发生不直观的结果:

console.log(1 == [1]); // true
console.log(0 == ''); // true
console.log(0 == '0'); // true
console.log('' == '0'); // false

作为一般经验法则,永远不要使用 == 运算符,除非为了方便与 nullundefined 进行比较,其中 a == null 将在 anullundefined 时返回 true

var a = null;
console.log(a == null); // true
console.log(a == undefined); // true

严格相等运算符 (===)

=== 运算符,也称为严格相等运算符,检查两个值是否相等,而不执行类型转换。这意味着,只有当值和类型都相同时,比较才会返回 true。

console.log(42 === '42'); // false
console.log(0 === false); // false
console.log(null === undefined); // false
console.log([] === false); // false
console.log('' === false); // false

对于这些比较,不执行类型转换,因此如果类型不同,该语句将返回 false。例如,42 === '42'false,因为类型(数字和字符串)不同。

// 带有类型转换的比较 (==)
console.log(42 == '42'); // true
console.log(0 == false); // true
console.log(null == undefined); // true
// 不带类型转换的严格比较 (===)
console.log(42 === '42'); // false
console.log(0 === false); // false
console.log(null === undefined); // false

额外内容:Object.is()

JavaScript 中最后一个值比较操作是 Object.is() 静态方法。Object.is()=== 之间唯一的区别在于它们如何处理带符号的零和 NaN 值。=== 运算符(和 == 运算符)将数字值 -0+0 视为相等,但将 NaN 视为彼此不相等。

结论

  • 当您希望通过类型转换比较值(并了解其含义)时,请使用==。 实际上,相等运算符的唯一合理用例是在单个比较中同时检查nullundefined以方便使用。
  • 当您希望确保值和类型都相同时,请使用===,这在大多数情况下是更安全、更可预测的选择。

笔记

  • 建议使用 ===(严格相等)以避免类型强制转换的缺陷,这可能导致代码中出现意外行为和错误。它使您的比较意图更清晰,并确保您同时比较值和类型。
  • ESLint 的 eqeqeq 规则强制使用严格相等运算符 ===!==,甚至提供了一个选项,始终强制使用严格相等,除非与 null 字面量进行比较。

延伸阅读

JavaScript 变量 `null`、`undefined` 和未声明的区别是什么?

如何检查这些状态?
主题
JavaScript

TL;DR

特性nullundefined未声明
含义由开发人员显式设置,表示变量没有值变量已声明但未赋值变量根本未声明
类型(通过 typeof 运算符)'object''undefined''undefined'
等式比较null == undefinedtrueundefined == nulltrue抛出 ReferenceError

未声明

未声明 变量是在您将值分配给先前未使用 varletconst 创建的标识符时创建的。未声明的变量将在全局范围内定义,在当前范围之外。在严格模式下,当您尝试分配给未声明的变量时,将抛出 ReferenceError。未声明的变量和全局变量一样糟糕。不惜一切代价避免它们!要检查它们,请将其用法包装在 try/catch 块中。

function foo() {
x = 1; // 在严格模式下抛出 ReferenceError
}
foo();
console.log(x); // 1 (如果不在严格模式下)

对未声明的变量使用 typeof 运算符将给出 'undefined'

console.log(typeof y === 'undefined'); // true

undefined

一个undefined的变量是已声明但未赋值的变量。它的类型是undefined。如果一个函数没有返回值,并且它的结果被分配给一个变量,那么该变量也将具有undefined值。要检查它,请使用严格相等运算符(===)或typeof进行比较,这将给出'undefined'字符串。请注意,您不应该使用宽松相等运算符(==)进行检查,因为它在值为null时也会返回true

let foo;
console.log(foo); // undefined
console.log(foo === undefined); // true
console.log(typeof foo === 'undefined'); // true
console.log(foo == null); // true. 错误,不要使用它来检查一个值是否为 undefined!
function bar() {} // 如果没有任何返回,则返回 undefined。
let baz = bar();
console.log(baz); // undefined

null

一个 null 的变量将被显式地赋值为 null 值。它表示没有值,并且与 undefined 的不同之处在于它已被显式赋值。要检查 null,只需使用严格相等运算符进行比较。请注意,与上述类似,您不应该使用松散相等运算符 (==) 进行检查,因为它在值为 undefined 时也会返回 true

const foo = null;
console.log(foo === null); // true
console.log(typeof foo === 'object'); // true
console.log(foo == undefined); // true. 错误,不要使用它来检查一个值是否为 null!

注意事项

  • 养成一个好习惯,永远不要让你的变量未声明或未赋值。如果您不打算使用它们,在声明后显式地将 null 赋值给它们。
  • 始终在变量使用前显式声明它们以防止错误。
  • 在您的工作流程中使用一些静态分析工具(例如 ESLint、TypeScript 编译器),将启用您未引用未声明变量的检查。

实践

在 GreatFrontEnd 上练习实现 检查 nullundefined 的类型实用程序

延伸阅读

JavaScript 中`.call` 和 `.apply` 有什么区别?

主题
JavaScript

TL;DR

.call.apply 都用于使用特定的 this 上下文和参数来调用函数。主要区别在于它们接受参数的方式:

  • .call(thisArg, arg1, arg2, ...):单独获取参数。
  • .apply(thisArg, [argsArray]):将参数作为数组获取。

假设我们有一个函数 add,可以使用以下方式使用 .call.apply 调用该函数:

function add(a, b) {
return a + b;
}
console.log(add.call(null, 1, 2)); // 3
console.log(add.apply(null, [1, 2])); // 3

Call vs Apply

.call.apply 都用于调用函数,第一个参数将用作函数中 this 的值。但是,.call 将逗号分隔的参数作为下一个参数,而 .apply 将参数数组作为下一个参数。

记住这一点的一个简单方法是 C 代表 call 和逗号分隔,A 代表 apply 和参数数组。

function add(a, b) {
return a + b;
}
console.log(add.call(null, 1, 2)); // 3
console.log(add.apply(null, [1, 2])); // 3

使用 ES6 语法,我们可以使用数组以及用于参数的扩展运算符来调用 call

function add(a, b) {
return a + b;
}
console.log(add.call(null, ...[1, 2])); // 3

用例

上下文管理

.call.apply 可以在调用不同对象上的方法时显式设置 this 上下文。

const person = {
name: 'John',
greet() {
console.log(`Hello, my name is ${this.name}`);
},
};
const anotherPerson = { name: 'Alice' };
person.greet.call(anotherPerson); // Hello, my name is Alice
person.greet.apply(anotherPerson); // Hello, my name is Alice

函数借用

.call.apply 都允许从一个对象借用方法并在另一个对象的上下文中使它们。当将函数作为参数(回调)传递并且原始 this 上下文丢失时,这很有用。.call.apply 允许使用预期的 this 值调用该函数。

function greet() {
console.log(`Hello, my name is ${this.name}`);
}
const person1 = { name: 'John' };
const person2 = { name: 'Alice' };
greet.call(person1); // Hello, my name is John
greet.apply(person2); // Hello, my name is Alice

调用对象方法的替代语法

.apply 可以通过将对象作为第一个参数传递,然后传递通常的参数来与对象方法一起使用。

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
Array.prototype.push.apply(arr1, arr2); // Same as arr1.push(4, 5, 6)
console.log(arr1); // [1, 2, 3, 4, 5, 6]

分解上述内容:

  1. 第一个对象 arr1 将用作 this 值。
  2. arr1 上调用 .push(),使用 arr2 作为参数数组,因为它使用 .apply()
  3. Array.prototype.push.apply(arr1, arr2) 等同于 arr1.push(...arr2)

可能并不明显,但 Array.prototype.push.apply(arr1, arr2) 会导致修改 arr1。如果可能,使用面向 OOP 的方式调用方法会更清晰。

后续问题

  • .call.applyFunction.prototype.bind 有什么不同?

实践

在 GreatFrontEnd 上实践实现你自己的 Function.prototype.call 方法Function.prototype.apply 方法

延伸阅读

JavaScript 和浏览器中 `mouseenter` 和 `mouseover` 事件有什么区别?

主题
Web APIHTMLJavaScript

TL;DR

主要区别在于 mouseentermouseover 事件的冒泡行为。mouseenter 不冒泡,而 mouseover 冒泡。

mouseenter 事件不冒泡。mouseenter 事件仅在鼠标指针进入元素本身时触发,而不是其后代元素。如果父元素有子元素,并且鼠标指针进入子元素,则不会再次在父元素上触发 mouseenter 事件,它仅在进入父元素时触发一次,而不考虑其内容。如果父元素和子元素都附加了 mouseenter 侦听器,并且鼠标指针从父元素移动到子元素,则 mouseenter 将仅为子元素触发。

mouseover 事件会在 DOM 树中冒泡。当鼠标指针进入元素或其后代时,会触发 mouseover 事件。如果父元素有子元素,并且鼠标指针进入子元素,则父元素也会再次触发 mouseover 事件。如果父元素有多个子元素,这可能导致多次触发事件回调。如果有子元素,并且鼠标指针从父元素移动到子元素,则 mouseover 将同时为父元素和子元素触发。

属性mouseentermouseover
冒泡
触发仅在进入自身时进入自身和进入后代时

mouseenter 事件:

  • 不冒泡mouseenter 事件不冒泡。它仅在鼠标指针进入附加了事件侦听器的元素时触发,而不是进入任何子元素时触发。
  • 触发一次:当鼠标指针进入元素时,mouseenter 事件仅触发一次,这使得它在某些情况下更可预测且更易于管理。

mouseenter 的一个用例是,当您希望检测鼠标进入元素而无需担心子元素多次触发事件时。

mouseover 事件:

  • 在 DOM 中冒泡mouseover 事件通过 DOM 冒泡。这意味着,如果您在父元素上有一个事件侦听器,当鼠标指针移动到任何子元素上时,它也会触发。
  • 多次触发:每次鼠标指针移动到元素或其任何子元素上时,都会触发 mouseover 事件。如果您有嵌套元素,这可能导致多次触发。

mouseover 的一个用例是,当您希望检测鼠标进入元素或其任何子元素,并且可以接受事件多次触发时。

示例

这是一个演示 mouseovermouseenter 事件之间区别的示例:

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mouse Events Example</title>
<style>
.parent {
width: 200px;
height: 200px;
background-color: lightblue;
padding: 20px;
}
.child {
width: 100px;
height: 100px;
background-color: lightcoral;
}
</style>
</head>
<body>
<div class="parent">
Parent Element
<div class="child">Child Element</div>
</div>
<script>
const parent = document.querySelector('.parent');
const child = document.querySelector('.child');
// Mouseover event on parent.
parent.addEventListener('mouseover', () => {
console.log('Mouseover on parent');
});
// Mouseenter event on parent.
parent.addEventListener('mouseenter', () => {
console.log('Mouseenter on parent');
});
// Mouseover event on child.
child.addEventListener('mouseover', () => {
console.log('Mouseover on child');
});
// Mouseenter event on child.
child.addEventListener('mouseenter', () => {
console.log('Mouseenter on child');
});
</script>
</body>
</html>

预期行为

  • 当鼠标进入父元素时:
    • 父元素上的 mouseover 事件将触发。
    • 父元素上的 mouseenter 事件将触发。
  • 当鼠标进入子元素时:
    • 父元素上的 mouseover 事件将再次触发,因为 mouseover 从子元素冒泡。
    • 子元素上的 mouseover 事件将触发。
    • 子元素上的 mouseenter 事件将触发。
    • 父元素上的 mouseenter 事件将不会再次触发,因为 mouseenter 不冒泡。

延伸阅读

JavaScript 中的各种数据类型是什么?

主题
JavaScript

TL;DR

在 JavaScript 中,数据类型可以分为 原始非原始 类型:

原始数据类型

  • Number:表示整数和浮点数。
  • String:表示字符序列。
  • Boolean:表示 truefalse 值。
  • Undefined:已声明但未赋值的变量。
  • Null:表示有意缺失任何对象值。
  • Symbol:用作对象属性键的唯一且不可变的值。在我们的 关于 Symbol 的深入探讨 中阅读更多内容
  • BigInt:表示任意精度的整数。

非原始(引用)数据类型

  • Object:用于存储数据集合。
  • Array:数据的有序集合。
  • Function:可调用对象。
  • Date:表示日期和时间。
  • RegExp:表示正则表达式。
  • Map:键控数据项的集合。
  • Set:唯一值的集合。

原始类型存储单个值,而非原始类型可以存储数据集合或复杂实体。


JavaScript 中的数据类型

与许多编程语言一样,JavaScript 具有多种数据类型来表示不同种类的数据。 JavaScript 中的主要数据类型可以分为两类:原始类型和非原始(引用)类型。

原始数据类型

  1. Number:表示整数和浮点数。 JavaScript 只有一种数字类型。
let age = 25;
let price = 99.99;
console.log(price); // 99.99
  1. String:表示字符序列。 Strings 可以用单引号、双引号或反引号(用于模板字面量)括起来。
let myName = 'John Doe';
let greeting = 'Hello, world!';
let message = `Welcome, ${myName}!`;
console.log(message); // "Welcome, John Doe!"
  1. Boolean:表示逻辑实体,可以有两个值:truefalse
let isActive = true;
let isOver18 = false;
console.log(isOver18); // false
  1. Undefined:已声明但未赋值的变量的类型为 undefined
let user;
console.log(user); // undefined
  1. Null: 表示任何对象值有意缺失。它是一个原始值,被视为假值。
let user = null;
console.log(user); // null
if (!user) {
console.log('user is a falsy value');
}
  1. Symbol: 一种独特且不可变的原始值,通常用作对象属性的键。
let sym1 = Symbol();
let sym2 = Symbol('description');
console.log(sym1); // Symbol()
console.log(sym2); // Symbol(description)
  1. BigInt:用于表示任意精度的整数,对于处理非常大的数字很有用。
let bigNumber = BigInt(9007199254740991);
let anotherBigNumber = 1234567890123456789012345678901234567890n;
console.log(bigNumber); // 9007199254740991n
console.log(anotherBigNumber); // 1234567890123456789012345678901234567890n

非原始(引用)数据类型

  1. Object: 用于存储数据集合和更复杂的实体。Objects 使用大括号 {} 创建。
let person = {
name: 'Alice',
age: 30,
};
console.log(person); // {name: "Alice", age: 30}
  1. Array: 一种特殊类型的对象,用于存储有序的数据集合。Arrays 使用方括号 [] 创建。
let numbers = [1, 2, 3, 4, 5];
console.log(numbers);
  1. Function: JavaScript 中的函数对象。它们可以使用函数声明或表达式来定义。
function greet() {
console.log('Hello!');
}
let add = function (a, b) {
return a + b;
};
greet(); // "Hello!"
console.log(add(2, 3)); // 5
  1. Date: 表示日期和时间。Date 对象用于处理日期。
let today = new Date().toLocaleTimeString();
console.log(today);
  1. RegExp: 表示正则表达式,正则表达式是用于匹配字符串中字符组合的模式。
let pattern = /abc/;
let str = '123abc456';
console.log(pattern.test(str)); // true
  1. Map: 键控数据项的集合,类似于对象,但允许任何类型的键。
let map = new Map();
map.set('key1', 'value1');
console.log(map);
  1. Set: 唯一值的集合。
let set = new Set();
set.add(1);
set.add(2);
console.log(set); // { 1, 2 }

确定数据类型

JavaScript 是一种动态类型语言,这意味着变量可以随时间推移保存不同数据类型的值。typeof 运算符可用于确定值或变量的数据类型。

console.log(typeof 42); // "number"
console.log(typeof 'hello'); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object" (this is a historical bug in JavaScript)
console.log(typeof Symbol()); // "symbol"
console.log(typeof BigInt(123)); // "bigint"
console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof function () {}); // "function"

陷阱

类型强制转换

JavaScript 经常执行类型强制转换,将值从一种类型转换为另一种类型,这可能导致意外结果。

let result = '5' + 2;
console.log(result, typeof result); // "52 string" (string concatenation)
let difference = '5' - 2;
console.log(difference, typeof difference); // 3 "number" (numeric subtraction)

在第一个例子中,由于字符串可以使用 + 运算符进行连接,因此该数字被转换为字符串,并且两个字符串连接在一起。在第二个例子中,字符串不能与减号运算符 (-) 一起使用,但两个数字可以相减,因此字符串首先被转换为数字,结果是差值。

延伸阅读

JavaScript 中 `Map` 对象和普通对象有什么区别?

主题
JavaScript

TL;DR

JavaScript 中的 Map 对象和普通对象都可以存储键值对,但它们有几个关键的区别:

特性Map普通对象
键类型任何数据类型字符串(或 Symbol)
键顺序保持不保证
大小属性是 (size)
迭代forEachkeys()values()entries()for...inObject.keys()
继承
性能通常更适合大型数据集和频繁的添加/删除适用于小型数据集和简单操作
可序列化

Map vs 普通 JavaScript 对象

在 JavaScript 中,Map 对象和普通对象(也称为“POJO”或“普通旧 JavaScript 对象”)都用于存储键值对,但它们具有不同的特性、用例和行为。

普通 JavaScript 对象 (POJO)

普通对象是使用 {} 语法创建的基本 JavaScript 对象。它是一个键值对的集合,其中每个键都是一个字符串(或现代 JavaScript 中的符号),每个值可以是任何类型的值,包括字符串、数字、布尔值、数组、对象等。

const person = { name: 'John', age: 30, occupation: 'Developer' };
console.log(person);

Map 对象

在 ECMAScript 2015 (ES6) 中引入的 Map 对象是一种更高级的数据结构,它允许您存储具有附加功能的键值对。Map 是可迭代的,这意味着您可以在 for...of 循环中使用它,并且它提供了用于常见操作(如 getsethasdelete)的方法。

const person = new Map([
['name', 'John'],
['age', 30],
['occupation', 'Developer'],
]);
console.log(person);

关键区别

以下是 Map 对象和普通对象之间的主要区别:

  1. 键类型:在普通对象中,键始终是字符串(或符号)。在 Map 中,键可以是任何类型的值,包括对象、数组,甚至其他 Map
  2. 键顺序:在普通对象中,键的顺序不保证。在 Map 中,键的顺序被保留,您可以按照它们插入的顺序进行迭代。
  3. 迭代Map 是可迭代的,这意味着您可以使用 for...of 循环来迭代其键值对。默认情况下,普通对象不可迭代,但您可以使用 Object.keys()Object.entries() 来迭代其属性。
  4. 性能Map 对象通常比普通对象更快、更高效,尤其是在处理大型数据集时。
  5. 方法Map 对象提供了其他方法,例如 getsethasdelete,这使得处理键值对更容易。
  6. 序列化:将 Map 对象序列化为 JSON 时,它将被转换为一个对象,但现有的 Map 属性可能会在转换过程中丢失。另一方面,普通对象被序列化为具有相同结构的 JSON 对象。

何时使用哪个

当您需要时使用普通对象 (POJO):

  • 您需要一个具有字符串键的简单、轻量级对象。
  • 您正在处理小型数据集。
  • 您需要将对象序列化为 JSON(例如,通过网络发送)。

当您需要时使用 Map 对象:

  • 您需要使用非字符串键(例如对象、数组)存储键值对。
  • 您需要保留键值对的顺序。
  • 您需要按特定顺序迭代键值对。
  • 您正在处理大型数据集,需要更好的性能。

总而言之,虽然普通对象和 Map 对象都可以用来存储键值对,但 Map 对象提供了更高级的功能、更好的性能和额外的方法,使其成为更复杂用例的更好选择。

注意事项

Map 对象无法被序列化以在 HTTP 请求中发送,但像 superjson 这样的库允许它们被序列化和反序列化。

延伸阅读

JavaScript 中的代理有什么用?

主题
JavaScript

TL;DR

在 JavaScript 中,代理是一个充当对象和代码之间中介的 对象。代理用于拦截和自定义 JavaScript 对象的基本操作,例如属性访问、赋值、函数调用等。

以下是使用 Proxy 记录每个属性访问的基本示例:

const myObject = {
name: 'John',
age: 42,
};
const handler = {
get: function (target, prop, receiver) {
console.log(`Someone accessed property "${prop}"`);
return target[prop];
},
};
const proxiedObject = new Proxy(myObject, handler);
console.log(proxiedObject.name);
// Someone accessed property "name"
// 'John'
console.log(proxiedObject.age);
// Someone accessed property "age"
// 42

用例包括:

  • 属性访问拦截:拦截和自定义对对象的属性访问。
    • 属性赋值验证:在将属性值设置到目标对象之前验证它们。
    • 日志记录和调试:创建用于记录和调试与对象交互的包装器
    • 创建反应式系统:当对象属性更改时,触发应用程序其他部分的更新(数据绑定)。
    • 数据转换:转换从对象设置或检索的数据。
    • 在测试中模拟和存根:为测试目的创建模拟或存根对象,允许您隔离依赖项并专注于被测单元
  • 函数调用拦截:用于缓存和返回经常访问的方法的结果(如果它们涉及网络调用或计算密集型逻辑),从而提高性能
  • 动态属性创建:用于即时定义具有默认值的属性,并避免在对象中存储冗余数据。

JavaScript 代理

在 JavaScript 中,代理是一个允许您自定义另一个对象(通常称为目标对象)行为的对象。代理可以拦截和重新定义目标对象的各种操作,例如属性访问、赋值、枚举、函数调用等。这使得代理成为各种用例的强大工具,包括但不限于验证、日志记录、性能监控和实现高级数据结构。

以下是代理在 JavaScript 中使用的一些常见用例和示例:

属性访问拦截

代理可用于拦截和自定义对对象的属性访问。

const target = {
message: 'Hello, world!',
};
const handler = {
get: function (target, property) {
if (property in target) {
return target[property];
}
return `Property ${property} does not exist.`;
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.message); // Hello, world!
console.log(proxy.nonExistentProperty); // Property nonExistentProperty does not exist.
创建用于日志记录和调试的包装器

这对于创建用于记录和调试与对象交互的包装器很有用。

const target = {
name: 'Alice',
age: 30,
};
const handler = {
get: function (target, property) {
console.log(`Getting property ${property}`);
return target[property];
},
set: function (target, property, value) {
console.log(`Setting property ${property} to ${value}`);
target[property] = value;
return true;
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // Output: Getting property name
// Alice
proxy.age = 31; // Output: Setting property age to 31
console.log(proxy.age); // Output: Getting property age
// 31
属性赋值验证

代理可用于在将属性值设置到目标对象之前验证它们。

const target = {
age: 25,
};
const handler = {
set: function (target, property, value) {
if (property === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
target[property] = value;
return true;
},
};
const proxy = new Proxy(target, handler);
proxy.age = 30; // Works fine
proxy.age = 'thirty'; // Throws TypeError: Age must be a number
创建响应式系统

代理通常用于在对象属性更改时触发应用程序其他部分的更新(数据绑定)。

一个实际的例子是像 Vue.js 这样的 JavaScript 框架,其中代理用于创建响应式系统,当数据更改时自动更新 UI

const target = {
firstName: 'John',
lastName: 'Doe',
};
const handler = {
set: function (target, property, value) {
console.log(`Property ${property} set to ${value}`);
target[property] = value;
// Automatically update the UI or perform other actions
return true;
},
};
const proxy = new Proxy(target, handler);
proxy.firstName = 'Jane'; // Output: Property firstName set to Jane

访问拦截的其他用例包括:

  • 模拟和存根:代理可用于创建用于测试目的的模拟或存根对象,允许您隔离依赖项并专注于被测单元。

函数调用拦截

代理可以拦截和自定义函数调用。

const target = function (name) {
return `Hello, ${name}!`;
};
const handler = {
apply: function (target, thisArg, argumentsList) {
console.log(`Called with arguments: ${argumentsList}`);
return target.apply(thisArg, argumentsList);
},
};
const proxy = new Proxy(target, handler);
console.log(proxy('Alice')); // Called with arguments: Alice
// Hello, Alice!

如果频繁访问的方法涉及网络调用或计算密集型逻辑,则此拦截可用于缓存并返回结果,从而通过减少发出的请求/计算次数来提高性能。

动态属性创建

代理可用于动态地在对象上创建属性或方法。这对于使用默认值即时定义属性并避免在对象中存储冗余数据非常有用。

const target = {};
const handler = {
get: function (target, property) {
if (!(property in target)) {
target[property] = `Dynamic property ${property}`;
}
return target[property];
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.newProp); // Output: Dynamic property newProp
console.log(proxy.anotherProp); // Output: Dynamic property anotherProp

实现对象关系映射器 (ORM)

代理可用于创建数据库记录的对象,方法是拦截属性访问以从数据库中惰性加载数据。这提供了一个更面向对象的接口来与数据库交互。

实际用例

许多流行的库,尤其是状态管理解决方案,都是建立在 JavaScript 代理之上的:

  • Vue.js: Vue.js 是一个用于构建用户界面的渐进式框架。在 Vue 3 中,代理被广泛用于实现响应式系统。
  • MobX: MobX 使用代理使对象和数组可观察,允许组件自动响应状态变化。
  • Immer: Immer 是一个允许你以更方便的方式处理不可变状态的库。它使用代理来跟踪更改并生成下一个不可变状态。

总结

JavaScript 中的代理提供了一种强大而灵活的方式来拦截和自定义对象上的操作。它们适用于广泛的应用程序,包括验证、日志记录、调试、动态属性创建和实现响应式系统。通过使用代理,开发人员可以创建更强大、更易于维护且功能更丰富的应用程序。

延伸阅读

解释异步操作中回调函数的概念

主题
异步JavaScript

TL;DR

回调函数是作为参数传递给另一个函数的函数,然后在外部函数中调用该函数以完成某种例程或操作。在异步操作中,回调用于处理需要时间才能完成的任务,例如网络请求或文件 I/O,而不会阻塞其余代码的执行。例如:

function fetchData(callback) {
setTimeout(() => {
const data = { name: 'John', age: 30 };
callback(data);
}, 1000);
}
fetchData((data) => {
console.log(data);
});

什么是回调函数?

回调函数是作为参数传递给另一个函数并在完成某些操作后执行的函数。这在异步编程中特别有用,其中需要处理网络请求、文件 I/O 或计时器等操作,而不会阻塞主执行线程。

同步与异步回调

  • 同步回调 在传递给它们的函数中立即执行。它们是阻塞的,代码执行会等待它们完成。
  • 异步回调 在完成某个事件或操作后执行。它们是非阻塞的,允许代码执行在等待操作完成的同时继续进行。

同步回调示例

function greet(name, callback) {
console.log('Hello ' + name);
callback();
}
function sayGoodbye() {
console.log('Goodbye!');
}
greet('Alice', sayGoodbye);
// Output:
// Hello Alice
// Goodbye!

异步回调示例

function fetchData(callback) {
setTimeout(() => {
const data = { name: 'John', age: 30 };
callback(data);
}, 1000);
}
fetchData((data) => {
console.log(data);
});
// Output after 1 second:
// { name: 'John', age: 30 }

常见用例

  • 网络请求:从 API 获取数据
  • 文件 I/O:读取或写入文件
  • 计时器:使用 setTimeoutsetInterval 延迟执行
  • 事件处理:响应用户操作,如单击或按键

在回调中处理错误

处理异步操作时,正确处理错误非常重要。一个常见的模式是使用回调函数的第一个参数来传递一个错误对象(如果有)。

function fetchData(callback) {
// assume asynchronous operation to fetch data
const { data, error } = { data: { name: 'John', age: 30 }, error: null };
callback(error, data);
}
fetchData((error, data) => {
if (error) {
console.error('An error occurred:', error);
} else {
console.log(data);
}
});

延伸阅读

解释微任务队列的概念

主题
异步JavaScript

TL;DR

微任务队列是一个任务队列,需要在当前执行的脚本之后和任何其他任务之前执行。微任务通常用于需要紧接在当前操作之后执行的任务,例如 promise 回调。微任务队列在宏任务队列之前处理,确保微任务尽快执行。


微任务队列的概念

什么是微任务队列?

微任务队列是 JavaScript 事件循环机制的一部分。它是一个队列,用于保存需要在当前执行的脚本之后和宏任务队列中的任何其他任务之前执行的任务。微任务通常用于需要尽快执行的操作,例如 promise 回调和 MutationObserver 回调。

微任务队列如何工作?

  1. 执行顺序:微任务队列在当前执行的脚本之后和宏任务队列之前处理。这意味着微任务的优先级高于宏任务。
  2. 事件循环:在事件循环的每次迭代期间,JavaScript 引擎首先处理微任务队列中的所有微任务,然后再转到宏任务队列。
  3. 添加微任务:可以使用 Promise.resolve().then()queueMicrotask() 等方法将微任务添加到微任务队列中。

例子

这里有一个例子来说明微任务队列的工作原理:

console.log('Script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise 1');
})
.then(() => {
console.log('Promise 2');
});
console.log('Script end');

输出:

Script start
Script end
Promise 1
Promise 2
setTimeout

在这个例子中:

  • 同步代码(console.log('Script start')console.log('Script end'))首先执行。
  • promise 回调(Promise 1Promise 2)被添加到微任务队列中,然后执行。
  • setTimeout 回调被添加到宏任务队列中,最后执行。

用例

  1. Promise 回调:微任务通常用于 promise 回调,以确保它们在当前操作之后尽快执行。
  2. MutationObserverMutationObserver API 使用微任务来通知 DOM 中的更改。

延伸阅读

解释缓存的概念以及如何使用它来提高性能

主题
JavaScript性能

TL;DR

缓存是一种用于存储文件或数据的副本的临时存储技术,以减少访问它们所需的时间。它通过减少重复从原始来源获取数据的需要来提高性能。在前端开发中,可以使用浏览器缓存、service workers 和 HTTP 标头(如 Cache-Control)来实现缓存。


缓存的概念以及如何使用它来提高性能

什么是缓存?

缓存是一种用于将文件或数据的副本存储在临时存储位置(称为缓存)中的技术,以减少访问它们所需的时间。缓存的主要目标是通过最大限度地减少重复从原始来源获取数据的需要来提高性能。

缓存的类型

浏览器缓存

浏览器缓存将网页、图像和其他资源的副本存储在用户设备的本地。当用户重新访问网站时,浏览器可以从缓存中加载这些资源,而不是从服务器获取它们,从而加快加载时间。

Service workers

Service workers 是在后台运行的脚本,可以拦截网络请求。它们可以缓存资源并从缓存中提供它们,即使在用户离线时也是如此。这可以显著提高性能并提供更好的用户体验。

HTTP 缓存

HTTP 缓存涉及使用 HTTP 标头来控制如何以及何时缓存资源。常见的标头包括 Cache-ControlExpiresETag

缓存如何提高性能

减少延迟

通过将经常访问的数据存储在更靠近用户的位置,缓存减少了检索该数据所需的时间。这可以加快加载时间并提供更流畅的用户体验。

减少服务器负载

缓存减少了向服务器发出的请求数量,这有助于减少服务器负载并提高整体性能。

离线访问

通过 Service Worker,即使在用户离线时也可以提供缓存的资源,从而提供无缝体验。

实现缓存

浏览器缓存示例
<head>
<link rel="stylesheet" href="styles.css" />
<script src="app.js"></script>
</head>
Service worker 示例
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll(['/index.html', '/styles.css', '/app.js']);
}),
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
}),
);
});
HTTP 缓存示例
Cache-Control: max-age=3600

延伸阅读

解释代码覆盖率的概念以及如何使用它来评估测试质量

主题
JavaScript测试

TL;DR

代码覆盖率是一个衡量测试套件运行时执行代码百分比的指标。它通过识别代码库中未测试的部分来帮助评估测试的质量。较高的代码覆盖率通常表明测试更彻底,但并不能保证没有错误。可以使用 Istanbul 或 Jest 等工具来衡量代码覆盖率。


什么是代码覆盖率?

代码覆盖率是一个软件测试指标,用于确定在自动化测试期间执行的代码量。它提供了关于代码库的哪些部分正在被测试以及哪些部分没有被测试的见解。

代码覆盖率的类型

  1. 语句覆盖率:衡量已执行的代码中的语句数量。
  2. 分支覆盖率:衡量每个分支(例如,ifelse 块)是否已被执行。
  3. 函数覆盖率:衡量代码中每个函数是否已被调用。
  4. 行覆盖率:衡量已执行的代码行数。
  5. 条件覆盖率:衡量每个布尔子表达式是否已被评估为真和假。

示例

考虑以下 JavaScript 函数:

function isEven(num) {
if (num % 2 === 0) {
return true;
} else {
return false;
}
}

此函数的测试套件可能如下所示:

test('isEven returns true for even numbers', () => {
expect(isEven(2)).toBe(true);
});
test('isEven returns false for odd numbers', () => {
expect(isEven(3)).toBe(false);
});

在此测试套件上运行代码覆盖率工具将显示 100% 的语句、分支、函数和行覆盖率,因为代码的所有部分都已执行。

如何衡量代码覆盖率

工具

  1. Istanbul:一个流行的 JavaScript 代码覆盖率工具。
  2. Jest:一个测试框架,包含内置的代码覆盖率报告。
  3. Karma:一个测试运行器,可以配置为使用 Istanbul 进行代码覆盖率。

使用 Jest 的示例

要使用 Jest 衡量代码覆盖率,您可以在运行测试时添加 --coverage 标志:

jest --coverage

这将生成一个覆盖率报告,显示测试覆盖代码的百分比。

使用代码覆盖率评估测试质量

优点

  • 识别未测试的代码:帮助找到未被测试覆盖的代码库部分。
  • 改进测试套件:鼓励编写更全面的测试。
  • 增加信心:更高的覆盖率可以增加对代码稳定性的信心。

局限性

  • 虚假的安全感:高覆盖率并不能保证没有错误。
  • 质量胜于数量:100% 的覆盖率并不意味着测试质量高。测试还应检查边缘情况和潜在错误。

延伸阅读

解释内容安全策略 (CSP) 的概念以及它如何增强安全性

主题
JavaScript安全

总结

内容安全策略 (CSP) 是一项安全功能,通过指定受信任的内容来源,帮助防止各种类型的攻击,例如跨站点脚本 (XSS) 和数据注入攻击。它的工作原理是允许开发人员定义一个受信任的脚本、样式和图像等内容来源的白名单。这通过 HTTP 标头或 meta 标签完成。例如,您可以使用 Content-Security-Policy 标头来指定仅应执行来自您自己域的脚本:

Content-Security-Policy: script-src 'self'

什么是内容安全策略 (CSP)?

内容安全策略 (CSP) 是一项安全标准,旨在缓解一系列攻击,包括跨站点脚本 (XSS) 和数据注入攻击。CSP 允许 Web 开发人员控制用户代理允许为给定页面加载的资源。通过指定受信任内容来源的白名单,CSP 有助于防止恶意内容的执行。

CSP 的工作原理

CSP 的工作原理是允许开发人员定义一组规则,用于指定哪些内容来源被认为是可信的。这些规则通过 HTTP 标头或 meta 标签传递给浏览器。当浏览器加载页面时,它会检查 CSP 规则并阻止任何与指定来源不匹配的内容。

CSP 标头的示例

以下是一个简单的 CSP 标头的示例,该标头仅允许来自同一来源的脚本:

Content-Security-Policy: script-src 'self'

此策略告诉浏览器仅执行从与页面本身相同的来源加载的脚本。

常用指令

  • object-src:指定 Flash 等插件的有效来源。

使用 CSP 的好处

  • 提高安全态势:实施 CSP 是一项积极措施,可增强 Web 应用程序的整体安全性。

实施 CSP

可以使用 HTTP 标头或 meta 标签来实现 CSP。通常首选 HTTP 标头方法,因为它更安全,并且不易被攻击者覆盖。

使用 HTTP 标头
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com
使用 meta 标签
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' https://trusted.cdn.com" />

延伸阅读

解释跨站点请求伪造 (CSRF) 的概念及其缓解技术

主题
JavaScript网络安全

TL;DR

跨站点请求伪造 (CSRF) 是一种攻击,恶意网站会诱骗用户的浏览器向用户已通过身份验证的另一个站点发出不需要的请求。这可能导致代表用户执行未经授权的操作。缓解技术包括使用反 CSRF 令牌、SameSite cookie 和确保适当的 CORS 配置。


跨站点请求伪造 (CSRF) 及其缓解技术

什么是 CSRF?

跨站点请求伪造 (CSRF) 是一种攻击类型,当恶意网站导致用户的浏览器在用户已通过身份验证的不同站点上执行不需要的操作时,就会发生这种攻击。这可能导致未经授权的操作,例如更改帐户详细信息、进行购买或其他用户无意执行的操作。

CSRF 如何工作?

  1. 用户身份验证:用户登录到受信任的网站(例如,银行网站)并收到身份验证 cookie。
  2. 恶意网站:用户在仍登录到受信任网站的情况下访问恶意网站。
  3. 不需要的请求:恶意网站包含向受信任网站发出请求的代码,使用用户的身份验证 cookie 代表用户执行操作。

缓解技术

反 CSRF 令牌

防止 CSRF 攻击的最有效方法之一是使用反 CSRF 令牌。这些令牌是服务器生成的、包含在表单或请求中的唯一且不可预测的值。然后,服务器验证该令牌以确保请求是合法的。

<form method="POST" action="/update-profile">
<input type="hidden" name="csrf_token" value="unique_token_value" />
<!-- other form fields -->
<button type="submit">Update Profile</button>
</form>

在服务器端,验证令牌以确保它与预期值匹配。

SameSite cookie

cookie 上的 SameSite 属性可以通过限制 cookie 如何与跨站点请求一起发送来帮助缓解 CSRF 攻击。SameSite 属性可以设置为 StrictLaxNone

Set-Cookie: sessionId=abc123; SameSite=Strict
  • Strict:Cookie 仅在第一方上下文中发送,而不是与第三方网站发起的请求一起发送。
  • Lax:Cookie 不会通过正常的跨站点子请求(例如,加载图像)发送,但会在用户从外部站点导航到 URL 时发送(例如,点击链接)。
  • None:Cookie 在所有上下文中发送,包括跨源请求。
CORS(跨源资源共享)

正确配置 CORS 可以通过确保只有受信任的来源才能向您的服务器发出请求来帮助防止 CSRF 攻击。这涉及在服务器上设置适当的标头以指定允许访问资源的来源。

Access-Control-Allow-Origin: https://trustedwebsite.com

延伸阅读

解释防抖和节流的概念

主题
异步JavaScript性能

总结

防抖和节流是用于控制函数执行速率的技术。防抖确保函数仅在自上次调用后经过指定的延迟后才被调用。节流确保函数在一个指定的时间间隔内最多被调用一次。

防抖会延迟函数的执行,直到自上次调用后经过一定的时间。这对于搜索输入框等场景非常有用,在这些场景中,您希望在用户停止输入后才进行 API 调用。

function debounce(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
const debouncedHello = debounce(() => console.log('Hello world!'), 2000);
debouncedHello(); // Prints 'Hello world!' after 2 seconds

节流确保函数在一个指定的时间间隔内最多被调用一次。这对于窗口调整大小或滚动等场景非常有用,在这些场景中,您希望限制函数被调用的次数。

function throttle(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
const handleResize = throttle(() => {
// Update element positions
console.log('Window resized at', new Date().toLocaleTimeString());
}, 2000);
// Simulate rapid calls to handleResize every 100ms
let intervalId = setInterval(() => {
handleResize();
}, 100);
// 'Window resized' is outputted only every 2 seconds due to throttling

防抖和节流

防抖

防抖是一种技术,用于确保函数仅在自上次调用后经过一定时间后才执行。这在您希望限制函数被调用次数的场景中特别有用,例如处理用户输入事件(如按键或鼠标移动)。

使用案例

想象一下,您有一个搜索输入框,并且您想进行 API 调用来获取搜索结果。如果没有防抖,每次用户输入一个字符时都会进行 API 调用,这可能会导致大量不必要的调用。防抖确保仅在用户停止输入指定时间后才进行 API 调用。

代码示例
function debounce(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// Usage
const handleSearch = debounce((query) => {
// Make API call
console.log('API call with query:', query);
}, 300);
document.getElementById('searchInput').addEventListener('input', (event) => {
handleSearch(event.target.value);
});

节流

节流是一种技术,用于确保函数在一个指定的时间间隔内最多被调用一次。这在您希望限制函数被调用次数的场景中很有用,例如处理窗口调整大小或滚动等事件。

使用案例

想象一下,您有一个函数,该函数根据窗口大小更新屏幕上元素的位置。如果没有节流,此函数可能会在用户调整窗口大小时每秒被调用多次,从而导致性能问题。节流确保该函数在一个指定的时间间隔内最多被调用一次。

代码示例
function throttle(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// Usage
const handleResize = throttle(() => {
// Update element positions
console.log('Window resized');
}, 100);
window.addEventListener('resize', handleResize);

延伸阅读

解释对象和数组的解构赋值概念

主题
JavaScript

总结

解构赋值是 JavaScript 中的一种语法,它允许您将数组中的值或对象中的属性解包到不同的变量中。对于数组,您使用方括号,对于对象,您使用花括号。例如:

// 数组解构
const [a, b] = [1, 2];
// 对象解构
const { name, age } = { name: 'John', age: 30 };

对象和数组的解构赋值

解构赋值是一种方便的方法,用于将数组和对象中的值提取到单独的变量中。这可以使您的代码更具可读性和简洁性。

数组解构

数组解构允许您使用方括号将数组中的值解包到不同的变量中。

基本示例
const numbers = [1, 2, 3];
const [first, second, third] = numbers;
console.log(first); // 1
console.log(second); // 2
console.log(third); // 3
跳过值

您可以通过在逗号之间留一个空格来跳过数组中的值。

const numbers = [1, 2, 3];
const [first, , third] = numbers;
console.log(first); // 1
console.log(third); // 3
默认值

如果数组没有足够的元素,您可以分配默认值。

const numbers = [1];
const [first, second = 2] = numbers;
console.log(first); // 1
console.log(second); // 2

对象解构

对象解构允许您使用花括号将对象中的属性解包到不同的变量中。

基本示例
const person = { name: 'John', age: 30 };
const { name, age } = person;
console.log(name); // John
console.log(age); // 30
重命名变量

您可以在解构时重命名变量。

const person = { name: 'John', age: 30 };
const { name: personName, age: personAge } = person;
console.log(personName); // John
console.log(personAge); // 30
默认值

如果属性在对象中不存在,您可以分配默认值。

const person = { name: 'John' };
const { name, age = 25 } = person;
console.log(name); // John
console.log(age); // 25
嵌套对象

您也可以解构嵌套对象。

const person = { name: 'John', address: { city: 'New York', zip: '10001' } };
const {
name,
address: { city, zip },
} = person;
console.log(name); // John
console.log(city); // New York
console.log(zip); // 10001

延伸阅读

解释 JavaScript 中的错误传播概念

主题
JavaScript

TL;DR

JavaScript 中的错误传播指的是错误如何通过调用堆栈传递。当函数中发生错误时,可以使用 try...catch 块捕获和处理它。如果未捕获,错误会沿着调用堆栈向上冒泡,直到它被捕获或导致程序终止。例如:

function a() {
throw new Error('An error occurred');
}
function b() {
a();
}
try {
b();
} catch (e) {
console.error(e.message); // Outputs: An error occurred
}

JavaScript 中的错误传播

JavaScript 中的错误传播是一种机制,它允许错误沿着调用堆栈传递,直到它们被捕获和处理。这对于调试和确保错误不会导致整个应用程序意外崩溃至关重要。

错误如何传播

当函数中发生错误时,它可以在该函数内被捕获和处理,或者沿着调用堆栈传递到调用函数。如果调用函数不处理该错误,它会继续在堆栈上传播,直到它到达全局范围,这可能会导致程序终止。

使用 try...catch

为了处理错误并防止它们进一步传播,您可以使用 try...catch 块。这是一个例子:

function a() {
throw new Error('An error occurred');
}
function b() {
a();
}
try {
b();
} catch (e) {
console.error(e.message); // Outputs: An error occurred
}

在这个例子中,函数 a 中抛出的错误传播到函数 b,然后传播到 try...catch 块,在那里它最终被捕获和处理。

异步代码的传播

错误传播在异步代码(如 promises 和 async/await)中的工作方式有所不同。对于 promises,您可以使用 .catch() 来处理错误:

function a() {
return Promise.reject(new Error('An error occurred'));
}
function b() {
return a();
}
b().catch((e) => {
console.error(e.message); // Outputs: An error occurred
});

对于 async/await,您可以使用 try...catch 块:

async function a() {
throw new Error('An error occurred');
}
async function b() {
await a();
}
(async () => {
try {
await b();
} catch (e) {
console.error(e.message); // Outputs: An error occurred
}
})();

最佳实践

  • 始终在适当的级别处理错误,以防止它们不必要地传播。
  • 对同步代码使用 try...catch 块,对异步代码使用 .catch() 或带有 async/awaittry...catch
  • 记录错误以帮助调试,并向用户提供有意义的错误消息。

延伸阅读

解释关于函数的提升概念

主题
JavaScript

TL;DR

JavaScript 中的提升是一种行为,在编译阶段,函数声明会被移动到其包含作用域的顶部。这意味着你可以在代码中定义函数之前调用它。但是,这不适用于函数表达式或箭头函数,它们不会以相同的方式被提升。

// Function declaration
hoistedFunction(); // Works fine
function hoistedFunction() {
console.log('This function is hoisted');
}
// Function expression
nonHoistedFunction(); // Throws an error
var nonHoistedFunction = function () {
console.log('This function is not hoisted');
};

什么是提升?

提升是一种 JavaScript 机制,在编译阶段,变量和函数声明会被移动到其包含作用域的顶部。这允许在代码中定义函数之前调用它们。

函数声明

函数声明会被完全提升。这意味着你可以在代码中声明函数之前调用它。

hoistedFunction(); // Works fine
function hoistedFunction() {
console.log('This function is hoisted');
}

函数表达式

函数表达式(包括箭头函数)不会以相同的方式被提升。它们被视为变量赋值,并且仅被提升为 undefined。

nonHoistedFunction(); // Throws an error: TypeError: nonHoistedFunction is not a function
var nonHoistedFunction = function () {
console.log('This function is not hoisted');
};

箭头函数

就提升而言,箭头函数的行为类似于函数表达式。

arrowFunction(); // Throws an error: TypeError: arrowFunction is not a function
var arrowFunction = () => {
console.log('This arrow function is not hoisted');
};

延伸阅读

解释 ES2015 类中的继承概念

主题
JavaScriptOOP

TL;DR

ES2015 类中的继承允许一个类扩展另一个类,使子类能够从父类继承属性和方法。这是使用 extends 关键字完成的。super 关键字用于调用父类的构造函数和方法。这是一个快速示例:

class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
speak() {
console.log(`${this.name} barks.`);
}
}
const dog = new Dog('Rex', 'German Shepherd');
dog.speak(); // Rex barks.

ES2015 类中的继承

基本概念

ES2015 类中的继承允许一个类(子类)从另一个类(父类)继承属性和方法。这促进了代码重用和分层类结构。

使用 extends 关键字

使用 extends 关键字可以创建一个作为另一个类子类的类。子类继承父类的所有属性和方法。

class ParentClass {
constructor() {
this.parentProperty = 'I am a parent property';
}
parentMethod() {
console.log('This is a parent method');
}
}
class ChildClass extends ParentClass {
constructor() {
super(); // Calls the parent class constructor
this.childProperty = 'I am a child property';
}
childMethod() {
console.log('This is a child method');
}
}
const child = new ChildClass();
console.log(child.parentProperty); // I am a parent property
child.parentMethod(); // This is a parent method

使用 super 关键字

super 关键字用于调用父类的构造函数并访问其方法。当您想在子类中初始化父类属性时,这是必需的。

class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Calls the parent class constructor
this.breed = breed;
}
speak() {
super.speak(); // Calls the parent class method
console.log(`${this.name} barks.`);
}
}
const dog = new Dog('Rex', 'German Shepherd');
dog.speak();
// Rex makes a noise.
// Rex barks.

方法重写

子类可以重写父类中的方法。这允许子类提供已经在父类中定义的方法的特定实现。

class Animal {
speak() {
console.log('Animal makes a noise.');
}
}
class Dog extends Animal {
speak() {
console.log('Dog barks.');
}
}
const dog = new Dog();
dog.speak(); // Dog barks.

延伸阅读

解释输入验证的概念及其在安全中的重要性

主题
JavaScript安全

TL;DR

输入验证是确保用户输入正确、安全并满足应用程序要求的过程。它对安全至关重要,因为它有助于防止 SQL 注入、跨站点脚本 (XSS) 和其他形式的数据操纵等攻击。通过验证输入,您可以确保只有格式正确的数据才能进入您的系统,从而降低恶意数据造成损害的风险。


输入验证及其在安全中的重要性

什么是输入验证?

输入验证是验证用户或其他外部来源提供的数据在应用程序处理之前是否满足预期格式、类型和约束的过程。这可以包括检查:

  • 正确的数据类型(例如,字符串、数字)
  • 适当的格式(例如,电子邮件地址、电话号码)
  • 可接受的数值范围(例如,年龄在 0 到 120 岁之间)
  • 必填字段已填写

输入验证的类型

  1. 客户端验证:这发生在用户将数据发送到服务器之前,在用户的浏览器中进行。它向用户提供即时反馈,并可以改善用户体验。但是,不应仅依赖它来实现安全目的,因为它很容易被绕过。

    <form>
    <input type="text" id="username" required pattern="[A-Za-z0-9]{5,}" />
    <input type="submit" />
    </form>
  2. 服务器端验证:这发生在数据提交到服务器之后。它对于安全至关重要,因为它确保所有数据都经过验证,而不管客户端的行为如何。

    const express = require('express');
    const app = express();
    app.post('/submit', (req, res) => {
    const username = req.body.username;
    if (!/^[A-Za-z0-9]{5,}$/.test(username)) {
    return res.status(400).send('Invalid username');
    }
    // Proceed with processing the valid input
    });

输入验证在安全中的重要性

  1. 防止 SQL 注入:通过验证和清理输入,您可以防止攻击者将恶意 SQL 代码注入到您的数据库查询中。

    const username = req.body.username;
    const query = 'SELECT * FROM users WHERE username = ?';
    db.query(query, [username], (err, results) => {
    // Handle results
    });
  2. 防止跨站点脚本 (XSS):输入验证有助于确保用户输入不包含可能在浏览器中执行的恶意脚本。

    const sanitizeHtml = require('sanitize-html');
    const userInput = req.body.comment;
    const sanitizedInput = sanitizeHtml(userInput);
  3. 防止缓冲区溢出攻击:通过验证输入数据的长度,您可以防止攻击者发送过大的输入,从而导致缓冲区溢出并使您的应用程序崩溃。

  4. 确保数据完整性:输入验证通过确保仅处理和存储格式正确且预期的数据来帮助维护数据的完整性。

输入验证的最佳实践

  • 始终在服务器端验证输入,即使您也在客户端进行验证
  • 尽可能使用内置验证函数和库
  • 清理输入以删除或转义潜在的有害字符
  • 实施白名单(仅允许已知的良好输入)而不是黑名单(阻止已知的错误输入)
  • 定期更新和审查您的验证规则以应对新的安全威胁

延伸阅读

解释惰性加载的概念以及它如何提高性能

主题
JavaScript性能

TL;DR

惰性加载是一种设计模式,它将资源的加载延迟到实际需要时。这可以通过减少初始加载时间和节省带宽来显着提高性能。例如,网页上的图像可以进行惰性加载,以便它们仅在进入视口时加载。这可以使用 HTML 中的 loading="lazy" 属性或使用 JavaScript 库来实现。

<img src="image.jpg" loading="lazy" alt="Lazy loaded image" />

惰性加载的概念以及它如何提高性能

什么是惰性加载?

惰性加载是一种设计模式,用于推迟对象的初始化,直到需要时才进行。这可以应用于各种类型的资源,例如图像、视频、脚本,甚至从 API 获取的数据。

惰性加载如何工作?

惰性加载通过延迟加载资源直到实际需要时才进行。例如,网页上的图像可以进行惰性加载,以便它们仅在进入视口时加载。这可以使用 HTML 中的 loading="lazy" 属性或使用 JavaScript 库来实现。

惰性加载的好处

  • 提高性能:通过仅在初始加载时加载必要的资源,可以缩短页面加载时间,从而带来更快、响应更快的用户体验。
  • 减少带宽使用:惰性加载有助于通过仅在需要时加载资源来节省带宽。
  • 更好的用户体验:用户可以更快地开始与内容交互,因为初始加载时间缩短了。

实现惰性加载

使用 HTML 中的 loading 属性

实现图像惰性加载的最简单方法是使用 HTML 中的 loading 属性。

<img src="image.jpg" loading="lazy" alt="Lazy loaded image" />
使用 JavaScript

对于更复杂的场景,您可以使用 JavaScript 来实现惰性加载。这是一个使用 Intersection Observer API 的示例:

document.addEventListener('DOMContentLoaded', function () {
const lazyImages = document.querySelectorAll('img.lazy');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
observer.unobserve(img);
}
});
});
lazyImages.forEach((image) => {
imageObserver.observe(image);
});
});

在此示例中,带有 lazy 类的图像仅在进入视口时加载。

延伸阅读

解释词法作用域的概念

主题
JavaScript

TL;DR

词法作用域意味着变量的作用域由其在源代码中的位置决定,嵌套函数可以访问在其外部作用域中声明的变量。例如:

function outerFunction() {
let outerVariable = 'I am outside!';
function innerFunction() {
console.log(outerVariable); // 'I am outside!'
}
innerFunction();
}
outerFunction();

在这个例子中,innerFunction 可以访问 outerVariable,因为词法作用域。


词法作用域

词法作用域是 JavaScript 和许多其他编程语言中的一个基本概念。它决定了在嵌套函数中如何解析变量名。变量的作用域由其在源代码中的位置定义,嵌套函数可以访问在其外部作用域中声明的变量。

词法作用域的工作原理

当一个函数被定义时,它会捕获创建它的作用域。这意味着该函数可以访问其自身作用域中的变量以及任何包含(外部)作用域中的变量。

例子

考虑以下示例:

function outerFunction() {
let outerVariable = 'I am outside!';
function innerFunction() {
console.log(outerVariable); // 'I am outside!'
}
innerFunction();
}
outerFunction();

在这个例子中:

  • outerFunction 声明一个变量 outerVariable
  • innerFunction 嵌套在 outerFunction 内部,并将 outerVariable 记录到控制台。
  • 当调用 innerFunction 时,由于词法作用域,它可以访问 outerVariable

嵌套函数和闭包

词法作用域与闭包密切相关。当一个函数保留对其词法作用域的访问权限时,即使该函数在作用域之外执行,也会创建一个闭包。

function outerFunction() {
let outerVariable = 'I am outside!';
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}
const myInnerFunction = outerFunction();
myInnerFunction(); // 'I am outside!'

在这个例子中:

  • outerFunction 返回 innerFunction
  • myInnerFunction 被赋值为返回的 innerFunction
  • 当调用 myInnerFunction 时,它仍然可以访问 outerVariable,因为词法作用域创建了闭包。

延伸阅读

解释部分应用的理念

主题
闭合JavaScript

总结

部分应用是函数式编程中的一种技术,它将一个函数应用于其部分参数,从而产生一个接受剩余参数的新函数。这允许你从通用函数中创建更具体的函数。例如,如果你有一个函数 add(a, b),你可以部分应用它来创建一个新的函数 add5,它总是将其参数加上 5。

function add(a, b) {
return a + b;
}
const add5 = add.bind(null, 5);
console.log(add5(10)); // Outputs 15

部分应用

部分应用是一种函数式编程技术,它将一个函数应用于其部分参数,从而产生一个接受剩余参数的新函数。这对于从通用函数创建更具体的函数、提高代码可重用性和可读性很有用。

示例

考虑一个接受两个参数的简单 add 函数:

function add(a, b) {
return a + b;
}

使用部分应用,你可以创建一个新的函数 add5,它总是将其参数加上 5:

const add5 = add.bind(null, 5);
console.log(add5(10)); // Outputs 15

工作原理

在上面的例子中,add.bind(null, 5) 创建了一个新函数,其中第一个参数 (a) 被固定为 5。null 值用作 this 上下文,在这种情况下与此无关。

优点

  • 代码可重用性:你可以从通用函数中创建更具体的函数,使你的代码更模块化和可重用。
  • 可读性:部分应用的函数可以通过减少你需要传递的参数数量来使你的代码更容易阅读和理解。

真实世界的例子

部分应用经常用于 Lodash 等库中。例如,Lodash 的 _.partial 函数允许你轻松创建部分应用的函数:

const _ = require('lodash');
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
const sayHelloTo = _.partial(greet, 'Hello');
console.log(sayHelloTo('John')); // Outputs "Hello, John!"

延伸阅读

解释 JavaScript 中的作用域概念

主题
JavaScript

TL;DR

在 JavaScript 中,作用域决定了变量和函数在代码不同部分的可访问性。主要有三种作用域:全局作用域、函数作用域和块级作用域。全局作用域意味着变量可以在代码中的任何地方访问。函数作用域意味着变量只能在其声明的函数内部访问。块级作用域是在 ES6 中引入的,意味着变量只能在其声明的块(例如,在大括号 {} 内)中访问。

var globalVar = 'I am a global var';
function myFunction() {
var functionVar = 'I am a function-scoped var';
if (true) {
let blockVar = 'I am a block-scoped var';
console.log('Inside block:');
console.log(globalVar); // Accessible
console.log(functionVar); // Accessible
console.log(blockVar); // Accessible
}
console.log('Inside function:');
console.log(globalVar); // Accessible
console.log(functionVar); // Accessible
// console.log(blockVar); // Uncaught ReferenceError
}
myFunction();
console.log('In global scope:');
console.log(globalVar); // Accessible
// console.log(functionVar); // Uncaught ReferenceError
// console.log(blockVar); // Uncaught ReferenceError

JavaScript 中的作用域

全局作用域

在任何函数或块外部声明的变量具有全局作用域。它们可以在代码中的任何地方访问。

var globalVar = 'I am global';
function myFunction() {
console.log(globalVar); // Accessible here
}
myFunction();
console.log(globalVar); // Accessible here

函数作用域

在函数内声明的变量属于函数作用域。它们只能在该函数内部访问。

function myFunction() {
var functionVar = 'I am in a function';
console.log(functionVar); // Accessible here
}
myFunction();
console.log(functionVar); // Uncaught ReferenceError: functionVar is not defined

块级作用域

在块(例如,在大括号 {} 内)中使用 letconst 声明的变量具有块级作用域。它们只能在该块内访问。

if (true) {
let blockVar = 'I am in a block';
console.log(blockVar); // Accessible here
}
console.log(blockVar); // Uncaught ReferenceError: blockVar is not defined

词法作用域

JavaScript 使用词法作用域,这意味着变量的作用域由其在源代码中的位置决定。嵌套函数可以访问在其外部作用域中声明的变量。

function outerFunction() {
var outerVar = 'I am outside';
function innerFunction() {
console.log(outerVar); // Accessible here
}
innerFunction();
}
outerFunction();

延伸阅读

解释带标签的模板的概念

主题
JavaScript

TL;DR

JavaScript 中的带标签的模板允许您使用函数解析模板字面量。该函数将字面量字符串和值作为参数接收,从而可以自定义处理模板。例如:

function tag(strings, ...values) {
return strings[0] + values[0] + strings[1] + values[1] + strings[2];
}
const result = tag`Hello ${'world'}! How are ${'you'}?`;
console.log(result); // "Hello world! How are you?"

带标签的模板

什么是带标签的模板?

带标签的模板是 JavaScript 中的一个特性,它允许您使用模板字面量调用一个函数(“标签”)。然后,标签函数可以以自定义方式处理模板字面量的各个部分(字面量字符串和插值)。

语法

带标签的模板的语法涉及在模板字面量之前放置一个函数名称:

function tag(strings, ...values) {
// Custom processing
}
tag`template literal with ${values}`;

工作原理

当调用带标签的模板时,标签函数会收到:

  1. 字面量字符串的数组(模板中未插值的各个部分)
  2. 作为附加参数的插值

例如:

function tag(strings, ...values) {
console.log(strings); // ["Hello ", "! How are ", "?"]
console.log(values); // ["world", "you"]
}
tag`Hello ${'world'}! How are ${'you'}?`;

用例

带标签的模板可用于各种目的,例如:

  • 字符串转义:通过转义用户输入来防止 XSS 攻击
  • 本地化:将模板字面量翻译成不同的语言
  • 自定义格式:将自定义格式应用于插值

示例

这是一个转义 HTML 的带标签模板的简单示例:

function escapeHTML(strings, ...values) {
return strings.reduce((result, string, i) => {
const value = values[i - 1];
return (
result +
(value
? String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
: '') +
string
);
});
}
const userInput = '<script>alert("XSS")</script>';
const result = escapeHTML`User input: ${userInput}`;
console.log(result); // "User input: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;"

延伸阅读

解释测试驱动开发 (TDD) 的概念

主题
JavaScript测试

TL;DR

测试驱动开发 (TDD) 是一种软件开发方法,您在编写实际代码之前编写测试。该过程包括编写一个失败的测试,编写通过测试所需的最小代码,然后在保持测试通过的同时重构代码。这确保了代码始终经过测试,并有助于保持高代码质量。


什么是测试驱动开发 (TDD)?

测试驱动开发 (TDD) 是一种软件开发方法,它强调在编写实际代码之前编写测试。 TDD 的主要目标是确保代码经过全面测试并满足指定的要求。 TDD 过程可以分为三个主要步骤:红色、绿色和重构。

红色:编写一个失败的测试

  1. 为新功能或功能编写测试。
  2. 运行测试以确保它失败,确认该功能尚未实现。
// Example using Jest
test('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3);
});

绿色:编写通过测试所需的最小代码

  1. 编写最简单的代码以使测试通过。
  2. 运行测试以确保它通过。
function add(a, b) {
return a + b;
}

重构:改进代码

  1. 重构代码以改进其结构和可读性,而无需更改其行为。
  2. 确保所有测试在重构后仍然通过。
// Refactored code (if needed)
function add(a, b) {
return a + b; // In this simple example, no refactoring is needed
}

TDD 的好处

改进的代码质量

TDD 确保代码经过全面测试,这有助于在开发过程的早期识别和修复错误。

更好的设计

首先编写测试迫使开发人员思考代码的设计和需求,从而产生结构更好、更易于维护的代码。

更快的调试

由于为每个功能都编写了测试,因此当测试失败时,更容易确定错误的来源。

文档

测试充当代码的文档,使其他开发人员更容易理解代码的功能和目的。

TDD 的挑战

初始学习曲线

刚接触 TDD 的开发人员可能会发现最初采用这种方法具有挑战性。

耗时

在编写实际代码之前编写测试可能很耗时,尤其对于复杂的功能而言。

开销

维护大量测试可能会成为一种开销,尤其是在代码库经常更改时。

延伸阅读

解释原型模式的概念

主题
JavaScriptOOP

TL;DR

原型模式是一种创建型设计模式,用于通过复制现有对象(称为原型)来创建新对象。当创建新对象的成本高于克隆现有对象时,此模式非常有用。在 JavaScript 中,这可以使用 Object.create 方法或使用构造函数函数的 prototype 属性来实现。

const prototypeObject = {
greet() {
console.log('Hello, world!');
},
};
const newObject = Object.create(prototypeObject);
newObject.greet(); // Outputs: Hello, world!

原型模式

原型模式是一种创建型设计模式,它允许您通过复制现有对象(称为原型)来创建新对象。当创建新对象的成本高于克隆现有对象时,此模式特别有用。

工作原理

在原型模式中,一个对象被用作创建新对象的蓝图。这个蓝图对象被称为原型。通过复制原型来创建新对象,这可以通过多种方式完成,具体取决于编程语言。

在 JavaScript 中的实现

在 JavaScript 中,原型模式可以使用 Object.create 方法或使用构造函数函数的 prototype 属性来实现。

使用 Object.create

Object.create 方法使用指定原型对象和属性创建一个新对象。

const prototypeObject = {
greet() {
console.log('Hello, world!');
},
};
const newObject = Object.create(prototypeObject);
newObject.greet(); // Outputs: Hello, world!

在此示例中,newObjectprototypeObject 作为其原型创建。这意味着 newObjectprototypeObject 继承了 greet 方法。

使用构造函数和 prototype 属性

在 JavaScript 中实现原型模式的另一种方法是使用构造函数和 prototype 属性。

function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log(`Hello, my name is ${this.name}`);
};
const person1 = new Person('Alice');
const person2 = new Person('Bob');
person1.greet(); // Outputs: Hello, my name is Alice
person2.greet(); // Outputs: Hello, my name is Bob

在此示例中,Person 构造函数用于创建新的 Person 对象。greet 方法被添加到 Person.prototype,因此 Person 的所有实例都继承此方法。

优点

  • 通过克隆现有对象来降低创建新对象的成本
  • 简化复杂对象的创建
  • 促进代码重用并减少冗余

缺点

  • 在某些情况下,克隆对象可能不如创建新对象有效
  • 如果原型对象包含嵌套对象,则可能导致深度克隆出现问题

延伸阅读

解释单例模式的概念

主题
JavaScript

TL;DR

单例模式确保一个类只有一个实例,并提供对该实例的全局访问点。当只需要一个对象来协调整个系统中的操作时,这非常有用。在 JavaScript 中,这可以使用闭包或 ES6 类来实现。

class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
}
return Singleton.instance;
}
}
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true

单例模式

单例模式是一种设计模式,它将一个类的实例化限制为单个实例。当只需要一个对象来协调整个系统中的操作时,这特别有用。

关键特征

  • 单个实例:确保一个类只有一个实例。
  • 全局访问:提供对该实例的全局访问点。
  • 延迟初始化:仅在需要时才创建实例。

在 JavaScript 中的实现

有几种方法可以在 JavaScript 中实现单例模式。以下是两种常见的方法:

使用闭包
const Singleton = (function () {
let instance;
function createInstance() {
const object = new Object('I am the instance');
return object;
}
return {
getInstance: function () {
if (!instance) {
instance = createInstance();
}
return instance;
},
};
})();
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true
使用 ES6 类
class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
}
return Singleton.instance;
}
}
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true

用例

  • 配置对象:当您需要一个在整个应用程序中共享的配置对象时。
  • 日志记录:用于管理日志条目的单个日志记录实例。
  • 数据库连接:确保只建立一个到数据库的连接。

延伸阅读

解释展开运算符的概念及其用途

主题
JavaScript

TL;DR

JavaScript 中的展开运算符 (...) 允许你将可迭代对象(如数组或对象)的元素展开为单个元素。它通常用于复制数组或对象、合并数组或对象,以及将数组的元素作为参数传递给函数。

// Copying an array
const arr1 = [1, 2, 3];
const arr2 = [...arr1];
console.log(arr2); // Output: [1, 2, 3]
// Merging arrays
const arr3 = [4, 5, 6];
const mergedArray = [...arr1, ...arr3];
console.log(mergedArray); // Output: [1, 2, 3, 4, 5, 6]
// Copying an object
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1 };
console.log(obj2); // Output: { a: 1, b: 2 }
// Merging objects
const obj3 = { c: 3, d: 4 };
const mergedObject = { ...obj1, ...obj3 };
console.log(mergedObject); // Output: { a: 1, b: 2, c: 3, d: 4 }
// Passing array elements as function arguments
const sum = (x, y, z) => x + y + z;
const numbers = [1, 2, 3];
console.log(sum(...numbers)); // Output: 6

展开运算符及其用途

复制数组

展开运算符可用于创建数组的浅拷贝。当你想要复制一个数组而不影响原始数组时,这很有用。

const arr1 = [1, 2, 3];
const arr2 = [...arr1];
console.log(arr2); // Output: [1, 2, 3]

合并数组

你可以使用展开运算符将多个数组合并成一个。这是一种简洁易读的组合数组的方式。

const arr1 = [1, 2, 3];
const arr3 = [4, 5, 6];
const mergedArray = [...arr1, ...arr3];
console.log(mergedArray); // Output: [1, 2, 3, 4, 5, 6]

复制对象

与数组类似,展开运算符可用于创建对象的浅拷贝。这对于复制对象而不影响原始对象很有用。

const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1 };
console.log(obj2); // Output: { a: 1, b: 2 }

合并对象

展开运算符也可用于将多个对象合并成一个。这对于组合来自不同对象的属性特别有用。

const obj1 = { a: 1, b: 2 };
const obj3 = { c: 3, d: 4 };
const mergedObject = { ...obj1, ...obj3 };
console.log(mergedObject); // Output: { a: 1, b: 2, c: 3, d: 4 }

将数组元素作为函数参数传递

展开运算符允许你将数组的元素作为单个参数传递给函数。这对于接受多个参数的函数很有用。

const sum = (x, y, z) => x + y + z;
const numbers = [1, 2, 3];
console.log(sum(...numbers)); // Output: 6

延伸阅读

解释策略模式的概念

主题
JavaScript

TL;DR

策略模式是一种行为型设计模式,它允许您定义一系列算法,将每个算法封装成一个单独的类,并使它们可以互换。这种模式使算法可以独立于使用它的客户端而变化。例如,如果您有不同的排序算法,您可以将每个算法定义为一个策略,并在它们之间切换,而无需更改客户端代码。

class Context {
constructor(strategy) {
this.strategy = strategy;
}
executeStrategy(data) {
return this.strategy.doAlgorithm(data);
}
}
class ConcreteStrategyA {
doAlgorithm(data) {
// Implementation of algorithm A
return 'Algorithm A was run on ' + data;
}
}
class ConcreteStrategyB {
doAlgorithm(data) {
// Implementation of algorithm B
return 'Algorithm B was run on ' + data;
}
}
// Usage
const context = new Context(new ConcreteStrategyA());
context.executeStrategy('someData'); // Output: Algorithm A was run on someData

策略模式

定义

策略模式是一种行为型设计模式,它允许在运行时选择算法的行为。它定义了一系列算法,封装了每一个算法,并使它们可以互换。这种模式允许算法独立于使用它的客户端而变化。

组件

  1. Context: 维护对 Strategy 对象的引用,并使用 ConcreteStrategy 对象进行配置。
  2. Strategy: 所有支持的算法通用的接口。Context 使用此接口来调用由 ConcreteStrategy 定义的算法。
  3. ConcreteStrategy: 实现 Strategy 接口以提供特定的算法。

示例

考虑一个场景,您有不同的排序算法,并且您希望在它们之间切换,而无需更改客户端代码。

// Strategy interface
class Strategy {
doAlgorithm(data) {
throw new Error('This method should be overridden!');
}
}
// ConcreteStrategyA
class ConcreteStrategyA extends Strategy {
doAlgorithm(data) {
return data.sort((a, b) => a - b); // Example: ascending sort
}
}
// ConcreteStrategyB
class ConcreteStrategyB extends Strategy {
doAlgorithm(data) {
return data.sort((a, b) => b - a); // Example: descending sort
}
}
// Context
class Context {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
executeStrategy(data) {
return this.strategy.doAlgorithm(data);
}
}
// Usage
const data = [3, 1, 4, 1, 5, 9];
const context = new Context(new ConcreteStrategyA());
console.log(context.executeStrategy([...data])); // Output: [1, 1, 3, 4, 5, 9]
context.setStrategy(new ConcreteStrategyB());
console.log(context.executeStrategy([...data])); // Output: [9, 5, 4, 3, 1, 1]

优点

  • 灵活性:您可以在运行时更改算法。
  • 可维护性:添加新的策略不会影响现有代码。
  • 封装:每个算法都封装在自己的类中。

缺点

  • 开销:增加了类和对象的数量。
  • 复杂性:如果使用不当,会使系统更加复杂。

延伸阅读

解释 Web Socket API 的概念

主题
JavaScript网络

TL;DR

WebSocket API 提供了一种在客户端和服务器之间打开持久连接的方式,允许进行实时的双向通信。与基于请求-响应的 HTTP 不同,WebSocket 实现了全双工通信,这意味着客户端和服务器都可以独立地发送和接收消息。这对于聊天应用程序、实时更新和在线游戏等应用程序特别有用。

以下示例使用 Postman 的 WebSocket 回显服务来演示 WebSockets 的工作原理。

// Postman's echo server that will echo back messages you send
const socket = new WebSocket('wss://ws.postman-echo.com/raw');
// Event listener for when the connection is open
socket.addEventListener('open', function (event) {
socket.send('Hello Server!'); // Sends the message to the Postman WebSocket server
});
// Event listener for when a message is received from the server
socket.addEventListener('message', function (event) {
console.log('Message from server ', event.data);
});

什么是 WebSocket API?

WebSocket API 是一种技术,它提供了一种在客户端(通常是 Web 浏览器)和服务器之间建立持久的、低延迟的、全双工通信通道的方式。这与传统的 HTTP 请求-响应模型不同,后者是无状态的,并且需要为每个请求建立新的连接。

关键特性

  • 全双工通信:客户端和服务器都可以独立地发送和接收消息。
  • 低延迟:持久连接减少了为每条消息建立新连接的开销。
  • 实时更新:非常适合需要实时数据的应用程序,例如聊天应用程序、实时体育更新和在线游戏。

它是如何工作的

  1. 连接建立:客户端通过向服务器发送握手请求来启动 WebSocket 连接。
  2. 握手响应:服务器使用握手响应进行响应,如果成功,则建立连接。
  3. 数据交换:客户端和服务器现在可以通过已建立的连接独立地发送和接收消息。
  4. 连接关闭:当不再需要连接时,客户端或服务器都可以关闭连接。

使用示例

这是一个关于如何在 JavaScript 中使用 WebSocket API 的基本示例,使用 Postman 的 WebSocket Echo Service。

// Postman's echo server that will echo back messages you send
const socket = new WebSocket('wss://ws.postman-echo.com/raw');
// Event listener for when the connection is open
socket.addEventListener('open', function (event) {
console.log('Connection opened');
socket.send('Hello Server!'); // Sends the message to the Postman WebSocket server
});
// Event listener for when a message is received from the server
socket.addEventListener('message', function (event) {
console.log('Message from server ', event.data);
});
// Event listener for when the connection is closed
socket.addEventListener('close', function (event) {
console.log('Connection closed');
});
// Event listener for when an error occurs
socket.addEventListener('error', function (event) {
console.error('WebSocket error: ', event);
});

用例

  • 聊天应用程序:用户之间的实时消息传递。
  • 实时更新:股票价格、体育比分或新闻更新。
  • 在线游戏:玩家之间的实时互动。
  • 协作工具:实时文档编辑或白板。

延伸阅读

解释事件处理程序中 `this` 绑定的概念

主题
闭合Web APIJavaScript

TL;DR

在 JavaScript 中,this 关键字指的是当前正在执行代码的对象。在事件处理程序中,this 通常指的是触发事件的元素。但是,this 的值可能会根据事件处理程序的定义和调用方式而改变。为了确保 this 指的是期望的对象,你可以使用 bind() 方法、箭头函数或显式地分配上下文。


事件处理程序中 this 绑定的概念

理解 JavaScript 中的 this

在 JavaScript 中,this 关键字是对当前正在执行代码的对象的引用。this 的值由函数的调用方式决定,而不是定义的位置。这可能导致 this 在不同的上下文中具有不同的值。

事件处理程序中的 this

在事件处理程序的上下文中,this 通常指的是触发事件的 DOM 元素。例如:

// 创建一个 button 元素并将其附加到 DOM
const button = document.createElement('button');
button.id = 'myButton';
document.body.appendChild(button);
document.getElementById('myButton').addEventListener('click', function () {
console.log(this); // `this` 指的是 'myButton' 元素
});
button.click(); // 记录 button 元素

在这个例子中,事件处理程序中的 this 指的是被点击的 button 元素。

改变 this 的值

有几种方法可以改变事件处理程序中 this 的值:

使用 bind()

bind() 方法创建一个新函数,当调用该函数时,其 this 关键字设置为提供的值:

// 创建一个 button 元素并将其附加到 DOM
const button = document.createElement('button');
button.id = 'myButton';
document.body.appendChild(button);
function handleClick() {
console.log(this); // 记录传递给 bind() 的对象
}
const obj = { name: 'MyObject' };
document
.getElementById('myButton')
.addEventListener('click', handleClick.bind(obj));
button.click(); // 记录 obj,因为 handleClick 使用 bind() 绑定到 obj

在这个例子中,handleClick 中的 this 指的是 obj

使用箭头函数

箭头函数没有自己的 this 上下文;它们从周围的词法上下文中继承 this

// 创建一个 button 元素并将其附加到 DOM
const button = document.createElement('button');
button.id = 'myButton';
document.body.appendChild(button);
const obj = {
name: 'MyObject',
handleClick: function () {
document.getElementById('myButton').addEventListener('click', () => {
console.log(this); // 记录 obj
});
},
};
obj.handleClick();
button.click(); // 这将记录 obj

在这个例子中,箭头函数中的 this 指的是 obj

显式地分配上下文

您还可以使用变量显式地分配上下文:

// Create a button element and append it to the DOM
const button = document.createElement('button');
button.id = 'myButton';
document.body.appendChild(button);
const obj = {
name: 'MyObject',
handleClick: function () {
const self = this;
document.getElementById('myButton').addEventListener('click', function () {
console.log(self); // Logs obj
});
},
};
obj.handleClick();
button.click(); // This will log obj

在这个例子中,self 用于捕获外部函数中 this 的值。

延伸阅读

解释模块打包中的 Tree Shaking 概念

主题
JavaScript

TL;DR

Tree shaking 是一种用于模块打包的技术,用于消除死代码,即从未被使用或执行的代码。这有助于减小最终的 bundle 大小并提高应用程序的性能。它通过分析代码的依赖关系图并删除任何未使用的导出来实现。Webpack 和 Rollup 等工具在使用 ES6 模块语法(importexport)时支持 Tree shaking。


模块打包中的 Tree Shaking 概念

Tree shaking 是一个常用术语,通常用于 JavaScript 模块打包器,如 Webpack 和 Rollup。它指的是从最终的 bundle 中消除死代码的过程,这有助于减小 bundle 大小并提高应用程序的性能。

Tree shaking 的工作原理

Tree shaking 通过分析代码的依赖关系图来工作。它查看 importexport 语句,以确定代码的哪些部分实际被使用,哪些没有被使用。然后,将未使用的代码(也称为死代码)从最终的 bundle 中删除。

示例

考虑以下示例:

// utils.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// main.js
import { add } from './utils';
console.log(add(2, 3));

在这个例子中,subtract 函数在 main.js 中从未使用。支持 Tree Shaking 的打包器会识别这一点,并将 subtract 函数从最终的 bundle 中排除。

Tree shaking 的要求

  1. ES6 模块语法:Tree shaking 依赖于 ES6 模块语法(importexport)的静态结构。CommonJS 模块(requiremodule.exports)无法进行静态分析,因此不适合进行 Tree shaking。
  2. Bundler 支持:您使用的打包器必须支持 Tree shaking。Webpack 和 Rollup 都内置了对 Tree shaking 的支持。

支持 Tree shaking 的工具

  • Webpack: 使用 ES6 模块时,Webpack 开箱即支持 Tree Shaking。您可以通过在 Webpack 配置中将 mode 设置为 production 来启用它。
  • Rollup: Rollup 在设计时就考虑了 Tree Shaking,并为此提供了出色的支持。

Tree shaking 的好处

  • 减小 bundle 大小:通过删除未使用的代码,最终的 bundle 大小减小,从而加快加载时间。
  • 提高性能:更小的 bundle 意味着更少的代码需要解析和执行,这可以提高应用程序的性能。

延伸阅读

解释经典继承和原型继承的区别

主题
JavaScriptOOP

TL;DR

经典继承是一种模型,其中类从其他类继承,通常在 Java 和 C++ 等语言中可见。原型继承(在 JavaScript 中使用)涉及对象直接从其他对象继承。在经典继承中,您定义一个类并从中创建实例。在原型继承中,您创建一个对象并将其用作其他对象的原型。


经典继承和原型继承的区别

经典继承

经典继承是许多面向对象编程语言(如 Java、C++ 和 Python)中使用的一种模式。它涉及创建一个类层次结构,其中类从其他类继承属性和方法。

  • 类定义:您定义一个具有属性和方法的类。
  • 实例化:您创建类的实例(对象)。
  • 继承:一个类可以从另一个类继承,形成父子关系。

Java 中的示例:

class Animal {
void eat() {
System.out.println("This animal eats food.");
}
}
class Dog extends Animal {
void bark() {
System.out.println("The dog barks.");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // Inherited method
dog.bark(); // Own method
}
}

原型继承

原型继承是 JavaScript 的一个特性,其中对象直接从其他对象继承。没有类;相反,对象充当其他对象的原型。

  • 对象创建:您直接创建一个对象。
  • 原型链:对象可以通过原型链从其他对象继承属性和方法。
  • 灵活性:您可以动态地添加或修改属性和方法。

JavaScript 中的示例:

const animal = {
eat() {
console.log('This animal eats food.');
},
};
const dog = Object.create(animal);
dog.bark = function () {
console.log('The dog barks.');
};
dog.eat(); // Inherited method (Output: The animal eats food.)
dog.bark(); // Own method (Output: The dog barks.)

主要区别

  • 基于类的与基于原型的:经典继承使用类,而原型继承使用对象。
  • 继承模型:经典继承形成一个类层次结构,而原型继承形成一个原型链。
  • 灵活性:原型继承更灵活和动态,允许在运行时进行更改。

延伸阅读

解释 `document.querySelector()` 和 `document.getElementById()` 之间的区别

主题
Web APIJavaScriptHTML

TL;DR

document.querySelector()document.getElementById() 都是用于从 DOM 中选择元素的方法,但它们有关键的区别。document.querySelector() 可以使用 CSS 选择器选择任何元素并返回第一个匹配项,而 document.getElementById() 通过其 ID 选择一个元素并返回具有该特定 ID 的元素。

// 使用 document.querySelector()
const element = document.querySelector('.my-class');
// 使用 document.getElementById()
const elementById = document.getElementById('my-id');

document.querySelector()document.getElementById() 之间的区别

document.querySelector()

  • 可以使用任何有效的 CSS 选择器选择元素,包括类、ID、标签、属性和伪类
  • 返回与指定选择器匹配的第一个元素
  • 更通用,但由于 CSS 选择器的灵活性,速度稍慢
// 选择具有类 'my-class' 的第一个元素
const element = document.querySelector('.my-class');
// 选择第一个 <div> 元素
const divElement = document.querySelector('div');
// 选择具有属性 data-role='button' 的第一个元素
const buttonElement = document.querySelector('[data-role="button"]');

document.getElementById()

  • 通过其 ID 属性选择一个元素
  • 返回具有指定 ID 的元素
  • 通过 ID 选择元素更快更有效,但通用性较差
// 选择具有 ID 'my-id' 的元素
const elementById = document.getElementById('my-id');

关键区别

  • 选择器类型document.querySelector() 使用 CSS 选择器,而 document.getElementById() 仅使用 ID 属性。
  • 返回值document.querySelector() 返回第一个匹配的元素,而 document.getElementById() 返回具有指定 ID 的元素。
  • 性能document.getElementById() 通常更快,因为它直接通过 ID 访问元素,而 document.querySelector() 必须解析 CSS 选择器。

延伸阅读

解释使用点符号和方括号符号访问对象属性的区别

主题
JavaScript

TL;DR

点符号和方括号符号是 JavaScript 中访问对象属性的两种方式。点符号更简洁易读,但只能用于有效的 JavaScript 标识符。方括号符号更灵活,可以用于不是有效标识符的属性名称,例如包含空格或特殊字符的属性名称。

const obj = { name: 'Alice', 'favorite color': 'blue' };
// Dot notation
console.log(obj.name); // Alice
// Bracket notation
console.log(obj['favorite color']); // blue

点符号 vs. 方括号符号

点符号

点符号是访问对象属性最常见和最直接的方式。它简洁易读,但也有一些限制。

语法
object.property;
示例
const person = {
name: 'Alice',
age: 30,
};
console.log(person.name); // Alice
console.log(person.age); // 30
限制
  • 属性名称必须是有效的 JavaScript 标识符(例如,没有空格、特殊字符,或不以数字开头)
  • 属性名称不能是动态的(即,它们必须是硬编码的)

方括号符号

方括号符号更灵活,可以在点符号无法使用的情况下使用。

语法
object['property'];
示例
const person = {
name: 'Alice',
'favorite color': 'blue',
1: 'one',
};
console.log(person['name']); // Alice
console.log(person['favorite color']); // blue
console.log(person[1]); // one
优点
  • 可以访问名称不是有效 JavaScript 标识符的属性
  • 可以使用变量或表达式来动态确定属性名称
动态属性名示例
const person = {
name: 'Alice',
age: 30,
};
const property = 'name';
console.log(person[property]); // Alice

延伸阅读

解释全局作用域、函数作用域和块级作用域的区别

主题
JavaScript

TL;DR

全局作用域意味着变量可以在代码中的任何地方访问。函数作用域意味着变量只能在其声明的函数内部访问。块级作用域意味着变量只能在其声明的块(例如,在 {} 内)中访问。

var globalVar = "I'm global"; // 全局作用域
function myFunction() {
var functionVar = "I'm in a function"; // 函数作用域
if (true) {
let blockVar = "I'm in a block"; // 块级作用域
console.log(blockVar); // 可以在这里访问
}
// console.log(blockVar); // Uncaught ReferenceError: blockVar is not defined
}
// console.log(functionVar); // Uncaught ReferenceError: functionVar is not defined
myFunction();

全局作用域、函数作用域和块级作用域

全局作用域

在全局作用域中声明的变量可以在代码中的任何地方访问。在浏览器环境中,这些变量会成为 window 对象的属性。

var globalVar = "I'm global";
function checkGlobal() {
console.log(globalVar); // Accessible here
}
checkGlobal(); // Output: "I'm global"
console.log(globalVar); // Output: "I'm global"

函数作用域

在函数内声明的变量只能在该函数内部访问。这对于使用 varletconst 声明的变量都是正确的。

function myFunction() {
var functionVar = "I'm in a function";
console.log(functionVar); // 可以在这里访问
}
myFunction(); // Output: "I'm in a function"
console.log(functionVar); // Uncaught ReferenceError: functionVar is not defined

块级作用域

使用 letconst 在块(例如,在 {} 内)中声明的变量只能在该块内访问。这对于 var 来说是不成立的,var 是函数作用域的。

if (true) {
let blockVar = "I'm in a block";
console.log(blockVar); // 可以在这里访问
}
// console.log(blockVar); // Uncaught ReferenceError: blockVar is not defined
if (true) {
var blockVarVar = "I'm in a block but declared with var";
console.log(blockVarVar); // 可以在这里访问
}
console.log(blockVarVar); // Output: "I'm in a block but declared with var"

延伸阅读

解释浅拷贝和深拷贝的区别

主题
JavaScript

TL;DR

浅拷贝复制对象的顶层属性,但嵌套对象仍然被引用。深拷贝复制对象的所有层级,创建嵌套对象的全新实例。例如,使用 Object.assign() 创建浅拷贝,而使用像 Lodash 或现代 JavaScript 中的 structuredClone() 这样的库可以创建深拷贝。

// Shallow copy example
let obj1 = { a: 1, b: { c: 2 } };
let shallowCopy = Object.assign({}, obj1);
shallowCopy.b.c = 3;
console.log(shallowCopy.b.c); // Output: 3
console.log(obj1.b.c); // Output: 3 (original nested object changed too!)
// Deep copy example
let obj2 = { a: 1, b: { c: 2 } };
let deepCopy = JSON.parse(JSON.stringify(obj2));
deepCopy.b.c = 4;
console.log(deepCopy.b.c); // Output: 4
console.log(obj2.b.c); // Output: 2 (original nested object remains unchanged)

浅拷贝和深拷贝的区别

浅拷贝

浅拷贝创建一个新对象,并将原始对象的顶层属性的值复制到新对象中。但是,如果这些属性中的任何一个是其他对象的引用,则仅复制引用,而不是实际对象。这意味着对复制对象中嵌套对象的更改将影响原始对象。

示例
let obj1 = { a: 1, b: { c: 2 } };
let shallowCopy = Object.assign({}, obj1);
shallowCopy.b.c = 3;
console.log(shallowCopy.b.c); // Output: 3
console.log(obj1.b.c); // Output: 3 (original nested object changed too!)

在此示例中,shallowCopyobj1 的浅拷贝。更改 shallowCopy.b.c 也会更改 obj1.b.c,因为 bobj1shallowCopy 中对同一对象的引用。

深拷贝

深拷贝创建一个新对象,并递归地复制原始对象的所有属性和嵌套对象。这意味着新对象完全独立于原始对象,并且对复制对象中嵌套对象的更改不会影响原始对象。

示例
let obj1 = { a: 1, b: { c: 2 } };
let deepCopy = JSON.parse(JSON.stringify(obj1));
deepCopy.b.c = 4;
console.log(deepCopy.b.c); // Output: 4
console.log(obj1.b.c); // Output: 2 (original nested object remains unchanged)

在此示例中,deepCopyobj1 的深拷贝。更改 deepCopy.b.c 不会影响 obj1.b.c,因为 bdeepCopy 中是一个全新的对象。

创建浅拷贝和深拷贝的方法

浅拷贝方法
  • Object.assign()
  • 展开运算符 (...)
深拷贝方法
  • JSON.parse(JSON.stringify())
  • structuredClone() (现代 JavaScript)
  • Lodash 这样的库 (_.cloneDeep)

延伸阅读

解释单元测试、集成测试和端到端测试的区别

主题
JavaScript测试

TL;DR

单元测试侧重于隔离测试单个组件或函数,以确保它们按预期工作。集成测试检查不同模块或服务如何协同工作。端到端测试模拟真实用户场景,以验证从开始到结束的整个应用程序流程。


单元测试、集成测试和端到端测试的区别

单元测试

单元测试涉及隔离测试单个组件或函数。目标是确保代码的每个部分都能正常工作。这些测试通常由开发人员编写,是防止错误的的第一道防线。

  • 范围:单个函数或组件
  • 工具:Jest、Mocha、Jasmine
  • 示例:测试一个将两个数字相加的函数
function add(a, b) {
return a + b;
}
test('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3);
});

集成测试

集成测试侧重于验证不同模块或服务之间的交互。目标是确保应用程序的组合部分按预期协同工作。这些测试通常比单元测试更复杂,可能涉及多个组件。

  • 范围:多个组件或服务
  • 工具:Jest、Mocha、Jasmine、Postman(用于 API 测试)
  • 示例:测试一个从 API 获取数据并处理数据的函数
async function fetchData(apiUrl) {
const response = await fetch(apiUrl);
const data = await response.json();
return processData(data);
}
test('fetches and processes data correctly', async () => {
const apiUrl = 'https://api.example.com/data';
const data = await fetchData(apiUrl);
expect(data).toEqual(expectedProcessedData);
});

端到端测试

端到端 (E2E) 测试模拟真实用户场景,以验证从开始到结束的整个应用程序流程。目标是确保应用程序作为一个整体工作,包括用户界面、后端和任何外部服务。

  • 范围:整个应用程序
  • 工具:Cypress、Selenium、Puppeteer
  • 示例:测试用户登录流程
describe('User Login Flow', () => {
it('should allow a user to log in', () => {
cy.visit('https://example.com/login');
cy.get('input[name="username"]').type('testuser');
cy.get('input[name="password"]').type('password123');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
});
});

延伸阅读

解释 `var`、`let` 和 `const` 之间的变量提升区别

主题
JavaScript

总结

var 声明会被提升到其作用域的顶部并使用 undefined 初始化,允许它们在其声明之前被使用。letconst 声明也会被提升,但不会被初始化,如果在声明之前访问它们,则会导致 ReferenceErrorconst 声明在声明时还需要一个初始值。


varletconst 之间的变量提升区别

var 的变量提升

var 声明会被提升到其包含的函数或全局作用域的顶部。这意味着变量可以在整个函数或脚本中使用,即使在声明它的行之前也是如此。但是,在遇到实际声明之前,变量会被初始化为 undefined

console.log(a); // Output: undefined
var a = 10;
console.log(a); // Output: 10

let 的变量提升

let 声明也会被提升到其块级作用域的顶部,但它们不会被初始化。这会在块的开始到声明被遇到之间创建一个“暂时性死区”(TDZ)。在 TDZ 中访问变量会导致 ReferenceError

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;
console.log(b); // Output: 20

const 的变量提升

const 声明在变量提升方面与 let 类似。它们被提升到其块级作用域的顶部,但未被初始化,从而导致 TDZ。此外,const 在声明时需要一个初始值,并且不能被重新赋值。

console.log(c); // ReferenceError: Cannot access 'c' before initialization
const c = 30;
console.log(c); // Output: 30

延伸阅读

解释 Promise 的不同状态

主题
异步JavaScript

总结

JavaScript 中的 Promise 可以处于三种状态之一:pending(待定)、fulfilled(已实现)或 rejected(已拒绝)。创建 Promise 时,它首先处于 pending 状态。如果操作成功完成,Promise 将转换为 fulfilled 状态,如果失败,则转换为 rejected 状态。 这是一个快速示例:

let promise = new Promise((resolve, reject) => {
// some asynchronous operation
if (success) {
resolve('Success!');
} else {
reject('Error!');
}
});

Promise 的不同状态

待定

首次创建 Promise 时,它处于 pending 状态。 这意味着异步操作尚未完成。

let promise = new Promise((resolve, reject) => {
// asynchronous operation
});

已实现

当异步操作成功完成时,Promise 转换为 fulfilled 状态。 调用 resolve 函数来指示这一点。

let promise = new Promise((resolve, reject) => {
resolve('Success!');
});

已拒绝

当异步操作失败时,Promise 转换为 rejected 状态。 调用 reject 函数来指示这一点。

let promise = new Promise((resolve, reject) => {
reject('Error!');
});

延伸阅读

解释 `this` 关键字可以绑定的不同方式

主题
闭合JavaScript

TL;DR

this 关键字在 JavaScript 中可以通过几种方式绑定:

  • 默认绑定:在非严格模式下,this 指向全局对象(在浏览器中是 window)。在严格模式下,thisundefined
  • 隐式绑定:当一个函数作为对象的方法被调用时,this 指向该对象。
  • 显式绑定:使用 callapplybind 方法显式设置 this
  • new 绑定:当一个函数与 new 关键字一起用作构造函数时,this 指向新创建的对象。
  • 箭头函数:箭头函数没有自己的 this,并从周围的词法环境继承 this

默认绑定

在非严格模式下,如果一个函数在没有任何上下文的情况下被调用,this 指向全局对象(在浏览器中是 window)。在严格模式下,thisundefined

function showThis() {
console.log(this);
}
showThis(); // 在非严格模式下:window,在严格模式下:undefined

隐式绑定

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

const obj = {
name: 'Alice',
greet: function () {
console.log(this.name);
},
};
obj.greet(); // 'Alice'

显式绑定

使用 callapplybind 方法,你可以显式地设置 this

使用 call

function greet() {
console.log(this.name);
}
const person = { name: 'Bob' };
greet.call(person); // 'Bob'

使用 apply

function greet(greeting) {
console.log(greeting + ', ' + this.name);
}
const person = { name: 'Charlie' };
greet.apply(person, ['Hello']); // 'Hello, Charlie'

使用 bind

function greet() {
console.log(this.name);
}
const person = { name: 'Dave' };
const boundGreet = greet.bind(person);
boundGreet(); // 'Dave'

New 绑定

当一个函数与 new 关键字一起用作构造函数时,this 指的是新创建的对象。

function Person(name) {
this.name = name;
}
const person = new Person('Eve');
console.log(person.name); // 'Eve'

箭头函数

箭头函数没有自己的 this,并从周围的词法上下文中继承 this

const obj = {
firstName: 'Frank',
greet: () => {
console.log(this.firstName);
},
};
obj.greet(); // undefined, because `this` is inherited from the global scope

延伸阅读

解释浏览器中的事件阶段

主题
浏览器JavaScript

TL;DR

在浏览器中,事件经历三个阶段:捕获、目标和冒泡。在捕获阶段,事件从根节点传递到目标元素。在目标阶段,事件到达目标元素。最后,在冒泡阶段,事件从目标元素传递回根节点。您可以使用带有 capture 选项的 addEventListener 来控制事件处理。


浏览器中的事件阶段

捕获阶段

捕获阶段,也称为渗透阶段,是事件传播的第一个阶段。在此阶段,事件从 DOM 树的根开始,向下传递到目标元素。在此阶段注册的事件监听器将按照从最外层祖先到目标元素的顺序触发。

element.addEventListener('click', handler, true); // true indicates capturing phase

目标阶段

目标阶段是事件传播的第二个阶段。在此阶段,事件已到达目标元素本身。直接在目标元素上注册的事件监听器将在此阶段触发。

element.addEventListener('click', handler); // default is target phase

冒泡阶段

冒泡阶段是事件传播的最后一个阶段。在此阶段,事件从目标元素传递回 DOM 树的根。在此阶段注册的事件监听器将按照从目标元素到最外层祖先的顺序触发。

element.addEventListener('click', handler, false); // false indicates bubbling phase

控制事件传播

您可以使用 stopPropagationstopImmediatePropagation 等方法来控制事件传播。这些方法可以在事件处理程序中调用,以阻止事件进一步传播。

element.addEventListener('click', function (event) {
event.stopPropagation(); // Stops the event from propagating further
});

延伸阅读

解释观察者模式及其用例

主题
JavaScript

总结

观察者模式是一种设计模式,其中一个对象(称为主题)维护其依赖项(称为观察者)的列表,并通知它们任何状态更改。此模式对于实现分布式事件处理系统非常有用,例如响应数据更改更新用户界面或实现事件驱动的架构。


什么是观察者模式?

观察者模式是一种行为设计模式,它定义了对象之间的一对多依赖关系。当主题(一个)的状态发生变化时,它的所有观察者(多个)都会自动收到通知并更新。此模式特别适用于需要在不紧密耦合的情况下,将一个对象中的更改反映到多个其他对象中的场景。

关键组件

  1. 主题:保存状态并向观察者发送通知的对象。
  2. 观察者:需要接收主题更改通知的对象。
  3. 具体主题:主题的具体实现。
  4. 具体观察者:观察者的具体实现。

示例

这是一个简单的 JavaScript 示例:

class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
this.observers = this.observers.filter((obs) => obs !== observer);
}
notifyObservers() {
this.observers.forEach((observer) => observer.update());
}
}
class Observer {
update() {
console.log('Observer updated');
}
}
// Usage
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notifyObservers(); // Both observers will be updated

用例

用户界面更新

在前端开发中,观察者模式通常用于响应数据更改来更新用户界面。例如,在模型-视图-控制器 (MVC) 架构中,视图可以观察模型,并在模型状态发生变化时自行更新。

事件处理

观察者模式对于实现事件驱动系统非常有用。例如,在 JavaScript 中,addEventListener 方法允许您为单个事件(主题)注册多个事件处理程序(观察者)。

实时数据馈送

需要实时更新的应用程序(例如股票行情或聊天应用程序)可以从观察者模式中受益。观察者可以订阅数据馈送,并在有新数据可用时收到通知。

依赖管理

在复杂的应用程序中,管理不同模块之间的依赖关系可能具有挑战性。观察者模式有助于解耦这些模块,使系统更具模块化,更易于维护。

延伸阅读

如何使用闭包创建私有变量?

主题
闭合JavaScript

TL;DR

JavaScript 中的闭包可以通过在另一个函数中定义一个函数来创建私有变量。内部函数可以访问外部函数的变量,但这些变量无法从外部函数外部访问。这允许您封装和保护变量,防止它们被直接访问或修改。

function createCounter() {
let count = 0; // private variable
return {
increment: function () {
count++;
return count;
},
decrement: function () {
count--;
return count;
},
getCount: function () {
return count;
},
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.getCount()); // 1
console.log(counter.count); // undefined

如何使用闭包创建私有变量?

了解闭包

闭包是 JavaScript 中的一个特性,其中内部函数可以访问外部(封闭)函数的变量。这包括:

  • 在外部函数作用域内声明的变量
  • 外部函数的参数
  • 来自全局作用域的变量

创建私有变量

要使用闭包创建私有变量,您可以定义一个返回包含方法的对象的函数。这些方法可以访问和修改私有变量,但变量本身无法从函数外部访问。

示例

这是一个详细的示例,说明如何使用闭包创建私有变量:

function createCounter() {
let count = 0; // private variable
return {
increment: function () {
count++;
return count;
},
decrement: function () {
count--;
return count;
},
getCount: function () {
return count;
},
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount()); // 1
console.log(counter.count); // undefined

解释

  1. 外部函数createCounter 是定义私有变量 count 的外部函数。
  2. 内部函数createCounter 返回的对象包含形成闭包的方法(incrementdecrementgetCount)。这些方法可以访问 count 变量。
  3. 封装count 变量无法直接从 createCounter 函数外部访问。它只能通过提供的方法进行访问和修改。

优点

  • 封装:私有变量有助于封装对象的状态和行为,防止意外干扰。
  • 数据完整性:通过限制对变量的直接访问,您可以确保它们仅通过受控方法进行修改。

延伸阅读

JavaScript 中 `==` 和 `===` 的区别是什么?