一、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和自动清理机制降低泄漏风险,但复杂状态管理仍需开发者关注闭包生命周期。

ES3(2000年)
词法作用域确立:JavaScript首次定义静态作用域规则,函数作用域由定义位置决定。
ES5(2009年)
严格模式引入:禁止隐式全局变量声明,规范闭包行为;"use strict"提升代码可预测性。
ES6(2015年)
块级作用域支持:let/const使{}形成独立作用域,与函数作用域并存;let声明的变量无变量提升。
ES2020+(2020年至今)
模块化规范: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陷阱
框架中的this实践

高频场景

  • 回调函数丢失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广泛用于函数组合,但需注意性能开销。

2009年
Array方法标准化:forEach、map、filter等高阶函数加入ES5,推动函数式编程普及。
2015年
箭头函数诞生:简化回调语法,解决this绑定问题,使函数式编程更简洁。
2021年
管道操作符提案: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对象的函数

【性能对比】:箭头函数创建开销更低,但现代引擎中差异微乎其微,应以可读性优先。