彻底搞懂OOP(一):Closures 与继承链

576 阅读6分钟

✅OOP(Object Oriented Programming)

中文翻作「物件导向程式设计」,简单来说就是以 物件 (object)  为主体的程式设计风格。

这句话是否似曾相识呢?

没错,因为前三天提到的 FP (Functional Programming),就是以 函式 (function)  为主体的程式设计风格。

FP 是一种程式设计风格,OOP 则是另一种程式设计风格。

就好像 RPG 游戏里面玩骑士的角色擅长近距离战斗,但肯定也有不擅长的吧!所以我们会有其它角色像是弓箭手,擅长远距离战斗。

FP 跟 OOP 就像是上述的关系,FP 擅长处理「流程」相关的问题,那遇到「非流程」相关的问题该怎么办?就可以考虑用其它像是 OOP 这种,擅长解决不同问题的程式设计风格。

OOP 是以 object 为主体的思考,所以我们要先来学习,Javascript 跟物件相关的两个重点:Closures、Prototypal Inheritance

✅Closures

中文翻作「闭包」,它是 JavaScript 的一个资讯隐藏机制。

资讯隐藏

先从我们常见的物件当范例吧:

const person = {
    name: 'Joey',
    age: 20
};

有够普通的物件,但如果今天我们是个员工管理系统,可能需要纪录员工的薪水:

const person = {
    name: 'Joey',
    age: 20,
    salary: 40000,
};

我只要不小心印出 person.salary 就看光光了,甚至不小心 person.salary *= 3 就晋升中产阶级了。

即便不是这么刻意,也很容易因为 Object.entries(person) 这种不经意的列举,就把比较隐私的资料暴露出来。

这边所谓的「隐私」资料其实不一定是「不能被看到」,因为要是不能看干脆就不要放就好啦!这个「隐私」比较是「避免无意中被修改」。

私有特性 (private properties)

为了避免无意中被修改,我们需要让某些 property 是 private,但 Javascript 并没有提供这样的关键字,以往都只能用 coding 的惯例来约束这件事,比如在私有特性的名称前后缀一个底线 (_),比如_salary,告诉其他开发者,这是一个不能直接被检视、编辑的属性。

但很明显这种方式,防君子不防猪队友,如果要硬改绝对是可以,因此它来了 - Closures。

搭配函式实现 Closures

要使用 closures 的机制,必须透过 function 来产生 object,先把上面的范例改成用 function 来产生:

const createPerson = (name, age) => {
    const salary = 40000;
    const getSalary = () => salary;
    
    return {
        name,
        age,
        getSalary
    };
}
const person = createPerson('Joey', 20);

console.log(person.salary);
console.log(person.getSalary());

执行结果

undefined
40000

有发现这里发生很神奇的事情吗?乍看之下 salary 这个变数只有在 createPerson 函式的 scope 里面,而且又没有被回传,感觉在函式外应该无法取得才对。。。

Closures 的形成方式

在函式中可以定义另一个函式时,如果内部的函式参照了外部的函式的变数,一旦外部的函式被执行,则产生闭包。

以上面的范例来说,会做以下判断:

  1. createPerson里面的 salary 没有被存取
  2. 但是 salary 有出现在 getSalary 里面
  3. 而 getSalary 被回传到外面去了
  4. 此时 closures 机制启动,把 salary 放到封闭的变数环境内
  5. 若呼叫.getSalary(),就会到变数环境里面把 salary 抓出来 总结一句话就会是:内部函式变数有参照外部变数,就会产生 closures

Closures 机制会把资料储存在他们封闭起来的变数环境中,不提供对这些变数的直接存取。唯一的办法是,要在函式内明确提供存取它的方式。

Prototypal Inheritance

中文是「原型继承」。这东西大家应该就比较有概念了,因为这东西可能从学习 Javascript 的第一天,就已经在用继承的东西了。

阵列的身世之谜

比如说:

const arr = ['Jack', 'Allen', 'Alice'];

