JavaScript面试题

421 阅读37分钟

持续更新中

JavaScript.png

基础概念

数据类型

一、JavaScript有哪些数据类型,它们的区别?

JavaScript的数据类型可以分为两大类,基本数据类型和引用数据类型。

基本数据类型:

  1. Number: 表示数字,可以是整数或浮点数,包括Infinity、-Infinity和NaN(非数字)。
  2. String: 表示文本,由零个或多个字符组成,使用单引号(' ')或双引号(" ")包围。
  3. Boolean: 只有两个值:true 和 false,用于逻辑判断。
  4. Null: 表示一个刻意的空值,只有一个值null,用来表示一个变量被赋值为空对象指针。
  5. Undefined: 表示变量已被声明但未被赋值,只有一个值undefined。
  6. BigInt: 用于存储超过常规Number类型安全整数范围的大整数。
  7. Symbol: ES6引入的新类型,表示独一无二的值,常用于对象的唯一属性键。

引用数据类型:

  1. Object: 包括普通对象、数组(Array)、函数(Function)等,存储在堆内存中,变量实际存储的是指向这些数据的引用(地址)。

数据类型的区别:

  1. 存储方式:基本数据类型直接存储值,存储在栈内存中;引用数据类型在栈中存储的是引用地址,数据本身存储在堆内存中。
  2. 复制行为:基本数据类型的变量复制给另一个变量时,会创建一个新的值,两者互不影响。而引用数据类型复制时,复制的是引用,因此改变其中一个变量的值可能会影响到另一个。
  3. 值的比较:基本数据类型使用==或===比较时,直接比较值或者类型是否相等。引用数据类型使用==比较可能会发生类型转换,使用===则要求类型和值都相等。
  4. 内存占用:基本数据类型占用的内存相对固定且较小。引用数据类型根据数据的实际大小动态分配内存,可能占用更多内存空间。

二、数据类型检测的方式有哪些?

  1. typeof:返回一个表示变量数据类型的字符串。对于基本类型值(不包括null和undefined)和函数非常准确。
  2. instanceof:用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上,以此判断该对象是否是这个构造函数的实例。
variable instanceof Constructor // 语法
[] instanceof Array  // true
  1. constructor:每个对象都有一个内置的constructor属性,指向创建该对象的构造函数。但这个属性可以被修改,所以不是完全可靠。
variable.constructor === Constructor // 语法
(new Date()).constructor === Date  // true
  1. Object.prototype.toString.call():是最准确的数据类型检测方法之一,可以区分所有基本类型和引用类型,包括null和undefined。
Object.prototype.toString.call(variable)  // 语法
Object.prototype.toString.call([])  // "[object Array]"

三、判断数组的方式有哪些?

  1. Array.isArray()

    • 用途:ES5引入的标准方法,专门用来判断一个值是否为数组。

    • 语法:Array.isArray(variable)

    • 例子:Array.isArray([]) 返回 true。

  2. instanceof

    • 用途:检查一个对象是否是某个构造函数的实例。

    • 语法:variable instanceof Array

    • 例子:[] instanceof Array 返回 true。

  3. 对象的constructor属性

    • 用途:检查对象的constructor属性是否指向Array构造函数。

    • 语法:variable.constructor === Array

    • 注意:此方法可能因constructor被重写而不准确。

  4. 检查__proto__

    • 用途:检查对象的__proto__属性是否等于Array.prototype。

    • 语法:variable.__proto__ === Array.prototype

    • 注意:__proto__属性在某些旧的或不兼容的环境中可能不存在。

  5. toString()方法

    • 用途:调用对象的toString()方法,返回"[object Array]"表示是数组。

    • 语法:Object.prototype.toString.call(variable) === "[object Array]"

    • 优点:这种方法比较通用,适用于各种情况,包括跨环境。

在实际开发中,Array.isArray()通常是首选方法,因为它是最安全和最标准的。其他方法在某些特定情况下可能不可靠,尤其是当涉及到对象的原型链被篡改时。

四、null和undefined区别?

  1. 类型差异:

    • undefined是一种基本数据类型,其类型本身即为undefined。

    • null也是一种基本数据类型,但有趣的是,使用typeof操作符检查null时,会返回"object",这是一个历史遗留问题,实际上它并不是对象类型。

  2. 含义差异:

    • undefined通常表示变量已声明但尚未被赋值,或者声明前使用变量,再或者函数没有返回值的情况。

    • null则常常被用作一个故意设置的空值,表示某个变量意在指向一个对象,但现在没有指向任何对象。

  3. 逻辑比较:

    • 在宽松相等(==)比较中,null和undefined被认为是相等的,因为它们都代表“无值”。

    • 使用严格相等(===)时,它们不相等,因为它们是两种不同的数据类型。

  4. 转换行为:

    • 在转换为布尔值时,两者都会被转换为false。

    • 在转换为数值时,undefined转换为NaN,而null转换为0。

