编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

    • 基础入门

      • README
      • 入门介绍
      • 数据类型
      • 运算符
      • 函数
        • 4.1 函数定义
          • 4.1.1 函数声明
          • 4.1.2 函数表达式
          • 4.1.3 箭头函数
          • 4.1.4 函数重复声明
          • 4.1.5 函数提升的底层原理
          • 4.1.6 三种定义方式的对比
        • 4.2 属性和方法
          • 4.2.1 name属性
          • 4.2.2 length属性
          • 4.2.3 toString()
          • 4.2.4 方法
          • 4.2.5 函数是一等公民
        • 4.3 函数作用域
          • 4.3.1 作用域链的形成原理
          • 4.3.2 词法作用域与动态作用域
          • 4.3.3 执行上下文与变量对象
        • 4.4 函数参数
          • 4.4.1 默认参数
          • 4.4.2 剩余参数
          • 4.4.3 解构参数
          • 4.4.4 arguments对象
          • 4.4.5 参数传递机制:值传递的本质
        • 4.5 返回值
          • 4.5.1 基础用法
          • 4.5.2 返回函数
          • 4.5.3 返回Promise
          • 4.5.4 显式和隐式
        • 4.6 匿名函数
          • 4.6.1 匿名函数语法
          • 4.6.2 作为回调函数
          • 4.6.3 立即执行函数表达式
          • 4.6.4 赋值给变量
          • 4.6.5 作为对象方法
          • 4.6.6 动态生成函数
          • 4.6.7 匿名函数表达式
        • 4.7 闭包
          • 4.7.1 闭包定义
          • 4.7.2 闭包的形成
          • 4.7.3 闭包作用
          • 4.7.4 闭包注意事项
          • 4.7.5 经典面试题
          • 4.7.6 闭包与V8垃圾回收
        • 4.8 立即执行函数
          • 4.8.1 IIFE的设计由来
        • 4.9 回调函数
          • 4.9.1 回调地狱与解决方案
        • 4.10 异步函数
          • 4.10.1 普通异步函数
          • 4.10.2 导出异步函数
          • 4.10.3 async函数的底层原理
        • 4.11 函数绑定
          • 4.11.1 call绑定
          • 4.11.2 apply绑定
          • 4.11.3 bind绑定
          • 4.11.4 手写实现call/apply/bind
        • 4.12 this对象
          • 4.12.1 this绑定规则优先级
      • 面向对象
      • 标准库
      • 异步操作
      • 事件设计
      • 错误机制
      • 模块开发
      • 字符串处理
      • 迭代器与生成器
      • Symbol
      • DOM操作
      • 网络请求
    • 综合案例

    • 专栏博客

  • CodeX
  • JavaScript入门
  • 基础入门
杨充
2025-11-19
目录

函数

# 05.函数

# 目录介绍

  • 4.1 函数定义
    • 4.1.1 函数声明
    • 4.1.2 函数表达式
    • 4.1.3 箭头函数
    • 4.1.4 函数重复声明
    • 4.1.5 函数提升的底层原理
    • 4.1.6 三种定义方式的对比
  • 4.2 属性和方法
    • 4.2.1 name属性
    • 4.2.2 length属性
    • 4.2.3 toString()
    • 4.2.4 方法
    • 4.2.5 函数是一等公民
  • 4.3 函数作用域
    • 4.3.1 作用域链的形成原理
    • 4.3.2 词法作用域与动态作用域
    • 4.3.3 执行上下文与变量对象
  • 4.4 函数参数
    • 4.4.1 默认参数
    • 4.4.2 剩余参数
    • 4.4.3 解构参数
    • 4.4.4 arguments对象
    • 4.4.5 参数传递机制:值传递的本质
  • 4.5 返回值
    • 4.5.1 基础用法
    • 4.5.2 返回函数
    • 4.5.3 返回Promise
    • 4.5.4 显式和隐式
  • 4.6 匿名函数
    • 4.6.1 匿名函数语法
    • 4.6.2 作为回调函数
    • 4.6.3 立即执行函数表达式
    • 4.6.4 赋值给变量
    • 4.6.5 作为对象方法
    • 4.6.6 动态生成函数
    • 4.6.7 匿名函数表达式
  • 4.7 闭包
    • 4.7.1 闭包定义
    • 4.7.2 闭包的形成
    • 4.7.3 闭包作用
    • 4.7.4 闭包注意事项
    • 4.7.5 经典面试题
    • 4.7.6 闭包与V8垃圾回收
  • 4.8 立即执行函数
    • 4.8.1 IIFE的设计由来
  • 4.9 回调函数
    • 4.9.1 回调地狱与解决方案
  • 4.10 异步函数
    • 4.10.1 普通异步函数
    • 4.10.2 导出异步函数
    • 4.10.3 async函数的底层原理
  • 4.11 函数绑定
    • 4.11.1 call绑定
    • 4.11.2 apply绑定
    • 4.11.3 bind绑定
    • 4.11.4 手写实现call/apply/bind
  • 4.12 this对象
    • 4.12.1 this绑定规则优先级

# 4.1 函数定义

函数是一段可以反复调用的代码块。函数还能接受输入的参数,不同的参数有唯一对应的返回值。

函数的底层本质:在 JavaScript 引擎中,函数本质上是一个可调用的对象(Callable Object)。每个函数对象内部都有一个 [[Call]] 方法,当你使用 () 调用函数时,引擎实际调用的是这个内部方法。此外,用 function 声明或 function 表达式创建的函数还有 [[Construct]] 方法(可以被 new 调用),而箭头函数没有 [[Construct]],所以不能作为构造函数。

# 4.1.1 函数声明

使用 function 关键字定义函数。

函数声明会被提升(hoisted),可以在定义之前调用。

// 可以在声明前调用——函数提升
console.log(greet("Alice")); // Hello, Alice!

function greet(name) {
    return `Hello, ${name}!`;
}
1
2
3
4
5
6

# 4.1.2 函数表达式

函数表达式不会被提升,必须在定义之后调用。将函数赋值给变量。

const greet = function(name) {
    return `Hello, ${name}!`;
};

console.log(greet("Bob")); // Hello, Bob!
1
2
3
4
5

