Skip to content
On This Page

面试题[JavaScript]

1. JavaScript 基础知识

1.1 数据类型

  1. JavaScript 中将数据类型分为基本数据类型和引用数据类型。
    • 基本数据类型:boolean、string、number、undefined、null、symbol、bigint。
    • 引用数据类型:array、object。
  2. 另外一种说法是,JavaScript 有八种基本数据类型,分为原始类型(Primitive Types)和引用类型(Reference Types):
    • 原始类型:boolean、string、number、undefined、null、symbol、bigint。
    • 引用类型:Object(包括普通对象、数组、函数等)
  3. 两者区别
    1. 存储区别
      • 原始类型存储在栈(stack)中,值直接保存在变量访问的位置,由于其大小固定且频繁使用,存储在栈中具有更高的性能。
      • 引用类型存储在堆(heap)中,占用空间较大且大小不固定,变量保存的是对实际对象的引用(即指针),这些引用存储在栈中。
    2. 赋值方式区别
      • 原始类型:复制的是值本身。例如,将一个 number 类型的变量赋值给另一个变量,两个变量互不影响。
      • 引用类型:复制的是引用(指针)。多个变量引用同一个对象时,一个变量的修改会影响其他变量。
  4. 类型检测
    • 使用 typeof 检查原始类型(例如:typeof 123 === "number")。
    • 使用 instanceof 检查引用类型(例如:[] instanceof Array === true)。
    • null 是一个特殊情况,typeof null 返回 "object",这是 JavaScript 早期实现中的一个 bug,但被保留了下来。
    • 每个对象都有一个 constructor 属性,指向创建该对象的构造函数。
  5. 类型转换
    • 自动类型转换:如字符串与数字相加时,数字会被转换为字符串。
    • 显式类型转换:使用 Number()、String()、Boolean() 等函数将值转换为指定类型。
  6. 堆和栈的区别
    • 栈:内存分配效率高,自动管理(由编译器分配和释放)。
    • 堆:内存分配灵活,但需要由开发者手动管理内存(通过垃圾回收机制)。
javascript
// 判断变量是否是数组
Array.isArray(arr)
arr instanceof Array
Object.prototype.toString.call(arr) === '[object Array]'

1.2 typeof 和 instanceof 的区别

typeof 和 instanceof 是 JavaScript 中用于检查变量类型的两个关键字,但它们的使用场景和功能有所不同。

  1. typeof

    typeof 操作符用于检测变量的类型,返回一个字符串,表示操作数的数据类型,常见的返回值如下:

    • 1)"undefined":表示值未定义。
    • 2)"boolean":表示布尔值。
    • 3)"number":表示数字。
    • 4)"string":表示字符串。
    • 5)"object":表示对象(包括 null,数组,对象字面量等)。
    • 6)"function":表示函数。
    • 7)"symbol":表示符号(ES6 引入)。
    • 8)"bigint":表示大整数(ES11 引入)。 示例如下:
    javascript
    console.log(typeof undefined); // "undefined"
    console.log(typeof true);      // "boolean"
    console.log(typeof 42);        // "number"
    console.log(typeof "hello");   // "string"
    console.log(typeof {});        // "object"
    console.log(typeof []);        // "object"
    console.log(typeof null);      // "object" (特殊情况)
    console.log(typeof function(){}); // "function"
    console.log(typeof Symbol());  // "symbol"
    console.log(typeof 10n);       // "bigint"
    
    console.log(typeof NaN); // 输出: "number"
  2. instanceof

    MDNinstanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

    instanceof 操作符用于检测某个对象是否是另一个对象(构造函数)的实例,返回一个布尔值,一些使用场景如下:

    • 1)用于检测复杂类型,比如对象、数组、函数等。
    • 2)检测某个对象是否继承自某个构造函数的原型链。 示例如下:
    javascript
    console.log({} instanceof Object);           // true
    console.log([] instanceof Array);            // true
    console.log(function(){} instanceof Function); // true
    console.log(new Date() instanceof Date);     // true
    
    function MyClass() {}
    let myInstance = new MyClass();
    console.log(myInstance instanceof MyClass);  // true
  3. 两者区别

    • 检测类型的范围:typeof 主要用于检测基本数据类型(如 number,string,boolean 等)以及函数、未定义类型和 symbol,而 instanceof 主要用于检测对象的具体类型,检查某个对象是否是某个构造函数的实例。
    • 检测基本类型和引用类型:typeof 对于基本类型非常有用,但对于复杂引用类型(如数组、对象字面量)只会返回 "object",而 instanceof 只能用于引用类型,不能用于检测基本数据类型。
    • 特殊情况:typeof null 返回 "object",这是一个 JavaScript 语言的历史遗留问题,而 instanceof 可以用来检测自定义对象的类型,通过检查原型链来确认实例关系。

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

在 JavaScript 中,typeof null 的结果是 "object"。这是一个历史遗留问题,而不是出于设计上的考虑。

在 JavaScript 中,所有事物都是对象,但有一个例外:null。然而,为了简化类型检测,typeof 操作符对于原始数据类型(如 numberstringboolean)返回的是它们的小写形式字符串(如 "number""string""boolean"),但对于复杂的数据类型(如对象和函数),它返回的是 "object"

原因可以追溯到 JavaScript 的早期版本,特别是 ECMAScript 3 和更早的版本。在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object"。(参考来源 (opens new window)

曾有一个 ECMAScript 的修复提案(通过选择性加入的方式),但被拒绝了 (opens new window)。该提案会导致 typeof null === "null"

1.4 字符串的常用方法

  1. 字符串的创建与转换
    • String():将给定值转换为字符串。
    • toString():调用对象上的方法,将其转换为字符串。
    • charAt(index):返回指定索引处的字符。
    • charCodeAt(index):返回指定索引处字符的Unicode编码。
    • fromCharCode():静态方法,接受一个或多个字符编码,将它们组合成一个字符串。
  2. 字符串的截取与分割
    • slice(beginIndex, endIndex):提取字符串的片段,并在新的字符串中返回被提取的部分,不包括endIndex处的字符。
    • substring(startIndex, endIndex):提取字符串中介于两个指定下标之间的字符。
    • substr(startIndex, length):从起始索引号提取指定长度的子字符串。
    • split(separator, limit):将一个字符串分割成字符串数组。
  3. 字符串的查找与匹配
    • indexOf(searchValue, fromIndex):返回在字符串中首次找到指定值的索引,如果未找到则返回-1。
    • lastIndexOf(searchValue, fromIndex):返回字符串中指定值最后出现的位置,如果未找到则返回-1。
    • includes(searchString, position):判断一个字符串是否包含在另一个字符串中,根据情况返回true或false。
    • startsWith(searchString, position):判断字符串是否以指定的子字符串开头。
    • endsWith(searchString, length):判断字符串是否以指定的子字符串结尾。
    • match(regexp):检索返回一个字符串匹配正则表达式的结果。
    • search(regexp):检索与正则表达式相匹配的值。
  4. 字符串的替换与连接
    • replace(searchValue, newValue):在字符串中查找匹配的子串并替换与正则表达式匹配的子串。
    • concat(string2, string3[, ..., stringN]):连接两个或多个字符串,并返回新的字符串。
    • padStart(targetLength, padString):在当前字符串的开始填充指定的字符串,直到达到指定的长度。
    • padEnd(targetLength, padString):在当前字符串的末尾填充指定的字符串,直到达到指定的长度。
  5. 字符串的比较与转换大小写
    • localeCompare(target):比较两个字符串,并返回基于字符串顺序的指示符。
    • toLowerCase():将字符串中的所有字符转换为小写。
    • toUpperCase():将字符串中的所有字符转换为大写。
  6. 其他常用方法
    • trim():去除字符串两端的空白字符。
    • trimStart():去除字符串开始处的空白字符。
    • trimEnd():去除字符串末尾的空白字符。
    • repeat(count):返回一个新字符串,该字符串包含被连接在一起的指定数量的字符串副本。

1.5 数组的常用方法

  1. 创建和初始化数组
    • Array() 构造函数:创建一个新的数组实例。
    • 数组字面量:使用方括号[]直接创建数组。
  2. 添加和删除元素
    • push(element1, ..., elementN):在数组的末尾添加一个或多个元素,并返回新的长度。
    • pop():移除数组的最后一个元素,并返回该元素的值。
    • shift():移除数组的第一个元素,并返回该元素的值。
    • unshift(element1, ..., elementN):在数组的开头添加一个或多个元素,并返回新的长度。
  3. 查找元素
    • indexOf(searchElement[, fromIndex]):返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。
    • includes(searchElement[, fromIndex]):判断一个数组是否包含一个指定的值,根据情况返回truefalse
    • lastIndexOf(searchElement[, fromIndex]):返回在数组中可以找到一个给定元素的最后一个索引,如果不存在,则返回-1。
    • find(callback[, thisArg]):返回数组中满足提供的测试函数的第一个元素的值。
    • findIndex(callback[, thisArg]):返回数组中满足提供的测试函数的第一个元素的索引。
  4. 迭代数组
    • forEach(callback[, thisArg]):为数组中的每个元素执行一次提供的函数。
    • map(callback[, thisArg]):创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。
    • filter(callback[, thisArg]):创建一个新数组,其包含通过所提供函数实现的测试的所有元素。
    • reduce(callback[, initialValue]):对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
    • some(callback[, thisArg]):测试数组中是不是至少有1个元素通过了被提供的函数测试。
    • every(callback[, thisArg]):测试数组的所有元素是否都通过了指定函数的测试。
  5. 修改数组
    • sort([compareFunction]):对数组的元素进行排序并返回数组。
    • reverse():颠倒数组中元素的顺序。
    • splice(start[, deleteCount[, item1[, item2[, ...]]]]):通过删除或替换现有元素或者添加新元素来修改数组,返回由被删除的元素组成的数组。
    • copyWithin(target, start[, end]):在数组内部,将指定位置的成员复制到另一个指定位置,并返回这个数组。
  6. 合并数组
    • concat(array2[, array3[, ...arrayN]]):合并两个或多个数组。此方法不会改变现有数组,而是返回一个新数组。
  7. 其他方法
    • join(separator):将一个数组的所有元素连接成一个字符串。
    • slice([begin[, end]]):返回一个新的数组对象,这一对象是一个由原数组中的从开始到结束(不包括结束)的一个浅拷贝。
    • toLocaleString():返回一个字符串,该字符串表示数组中的所有元素。元素将通过各自的toLocaleString方法进行转换。
    • toString():返回一个字符串,该字符串表示数组中的所有元素。
    • length:数组的长度属性,不是方法,但经常用于操作数组。
  8. 扩展知识
    • 在 JavaScript 中,map 和 forEach 方法中是不能直接使用 break 或 continue 语句来结束循环的。这是因为 map 和 forEach 是高阶函数,它们的设计初衷就是要遍历整个数组。

1.6 对象的常用方法

  1. 通用对象方法

    这些方法适用于所有JavaScript对象,因为它们是从Object原型继承的。

    • toString():返回对象的字符串表示。
    • toLocaleString():返回对象的本地化字符串表示。
    • valueOf():返回对象的原始值。
    • hasOwnProperty(prop):检查对象自身(而不是原型链)是否具有指定的属性。
    • isPrototypeOf(obj):检查一个对象是否存在于另一个对象的原型链上。
    • propertyIsEnumerable(prop):检查对象的某个属性是否可枚举。
    • getOwnPropertyDescriptor(prop):返回指定属性的属性描述符。
    • getOwnPropertyNames():返回一个数组,其包含对象自身的所有属性名(非可枚举属性除外)。
  2. 创建和修改对象

    • Object.create(proto[, propertiesObject]):创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
    • Object.defineProperty(obj, prop, descriptor):在对象上定义一个新属性,或修改一个对象的现有属性,并返回该对象。
    • Object.defineProperties(obj, props):在对象上定义多个新属性或修改现有属性,并返回该对象。
    • Object.assign(target, ...sources):将所有可枚举属性的值从一个或多个源对象复制到目标对象。返回目标对象。
  3. 遍历对象属性

    • for...in 循环:遍历对象的可枚举属性(包括原型链上的属性)。
    • Object.keys(obj):返回一个数组,其包含对象自身的所有可枚举属性名。
    • Object.values(obj):返回一个数组,其包含对象自身的所有可枚举属性值的集合。
    • Object.entries(obj):返回一个给定对象自身可枚举属性的键值对数组。
  4. 其他对象方法

    • Object.freeze(obj):阻止新属性添加到对象,并标记所有现有属性为不可配置。同时,阻止修改现有属性的值,以及阻止删除属性。
    • Object.seal(obj):阻止新属性添加到对象,并标记所有现有属性为不可配置。但属性的值仍然可以修改。
    • Object.isFrozen(obj):判断一个对象是否被冻结。
    • Object.isSealed(obj):判断一个对象是否被密封。
    • Object.isExtensible(obj):判断一个对象是否是可扩展的(即是否能够添加新的属性)。
    • Object.preventExtensions(obj):阻止新属性添加到对象。
  5. 特定内置对象的方法

    除了上述通用方法外,JavaScript还提供了一些特定内置对象的方法,如ArrayStringDateMath等对象的方法。这些方法在各自的对象文档中有详细描述,并用于执行与该对象相关的特定操作。

1.7 什么是可迭代对象

可迭代对象是一个允许你对其元素进行遍历的对象

在 JavaScript 中,可迭代对象(iterable)是一个实现了迭代协议的对象,允许你在对象上创建迭代器(iterator)。迭代器是一个实现了迭代器协议的对象,它保持了对集合中每个元素的跟踪,并记住遍历的位置。

可迭代对象:一个对象要实现可迭代协议,必须实现一个特殊的方法 @@iterator(),这个方法在对象上调用时会返回一个迭代器。在大多数情况下,你会通过调用对象的 Symbol.iterator 方法([Symbol.iterator]())来获取迭代器,但更常见的是使用扩展运算符(...)、for...of 循环或 Array.from() 等内置功能来自动处理迭代。

迭代器:迭代器必须实现一个迭代器协议,这个协议定义了一个对象的 next() 方法,该方法返回一个对象,该对象具有两个属性:valuedonevalue 属性表示迭代器返回的当前元素的值,done 属性是一个布尔值,当迭代器遍历完所有元素时,它为 true

常见的可迭代对象:

  • 数组(Array):数组是可迭代对象,因为它们实现了 @@iterator() 方法。
  • 字符串(String):字符串也是可迭代对象,可以按字符迭代。
  • Map 和 Set:ES6 引入的 MapSet 对象也是可迭代对象。
  • arguments 对象:函数中的 arguments 对象是可迭代的。
  • NodeList:在DOM操作中,NodeList 对象(例如,通过 document.querySelectorAll 获取的节点列表)通常是可迭代的(尽管这取决于具体的浏览器实现)。

使用示例:

javascript
// 数组是可迭代的
const arr = [1, 2, 3];
for (const value of arr) {
  console.log(value); // 输出 1, 2, 3
}

// 字符串是可迭代的
const str = 'hello';
for (const char of str) {
  console.log(char); // 输出 'h', 'e', 'l', 'l', 'o'
}