五、map和Object的区别?

  1. 键的类型:Map的键类型可以是任意类型,Object的键类型只能是string或者Symbol。
  2. 遍历顺序:遍历Map对象时,会按照插入的顺序返回键值对。Object的遍历顺序在不同的JS引擎下可能是不同的。
  3. 获取属性值:Map通过get方法获取指定键的值,Object通过 点语法 或者 方括号 的语法访问属性值。
  4. 使用场景:当需要频繁操作数据,同时键的类型比较复杂时,可以选择Map。

六、map和weakMap的区别?

  1. 键的类型:Map的键类型可以是任意类型,WeakMap的键类型只能是对象。
  2. 内存管理:Map中的键值对只要没有被显式删除,就会一直存在于内存中,会阻止垃圾回收器回收作为键的对象,即使在代码的其他地方不再使用这个对象。当一个对象作为键被添加到 WeakMap 后,如果在 WeakMap 之外没有对这个对象的其他引用,垃圾回收器可以在适当的时候回收这个对象及其相关的键值对。这有助于防止内存泄漏,特别是在处理对象缓存等场景时非常有用。
  3. 可枚举性:Map对象是可枚举的,可以通过entries()获取迭代器。WeakMap是不可枚举的,因为 WeakMap 的键是弱引用,其内容随时会因为垃圾回收而改变,所以不适合提供遍历机制。

七、其他值到字符串的转换规则?

  1. Null 和 Undefined 类型 ,null 转换为 "null",undefined 转换为 "undefined",
  2. Boolean 类型,true 转换为 "true",false 转换为 "false"。
  3. Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。
  4. Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。
  5. 对普通对象来说,除非自行定义 toString() 方法,否则会调用 toString()(Object.prototype.toString())来返回内部属性 [[Class]] 的值,如"[object Object]"。如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。

八、其他值到数字值的转换规则?

  1. Undefined 类型的值转换为 NaN。
  2. Null 类型的值转换为 0。
  3. Boolean 类型的值,true 转换为 1,false 转换为 0。
  4. String 类型的值转换如同使用 Number() 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0。
  5. Symbol 类型的值不能转换为数字,会报错。
  6. 对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。

九、其他值到布尔类型的值的转换规则?

以下这些是假值:

  1. undefined
  2. null
  3. false
  4. +0、-0
  5. NaN
  6. ""

假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。

十、substring和substr函数的区别是什么?

从兼容性方面来说,substring 是 JavaScript 标准方法,在各种新旧浏览器中兼容性良好,自 ECMAScript 1 起就被支持。substr 虽被广泛支持,但已被标记为非标准方法,在未来的 JavaScript 版本或严格环境中可能被废弃或修改,不过目前主流浏览器如 Chrome、Firefox、Safari 等仍支持它。

在实际应用场景中,substring 更适用于依据明确的索引位置范围提取子字符串,比如从文件路径中提取文件名。而 substr 侧重于从指定起始位置提取固定长度的字符串,例如从固定格式的短信内容里提取特定长度的验证码等。

变量声明

一、let、const、var的区别?

  1. 作用域:

    • var: 在函数作用域或全局作用域中声明变量。在函数外部声明的 var 变量会成为全局变量。在函数内部,var 会被提升至函数作用域的顶部。

    • let 和 const: 引入了块级作用域的概念。这意味着它们声明的变量仅在其所在的代码块(如 if 语句、for 循环或其他大括号包裹的任意代码块)内有效。let 和 const 不会被提升至块级作用域的顶部。

  2. 变量重新赋值

    • var和let: 允许变量被重新赋值。

    • const: 声明的是常量,一旦赋值就不能再次更改,如果试图重新赋值给 const 变量会导致错误。如果是引用数据类型,虽然不能重新赋值整个变量,但可以修改其内部属性或元素。

  3. 变量提升

    • var: 存在变量提升现象,可以在声明之前访问变量,此时值为 undefined。

    • let 和 const: 没有变量提升到作用域顶部的现象,如果在声明前访问,会引发 ReferenceError。

  4. 暂时性死区

    • let 和 const: 在它们声明之前的区域被称为暂时性死区,在这个区域内访问这些变量会报错。

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

在JS中,当使用const关键字声明并且初始化一个对象,这个变量实际上保存的是指向那个对象的一个引用地址。这个地址是固定的,引用地址指向的内存是可以修改的。

运算符

一、typeof null 的结果是什么,为什么?