疑惑:函数声明和函数表达式看起来功能一样,为什么要有两种方式?

答疑:两者的核心区别在于提升行为不同。函数声明整体被提升到作用域顶部,而函数表达式只有变量声明被提升(值为 undefined),赋值在运行时才发生。

论证:

// 函数声明:整体提升
console.log(typeof foo); // "function"
function foo() { return 1; }

// 函数表达式:只有变量提升
console.log(typeof bar); // "undefined"
var bar = function() { return 2; };
1
2
3
4
5
6
7

结果展示:函数表达式更安全——避免了函数在声明前就被调用的潜在问题,配合 const 使用时还能防止函数被意外重新赋值。现代 JavaScript 推荐使用 const + 函数表达式或箭头函数。

# 4.1.3 箭头函数

使用 => 语法定义函数。箭头函数(ES6)

箭头函数的注意点:

  1. 如果形参只有一个,则小括号可以省略
  2. 函数体如果只有一条语句,则花括号可以省略,函数的返回值为该条语句的执行结果
  3. 箭头函数 this 指向声明时所在作用域下 this 的值,箭头函数不会更改 this 指向,用来指定回调函数会非常合适
  4. 箭头函数不能作为构造函数实例化
  5. 不能使用 arguments 实参

箭头函数的底层原理:箭头函数在 V8 引擎中的实现与普通函数有显著不同。普通函数在每次调用时会创建一个新的执行上下文(Execution Context),其中包含 this、arguments、new.target 等绑定。箭头函数不创建自己的 this 绑定,而是在创建时捕获外层作用域的 this 值,存储在其 [[Environment]] 内部属性中。这就是为什么箭头函数的 this 是"词法绑定"——它由定义位置决定,而非调用方式决定。

省略小括号的情况:

let fn = num => {
    return num * 10;
};
1
2
3

省略花括号的情况:箭头函数没有自己的 this,适合用于回调函数。

const greet = (name) => `Hello, ${name}!`;
console.log(greet("Charlie")); // Hello, Charlie!
1
2

箭头函数返回对象字面量的陷阱:

// 错误:花括号被解析为函数体
const getObj = () => { name: "Alice" };
console.log(getObj()); // undefined

// 正确:用圆括号包裹对象字面量
const getObj2 = () => ({ name: "Alice" });
console.log(getObj2()); // { name: "Alice" }
1
2
3
4
5
6
7

来看一个比较复杂的箭头函数:

const getStringLang = (key: string): string => {
  return translations[key] || ''; // 如果 key 不存在,返回空字符串
};

console.log(getStringLang("welcome")); // "Welcome"
console.log(getStringLang("hello"));   // ""
1
2
3
4
5
6
  • getStringLang:函数名。
  • (key: string):函数参数,名为 key,类型为 string。
  • : string:函数返回值类型为 string。
  • => { ... }:箭头函数体。
  • return '';:函数返回一个空字符串。

# 4.1.4 函数重复声明

如果同一个函数被多次声明,后面的声明就会覆盖前面的声明。

function f() {
  console.log(1);
}
f() // 2

function f() {
  console.log(2);
}
f() // 2
1
2
3
4
5
6
7
8
9

上面代码中,后一次的函数声明覆盖了前面一次。而且,由于函数名的提升(参见下文),前一次声明在任何时候都是无效的,这一点要特别注意。

# 4.1.5 函数提升的底层原理

疑惑:为什么函数声明可以在代码的任何位置调用,而函数表达式不行?

答疑:这是因为 JavaScript 引擎在执行代码前,会进行一个编译阶段。在这个阶段,引擎会扫描所有的声明,将它们注册到对应的作用域中。

论证:JavaScript 代码的执行分为两个阶段:

  1. 编译阶段(Creation Phase):引擎创建执行上下文,扫描当前作用域中的所有声明
    • function 声明:整体(函数名 + 函数体) 被添加到变量对象中
    • var 声明:变量名被添加到变量对象中,初始值为 undefined
    • let/const 声明:变量名被添加到词法环境中,但不初始化(TDZ)
  2. 执行阶段(Execution Phase):逐行执行代码
// 编译阶段后的等效代码
var bar;                          // var 提升,值为 undefined
function foo() { return 1; }     // 函数声明整体提升

console.log(foo()); // 1 —— 函数已完整存在
console.log(bar);   // undefined —— 变量已声明但未赋值
bar = function() { return 2; };
1
2
3
4
5
6
7

结果展示:函数声明提升优先于变量声明提升。如果同名的函数声明和变量声明同时存在,函数声明优先:

console.log(typeof a); // "function"(不是 "undefined")
var a = 1;
function a() {}
console.log(typeof a); // "number"(执行阶段 a 被赋值为 1)
1
2
3
4

# 4.1.6 三种定义方式的对比

特性 函数声明 函数表达式 箭头函数
提升 整体提升 仅变量名提升 仅变量名提升
this 绑定 动态绑定 动态绑定 词法绑定(定义时确定)
arguments 有 有 无(需用剩余参数)
可作为构造函数 是 是 否
prototype 属性 有 有 无
可用于 Generator 是 是 否

# 4.2 属性和方法

# 4.2.1 name属性

函数的name属性返回函数的名字。

function f1() {}
f1.name // "f1"

//如果是通过变量赋值定义的函数,那么`name`属性返回变量名。
var f2 = function () {};
f2.name // "f2"
1
2
3
4
5
6

name属性的一个用处,就是获取参数函数的名字。

var myFunc = function () {};

function test(f) {
  console.log(f.name);
}

test(myFunc) // myFunc
1
2
3
4
5
6
7

不同场景下的 name 属性:

// bind 返回的函数
function foo() {}
foo.bind({}).name; // "bound foo"

// 构造函数创建的函数
(new Function()).name; // "anonymous"

// Symbol 作为函数名
const sym = Symbol('description');
const obj = { [sym]() {} };
obj[sym].name; // "[description]"

// getter/setter
const descriptor = Object.getOwnPropertyDescriptor({
  get age() { return 25; }
}, 'age');
descriptor.get.name; // "get age"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 4.2.2 length属性

