JavaScript函数和作用域(经典面试题)
作者:meng半颗糖
一、闭包,使用场景
1.闭包的定义
闭包(Closure)是指函数与其引用环境(lexical environment)的组合。当一个函数能够记住并访问其定义时的作用域,即使该函数在其作用域外执行,依然可以访问那些变量。闭包的核心特点是:
- 函数嵌套:闭包通常涉及嵌套函数(内部函数引用外部函数的变量)。
- 变量持久化:外部函数的变量不会被垃圾回收,因为内部函数仍持有引用。
2.闭包的实现原理
function outer() { let count = 0; function inner() { count++; console.log(count); } return inner; } const closureFunc = outer(); closureFunc(); // 输出 1 closureFunc(); // 输出 2
inner
函数引用了outer
的变量count
,形成闭包。- 每次调用
closureFunc
时,count
的值会被保留。
3.闭包的应用场景
(1)数据封装与私有变量
通过闭包模拟私有变量,避免全局污染:
function createCounter() { let privateCount = 0; return { increment: function() { privateCount++; }, getValue: function() { return privateCount; } }; } const counter = createCounter(); counter.increment(); console.log(counter.getValue()); // 输出 1
(2)函数柯里化
将多参数函数转换为单参数链式调用:
function multiply(a) { return function(b) { return a * b; }; } const double = multiply(2); console.log(double(5)); // 输出 10
(3)事件处理与回调
在异步操作中保留上下文:
function setupClickHandler() { let clicked = 0; document.getElementById('btn').addEventListener('click', function() { clicked++; console.log(`按钮被点击 ${clicked} 次`); }); } setupClickHandler();
(4)模块化开发
实现模块的隔离作用域:
const module = (function() { let privateData = '秘密'; return { getData: function() { return privateData; } }; })(); console.log(module.getData()); // 输出 "秘密"
4.注意事项
- 内存泄漏:闭包可能导致变量长期驻留内存,需手动解除引用(如
closureFunc = null
)。 - 性能影响:过度使用闭包会增加内存消耗。
二、词法作用域和动态作用域区别
1.词法作用域(Lexical Scoping)
词法作用域由代码书写时的结构决定,作用域在代码编译阶段就已确定。变量的访问权限取决于其在代码中的物理位置(即定义时的嵌套关系),与函数调用时的执行上下文无关。
- 特点:
- 作用域链基于函数或块的定义位置建立,内部函数可访问外部函数的变量,但外部函数无法访问内部函数的变量。
- 大多数现代编程语言(如JavaScript、Python、C++)默认采用词法作用域。
- 示例:
function outer() { let x = 10; function inner() { console.log(x); // 访问外部函数的x(词法作用域) } inner(); } outer(); // 输出10
2.动态作用域(Dynamic Scoping)
动态作用域由函数调用时的执行上下文决定,作用域链在运行时动态生成。变量的访问权限取决于调用链的顺序,而非代码的静态结构。
- 特点:
- 函数内部的变量可能因调用方式不同而引用不同的值。
- 传统语言如Bash、Perl的局部变量支持动态作用域,但现代语言较少使用。
- 示例:
x=1 function foo() { echo $x # 动态作用域下,x的值取决于调用者 } function bar() { local x=2 foo } bar # 输出2(动态作用域)
3.核心区别
- 确定时机:
- 词法作用域在代码编译时静态确定。
- 动态作用域在函数运行时动态确定。
- 依赖关系:
- 词法作用域依赖代码的物理结构(嵌套关系)。
- 动态作用域依赖函数的调用栈。
- 典型语言:
- 词法作用域:JavaScript、Python、C。
- 动态作用域:Bash、Emacs Lisp(部分支持)。
- 性能影响:
- 词法作用域允许更高效的静态优化。
- 动态作用域需在运行时解析变量,可能降低性能。
4.实际影响
- 闭包(Closure):词法作用域是闭包的基础,内部函数可“记住”定义时的环境。
- 调试难度:动态作用域可能导致变量值难以预测,增加调试复杂度。
三、call、apply和bind方法的区别
1.功能:
这三个方法都是 JavaScript 中函数对象的方法,用于显式绑定函数的 this
值,并执行函数。核心功能是动态改变函数运行时 this
的指向。
2.call 方法
- 语法:
func.call(thisArg, arg1, arg2, ...)
- 作用: 立即调用函数,并指定函数内部的
this
值为thisArg
,同时传递参数列表(逐个传递)。 - 示例:
function greet(name) { console.log(`Hello, ${name}! I'm ${this.title}.`); } const context = { title: 'Mr' }; greet.call(context, 'Alice'); // 输出: Hello, Alice! I'm Mr.
3.apply 方法
- 语法:
func.apply(thisArg, [argsArray])
- 作用: 立即调用函数,并指定
this
值为thisArg
,参数通过数组(或类数组对象)传递。 - 与 call 的区别: 仅参数传递方式不同(数组 vs 逐个参数)。
- 示例:
greet.apply(context, ['Alice']); // 输出与 call 相同
4.bind 方法
- 语法:
func.bind(thisArg, arg1, arg2, ...)
- 作用: 返回一个新函数,新函数的
this
永久绑定为thisArg
,并可预先绑定部分参数(柯里化)。 - 特点: 不会立即执行函数,需手动调用返回的新函数。
- 示例:
const boundGreet = greet.bind(context, 'Alice'); boundGreet(); // 输出: Hello, Alice! I'm Mr.
5.区别:
方法 | 调用时机 | 参数形式 | 返回值 |
---|---|---|---|
call | 立即执行 | 逐个参数 | 函数执行结果 |
apply | 立即执行 | 数组或类数组 | 函数执行结果 |
bind | 延迟执行 | 逐个参数 | 绑定后的函数 |
6.使用场景
- call/apply: 需要立即调用函数并明确
this
时,如借用其他对象的方法(如数组方法用于类数组对象)。 - bind: 需要固定
this
或部分参数时(如事件监听回调、定时器函数)。
示例:借用数组方法
const arrayLike = { 0: 'a', 1: 'b', length: 2 }; Array.prototype.push.call(arrayLike, 'c'); // arrayLike 变为 { 0: 'a', 1: 'b', 2: 'c', length: 3 }
四、什么是高阶函数
1.高阶函数的定义
高阶函数是指能够接收其他函数作为参数,或者将函数作为返回值的函数。这种特性使得代码更具抽象性和灵活性,常用于函数式编程中。
2.高阶函数的特点
(1)接收函数作为参数
高阶函数可以接受一个或多个函数作为输入参数。例如,map
、filter
和 reduce
是常见的高阶函数,它们通过传入的函数对数据进行处理。
(2)返回函数作为结果
高阶函数可以生成并返回一个新函数。例如,闭包或装饰器(在 Python 中)通常会返回一个函数。
3.高阶函数的常见用例
(1)函数组合
将多个函数组合成一个新的函数,例如 compose(f, g)(x)
等价于 f(g(x))
。
(2)回调函数
在异步编程中,高阶函数常用于处理回调逻辑,例如 setTimeout
或事件监听器。
(3)装饰器模式
在不修改原函数代码的情况下,通过高阶函数扩展其功能。例如 Python 的 @decorator
语法。
4.示例
// 接收函数作为参数 const numbers = [1, 2, 3]; const squared = numbers.map(x => x * x); // [1, 4, 9] // 返回函数 function multiplier(factor) { return x => x * factor; } const double = multiplier(2); console.log(double(5)); // 10
5.优势
- 代码复用性:通过抽象通用逻辑,减少重复代码。
- 灵活性:动态调整函数行为,适应不同场景。
- 可读性:通过函数组合,使代码更符合声明式编程风格。
五、解释箭头函数的特性及其与普通函数的区别
1.特性
箭头函数(Arrow Function)是ES6引入的一种简化函数定义的语法,具有以下核心特性:
简洁语法:省略function
关键字,使用=>
定义函数。单行表达式可省略{}
和return
。
const add = (a, b) => a + b;
无this
绑定:箭头函数不绑定自身的this
,而是继承外层作用域的this
值。
const obj = { value: 10, getValue: () => console.log(this.value) // 输出undefined(继承全局作用域) };
无arguments
对象:需使用剩余参数(...args
)替代。
const logArgs = (...args) => console.log(args);
不可作为构造函数:无法通过new
调用,且没有prototype
属性。
const Foo = () => {}; new Foo(); // TypeError: Foo is not a constructor
2.区别
(1)this绑定
普通函数的this
由调用方式决定(动态绑定),箭头函数继承定义时的this
(静态绑定)。
function regularFunc() { console.log(this); } const arrowFunc = () => console.log(this); const obj = { method: regularFunc, arrowMethod: arrowFunc }; obj.method(); // 输出obj(普通函数) obj.arrowMethod(); // 输出全局对象(箭头函数继承定义时的this)
(2)构造函数与prototype
普通函数可作为构造函数,箭头函数不能。
(3)arguments对象
普通函数可通过arguments
访问参数,箭头函数需使用剩余参数。
(4)语法灵活性
普通函数支持函数声明和表达式,箭头函数仅限表达式形式。
3.适用场景
箭头函数:适合需要绑定外层this
的场景(如回调函数、数组方法)。
[1, 2, 3].map(item => item * 2);
普通函数:需要动态this
、构造函数或arguments
时使用。
function Person(name) { this.name = name; }
六、什么是生成器函数
1.概念
生成器函数是 JavaScript 中的一种特殊函数,允许通过 yield
关键字暂停和恢复执行。它返回一个生成器对象(Generator),该对象遵循迭代器协议,可以逐步产生值,而非一次性计算所有结果。
2.基本语法
生成器函数通过在 function
后添加星号(*
)定义:
function* generatorFunction() { yield 'value1'; yield 'value2'; return 'finalValue'; // 可选 }
调用生成器函数时,不会立即执行函数体,而是返回一个生成器对象:
const generator = generatorFunction();
3.核心特性
暂停与恢复
通过 yield
暂停函数执行,并返回当前值。调用 next()
方法恢复执行,直到下一个 yield
或 return
。
惰性求值
值按需生成,节省内存。例如处理大型数据集时,无需预先生成全部结果。
4.对象的方法
next()
:恢复执行,返回{ value: yieldedValue, done: boolean }
。return(value)
:终止生成器,返回{ value: value, done: true }
。throw(error)
:向生成器抛入错误,可在函数内通过try-catch
捕获。
示例:生成斐波那契数列
function* fibonacci() { let [a, b] = [0, 1]; while (true) { yield a; [a, b] = [b, a + b]; } } const fib = fibonacci(); console.log(fib.next().value); // 0 console.log(fib.next().value); // 1 console.log(fib.next().value); // 1
5.区别
特性 | 生成器函数 | 普通函数 |
---|---|---|
执行方式 | 可暂停和恢复 | 一次性执行完毕 |
返回值 | 生成器对象(迭代器) | 直接返回结果 |
内存占用 | 惰性计算,内存效率高 | 可能占用更多内存 |
6.常见应用场景
- 异步编程:与
Promise
结合简化异步流程(如redux-saga
)。 - 无限序列:按需生成无限序列(如斐波那契数列)。
- 状态管理:通过
yield
暂停保存中间状态。
七、防抖
1.概念
防抖(Debounce)是一种优化高频触发事件的技术,确保事件在停止触发后的一段时间内只执行一次。常用于输入框搜索、窗口大小调整等场景。
2.基础实现
function debounce(func, delay) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); }, delay); }; }
- 核心逻辑:每次触发时清除旧定时器,重新设置延迟执行。
- 参数说明:
func
:需要防抖的目标函数。delay
:延迟时间(毫秒)。
3.立即执行版本
若需首次触发立即执行,后续延迟可添加标志位控制:
function debounce(func, delay, immediate) { let timer; return function(...args) { const context = this; if (immediate && !timer) { func.apply(context, args); } clearTimeout(timer); timer = setTimeout(() => { timer = null; if (!immediate) { func.apply(context, args); } }, delay); }; }
immediate
为true
时:首次触发立即执行,后续停止触发delay
毫秒后再恢复立即执行能力。
4.取消功能扩展
为防抖函数添加取消方法:
function debounce(func, delay) { let timer; const debounced = function(...args) { clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); }, delay); }; debounced.cancel = () => { clearTimeout(timer); timer = null; }; return debounced; }
调用示例:
const debouncedFn = debounce(handler, 500); debouncedFn.cancel(); // 取消延迟执行
5.应用场景
- 输入框搜索:用户停止输入500毫秒后发起请求。
- 窗口调整:停止调整窗口后计算布局。
- 按钮防重复点击:避免用户快速多次提交。
6.注意事项
- 确保
func
的this
指向正确,需通过apply
或call
绑定上下文。 - 频繁触发的场景下(如动画),防抖可能导致延迟过高,需权衡延迟时间。
八、节流
1.概念
函数节流(Throttle)是一种限制函数执行频率的技术,确保函数在指定时间间隔内只执行一次。常用于高频事件(如滚动、输入、窗口调整)的性能优化。
2.时间戳版节流
通过记录上一次执行的时间戳,判断当前时间是否超过间隔:
function throttle(func, delay) { let lastTime = 0; return function(...args) { const now = Date.now(); if (now - lastTime >= delay) { func.apply(this, args); lastTime = now; } }; }
特点:立即执行,但最后一次触发可能不执行。
3.定时器版节流
通过定时器控制执行,确保间隔结束后触发:
function throttle(func, delay) { let timer = null; return function(...args) { if (!timer) { timer = setTimeout(() => { func.apply(this, args); timer = null; }, delay); } }; }
特点:延迟执行,确保最后一次触发会执行。
4.结合时间戳与定时器
综合两种方案,实现首次立即执行、末次延迟执行的效果:
function throttle(func, delay) { let lastTime = 0, timer = null; return function(...args) { const now = Date.now(); const remaining = delay - (now - lastTime); if (remaining <= 0) { if (timer) { clearTimeout(timer); timer = null; } func.apply(this, args); lastTime = now; } else if (!timer) { timer = setTimeout(() => { func.apply(this, args); lastTime = Date.now(); timer = null; }, remaining); } }; }
5.应用示例
监听滚动事件时,限制处理函数的频率:
const handleScroll = throttle(() => { console.log('Scroll event throttled'); }, 200); window.addEventListener('scroll', handleScroll);
6.注意事项
- 间隔时间:根据实际场景调整
delay
参数,避免卡顿或响应不足。 - 上下文绑定:使用
apply
确保函数内的this
指向正确。 - 取消机制:可扩展功能,支持手动取消未执行的定时器。
九、递归函数的优缺点
1.优点
- 代码简洁清晰
递归能将复杂问题分解为相似的子问题,代码逻辑更接近数学定义或自然语言描述。例如阶乘、斐波那契数列的实现只需几行代码。
- 减少显式循环结构
对于树形结构(如DOM遍历、目录扫描)或分治算法(如快速排序、归并排序),递归无需手动维护栈或循环变量,降低实现复杂度。
- 天然的问题分解能力
适合解决具有自相似性的问题(如汉诺塔、回溯算法),子问题的解能直接组合成原问题的解。
2.缺点
- 栈溢出风险
每次递归调用会占用栈帧空间,深度过大时(如未优化的斐波那契数列计算)可能导致栈溢出错误。尾递归优化可缓解此问题,但并非所有语言支持。
- 性能开销较高
函数调用涉及压栈、跳转等操作,比循环开销更大。例如计算fib(30)
可能触发数百万次递归调用,而迭代法只需几十次循环。
- 调试难度增加
多层递归时,调用栈跟踪复杂,错误可能难以定位。需依赖日志或调试工具观察中间状态。
- 重复计算问题
某些递归(如朴素斐波那契实现)会重复计算相同子问题,需配合备忘录(Memoization)优化。
# 未优化的斐波那契递归 def fib(n): if n <= 1: return n return fib(n-1) + fib(n-2) # 存在大量重复计算
3.使用建议
- 适用场景:问题可明确分解为子问题,且子问题与原问题同构(如树的遍历)。
- 避免场景:对性能敏感或递归深度不可控时(如处理用户输入的未知层级数据)。
- 优化手段:尾递归改写(如Scheme)、备忘录模式(缓存中间结果)、改用迭代实现。
十、什么是纯函数
1.定义
纯函数是指在相同的输入下始终返回相同的输出,并且不会产生任何副作用的函数。这意味着纯函数不依赖于外部状态,也不会修改外部状态。
2.特性
- 确定性:对于相同的输入,纯函数总是返回相同的输出。例如:
function add(a, b) { return a + b; }
无论何时调用 add(2, 3)
,结果始终是 5
。
- 无副作用:纯函数不会修改外部变量、全局状态或传入的参数。例如:
let counter = 0; function increment() { counter++; // 副作用:修改了外部变量 }
increment
不是纯函数,因为它修改了外部变量 counter
。
3.优点
- 可预测性:由于纯函数的行为完全由输入决定,结果更容易预测和测试。
- 可缓存性:相同的输入总是对应相同的输出,因此可以缓存结果以提高性能。
- 易于并行化:纯函数不依赖外部状态,可以在多线程环境下安全运行。
4示例
// 纯函数 function square(x) { return x * x; } // 非纯函数 function getRandomNumber() { return Math.random(); // 输出不确定 }
5.如何编写
避免修改外部状态或传入的参数:
// 不纯的函数 function updateUser(user) { user.age = 30; // 直接修改了参数 return user; } // 纯函数 function updateUserPure(user) { return { ...user, age: 30 }; // 返回新对象,不修改原参数 }
避免依赖外部变量:
// 不纯的函数 let taxRate = 0.1; function calculateTax(amount) { return amount * taxRate; // 依赖外部变量 } // 纯函数 function calculateTaxPure(amount, rate) { return amount * rate; // 所有依赖都通过参数传入 }
十一、解释函数重载在JavaScript中的实现方式
1.概念
函数重载允许同一函数名根据参数类型或数量不同执行不同操作。JavaScript本身不支持传统意义上的函数重载(如Java或C++),但可以通过特定技巧模拟实现。
2.基于参数数量的重载
通过检查arguments
对象或剩余参数(rest parameters)判断传入参数数量,执行不同逻辑:
function example() { if (arguments.length === 1) { console.log("处理单参数逻辑"); } else if (arguments.length === 2) { console.log("处理双参数逻辑"); } } example("A"); // 输出: 处理单参数逻辑 example("A", "B"); // 输出: 处理双参数逻辑
3.基于参数类型的重载
使用typeof
或instanceof
检查参数类型,动态调整行为:
function process(input) { if (typeof input === "string") { console.log("处理字符串逻辑"); } else if (typeof input === "number") { console.log("处理数字逻辑"); } } process("text"); // 输出: 处理字符串逻辑 process(42); // 输出: 处理数字逻辑
4.使用对象参数模拟重载
通过传递配置对象统一参数,避免参数顺序问题:
function configure(options) { if (options.mode === "fast") { console.log("启用快速模式"); } else if (options.mode === "safe") { console.log("启用安全模式"); } } configure({ mode: "fast" }); // 输出: 启用快速模式
5.结合默认参数与条件判断
利用ES6默认参数简化逻辑分支:
function load(data, format = "json") { if (format === "json") { console.log("解析JSON数据"); } else if (format === "xml") { console.log("解析XML数据"); } } load({}); // 输出: 解析JSON数据 load({}, "xml"); // 输出: 解析XML数据
6.注意事项
- JavaScript函数重载本质是手动模拟,需显式编写参数检查逻辑。
- 过度使用可能导致代码可读性下降,建议优先考虑清晰命名的独立函数。
- TypeScript提供更规范的重载语法支持,适合大型项目。
十二、什么是偏函数
1.概念
偏函数(Partial Function)是一种在函数式编程中常见的概念,指的是通过固定一个函数的部分参数,生成一个新的函数。这种技术允许开发者复用已有函数,同时减少重复代码。
2.作用
偏函数的主要作用是将一个多参数函数转换为一个参数较少的函数。通过预先绑定某些参数,可以在后续调用时简化操作。例如,一个计算乘方的函数可以通过偏函数固定底数,生成一个专门计算平方或立方的函数。
3.实现方式
在Python中,可以使用functools.partial
来实现偏函数。以下是一个示例:
from functools import partial def power(base, exponent): return base ** exponent square = partial(power, exponent=2) cube = partial(power, exponent=3) print(square(5)) # 输出25 print(cube(3)) # 输出27
4.优点
- 代码复用:通过固定部分参数,避免重复编写相似的函数。
- 灵活性:可以在运行时动态生成新的函数,适应不同的需求。
- 可读性:通过赋予偏函数有意义的名称,提升代码的可读性。
5.应用场景
- 回调函数:在事件处理中,可以通过偏函数预先绑定部分参数,简化回调函数的定义。
- 配置函数:在需要多次调用同一函数但部分参数固定的场景中,使用偏函数可以减少冗余代码。
- 数学运算:如上述示例所示,通过偏函数可以快速生成特定运算的函数。
6.注意事项
- 参数顺序:偏函数绑定的参数顺序需与原函数一致,否则可能导致错误。
- 不可变参数:偏函数生成的新函数不能修改已绑定的参数值。
- 性能开销:尽管偏函数的性能开销通常较小,但在高性能场景中仍需谨慎使用。
到此这篇关于JavaScript函数和作用域(经典面试题)的文章就介绍到这了,更多相关js函数和作用域内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!