avatar
【面试精选】JavaScript常见面试题
🔠

【面试精选】JavaScript常见面试题

published_date
最新编辑 2024年09月08日
slug
JavaScript基础、ES6+特性、以及现代JavaScript最佳实践的常见面试题及解答
tags
JavaScript
面试

1、JavaScript 的数据类型有哪些?

8 种基本数据类型(原始类型)和 1 种引用类型:
  • 基本数据类型(Primitive Types):
      1. string
      1. number
      1. boolean
      1. null
      1. undefined
      1. symbol
      1. bigint
  • 引用类型(Reference Type): 8. object(包括数组、函数等)

2、nullundefined 的区别是什么?

  • null: 是一个表示“没有值”的特殊对象,通常用于显式地赋值给一个变量,表示这个变量目前没有对象。
  • undefined: 表示变量未定义或未赋值,是变量的默认值。
区别:
  • 类型不同:typeof null"object"typeof undefined"undefined"
  • 语义不同:null 是一种占位符,表示变量有意地没有值;undefined 则表示未初始化的变量。

3. callapplybind 的区别是什么?

this 关键字

在 JavaScript 中,this 关键字的值取决于它的上下文(也就是它被调用的方式)。callapplybind 方法都可以显式地指定函数调用时的 this 值。

call 方法

  • 用法: function.call(thisArg, arg1, arg2, ...)
  • 功能: 调用一个函数,并显式地设置 this 的值,同时逐个传入参数
  • 特点:
    • call 方法立即调用函数
    • this 的值是第一个参数 thisArg,后续参数作为函数的参数依次传递
//greet 函数通过 call 方法调用时,this 被显式地设置为 person 对象 function greet(greeting, punctuation) { console.log(greeting + ', ' + this.name + punctuation); } const person = { name: 'Alice' }; // 使用 call 方法,设置 this 为 person 对象,并传递两个参数 greet.call(person, 'Hello', '!'); // 输出: Hello, Alice!

apply 方法

  • 用法: function.apply(thisArg, [argsArray])
  • 功能: 调用一个函数,并显式地设置 this 的值,同时以数组形式传入参数。
  • 特点:
    • apply 方法立即调用函数
    • this 的值是第一个参数 thisArg,第二个参数是一个数组,作为函数的参数传递。
function greet(greeting, punctuation) { console.log(greeting + ', ' + this.name + punctuation); } const person = { name: 'Alice' }; // 使用 apply 方法,设置 this 为 person 对象,并传递参数数组 greet.apply(person, ['Hi', '?']); // 输出: Hi, Alice?

bind 方法

  • 用法: function.bind(thisArg, arg1, arg2, ...)
  • 功能: 创建一个新的函数,在调用时强制其 this 的值为指定的对象,同时可以预先传入部分参数。
  • 特点:
    • bind 方法不会立即调用函数,它返回一个新的函数。
    • 新函数的 this 值是 bind 方法的第一个参数 thisArg,并且可以在绑定时传入部分参数。
    • 新函数可以像普通函数一样继续传递其他参数。
function greet(greeting, punctuation) { console.log(greeting + ', ' + this.name + punctuation); } const person = { name: 'Alice' }; // 使用 bind 方法,创建一个新的函数,将 this 绑定为 person 对象,并预设第一个参数 const greetAlice = greet.bind(person, 'Hey'); // 调用新的函数时,只需要传递剩余的参数 greetAlice('!'); // 输出: Hey, Alice!
方法
是否立即调用
参数形式
返回值
适用场景
call
逐个参数传入
函数执行结果
当需要调用函数,并设置 this 值时使用。
apply
参数以数组形式传入
函数执行结果
当需要调用函数,并设置 this 值,且参数以数组形式传递时使用。如对象方法借用或借用构造函数。
bind
逐个参数传入或部分参数预设
绑定了 this 和部分参数的新函数
常用于函数需要在稍后调用且希望永久绑定 this 的值的场景,如事件处理器或回调函数。

4、let、var、const的区别?

