10道经典JavaScript面试题解析:从作用域到算法优化

作者:异步漫步时间:2026-10-21 23:58:32浏览:668

本文深入解析10道高频JavaScript面试题,涵盖作用域规则、变量提升机制、this绑定逻辑、闭包应用及事件循环原理。重点讲解如何通过call/apply修正上下文,利用IIFE解决循环闭包问题,并详细剖析质数判断函数的输入校验与性能优化策略,帮助开发者巩固核心概念。

作用域与变量提升机制

问题1:作用域(Scope)

请分析以下代码片段:


(function() {
 var a = b = 5;
})();
console.log(b);

控制台(console)最终会输出什么内容?

输出结果为:5

若代码启用了严格模式('use strict'),执行时将抛出错误“Uncaught ReferenceError: b is not defined”。在严格模式下,若需引用全局作用域,必须显式使用全局对象引用。因此,修正后的写法应如下所示:


(function() {
 'use strict';
 var a = window.b = 5;
})();
console.log(b);

原生方法扩展与原型链保护

问题2:扩展“原生(native)”方法

请在 String 原型上定义一个名为 repeatify 的方法。该方法接收一个整数参数,用于指定字符串重复的次数。例如:

console.log('hello'.repeatify(3));

预期控制台输出应为:hellohellohello.

参考实现如下:


String.prototype.repeatify = String.prototype.repeatify || function(times) {
 var str = '';
 for (var i = 0; i < times; i++) {
  str += this;
 }
 return str;
};

此处需注意避免覆盖已存在的方法。最佳实践是在定义新方法前,先检测该方法是否已存在于原型中。

String.prototype.repeatify = String.prototype.repeatify || function(times) {/* code here */};

变量提升与函数声明行为

问题3:变量提升(Hoisting)

执行以下代码后,控制台会输出什么?请说明原因。


function test() {
 console.log(a);
 console.log(foo());
 var a = 1;
 function foo() {
  return 2;
 }
}
test();

预期输出结果:

实际输出为 undefined 和 2。

原因在于 JavaScript 中的变量声明和函数定义会被提升至其所在作用域的顶部。因此,当打印变量 a 时,虽然它已在函数作用域内声明(故未报错),但其值尚未赋值,仍为 undefined。上述代码在逻辑上等价于:


function test() {
 var a;
 function foo() {
  return 2;
 }
 console.log(a);
 console.log(foo());
 a = 1;
} 
test();

this 关键字的上下文绑定与修正

问题4:JavaScript 中 this 关键字的工作原理

以下代码的执行结果是什么?请解释原因。


var fullname = 'John Doe';
var obj = {
 fullname: 'Colin Ihrig',
 prop: {
  fullname: 'Aurelio De Rosa',
  getFullname: function() {
   return this.fullname;
  }
 }
};
console.log(obj.prop.getFullname());
var test = obj.prop.getFullname;
console.log(test());

预期输出结果:

代码输出结果为:Aurelio De Rosa 和 John Doe。这是因为 JavaScript 中的 this 关键字指向函数被调用时的上下文,而非定义时的上下文。

在第一个 console.log() 中,getFullname() 是作为 obj.prop 对象的属性被调用的,因此 this 指向该对象,返回其 fullname 属性。而在第二个 console.log() 中,getFullname() 被赋值给变量 test 后调用,此时 this 指向全局对象 window(因为 test 是全局作用域的变量),因此返回 window 的 fullname 属性,即第一行代码设置的值。

问题5:call() 和 apply() 方法的应用

修复上述问题,使最后一个 console.log() 输出 Aurelio De Rosa。

参考实现:

可以通过调用 call() 或 apply() 方法,强制指定函数执行时的上下文环境来解决此问题。

console.log(test.call(obj.prop));

闭包陷阱与立即执行函数解决方案

问题6:闭包(Closures)

考虑以下代码逻辑:


var nodes = document.getElementsByTagName('button');
for (var i = 0; i < nodes.length; i++) {
 nodes[i].addEventListener('click', function() {
  console.log('You clicked element #' + i);
 });
}

如果用户依次点击第一个和第四个按钮,控制台分别会打印什么?请解释原因。

预期输出结果:

两次点击均会打印 nodes.length 的值。这是因为循环结束后,变量 i 的值固定为 nodes.length,而所有按钮的点击事件监听器共享同一个闭包环境,引用的是最终的 i 值。

问题7:闭包(Closures)的解决方案

修复上一题的问题,使得点击第一个按钮输出 0,点击第二个按钮输出 1,依此类推。

参考实现:

解决此问题有多种方案,主要介绍以下两种。

第一种方案利用立即执行函数表达式(IIFE)创建一个新的闭包,从而捕获当前循环变量 i 的值。实现代码如下:


var nodes = document.getElementsByTagName('button');
for (var i = 0; i < nodes.length; i++) {
 nodes[i].addEventListener('click', (function(i) {
  return function() {
   console.log('You clicked element #' + i);
  }
 })(i));
}

第二种方案不使用 IIFE,而是将事件处理函数提取到循环外部。实现代码如下:


function handlerWrapper(i) {
 return function() {
  console.log('You clicked element #' + i);
 }
}
var nodes = document.getElementsByTagName('button');
for (var i = 0; i < nodes.length; i++) {
 nodes[i].addEventListener('click', handlerWrapper(i));
}

数据类型检测与事件循环原理

问题8:数据类型检测

分析以下代码的输出:


console.log(typeof null);
console.log(typeof {});
console.log(typeof []);
console.log(typeof undefined);

预期输出结果:


object
object
object
undefined

问题9:事件循环(Event Loop)机制

以下代码的运行结果是什么?请详细解释执行顺序。


function printing() {
 console.log(1);
 setTimeout(function() { console.log(2); }, 1000);
 setTimeout(function() { console.log(3); }, 0);
 console.log(4);
}
printing();

预期输出结果:


1
4
3
2

要理解输出顺序,需掌握 setTimeout() 的工作原理及浏览器的事件循环机制。浏览器维护一个事件队列,用于处理 UI 事件(如点击、滚动)、Ajax 回调以及 setTimeout/setInterval 的回调。当调用 setTimeout() 时,即使延迟时间设为 0,其回调函数也会被放入队列中等待执行。只有当当前执行栈清空且时间到达后,引擎才会从队列中取出回调执行。因此,即使延迟为 0,该回调也会排在同步代码之后执行。

质数判断算法与性能优化策略

问题10:算法实现

编写一个 isPrime() 函数,当输入数字为质数时返回 true,否则返回 false。

参考实现:

这是面试中极常见的问题。尽管题目简单,但通过回答质量可以考察候选人的数学基础和算法优化意识。

首先,JavaScript 不像 C 或 Java 那样有严格的类型系统,因此不能盲目信任传入参数的类型。若面试官未明确说明,应主动询问是否需要进行输入校验。从严谨性角度,必须对输入进行检查。

第二点需牢记:负数非质数。同理,1与0亦非质数,故首要步骤即检验此类数值。此外,2为质数中唯一偶数。无需通过循环验证4、6、8等数。再者,若某数无法被2整除,则必然无法被4、6、8等偶数整除。因此,循环应跳过此类数字。若对输入偶数进行测试,算法速度将减半(即测试双倍数量的数字)。可采取其他更明智的优化策略,此处采用的方案适用于大多数场景。例如,若某数无法被5整除,则其倍数(如10、15、20等)亦无需检测。

最后,循环终止条件无需检查超过输入数字平方根的因子。展示这一优化点虽不扣分,但能体现候选人的算法素养,获得额外加分。

综合上述考量,完整的优化解决方案如下:


function isPrime(number) {
 // If your browser doesn't support the method Number.isInteger of ECMAScript 6,
 // you can implement your own pretty easily
 if (typeof number !== 'number' || !Number.isInteger(number)) {
  // Alternatively you can throw an error.
  return false;
 }
 if (number < 2) {
  return false;
 }
 if (number === 2) {
  return true;
 } else if (number % 2 === 0) {
  return false;
 }
 var squareRoot = Math.sqrt(number);
 for(var i = 3; i <= squareRoot; i += 2) {
  if (number % i === 0) {
   return false;
  }
 }
 return true;
}

以上内容涵盖了本文的核心知识点,希望对各位的学习或工作有所帮助。欢迎继续支持本站获取更多技术干货!

作者最新文章