typeof null 的结果是 "object"。原因是JavaScript最初实现的时候,所有值都被设计为可以存储在一个32位的单元中,其中包含一个类型标签来指示值的类型。null 值的二进制表示是全0,这在当时的类型标记系统中恰好与对象类型的标记相匹配。因此,当对 null 应用 typeof 操作符时,它会错误地被识别为 "object"。尽管这一行为被认为是一个错误,由于修正它会导致大量依赖现有行为的代码出现问题,因此这一特征就被保留了下来。

二、intanceof 操作符的实现原理?

instanceof 操作符用于判断一个对象是否是另一个对象的实例,或者是否在其原型链上存在另一个对象的构造函数。它的实现原理基于JavaScript的原型链机制。

三、typeof 与 instanceof 区别?

  1. 作用不同:typeof 主要用于检测基本数据类型,而 instanceof 用于检查对象是否属于特定类或构造函数的实例。
  2. 适用范围不同:typeof 对于原始值和函数非常有效,但对于所有对象(包括数组和 null)都返回 "object"。instanceof 则主要用于对象,特别是当需要区分不同类型的对象时。
  3. 实现原理不同:typeof 是基于值的类型直接判断,而 instanceof 是通过原型链来判断一个对象是否是某个构造函数的实例。

浮点数

一、为什么0.1+0.2 ! == 0.3,如何让其相等?

这个现象出现是因为浮点数的表示和计算存在精度问题。

  1. 使用容差比较:定义一个很小的正数作为容差,然后比较两个浮点数之差的绝对值是否小于这个容差。
   function isEqualWithPrecision(a, b, precision = 1e-15) {
     return Math.abs(a - b) < precision;
   }
   console.log(isEqualWithPrecision(0.1 + 0.2, 0.3)); // 输出:true
  1. 使用toFixed方法:将浮点数转换为字符串,指定小数点后的位数,然后再转回数字进行比较。但这种方法也有其局限性,特别是当涉及较大或较小的数值时,可能会有信息丢失。
   console.log((0.1 + 0.2).toFixed(10) === '0.3'); // 注意:这种方法在特定情况下可能不准确

深拷贝&浅拷贝

一、object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别?

两者都是浅拷贝的方式。

区别:

  • Object.assign() 方法可以将所有可枚举的属性的值从一个或多个源对象复制到目标对象,返回值是目标对象。
  • 扩展运算符与 Object.assign() 类似,也是用来创建一个新的实例,但它通常用于函数调用或字面量表达式中。

ES6+特性

一、ES6引入了哪些特性?

ES6(ECMAScript 2015)是JavaScript语言的一个重要版本,它在ES5的基础上增加了很多新特性,极大地丰富了JavaScript的语法和功能。以下是ES6引入的一些主要特性:

  1. 块级作用域:let 和 const 关键字用于声明变量,与var不同的是,它们具有块级作用域,并且const声明的变量值不可改变。
  2. 箭头函数:提供了一种更简洁的函数表达式语法,同时改变了函数内部this的绑定规则,箭头函数内的this会继承外部作用域的this值。
  3. 解构赋值:可以从数组或对象中提取数据并赋值给变量,简化了数据处理的过程。
  4. 模块化:ES6引入了import和export关键字,支持按需加载代码,提高了代码的复用性和组织性。
  5. 类:类是基于原型的面向对象编程的一种语法糖,使创建对象更加方便,支持继承、静态方法等特性。
  6. Promise:虽然Promise的概念在ES6之前就已经存在,但是ES6将其纳入标准,使得异步操作更加规范和易于管理。
  7. Symbol:是一种新的原始数据类型,它的值是唯一的,可以作为对象属性的标识符,确保不会与其他属性名冲突。
  8. Set 和 Map 数据结构:
    1. Set 是一个不包含重复值的集合。
    2. Map 是一个简单键值对的数据结构,其中每个键都是唯一的。
  9. 迭代器和生成器:
    1. 迭代器是一种访问集合元素的方式,而生成器是一种可以返回迭代器的特殊函数。
  10. 10.Proxy 和 Reflect:
    1. Proxy 对象用于定义自定义行为(如属性查找、赋值、枚举、函数调用等),Reflect 是一组用于操作对象的静态方法。

二、在 JavaScript 的 ES6 类语法中,解释构造函数(constructor)的作用。如何在类中定义方法和属性?请举例说明。

它是类中的一种特殊方法,在使用 new 关键字创建类的实例时被自动调用。主要用于初始化实例对象的属性,可以接受参数,并将这些参数赋值给实例的属性,从而确定实例的初始状态。

定义属性:可以直接在类的内部使用 this 关键字来定义属性。

class Person {
  constructor(name, age) {
    // 定义实例属性
    this.name = name;
    this.age = age;
  }
}

定义方法:在类中定义方法时,直接在类的内部声明函数即可。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  // 定义实例方法
  sayHello() {
    console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
  }
}

