前置知识: JavaScript

函数、作用域与闭包

00:00
7 min Intermediate

函数声明、箭头函数、作用域链与闭包原理。

1. 函数定义 (Definitions)

1.1 函数声明

语法

function functionName(parameters) {
  // 函数体
  return value; // 可选
}

特点

  • 存在函数提升(hoisting),可以在声明前调用
  • 函数名是必需的 示例
// 可以在声明前调用
console.log(add(2, 3)); // 输出: 5
function add(a, b) {
  return a + b;
}

1.2 函数表达式

语法

const functionName = function (parameters) {
  // 函数体
  return value; // 可选
};

特点

  • 不存在函数提升,不能在声明前调用
  • 可以是匿名函数
  • 可以作为变量赋值 示例
// 不能在声明前调用,会报错
// console.log(subtract(5, 2)); // 错误: subtract is not defined
const subtract = function (a, b) {
  return a - b;
};
console.log(subtract(5, 2)); // 输出: 3

1.3 箭头函数 (ES6+)

语法

// 基本语法
const functionName = (parameters) => {
  // 函数体
  return value;
};
// 单个参数可以省略括号
const double = (num) => {
  return num * 2;
};
// 单个表达式可以省略花括号和 return
const triple = (num) => num * 3;
// 无参数
const greet = () => console.log('Hello!');
// 多个参数
const multiply = (a, b) => a * b;

特点

  • 不绑定 this,继承父级作用域的 this
  • 不能作为构造函数使用
  • 没有 arguments 对象
  • 语法简洁,适合作为回调函数 示例
// 箭头函数作为回调
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((num) => num * 2);
console.log(doubled); // 输出: [2, 4, 6, 8, 10]
// 箭头函数与 this
const obj = {
  name: '张三',
  regularFunction: function () {
    console.log(this.name); // 输出: 张三
  },
  arrowFunction: () => {
    console.log(this.name); // 输出: undefined (继承全局作用域的 this)
  },
};
obj.regularFunction();
obj.arrowFunction();

1.4 函数参数

1.4.1 默认参数 (ES6+)

function greet(name = '世界') {
  return `Hello, ${name}!`;
}
console.log(greet()); // 输出: Hello, 世界!
console.log(greet('张三')); // 输出: Hello, 张三!

1.4.2 剩余参数 (ES6+)

function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 输出: 15

1.4.3 解构参数 (ES6+)

function printUser({ name, age }) {
  console.log(`Name: ${name}, Age: ${age}`);
}
const user = { name: '张三', age: 30 };
printUser(user); // 输出: Name: 张三, Age: 30

2. 作用域 (Scope)

2.1 全局作用域

定义:在所有函数外部定义的变量,在整个脚本中都可以访问。 示例

const globalVar = '全局变量';
function test() {
  console.log(globalVar); // 可以访问全局变量
}
test(); // 输出: 全局变量
console.log(globalVar); // 可以访问全局变量

2.2 函数作用域

定义:在函数内部定义的变量,只能在函数内部访问。 示例

function test() {
  const localVar = '局部变量';
  console.log(localVar); // 可以访问局部变量
}
test(); // 输出: 局部变量
// console.log(localVar); // 错误: localVar is not defined

2.3 块级作用域 (ES6+)

定义:使用 letconst{} 内部定义的变量,只能在块内部访问。 示例

if (true) {
  let blockVar = '块级变量';
  const constBlockVar = '常量块级变量';
  console.log(blockVar); // 可以访问
  console.log(constBlockVar); // 可以访问
}
// console.log(blockVar); // 错误: blockVar is not defined
// console.log(constBlockVar); // 错误: constBlockVar is not defined

2.4 作用域链

定义:当访问一个变量时,JavaScript 会从当前作用域开始查找,如果找不到,就会向上一级作用域查找,直到找到或到达全局作用域。 示例

const globalVar = '全局变量';
function outer() {
  const outerVar = '外部函数变量';
  function inner() {
    const innerVar = '内部函数变量';
    console.log(innerVar); // 内部函数变量
    console.log(outerVar); // 外部函数变量
    console.log(globalVar); // 全局变量
  }
  inner();
  // console.log(innerVar); // 错误: innerVar is not defined
}
outer();
// console.log(outerVar); // 错误: outerVar is not defined

3. 闭包 (Closures)

3.1 闭包的概念

定义:闭包是函数与其绑定的词法作用域的组合。简单来说,闭包是一个函数,它可以访问其创建时所在的作用域,即使该函数在其他作用域中被调用。 示例

