java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > js函数和作用域

JavaScript函数和作用域(经典面试题)

作者:meng半颗糖

文章系统梳理了JavaScript中的核心函数概念,包括闭包、词法作用域与动态作用域的区别、call/apply/bind方法、高阶函数、箭头函数、生成器、防抖节流技术、递归优缺点、纯函数及函数重载与偏函数,感兴趣的朋友跟随小编一起看看吧

一、闭包,使用场景

1.闭包的定义

闭包(Closure)是指函数与其引用环境(lexical environment)的组合。当一个函数能够记住并访问其定义时的作用域,即使该函数在其作用域外执行,依然可以访问那些变量。闭包的核心特点是:

  1. 函数嵌套:闭包通常涉及嵌套函数(内部函数引用外部函数的变量)。
  2. 变量持久化:外部函数的变量不会被垃圾回收,因为内部函数仍持有引用。

2.闭包的实现原理

function outer() {
    let count = 0;
    function inner() {
        count++;
        console.log(count);
    }
    return inner;
}
const closureFunc = outer();
closureFunc(); // 输出 1
closureFunc(); // 输出 2

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.注意事项

  1. 内存泄漏:闭包可能导致变量长期驻留内存,需手动解除引用(如 closureFunc = null)。
  2. 性能影响:过度使用闭包会增加内存消耗。

二、词法作用域和动态作用域区别

1.词法作用域(Lexical Scoping)

词法作用域由代码书写时的结构决定,作用域在代码编译阶段就已确定。变量的访问权限取决于其在代码中的物理位置(即定义时的嵌套关系),与函数调用时的执行上下文无关。

function outer() {
  let x = 10;
  function inner() {
    console.log(x); // 访问外部函数的x(词法作用域)
  }
  inner();
}
outer(); // 输出10

2.动态作用域(Dynamic Scoping)

动态作用域由函数调用时的执行上下文决定,作用域链在运行时动态生成。变量的访问权限取决于调用链的顺序,而非代码的静态结构。

x=1
function foo() {
  echo $x # 动态作用域下,x的值取决于调用者
}
function bar() {
  local x=2
  foo
}
bar # 输出2(动态作用域)

3.核心区别

4.实际影响

三、call、apply和bind方法的区别

1.功能:

这三个方法都是 JavaScript 中函数对象的方法,用于显式绑定函数的 this 值,并执行函数。核心功能是动态改变函数运行时 this 的指向。

2.call 方法

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 方法

greet.apply(context, ['Alice']); // 输出与 call 相同

4.bind 方法

const boundGreet = greet.bind(context, 'Alice');
boundGreet(); // 输出: Hello, Alice! I'm Mr.

5.区别:

方法调用时机参数形式返回值
call立即执行逐个参数函数执行结果
apply立即执行数组或类数组函数执行结果
bind延迟执行逐个参数绑定后的函数

6.使用场景

示例:借用数组方法

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)接收函数作为参数
高阶函数可以接受一个或多个函数作为输入参数。例如,mapfilterreduce 是常见的高阶函数,它们通过传入的函数对数据进行处理。

(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() 方法恢复执行,直到下一个 yieldreturn

惰性求值
值按需生成,节省内存。例如处理大型数据集时,无需预先生成全部结果。

4.对象的方法

示例:生成斐波那契数列

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.常见应用场景

七、防抖

1.概念

防抖(Debounce)是一种优化高频触发事件的技术,确保事件在停止触发后的一段时间内只执行一次。常用于输入框搜索、窗口大小调整等场景。

2.基础实现

function debounce(func, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, args);
    }, 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);
  };
}

immediatetrue时:首次触发立即执行,后续停止触发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.应用场景

  1. 输入框搜索:用户停止输入500毫秒后发起请求。
  2. 窗口调整:停止调整窗口后计算布局。
  3. 按钮防重复点击:避免用户快速多次提交。

6.注意事项

八、节流

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.注意事项

九、递归函数的优缺点

1.优点

递归能将复杂问题分解为相似的子问题,代码逻辑更接近数学定义或自然语言描述。例如阶乘、斐波那契数列的实现只需几行代码。

对于树形结构(如DOM遍历、目录扫描)或分治算法(如快速排序、归并排序),递归无需手动维护栈或循环变量,降低实现复杂度。

适合解决具有自相似性的问题(如汉诺塔、回溯算法),子问题的解能直接组合成原问题的解。

2.缺点

每次递归调用会占用栈帧空间,深度过大时(如未优化的斐波那契数列计算)可能导致栈溢出错误。尾递归优化可缓解此问题,但并非所有语言支持。

函数调用涉及压栈、跳转等操作,比循环开销更大。例如计算fib(30)可能触发数百万次递归调用,而迭代法只需几十次循环。

多层递归时,调用栈跟踪复杂,错误可能难以定位。需依赖日志或调试工具观察中间状态。

某些递归(如朴素斐波那契实现)会重复计算相同子问题,需配合备忘录(Memoization)优化。

# 未优化的斐波那契递归
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)  # 存在大量重复计算

3.使用建议

十、什么是纯函数

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.基于参数类型的重载

使用typeofinstanceof检查参数类型,动态调整行为:

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.注意事项

十二、什么是偏函数

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.优点

  1. 代码复用:通过固定部分参数,避免重复编写相似的函数。
  2. 灵活性:可以在运行时动态生成新的函数,适应不同的需求。
  3. 可读性:通过赋予偏函数有意义的名称,提升代码的可读性。

5.应用场景

  1. 回调函数:在事件处理中,可以通过偏函数预先绑定部分参数,简化回调函数的定义。
  2. 配置函数:在需要多次调用同一函数但部分参数固定的场景中,使用偏函数可以减少冗余代码。
  3. 数学运算:如上述示例所示,通过偏函数可以快速生成特定运算的函数。

6.注意事项

  1. 参数顺序:偏函数绑定的参数顺序需与原函数一致,否则可能导致错误。
  2. 不可变参数:偏函数生成的新函数不能修改已绑定的参数值。
  3. 性能开销:尽管偏函数的性能开销通常较小,但在高性能场景中仍需谨慎使用。

到此这篇关于JavaScript函数和作用域(经典面试题)的文章就介绍到这了,更多相关js函数和作用域内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文