let person = new Person('John', 25);
person.sayHello(); 

遍历

一、for...in和for...of的区别?

  1. 语法含义:
    1. a.for...in:主要用于遍历对象的可枚举属性(包括继承的可枚举属性)。例如,对于一个对象,for...in循环会遍历对象的键名。
    2. for...of:用于遍历可迭代对象(iterable)的值,如数组、字符串、Map、Set等。它是在 ES6 中引入的,提供了一种简洁的方式来遍历这些数据结构中的元素。
  2. 不适应场景:
    1. a.for...in 用于数组,虽然for...in可以用于数组,但它遍历的是数组的索引(作为对象的属性),而不是数组元素本身。这可能会导致一些意外的结果,特别是如果数组的原型被修改或者在遍历过程中数组的长度发生变化。
    2. for...of 用于普通对象,for...of不能直接用于普通对象,因为普通对象不是可迭代对象(它们没有默认的迭代器)。如果要使用for...of遍历对象,需要先定义对象的迭代器(通过Symbol.iterator方法)。

二、如何使用for...of遍历对象?

虽然for...of循环不直接支持遍历普通对象的属性,但有一些策略可以实现类似的功能。以下是几种方法:

  1. 使用Object.keys()或Object.entries():

    • Object.keys(obj)返回一个包含对象所有可枚举属性名称的数组。

    • Object.entries(obj)返回一个数组,其中每个元素是一个包含属性名和对应值的数组。

可以使用for...of遍历这两个方法返回的数组:

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

   for (const key of Object.keys(obj)) {
     console.log(key, obj[key]);
   }

   // 或者

   for (const [key, value] of Object.entries(obj)) {
     console.log(key, value);
   }
  1. 使用Symbol.iterator:如果想要在对象上实现迭代器,可以给对象添加Symbol.iterator属性,指向一个返回迭代器的函数。然后,for...of可以遍历这个迭代器。
   const obj = {
     [Symbol.iterator]: function* () {
       yield 'a';
       yield 'b';
       yield 'c';
     },
   };

   for (const key of obj) {
     console.log(key);
   }
  1. 使用Object.getOwnPropertyNames():这个方法返回一个包含对象所有(包括不可枚举的)属性名称的数组,可以配合for...of遍历。
   const obj = { a: 1, b: 2, c: 3, [Symbol.unscopables]: true };

   for (const key of Object.getOwnPropertyNames(obj)) {
     console.log(key);
   }
  1. 使用Reflect.ownKeys():返回一个包含对象所有属性(包括不可枚举的和Symbol属性)的数组。
   const obj = { a: 1, [Symbol('secret')]: 2 };

   for (const key of Reflect.ownKeys(obj)) {
     console.log(key);
   }

函数与作用域

闭包

一、对闭包的理解?

闭包是JavaScript中的一个重要概念,它涉及到函数和作用域。简单来说,闭包就是一个函数,这个函数可以访问并操作其外部作用域中的变量,即使在其外部函数执行完毕后,这些变量仍然可以被保留和访问。闭包的关键在于它创建了一个持久的作用域链,使得内部函数能够“记住”其诞生时的环境。

闭包的主要特点体现在以下几个方面:

  1. 作用域链:闭包形成了一条作用域链,允许内部函数访问外部函数的局部变量,即使外部函数已经执行完毕,这些变量仍然在内存中存在,因为它们被内部函数引用。

  2. 变量生命周期:由于闭包,外部函数的局部变量不会在外部函数执行完后立即被垃圾回收,它们的生命周期得以延长,直到没有其他引用指向它们。

  3. this的绑定:闭包中的this指向在函数定义时确定,而不是调用时,这可以帮助在特定上下文中保持this的值,尤其是在异步编程或事件处理中。

  4. 应用场景:

    • 私有变量:闭包常用于创建私有变量和方法,避免全局污染。

    • 模块化:通过闭包实现模块化,封装数据和逻辑。

    • 异步编程:在回调函数、Promise或async/await中,闭包可以帮助保存异步操作的上下文。

    • 事件处理:在事件监听器中,闭包可以确保事件处理器能够访问外部的变量和状态。

然而,闭包也需要注意内存管理。过度使用闭包可能导致内存泄漏,特别是在循环中创建闭包时。因此,理解何时创建和销毁闭包至关重要,以避免不必要的资源消耗。

总的来说,闭包是JavaScript中实现功能强大且灵活代码的关键工具,但需要谨慎使用,以确保代码的性能和可维护性。

this关键字

