10道经典JavaScript面试题解析:从作用域到算法优化
本文深入解析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;
}
以上内容涵盖了本文的核心知识点,希望对各位的学习或工作有所帮助。欢迎继续支持本站获取更多技术干货!
D3.js 比例尺与坐标轴的配置详解
2026-11-05 16:54:46
JavaScript怎么遍历对象的Symbol键和不可枚举属性
2026-11-05 13:13:39
封装一个健壮的JS字符串判空工具函数
2026-11-05 12:16:14
JavaScript合并数组的三种常用方法:concat、扩展运算符与push
2026-11-05 11:22:29
JavaScript立即执行函数(IIFE)是什么及基本语法
2026-11-05 10:51:19
相关标签:
