一、js函数面试题总览:高频考点与学习路径
在当前前端技术栈快速演进的背景下,JavaScript函数作为语言核心范式之一,是各类技术面试中不可或缺的考察重点。从初级岗位到资深工程师,js函数面试题贯穿整个技术评估体系,其考察维度不断延伸,从基础语法到函数式编程思想,从作用域机制到性能优化策略。
根据2024年国内主流招聘平台数据显示,超过87%的前端岗位在技术一面中会涉及函数相关问题。本页面系统整理了近年来真实企业面试题库,结合浏览器引擎实现原理与ES规范演进,为求职者提供可落地的知识图谱与解题思路。
我们建议采用“三层递进式”学习路径:
- 第一层:语法与机制——掌握函数定义方式、参数传递、作用域链、this绑定等基础机制;
- 第二层:运行时行为——理解执行上下文、变量提升、闭包形成原理、原型查找链;
- 第三层:工程化应用——高阶函数、柯里化、偏函数、函数式编程范式在实际项目中的实践。
以下按技术难度与考察频率分模块展开,每节均配备:① 真实面试题原文② 核心考点拆解③ 深度原理延伸④ 最佳实践建议。
二、基础语法与定义方式:ES5到ES6的演进
1. 函数声明 vs 函数表达式 vs 箭头函数
这是最基础但极易混淆的问题,常出现在初级面试官的“热身题”中。请分析以下代码输出结果:
// 问题1:函数声明
console.log(a());
function a() { return "声明函数"; }
// 问题2:函数表达式
console.log(b());
var b = function() { return "表达式函数"; }
【标准答案】:第一行输出"声明函数",第二行报错"TypeError: b is not a function"。
【原理解析】:函数声明具有“函数提升”(Function Hoisting),在预编译阶段整个函数体被提升至作用域顶部;而函数表达式中,仅变量b被提升(值为undefined),赋值操作保留在原位置,导致调用时b仍为undefined。
【延伸考点】:ES6引入的let/const无变量提升,若将var改为const,则报错"ReferenceError: Cannot access 'b' before initialization"。
【企业真题】(某大厂一面):
const obj = {
foo: function() { console.log(1); },
bar: () => console.log(2),
baz() { console.log(3); }
};
obj.foo();
obj.bar();
obj.baz();
【答案】:依次输出1、2、3。三种写法本质等价,但箭头函数无自身this绑定,而前两者均有独立的[[Environment]]环境引用。
2. 参数传递机制:按值传递的真相
JavaScript中所有参数传递均为“按值传递”(Call by Value),但需注意“值”的含义:基本类型传递原始值,引用类型传递引用地址值。
function modify(obj, val) {
obj.name = "changed";
val = 100;
}
let a = { name: "original" };
let b = 10;
modify(a, b);
console.log(a.name, b); // 输出:changed 10
【关键点】:函数内修改obj.name影响外部对象,但重新赋值val不影响外部变量;这印证了“传递的是地址值的副本”,而非“引用传递”(Call by Reference)。
【企业真题】(某独角兽企业二面):
function changeArg({ a, b }) {
a = 2;
b.c = 3;
}
let obj = { a: 1, b: { c: 1 } };
changeArg(obj);
console.log(obj); // 输出:{ a: 1, b: { c: 3 } }
【陷阱解析】:解构参数{a, b}创建了新对象,但a为基本类型值拷贝,b为对象引用地址拷贝,故仅b.c可修改。
高频易错点
- 将
arguments误认为数组(实际为类数组对象) - 在严格模式下,函数参数默认为严格模式(ES6+)
- 箭头函数不能用作构造函数(无
[[Construct]]内部方法) - 函数声明在
if块内存在浏览器兼容性差异(ES5不支持块级函数)
命名规范
- 动词优先:如
getName()、isValid() - 避免缩写:用
calculateTotal而非calcTot - 布尔函数用
is/has前缀:如isFinite、hasOwnProperty - 避免保留字:如
class、interface等
【企业实践】:阿里前端规范要求所有异步函数以async开头(如asyncGetData),但ES7+中async function声明已强制语义标识,可简化命名。
三、作用域与闭包:理解执行上下文的深度机制
1. 作用域链形成原理
当函数嵌套定义时,内部函数可访问外部函数变量,形成“作用域链”。该链在函数创建时即确定(静态作用域),而非调用时。
function outer() {
let x = 10;
function inner() {
console.log(x);
}
return inner;
}
let fn = outer();
fn(); // 输出:10
【关键机制】:inner函数的[[Environment]]指向outer的词法环境;即使outer已执行完毕,inner仍持有对x的引用,防止垃圾回收。
【企业真题】(某云服务商笔试):
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
【答案】:三次输出均为3。因var声明的i为全局变量,setTimeout回调函数共享同一i引用。
【优化方案】:
// 方案1:使用let(块级作用域)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 方案2:立即执行函数(IIFE)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
2. 闭包的副作用与内存泄漏
闭包可能导致内存无法释放,尤其在DOM操作中需警惕循环引用问题:
function bindEvent() {
let el = document.getElementById('btn');
el.onclick = function() {
console.log('clicked');
};
// 若未移除事件监听器,el引用函数,函数引用el → 内存泄漏
}
【解决方案】:
- 使用
WeakMap存储DOM引用 - 在组件卸载时手动解绑事件
- 避免在闭包中直接引用大型对象
【现代框架实践】:React/Vue通过虚拟DOM和自动清理机制降低泄漏风险,但复杂状态管理仍需开发者关注闭包生命周期。
"use strict"提升代码可预测性。
let/const使{}形成独立作用域,与函数作用域并存;let声明的变量无变量提升。
import/export创建独立模块作用域,变量默认严格模式;let在模块顶层行为与var一致。
四、this指向:JavaScript最复杂的动态绑定机制
1. this绑定四规则
JavaScript中this的绑定取决于函数调用方式,而非定义位置:
| 调用方式 | this指向 | 示例 |
|---|---|---|
| 普通函数调用 | 全局对象(非严格模式)或undefined(严格模式) |
fn() |
| 作为对象方法调用 | 调用该方法的对象 | obj.method() |
| 构造函数调用 | 新创建的实例对象 | new Fn() |
| 显式绑定(call/apply/bind) | 传入的第一个参数 | fn.call(obj) |
【企业真题】(某大厂终面):
const obj = {
x: 1,
getX: function() {
return this.x;
}
};
const unbound = obj.getX;
console.log(unbound()); // 严格模式下输出undefined
【解析】:unbound脱离对象调用,this未绑定,故在严格模式下为undefined。
2. 箭头函数的this继承机制
箭头函数无自身this,其this词法继承自外层作用域:
const obj = {
x: 1,
getX: () => this.x
};
console.log(obj.getX()); // undefined(全局this.x为undefined)
【陷阱解析】:箭头函数在对象字面量中定义时,其外层作用域是全局环境,而非obj。
【正确用法】:箭头函数适用于回调函数(如setTimeout、事件监听器),避免var self = this的变通写法:
setTimeout(() => {
console.log(this.value); // 继承外层this
}, 100);
【性能提示】:箭头函数无arguments对象,需用剩余参数...args替代;其prototype为undefined,不可用作构造函数。
高频场景
- 回调函数丢失this:如
array.map(this.handle)需绑定.bind(this) - 事件监听器:DOM事件回调中
this指向触发元素 - 解构赋值:
const {method} = obj后调用method()导致this丢失 - 嵌套箭头函数:多层箭头函数共享同一this引用
框架差异
- Vue 2:选项式API中
methods的this自动绑定实例; Composition API使用ref替代reactive避免this问题 - Vue 3:Composition API彻底规避this,通过
setup()返回响应式变量 - React:类组件需绑定
this,函数组件+Hooks完全避免this - Node.js:模块顶层
this指向module.exports,非全局对象
五、原型与原型链:面向对象的基础机制
1. 原型继承的三种实现方式
【问题】:如何让B继承A的属性与方法?
// 方式1:构造函数继承(借用call)
function A(name) { this.name = name; }
function B(name, age) {
A.call(this, name);
this.age = age;
}
// 方式2:原型链继承(替换原型)
function C() {}
C.prototype = new A();
C.prototype.constructor = C;
// 方式3:组合继承(最优解)
function D(name) { this.name = name; }
D.prototype.sayHi = function() { console.log('Hi, ' + this.name); }
function E(name, age) {
D.call(this, name); // 实例属性继承
this.age = age;
}
E.prototype = new D(); // 原型方法继承
【关键问题】:方式2中C.prototype = new A()会执行A的构造函数,若A有副作用(如DOM操作)则产生非预期行为。
2. ES6 class语法的本质
class是语法糖,底层仍基于原型链:
class Person {
constructor(name) { this.name = name; }
sayHi() { console.log('Hi, ' + this.name); }
}
// 等价于:
function Person(name) { this.name = name; }
Person.prototype.sayHi = function() { console.log('Hi, ' + this.name); }
【面试陷阱】:class声明无变量提升(与let一致),必须先定义后使用。
| 机制 | 原型对象(prototype) | 实例对象(instance) | 隐式原型(__proto__) |
|---|---|---|---|
| 构造函数 | 有(默认为空对象) | 无 | 指向构造函数的prototype |
| 普通对象 | 无 | 有 | 指向其构造函数的prototype |
| 箭头函数 | 无(prototype为undefined) | 不可用作构造函数 | 继承外层原型链 |
六、高阶函数与函数式编程:进阶技术深度
1. 高阶函数定义与应用
高阶函数满足:① 接受函数作为参数 ② 返回函数作为结果。
// 示例1:filter(接受函数参数)
const nums = [1, 2, 3, 4, 5];
const evens = nums.filter(n => n % 2 === 0); // [2, 4]
// 示例2:返回函数(柯里化)
const add = x => y => x + y;
const add5 = add(5);
console.log(add5(3)); // 8
【企业真题】(某金融科技公司):
function compose(...fns) {
return function(x) {
return fns.reduceRight((v, f) => f(v), x);
};
}
const toUpperCase = s => s.toUpperCase();
const addPrefix = s => 'PREFIX_' + s;
const composed = compose(addPrefix, toUpperCase);
console.log(composed('test')); // 输出:PREFIX_TEST
【原理】:reduceRight从右向左组合函数,实现管道(pipe)逻辑。
2. 柯里化与偏函数应用
【柯里化】:将多参数函数转换为单参数函数的链式调用。
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) return fn(...args);
return function(...nextArgs) {
return curried(...args, ...nextArgs);
};
};
}
const sum = (a, b, c) => a + b + c;
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
【偏函数应用】:固定部分参数,生成新函数。
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
const greet = (greeting, name) => `${greeting}, ${name}!`;
const greetHello = partial(greet, 'Hello');
console.log(greetHello('Alice')); // "Hello, Alice!"
【企业实践】:Lodash的_.partial与_.curry广泛用于函数组合,但需注意性能开销。
forEach、map、filter等高阶函数加入ES5,推动函数式编程普及。
value |> func提供更直观的函数组合语法(尚未进入ES标准)。
七、递归函数与性能优化:从理论到工程
1. 递归的两种实现形式
【直接递归】:函数直接调用自身。
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
【间接递归】:通过其他函数调用自身。
function isEven(n) {
if (n === 0) return true;
return isOdd(n - 1);
}
function isOdd(n) {
return n !== 0 && isEven(n - 1);
}
【企业真题】(某游戏公司):
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
console.time('fib');
console.log(fibonacci(40)); // 约102ms
console.timeEnd('fib');
【性能问题】:重复计算导致指数级时间复杂度,需优化为迭代或记忆化递归。
2. 尾递归优化(TSO)
尾递归:函数调用自身且返回值为递归调用结果(无额外计算)。
function factorialTail(n, acc = 1) {
if (n <= 1) return acc;
return factorialTail(n - 1, n * acc);
}
【理论优势】:可被引擎优化为循环,避免调用栈溢出。
【现实限制】:当前主流浏览器(Chrome/V8)未实现TSO,需手动转换为迭代:
function factorialIter(n) {
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
}
【企业建议】:生产环境优先使用迭代,递归仅用于树/图遍历等天然递归结构。
八、箭头函数:现代JavaScript的双刃剑
1. 箭头函数的四个特性
| 特性 | 描述 | 影响 |
|---|---|---|
| 无this绑定 | this继承自外层作用域 | 不可用作构造函数 |
| 无arguments | 需用剩余参数...args替代 |
无法直接访问arguments对象 |
| 无prototype | arrowFn.prototype === undefined |
不可用作构造函数 |
| 不可用作Generator | 不支持function*语法 |
无法创建生成器函数 |
【企业真题】(某大厂):
const obj = {
name: 'Test',
method: () => {
console.log(this.name);
}
};
obj.method(); // 输出:undefined
【原因】:箭头函数的this指向定义时的词法作用域(全局对象),而非obj。
2. 箭头函数的适用场景
【推荐使用】:
- 回调函数(如
.map()、.filter()) - 事件监听器(需注意this指向)
- Promise链式调用
- 简短函数表达式(单行逻辑)
【避免使用】:
- 对象方法定义(应使用
method() {}简写) - 构造函数(应使用
class或普通函数) - 需要动态this的场景(如DOM事件处理器)
- 需要arguments对象的函数
【性能对比】:箭头函数创建开销更低,但现代引擎中差异微乎其微,应以可读性优先。