JS进阶系列-第三篇-面向对象

Posted by Kylen on 2019-07-17

面向对象

面向对象的概念

OOP(object-oriented programming)面向对象程序设计是一种程序开发的方式。常常与面向过程的程序设计方式比较。

面向对象程序设计和传统的思想不同的是:传统的程序主张将程序看作一系列函数的集合,或者一系列对电脑下达的指令。面向对象的程序设计中每一个对象都应该能够接收数据、处理数据并且能够将数据传达给其他对象,因此它们都可以被看作是一个小型的“机器”,即对象。

面向对象的程序开发方式的描述性更强,是人们可以更简单的理解、设计并维护程序,特别适合在大型项目中应用。当然,也有一部分反对者在某些领域对此予以否认。

面向对象的设计

面向对象的编程语言通常使用继承其他达到代码重用和扩展的性的特性。这里提到了继承的概念。继承后面再说,

对象指的是的实例,它将对象做为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。

类(Class)定义了一件事物的抽象特点。类的定义包含了数据的形式以及对数据的操作。对象则是类的实例。

面向对象的三大基本特征

  • 封装性
    隐藏对象的属性和实现细节。类中定义的属性和方法,通过类实例化而来的对象不必关系类中定义的属性和方法的细节。封装的目的是安全和简化编程

  • 继承
    在某种情况下,一个类会有“子类”。子类比原本的类(称为父类)要更加具体化。例如,“狗”这个类可能会有它的子类“牧羊犬”和“吉娃娃犬”。子类会继承父类的属性和行为,并且也可包含它们自己的。

  • 多态
    多态(Polymorphism)是指由继承而产生的相关的不同的类,其对象对同一消息会做出不同的响应。例如,狗和鸡都有“叫()”这一方法,但是调用狗的“叫()”,狗会吠叫;调用鸡的“叫()”,鸡则会啼叫。

JS的面向对象

关于JS是否是面向对象的语言这一点倒是有点争议,JS本身是基本符合面向对象的三大基本特征的。与Java面向不同之处在于JS是基于原型的面向对象,而Java是基于类的面向对象。

关于JS是否是基于对象的语言的争议,可以参考贺师俊的回答基于对象是指有对象的结构但是没有继承和多态的语言(比如VB),所以JS并不是基于对象的语言。

基于类和基于原型的区别

基于类的面向对象的语言,比如Java和C++,是构建在两个不同实体的感念之上的,即:类和实例。

所以上面有关面向对象的介绍其实都是在说基于类的面向对象。JS的面向对象则与此不同。

在JS中,所有的对象都是实例,ES6之前使用构造函数,ES新增Class语法糖,注意这里的Class并不和Java中的类对等,JS的底层依旧是基于原型。严格来说,在JS中,没有“类”。

差异总结

基于类的(Java) 基于原型的(JavaScript)
类和实例是不同的事物。 所有对象均为实例。
通过类定义来定义类;通过构造器方法来实例化类。 通过构造器函数来定义和创建一组对象。
通过 new 操作符创建单个对象。 相同
通过类定义来定义现存类的子类,从而构建对象的层级结构。 指定一个对象作为原型并且与构造函数一起构建对象的层级结构
遵循类链继承属性 遵循原型链继承属性
类定义指定类的所有实例的所有属性。无法在运行时动态添加属性。 构造器函数或原型指定初始的属性集。允许动态地向单个的对象或者整个对象集中添加或移除属性。

JS基于原型的继承

基于类的面向对象中,子类在继承的时候会复制父类的定义,子类和父类通过类链关联;基于原型的面向对象中,通过构造器创建对象,构造器都有自己的原型对象(prototype),一个构造器可以继承另一个构造器,通过可以复制原来的构造器的原型,并进行扩展来达到继承的能力。

JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也是一个对象,有自己的私有属性( proto )指向它的构造函数的原型对象(protorype) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

注意:__proto__是JS中每个对象都有的属性,这里的对象包括numble、boolean、string等,它们分别指向对象构造函数的原型;prototype是构造函数才有的属性。通过同一个构造函数创造的实例对象的___proto__属性执行同一个prototype

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

1
2
3
4
5
6
7
8
const Foo = new Function()
const o = new Object()
const f1 = new Foo()
// f1.__proto__ o.__proto__
// Foo.prototype Foo.__proto__
// Object.prototype Object.__proto__
// Function.prototype Function.__proto__
// 之间的关系

用下图展示应该更清晰
js-advanced 2019-07-17 下午4.16.59.jpg

注意Function本身也是函数,所以Function.__proto__ === Function.prototypetrue,即Function的构造函数就是Function本身。null是原型链的终点。

举一个具体的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ES5的写法
function People(name) {
this.name = name;
}
People.prototype.sayName = function() {
console.log(this.name);
};
function Student(name, age) {
People.call(this, name);
this.age = age;
}
Student.prototype = People.prototype // 直接使用People的原型
Student.prototype.sayAge = function(){
console.log(this.age);
}
const stu1 = new Student('Kylen', 25);
stu1.sayName();
stu1.sayAge();

console.log(Student.prototype.constructor) // [Function] People

这里直接使用了People的原型,这样写某些普通的业务可能没什么问题,但其实很不严谨,因为了People构造器共用一个原型,Student不能轻易修改People原型本来的属性和方法,People原型修改也可能影响到Student。
修改之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function People(name) {
this.name = name;
}
People.prototype.sayName = function() {
console.log(this.name);
};
function Student(name, age) {
People.call(this, name);
this.age = age;
}
// 继承的关键就在这一步,Student的原型对象的__proto__属性指向People的原型,Student的原型其实就是People构造器的一个实例。这样通过Student构造器创造的实例对象的参数__proto__指向Student的原型,再往上就是People的原型,描述的不太好,可以看下图:
// 使用Object.create,创造一个新的对象,新的对象的__proto__属性指向现有对象
Student.prototype = Object.create(People.prototype);
// 也可以写成,两种方式的作用是一样的
// Student.prototype = new People();

Student.prototype.constructor = Student // 保持默认指向
Student.prototype.sayAge = function(){
console.log(this.age);
}
// 覆盖People的sayName方法
Student.prototype.sayName = function() {
console.log('hello ' + this.name);
};
const stu1 = new Student('Kylen', 25);
stu1.sayName();
stu1.sayAge();
console.log(Student.prototype.constructor) [Function] Student

这就是JS实现继承的一个基本方式,Student的构造器想要继承People构造器,通过加入原型链
js-advanced 2019-07-25 下午3.49.13.png

使用ES6的class则没有这么麻烦,不过ES6的class本身只是个语法糖,最终底层的实现和ES5的没有本质的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class People {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Student extends People {
constructor(name, age) {
super(name);
this.age = age;
}
sayAge() {
console.log(this.age);
}
}
const stu1 = new Student('Kylen', 25);
stu1.sayName();
stu1.sayAge();

参考链接

wiki/面向对象程序设计
对象模型的细节
九、面向对象