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 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]"

1.3 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

    MDN:instanceof 运算符用于检测构造函数的 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
    
    let num = 42;
    console.log(num instanceof Number); // false (因为 num 是基本类型)
  3. 两者区别

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

1.4 typeof null 的结果 ​

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

在 JavaScript 中,所有事物都是对象,但有一个例外:null。然而,为了简化类型检测,typeof 操作符对于原始数据类型(如 number、string、boolean)返回的是它们的小写形式字符串(如 "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.5 字符串的常用方法 ​

  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.6 数组的常用方法 ​

  1. 创建和初始化数组
    • Array() 构造函数:创建一个新的数组实例。
    • 数组字面量:使用方括号[]直接创建数组。
  2. 添加和删除元素
    • push(element1, ..., elementN):在数组的末尾添加一个或多个元素,并返回新的长度。
    • pop():移除数组的最后一个元素,并返回该元素的值。
    • unshift(element1, ..., elementN):在数组的开头添加一个或多个元素,并返回新的长度。
    • shift():移除数组的第一个元素,并返回该元素的值。
  3. 查找元素
    • indexOf(searchElement[, fromIndex]):返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。
    • includes(searchElement[, fromIndex]):判断一个数组是否包含一个指定的值,根据情况返回true或false。
    • 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.7 对象的常用方法 ​

  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还提供了一些特定内置对象的方法,如Array、String、Date和Math等对象的方法。这些方法在各自的对象文档中有详细描述,并用于执行与该对象相关的特定操作。

1.8 什么是可迭代对象 ​

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

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

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

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

常见的可迭代对象:

  • 数组(Array):数组是可迭代对象,因为它们实现了 @@iterator() 方法。
  • 字符串(String):字符串也是可迭代对象,可以按字符迭代。
  • Map 和 Set:ES6 引入的 Map 和 Set 对象也是可迭代对象。
  • 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.9 什么是类数组对象 ​

类数组对象(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']

2. JavaScript 进阶 ​

2.1 执行上下文、执行栈 ​

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

  1. 执行上下文(Execution Context)

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

    1. 变量对象(Variable Object, VO):
      • 在 ES5 中,变量对象包含了函数声明和变量声明。
      • 在 ES6 及以后,引入了 let 和 const,这些声明不再存储在变量对象中,而是有其特定的行为(块级作用域)。但为简化理解,可以认为变量对象存储了所有可访问的变量和函数。
    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 代码非常重要。通过掌握这些概念,你可以更好地理解和预测代码的行为。

2.2 闭包和作用域 ​

闭包定义

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

  1. 相关概念

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

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

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

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

    • 闭包是作用域应用的特殊场景。常见的闭包使用有两种场景:一种是函数作为参数被传递;一种是函数作为返回值被返回。
  5. 闭包的实现原理

    • JavaScript 的作用域链和垃圾回收机制。
  6. 怎么看待闭包的副作用?

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

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

2.3 变量提升 ​

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

这里有几点需要注意:

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

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

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

  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. 使用 let 和 const 的声明:let 和 const 声明的变量不会被提升到作用域的顶部,而是被“块级作用域”(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。

  5. 为什么要进行变量提升:实现函数间的相互递归调用。

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

2.4 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

2.5 this ​

JavaScript_this

2.6 原型和原型链 ​

JavaScript原型和原型链

2.7 实现继承 ​

JavaScript实现继承

2.8 事件循环 ​

JavaScript事件循环

2.9 事件模型 ​

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

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

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

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

    javascript
    parentElement.addEventListener('click', function(event) {
      if(event.target.matches('.childClass')) {
        // 子元素被点击的逻辑处理
      }
    });
  • 事件流阶段:事件流描述了页面中接受事件的顺序。

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

2.10 异步编程 ​

在 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

    async 和 await 是基于 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);
    });

2.11 箭头函数 ​

箭头函数(Arrow Functions)是ES6(ECMAScript 2015)引入的一种更简洁的函数写法。相比于传统的函数表达式,箭头函数提供了一种更简短的语法,并且不绑定自己的this,arguments,super,或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.12 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.exports或exports对象来导出模块成员。
      • 导入:使用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.13 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.14 setTimeout 不适合做动画 ​

  1. 无法与屏幕刷新同步:它的执行时机不保证在重绘之前,容易导致丢帧或过度绘制。
  2. 无自动节流与后台暂停:页面不可见时仍会执行,浪费资源;而 requestAnimationFrame 会在后台暂停。
  3. 精度和稳定性差:固定间隔无法适应不同刷新率的屏幕,且受事件循环阻塞影响,动画不平滑。

因此,现代动画应该使用 requestAnimationFrame 或 CSS 动画/过渡。

2.15 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.16 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.17 如何获取安全的 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.18 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.19 Object.assign 和对象扩展运算符的区别 ​

  1. 对目标对象的影响:
    • Object.assign:修改第一个参数对象(目标对象),然后将该对象返回。 例如:const target = { a: 1 }; Object.assign(target, { b: 2 }); → target 被直接修改。
    • 扩展运算符:创建一个全新的对象,不会修改任何已有对象。 例如:const target = { a: 1 }; const newObj = { ...target, b: 2 }; → target 保持不变。
  2. 是否触发 setter
    • Object.assign:在赋值过程中会触发目标对象上的 setter 方法(如果存在)。
    • 扩展运算符:在创建新对象时,只是简单地定义属性,不会触发任何 setter。
  3. 处理 null / undefined 源对象
    • Object.assign:如果第一个参数是 null 或 undefined,会报错(因为它要求目标必须是一个对象)。 但源对象为 null / undefined 时会被忽略。
    • 扩展运算符:{ ...null } 或 { ...undefined } 会得到空对象 {},不会报错。