一、对this对象的理解?

  1. 全局上下文:

    • 在全局执行环境中(非严格模式下),this指向全局对象。在浏览器中,这通常是window对象;在Node.js环境中,是global对象。

    • 在严格模式('use strict';)下,全局范围内的this是undefined。

  2. 函数调用:

    • 直接调用一个函数时,非严格模式下this同样指向全局对象。在严格模式下,this是undefined。

    • 如果函数作为对象的方法被调用,this指向该对象。

  3. 构造函数:

    • 使用new关键字调用构造函数时,this指向新创建的实例对象。
  4. 箭头函数:

    • 箭头函数不绑定自己的this,它会捕获其所在上下文的this值作为自己的this值。这意味着箭头函数的this由其定义时的环境决定,而非调用时的环境。
  5. call(), apply(), bind()方法:

    • 这些方法可以显式地设置函数调用时的this值。

    • call()和apply()方法可以立即调用函数,并传入指定的this值和参数。

    • bind()方法创建一个新的函数,其this值被永久绑定到传入的值,但不会立即调用函数。

  6. 事件处理器:

    • 在DOM事件处理程序中,this通常指向触发事件的元素,但这也会受到事件绑定方式的影响。
  7. 异步编程:

    • 在Promise链、async/await或回调函数中,this的值通常需要特别注意,因为它们的上下文可能与预期不符,可能需要使用.bind()、箭头函数或其它技巧来确保正确的this值。

二、bind、call和apply的区别?

  1. 三者都可以改变函数的this对象指向
  2. 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window
  3. 三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入
  4. bind是返回绑定this之后的函数,apply、call 则是立即执行

构造函数

一、如果new一个箭头函数的会怎么样?

在JS中,箭头函数与普通函数不同,不会创建自己的this上下文,而是从外围作用域继承this。使用new关键字实例化箭头函数就会抛出一个错误,原因是new关键字期望构造函数创建一个实例对象,并且该函数内部的this应该绑定到新创建对象。但是箭头函数没有属于自己的this,也不支持用作构造函数,因此这种用法是无效的,并且会导致抛出错误。

二、new操作符的实现原理?

在JavaScript中,new操作符用来创建一个给定构造函数(通常是函数)的新实例。当使用 new 关键字调用构造函数时,会发生以下步骤:

  1. 创建一个新的空对象。
  2. 将这个新对象的原型设置为构造函数的原型属性(即 constructor.prototype)。
  3. 将构造函数的 this 上下文绑定到这个新的对象。
  4. 执行构造函数内的代码(为这个新对象添加属性)。
  5. 如果构造函数没有返回其他对象,则 new 操作默认返回这个新的对象。

异步编程

一、setTimeout、Promise、Async/Await 的区别?

  1. setTimeout:

    • setTimeout 是一个全局函数,用于在指定的毫秒数后执行一个回调函数。它不保证精确的延迟时间,因为其他任务可能会插入到执行队列中。

    • 它是回调函数的一个基础形式,但不支持链式操作,也不直接与Promise结合。

  2. Promise:

    • Promise 是一个对象,用于表示一个异步操作的最终完成(成功或失败)及其结果。它有三个状态:pending(等待中)、fulfilled(已完成)和rejected(已拒绝)。

    • 通过.then()和.catch()方法,可以链式处理异步操作的结果,支持错误处理。

    • Promise 提供了更优雅的方式来组织异步代码,避免了回调地狱。

  3. Async/Await:

    • async/await 是ES2017引入的语法糖,用于简化Promise的使用。async关键字定义了一个异步函数,而await关键字用于等待Promise的结果。

    • 通过await,异步操作可以在代码中看起来像是同步的,提高了代码的可读性。

    • async/await 结合了Promise的优点,同时提供了更接近同步代码的编写体验。

总的来说,setTimeout 是一个简单的异步延迟执行工具,Promise 用于管理和组合异步操作,而async/await 是Promise的高级语法,提供了更易读和易于管理的异步代码编写方式。

二、对Promise的理解?

Promise是JavaScript中用于处理异步操作的一种编程模式,它提供了一种更加结构化的方式来管理异步操作的结果,避免了传统的回调地狱,使得异步代码更加清晰和易于维护。

  1. 基本概念:

    • Promise代表一个异步操作的最终完成(或失败)及其结果的容器。它有三种状态:pending(进行中)、fulfilled(已成功,又称resolved)和rejected(已失败)。

    • 一旦Promise从pending变为fulfilled或rejected,其状态就不能再改变,这就是所谓的“状态不可逆”。

  2. 创建Promise:

    • 使用new Promise((resolve, reject) => {})构造函数创建Promise实例,其中resolve和reject是两个函数,分别用于标记操作成功或失败,并传递结果值。
  3. 链式调用:

    • 通过.then()方法处理成功的结果,通过.catch()处理失败的情况。.then()和.catch()返回新的Promise,允许进一步链式调用。

    • 另外,.finally()方法无论Promise成功还是失败都会执行,用于清理操作或执行最终的逻辑。

  4. 错误处理:

    • 错误可以通过.catch()捕获,它会在Promise链中任何地方抛出的错误处被捕获,包括前面的.then()或.catch()中的错误。
  5. 并发与组合:

    • 使用Promise.all()可以并行执行多个Promise,并在所有Promise都成功完成后解析结果数组。

    • Promise.race()则是只要任意一个Promise完成(不论是成功还是失败)就立即返回结果或错误。

  6. 静态方法:

    • Promise.resolve(value)用于直接创建一个已解决的Promise。

    • Promise.reject(reason)用于直接创建一个已拒绝的Promise。

  7. 异步编程基础:

    • Promise是现代JavaScript异步编程的基础,特别是与async/await语法一起使用时,可以编写看起来像同步代码的异步逻辑,极大地提高了代码的可读性和可维护性。