函数的length属性返回函数预期传入的参数个数,即函数定义之中的参数个数。

function f(a, b) {}
f.length // 2
1
2

上面代码定义了空函数f,它的length属性就是定义时的参数个数。不管调用时输入了多少个参数,length属性始终等于2。

length属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的"方法重载"(overload)。

length 的精确规则:length 只统计第一个默认参数之前的参数个数,不包含剩余参数:

function a(x, y, z) {}      // a.length = 3
function b(x, y = 1, z) {}  // b.length = 1(y 有默认值,从 y 开始不计)
function c(x, ...rest) {}   // c.length = 1(剩余参数不计)
function d(x = 1) {}        // d.length = 0
1
2
3
4

# 4.2.3 toString()

函数的toString()方法返回一个字符串,内容是函数的源码。

function f() {
  a();
  b();
  c();
}

f.toString()
// function f() {
//  a();
//  b();
//  c();
// }
1
2
3
4
5
6
7
8
9
10
11
12

上面示例中,函数f的toString()方法返回了f的源码,包含换行符在内。

ES2019 的改进:在 ES2019 之前,toString() 可能会省略注释和空白。ES2019 规范要求 toString() 必须返回函数的原始源代码,包括注释、空白等:

function /* 注释 */ foo() {
  // 函数体
}
foo.toString();
// "function /* 注释 */ foo() {\n  // 函数体\n}"
1
2
3
4
5

# 4.2.4 方法

方法是对象的属性,值为函数。

const person = {
    name: "Alice",
    greet: function() {
        return `Hello, ${this.name}!`;
    },
};

console.log(person.greet()); // Hello, Alice!
1
2
3
4
5
6
7
8

ES6 方法的简写语法:

const person = {
    name: "Alice",
    // ES6 简写方法——内部有 [[HomeObject]] 属性,可以使用 super
    greet() {
        return `Hello, ${this.name}!`;
    },
    // 等价的传统写法——没有 [[HomeObject]]
    greet2: function() {
        return `Hello, ${this.name}!`;
    }
};
1
2
3
4
5
6
7
8
9
10
11

ES6 简写方法和传统函数表达式的区别:简写方法有内部的 [[HomeObject]] 属性,可以使用 super 关键字访问原型,而传统函数表达式不行。

# 4.2.5 函数是一等公民

JavaScript 中函数是"一等公民"(First-Class Citizen),这意味着函数可以像其他值一样被使用:

// 1. 赋值给变量
const greet = function(name) { return `Hi, ${name}`; };

// 2. 作为参数传递
function execute(fn, value) {
  return fn(value);
}
execute(greet, "Alice"); // "Hi, Alice"

// 3. 作为返回值
function createGreeter(greeting) {
  return function(name) {
    return `${greeting}, ${name}!`;
  };
}
const hello = createGreeter("Hello");
hello("Bob"); // "Hello, Bob!"

// 4. 存储在数据结构中
const operations = {
  add: (a, b) => a + b,
  sub: (a, b) => a - b,
  mul: (a, b) => a * b,
};
operations.add(3, 4); // 7

// 5. 拥有属性(因为函数是对象)
function counter() { counter.count++; }
counter.count = 0;
counter(); counter(); counter();
console.log(counter.count); // 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 4.3 函数作用域

# 4.3.1 作用域链的形成原理

疑惑:JavaScript 是如何查找变量的?为什么内部函数能访问外部函数的变量?

答疑:JavaScript 使用词法作用域(Lexical Scope),变量的可访问性由代码的书写位置决定。引擎通过作用域链来查找变量。

论证:当引擎执行到一个函数时,会创建该函数的执行上下文(Execution Context)。每个执行上下文包含:

  1. 变量环境(Variable Environment):存储 var 声明和函数声明
  2. 词法环境(Lexical Environment):存储 let/const 声明
  3. 外部环境引用(Outer Reference):指向定义时的父作用域
var global = "全局";

function outer() {
  var outerVar = "外层";
  
  function inner() {
    var innerVar = "内层";
    console.log(innerVar);  // 在 inner 的变量环境中找到
    console.log(outerVar);  // inner 中没有 → 沿 Outer Reference 到 outer 中找到
    console.log(global);    // inner 和 outer 中都没有 → 继续沿链到全局找到
  }
  
  inner();
}

outer();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

结果展示:作用域链的形成是在函数定义时确定的(而非调用时),这就是"词法作用域"的含义。

# 4.3.2 词法作用域与动态作用域

var value = 1;

function foo() {
  console.log(value);
}

function bar() {
  var value = 2;
  foo(); // 输出 1,不是 2!
}

bar();
1
2
3
4
5
6
7
8
9
10
11
12

分析:foo 在全局作用域中定义,因此其外部环境引用指向全局作用域。无论 foo 在哪里被调用,它查找 value 时都会沿着定义时的作用域链查找,找到全局的 value = 1。

如果 JavaScript 采用动态作用域(像 Bash 那样),foo 会在调用位置的作用域中查找 value,结果会是 2。但 JavaScript 是词法作用域,结果是 1。

# 4.3.3 执行上下文与变量对象

V8 引擎管理执行上下文的完整流程:

1. 全局代码开始执行
   → 创建全局执行上下文,压入调用栈

2. 遇到函数调用 outer()
   → 创建 outer 的执行上下文,压入调用栈
   → 初始化变量环境(var 声明)
   → 初始化词法环境(let/const 声明)
   → 设置 Outer Reference 指向全局词法环境
   → 执行 outer 的代码

3. outer 中遇到 inner()
   → 创建 inner 的执行上下文,压入调用栈
   → Outer Reference 指向 outer 的词法环境
   → 执行 inner 的代码

4. inner 执行完毕 → 弹出调用栈
5. outer 执行完毕 → 弹出调用栈
6. 回到全局执行上下文
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 4.4 函数参数

# 4.4.1 默认参数

为参数设置默认值。

function greet(name = "Guest") {
    return `Hello, ${name}!`;
}

console.log(greet()); // Hello, Guest!
1
2
3
4
5

默认参数的求值时机:默认参数是在函数调用时求值的(惰性求值),不是在函数定义时:

let count = 0;
function getDefault() {
  return ++count;
}

function foo(x = getDefault()) {
  console.log(x);
}

foo();  // 1(第一次调用 getDefault)
foo();  // 2(第二次调用 getDefault)
foo(10); // 10(传了参数,不触发默认值求值)
1
2
3
4
5
6
7
8
9
10
11
12

默认参数有自己的作用域:

var x = 1;
function foo(x, y = function() { x = 2; }) {
  var x = 3;
  y();
  console.log(x); // 3(var x 是函数体的局部变量)
}
foo();
console.log(x); // 1(全局 x 未被修改)
1
2
3
4
5
6
7
8

# 4.4.2 剩余参数

使用 ... 语法将多个参数收集到一个数组中。

function sum(...numbers) {
    return numbers.reduce((acc, num) => acc + num, 0);
}

console.log(sum(1, 2, 3, 4)); // 10
1
2
3
4
5

剩余参数必须放在最后:

// 正确
function foo(first, second, ...rest) {
  console.log(first);  // 1
  console.log(second); // 2
  console.log(rest);   // [3, 4, 5]
}
foo(1, 2, 3, 4, 5);

// 错误:SyntaxError
// function bar(...rest, last) {}
1
2
3
4
5
6
7
8
9
10

# 4.4.3 解构参数

使用解构语法从对象或数组中提取参数。

function greet({ name, age }) {
    return `Hello, ${name}! You are ${age} years old.`;
}

console.log(greet({ name: "Alice", age: 25 })); // Hello, Alice! You are 25 years old.
1
2
3
4
5

解构参数与默认值的组合使用:

// 解构 + 默认值 + 整体默认值
function createUser({
  name = "Anonymous",
  age = 0,
  role = "user"
} = {}) {
  return { name, age, role };
}

console.log(createUser());                      // { name: "Anonymous", age: 0, role: "user" }
console.log(createUser({ name: "Alice" }));     // { name: "Alice", age: 0, role: "user" }
console.log(createUser({ name: "Bob", age: 25, role: "admin" }));
1
2
3
4
5
6
7
8
9
10
11
12

# 4.4.4 arguments对象

在非箭头函数中,arguments 是一个类数组对象,包含了函数调用时传入的所有参数:

function showArgs() {
  console.log(arguments.length); // 参数个数
  console.log(arguments[0]);     // 第一个参数
  
  // arguments 不是真正的数组,没有 map、filter 等方法
  // 转换为数组的方式:
  const arr1 = Array.from(arguments);
  const arr2 = [...arguments];
  const arr3 = Array.prototype.slice.call(arguments);
}

showArgs(1, 2, 3);
1
2
3
4
5
6
7
8
9
10
11
12

arguments 与参数的关系(非严格模式的坑):

function foo(a) {
  console.log(a === arguments[0]); // true
  arguments[0] = 99;
  console.log(a); // 99 —— 非严格模式下,arguments 和命名参数共享引用!
}

function bar(a) {
  "use strict";
  arguments[0] = 99;
  console.log(a); // 1 —— 严格模式下,arguments 是参数的副本
}

foo(1);
bar(1);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

现代 JavaScript 推荐使用剩余参数代替 arguments,因为:

  • 剩余参数是真正的数组
  • 语义更清晰
  • 箭头函数中可用

# 4.4.5 参数传递机制:值传递的本质

疑惑:JavaScript 函数参数是"值传递"还是"引用传递"?为什么修改对象参数时外部对象也会变?

答疑:JavaScript 始终是值传递。但对于对象类型,传递的"值"是对象的引用地址(指针的拷贝),而不是对象本身。

论证:

// 基本类型:传递值的副本
function changeValue(x) {
  x = 100;
}
let a = 1;
changeValue(a);
console.log(a); // 1 —— 未改变

// 对象类型:传递引用的副本
function changeProp(obj) {
  obj.name = "Bob";  // 通过引用修改对象属性——外部可见
}
let person = { name: "Alice" };
changeProp(person);
console.log(person.name); // "Bob" —— 改变了!

// 关键:重新赋值引用不影响外部
function replaceObj(obj) {
  obj = { name: "Charlie" }; // 这只是修改了局部变量 obj 的指向
}
replaceObj(person);
console.log(person.name); // "Bob" —— 未改变!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

结果展示:如果 JavaScript 是真正的"引用传递",replaceObj 应该能改变外部 person 的指向。但事实是不能——这证明了 JavaScript 是值传递,只不过传递的值是引用地址。

# 4.5 返回值

# 4.5.1 基础用法

使用 return 语句返回函数的结果。如果没有 return,函数默认返回 undefined。

function add(a, b) {
    return a + b;
}

console.log(add(2, 3)); // 5
1
2
3
4
5

return 的执行机制:return 语句会立即终止函数执行,函数中 return 之后的代码不会执行。但 finally 块中的 return 可以覆盖 try 块中的 return:

function foo() {
  try {
    return 1;
  } finally {
    return 2; // 会覆盖 try 中的 return
  }
}
console.log(foo()); // 2
1
2
3
4
5
6
7
8

# 4.5.2 返回函数

JavaScript 函数可以返回另一个函数,这种特性称为 高阶函数。

function createMultiplier(factor) {
  return function (number) {
    return number * factor;
  };
}

let double = createMultiplier(2);
console.log(double(5)); // 输出: 10
1
2
3
4
5
6
7
8

# 4.5.3 返回Promise

在异步编程中,函数可以返回一个 Promise 对象,用于处理异步操作的结果。

function fetchData(){
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data fetched!");
    }, 1000);
  });
}

fetchData().then((data) => {
    console.log(data); // 输出: Data fetched!
});
1
2
3
4
5
6
7
8
9
10
11

# 4.5.4 显式和隐式

function fetchData() : Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("Data fetched!");
        }, 1000);
    });
}

function fetchData(){
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data fetched!");
    }, 1000);
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

1. 第一种写法:带有显式类型声明

  • 显式类型声明:函数的返回类型被明确声明为 Promise<string>。
  • 类型检查:TypeScript 会检查函数是否返回了正确类型的值。如果 resolve 的值不是 string 类型,TypeScript 会报错。
  • 代码可读性:显式类型声明提高了代码的可读性和可维护性,开发者可以清楚地知道函数的返回类型。

