这里我们继续学习两个比较重要的类型,就是 Object
和 Symbol
。我们主要讲的是 Object,相对 Object 来说 Symbol 只是一个配角。
关于对象这个概念大家非常早就会接触到了,其实人大概在 5 岁的时候就会产生对象的抽象。很多时候我们看起来好像对象是我们学编程的时候才知道有面向对象。但是从认知的角度来说,应该是比我们平时对数字中的值这个类型的认知要早的多。所以历史的角度也一直被评价为,对象是更贴近人类的自然思维的。
刚刚说到我们从小时候就已经产生了对象的概念了,那为什么说从小就有呢?Object 在英文里其实它的意思是一个非常广泛的东西,他是任何一个物体,可以是抽象的物体,也可以是一个实际的物体。但是在我们中文里,找不到一个合适的词,可以代表保罗万物的词来表达 Object 的含义。所以在中文中我们就直接翻译成 “对象”。
所以这个中文翻译过来的词,就造成了我们对 Object 的一定误解。因为对象在英文中,我觉得更接近 target 这个单词的意思。其实在台湾就会把 Object 翻译成 “物件”。物件这个词在语义上确实会更贴合一些,但是物件这个词大家也不是特别熟悉,所以它就演变成了一个技术的专用名词。
但是不论如何,我们脑子里面应该是有这么一个概念的,从小我们就应该知道我们有三条一模一样的鱼,但是其实他是三个不同的对象。那为什么一模一样的鱼,他们是不同的对象呢?
我们可以这么理解哈,突然有一天其中一条鱼的尾巴被咬掉了。很惊奇的发现,另外两条鱼并不会受到影响。因此,当我们在计算机中描述这三条鱼的时候,那肯定是三组相同的数据的对象,但是是单独储存了三份,互相独立的。
这种鱼和鱼之间的区别其实就是,他们的对象的一个特性的体现。一些认知学的研究认为我们人在小时候大概 5 岁的时候就有
这样的认知了,其实现在的孩子发育的比较早,5岁已经是一个最低的年龄了。2 ~ 3 岁的时候大家都知道这个苹果和那个苹果是不一样的,这个咬一口,另外一个苹果安然无事。
所以如果我们在计算机里面描述这三条鱼的时候,我们就必须要把数据单独存储三份,因为是三个对象的状态,而不是我们把同一个数据存了三份,而是恰巧他们是相等而已。其实这个正是所有的面向对象编程的一个基础,也就是说,他是这条鱼就是这条鱼,不是这条鱼就不是这条鱼,不会因为对象本身的状态改变而变得有区别。
所以我们对对象的认知是?
任何一个对象都是唯一的,这与它本身的状态无关,状态是由对象决定的
即使状态完全一致的两个对象,也并不相等。所以有时候我们会把对象当数据用,但是这个其实是一种语言的使用技巧而已,并不是把对象当做对象用,比如我们传一个 config,其实传 config 的过程其实它并不是把对象当对象去传,而是我们把对象当成一种数据载体去传。这个时候就涉及到我们对对象类型的使用,跟语言本身的设计用途的偏差。
我们用状态来描述对象,比如我们有一个对象 “鱼”,然后他的状态就是,它有没有 “尾巴”、“眼睛多大”,我们都会用这些状态值来描述一个对象。
我们的状态的改变既是行为,状态的改变就是鱼的尾巴没有了,被咬掉了。然后过了一段时间它又长出一条新尾巴了,然后尾巴还可以来回摆动。这些都属于它的状态的改变。而这些状态的改变都是行为。
其实哲学家他们就会研究一个 Object,比如鱼的唯一标识是什么,这条鱼的骨头全部挑出来看还是不是这条鱼。然后把肉都切下来,再拼起来看是不是这一条鱼,这就是著名的哲学问题 “忒修斯之船”。
这个我们就不用关心,我们就说变量它是有一个唯一标识性,这个也是对象的一个核心要素具备了。
对象就要有状态,状态是可以被改变的,改变就是行为。这样对象的三要素就成立了。
我们脑子里的任何一个概念和现实中的任何一个物品,都可以成为一个对象,只要三要素是齐备的。
首先
Class
类 和Type
类型是两个不一样的概念。
我们认识对象的一个重要的方式叫做分类,我们可以用分类的方式去描述对象。比如我们研究透测一条鱼之后,它与所有同类型的鱼特性都是类似的,所以我们就可以把这些鱼归为一类,叫 “鱼类”(Fish Class)。
其实在鱼的分类上还有更大的为 “动物分类 (Animal)”,那么动物下面还有其他动物的分类,比如说羊 (Sheep)。所以说鱼和羊之间他们的共性就会用 “动物” 来描述。然后我们一层一层的抽象,在 “Animal” 之上还会有 Object。
类是一个非常常见的描述对象的一种方式,比如说我们刚刚讲到的生物,用对象可以把所有的生物分成界门纲目科属种,是一个庞大的分类体系。在写代码的时候,分类是一个为业务服务的,我们没有必要分的那么细。通常我们会把有共性的需要写在代码里的,我们就把 Animal 提出来,就不再分这个哺乳动物,还是卵生,还是脊索动物等等。
分类有两个流派,一种是归类
,一种是分类
。
归类
—— 就是我们去研究单个对象,然后我们从里面提取共性变成类,之后我们又在类之间去提取共性,把它们变成更高的抽象类。比如我们在 “羊” 和 “鱼” 中提取共性,然后把它们之间的共享再提取出来变成 “动物” 的类。对于 “归类” 方法而言,多继承是非常自然的事情,如 C++ 中的菱形继承,三角形继承等。分类
—— 则是把世界万物都抽象为一个基类 Object,然后定义这个 Object 中有什么。采用分类思想的计算机语言,则是单继承结构。并且会有一个基类 Object。JavaScript 这个语言比较接近 “分类” 这个思想,但是它也不完全是分类的思想,因为它是一个多范式的面向对象语言。
接下来我们讲一讲 JavaScript 描述对象的方式。
其实分类 Class Based 的 Object 并不是一个唯一的认识对象的方法,我们还有一个更接近人类自然认知的。分类的能力可能至少要到小学才有的。但是我们认识对象之后,几乎是马上就可以得到另外一种描述对象的方式。那就是 “原型”。
原型其实用 “照猫画虎” 来理解 ,其实照猫画虎就是用的一种原型方法。因为猫和虎很像,所以我们只需要把它们直接的有区别的地方分出来就可以了。
比如说我们现在想研究鱼,那么找一种典型的鱼,比如找一条具体的鲤鱼,然后我们把这条鲤鱼所有的特征都加到鱼类的原型上。其他的鱼只要有对象,我们就根据鱼的原型进行修改。比如说鲶鱼比鲤鱼更能吃,它是吃肉的,而且身上还是滑滑的,所以我们就可以在鲤鱼的原型基础上把这些特征加上,这样我们就能描述出鲶鱼了。
那么在羊类里面,我们也选中一只小绵羊来做我们的基础原型。然后如果我们找到一只山羊,我们分析出它的特性是多胡子,脚是弯点,又长又硬又能爬山,那么我们就在小绵羊的原型上加上这些特性,那我们就描述了一只山羊了。
那么在上级的 “动物” 中我们也选一只典型的动物,比如说老虎,有四个蹄,但是不一定所有动物都有4个蹄子,不过原型选择相对来说它是比较自由的。比如说我们选择蛇作为动物的原型的话,那么我们在描述鱼的时候就特别费劲了,描述猫的时候就更费劲了。
原型里面也会有一个最终版的原型叫 Object Prototype
,这个就是所有物品的典型的物品,也可以说是我们所有对象的老祖宗。我们描述任何对象都是从它与描述对象的区别来进行描述的。
然后在 Object Prototype
之上一般来说是不会再有原型了,但是有一些语言里面会允许有一种 Nihilo
原型。Nihilo 的意思就是虚无空虚,这个是语言中立的讲法。如果我们用 JavaScript 的具体的设施来描述,那这个 Nihilo
原型就是 null
,这个大家就很容易理解了,我们很容易就可以一个 null
对象的原型。
小总结:
- 我们这种原型是更接近人类原始认知的描述对象的方法
- 所以面向对象的各种方法其实并没有绝对的对错,只存在在不同场景下不同的代价
- 原型的认知成本低,选错的成本也比较低,所以原型适合一些不是那么清晰和描述上比较自由的场景
- 而分类(Class)更适合用在一些比较严谨的场景,而 Class 有一个优点,它天然的跟类型系统有一定的整合的,所以很多的语言就会选择把 Class 的继承关系整合进类型系统的继承关系当中
我们如果需要编写一个 “狗 咬 人” 的 Class,我们需要怎么去设计呢?
如果我们按照一个比较朴素的方法,我们就会去定义一个 Dog
Class,然后里面给予这个 Class 一个 bite
的方法。
class Dog { bite(Human) { // ...... } }
这样的一段代码是跟我们的题目是一模一样的,但是这个抽象是一个错误的抽象。因为这个违背了面向对象的基本特征,不管我们是怎么设计,只要这个 bite
发生在狗身上就是错误的。
为什么?
因为我们前面讲到了面向对象的三要素,对象的状态必须是对象本身的行为才能改变的。那么如果我们在狗的 Class 中写 bite
这个动作,但是改变的状态是 “人”,最为狗咬了人之后,只会对人造成伤害。所以在这个行为中 “人” 的状态是发生变化的,那么如果行为是在狗的 Class 中就违反了面向对象的特征了。
当然如果是狗吃人,那我们勉强是可以成立的,因为狗吃了人狗就饱了,那对狗的状态是有发生改变的。但是狗咬人,我们基本可以认为这个行为对狗的状态是没有发生任何改变的。
所以我们应该在 “人” 的 Class 中设计一个行为。那么有些同学就会问,我们是应该在人的身上加入一个 biteBy
行为吗?就是人被咬的一个行为?似乎也不对,因为人 Class 里面的行为应该是用于改变人的状态的,那这个行为的命名应该是怎么样的呢?
这里更加合理的行为应该是 hurt
表示被伤害了,然后传入这个行为的参数就是受到的伤害程度 damage
。因为这里人只关心它受到的伤害有多少就可以了,他是不需要关心是狗咬的还是什么咬的。
class Human { hurt(damage) { //...... } }
狗咬人在实际开发场景中,是一个业务逻辑,我们只需要设计改变人 Human
对象内部的状态的行为,所以它正确的命名应该是 hurt
。这里的 damage
,可以从狗 Class 中咬 bite
, 的行为方法中计算或者生成出来的一个对象,但是如果我们直接传狗 Dog
的对象进来的话,肯定是不符合我们对对象的抽象原则的。
最终我们的代码实现逻辑如下:
class Human { constructor(name = '人') { this.name = name; this.hp = 100; } hurt(damage) { this.hp -= damage; console.log(`${this.name} 受到了 ${damage} 点伤害,剩余生命中为 ${this.hp}`); } } class Dog { constructor(name = '狗') { this.name = name; this.attackPower = 10; // 攻击力 } bite() { return this.attackPower; } } let human = new Human('三钻'); let dog = new Dog(); human.hurt(dog.bite()); // 输出:三钻 受到了 10 点伤害,剩余生命中为 90
设计对象的原则:
- 我们不应该受到语言描述的干扰(特别是业务需求的干扰)
- 在设计对象的状态和行为时,我们总是遵循 “行为改变状态” 的原则
- 违背了这个原则,整个对象的内聚性就没有了,这个对架构上会造成巨大的破坏
更多编程相关知识,请访问:编程入门!!
以上是深入了解JavaScript中的Object(对象)的详细内容。更多信息请关注PHP中文网其他相关文章!