// 自定义可迭代对象
const iterable = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.length) {
          return { value: this[index++], done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
};
for (const value of iterable) {
  console.log(value); // 输出 'a', 'b', 'c'
}

1.8 什么是类数组对象

类数组对象(Array-like Object)指的是具有类似数组结构的对象

  1. 定义与特性

    • 定义:类数组是一个对象,它拥有length属性,并且其属性名通常为非负整数。这使得它在外观上类似于数组,但实际上并不是数组。
    • 特性
      • length属性:类数组对象通常具有一个length属性,用于表示对象中元素的个数。
      • 索引访问:可以通过索引值从类数组对象中获取元素,就像访问数组元素一样。
      • 无数组方法:尽管类数组对象具有length属性和可以通过索引访问元素,但它们并不具备数组原型上的方法(如push、pop、forEach等)。因此,无法直接使用这些方法对类数组对象进行操作。
  2. 常见类数组对象

    • arguments对象:在JavaScript函数内部自动创建的对象,用于存储函数调用时传递的参数。它是一个类数组对象,可以通过索引访问参数,并具有length属性。
    • HTMLCollection和NodeList:DOM操作返回的一些对象,如document.getElementsByTagName()返回的对象集合,也是类数组对象。
    • 字符串:在JavaScript中,字符串也可以被视为类数组对象,因为可以通过索引访问每个字符,并具有length属性。但需要注意的是,字符串本身并不是数组。
  3. 类数组与数组的区别

    • 原型链:类数组的原型关系与数组不同,因此无法直接调用数组的方法。而数组则继承自Array.prototype,可以使用数组的所有方法。
    • 类型:类数组本质上是对象,而数组则是一种特殊的数据结构。
  4. 类数组转换为数组

    由于类数组对象无法直接使用数组的方法,因此有时需要将其转换为真正的数组。在JavaScript中,可以使用以下方法将类数组对象转换为数组:

    • Array.from()方法:这是一个静态方法,它接受一个类数组对象或可迭代对象,并返回一个新的数组实例。

      javascript
      const arrayLike = {0: 'a', 1: 'b', length: 2};
      const array = Array.from(arrayLike);
      console.log(array); // ['a', 'b']
    • 扩展运算符(...:在ES6中,扩展运算符可以用于将类数组对象或可迭代对象展开为数组元素。

      javascript
      const arrayLike = {0: 'a', 1: 'b', length: 2};
      const array = [...arrayLike];
      console.log(array); // ['a', 'b']
    • Array.prototype.slice.call()方法:在ES6之前,这种方法常用于将类数组对象转换为数组。它利用了Array.prototype.slice方法,该方法可以接受一个类数组对象作为上下文(this值),并返回一个新的数组。

      javascript
      const arrayLike = {0: 'a', 1: 'b', length: 2};
      const array = Array.prototype.slice.call(arrayLike);
      console.log(array); // ['a', 'b']

1.9 执行上下文、执行栈

在 JavaScript 中,执行上下文(Execution Context)和执行栈(Call Stack)是理解代码执行流程的两个核心概念。让我们详细探讨一下它们。

  1. 执行上下文(Execution Context)

    执行上下文是 JavaScript 代码执行时的一个抽象环境。它包含了三个重要的部分:

    1. 变量对象(Variable Object, VO):
      • 在 ES5 中,变量对象包含了函数声明和变量声明。
      • 在 ES6 及以后,引入了 letconst,这些声明不再存储在变量对象中,而是有其特定的行为(块级作用域)。但为简化理解,可以认为变量对象存储了所有可访问的变量和函数。
    2. 作用域链(Scope Chain):
      • 作用域链是一个对象列表,它决定了变量和函数在何处查找它们的值。它基于嵌套函数的层级关系形成。
    3. this 值:
      • this 值在函数调用时确定,并且与函数的调用方式有关。

    执行上下文分为两类:

    • 全局执行上下文(Global Execution Context):
      • 在脚本开始执行时创建,并且在整个脚本的生命周期内都存在。
      • 全局对象(在浏览器中为 window,在 Node.js 中为 global)是全局执行上下文的变量对象。
    • 函数执行上下文(Function Execution Context):
      • 每次调用函数时都会创建一个新的函数执行上下文。
  2. 执行栈(Call Stack)

    执行栈(也称为调用栈)是一种后进先出(LIFO)的数据结构,用于管理执行上下文。JavaScript 引擎使用执行栈来跟踪当前正在执行的函数。

    执行栈的工作流程

    1. 全局执行上下文:
      • 当 JavaScript 引擎开始执行脚本时,它会首先创建一个全局执行上下文并将其推入执行栈。
    2. 函数调用:
      • 当一个函数被调用时,JavaScript 引擎会为该函数创建一个新的执行上下文,并将其推入执行栈的顶部。
      • 函数执行完毕后,其对应的执行上下文会从执行栈中弹出。
    3. 递归调用:
      • 如果函数内再次调用函数(包括递归调用),新的执行上下文会被创建并推入执行栈。
      • 当所有调用都返回时,这些执行上下文会依次从栈中弹出。
    4. 栈溢出(Stack Overflow):
      • 如果执行栈中的执行上下文过多(例如,递归调用没有正确的终止条件),JavaScript 引擎会抛出栈溢出错误。
    5. 栈清空:
      • 当执行栈为空时,JavaScript 引擎认为当前脚本执行完毕。
  3. 示例

    javascript
    function firstFunction() {
      console.log('First function');
      secondFunction();
    }
    
    function secondFunction() {
      console.log('Second function');
      thirdFunction();
    }
    
    function thirdFunction() {
      console.log('Third function');
    }
    
    firstFunction();

    执行流程:

    1. 创建全局执行上下文,并将其推入执行栈。
    2. 调用 firstFunction,创建 firstFunction 的执行上下文,并将其推入执行栈。
    3. firstFunction 调用 secondFunction,创建 secondFunction 的执行上下文,并将其推入执行栈。
    4. secondFunction 调用 thirdFunction,创建 thirdFunction 的执行上下文,并将其推入执行栈。
    5. thirdFunction 执行完毕,其执行上下文从执行栈中弹出。
    6. secondFunction 执行完毕,其执行上下文从执行栈中弹出。
    7. firstFunction 执行完毕,其执行上下文从执行栈中弹出。
    8. 全局执行上下文是最后一个从执行栈中弹出的(但通常不会显式弹出,因为它会一直存在)。

理解执行上下文和执行栈对于调试和优化 JavaScript 代码非常重要。通过掌握这些概念,你可以更好地理解和预测代码的行为。

1.10 闭包和作用域

闭包定义

根据 JavaScript 中的词法作用域规则,内部函数总是可以访问其外部函数中声明的变量。当内部函数被返回到外部函数之外时,即使外部函数执行结束了,但是内部函数引用了外部函数的变量,这些变量仍然会被保存在内存中。这个现象称为闭包。

  1. 相关概念

    • JavaScript 中常见的作用域包括全局作用域、函数作用域、块级作用域。
    • JavaScript 中自由变量的查找是在函数定义的地方,向上级作用域查找,不是在执行的地方。
  2. 作用域

    作用域,其实就是一个变量或函数在代码中的可访问范围。在 JavaScript 中,主要有两种作用域:全局作用域和局部作用域。全局作用域的变量在整个脚本中都可访问,而局部作用域的变量只能在特定的代码块、函数内使用。

    • 全局作用域:定义在所有函数体以及其他代码块之外的变量,称为全局变量。它们在脚本的任何地方都是可访问的。
    • 局部作用域:局部变量定义在函数内或代码块内(如 iffor 块),它们只能在函数内或代码块内访问。局部作用域又可细分为函数作用域和块作用域。
    • 函数作用域:只在函数内部可见的变量,这种作用域在早期的 JavaScript 中非常常见。
    • 块作用域:ES6 引入的 letconst 关键字,使得可以在块级代码(类似 {})内部定义变量,即所谓的块作用域。
  3. 作用域扩展

    1. 提升(Hoisting):发生在变量声明和函数声明上,旨在解释为什么即使在声明之前使用变量也不会报错。变量提升是指不论变量在代码中的位置,它们会被提升到代码的顶部进行声明,而函数提升不仅是声明,它会把整个函数提升到顶部。
    2. 作用域链:当查找一个变量时,JavaScript 引擎会首先在当前作用域中查找,如果未找到,它会沿着作用域链向上查找,直到全局作用域。如果还未找到,则返回 undefined
    3. 闭包(Closure):函数内定义的函数能够访问外部函数的变量,这就是闭包。它是一种特殊的作用域情况,能让我们创建私有变量和函数。
    4. 立即执行函数表达式(IIFE):一种常见的技术,通过定义和立即调用一个匿名函数,创建一个新的作用域,从而保护内部变量不受外部干扰。同时它也会避免全局变量污染的问题。
    5. 严格模式(Strict Mode):严格模式扩展了 ECMAScript 3 的语法和语义范围,使 JavaScript 在更严格的条件下执行,有助于更好地调试和提升代码的安全性。
    6. 模块化(Modules):现代 JavaScript 越来越依赖模块化,通过 importexport 关键字,可以在不同模块之间共享代码,避免作用域污染,并且更好地组织代码。
  4. 闭包的应用场景

    • 闭包是作用域应用的特殊场景。常见的闭包使用有两种场景:一种是函数作为参数被传递;一种是函数作为返回值被返回。
  5. 怎么看待闭包的副作用?

    1. 闭包的正面副作用
      1. 数据封装与状态维护
        • 闭包可以用来封装数据,从而避免全局变量的污染。
        • 它们可以帮助维护内部状态,这对于某些特定的算法和数据结构(如私有变量、计数器、工厂函数等)特别有用。
      2. 创建高阶函数
        • 闭包使得函数可以作为参数传递给其他函数,并可以返回另一个函数。
        • 这种能力允许我们创建更具灵活性和复用性的代码。
      3. 回调函数与异步编程
        • 在处理异步操作时,闭包常用于传递上下文数据给回调函数。
        • 这对于在JavaScript等单线程语言中进行异步编程特别重要。
    2. 闭包的负面副作用
      1. 内存泄漏
        • 如果闭包持续引用其外部作用域中的变量,而这些变量不再被需要,可能会导致内存无法被垃圾回收器回收,进而造成内存泄漏。
        • 这在长时间运行的服务器或前端应用中尤为关键。
      2. 难以调试
        • 闭包可能导致代码的隐藏依赖,使得理解代码的上下文变得更加困难。
        • 特别是在大型或复杂的代码库中,闭包的使用可能会使得调试过程变得更加繁琐。
      3. 性能问题
        • 如果闭包被大量使用,尤其是在密集计算或高频率调用的场景下,可能会导致性能下降。
        • 这主要是因为闭包创建时会捕捉其外部作用域,这可能会增加内存开销和运行时复杂度。
    3. 如何合理使用闭包
      1. 明确用途
        • 在使用闭包之前,确保明确其用途和目的。
        • 避免在没有必要的情况下使用闭包。
      2. 谨慎管理内存
        • 时刻注意闭包对外部变量的引用,确保不再需要的变量可以被垃圾回收。
        • 尤其是在使用闭包进行事件监听或定时器时,要记得及时移除它们以避免内存泄漏。
      3. 代码文档化
        • 对于复杂的闭包逻辑,要确保编写详细的文档和注释。
        • 这有助于其他开发者(或未来的你)更好地理解代码的意图和行为。
      4. 测试和性能监控
        • 对使用闭包的代码进行充分的测试,确保它们按预期工作。
        • 使用性能监控工具来评估闭包对应用性能的影响,并在必要时进行优化。

    综上所述,闭包是一个强大的工具,但也需要谨慎使用。通过明确其用途、谨慎管理内存、编写文档以及进行测试和性能监控,我们可以最大化地利用闭包的优点,同时最小化其潜在的副作用。

1.11 变量提升

在 JavaScript 中,变量提升(Hoisting)是指在变量声明和函数声明被提升到其作用域的顶部的行为。这意味着,无论这些声明在代码中的哪个位置,它们都会被提升到作用域的开头,但需要注意的是,只有声明会被提升,赋值操作不会。

这里有几点需要注意:

  1. 变量声明提升:使用 var 关键字声明的变量会被提升到作用域的顶部,但赋值操作不会提升

    javascript
    console.log(a); // 输出: undefined
    var a = 2;

    在上述代码中,var a 被提升到了作用域的顶部,但 a = 2 没有被提升,所以在 console.log(a) 时,aundefined

  2. 函数声明提升:函数声明也会被提升到作用域的顶部,并且整个函数定义(包括函数体和参数)都会被提升

    javascript
    console.log(foo()); // 输出: "Hello, world!"
    function foo() {
      return "Hello, world!";
    }

    在上面的例子中,function foo() {...} 被提升到了作用域的顶部。

  3. 函数表达式提升函数表达式不同于函数声明,它们不会被提升。如果你尝试在函数表达式声明之前调用它,将会导致 ReferenceError

    javascript
    console.log(bar()); // ReferenceError: bar is not defined
    var bar = function() {
      return "Hello, world!";
    };
    
    console.log(bar); // 输出: undefined
    var bar = function() {
      return "Hello, world!";
    };

    在上面的例子中,var bar 被提升,但 bar 被初始化为函数表达式的过程没有提升,所以在 console.log(bar()) 时,bar 还未被定义。

  4. 使用 letconst 的声明letconst 声明的变量不会被提升到作用域的顶部,而是被“块级作用域”(block scope)所限制,并且存在“暂时性死区”(Temporal Dead Zone,TDZ)。在变量声明之前的区域被称为 TDZ,访问这些变量会导致 ReferenceError

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

    在上述代码中,尝试在 let b 声明之前访问 b 会导致 ReferenceError

理解变量提升对于编写健壮的 JavaScript 代码非常重要,因为它可以帮助你避免一些常见的陷阱和错误。

1.12 对 this 的理解

JavaScript_This

1.13 原型和原型链

JavaScript原型和原型链

1.14 new 操作符做了什么

JavaScript 中,new 操作符用于创建一个给定构造函数的实例对象。

javascript
// 构造函数返回值是基本数据类型
function Test(name) {
  this.name = name;
  return 1;
}
const t = new Test('xxx');
console.log(t.name); // 'xxx'

// 构造函数返回值是对象
function Test(name) {
  this.name = name;
  console.log(this);
  return { age: 26 };
}
const t = new Test('xxx'); // Test { name: 'xxx' }
console.log(t); // { age: 26 }
console.log(t.name); // undefined

流程:

  • 【1】创建一个新的对象 obj
  • 【2】将对象与构造函数通过原型链连接起来;
  • 【3】将构造函数中的 this 绑定到新建的对象 obj 上;
  • 【4】根据构造函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理(需要特别注意一下,上面有对比例子)。
javascript
// 手写 new 的执行过程:
function mynew(Func, ...args) {
  // 1.创建一个新对象
  const obj = {};
  // 2.新对象原型指向构造函数原型对象
  obj.__proto__ = Func.prototype;
  // 3.将构造函数的this指向新对象
  let result = Func.apply(obj, args);
  // 4.根据返回值判断
  return result instanceof Object ? result : obj;
}

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.say = function () {
  console.log(this.name)
}
let p = mynew(Person, "huihui", 123)
console.log(p) // Person { name: "huihui", age: 123 }
p.say() // huihui

1.15 实现继承

JavaScript实现继承

1.16 事件模型

事件冒泡(Event Bubbling)、事件捕获(Event Capturing)、事件委托(Event Delegation)。

JavaScript 中的事件处理模型主要分为两种:事件冒泡和事件捕获。简单来说,事件冒泡是事件从最深的嵌套元素开始逐级向上传播,直到传到顶层元素;事件捕获则相反,事件从顶层元素开始逐级向下传递,直到触发目标元素。默认的事件传递机制是事件冒泡

  1. 事件冒泡(Bubbling):事件先从目标元素开始,一层一层向上传播到根元素。

  2. 事件捕获(Capturing):事件从根元素一层一层向下传播到目标元素。

  3. 事件委托:事件冒泡机制允许我们使用事件委托(Event Delegation)技术,以减少事件监听器的数量,从而提高性能。例如,把所有子元素的点击事件委托给父元素处理:

javascript
parentElement.addEventListener('click', function(event) {
  if(event.target.matches('.childClass')) {
    // 子元素被点击的逻辑处理
  }
});
  1. 事件流阶段

    • 捕获阶段:从 document 根对象往目标元素传播。
    • 目标阶段:事件到达目标元素处。
    • 冒泡阶段:从目标元素往 document 根对象传播。

1.17 事件循环

JavaScript事件循环机制

1.18 异步编程

在 JavaScript 中,异步编程是一项非常核心且重要的技能,它允许程序在等待某些操作(如网络请求、文件读取或定时器等)完成时,继续执行其他任务,从而提高应用程序的响应性和性能。以下是 JavaScript 中处理异步编程的几种主要方法:

  1. 回调函数(Callbacks)

    回调函数是最基本的异步编程技术。你可以将一个函数作为参数传递给另一个函数,并在异步操作完成时调用这个回调函数。

    javascript
    function fetchData(callback) {
      setTimeout(() => {
        const data = "Hello, World!";
        callback(data);
      }, 1000);
    }
    
    fetchData((data) => {
      console.log(data);
    });
  2. Promise

    Promise 是一种更强大和灵活的异步编程模式,用于处理更复杂的异步操作链。一个 Promise 对象表示一个最终可能完成(并得到一个结果值)或失败(并得到一个原因)的异步操作。

    javascript
    function fetchData() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          const success = true;
          if (success) {
            resolve("Hello, World!");
          } else {
            reject("Error fetching data");
          }
        }, 1000);
      });
    }
    
    fetchData()
      .then((data) => {
        console.log(data);
      })
      .catch((error) => {
        console.error(error);
      });
  3. async/await

    asyncawait 是基于 Promise 的语法糖,使得异步代码看起来更像同步代码,增强了可读性和可维护性。async 函数返回一个 Promise,而 await 表达式用于等待 Promise 完成,并返回 Promise 的结果。

    javascript
    function fetchData() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          const success = true;
          if (success) {
            resolve("Hello, World!");
          } else {
            reject("Error fetching data");
          }
        }, 1000);
      });
    }
    
    async function getData() {
      try {
        const data = await fetchData();
        console.log(data);
      } catch (error) {
        console.error(error);
      }
    }
    
    getData();
  4. 事件循环(Event Loop)

    JavaScript事件循环机制

  5. Generator 函数

    Generator 函数提供了一种更为复杂的方式来处理异步操作,但相对于 async/await,它们更不常用。Generator 函数可以暂停和恢复执行,使得可以在等待异步操作完成时返回控制权。

    javascript
    function* fetchDataGenerator() {
      const promise = new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve("Hello, World!");
        }, 1000);
      });
      const data = yield promise;
      console.log(data);
    }
    
    const gen = fetchDataGenerator();
    
    gen.next().value.then((data) => {
      gen.next(data);
    });

