✅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 的形成方式
在函式中可以定义另一个函式时,如果内部的函式参照了外部的函式的变数,一旦外部的函式被执行,则产生闭包。
以上面的范例来说,会做以下判断:
createPerson
里面的salary
没有被存取- 但是
salary
有出现在getSalary
里面 - 而
getSalary
被回传到外面去了 - 此时 closures 机制启动,把
salary
放到封闭的变数环境内 - 若呼叫
.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
关键字代表「这个物件」
从范例可以看到:
- 有一个建构子函式叫做
Person
- 我透过在 new 的后面放上
Person
,产生了一个Person
物件 - 并且把这个
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 的领域,有一些先备知识需要先掌握,明天才会如鱼得水!
真正要上场实战之前,还是要先蹲一下马步啰!