2. 第二种写法:没有显式类型声明

  • 隐式类型推断:TypeScript 会根据 resolve 的值("Data fetched!")自动推断函数的返回类型为 Promise<string>。
  • 灵活性:如果 resolve 的值类型发生变化,TypeScript 会自动更新推断的类型。
  • 代码简洁:省略了类型声明,代码更简洁。

主要区别

特性 第一种写法(显式类型声明) 第二种写法(隐式类型推断)
类型声明 显式声明返回类型为 Promise<string> 无显式声明,TypeScript 自动推断
类型检查 严格检查返回值是否符合声明类型 根据 resolve 的值自动推断类型
代码可读性 更清晰,开发者明确知道返回类型 依赖 TypeScript 推断,可能不够直观
灵活性 如果返回值类型变化,需要手动更新类型声明 自动适应返回值类型的变化
适用场景 适合需要明确类型声明的场景 适合代码简洁、类型简单的场景

实际开发中的建议

  • 推荐使用显式类型声明:在团队协作或复杂项目中,显式类型声明可以提高代码的可读性和可维护性,减少潜在的错误。
  • 隐式类型推断适合简单场景:如果函数逻辑简单,且类型明确,可以使用隐式类型推断,减少代码冗余。

# 4.6 匿名函数

通过匿名函数表达式来定义一个函数。其语法形式为在 function 后跟可以省略的函数名、参数列表以及函数体。

# 4.6.1 匿名函数语法

匿名函数没有名称,直接通过 function 关键字定义。

const greet = function() {
  console.log("Hello, World!");
};

greet(); // 输出: Hello, World!
1
2
3
4
5

函数声明语句和匿名函数表达式在语法上几乎一模一样,唯一的区别仅仅是能否省略函数名称而已。

# 4.6.2 作为回调函数

匿名函数常用于作为回调函数,传递给其他函数。

setTimeout(function() {
  console.log("This runs after 1 second.");
}, 1000);
1
2
3

# 4.6.3 立即执行函数表达式

匿名函数可以立即执行,称为立即执行函数表达式(IIFE)。

(function() {
  console.log("This runs immediately.");
})();
1
2
3

IIFE 通常用于创建一个独立的作用域,避免变量污染全局命名空间。

# 4.6.4 赋值给变量

匿名函数可以赋值给变量,作为变量值使用。

const sayHello = function(name) {
  console.log(`Hello, ${name}!`);
};

sayHello("Alice"); // 输出: Hello, Alice!
1
2
3
4
5

# 4.6.5 作为对象方法

如果一个函数作为一个对象的属性保存,那么我们称这个函数是这个对象的方法,调用这个函数就说调用对象的方法(method)。

注意:方法和函数只是名称上的区别,没有其它别的区别

const person = {
  name: "Alice",
  greet: function() {
    console.log(`Hello, my name is ${this.name}.`);
  }
};

person.greet(); // 输出: Hello, my name is Alice.
1
2
3
4
5
6
7
8

JavaScript 的函数是一种对象,不过并不是说所有的对象都是函数。函数是一种包含了可执行代码,并能够被其他代码调用的特殊的对象。

# 4.6.6 动态生成函数

匿名函数可以动态生成,根据条件返回不同的函数。

const getGreeting = function(isFormal) {
  return isFormal
    ? function() { console.log("Good day to you!"); }
    : function() { console.log("Hey!"); };
};

const greeting = getGreeting(true);
greeting(); // 输出: Good day to you!
1
2
3
4
5
6
7
8

# 4.6.7 匿名函数表达式

如果需要在调试时追踪函数,可以使用命名函数表达式(Named Function Expression),它是匿名函数的一种变体。

const greet = function sayHello() {
  console.log("Hello, World!");
};

greet(); // 输出: Hello, World!
console.log(greet.name); // 输出: sayHello
1
2
3
4
5
6

匿名函数表达式在一些程序设计语言中并不存在(至少 Java 中的方法是无法实现这样的功能的)。

命名函数表达式的特殊性:函数名 sayHello 只在函数内部可见,外部无法访问:

const greet = function sayHello() {
  console.log(typeof sayHello); // "function"(内部可见)
  // 可以用于递归:sayHello()
};

console.log(typeof sayHello); // "undefined"(外部不可见)
1
2
3
4
5
6

这个特性使得命名函数表达式非常适合用于递归,即使外部变量被重新赋值也不会影响递归调用。

# 4.7 闭包

闭包(Closure) 是 JavaScript 中一个非常重要的概念,它允许函数访问其词法作用域(Lexical Scope)中的变量,即使函数在其词法作用域之外执行。

闭包是 JavaScript 强大功能的体现,广泛应用于模块化、数据封装、回调函数等场景。

# 4.7.1 闭包定义

闭包是指一个函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行。换句话说,闭包是函数和其词法环境的组合。

闭包的底层原理:在 V8 引擎中,每个函数在创建时都会携带一个 [[Environment]] 内部属性,指向创建时所在的词法环境(Lexical Environment)。词法环境包含一个环境记录(Environment Record)(存储局部变量)和一个外部环境引用(Outer Reference)(指向父作用域)。当内部函数引用了外部函数的变量时,即使外部函数已经执行完毕,其词法环境不会被垃圾回收——因为内部函数的 [[Environment]] 仍然引用着它。V8 的优化策略是:只有被内部函数实际引用的变量才会被保留在闭包中(称为闭包变量),未被引用的变量仍然可以被 GC 回收。

# 4.7.2 闭包的形成

闭包的形成需要满足以下条件:

  1. 函数嵌套:一个函数内部定义了另一个函数。
  2. 内部函数引用外部函数的变量。
  3. 内部函数在其词法作用域之外执行。
function outer() {
  const outerVar = 'I am from outer function';

  function inner() {
    console.log(outerVar); // 访问外部函数的变量
  }

  return inner;
}

const closureFunc = outer(); // 返回 inner 函数
closureFunc(); // 输出: I am from outer function
1
2
3
4
5
6
7
8
9
10
11
12