三、Promise.all和Promise.race的区别的使用场景?

Promise.all:

  • 用途:当需要等待多个异步操作都完成时使用。它接收一个Promise数组作为参数,只有当所有这些Promise都变为fulfilled状态时,Promise.all返回的Promise才会变为fulfilled状态,结果是一个包含所有Promise结果的数组。
  • 适用场景:
    • 页面加载需要多个独立数据源的数据全部准备好后才能渲染。
    • 批量上传文件,只有当所有文件上传成功后才通知用户。
    • 并行处理多个任务,且所有任务完成是后续逻辑的前提。

Promise.race:

  • 用途:当只需要关注多个Promise中的最快完成的一个(无论是fulfilled还是rejected状态)时使用。它同样接收一个Promise数组作为参数,一旦数组中的任何一个Promise改变状态(变为fulfilled或rejected),Promise.race返回的Promise就会立即以相同的状态结束。
  • 适用场景:
    • 设置超时机制,比如某个操作不能超过一定时间,可以用一个定时器的Promise与实际操作的Promise进行race,先完成的决定后续逻辑。
    • 在多个数据源中选择响应最快的,比如从多个CDN下载资源,选取最先返回的资源使用。
    • 竞争条件检查,比如在并发操作中,只关心谁先完成,不在乎其他。

总结:

  • Promise.all适用于需要确保所有操作都成功完成的场景,强调的是集体协作完成。
  • Promise.race则适用于竞争场景,关注的是速度和优先级,哪个Promise先有结果就采用哪个结果。

四、对async/await 的理解?

  1. async 函数:

    • async 关键字用于定义一个异步函数。这样的函数总是返回一个Promise,即使函数中没有明确的返回值,也会隐式返回一个解析为undefined的Promise。

    • 在async函数内部,可以使用await关键字。

  2. await 表达式:

    • await 用于等待一个Promise完成。它后面通常跟一个Promise,这个Promise的结果将被返回给表达式。如果Promise解析,await表达式的值就是Promise的结果;如果Promise被拒绝,await表达式将抛出一个错误。

    • await 只能在async函数内部使用。

  3. 错误处理:

    • 在async函数内部,await后面的Promise如果被拒绝,会抛出一个异常,可以使用try...catch来捕获这个异常。
  4. 返回值:

    • async函数返回一个Promise,这个Promise的解析值是await表达式的结果,如果await后面没有Promise,那么这个Promise的解析值就是undefined。
  5. 链式调用:

    • async函数可以像Promise一样链式调用,因为它们都返回Promise,所以可以使用.then()和.catch()来处理异步操作的结果。
  6. 同步代码的外观:

    • async/await 让异步代码看起来更像同步代码,提高了代码的可读性和可维护性。
  7. 并发与顺序:

    • 即使在async函数内部,await后面的Promise也可以并行执行,但是await后面的代码会等待Promise解析后再继续执行,确保了执行顺序。

五、async/await的优势?

  1. 代码可读性:async/await使得异步代码看起来更像同步代码,减少了回调函数的嵌套,从而降低了阅读和理解代码的难度。代码结构更加线性,易于跟踪和维护。
  2. 简洁的错误处理:通过使用标准的try...catch语句,async/await使得错误处理更加集中和直观,避免了Promise链中繁琐的.catch()调用,使得代码更加整洁。
  3. 更自然的控制流:在async函数中,可以使用await暂停函数执行,等待Promise解析,这使得编写复杂的异步逻辑变得更加自然,例如条件语句、循环等,就像处理同步代码一样。
  4. 中间值处理简化:与Promise相比,使用await可以直接获取Promise的结果,无需通过.then()传递中间值,使得处理异步操作的结果更加直接和简便。
  5. 调试友好:在async函数中,可以像调试同步代码那样设置断点,因为await会暂停函数执行直到Promise解决,这使得在调试异步代码时更容易追踪和理解程序的执行流程。
  6. 保留执行堆栈:当异步操作中发生错误时,async/await能够提供更完整的堆栈跟踪,帮助开发者更快定位问题,这是因为它们保留了函数调用的上下文。
  7. 原生支持和广泛兼容:现代浏览器和Node.js环境都原生支持async/await,这意味着不需要额外的编译步骤,同时也享受到了语言层面的优化和支持。