function createCounter() {
  let count = 0; // 私有变量
  return function () {
    return ++count;
  };
}
const counter = createCounter();
console.log(counter()); // 输出: 1
console.log(counter()); // 输出: 2
console.log(counter()); // 输出: 3

3.2 闭包的作用

  1. 私有化变量:通过闭包可以创建私有变量,外部无法直接访问。
  2. 数据持久化:闭包可以保存函数创建时的环境,使变量的值在多次调用之间保持。
  3. 模拟模块:使用闭包可以创建模块化的代码,避免全局变量污染。

3.3 闭包的应用场景

3.3.1 计数器

function createCounter(initialValue = 0) {
  let count = initialValue;
  return {
    increment: function () {
      return ++count;
    },
    decrement: function () {
      return --count;
    },
    reset: function () {
      count = initialValue;
      return count;
    },
    getCount: function () {
      return count;
    },
  };
}
const counter = createCounter(10);
console.log(counter.increment()); // 输出: 11
console.log(counter.increment()); // 输出: 12
console.log(counter.decrement()); // 输出: 11
console.log(counter.reset()); // 输出: 10
console.log(counter.getCount()); // 输出: 10

3.3.2 模块模式

const mathModule = (function () {
  // 私有变量
  const PI = 3.14159;
  // 私有函数
  function add(a, b) {
    return a + b;
  }
  function subtract(a, b) {
    return a - b;
  }
  // 暴露公共接口
  return {
    add: add,
    subtract: subtract,
    getPI: function () {
      return PI;
    },
  };
})();
console.log(mathModule.add(5, 3)); // 输出: 8
console.log(mathModule.subtract(5, 3)); // 输出: 2
console.log(mathModule.getPI()); // 输出: 3.14159
// console.log(mathModule.PI); // 输出: undefined (私有变量)

3.3.3 事件处理

function createButtonClickHandler() {
  let clickCount = 0;
  return function () {
    clickCount++;
    console.log(`按钮被点击了 ${clickCount} 次`);
  };
}
const button = document.createElement('button');
button.textContent = '点击我';
document.body.appendChild(button);
button.addEventListener('click', createButtonClickHandler());

3.4 闭包的注意事项

  1. 内存泄漏:闭包会持有对外部变量的引用,导致这些变量无法被垃圾回收。如果闭包被长时间保存,可能会导致内存泄漏。
  2. 性能影响:闭包会增加内存使用,并且在访问外部变量时需要通过作用域链查找,可能会影响性能。
  3. 变量共享:在循环中创建闭包时,要注意变量共享的问题。 示例
// 问题代码
for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i); // 输出: 5, 5, 5, 5, 5
  }, 1000);
}
// 解决方案 1: 使用 let
for (let i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i); // 输出: 0, 1, 2, 3, 4
  }, 1000);
}
// 解决方案 2: 使用立即执行函数表达式 (IIFE)
for (var i = 0; i < 5; i++) {
  (function (j) {
    setTimeout(function () {
      console.log(j); // 输出: 0, 1, 2, 3, 4
    }, 1000);
  })(i);
}

4. this 指向机制

4.1 普通函数中的 this

规则:普通函数中的 this 指向调用者,如果没有调用者,则指向全局对象(在严格模式下指向 undefined)。 示例

function test() {
  console.log(this);
}
// 直接调用,this 指向全局对象 (window)
test();
// 作为对象方法调用,this 指向对象
const obj = {
  name: '张三',
  test: test,
};
obj.test(); // this 指向 obj
// 使用 call 或 apply 显式绑定 this
const anotherObj = { name: '李四' };
test.call(anotherObj); // this 指向 anotherObj

4.2 箭头函数中的 this

规则:箭头函数不绑定自己的 this,而是继承父级作用域的 this示例

const obj = {
  name: '张三',
  regularFunction: function () {
    console.log(this.name); // 输出: 张三
    // 普通函数,this 指向全局对象
    setTimeout(function () {
      console.log(this.name); // 输出: undefined
    }, 1000);
    // 箭头函数,继承父级作用域的 this
    setTimeout(() => {
      console.log(this.name); // 输出: 张三
    }, 1000);
  },
};
obj.regularFunction();

4.3 构造函数中的 this

规则:构造函数中的 this 指向新创建的实例。 示例

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.greet = function () {
    console.log(`Hello, my name is ${this.name}`);
  };
}
const person = new Person('张三', 30);
console.log(person.name); // 输出: 张三
person.greet(); // 输出: Hello, my name is 张三

4.4 显式绑定 this

4.4.1 call() 方法

语法function.call(thisArg, arg1, arg2, ...) 示例