1.19 箭头函数

箭头函数(Arrow Functions)是ES6(ECMAScript 2015)引入的一种更简洁的函数写法。相比于传统的函数表达式,箭头函数提供了一种更简短的语法,并且不绑定自己的thisargumentssuper,或new.target。这些特性使得箭头函数在某些场景下更加适用和简洁。

  1. 基本语法

    javascript
    // 箭头函数的基本语法如下:
    const functionName = (parameters) => {
      // function body
    };
    
    // 如果函数体只有一条语句,并且需要返回结果,可以省略花括号和大括号内的return关键字:
    const functionName = (parameters) => expression;
    
    // 如果参数只有一个,可以省略参数的小括号:
    const functionName = parameter => expression;
    
    // 返回对象字面量
    // 由于箭头函数中的花括号会被解析为函数体的开始和结束,所以在返回对象字面量时需要用小括号包裹起来:
    const createPerson = (name, age) => ({ name: name, age: age });
    console.log(createPerson("Alice", 30));  // 输出: { name: "Alice", age: 30 }
  2. 特性

    • 不绑定自己的this:箭头函数不绑定自己的this,它会捕获其所在上下文的this值,作为自己的this值,这使得在回调函数中处理this变得更加容易:

      javascript
      function Person() {
        this.age = 0;
      
        setInterval(() => {
          this.age++; // 这里的this指向Person实例
          console.log(this.age);
        }, 1000);
      }
      
      const p = new Person();
    • 没有arguments对象:箭头函数不提供arguments对象。如果需要访问函数的参数列表,可以使用剩余参数(...args)代替:

      javascript
      const showArguments = (...args) => {
        console.log(args);
      };
      
      showArguments(1, 2, 3);  // 输出: [1, 2, 3]
    • 不能用作构造函数:箭头函数不能使用new操作符,否则会抛出一个错误:

      javascript
      const Foo = () => {};
      const bar = new Foo();  // TypeError: Foo is not a constructor
    • 没有prototype属性:由于箭头函数不能用作构造函数,它们也没有prototype属性:

      javascript
      const Foo = () => {};
      console.log(Foo.prototype);  // 输出: undefined
    • 不能改变this的绑定call()apply()bind()等方法对于箭头函数来说不起作用,因为箭头函数的this是在定义时就确定好的:

      javascript
      const func = () => {
        return this;
      };
      
      const boundFunc = func.bind({ a: 42 });
      console.log(boundFunc());  // 输出: 取决于func被定义时的上下文,而不是{ a: 42 }
  3. 总结

    箭头函数提供了一种更简洁、更优雅的函数定义方式,特别适用于回调函数和需要保持this上下文不变的场景。然而,由于它们的一些限制(如不能使用new、没有arguments对象、this不可变等),在某些情况下仍需使用传统的函数表达式。

2. JavaScript 拓展知识

2.1 ECMA 标准从提案到发布有几个阶段?哪个阶段是具有里程碑意义的

ECMA标准从提案到发布经历了以下几个阶段:

  1. Stage 0 - Strawman(草案):这个阶段是最初的提案阶段,通常由个人或小组提出,并还没有经过正式的标准化流程。提案可能只是一个想法或初步的概念。
  2. Stage 1 - Proposal(提案):在这个阶段,提案开始进入正式的标准化流程。提案需要详细说明其功能、语法和语义,并且需要提供示例代码和使用案例。
  3. Stage 2 - Draft(草稿):在这个阶段,提案转化为一份详细的草稿,其中包含了具体的语法规范和语义定义。草稿需要经过审查和讨论,并且需要有多个独立实现的证明。
  4. Stage 3 - Candidate(候选):在这个阶段,提案已经足够成熟,可以被视为候选标准。这意味着提案已经通过了实际应用并经过广泛的测试和实现。
  5. Stage 4 - Finished(完成):在这个阶段,提案被接受为最终的标准,已经准备好发布。提案的规范细节已经完善,并且已经有多个独立实现通过了所有测试。

这些阶段代表了ECMA标准的不同发展阶段,其中最具里程碑意义的是Stage 4 - Finished(完成)阶段。在这个阶段,提案被接受为最终的标准,意味着它已经经过了广泛的实现、测试和审查,并被认为是稳定和可靠的。完成阶段的标准可以被广泛采用和应用于实际的编程环境中。

2.2 JavaScript 中的严格模式