六、说说你对事件循环的理解?

  1. 首先,JavaScript 引擎会执行同步代码,将同步代码按顺序执行完。在执行同步代码的过程中,可能会产生异步任务,这些异步任务会根据其类型(宏任务或微任务)分别被添加到任务队列或微任务队列中。
  2. 当同步代码执行完毕后,引擎会检查微任务队列。如果微任务队列中有任务,就会按照先进先出的顺序逐个执行微任务,直到微任务队列为空。
  3. 微任务队列清空后,引擎会检查任务队列。如果任务队列中有任务,就会取出一个任务并执行,执行完这个任务后,又会检查微任务队列,看是否有新的微任务产生(因为执行宏任务的过程中可能会产生微任务),重复上述步骤。
  4. 这个过程会不断循环,直到任务队列和微任务队列都为空,这时 JavaScript 引擎就会等待新的任务进入队列。

编程语言特性

一、说说你对函数式编程的理解?优缺点?

对函数式编程的理解:

函数式编程把计算当作函数求值,函数可被传递、返回与作为参数。其核心概念包括不可变数据,即数据创建后不可修改,操作数据返回新副本;纯函数对于相同输入总返回相同输出且无副作用;还有函数组合,能将多个函数组合构建复杂功能。例如在 JavaScript 中,可轻松将函数作为参数传递给其他函数实现特定逻辑。

优点:

  • 可维护性高:纯函数行为可预测,无外部状态依赖与副作用,且复杂任务可分解为易理解与测试的小函数,修改代码时只需关注相关小函数。
  • 易于测试:纯函数仅依赖输入参数,无外部干扰,方便用单元测试工具测试,函数组合也利于对组合函数进行测试。
  • 并行处理能力:因无共享状态与可变数据,适合并行计算,多线程或分布式环境中各函数实例可独立处理数据,能充分利用多核性能。
  • 代码复用性强:纯函数不依赖特定上下文,可在多场景复用,函数组合也使复用更灵活,能快速构建新功能。

缺点:

  • 性能问题:不可变数据结构处理大量或频繁修改数据时,因创建副本会占用更多内存与资源,函数组合与柯里化等高级特性也可能有额外函数调用开销。
  • 生态系统和工具支持相对较弱:相比成熟编程范式,在一些领域工具与库支持不够完善,新兴技术缺乏足够社区支持与最佳实践案例,应用时开发人员易遇困难。

二、解释 JavaScript 严格模式的概念和作用。启用严格模式后,会对代码产生哪些影响?

JavaScript 严格模式是 ECMAScript 5 引入的一种运行模式,通过在脚本或函数开头添加 "use strict"; 来启用。

它的主要作用在于:一是能捕获常见错误,让开发者更早发现代码问题,比如使用未声明变量这种在非严格模式下可能被忽略的情况,在严格模式下会抛出 ReferenceError;二是防止不安全操作,像禁止删除不可配置属性,否则会抛出 TypeError;三是利于 JavaScript 引擎优化,限制一些复杂难优化的语法和行为,提升执行效率。

启用严格模式后对代码有诸多影响:变量必须先声明再使用;函数参数不能同名,否则抛出 SyntaxError;函数内部 this 不再默认绑定全局对象,无显式绑定时为 undefined;eval() 有自己独立作用域,内部声明变量不影响外部;对象字面量属性名不能重复,否则也会抛出 SyntaxError。总之,严格模式有助于提高代码质量、安全性与执行性能,是 JavaScript 开发中重要的规范机制。

高级特性

原型链

一、对原型、原型链的理解?

原型:每个JavaScript对象(除null外)都有一个内置的属性,被称为原型,通常可以通过__proto__访问。这个原型属性是一个指针,指向另一个对象,这个对象就是当前对象的原型对象。原型对象可以拥有自己的属性和方法,当尝试访问一个对象的属性或方法时,如果在该对象自身找不到,则会沿着原型链向上查找,直至找到该属性或方法,或者到达原型链的末端(通常是Object.prototype)。

原型链:是由一系列对象通过它们的原型属性连接起来的链式结构。当试图访问一个对象的属性或方法时,如果该对象本身没有定义,JavaScript引擎会继续在其原型对象中查找,如果原型对象也没有定义,则继续在原型的原型中查找,以此类推,直到找到该属性或方法,或者到达原型链的终点(null),此时返回undefined表示未找到。