在上面的例子中:

  • inner 函数引用了 outer 函数的变量 outerVar。
  • 即使 outer 函数已经执行完毕,inner 函数仍然可以访问 outerVar,这就是闭包。

# 4.7.3 闭包作用

1.数据封装

闭包可以创建私有变量,避免全局污染。示例:

function createCounter() {
  let count = 0; // 私有变量

  return {
    increment: function () {
      count++;
      console.log(count);
    },
    decrement: function () {
      count--;
      console.log(count);
    },
  };
}

const counter = createCounter();
counter.increment(); // 输出: 1
counter.increment(); // 输出: 2
counter.decrement(); // 输出: 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

2.回调函数

闭包常用于回调函数中,保留上下文信息。示例:

function delayGreeting(name) {
  setTimeout(function () {
    console.log(`Hello, ${name}!`);
  }, 1000);
}

delayGreeting('Alice'); // 1秒后输出: Hello, Alice!
1
2
3
4
5
6
7

3.函数柯里化(Currying)

闭包可以用于实现函数柯里化,即将多参数函数转换为单参数函数。示例:

function add(a) {
  return function (b) {
    return a + b;
  };
}

const add5 = add(5);
console.log(add5(3)); // 输出: 8
1
2
3
4
5
6
7
8

4.函数记忆化(Memoization)

利用闭包缓存计算结果,避免重复计算:

function memoize(fn) {
  const cache = {};  // 闭包变量,在函数外部不可访问
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache[key] !== undefined) {
      console.log('从缓存中获取');
      return cache[key];
    }
    console.log('计算中...');
    const result = fn.apply(this, args);
    cache[key] = result;
    return result;
  };
}

const expensiveCalc = memoize((n) => {
  // 模拟耗时计算
  let sum = 0;
  for (let i = 0; i < n; i++) sum += i;
  return sum;
});

expensiveCalc(10000); // "计算中..." → 49995000
expensiveCalc(10000); // "从缓存中获取" → 49995000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 4.7.4 闭包注意事项

闭包会保留对其词法作用域的引用,可能导致内存泄漏。如果不再需要闭包,应手动释放引用。

function createHeavyClosure() {
  const largeArray = new Array(1000000).fill('data');

  return function () {
    console.log('Closure is still holding the largeArray');
  };
}
let heavyClosure = createHeavyClosure();
// 不再需要时,手动释放
heavyClosure = null;
1
2
3
4
5
6
7
8
9
10

# 4.7.5 经典面试题

以下代码会输出什么?如何修复?

for (var i = 1; i <= 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}
1
2
3
4
5

输出:

4
4
4
1
2
3

原因:var 是函数作用域,循环结束后 i 的值为 4,所有 setTimeout 回调都引用同一个 i。

修复:使用 let(块级作用域)或闭包。

方法 1:使用 let

for (let i = 1; i <= 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}
1
2
3
4
5

方法 2:使用闭包

for (var i = 1; i <= 3; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i);
    }, 1000);
  })(i);
}
1
2
3
4
5
6
7

方法 3:使用 setTimeout 的第三个参数

for (var i = 1; i <= 3; i++) {
  setTimeout(function (j) {
    console.log(j);
  }, 1000, i); // i 作为参数传给回调
}
1
2
3
4
5

# 4.7.6 闭包与V8垃圾回收

疑惑:闭包到底会不会导致内存泄漏?V8 引擎是如何处理闭包中的变量的?

答疑:V8 引擎对闭包有精细的优化。并非外部函数的所有变量都会被保留在闭包中——V8 在编译时会分析内部函数实际引用了哪些外部变量,只有被引用的变量才会被存储在**闭包对象(Closure Object)**中。

论证:

function outer() {
  const used = "我被引用了";
  const unused = new Array(1000000).fill("大数组");
  
  return function inner() {
    console.log(used); // 只引用了 used
  };
}

const fn = outer();
// unused 虽然在 outer 的作用域中,但 inner 没有引用它
// V8 会在 outer 执行完毕后回收 unused 占用的内存
1
2
3
4
5
6
7
8
9
10
11
12

结果展示:可以通过 Chrome DevTools 的 Memory 面板验证——使用 Heap Snapshot 对比闭包中实际保留的变量。但要注意:如果使用了 eval() 或 with,V8 无法进行静态分析,会保留外部函数的所有变量。

# 4.8 立即执行函数

立即执行函数(IIFE),定义后立即执行的函数。

(function() {
    console.log("IIFE executed!");
})();
1
2
3

# 4.8.1 IIFE的设计由来

疑惑:为什么需要 IIFE?直接写代码不行吗?

答疑:在 ES6 之前,JavaScript 只有函数作用域和全局作用域,没有块级作用域。IIFE 是在这个限制下创造出的"立即创建独立作用域"的技巧。

论证:IIFE 的多种写法和应用场景:

// 写法一:外层括号包裹(推荐)
(function() { /* ... */ })();

// 写法二:内层括号
(function() { /* ... */ }());

// 写法三:使用运算符(让引擎将 function 识别为表达式)
!function() { /* ... */ }();
+function() { /* ... */ }();
void function() { /* ... */ }();

// 经典应用:模块模式(ES6 之前的模块化方案)
var Module = (function() {
  var privateVar = 0;
  
  function privateMethod() {
    return ++privateVar;
  }
  
  return {
    publicMethod: function() {
      return privateMethod();
    },
    getCount: function() {
      return privateVar;
    }
  };
})();

Module.publicMethod(); // 1
Module.publicMethod(); // 2
Module.getCount();     // 2
// Module.privateVar   → undefined(外部不可访问)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

结果展示:jQuery、Lodash 等经典库在 ES6 之前都大量使用 IIFE 进行模块封装。ES6 的模块系统(import/export)和块级作用域(let/const)出现后,IIFE 的使用频率大大降低,但在需要立即执行且创建独立作用域的场景中仍然有用。

# 4.9 回调函数

作为参数传递给其他函数的函数。

function fetchData(callback) {
    setTimeout(() => {
        callback("Data received!");
    }, 1000);
}

fetchData((data) => {
    console.log(data); // Data received!
});
1
2
3
4
5
6
7
8
9