function greet(greeting) {
  console.log(`${greeting}, ${this.name}!`);
}
const person = { name: '张三' };
greet.call(person, 'Hello'); // 输出: Hello, 张三!

4.4.2 apply() 方法

语法function.apply(thisArg, [argsArray]) 示例

function sum(a, b, c) {
  return a + b + c;
}
const numbers = [1, 2, 3];
const result = sum.apply(null, numbers);
console.log(result); // 输出: 6

4.4.3 bind() 方法

语法function.bind(thisArg, arg1, arg2, ...) 示例

function greet() {
  console.log(`Hello, ${this.name}!`);
}
const person = { name: '张三' };
const boundGreet = greet.bind(person);
boundGreet(); // 输出: Hello, 张三!

5. 最佳实践

5.1 函数使用最佳实践

  1. 优先使用箭头函数:对于简短的函数,尤其是回调函数,优先使用箭头函数。
  2. 使用默认参数:对于有默认值的参数,使用 ES6 的默认参数语法。
  3. 使用剩余参数:对于不确定数量的参数,使用剩余参数语法。
  4. 使用解构参数:对于对象或数组参数,使用解构语法可以使代码更清晰。

5.2 作用域最佳实践

  1. 使用 let 和 const:优先使用 letconst 而不是 var,以避免变量提升和全局变量污染。
  2. 最小作用域变量作用域应该尽可能小,只在需要的地方定义
  3. 避免全局变量:尽量避免使用全局变量,使用模块化的方式组织代码。

5.3 闭包最佳实践

  1. 合理使用闭包:只在需要时使用闭包,避免不必要的闭包
  2. 注意内存泄漏:如果闭包被长时间保存,要确保其中引用的变量不会导致内存泄漏
  3. 避免循环中的闭包问题:在循环中创建闭包时,要注意变量共享问题,使用 let 或 IIFE 来解决

5.4 this 指向最佳实践

  1. 理解 this 的指向:要清楚不同情况下 this 的指向规则
  2. 使用箭函数:在需要继承父级作用域 this 的场景中,使用箭函数。
  3. 使用 bind():在需要固定 this 指向的场景中,使用 bind() 方法
  4. 避免混合使用普通函数和箭函数:在对象方法中,要注意普通函数和箭函数的 this 指向差异。

6. 实际应用示例

6.1 示例 1:防抖函数

function debounce(func, delay) {
  let timeoutId;
  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}
// 使用示例
const debouncedSearch = debounce(function (query) {
  console.log('搜索:', query);
  // 实际的搜索逻辑
}, 300);
// 输入时会防抖,只有停止输入 300ms 后才会执行搜索
debouncedSearch('JavaScript');
debouncedSearch('JavaScript 闭包');
debouncedSearch('JavaScript 闭包 this');
// 最终只会执行一次: 搜索: JavaScript 闭包 this

6.2 示例 2:节流函数

function throttle(func, limit) {
  let inThrottle;
  return function (...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  };
}
// 使用示例
const throttledScroll = throttle(function () {
  console.log('滚动事件触发');
  // 实际的滚动处理逻辑
}, 1000);
// 滚动时会节流,每 1000ms 最多执行一次
window.addEventListener('scroll', throttledScroll);

6.3 示例 3:模块化代码

// 模块定义
const calculator = (function () {
  // 私有方法
  function validateNumber(num) {
    return typeof num === 'number' && !isNaN(num);
  }
  // 公共接口
  return {
    add: function (a, b) {
      if (validateNumber(a) && validateNumber(b)) {
        return a + b;
      }
      return NaN;
    },
    subtract: function (a, b) {
      if (validateNumber(a) && validateNumber(b)) {
        return a - b;
      }
      return NaN;
    },
    multiply: function (a, b) {
      if (validateNumber(a) && validateNumber(b)) {
        return a * b;
      }
      return NaN;
    },
    divide: function (a, b) {
      if (validateNumber(a) && validateNumber(b) && b !== 0) {
        return a / b;
      }
      return NaN;
    },
  };
})();
// 使用示例
console.log(calculator.add(5, 3)); // 输出: 8
console.log(calculator.subtract(5, 3)); // 输出: 2
console.log(calculator.multiply(5, 3)); // 输出: 15
console.log(calculator.divide(5, 3)); // 输出: 1.6666666666666667
console.log(calculator.divide(5, 0)); // 输出: NaN

更新日志 (Changelog)

  • 2026-04-05: 深入细化闭包与 This 指向原理。
  • 2026-04-05: 扩写内容增加详细的函数定义作用域闭包this 指向的概念、示例和最佳实践

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式