模块化

一、ES6模块与CommonJS模块有什么异同?

相同点:

  1. 代码模块化,解决代码变量名冲突、难以维护等问题

不同点:

  1. 模块加载时机:
    1. CommonJS 模块是运行时加载,模块在被运行时才会被加载和执行。
    2. ES6模块是静态加载,也是编译时加载。在代码的编译阶段就会确定模块的依赖关系。这使得在代码执行之前就能进行模块的优化,如确定模块的加载顺序、对模块进行静态分析等。
  2. 语法差异:
    1. CommonJS 模块使用require()函数来引入模块,用module.exports或exports对象来导出模块内容。
    2. ES6 模块使用import关键字来引入模块,export关键字来导出模块。有多种导出和导入的方式,如默认导出(export default)和命名导出(export)。

性能优化

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

  1. defer属性:当在<script>标签中添加defer属性后,浏览器会继续解析HTML文档,而不会阻塞文档的解析。脚本会在文档解析完成后,按照在文档中出现的顺序依次执行。
  2. async属性:async属性也能让脚本异步加载。和defer不同的是,async加载的脚本一旦加载完成,就会立即执行。主要用于那些不依赖于页面其他内容的独立脚本,例如一些统计脚本或者广告脚本。
  3. 动态创建脚本元素:通过JavaScript动态创建<script>元素,并且将其添加到文档中。这种方式可以更加灵活地控制脚本的加载时机,例如在某些用户交互事件发生后或者满足一定条件后再加载脚本。
  4. 使用模块加载器,实现模块的异步加载。

二、哪些情况会导致内存泄漏?

内存泄漏指的是程序分配的内存没有被适时释放,导致可用内存逐渐减少,可能最终影响程序性能甚至导致程序崩溃。以下是几种常见的导致内存泄漏的情况:

  1. 全局变量:未及时解除对全局变量的引用,使得本应被释放的对象一直被引用,无法被垃圾回收。
  2. 闭包:当闭包中引用了外部函数的变量,且这个外部变量所占用的内存空间很大或者生命周期很长时,若闭包长时间存在,则可能导致这部分内存无法被释放。
  3. 事件监听器:未移除的事件监听器,特别是当监听器绑定到DOM元素上,而该DOM元素又未被适当清理时,会导致相关对象和它们所引用的内存无法被回收。
  4. 定时器(setTimeout, setInterval):如果定时器启动后没有被正确清除,它们会持续引用相关的回调函数及其作用域链上的变量,阻止这些对象被回收。
  5. 未清理的DOM元素引用:即使DOM元素从页面中移除,但如果JavaScript代码中仍有对该元素的引用,该元素及其相关子元素所占用的内存就不会被释放。
  6. 缓存未限制:无限制地缓存数据或对象,如果缓存策略不当,可能会无限积累,占用越来越多的内存。
  7. 单例和静态变量:单例对象和静态变量的生命周期与应用程序相同,如果它们不慎持有其他对象的引用,可能导致这些对象无法被回收。
  8. 循环引用:特别是在使用一些语言特性如JavaScript的原型链或复合对象时,对象之间形成循环引用,而这些对象又不再被外部所引用,也可能导致内存泄漏。
  9. 第三方库或框架使用不当:不正确地使用第三方库或框架也可能导致内存泄漏,尤其是在它们管理资源或生命周期方面。

三、什么是防抖和节流,有什么区别?

  • 防抖是指在事件被触发 n 秒后再执行回调函数,如果在这 n 秒内事件又被触发,则重新计时。就像是等电梯,有人按了电梯按钮(触发事件),电梯会等待一段时间(n 秒)才关门出发,如果在这等待期间又有人按按钮,那么等待时间就会重新计算。
  • 节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在这个单位时间内触发多次事件,只有一次能生效。可以想象成水龙头放水,不管你怎么频繁地拧水龙头(触发事件),水(回调函数执行)都是按照一定的速率流出来。

区别:

  1. 执行次数不同:

    • 防抖:在事件持续触发的过程中,回调函数可能一直不会执行,只有当事件触发停止后,并且在规定的等待时间内没有再次触发,回调函数才会执行一次。

    • 节流:在规定的单位时间内,无论事件触发多少次,回调函数最多只执行一次。在持续触发事件的过程中,会按照固定的时间间隔执行回调函数。

  2. 应用场景侧重点不同

    • 防抖:更侧重于在事件停止触发后的一次性操作,主要用于一些 “输入完成后” 的操作,如用户输入完成后的搜索、用户停止拖拽后的操作等。

    • 节流:侧重于在事件持续触发过程中,按照一定的频率来执行操作,适用于需要持续监听事件,但又不能让事件处理函数过于频繁执行的场景,如滚动加载、鼠标跟踪等。