# 4.9.1 回调地狱与解决方案

疑惑:回调函数看起来很简单,为什么人们常说"回调地狱"?

答疑:当多个异步操作需要顺序执行时,回调嵌套会越来越深,形成"金字塔"结构,难以阅读和维护。

论证:

// 回调地狱示例
getUser(userId, function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetails(orders[0].id, function(details) {
      getShippingInfo(details.shippingId, function(shipping) {
        console.log(shipping);
        // 如果还需要更多异步操作...
      }, function(err) { console.error(err); });
    }, function(err) { console.error(err); });
  }, function(err) { console.error(err); });
}, function(err) { console.error(err); });
1
2
3
4
5
6
7
8
9
10
11

技术演变过程:

// 阶段1:回调函数(ES3)
fs.readFile('a.txt', function(err, data) { /* ... */ });

// 阶段2:Promise(ES6/2015)
readFile('a.txt')
  .then(data => process(data))
  .then(result => writeFile('b.txt', result))
  .catch(err => console.error(err));

// 阶段3:async/await(ES8/2017)
async function main() {
  try {
    const data = await readFile('a.txt');
    const result = await process(data);
    await writeFile('b.txt', result);
  } catch (err) {
    console.error(err);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

结果展示:从回调到 Promise 到 async/await,每一步都在解决前一代方案的可读性和可维护性问题。现代 JavaScript 推荐使用 async/await,它让异步代码看起来像同步代码。

# 4.10 异步函数

# 4.10.1 普通异步函数

使用 async 和 await 简化异步代码。

async function fetchData() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve("Data received!");
        }, 1000);
    });
}

async function main() {
    const data = await fetchData();
    console.log(data); // Data received!
}

main();
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 4.10.2 导出异步函数

使用了 export const 来导出一个常量函数 handleExceptionError。这个函数接收一个参数 errorType,其类型为 ERROR_TYPE。

export const handleExceptionError = async (errorType: ERROR_TYPE) => {
  // 函数体
};
1
2
3
  • export:将 handleExceptionError 函数导出,以便其他模块可以导入并使用它。
  • const:声明一个常量函数,确保函数不能被重新赋值。
  • async:表示这是一个异步函数,函数内部可以使用 await 关键字。
  • errorType: ERROR_TYPE:参数 errorType 的类型为 ERROR_TYPE(假设 ERROR_TYPE 是一个自定义类型或枚举)。

在实际应用中,handleExceptionError 可能会在 try-catch 块中调用,用于捕获和处理异常。

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
  } catch (error) {
    await handleExceptionError(ERROR_TYPE.NETWORK_ERROR);
  }
}
1
2
3
4
5
6
7
8

# 4.10.3 async函数的底层原理

疑惑:async/await 是怎么实现"暂停"和"恢复"函数执行的?JavaScript 不是单线程吗?

答疑:async/await 本质上是 Generator + Promise 的语法糖。await 并不会真正阻塞线程,而是将 await 之后的代码注册为 Promise 的回调(微任务),然后让出执行权给事件循环。

论证:

// async/await 写法
async function foo() {
  const a = await fetchA();
  const b = await fetchB(a);
  return a + b;
}

// 等价的 Generator + Promise 写法
function foo() {
  return spawn(function* () {
    const a = yield fetchA();
    const b = yield fetchB(a);
    return a + b;
  });
}

