`this`、`new` 与面向对象编程简介
学习面向对象编程的概念及其在 JavaScript 中的实现。我们将探讨 OOP 的理论,并讨论代码中关键字 'this' 和 'new' 的含义。
面向对象编程的入门介绍
从最高层次来看,面向对象编程(OOP)可以被理解为一种将数据和功能打包成一个可操作的整体的尝试。其核心思想是:与某段信息相关的所有数据以及操作该信息的功能(如修改、记录、删除等)应当构成一个自包含的单元。这是一种以数据为中心的编程模型。
这种设计能够避免许多问题,例如全局变量之间的冲突,以及代码分散在文件各处导致难以阅读和追踪。
在 JavaScript 中,我们用来封装所有数据和函数的“容器”就是一个普通的对象。
关键字 this 和 new 是开发者识别是否使用了面向对象编程的重要信号。它们表明我们正在创建并操作一组封装好的信息。
关于 'this' 的概念
this 与面向对象编程密切相关。对于许多开发者而言,即使有多年经验,this 也常常是一个令人困扰的概念。它在 JavaScript 中的行为与其他语言有所不同。
我们可以用以下原则作为指导:在 JavaScript 中,函数内部的 this 应当指向该函数所属的对象。
JavaScript 中关于 this 的规则看似有些奇怪,但只要理解了 this 在编程语言中的存在意义,就能更容易地掌握它的机制。this 的作用是将开发者的注意力集中到唯一一个特定的对象上。当使用 this 时,实际上是在强调:这个函数所要操作的核心对象就是 this。
探究 'this'
在 JavaScript 中,每一个函数都是在某个对象上调用的,因此每个函数中 this 都有值。我们先从一个普通的函数调用开始。
自由函数调用中的无用 this
1function fn() { 2 // 做一些事情... 3} 4 5fn();
虽然看起来 fn 是自由调用的,没有绑定到任何对象,但实际上 JavaScript 引擎会将其存储为全局对象的一个属性。在浏览器环境中,全局对象是 window。如果我们打开浏览器控制台,输入上述代码,并添加以下一行,就可以验证 fn 确实是 window 的属性:
1console.log(window.fn === fn); // -> true
我们知道,当比较函数或其他对象时,相等运算符检查的是引用,因此可以确定 fn 和 window.fn 是同一个对象。当我们省略 window 时,JavaScript 会自动在背后插入它。
这对 this 意味着什么?根据我们的指导原则,函数中的 this 应该代表调用该函数的对象。在直接调用 fn() 的情况下,我们正好看到这一点。
1function fn() { 2 console.log(this); 3} 4 5fn(); // -> Window {frames: Window, postMessage: ƒ, …}
我们发现 this 等于 window,这完全符合我们的原则:它代表了函数被调用的对象。
这种不绑定到任何特定对象的函数调用方式被称为 自由函数调用(Free Function Invocation)。在 FFI 中,this 的值就是全局对象。
为什么这里无用?
正如我们所见,让 this 指向全局对象并没有太大实际用途。如果想在函数内部设置全局变量(这本应避免),完全可以省略 var、let 或 const 关键字。
如果需要访问全局作用域中的内容,也不需要依赖 this。如果真有必要访问全局作用域,可以直接使用 window,或当前环境下的全局对象名称。
理想情况下,自由调用的函数内部的 this 应该是 undefined。可惜现实并非如此,我们必须面对由此带来的代码可读性和理解上的挑战。
我们已经了解了 this 的无用场景。虽然我们不会在实际开发中这样使用它,但必须清楚这种机制的存在。接下来,我们进入 this 在面向对象编程中真正的用法。
方法调用
一个作为对象属性的函数被称为 方法。严格来说,所有函数都可以看作方法,因为自由函数本质上会被隐式当作全局对象的方法。让我们看一个显式定义为某个对象方法的例子:
1const obj = { 2 str: 'Hello!', 3 fn: function() { 4 console.log(this); 5 } 6}; 7 8obj.fn(); // -> { str: 'Hello!', fn: [Function: fn] }
函数是 obj 的属性。当我们调用 obj.fn() 时,函数内部的 this 等于对象 obj。这再次符合我们的原则:函数中的 this 应该是该函数所属的对象。
假设我们希望在 obj 上添加一个计数器。我们需要一个方法来递增计数器。
1const obj = { 2 counter: 0, 3 incrementCounter: function() { 4 this.counter++; 5 this.logCounter(); 6 }, 7 logCounter: function() { 8 console.log(this.counter); 9 } 10}; 11 12obj.incrementCounter(); // -> 1 13obj.incrementCounter(); // -> 2 14obj.incrementCounter(); // -> 3
我们已经开始体会到面向对象编程的核心思想:对象被构造成一个自包含的整体,应通过其自身的方法来操作和管理内部状态。
new 的引入
new 是另一个在面向对象编程中使用的关键词。new 用于以不同于常规的方式调用函数。它的作用是明确表示:我们希望函数返回一个对象。
new 表明我们意图使用面向对象编程,且函数返回的对象将包含一组封装的数据,可能还附带一些操作这些数据的方法。
该关键字也保留了其原始含义——使用 new 时,总会返回一个全新的、刚构造出来的对象,拥有独立的引用。这个对象会在函数内部被创建出来。
1function fn() { 2} 3 4console.log(fn()); // -> undefined 5console.log(new fn()); // -> fn {}
尽管 fn 内部没有任何代码,但使用 new 调用时,它仍然会自动返回一个新对象。关于这些规则和细节,我们将在下一课深入探讨。目前,我们只需知道:new 与面向对象中对象的生成密切相关。
注意,上面第5行代码输出的对象在控制台显示为 fn {}。如果通过 new 创建了一个对象,控制台会打印出创建该对象的函数名,然后是对象本身。
这一小节的面向对象理论知识,应该已为你准备好迎接接下来的学习内容。