2.20 encodeURI、encodeURIComponent ​

  1. escape ()

    首先,escape() 函数已经被废弃了,我们不应该再在新的代码中使用它。但是我们可能在维护旧代码时遇到它。所以需要了解 escape() 主要用于对字符串进行编码。它会将所有非字母数字字符转换为它们的十六进制转义序列,除了 @*_+-./ 这些字符。 例如:

    javascript
    console.log(escape("Hello World! @*_+-./"));
    // 输出: Hello%20World%21%20@*_+-./
  2. encodeURI ()

    encodeURI() 用于编码完整的 URI。它会编码所有对 URI 有特殊含义的字符,除了 ,/?: @&=+$# 这些在 URI 中有特定含义的字符。

    javascript
    console.log(encodeURI("https://example.com/path?name=张三&age=18"));
    // 输出: https://example.com/path?name=%E5%BC%A0%E4%B8%89&age=18
  3. encodeURIComponent ()

    encodeURIComponent() 用于编码 URI 的组成部分。它会编码所有对 URI 有特殊含义的字符,包括 ,/?: @&=+$# 这些字符。

    javascript
    console.log(encodeURIComponent("https://example.com/path?name=张三&age=18"));
    // 输出: https%3A%2F%2Fexample.com%2Fpath%3Fname%3D%E5%BC%A0%E4%B8%89%26age%3D18
  4. 实际运用

    • 当我需要编码整个 URL 时,我会使用 encodeURI()。
    • 当我需要编码 URL 参数时,我会使用 encodeURIComponent()。
    • 我会避免使用 escape(),因为它已经被废弃了。

3. JavaScript 拓展 ​

3.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(完成)阶段。在这个阶段,提案被接受为最终的标准,意味着它已经经过了广泛的实现、测试和审查,并被认为是稳定和可靠的。完成阶段的标准可以被广泛采用和应用于实际的编程环境中。

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

  • 解构赋值:适用于数组、对象和其他可迭代的数据结构。
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;

3.3 事件 level0、level1 ​

在HTML中,level0和level1并不是标准的事件处理术语。然而,我们可以讨论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添加的事件监听器。相反,应该选择其中一种方法,并保持一致的事件处理策略。这样可以减少潜在的冲突和不确定性,使代码更加清晰和可维护。

3.4 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 模块或类时,严格模式已经成为默认行为。

3.5 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 会被转换为原始值进行比较。

3.6 JavaScript 文件相互引用问题 ​

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

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

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

3.7 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 => {
      // 使用加载的模块
    });

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

3.8 JavaScript 数值精度问题 ​

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

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

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

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

3.9 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 塔问题等。通过将递归过程转化为尾递归,可以显著提高程序在处理大数据量时的性能表现。

3.10 逻辑操作符 ​

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

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

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

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

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

3.11 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

3.12 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)

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

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

  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

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

  1. 双等号(==)

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

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

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

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

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

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

3.15 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

3.16 Object.keys 的返回值是无序的 ​

Object.keys 方法返回一个数组,该数组的元素是一个对象自身可枚举属性的字符串键。尽管对象的属性在 ECMAScript 标准中没有明确的顺序要求,但现代 JavaScript 引擎对对象的属性顺序做出了一定保证,因此我们通常可以认为 Object.keys 返回值是有序的。

  1. 数字属性优先:数字属性(包括可以转换为数字的字符串属性)会被优先返回,并按照数值大小升序排序。

  2. 字符串属性其次:非数字的字符串属性按照它们被添加到对象时的顺序排序。

  3. Symbol 属性不会被返回:Object.keys 不会返回 Symbol 类型的属性,如果需要获取 Symbol 属性,需要使用 Object.getOwnPropertySymbols。

  4. 示例

    javascript
    // 属性顺序
    const obj = {
      b: 'b',
      1: '1',
      a: 'a',
      2: '2'
    };
    console.log(Object.keys(obj)); // 输出: ['1', '2', 'b', 'a']
    
    // 特殊情况处理: 对于数组
    const arr = ['a', 'b', 'c'];
    arr.test = 'test';
    console.log(Object.keys(arr)); // 输出: ['0', '1', '2', 'test']
    
    // 特殊情况处理: 对于类数组对象
    const arrayLike = {
      length: 3,
      0: 'a',
      1: 'b',
      2: 'c'
    };
    console.log(Object.keys(arrayLike)); // 输出: ['0', '1', '2', 'length']

3.17 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.18 如何提取深度嵌套的对象中的指定属性 ​

从深度嵌套的对象中提取指定属性,关键是安全地访问可能不存在的中间路径,避免因 undefined 或 null 导致报错。以下是几种常用方法:

  1. 可选链操作符(?.) — 最推荐(ES2020+)

    直接链式访问,遇到 null 或 undefined 会短路返回 undefined,不会报错。

    javascript
    const value = obj?.a?.b?.c?.target; // 如果中间任何一层缺失,value === undefined
  2. 解构赋值 + 默认值(仅当层级固定且较少时)

    javascript
    const { a: { b: { c: { target = '默认值' } = {} } = {} } = {} } = obj;
  3. Lodash 的 _.get 方法

    javascript
    const target = _.get(obj, 'a.b.c.target', '默认值');