// spawn 是一个自动执行 Generator 的函数
function spawn(genFunc) {
  return new Promise((resolve, reject) => {
    const gen = genFunc();
    
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch (e) {
        return reject(e);
      }
      if (next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(
        (v) => step(() => gen.next(v)),
        (e) => step(() => gen.throw(e))
      );
    }
    
    step(() => gen.next(undefined));
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

结果展示:当执行到 await 时,引擎会:

  1. 将 await 后面的表达式包装为 Promise(如果不是的话)
  2. 将 await 之后的代码注册为该 Promise 的 .then() 回调
  3. 让出执行权,返回到调用者(事件循环继续执行其他任务)
  4. 当 Promise 决议后,微任务队列中的回调被执行,函数从 await 处恢复

# 4.11 函数绑定

在 JavaScript 中,函数绑定(Function Binding)是指将函数的 this 值显式地绑定到某个对象。

JavaScript 中的 this 值在函数调用时动态确定,因此通过绑定可以确保函数在特定上下文中执行。

  • call 和 apply:立即调用函数,并显式指定 this 值。
  • bind:返回一个绑定 this 的新函数,适合需要延迟调用的场景。
  • 箭头函数:无法绑定 this,它会捕获外层作用域的 this 值。

# 4.11.1 call绑定

call 方法允许你调用一个函数,并显式指定 this 的值,同时可以传递参数列表。

function.call(thisArg, arg1, arg2, ...);
1

示例

const person = {
  name: "Alice",
  greet: function (message) {
    console.log(`${message}, ${this.name}!`);
  },
};

const anotherPerson = { name: "Bob" };

// 使用 call 绑定 this 到 anotherPerson
person.greet.call(anotherPerson, "Hello"); // 输出: Hello, Bob!
1
2
3
4
5
6
7
8
9
10
11

# 4.11.2 apply绑定

apply 方法与 call 类似,但参数以数组形式传递。

function.apply(thisArg, [arg1, arg2, ...]);
1

示例

const person = {
  name: "Alice",
  greet: function (message) {
    console.log(`${message}, ${this.name}!`);
  },
};

const anotherPerson = { name: "Bob" };

// 使用 apply 绑定 this 到 anotherPerson
person.greet.apply(anotherPerson, ["Hi"]); // 输出: Hi, Bob!
1
2
3
4
5
6
7
8
9
10
11

call 与 apply 的经典用法:

// 借用 Array.prototype 方法处理类数组
function listArgs() {
  // arguments 是类数组,没有 join 方法
  const str = Array.prototype.join.call(arguments, '-');
  console.log(str);
}
listArgs('a', 'b', 'c'); // "a-b-c"

// 找数组最大值(ES6 之前)
const max = Math.max.apply(null, [1, 5, 3, 9, 2]); // 9
// ES6 之后可以用展开运算符
const max2 = Math.max(...[1, 5, 3, 9, 2]); // 9
1
2
3
4
5
6
7
8
9
10
11
12

# 4.11.3 bind绑定

bind 方法创建一个新的函数,并将 this 值永久绑定到指定的对象。与 call 和 apply 不同,bind 不会立即调用函数,而是返回一个绑定后的函数。

const boundFunction = function.bind(thisArg, arg1, arg2, ...);
1

示例

const person = {
  name: "Alice",
  greet: function (message) {
    console.log(`${message}, ${this.name}!`);
  },
};

const anotherPerson = { name: "Bob" };

// 使用 bind 绑定 this 到 anotherPerson
const boundGreet = person.greet.bind(anotherPerson);
boundGreet("Hey"); // 输出: Hey, Bob!
1
2
3
4
5
6
7
8
9
10
11
12

bind 的偏函数应用(Partial Application):

function multiply(a, b) {
  return a * b;
}

// bind 可以预设部分参数
const double = multiply.bind(null, 2);  // a 固定为 2
const triple = multiply.bind(null, 3);  // a 固定为 3

console.log(double(5));  // 10
console.log(triple(5));  // 15
1
2
3
4
5
6
7
8
9
10

bind 的不可重新绑定特性:

function foo() {
  console.log(this.name);
}

const bound1 = foo.bind({ name: "Alice" });
const bound2 = bound1.bind({ name: "Bob" }); // 再次 bind 无效!

bound1(); // "Alice"
bound2(); // "Alice"(仍然是 Alice,不是 Bob)
1
2
3
4
5
6
7
8
9

# 4.11.4 手写实现call/apply/bind

理解 call/apply/bind 的底层原理,最好的方式是手写实现:

// 手写 call
Function.prototype.myCall = function(context, ...args) {
  context = context == null ? globalThis : Object(context);
  const key = Symbol('temp');   // 用 Symbol 避免属性冲突
  context[key] = this;          // 将函数作为 context 的方法
  const result = context[key](...args); // 调用
  delete context[key];          // 清理
  return result;
};

// 手写 apply
Function.prototype.myApply = function(context, args = []) {
  context = context == null ? globalThis : Object(context);
  const key = Symbol('temp');
  context[key] = this;
  const result = context[key](...args);
  delete context[key];
  return result;
};

// 手写 bind
Function.prototype.myBind = function(context, ...outerArgs) {
  const fn = this;
  return function boundFunction(...innerArgs) {
    // 支持 new 调用:new 时 this 指向新创建的对象
    if (new.target) {
      return new fn(...outerArgs, ...innerArgs);
    }
    return fn.call(context, ...outerArgs, ...innerArgs);
  };
};

// 验证
function greet(greeting, punctuation) {
  console.log(`${greeting}, ${this.name}${punctuation}`);
}

greet.myCall({ name: "Alice" }, "Hello", "!");   // Hello, Alice!
greet.myApply({ name: "Bob" }, ["Hi", "?"]);     // Hi, Bob?
const bound = greet.myBind({ name: "Charlie" }, "Hey");
bound(".");                                        // Hey, Charlie.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 4.12 this对象

解析器在调用函数每次都会向函数内部传递进一个隐含的参数,这个隐含的参数就是this,this指向的是一个对象,这个对象我们称为函数执行的上下文对象,根据函数的调用方式的不同,this会指向不同的对象

以函数的形式调用时,this永远都是window

以方法的形式调用时,this就是调用方法的那个对象

this 绑定的底层原理:JavaScript 中的 this 不是在函数定义时确定的,而是在函数调用时根据调用方式动态绑定的。引擎内部通过调用记录(Call Record) 中的 thisValue 字段来传递 this。绑定规则优先级从高到低为:①new 绑定(this 指向新创建的对象);②显式绑定(call/apply/bind 指定的对象);③隐式绑定(作为对象方法调用时,this 指向该对象);④默认绑定(独立函数调用,非严格模式下为 window,严格模式下为 undefined)。箭头函数没有自己的 this,它会捕获定义时外层作用域的 this 值(词法 this)。

# 4.12.1 this绑定规则优先级

通过代码验证四种绑定规则的优先级:

// 规则1:默认绑定
function showThis() {
  console.log(this);
}
showThis(); // 非严格模式:window;严格模式:undefined

// 规则2:隐式绑定
const obj = {
  name: "Alice",
  showThis: function() {
    console.log(this.name);
  }
};
obj.showThis(); // "Alice"

// 隐式绑定丢失
const fn = obj.showThis;
fn(); // undefined(默认绑定生效)

// 规则3:显式绑定 > 隐式绑定
const obj2 = { name: "Bob" };
obj.showThis.call(obj2); // "Bob"(显式绑定覆盖隐式绑定)

// 规则4:new 绑定 > 显式绑定
function Foo(name) {
  this.name = name;
}
const bound = Foo.bind({ name: "Alice" });
const instance = new bound("Bob");
console.log(instance.name); // "Bob"(new 绑定覆盖了 bind)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//创建一个全局变量name
var name = "全局变量name";

//创建一个函数
function fun() {
    console.log(this.name);
}

//创建一个对象
var obj = {
    name: "孙悟空",
    sayName: fun
};

//我们希望调用obj.sayName()时可以输出obj的名字而不是全局变量name的名字
obj.sayName();    //打印结果:孙悟空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

箭头函数的 this —— 一劳永逸的解决方案:

class Timer {
  constructor() {
    this.count = 0;
  }
  
  // 问题:普通函数作为回调时 this 丢失
  startBroken() {
    setInterval(function() {
      this.count++; // this 指向 window,不是 Timer 实例
      console.log(this.count); // NaN
    }, 1000);
  }
  
  // 解决方案1:箭头函数
  startArrow() {
    setInterval(() => {
      this.count++; // this 指向 Timer 实例
      console.log(this.count);
    }, 1000);
  }
  
  // 解决方案2:bind
  startBind() {
    setInterval(function() {
      this.count++;
      console.log(this.count);
    }.bind(this), 1000);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
上次更新: 2026/06/10, 11:13:41
运算符
面向对象

← 运算符 面向对象→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式