深入理解 JavaScript 函数:从《语言精粹》第四章看函数的精髓
函数是 JavaScript 的基础单元。所谓编程,就是将一组需求分解为一组函数与数据结构。本文将带你系统掌握 JavaScript 函数的核心概念。
前言
《JavaScript语言精粹》第四章是全书最精华的章节之一。Douglas Crockford 在这一章中深入剖析了 JavaScript 函数的本质------函数就是对象,并系统讲解了函数调用的四种模式、闭包、作用域、记忆等核心概念。
这些知识不仅是理解 JavaScript 的基石,也是大厂面试的高频考点。今天把学习笔记整理分享给你。
一、函数对象:函数的本质
1.1 函数就是对象
在 JavaScript 中,函数就是对象 。更准确地说,函数是连接到 Function.prototype 的对象,而 Function.prototype 本身又连接到 Object.prototype。
javascript
原型链关系
普通对象: myObj ──▶ Object.prototype
函数对象: myFunc ──▶ Function.prototype ──▶ Object.prototype
每个函数在创建时都附带了三个隐藏属性:
| 属性 | 说明 |
|---|---|
| 函数上下文 | this 的绑定 |
| 函数代码 | 实现函数行为的代码 |
| prototype | 拥有 constructor 属性的对象 |
💡 JavaScript 是第一个成为主流的 Lambda 语言。相对于 Java 而言,JavaScript 与 Lisp 和 Scheme 有更多的共同点。
二、函数调用的四种模式
这是第四章最重要的内容。JavaScript 中一共有 四种调用模式 ,它们在如何初始化 this 上存在差异。
2.1 方法调用模式
当一个函数被保存为对象的属性时,它被称为方法 。调用时,this 绑定到该对象。
javascript
var myObject = {
value: 0,
increment: function(inc) {
this.value += typeof inc === 'number' ? inc : 1;
}
};
myObject.increment();
console.log(myObject.value); // 1
myObject.increment(2);
console.log(myObject.value); // 3
📌 关键点 :
this到对象的绑定发生在调用的时候,而不是定义的时候。
2.2 函数调用模式
当一个函数并非对象的属性时,它被当作函数 来调用。此时 this 绑定到全局对象。
javascript
var add = function(a, b) {
return a + b;
};
var sum = add(3, 4); // 7
⚠️ 这是一个设计错误! 当内部函数被调用时,this 也会绑定到全局对象,导致无法访问外部方法的 this。
经典解决方案------that 变量:
javascript
myObject.double = function() {
var that = this; // 保存外部 this 的引用
var helper = function() {
that.value = add(that.value, that.value);
};
helper(); // 以函数形式调用,this 不再可靠
};
myObject.double();
console.log(myObject.value); // 6
💡
that是一个约定俗成的命名 ,用来在内部函数中访问外部this。在现代 JavaScript 中,箭头函数可以更优雅地解决这个问题。
2.3 构造器调用模式
在函数前加上 new 来调用,会创建一个新对象,this 绑定到该新对象。
javascript
var Quo = function(string) {
this.status = string;
};
Quo.prototype.get_status = function() {
return this.status;
};
var myQuo = new Quo('confused');
console.log(myQuo.get_status()); // 'confused'
⚠️ Crockford 不推荐这种模式! 如果忘记写
new,this会绑定到全局对象,造成难以发现的 bug。
推荐的替代方案------闭包实现:
javascript
var quo = function(status) {
return {
get_status: function() {
return status;
}
};
};
var myQuo = quo('amazed');
console.log(myQuo.get_status()); // 'amazed'
2.4 Apply 调用模式
apply 方法允许我们手动指定 this 的值,并传递参数数组。
javascript
var add = function(a, b) {
return a + b;
};
// 两个参数:this 绑定值 + 参数数组
var array = [3, 4];
var sum = add.apply(null, array); // 7
Apply 的强大之处------借用方法:
javascript
var statusObject = {
status: 'A-OK'
};
// statusObject 没有继承 Quo.prototype
// 但我们可以"借用" get_status 方法
var status = Quo.prototype.get_status.apply(statusObject);
console.log(status); // 'A-OK'
2.5 四种模式对比
| 调用模式 | this 绑定 | 典型场景 |
|---|---|---|
| 方法调用 | 所属对象 | 对象方法 |
| 函数调用 | 全局对象(⚠️设计缺陷) | 普通函数 |
| 构造器调用 | 新创建的对象 | 创建实例(不推荐) |
| Apply 调用 | 手动指定 | 借用方法、参数数组 |
三、参数与返回值
3.1 arguments 对象
函数被调用时,除了声明的形参外,还会收到两个额外的参数:this 和 arguments。
javascript
var sum = function() {
var i, sum = 0;
for (i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum;
};
console.log(sum(1, 2, 3, 4)); // 10
📝
arguments不是真正的数组,而是一个类数组对象。现代代码推荐使用剩余参数...args。
3.2 返回值
- 如果函数指定了
return,返回该值 - 如果没有
return,返回undefined - 如果以
new调用且返回值不是对象,则返回this
四、扩充类型的功能
4.1 给基本类型添加方法
JavaScript 允许给语言的基本类型扩充功能。通过给 Function.prototype 添加一个 method 方法,可以简化后续操作:
javascript
Function.prototype.method = function(name, func) {
this.prototype[name] = func;
return this;
};
4.2 实际应用
javascript
// 给 Number 添加取整方法
Number.method('integer', function() {
return Math[this < 0 ? 'ceil' : 'floor'](this);
});
console.log((-10 / 3).integer()); // -3
// 给 String 添加去除首尾空格的方法
String.method('trim', function() {
return this.replace(/^\s+|\s+$/g, '');
});
💡 现代提示:ES6/ES7 中
Math.trunc()和String.prototype.trim()已经内置,不需要手动扩展了。但理解这个原理对学习原型链非常重要。
五、作用域
5.1 词法作用域
JavaScript 使用词法作用域 (也称静态作用域),意味着变量的作用域在定义时就确定了,而不是在调用时。
javascript
var foo = function() {
var a = 3, b = 5;
var bar = function() {
var b = 7, c = 11;
// 此处:a = 3, b = 7, c = 11
a += b + c; // a = 21
};
// 此处:a = 3, b = 5, c = undefined
bar();
// 此处:a = 21, b = 5(bar 内部的 b 不影响外部)
};
5.2 作用域链示意
ini
bar 的作用域链
bar() ──▶ foo() ──▶ 全局作用域
│ │
│ b = 7 │ b = 5
│ c = 11 │ a = 3
└───────────┘
先在自己的作用域找,找不到就沿链向上找
⚠️ JavaScript 没有块级作用域 (ES6 之前)。
var声明的变量在函数内任何位置都可见,建议将变量声明放在函数顶部。ES6 的let/const解决了这个问题。
六、闭包:JavaScript 最强大的特性
6.1 什么是闭包?
闭包是指一个函数可以访问它被创建时所处的上下文环境,即使这个函数在其原始作用域之外执行。
javascript
var quo = function(status) {
return {
get_status: function() {
return status; // 访问的是 status 本身,不是副本
}
};
};
var myQuo = quo('amazed');
console.log(myQuo.get_status()); // 'amazed'
get_status 方法并没有访问 status 的副本,它访问的就是 status 本身。这就是闭包的力量。
6.2 实战案例:渐变动画
javascript
var fade = function(node) {
var level = 1;
var step = function() {
var hex = level.toString(16);
node.style.backgroundColor = '#FFFF' + hex + hex;
if (level < 15) {
level += 1;
setTimeout(step, 100);
}
};
setTimeout(step, 100);
};
fade(document.body);
为什么这段代码能工作?
scss
fade() 执行完毕后:
┌─────────────────────────────┐
│ 闭包保留的变量 │
│ ├── node (DOM 节点引用) │
│ └── level (当前渐变等级) │
│ │
│ step 函数仍然可以访问这些变量 │
│ 即使 fade 已经执行完毕 │
└─────────────────────────────┘
step 函数通过闭包持有对 level 和 node 的引用,即使 fade 函数已经返回,step 仍然可以正常工作。
七、模块模式
7.1 什么是模块模式?
模块模式利用闭包来创建私有变量和特权方法:
javascript
var module = function() {
// 私有变量
var privateVar = 0;
// 私有函数
var privateFunction = function() {
privateVar += 1;
};
// 返回特权方法(暴露给外部的接口)
return {
increment: function() {
privateFunction();
},
getValue: function() {
return privateVar;
}
};
}();
module.increment();
module.increment();
console.log(module.getValue()); // 2
console.log(module.privateVar); // undefined ------ 无法直接访问
7.2 模块模式的价值
sql
模块模式结构
┌──────────────────────────────────────┐
│ 模块(Module) │
│ ┌──────────────────────────────┐ │
│ │ 私有变量 / 私有函数 │ │
│ │ 外部无法直接访问 │ │
│ └──────────────────────────────┘ │
│ ┌──────────────────────────────┐ │
│ │ 特权方法(返回的对象) │ │
│ │ 外部可以调用,间接访问私有成员 │ │
│ └──────────────────────────────┘ │
└──────────────────────────────────────┘
🎯 模块模式是 JavaScript 中实现信息隐藏 和封装的经典方式,也是现代模块系统(CommonJS、ES Modules)的思想源头。
八、记忆(Memoization)
8.1 什么是记忆?
函数可以将先前操作的结果保存在对象中,从而避免无谓的重复运算。这种优化被称为记忆。
8.2 Fibonacci 数列的性能问题
javascript
// 朴素递归实现
var fibonacci = function(n) {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
};
// 计算 fibonacci(10) 时,函数被调用了 453 次!
// 其中大量是重复计算
scss
fibonacci(5) 的调用过程
fib(5)
/ \
fib(4) fib(3) ← fib(3) 被计算了 2 次
/ \ / \
fib(3) fib(2) fib(2) fib(1) ← fib(2) 被计算了 3 次
/ \
fib(2) fib(1)
大量重复计算 → 指数级时间复杂度
8.3 用闭包实现记忆
javascript
var fibonacci = function() {
var memo = [0, 1]; // 存储已计算的结果
var fib = function(n) {
var result = memo[n];
if (typeof result !== 'number') {
result = fib(n - 1) + fib(n - 2);
memo[n] = result; // 缓存结果
}
return result;
};
return fib;
}();
// 现在 fibonacci(10) 只需要调用 29 次!
8.4 通用记忆函数(memoizer)
将记忆模式抽象为一个通用函数:
javascript
var memoizer = function(memo, formula) {
var recur = function(n) {
var result = memo[n];
if (typeof result !== 'number') {
result = formula(recur, n);
memo[n] = result;
}
return result;
};
return recur;
};
// 用 memoizer 定义 fibonacci
var fibonacci = memoizer([0, 1], function(recur, n) {
return recur(n - 1) + recur(n - 2);
});
// 用 memoizer 定义阶乘
var factorial = memoizer([1, 1], function(recur, n) {
return n * recur(n - 1);
});
🚀 性能对比 :记忆化将 fibonacci(10) 的调用次数从 453 次降到了 29 次,这就是算法优化的力量。
九、级联(Cascade)
如果一些方法没有返回值,可以让它们返回 this 而不是 undefined,从而实现链式调用:
javascript
// 不返回 this
getElement(id).setColor('red').setHeight('100px');
// 每个方法都返回 this,实现链式调用
var builder = {
name: '',
setName: function(n) {
this.name = n;
return this; // 返回 this
},
setAge: function(a) {
this.age = a;
return this; // 返回 this
},
build: function() {
return this.name + ', ' + this.age;
}
};
var result = builder.setName('张三').setAge(25).build();
console.log(result); // '张三, 25'
💡 这种模式在现代 JavaScript 库中广泛使用,jQuery 就是经典的代表:
$('#id').css('color', 'red').show()。
十、知识图谱:第四章核心要点
kotlin
📚 《JavaScript语言精粹》第四章 ------ 函数
核心概念
├── 函数就是对象
├── 四种调用模式
│ ├── 方法调用(this → 对象)
│ ├── 函数调用(this → 全局)
│ ├── 构造器调用(this → 新对象)
│ └── Apply 调用(this → 手动指定)
│
进阶特性
├── 作用域(词法作用域)
├── 闭包(访问创建时的上下文)
├── 模块模式(私有变量 + 特权方法)
├── 记忆(Memoization 缓存计算结果)
├── 级联(返回 this 实现链式调用)
└── 扩充类型(给原型添加方法)
关键技巧
├── that 变量解决内部函数 this 问题
├── apply 借用其他对象的方法
├── memoizer 通用记忆化函数
└── 闭包实现数据私有化
结语
第四章的内容是 JavaScript 最核心的知识之一。从函数对象的本质,到四种调用模式的 this 绑定规则,再到闭包和记忆等高级特性,每一个概念都值得深入理解。
特别是闭包,它不仅是面试必考题,更是理解 JavaScript 设计模式、模块化、函数式编程的基础。
掌握这些知识后,再去学习 Vue、React 等框架,你会发现很多设计都能在这里找到根源。
希望这篇文章对你有帮助!有任何问题欢迎在评论区交流。
📌 相关阅读
- 《JavaScript语言精粹》第三章:对象
- MDN: Closures
- MDN: this
📌 文章标签
JavaScript函数闭包前端《JavaScript语言精粹》学习笔记
觉得有收获?点个赞鼓励一下吧!有问题欢迎评论区留言~ 👍