作用域
提升
可变性
var
声明的变量具有函数作用域,即它在函数内部声明的变量只在该函数内部可见;如果在函数外部声明,则是全局作用域。
var 声明的变量会被提升到函数或全局作用域的顶部,但赋值不会被提升。也就是说,可以在声明之前访问变量(但值是 undefined)
var 声明的变量可以被重新赋值,也可以被重新声明
let
let 声明的变量具有块级作用域,即它在块级(如 if 语句或循环)内部声明的变量只在该块内有效。
let 声明的变量会被提升,但不会被初始化。这意味着在声明之前访问该变量会导致 ReferenceError。
let 声明的变量可以被重新赋值,但不能被重新声明(在同一作用域内)。
const
const 声明的变量也具有块级作用域(block scope),与 let 相同。
const 声明的变量会被提升,但不会被初始化。这意味着在声明之前访问该变量会导致 ReferenceError。
const 声明的变量必须在声明时初始化,并且一旦赋值后,不能重新赋值(不可变)。但是,对于对象或数组,const 只保证变量绑定不变,对象的内容(如属性或元素)是可以改变的。

5、闭包

它指的是一个函数能够访问它外部的变量,即使这个函数在它的外部环境执行。
要理解闭包,我们需要先了解以下几个关键点:
  • 词法作用域(Lexical Scope)
    • JavaScript 使用的是词法作用域规则,这意味着函数的作用域在函数定义时就确定了,而不是在函数调用时。一个函数可以访问它定义时所在的作用域中的变量。
  • 函数嵌套
    • 在 JavaScript 中,我们可以在一个函数内部定义另一个函数。内层函数可以访问外层函数的变量。
  • 变量的生存期
    • 通常情况下,当一个函数执行完毕后,它的局部变量就会被销毁,因为它们的作用域仅限于这个函数内部。然而,如果有其他函数引用了这些局部变量,那么这些变量就会继续存在于内存中。

什么是闭包?

闭包 是指 函数和其定义时的词法环境的组合。也就是说,闭包可以“记住”它创建时所处的环境,即使在闭包被创建的环境之外执行。
🌰例子:
// outerFunction 是一个外部函数,它定义了一个局部变量 outerVariable 和一个内部函数 innerFunction function outerFunction() { let outerVariable = 'I am outside!'; //innerFunction 是一个闭包,因为它访问了外部函数 outerFunction 中的 outerVariable 变量。 function innerFunction() { console.log(outerVariable); } // 当 outerFunction 被调用时,它返回了 innerFunction(没有执行它,而是返回它的引用)。 return innerFunction; } // closure 变量保存了 innerFunction 的引用 const closure = outerFunction(); //当我们调用 closure() 时,实际上调用的是 innerFunction,它依然可以访问 outerVariable,即使 outerFunction 已经执行完毕并返回了。 closure(); // 输出: I am outside!

闭包应用

闭包的主要优势在于它们使得函数能够“记住”它们的词法作用域,即使在作用域链已经发生改变的情况下。闭包可以用来:
  • 数据封装:闭包可以隐藏一个函数的实现细节,只暴露想要公开的接口。
//count 变量无法从外部访问,只能通过 increment 函数访问,这就实现了封装。 function counter() { let count = 0; return function() { count++; console.log(count); }; } const increment = counter(); increment(); // 输出: 1 increment(); // 输出: 2 increment(); // 输出: 3
  • 模拟私有变量:做函数工厂,创建多个类似的函数,每个函数都拥有自己的私有变量
  • 回调函数和事件处理器:在异步编程(例如处理事件或 HTTP 请求)中,闭包非常有用,它们使得回调函数能够访问到在事件触发时所需的变量。

6、原型链

原型Prototype)是每个JS对象的内部属性,用于实现继承。JS中的所有对象都有一个[[Prototype]](除了一些特殊情况),它要么是另一个对象,要么是 null。
原型链 是多个对象之间的继承关系,它允许一个对象访问另一个对象的属性和方法。如果一个对象的属性或方法在自身找不到,JS 会沿着原型链往上查找,直到找到或到达链的末端(null)。
function Person(name) { this.name = name; } Person.prototype.sayHello = function() { console.log('Hello, ' + this.name); }; const john = new Person('John'); john.sayHello(); // Hello, John
在这个例子中,john 对象没有 sayHello 方法,所以它会沿着原型链查找到 Person.prototype,找到 sayHello 并执行。

7、事件循环

事件循环是JS处理异步操作的机制,在执行代码时,JS引擎将同步任务推入调用栈中执行,异步任务则放入任务队列中,任务队列中的任务在调用栈空闲时被放入调用栈执行。
💡
JS是单线程的,同一个时间只能执行一个任务,但它能处理异步任务而不阻塞主线程的执行

调用栈

  • 调用栈是一个 LIFO(Last In, First Out)结构,用来管理代码中正在执行的函数。
  • 每当一个函数被调用时,它会被推入栈顶,当函数执行完毕后,它会从栈中弹出。
  • 如果调用栈中有任务在执行,JavaScript 引擎将无法执行其他代码。

