JavaScript高级程序设计笔记 10

第10章 函数 — 快速复习笔记

1. 函数定义

1.1 函数声明与函数表达式

  • 函数声明function sum(a, b) { return a + b; }
    • 特点:会被提升(hoisting),可以在声明前调用。
  • 函数表达式let sum = function(a, b) { return a + b; };
    • 特点:不会被提升,必须在赋值后才能调用。

1.2 箭头函数(ES6)

let sum = (a, b) => a + b;
let square = x => x * x;          // 单个参数可省略括号
let log = () => console.log('hi'); // 无参数需括号
let getObj = () => ({ name: 'a' }); // 返回对象需加括号
  • 特点
    • 没有自己的 thisargumentssupernew.target
    • 不能用作构造函数(不能 new
    • 没有 prototype 属性
    • 不能作为生成器函数(不能用 yield

1.3 函数构造函数(不推荐)

let sum = new Function('a', 'b', 'return a + b');
  • 作用域:总是全局作用域,会多次解析代码,性能差。

2. 函数参数

2.1 参数传递

  • 所有参数按值传递(原始值拷值,引用值拷地址)。
  • 参数数量不限,没有类型检查。
  • 未传递的参数值为 undefined

2.2 arguments 对象

  • 类数组对象(不是 Array),可在函数内访问所有传入的参数。
  • 与命名参数联动(修改 arguments[0] 会同步修改对应的命名参数,严格模式下不联动)。
  • arguments.callee 指向函数自身(严格模式禁用)。
function foo(a, b) {
  console.log(arguments[0], arguments[1]); // 0, undefined
  arguments[0] = 10;
  console.log(a); // 10(非严格模式)
}
foo(0);

2.3 默认参数值(ES6)

function greet(name = 'Guest', age = 18) {
  console.log(`${name}, ${age}`);
}
greet();        // Guest, 18
greet('Tom');   // Tom, 18
  • 默认值可以是表达式/函数调用(惰性求值,每次调用时重新计算)。
  • 默认值可以使用前面的参数值。
function add(a, b = a) { return a + b; } // b 默认等于 a
add(5); // 10

2.4 剩余参数(rest 参数,ES6)

function sum(...nums) {
  return nums.reduce((total, n) => total + n, 0);
}
console.log(sum(1,2,3,4)); // 10
  • 剩余参数是真正的数组。
  • 只能有一个剩余参数,且必须在参数列表最后。

3. 函数调用与内部属性

3.1 函数内部的 this

  • 默认绑定:非严格模式下,独立函数调用 this 指向全局对象(浏览器 window),严格模式为 undefined
  • 方法调用obj.method()this 指向 obj
  • 构造函数new Fn()this 指向新创建的实例。
  • 箭头函数this 继承自外层函数的 this(词法作用域),不可改变。
let obj = {
  name: 'obj',
  say() {
    setTimeout(() => {
      console.log(this.name); // 'obj'(箭头函数继承外层 this)
    }, 0);
  }
};

3.2 caller 属性

  • arguments.callee.caller 或函数自身 .caller 指向调用该函数的函数。
  • 严格模式禁止使用。

3.3 new.target(ES6)

  • 用于检测函数是否通过 new 调用。
  • 在构造函数中:被 new 调用时返回构造函数引用,否则返回 undefined
function Person(name) {
  if (!new.target) throw new Error('必须使用 new 调用');
  this.name = name;
}

4. 函数属性与方法

4.1 属性

  • length:命名参数的个数
  • prototype:指向原型对象(不可枚举,不可配置)

4.2 方法

  • call(thisArg, arg1, arg2, ...):改变 this,逐个传参
  • apply(thisArg, [argsArray]):改变 this,数组传参
  • bind(thisArg, ...args):返回新函数,this 绑定后不会改变(硬绑定)
function add(a, b) { return a + b; }
let obj = {};
console.log(add.call(obj, 1, 2));   // 3
console.log(add.apply(obj, [1,2])); // 3
let bound = add.bind(obj, 1);
console.log(bound(2));              // 3

5. 闭包

5.1 定义

  • 闭包是指有权访问另一个函数作用域中变量的函数(即函数嵌套函数,内部函数引用了外部函数的变量)。
function outer() {
  let name = 'closure';
  function inner() {
    console.log(name); // 引用外部变量
  }
  return inner;
}
let fn = outer(); // 执行后,inner 仍然持有对 name 的引用
fn(); // 'closure'

5.2 闭包的作用与问题

  • 作用:数据封装/私有变量、模块模式、函数工厂、回调中保存状态。
  • 内存影响:闭包会保存外部函数的活动对象,导致外部函数执行完后无法立即释放内存(除非闭包被垃圾回收)。合理使用,避免过度闭包造成内存泄漏。

5.3 循环中的闭包陷阱(经典)

// 错误示例:期望输出 0,1,2,但输出 3,3,3
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 解决方案1:使用 let(块作用域)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 解决方案2:使用 IIFE 捕获 i 的值
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 0);
  })(i);
}

6. 立即调用的函数表达式(IIFE)

6.1 语法

(function() {
  console.log('IIFE');
})();
// 或
(function() {
  console.log('IIFE');
}());
  • 作用:创建一个独立的作用域,避免污染全局;常用于封装模块、提供私有变量。

7. 递归

7.1 递归函数

  • 函数调用自身。为避免硬编码函数名,建议使用 arguments.callee(非严格模式)或使用命名函数表达式。
// 命名函数表达式
let factorial = function f(n) {
  return n <= 1 ? 1 : n * f(n - 1);
};

7.2 尾递归优化(部分浏览器实现)

  • 递归最后一步调用自身,可能避免栈溢出,但目前支持有限。

8. 函数记忆(memoization)

  • 缓存函数的计算结果,避免重复计算。
function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

9. 常见面试题速查

  1. 函数声明与函数表达式的区别?
    → 声明会提升,表达式不会;表达式只能在定义后调用。

  2. 箭头函数与普通函数的区别?
    → 箭头函数无 thisargumentsprototype,不能 newthis 由外层词法环境决定。

  3. arguments 是什么?有什么特点?
    → 类数组对象,包含所有传入参数;与命名参数在非严格模式下联动;arguments.callee 可递归调用自身。

  4. 默认参数和剩余参数的使用场景?
    → 默认参数用于可选参数;剩余参数用于收集不定数量的参数为数组。

  5. callapplybind 的异同?
    → 都用来改变 thiscallapply 立即执行函数,传参方式不同;bind 返回新函数,不执行。

  6. 闭包是什么?有什么优缺点?
    → 能访问外部作用域的函数。优点:数据私有、模块化;缺点:可能造成内存泄漏(外部变量无法释放)。

  7. 如何解决循环中闭包的问题?
    → 使用 let(块作用域)或 IIFE 捕获循环变量当前值。

  8. IIFE 的作用是什么?
    → 创建独立作用域,避免变量污染,常用于隔离代码块。

  9. 递归函数如何避免函数名耦合?
    → 使用命名函数表达式或 arguments.callee(非严格模式)。

  10. new.target 的作用?
    → 检测函数是否被 new 调用,用于强制构造函数调用。

posted @ 2024-04-17 15:20  Li_pk  阅读(6)  评论(0)    收藏  举报