arr.forEach(item => console.log(item));

看起来很熟悉对吧!但你有想过吗?我们使用点运算子 ( .) 都是用在物件取得 property 对吧 (比如说person.name),那这明明是个阵列,为什么可以用arr.forEach

这边有个惊人的事实要告诉你:

const arr = ['Jack', 'Allen', 'Alice'];
console.log(typeof arr);

执行结果

"object"

是的各位观众,阵列在 Javascript 里面,被分类在 object 里面

其实前几天在讲阵列的时候就隐约有提过,阵列其实就是 key 比较特别的物件,因为阵列只能用数字当 key。

现在你知道有继承的概念了,就不难猜到,阵列就是继承物件产生的,所以阵列可以使用点运算子 ( .),也就一点都不奇怪了。

内建函式的源头?

即便知道阵列是其中一种物件,但我又没有在这个物件宣告 forEach 这个 property,为什么还是可以直接用?

那是因为虽然我们宣告阵列都偷懒使用中括号:

const arr = ['Jack', 'Allen', 'Alice'];

但其实它在背后是这样跑的:

const arr = new Array('Jack', 'Allen', 'Alice');

各位,「继承」的关键语法登场了你有看到吗?

继承的关键语法

隆重介绍!「继承」的关键语法就是new

new的功能是「产生物件」,但要产生什么物件呢?要在 new 后面放一个 constructor (建构子),定义要产生什么物件,可以想像建构子就是个工厂,专门量产物件用的,而建构子必须要是一个 function,我们拿上面 closures 提到的例子来改一下:

const Person = function (name, age) {
    const salary = 40000;
    this.name = name;
    this.age = age;
    this.getSalary = () => salary;
}
const person = new Person('Joey', 20);

console.log(person.salary);
console.log(person.getSalary());

执行结果

undefined
40000

注意几个重点:

  • 建构子 (constructor) 命名尽量使用大写开头(方便一眼识别)
  • 建构子函式必须是一般的 function,不可以用箭头函式
  • this关键字代表「这个物件」

从范例可以看到:

  1. 有一个建构子函式叫做Person
  2. 我透过在 new 的后面放上Person,产生了一个 Person 物件
  3. 并且把这个 Person 物件放到 person 变数

透过继承得到建构子的 property

如果以上你都有看懂,那能否回答我,为什么 person.getSalary() 可以执行呢?

思考的分界线

没错,因为我们在 Person 的这个建构子函式中,有宣告了 getSalary 这个 property,因此 person 才可以直接使用person.getSalary()

同理可证,上面那个 arr.forEach 的问题,你了解为什么 arr.forEach() 可以执行了吗: )?

继承链

把原型想像成有一条链子,链子上勾着一个个物件,而每个物件的原型都指向前面的物件 (可以透过 .proto 找到上层):

const arr = [];
console.log(arr);
console.log(arr.__proto__); // Array 的原型
console.log(arr.__proto__.__proto__); // Object 的原型
console.log(arr.__proto__.__proto__.__proto__); // null

可以看到 arr 的 property 是一层一层继承下来的,所以它才会同时有 Array 跟 Object 的原生属性。

换句话说,原本你可以直接用arr.forEach,但如果打断原型链,就会突然没了爸爸的庇护 (?):

const arr = [];
console.log(arr.forEach);

arr.__proto__ = null;
console.log(arr.forEach);

执行结果

ƒ forEach() { [native code] }
undefined

所以没事不要乱断绝父子关系啊 (?),会少了很多援军啊!

结语

今天开始了 OOP 之旅,但其实也是借 OOP 的名义,来学习一些跟 object 相关的进阶观念,当然其实是满复杂的,平常很多 method 我们用得很顺,也不曾认真去思考它从何而来。

当然我们不是来学考古的,但那是因为要进到 OOP 的领域,有一些先备知识需要先掌握,明天才会如鱼得水!

真正要上场实战之前,还是要先蹲一下马步啰!