深入理解 JavaScript 函数:从《语言精粹》第四章看函数的精髓

深入理解 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 不推荐这种模式! 如果忘记写 newthis 会绑定到全局对象,造成难以发现的 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 对象

函数被调用时,除了声明的形参外,还会收到两个额外的参数:thisarguments

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 函数通过闭包持有对 levelnode 的引用,即使 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 函数 闭包 前端 《JavaScript语言精粹》 学习笔记


觉得有收获?点个赞鼓励一下吧!有问题欢迎评论区留言~ 👍

相关推荐
宋浮檀s6 小时前
DVWA通关教程2
运维·服务器·前端·javascript
皮卡祺q6 小时前
【算法-0】背包问题(三维+二维)
java·javascript·算法
whuhewei6 小时前
手写Promise
开发语言·javascript·ecmascript
川冰ICE7 小时前
JavaScript入门⑤|数组方法全攻略,map/filter/reduce三剑客
开发语言·javascript·ecmascript
threelab7 小时前
Three.js 抽象艺术着色器效果 | 三维可视化 / AI 提示词
前端·javascript·人工智能·3d·着色器
008爬虫实战录7 小时前
【码上爬】 题十八:模拟大厂加密算法, 堆栈分析找加密点,扣自执行函数,jsdom补环境
开发语言·javascript·ecmascript
skywalk81637 小时前
脚本 isMobile.js(移动设备检测库)的核心实现
开发语言·javascript·ecmascript
i_am_a_div_日积月累_7 小时前
3.contextBridge桥梁
前端·javascript·vue.js·electron
阿正的梦工坊8 小时前
【Typescript】04-数组元组枚举与字面量类型
javascript·ubuntu·typescript