"use strict" 是 JavaScript 中的一个指令,用于启用严格模式。

  1. "use strict" 是什么

    "use strict" 是一个字符串声明,放在脚本或函数的开头,用来指定代码应该在严格模式下执行。

    javascript
    "use strict";
    // 后面的代码会在严格模式下执行
  2. 严格模式的主要作用

    严格模式主要是为了捕获一些常见的编程错误,并防止使用一些可能在未来版本中定义的语法。

  3. 使用 "use strict" 的主要区别

    • 1)变量必须声明后再使用

      javascript
      "use strict";
      x = 3.14; // 错误:x 未定义
    • 2)禁止使用 with 语句

      javascript
      "use strict";
      with (Math){x = cos(2)}; // 语法错误
    • 3) 创建 eval 作用域

      在严格模式下,eval() 中的代码会在自己的作用域中执行,而不是在当前作用域中。

    • 4)禁止 this 关键字指向全局对象

      javascript
      "use strict";
      function f(){
          return this;
      }
      f(); // 返回 undefined,而不是全局对象
    • 5)函数参数不能有重名

      javascript
      "use strict";
      function sum(a, a, c){ // 语法错误
          return a + a + c;
      }
    • 6)禁止八进制数字语法

      javascript
      "use strict";
      var sum = 015 + 197 + 142; // 语法错误
    • 7)禁止对只读属性赋值

      javascript
      "use strict";
      var obj = {};
      Object.defineProperty(obj, "x", { value: 0, writable: false });
      obj.x = 3.14; // 抛出错误
    • 8)禁止删除不可删除的属性

      javascript
      "use strict";
      delete Object.prototype; // 抛出错误
  4. 使用 "use strict" 的好处

    • 消除 JavaScript 语法的一些不合理、不严谨之处,减少一些怪异行为。
    • 消除代码运行的一些不安全之处,保证代码运行的安全。
    • 提高编译器效率,增加运行速度。
    • 为未来新版本的 JavaScript 做好铺垫。
  5. 拓展

    在类和模块中,严格模式会自动启用。使用 "use strict" 可以帮助我们写出更加规范、安全的代码,并且可以在开发阶段就发现一些潜在的问题。在现代 JavaScript 开发中,特别是使用 ES6 模块或类时,严格模式已经成为默认行为。

2.3 JavaScript 中的包装类型

JavaScript 中,原始值没有方法或属性,但为了能够使用方法和属性,JavaScript 提供了包装类型,使得原始值可以像对象一样被操作。

  1. 包装类型的概念

    包装类型是 JavaScript 中的一种机制,它允许原始值临时拥有对象的属性和方法。JavaScript 提供了三个包装类型:

    • String
    • Number
    • Boolean

    这些包装类型分别对应于原始值 string、number 和 boolean。

  2. 包装类型的行为

    当你试图访问一个原始值的属性或方法时,JavaScript 会在后台自动创建一个对应的包装对象,然后在该对象上调用方法或访问属性。一旦操作完成,这个临时创建的对象就会被销毁,示例如下:

    javascript
    let str = "hello";
    console.log(str.toUpperCase()); // "HELLO"
    
    let num = 42;
    console.log(num.toFixed(2)); // "42.00"
    
    let bool = true;
    console.log(bool.toString()); // "true"
  3. 原始值和包装对象的区别

    虽然包装类型使得原始值可以像对象一样操作,但它们本质上是不同的,示例如下:

    javascript
    let strPrimitive = "hello";
    let strObject = new String("hello");
    
    console.log(typeof strPrimitive); // "string"
    console.log(typeof strObject);    // "object"
    
    console.log(strPrimitive === strObject); // false
    console.log(strPrimitive == strObject);  // true

    这个例子中,strPrimitive 是一个原始值,而 strObject 是一个 String 对象。它们在类型上是不同的,严格相等(===)比较时会返回 false,但宽松相等(==)比较时会返回 true,因为 strObject 会被转换为原始值进行比较。

2.4 不创建新变量的前提下,如何交换两个变量的位置

  • 解构赋值:适用于数组、对象和其他可迭代的数据结构。
javascript
let a = 10;
let b = 20;
[a, b] = [b, a];
  • 位运算符 -- 位异或(^)运算符:只适用于数字类型的变量。
javascript
let a = 1;
let b = 2;
a = a ^ b;
b = a ^ b;
a = a ^ b;
  • 加法和减法、乘法和除法:只适用于数字类型的变量。
javascript
let a = 1;
let b = 2;

a = a + b;
b = a - b;
a = a - b;

a = a * b;
b = a / b;
a = a / b;

2.5 JavaScript 文件相互引用有什么问题

可能导致代码执行错误或者无法正常工作。下面是一些常见的问题及其解决方法:

  1. 循环依赖(Circular Dependency): 循环依赖指的是两个或多个模块相互依赖,直接或间接地引用对方,导致模块无法正确加载。解决循环依赖问题的方法包括:
    • 重构代码结构,将共享的逻辑抽离到单独的模块中,避免直接相互引用。
    • 使用异步加载模块的方式,如 import() 动态导入语法,可以延迟加载模块,避免循环依赖问题。
  2. 加载顺序错误: 当 JavaScript 文件相互引用时,确保它们之间的加载顺序是正确的非常重要。如果加载顺序错误,可能会导致某些模块在使用时还未被加载,从而出现错误。解决加载顺序错误的方法包括:
    • 明确定义模块之间的依赖关系,确保先加载依赖的模块,再加载依赖它们的模块。
    • 使用模块打包工具(如 Webpack、Rollup 等)来管理模块之间的依赖关系和加载顺序,确保打包后的文件能正确加载所有模块。
  3. 使用命名导出和默认导出: 在模块间引用时,可以使用命名导出和默认导出来更清晰地定义模块之间的关系。通过明确导出和导入需要的函数、变量等,可以减少不必要的引用问题。
  4. 使用事件订阅/发布模式: 如果存在模块之间需要通信的情况,可以考虑使用事件订阅/发布模式(Event Emitter),模块之间通过事件进行通信,避免直接引用对方。

总的来说,避免 JavaScript 文件相互引用时的常见问题,需要注意模块之间的依赖关系、加载顺序以及合理设计模块之间的通信机制。通过合理的代码组织和模块化设计,可以有效减少相互引用带来的问题,并确保代码的可维护性和可扩展性。

2.6 JavaScript 脚本延迟加载的方式

在JavaScript中,脚本延迟加载的方式主要有以下几种:

  1. async 属性:

    async 属性用于让脚本尽可能地异步加载。它不会阻塞HTML解析,脚本一旦下载完成就立即执行。但是需要注意,如果有多个 async 脚本,它们的执行顺序是不确定的。通常用于独立性较高的脚本,比如第三方的统计代码、广告代码等。

    html
    <script src="example.js" async></script>
  2. defer 属性:

    defer 属性同样用于脚本延迟加载,但是它保证了所有 defer 脚本会按照在文档中出现的顺序执行,并且是在HTML解析完成后才执行。因此,适合于依赖于HTML结构的脚本,比如需要操作DOM的脚本。

    html
    <script src="example.js" defer></script>
  3. 动态创建脚本元素:

    通过JavaScript动态创建 <script> 标签并插入到文档中。这种方式可以较为灵活地控制脚本的加载和执行时机。常在需要某些条件触发时才加载的场景中使用。

    javascript
    var script = document.createElement('script');
    script.src = 'example.js';
    document.head.appendChild(script);
  4. 使用模块化加载工具:

    现在有很多前端模块化工具,比如RequireJS,Webpack的动态加载等等。这些工具提供了更为强大的依赖管理和延迟加载功能。适用于大型项目中,解决代码拆分和按需加载的问题。

    javascript
    // 以Webpack的动态加载为例
    import('example.js').then(module => {
      // 使用加载的模块
    });

以上就是脚本延迟加载的几种主要方式,它们适用于不同的需求和场景。在实际开发中,我们可以根据项目的具体情况选择合适的延迟加载方案,这样不仅可以提高页面的加载速度,还可以提升用户体验。

2.7 JavaScript 数值精度问题

在 JavaScript中,由于浮点数的表示方式(基于IEEE 754 标准),我们有时会遇到数值精度问题。这些问题主要源于二进制浮点数的表示限制,导致无法精确表示所有的十进制小数

以下是一些解决 JavaScript 数值精度问题的方法:

  1. toFixed() 方法
    • 当你需要将浮点数转换为字符串并保留一定的小数位数时,可以使用 toFixed() 方法。但请注意,toFixed() 返回的是一个字符串,而不是一个数字。
  2. Math.round(), Math.floor(), Math.ceil()
    • 这些函数可以帮助你四舍五入、向下取整或向上取整到最接近的整数。
  3. Number.EPSILON
    • 设置一个误差范围,通常称为“机器精度”。
  4. 使用第三方库
    • 有些第三方库,如 math.jsdecimal.jsbignumber.js,提供了高精度的十进制数运算。这些库可以处理比JavaScript内置Number类型更大或更精确的数值。

在处理金融或需要高精度计算的场景时,使用第三方库(如 math.jsdecimal.jsbignumber.js)通常是一个好主意,因为它们提供了比JavaScript内置Number类型更高的精度和可靠性。

2.8 事件 level0、level1