任务队列

任务队列是一个先进先出(FIFO)结构,用于存放已经准备好执行的回调函数。 当调用栈为空时,事件循环会检查任务队列,如果任务队列中有任务,则将其推入调用栈并开始执行
  • 微任务:微任务包括 Promise.then 回调、MutationObserver 等。微任务队列的优先级高于宏任务,每当一个宏任务执行完毕后,事件循环会首先清空所有的微任务队列,然后再开始下一个宏任务。
  • 宏任务:宏任务包括主代码块(script)、setTimeout、setInterval、I/O 操作等。宏任务执行时,事件循环会依次执行任务队列中的任务。

8、JS性能优化

减少 HTTP 请求

  • 合并文件: 将多个 CSS、JavaScript 文件合并成一个文件,以减少 HTTP 请求的次数。
  • CSS 雪碧图(Sprites): 将多个小图标合并成一张图片,通过 CSS 控制背景位置来显示不同的图标。

使用内容分发网络(CDN)

  • CDN 加速: 使用 CDN 将静态资源(如图片、CSS、JavaScript)分发到多个服务器节点,用户可以从距离最近的节点获取资源,减少加载时间。

资源压缩与缩小

  • 文件压缩: 使用 Gzip 或 Brotli 对 HTML、CSS、JavaScript 文件进行压缩,以减少文件体积。
  • 代码缩小(Minification): 使用工具(如 Terser、UglifyJS)去除 JavaScript 和 CSS 文件中的空格、注释、无用代码等,进一步减小文件体积。

图片优化

  • 图像格式: 使用适当的图片格式,如使用 WebP 替代传统的 JPEG 和 PNG 格式,因为 WebP 体积更小且支持有损和无损压缩。
  • 图像压缩: 使用工具(如 ImageOptim、TinyPNG)对图片进行无损或有损压缩,减小图片体积。
  • 响应式图片: 根据不同的设备和屏幕大小,提供不同分辨率的图片,以减少不必要的带宽消耗。

延迟加载(Lazy Loading)

  • 图片和视频: 对页面中的图片和视频使用延迟加载技术,只有在用户滚动到这些资源可见时才进行加载,以减少初始页面加载时间。
  • JavaScript: 对不需要立即执行的 JavaScript 文件使用 async 或 defer 属性,使其在不阻塞 HTML 解析的情况下异步加载。

缓存策略

  • 浏览器缓存: 利用浏览器缓存,通过设置适当的缓存头(如 Cache-Control、ETag),减少重复加载相同的资源。
  • 服务端缓存: 使用服务端缓存(如 Redis、Varnish)来缓存动态生成的内容,减少服务器的计算压力。

代码分割

  • 按需加载: 使用 Webpack 等打包工具进行代码分割,将应用程序分成多个代码块,按需加载用户当前需要的代码,减少初始加载时间。
  • 动态导入: 对于大型应用,可以使用动态导入(import())来异步加载模块,进一步优化资源加载。

减少重绘与回流

  • 减少 DOM 操作: 批量更新 DOM,避免频繁的 DOM 操作,因为每次 DOM 变化都会导致重绘和回流,影响页面性能。
  • CSS 优化: 尽量避免使用对性能影响较大的 CSS 属性(如 float、position: absolute)和复杂的选择器。

预加载与预渲染

  • 预加载(Preload): 使用 <link rel="preload"> 标签预加载关键资源,如字体、关键 CSS 和 JavaScript 文件,以确保这些资源在页面渲染前已被加载。
  • 预渲染(Prerender): 使用 <link rel="prerender"> 标签提前加载和渲染用户可能访问的页面,以加快用户点击后页面的显示速度。

减少第三方代码的使用

  • 优化第三方库: 使用体积更小的第三方库或自行实现一些轻量级的功能,减少加载不必要的第三方代码。
  • 异步加载第三方脚本: 对第三方的广告、社交分享、分析代码进行异步加载,避免其影响页面主线程的执行。

性能监控与分析

  • 工具使用: 使用 Lighthouse、Chrome DevTools、WebPageTest 等工具对页面性能进行分析,找出性能瓶颈并进行针对性的优化。
  • 实时监控: 实施性能监控方案(如 Google Analytics、New Relic),实时监控用户的实际加载时间和交互性能,及时发现和解决问题。
 

About

My desire to practice my skills and share my acquired knowledge fuels my endeavors.

Contact Me : znjessie858@gmail.com

Subscribe

Subscribe to receive notifications of my latest posts and unsubscribe at any time!