在HTML中,level0level1并不是标准的事件处理术语。然而,我们可以讨论HTML中的事件处理机制,以及如何在不同层级(如元素层级)上添加事件。

  1. 基本的 HTML 事件处理(类似于 “level0”)

    在HTML中,你可以直接在元素标签内使用on<event>属性来添加事件监听器。例如:

    html
    <button onclick="alert('Button clicked!')">Click Me</button>
  2. 使用 JavaScript 添加事件监听器(类似于 “level1” 或更高级别)

    为了更灵活和可维护的代码,通常建议使用JavaScript来添加事件监听器。这可以通过addEventListener方法来实现。

    html
    <!DOCTYPE html>  
    <html lang="en">  
    <head>  
      <meta charset="UTF-8">  
      <meta name="viewport" content="width=device-width, initial-scale=1.0">  
      <title>Event Handling Example</title>  
    </head>  
    <body>  
      <button id="myButton">Click Me</button>  
      
      <script>  
        // 获取按钮元素  
        var button = document.getElementById('myButton');  
        // 添加点击事件监听器  
        button.addEventListener('click', function() {  
          alert('Button clicked via addEventListener!');  
        });  
      </script>  
    </body>  
    </html>
  3. 层级概念

    在 HTML 和 JavaScript 中,事件可以在不同的层级上被捕获和处理。这通常涉及事件冒泡(event bubbling)和事件捕获(event capturing)的概念。

    • 事件冒泡:事件从触发它的最内层元素(目标元素)开始,然后向外层元素传播,直到到达文档的根元素(通常是document对象)。
    • 事件捕获:与事件冒泡相反,事件从文档的根元素开始,然后向目标元素传播,直到到达目标元素本身。

    在大多数情况下,你会使用事件冒泡,因为它更符合直觉,并且更容易管理。但是,在某些情况下,你可能需要使用事件捕获来阻止事件冒泡或实现特定的交互逻辑。

  4. level0、level1 对比

    虽然 “level0” 和 “level1” 不是 HTML 或 JavaScript 中的标准术语,但你可以理解它们为不同级别的事件处理方式。基本的 HTML 事件处理(如onclick 属性)可以看作是较低级别的方式,而使用 JavaScript 的 addEventListener 方法则提供了更高级别、更灵活的事件处理机制。

  5. 场景

    在HTML和JavaScript中,如果一个元素同时被添加了两种事件处理器(例如,通过HTML属性如onclick和通过JavaScript的addEventListener方法),那么事件的触发顺序通常遵循以下规则:

    • HTML属性事件处理器(如onclick:这些事件处理器是在元素标记中直接定义的,它们通常会在JavaScript添加的事件监听器之前被触发。这是因为这些事件处理器是在元素解析和构建DOM时就已经存在的,而JavaScript添加的事件监听器则是在页面加载和JavaScript执行时才被添加到DOM中的。
    • JavaScript添加的事件监听器(addEventListener:这些事件监听器是通过JavaScript代码动态添加到元素上的。它们会在HTML属性事件处理器之后被触发。这是因为addEventListener方法允许更精细的控制,包括指定事件是否在捕获阶段触发,以及添加多个事件监听器而不会相互覆盖。

    然而,需要注意的是,这种触发顺序并不是绝对的,因为它可能受到多种因素的影响,包括浏览器的实现细节、JavaScript代码的执行时机和顺序等。在某些情况下,如果JavaScript代码在页面加载的非常早期就执行了(例如,在<head>标签中或通过DOMContentLoaded事件),并且添加了事件监听器,那么这些监听器可能会在页面完全解析和构建DOM之前就已经存在,从而可能改变事件的触发顺序。

    此外,如果使用了事件委托(即在父元素上监听事件,然后根据事件的目标元素来执行相应的操作),那么事件的触发顺序也会受到影响,因为事件是在父元素上捕获的,然后根据需要冒泡到目标元素或停止传播。

    总的来说,为了确保事件的正确处理和触发顺序,建议避免在同一个元素上同时使用HTML属性事件处理器和JavaScript添加的事件监听器。相反,应该选择其中一种方法,并保持一致的事件处理策略。这样可以减少潜在的冲突和不确定性,使代码更加清晰和可维护。

2.9 CommonJS 和 ES Module 的区别

CommonJS 和 ES Module 是 JavaScript 中两种不同的模块规范,它们在定义、使用以及特性上存在显著的差异。以下是对这两者的详细比较:

  1. 定义与用途
    1. CommonJS
      • 定义:CommonJS是一种服务器端的模块规范,旨在为非浏览器的JavaScript环境(如Node.js)提供模块化支持。
      • 用途:主要用于服务器端JavaScript开发,特别是在Node.js平台上。
    2. ES Module(ESM)
      • 定义:ES Module是ECMAScript 2015(ES6)中引入的官方模块系统,也称为ESM。
      • 用途:适用于浏览器和服务器端环境,是现代JavaScript开发中广泛使用的模块规范。
  2. 模块导入导出语法
    1. CommonJS
      • 导出:使用module.exportsexports对象来导出模块成员。
      • 导入:使用require函数来导入其他模块。
    2. ES Module
      • 导出:使用export关键字来导出模块成员,可以导出变量、函数、类等。
      • 导入:使用import关键字来导入其他模块的成员,支持默认导入和命名导入。
  3. 加载时机与方式
    1. CommonJS
      • 加载时机:同步加载,即模块在运行时被加载和执行。
      • 加载方式:动态加载,可以在代码的任何地方使用require来加载模块。
    2. ES Module
      • 加载时机:静态加载,即模块在编译时就被确定和加载。
      • 加载方式:静态导入,import语句必须位于模块的顶层,不能在条件语句或函数内部使用。
  4. 缓存与值绑定
    1. CommonJS
      • 缓存:对每一个加载的模块都存在缓存,一旦模块被加载,除非手动清除缓存,否则在后续的require调用中会返回相同的模块实例。
      • 值绑定:导出的是值的拷贝,如果导出的是一个对象,则后续对该对象的修改会影响到导入模块中的对象(引用类型),但对于原始类型(如数字、字符串)的修改则不会。
    2. ES Module
      • 缓存:浏览器环境对ES Module的加载也实现了缓存机制,但缓存策略可能因浏览器而异。
      • 值绑定:导出的是值的引用,即导入和导出的值都指向同一个内存地址,因此导入模块中的值会随着导出模块中的值的变化而变化。
  5. 其他特性
    1. CommonJS
      • 支持循环引用:由于采用同步加载和缓存机制,CommonJS能够很好地处理循环引用问题。
      • 顶层this指向:在CommonJS模块中,顶层的this指向模块本身。
    2. ES Module
      • 严格模式:ES Module默认在严格模式下执行,无需显式声明use strict
      • 顶层this指向:在ES Module中,顶层的this值为undefined
      • Tree Shaking:ES Module支持Tree Shaking,即静态分析并移除未使用的代码,从而减小打包后的文件体积
      • Code Splitting:ES Module支持Code Splitting,即按需加载代码,提高应用的加载速度和性能

综上所述,CommonJS和ES Module在定义、语法、加载时机与方式、缓存与值绑定以及其他特性上均存在显著差异。开发者在选择使用哪种模块规范时,应根据具体的应用场景和需求进行权衡。

2.10 setTimeout 和 setInterval 的区别

setTimeout 和 setInterval 都是 JavaScript 中常用的定时器函数,用于在指定的时间点或周期性地执行代码。以下是两者的详细区别:

  1. 触发时间与执行频率

    1. setTimeout
      • 触发时间:一次性定时器,在设定的延迟时间之后执行一次指定的函数。
      • 执行频率:只执行一次,之后不再自动触发。
    2. setInterval
      • 触发时间:重复性定时器,会以设定的时间间隔重复执行指定的函数。
      • 执行频率:按照设定的时间间隔不断重复执行,直到被取消。
  2. 用法与参数

    1. setTimeout

      • 语法:

        setTimeout(function, delay, [param1, param2, ...])
        • function:要执行的函数。
        • delay:延迟的毫秒数。
        • [param1, param2, ...]:可选参数,传递给要执行的函数的参数。
      • 示例:setTimeout(function() { console.log("Hello, World!"); }, 2000); 这段代码将在2秒后执行一次指定的函数,打印“Hello, World!”到控制台。

    2. setInterval

      • 语法:

        setInterval(function, milliseconds, [param1, param2, ...])
        • function:要重复执行的函数或代码块。
        • milliseconds:重复执行的时间间隔(以毫秒为单位)。
        • [param1, param2, ...]:可选参数,传递给要执行的函数的参数。
      • 示例:setInterval(function() { console.log("Hello, World!"); }, 1000); 这段代码将每隔1秒执行一次指定的函数,不断打印“Hello, World!”到控制台。

  3. 执行时间准确性

    • setTimeout:不保证在指定的毫秒数之后立即执行,它只是将函数放入异步任务队列中,并在队列中的等待时间过后执行。如果队列中有其他任务需要执行,那么可能会稍微延迟执行
    • setInterval:同样受到 JavaScript 运行环境的影响,可能会有一些微小的延迟。特别是当设定的时间间隔较短时,可能会因为浏览器的性能限制而导致定时器不准确。建议将时间间隔设为100毫秒或以上,以保证计时器的准确性。
  4. 实际应用场景

    • setTimeout:一般用于需要延迟执行的场合,例如动画效果的延迟、按钮的防抖等。
    • setInterval:一般用于需要定时执行的场合,例如轮播图的切换、时钟的更新等。
  5. 取消定时器

    • clearTimeout:用于取消由setTimeout设置的定时器。
    • clearInterval:用于取消由setInterval设置的定时器。

综上所述,setTimeout 和 setInterval 在触发时间、执行频率、用法与参数、执行时间准确性以及实际应用场景等方面都存在明显的区别。在编写 JavaScript 代码时,应根据具体需求选择合适的定时器函数。

2.11 for...in 和 for...of 的区别

for...in 和 for...of 的区别主要有 2 点,最关键的区别在于它们遍历的对象和返回的值不同:

  1. for...in 循环

    • 用于遍历对象的可枚举属性,包括其原型链上的可枚举属性。
    • 返回的是属性名(键名)。
    • 可以遍历普通对象,也可以遍历数组(但不推荐)。
    • ES3 语法。
  2. for...of 循环

    • 用于遍历可迭代对象(如数组、字符串、Map、Set等)。
    • 返回的是每次迭代的值。
    • 不能直接用于遍历普通对象。
    • ES6 语法。
  3. 示例代码

    javascript
    // for...in 示例
    const obj = { a: 1, b: 2, c: 3 };
    for (const key in obj) {
      console.log(key); // 输出: "a", "b", "c"
    }
    
    // for...of 示例
    const arr = [1, 2, 3];
    for (const value of arr) {
      console.log(value); // 输出: 1, 2, 3
    }
  4. 重要区别

    • 遍历顺序:for...in 不保证遍历顺序,而 for...of 会按照迭代器定义的顺序进行遍历。
    • 性能:通常来说,for...of 的性能比 for...in 更好,特别是在遍历数组时。所以遍历优先选 for...of
    • 继承属性:for...in 会遍历对象的原型链,而 for...of 不会。
    • 使用场景:for...in 更适合用于遍历对象的属性,而 for...of 更适合用于遍历数组或其他可迭代对象的值。

2.12 Object.prototype.toString.call

Object.prototype.toString.call() 方法返回一个表示对象内部属性 [[Class]] 的字符串,通过它可以准确判断对象的类型,示例如下:

javascript
Object.prototype.toString.call(42);        // "[object Number]"
Object.prototype.toString.call("hello");   // "[object String]"
Object.prototype.toString.call(true);      // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null);      // "[object Null]"
Object.prototype.toString.call({});        // "[object Object]"
Object.prototype.toString.call([]);        // "[object Array]"
Object.prototype.toString.call(function() {}); // "[object Function]"

2.13 undefined 和 null 的区别

undefined 是 JavaScript 的一种内置数据类型,表示变量声明了但未赋值。null 同样是一种内置数据类型,表示一个空对象引用

  1. 类型检测
    • 使用 typeof 检测 undefined 会返回 "undefined"。
    • 使用 typeof 检测 null 会返回 "object",这是一个历史遗留问题。
  2. 比较操作
    • undefined 和 null 使用双等号 == 比较时会被认为相等,因为它们都代表“没有值”的概念。
    • 使用严格等号 === 比较时,它们是不相等的,因为它们是不同类型的值。
  3. 变量赋值
    • undefined 是 JavaScript 引擎自动赋予未赋值变量的值。
    • null 是开发者显式赋值以表示变量没有值。
  4. 数值转换
    • undefined 转换成数值的时候返回的是 NaN。
    • null 转换为数组返回的是 0。

2.14 如何获取安全的 undefined 值

使用 void 运算符对其后的表达式进行求值,然后返回 undefined。因为 void 运算符总是返回 undefined,而且 0 是一个非常短的常量表达式,所以 void 0 是一种简洁且安全的方式来获得 undefined,示例如下:

javascript
let safeUndefined = void 0;
console.log(safeUndefined); // 输出: undefined

补充:为什么 void 0 就是安全的,为什么不直接使用 undefined?

因为 undefined 在较低版本的 Node/浏览器 环境中可以被重定义。如果被重新定义了,全局的 undefined 就会受到污染,会导致之后的代码出现问题,例如:

javascript
let undefined = 'hello';
let test;
console.log(test === undefined ? 'undefined' : 'not undefined')
// 输出:not undefined
console.log(test === void 0 ? 'undefined' : 'not undefined')
// 输出:undefined

2.15 NaN

  1. NaN 的特点

    • NaN 是唯一一个不等于自身的值。这意味着 NaN !== NaN 总是返回 true,示例如下:

      javascript
      console.log(NaN === NaN); // 输出: false
      console.log(NaN !== NaN); // 输出: true
    • 判断 NaN 可以使用 isNaN() 函数来判断,示例如下:

      javascript
      console.log(isNaN(NaN)); // 输出: true
      console.log(isNaN(123)); // 输出: false
  2. NaN 产生的原因

    • 数学运算结果未定义或无法表示,示例如下:

      javascript
      console.log(0 / 0); // 输出: NaN
    • 将无法解析为数字的字符串转换为数字,示例如下:

      javascript
      console.log(Number("abc")); // 输出: NaN
    • 计算结果超出 JavaScript 能表示的数字范围,示例如下:

      javascript
      console.log(Math.sqrt(-1)); // 输出: NaN
  3. isNaN 和 Number.isNaN 函数有什么区别

    isNaN 函数会先尝试将传入的参数转换为数字,然后检查转换后的值是否为 NaN。这意味着它不仅检测 NaN 本身,还会将那些不能转换为有效数字的值视为 NaN,示例如下:

    javascript
    console.log(isNaN(NaN));          // 输出: true
    console.log(isNaN('hello'));      // 输出: true
    console.log(isNaN(undefined));    // 输出: true
    console.log(isNaN({}));           // 输出: true
    console.log(isNaN(123));          // 输出: false
    console.log(isNaN('123'));        // 输出: false

    Number.isNaN 函数不会进行类型转换,只会在参数本身是 NaN 的情况下返回 true。它更为严格,只有传入的值是 NaN 时才会返回 true,示例如下:

    javascript
    console.log(Number.isNaN(NaN));          // 输出: true
    console.log(Number.isNaN('hello'));      // 输出: false
    console.log(Number.isNaN(undefined));    // 输出: false
    console.log(Number.isNaN({}));           // 输出: false
    console.log(Number.isNaN(123));          // 输出: false
    console.log(Number.isNaN('123'));        // 输出: false

2.16 == 操作符的强制类型转换规则

== 会在比较两个值时进行强制类型转换。这种类型转换遵循一套规则,使得不同类型的值可以相互比较。

  1. 强制转换规则

    • null 和 undefined:null 和 undefined 仅相等于自身和对方,示例如下:

      javascript
      console.log(null == undefined);      // true
      console.log(null == null);           // true
      console.log(undefined == undefined); // true
      console.log(null == 0);              // false
      console.log(undefined == 0);         // false
    • Boolean 类型:如果有一个操作数是布尔值,JavaScript 会将布尔值转换为数字,然后再进行比较,示例如下:

      javascript
      console.log(true == 1);  // true
      console.log(false == 0); // true
      console.log(true == 2);  // false
    • 字符串和数字:如果是字符串和数字比较,JavaScript 会将字符串转换为数字,然后再进行比较,示例如下:

      javascript
      console.log('42' == 42);   // true
      console.log('42' == '42'); // true
      console.log('42' == 43);   // false
      console.log('0' == false); // true
    • 对象和原始类型:如果有一个操作数是对象,另一个是原始类型(字符串、数字、布尔值),JavaScript 会尝试调用对象的 toPrimitive 方法(valueOf 或 toString)将对象转换为原始类型,然后再进行比较,示例如下:

      javascript
      console.log([1, 2] == '1,2');         // true
      console.log([1] == 1);                // true
      console.log({} == '[object Object]'); // true
    • Symbol 和其他类型:Symbol 类型只能与 Symbol 类型进行比较,与其他类型的比较总是返回 false,示例如下:

      javascript
      console.log(Symbol() == Symbol()); // false
      console.log(Symbol() == 'symbol'); // false
      console.log(Symbol() == false);    // false
  2. 特殊情况

    • 空字符串:空字符串会被转换为数字 0 进行比较,示例如下:

      javascript
      console.log('' == 0);     // true
      console.log('' == false); // true
    • 对象转换为原始类型:对象的比较会触发类型转换,通过调用 toPrimitive 方法(valueOf 或 toString),转换为原始类型后再比较,示例如下:

      javascript
      let obj = { toString: () => '42' };
      console.log(obj == '42');  // true
      console.log(obj == 42);    // true

2.17 Object.is() 与比较操作符 == 和 === 的区别

  1. 双等号(==)

    双等号进行相等判断时,如果两边的类型不一致,则会进行类型转换后再进行比较,规则如下:

    • 如果类型不同,会进行类型转换。
    • 将 null 和 undefined 视为相等。
    • 将布尔值转换为数字再进行比较。
    • 将字符串和数字进行比较时,会将字符串转换为数字。
    • 对象与原始类型进行比较时,会将对象转换为原始类型。
  2. 三等号(===)

    三等号进行相等判断时,不会进行类型转换。如果两边的类型不一致,则直接返回 false,规则如下:

    • 如果类型不同,返回 false。
    • 如果类型相同,再进行值的比较。
  3. Object.is()

    Object.is() 在大多数情况下与三等号的行为相同,但它处理了一些特殊情况,如 -0 和 +0,以及 NaN,规则如下:

    • 如果类型不同,返回 false。
    • 如果类型相同,再进行值的比较。
    • 特殊情况:-0 和 +0 不相等,两个 NaN 是相等的。

2.18 逻辑操作符

  1. 逻辑或操作符 (||)

    逻辑或操作符 || 会在找到第一个真值时立即返回该值。如果所有操作数都为假值,则返回最后一个操作数。具体规则如下:

    • 对第一个操作数进行条件判断。
    • 如果第一个操作数的条件判断结果为 true,则返回第一个操作数的值。
    • 如果第一个操作数的条件判断结果为 false,则返回第二个操作数的值。
  2. 逻辑与操作符 (&&)

    逻辑与操作符 && 会在找到第一个假值时立即返回该值。如果所有操作数都为真值,则返回最后一个操作数。具体规则如下:

    • 对第一个操作数进行条件判断。
    • 如果第一个操作数的条件判断结果为 false,则返回第一个操作数的值。
    • 如果第一个操作数的条件判断结果为 true,则返回第二个操作数的值。

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

隐式类型转换也称为类型强制转换,是指 JavaScript 在表达式求值时自动将一种数据类型转换为另一种数据类型的过程。隐式类型转换主要发生在以下三种情况下:算术运算、比较运算和逻辑运算。常见的隐式类型转换规则如下:

  1. 算术运算

    在算术运算中,JavaScript 会将操作数转换为数字类型,示例如下:

    javascript
    console.log(5 + "5");   // "55"(字符串拼接)
    console.log("5" + 5);   // "55"(字符串拼接)
    console.log(5 + 5);     // 10(数值相加)
    console.log(5 - "2");   // 3
    console.log("6" * "2"); // 12
    console.log("8" / 2);   // 4
    console.log("10" % 3);  // 1
  2. 比较运算

    在比较运算中,JavaScript 会将操作数转换为相同的类型再进行比较,示例如下:

    javascript
    // 双等号 ==
    console.log(5 == "5");          // true(字符串 "5" 被转换为数字 5)
    console.log(false == 0);        // true(false 被转换为数字 0)
    console.log(true == 1);         // true(true 被转换为数字 1)
    console.log(null == undefined); // true
    
    // 三等号 ===
    console.log(5 === "5");          // false
    console.log(false === 0);        // false
    console.log(true === 1);         // false
    console.log(null === undefined); // false
  3. 其他比较运算符

    对于其他比较运算符(>、<、>=、<=),操作数会被转换为数字或字符串,示例如下:

    javascript
    console.log(5 > "2");    // true
    console.log("6" < "12"); // false(字符串比较)
    console.log("8" >= 8);   // true
    console.log("10" <= 20); // true
  4. 逻辑运算

    javascript
    // 逻辑!
    console.log(!0);       // true(0 被转换为 false,然后取反为 true)
    console.log(!1);       // false(1 被转换为 true,然后取反为 false)
    console.log(!"");      // true(空字符串被转换为 false,然后取反为 true)
    console.log(!"hello"); // false(非空字符串被转换为 true,然后取反为 false)
    
    // 逻辑||
    console.log(0 || 1); // 1(0 被转换为 false,因此返回第二个操作数 1)
    console.log(1 || 0); // 1(1 被转换为 true,因此返回第一个操作数 1)
    console.log(0 && 1); // 0(0 被转换为 false,因此返回第一个操作数 0)
    console.log(1 && 2); // 2(1 被转换为 true,因此返回第二个操作数 2)
    
    // 逻辑&&
    console.log(0 || 1); // 1(0 被转换为 false,因此返回第二个操作数 1)
    console.log(1 || 0); // 1(1 被转换为 true,因此返回第一个操作数 1)
    console.log(0 && 1); // 0(0 被转换为 false,因此返回第一个操作数 0)
    console.log(1 && 2); // 2(1 被转换为 true,因此返回第二个操作数 2)
  5. 字符串与数字之间的转换

    字符串和数字之间的隐式转换在很多情况下都会发生,示例如下:

    javascript
    console.log("5" - 2);   // 3("5" 被转换为数字 5,然后 5 - 2 = 3)
    console.log("5" * "2"); // 10(两个字符串都被转换为数字)
    console.log("5" / 2);   // 2.5("5" 被转换为数字 5,然后 5 / 2 = 2.5)

2.20 BigInt 解决了什么问题

JavaScript 中的数字类型是基于 IEEE 754 双精度浮点数标准实现的。这种实现方式虽然在绝大多数情况下足够,但在处理非常大的整数时会出现精度问题。为了处理和表示任意精度的整数,JavaScript 引入了 BigInt 类型。BigInt 可以表示和操作任意大的整数而不会丢失精度,从而解决了大整数运算中的问题,问题如下:

javascript
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992
console.log(Number.MAX_SAFE_INTEGER + 2); // 9007199254740992 (错误)
  1. BigInt 的优势

    • 支持任意大整数:BigInt 可以表示任意大的整数,而不会丢失精度。
    • 专门设计用于整数运算:与浮点数不同,BigInt 专门用于整数运算,确保了精度和一致性。
    • 与 Number 类型区别明确:BigInt 是一种新的原始数据类型,与现有的 Number 类型区别明确,避免了混淆。
  2. 使用示例

    • 使用 BigInt 可以非常简单,只需在整数后加上 n 后缀,或者使用 BigInt 构造函数,示例如下:

      javascript
      const bigInt1 = 1234567890123456789012345678901234567890n;
      const bigInt2 = BigInt("1234567890123456789012345678901234567890");
      
      console.log(bigInt1);      // 1234567890123456789012345678901234567890n
      console.log(bigInt1 + 1n); // 1234567890123456789012345678901234567891n
    • 需要注意的是,BigInt 和 Number 类型不能直接混合使用,示例如下:

      javascript
      const num = 42;
      const bigInt = 12345678901234567890n;
      console.log(num + bigInt); // TypeError: Cannot mix BigInt and other types
    • 需要进行显式转换,示例如下:

      javascript
      console.log(BigInt(num) + bigInt); // 12345678901234567932n

2.21 Map 和 Object 的区别

Map 和 Object 都是以 key-value(键值对)的形式对数据进行存储的集合,但它们在多个方面存在显著的区别。以下是Map和Object之间的主要差异:

  1. 键的类型
    • Object:键类型必须是 string 或者 Symbol,如果非 String 类型,会进行数据类型转换。
    • Map:键可以是任意类型,包括对象、数组、函数等,不会进行数据类型转换。
  2. 键的顺序
    • Object:key 是无序的,不会按照添加到顺序返回。对于大于等于0的整数,会按照大小的顺序进行排序;对于小数和负数会当做字符串处理;对于 Symbol 类型,会直接过滤掉,不会进行输出。如果想要输出 Symbol 类型属性,可以通过 Object.getOwnPropertySymbols() 方法。
    • Map:key 是有序的,按照插入的顺序返回。
  3. 键值对的数量
    • Object:只能手动计算,通过 Object.keys() 方法或者通过 for...in 循环统计。
    • Map:直接通过 size 属性访问。
  4. 键值对的访问
    • Object:添加或者修改属性,通过点(.)或者中括号([])的形式。判断属性是否存在用'属性名' in 对象;删除属性使用delete关键字。
    • Map:添加和修改键值对使用 set 方法,判断属性是否存在用 has 方法,获取值用 get 方法,删除键值对用 delete 方法,清空所有键值对用 clear 方法。
  5. 迭代器
    • Object:不具有 iterator 特性,默认情况下不能使用 for...of 进行遍历。
    • Map:Map 结构的 keys()、values()、entries() 方法返回值都具有 iterator 特性,因此可以使用 for...of 进行遍历。
  6. JSON序列化
    • Object:Object 类型可以通过 JSON.stringify() 进行序列化操作。
    • Map:Map 结构不能直接进行 JSON 序列化,但可以通过先将 Map 对象转换为数组(例如使用 Array.from() 方法),然后再进行 JSON 序列化。
  7. 构造方式和应用场景
    • Object:是 ECMAScript 1st 里添加的一种引用类型数据,是最常用的存储键值对的集合。
    • Map:是 ECMAScript 2015 版本里新增的键值对的集合,采用 Hash 结构存储。它提供了一种更灵活和强大的键值对存储方式,特别适用于需要保持键值对插入顺序的场合。

综上所述,Map 和 Object 在键的类型、顺序、键值对数量的获取、访问方式、迭代器支持、JSON 序列化以及构造方式和应用场景等方面都存在显著差异。开发者可以根据具体需求选择适合的集合类型来存储和操作数据。

2.22 arguments

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

  1. 关于为什么 arguments 是类数组而不是数组:

    • 历史原因: arguments 对象是在 JavaScript 语言早期就被引入的。那时候,JavaScript 还没有真正的数组对象,所以 arguments 被设计成了一个类数组对象。
    • 性能考虑: 将 arguments 实现为真正的数组可能会带来一些性能开销。类数组对象可以更高效地实现某些操作。 arguments 是一个对象,它的属性是从 0 开始依次递增的数字,还有 callee 和 length 等属性,与数组相似;但是它却没有数组常见的方法属性,如 forEach, reduce 等,所以叫它们类数组。
  2. 要遍历类数组,有三个方法:

    • 将数组的方法应用到类数组上,这时候就可以使用 call 和 apply 方法,如:

      javascript
      function foo(){ 
        Array.prototype.forEach.call(arguments, a => console.log(a))
      }
    • 使用Array.from方法将类数组转化成数组:‌

      javascript
      function foo(){ 
        const arrArgs = Array.from(arguments) 
        arrArgs.forEach(a => console.log(a))
      }
    • 使用展开运算符将类数组转化成数组:

      javascript
      function foo(){ 
        const arrArgs = [...arguments] 
        arrArgs.forEach(a => console.log(a)) 
      }
  3. 拓展

    在现代 JavaScript 中,我们通常推荐使用剩余参数(rest parameters)来替代 arguments剩余参数提供了一个真正的数组,更易于使用:

    javascript
    function example(...args) {
      args.forEach(arg => console.log(arg));
    }

3. JavaScript 进阶

3.1 什么是 JavaScript 的尾调用

尾调用是指函数内部的最后一个操作是调用另一个函数的情况。在 JavaScript 中,当一个函数调用发生在另一个函数的尾部(即调用结束后直接返回其结果,而无需进一步操作)时,这种调用称为尾调用。

使用尾调用的主要好处在于其对栈内存的优化。通常情况下,每一个函数调用都会在栈内存中占据一个新的框架(frame),直到函数执行完成。而尾调用因为不需要保留当前函数的执行上下文,因此可以直接复用当前的栈帧,从而使递归操作更加高效,避免栈溢出(stack overflow)的风险。

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

    尽管尾调用本身是一种编程方法,但要实现其内存优化效果,离不开 JavaScript 引擎对尾调用优化的支持。尾调用优化指的是 JavaScript 引擎在检测到尾调用时,会复用当前的栈帧,从而节省内存开销。需要注意的是,并非所有的 JavaScript 环境都支持尾调用优化。例如,某些现代浏览器和 Node.js 在严格模式下才会执行优化。

  2. 尾递归(Tail Recursion)

    尾递归是尾调用的一种特殊形式,指的是在递归函数中,递归调用是函数的最后一个操作。因为尾递归可以直接复用当前的栈帧,所以在处理深度递归时,尾递归能显著减少栈内存的使用量。举个简单的例子:

    javascript
    // 非尾递归
    function factorial(n) {
      if (n === 1) return 1;
      return n * factorial(n - 1);
    }
    
    // 尾递归
    function factorialTail(n, acc = 1) {
      if (n === 1) return acc;
      return factorialTail(n - 1, n * acc);
    }

    在上述例子中,factorialTail 函数是尾递归,因而在支持尾调用优化的环境下,即使 n 很大也不会导致栈溢出。

  3. JavaScript 严格模式

    在 JavaScript 中,为了使尾调用优化生效,代码需要运行在严格模式(strict mode)下,最简单的方法是在文件或函数的头部添加 "use strict"; 指令:

    javascript
    "use strict";
    function exampleFunction() {
      // 函数内容
    }
  4. 实际应用场景

    尾调用和尾递归特别适合解决那些包含大量迭代步骤的算法问题。常见的场景包括计算阶乘、斐波那契数列、Hanoi 塔问题等。通过将递归过程转化为尾递归,可以显著提高程序在处理大数据量时的性能表现。