面向对象编程
介绍
面向对象编程是软件开发过程的几种主要方法之一。
顾名思义,面向对象编程将代码组织成对象定义。这些有时被称为类,它们将数据和相关行为组合在一起。数据是对象的属性,行为(或函数)是方法。
对象结构能够在程序中灵活使用,比如对象可以通过调用数据并将数据传递给另一个对象的方法来传递信息。此外,新对象可以从基类(或父类)接收或继承所有功能,这有助于减少重复代码。
对于编程方法的选择取决于几个因素,其中包括问题的类型、如何构造数据以及算法等。这里介绍 JavaScript 中面向对象的编程原则。
对象
1 | let duck = { |
构造函数
构造函数
遵循一些惯例规则:
构造函数
函数名的首字母最好大写,这是为了方便我们区分构造函数
和其他非构造函数。构造函数
使用this
关键字来给它将创建的这个对象设置新的属性。在构造函数
里面,this
指向的就是它新创建的这个对象。构造函数
定义了属性和行为就可创建对象,而不是像其他函数一样需要设置返回值。
1 | function Dog(){ |
创建对象
通过构造函数创建对象的时候要使用new
操作符。因为只有这样,JavaScript 才知道要给Bird
这个构造函数创建一个新的实例
:blueBird
。如果不使用new
操作符来新建对象,那么构造函数里面的this
就无法指向新创建的这个对象实例,从而产生不可预见的错误。
1 | function Bird() { |
每个对象不相同,使用传入参数的构造函数可以方便的创建不同的对象
1 | function Dog(name,color) { |
instance of
凡是通过构造函数创建出的新对象,都叫做这个构造函数的实例
。JavaScript 提供了一种很简便的方法来验证这个事实,那就是通过instanceof
操作符。instanceof
允许你将对象与构造函数之间进行比较,根据对象是否由这个构造函数创建的返回true
或者false
。以下是一个示例:
1 | let Bird = function(name, color) { |
使用原型属性来减少重复代码
所有Bird
实例可能会有相同的numLegs
值,所以在每一个Bird
的实例中本质上都有一个重复的变量numLegs
。
当只有两个实例时可能并不是什么问题,但想象一下如果有数百万个实例,这将会产生许许多多重复的变量。
这里有一个更好的方法可以解决上述问题,那就是使用Bird
的原型
。原型
是一个可以在所有Bird
实例之间共享的对象。以下是一个在Bird prototype
中添加numLegs
属性的示例:
1 | Bird.prototype.numLegs = 2; |
现在所有的Bird
实例都拥有了共同的numLegs
属性值。
由于所有的实例都可以继承原型
上的属性,所以可以把原型
看作是创建对象的 “配方”。
请注意:duck
和canary
的原型
是Bird
构造函数原型Bird.prototype
的一部分。JavaScript 中几乎所有的对象都有一个原型
属性,这个属性是属于它所在的构造函数的一部分。
迭代所有属性
两种属性: 自身
属性和原型
属性。自身
属性是直接在对象上定义的。而原型
属性是定义在prototype
上的:
1 | function Bird(name) { |
将duck
的自身
属性和原型
属性分别添加到ownProps
数组和prototypeProps
数组里面:
1 | let ownProps = []; |
了解构造函数属性
创建的实例对象duck
和beagle
都有一个特殊的constructor
属性:
1 | let duck = new Bird(); |
注意:
由于constructor
属性可以被重写(在下面两节挑战中将会遇到),所以使用instanceof
方法来检查对象的类型会更好。
将原型更改为新对象
到目前为止,你已经可以单独给prototype
添加属性了:
1 | Bird.prototype.numLegs = 2; |
这将在添加许多属性的时候变得单调乏味。
一种更有效的方法就是给对象的prototype
设置为一个已经包含了属性的新对象。这样一来,所有属性都可以一次性添加进来:
1 | Bird.prototype = { |
更改原型时,记得设置构造函数属性
手动给新对象重新设置原型
对象,会产生一个重要的副作用:删除了constructor
属性!
为了解决这个问题,凡是手动给新对象重新设置过原型对象的,都别忘记在原型对象中定义一个constructor
属性:
1 | Bird.prototype = { |
了解对象的原型来自哪里
就像人们从父母那里继承基因一样,对象也可直接从创建它的构造函数那里继承其原型
。请看下面的例子:Bird
构造函数创建了一个duck
对象:
1 | function Bird(name) { |
duck
从Bird
构造函数那里继承了它的原型
,你可以使用isPrototypeOf
方法来验证他们之间的关系:
1 | Bird.prototype.isPrototypeOf(duck); |
了解原型链
JavaScript 中所有的对象(除了少数例外)都有自己的原型
。而且,对象的原型
本身也是一个对象。
1 | function Bird(name) { |
正因为原型
是一个对象,所以原型
对象也有它自己的原型
!这样看来的话,Bird.prototype
的原型
就是Object.prototype
:
1 | Object.prototype.isPrototypeOf(Bird.prototype); |
这有什么作用呢?你可能还记得我们在上一个挑战中学到的hasOwnProperty
方法:
1 | let duck = new Bird("Donald"); |
hasOwnProperty
是定义在Object.prototype
上的一个方法,尽管在Bird.prototype
和duck
上并没有定义该方法,但是我们依然可以在这两个对象上访问到。这就是一个原型
链。
在这个原型
链中,Bird
构造函数是父级
,duck
是子级
。Object
则是Bird
构造函数和duck
实例共同的父级
。
Object
是 JavaScript 中所有对象的父级
,也就是原型链的最顶层。因此,所有对象都可以访问hasOwnProperty
方法。
从超类继承行为
创建一个Animal 超类
,用来定义所有动物共有的行为:
1 | function Animal() { } |
如何给Bird
、Dog
重写Animal
中的方法,而无需重新定义它们。这里我们会用到构造函数的继承
特性。
第一步:创建一个超类
(或者叫父类)的实例。
你已经学会了一种创建Animal
实例的方法,即使用new
操作符:
1 | let animal = new Animal(); |
此语法用于继承
时会存在一些缺点,这些缺点对于当前我们这个挑战来说太复杂了。相反,我们学习另外一种没有这些缺点的方法来替代new
操作:
1 | let animal = Object.create(Animal.prototype); |
Object.create(obj)
创建了一个新对象,并指定了obj
作为新对象的原型
。回忆一下,我们之前说过原型
就像是创建对象的“配方”。如果我们把animal
的原型
设置为与Animal
构造函数的原型
一样,那么就相当于让animal
这个实例的配方与Animal
其他实例的配方一样了。
1 | animal.eat(); // 输出 "nom nom nom" |
将子辈的原型设置为父辈的实例
我们学习了从超类(或者叫父类) Animal
继承其行为的第一个步骤:创建一个Animal
的实例。
这一节挑战我们将学习第二个步骤:给子类型(或者子类)
设置原型
。这样一来,Bird
就是Animal
的一个实例了。
1 | Bird.prototype = Object.create(Animal.prototype); |
请记住,原型
类似于创建对象的“配方”。从某种意义上来说,Bird
对象的配方包含了Animal
构造函数的所有关键“成分”。
1 | let duck = new Bird("Donald"); |
duck
继承了Animal
构造函数的所有属性,其中包括了eat
方法。
- 相当于java中: person类—student类—李四
重置继承的构造函数属性
当一个对象从另一个对象那里继承了其原型
,那它也继承了父类
的 constructor 属性。
1 | function Bird() { } |
但是duck
和其他所有Bird
的实例都应该表明它们是由Bird
创建的,而不是由Animal
创建的。为此,你可以手动把Bird
的 constructor 属性设置为Bird
对象:
1 | Bird.prototype.constructor = Bird; |
继承后添加方法
1 | function Animal() { } |
重写继承的方法
一个对象可以通过复制另一个对象的原型
来继承其属性和行为(或方法):
1 | ChildObject.prototype = Object.create(ParentObject.prototype); |
然后,ChildObject
将自己的方法链接到它的原型
中,我们可以像这样访问:
1 | ChildObject.prototype.methodName = function() {...}; |
我们还可以重写继承的方法。以同样的方式——通过使用一个与需要重写的方法相同的方法名,向ChildObject.prototype
中添加方法。
请看下面的举例:Bird
重写了从Animal
继承来的eat()
方法:
1 | function Animal() { } |
如果你有一个实例:let duck = new Bird();
,然后你调用了duck.eat()
,以下就是 JavaScript 在duck
的原型
链上寻找方法的过程:
\1. duck => 这里定义了 eat() 方法吗?没有。
\2. Bird => 这里定义了 eat() 方法吗?=> 是的。执行它并停止往上搜索。
\3. Animal => 这里也定义了 eat() 方法,但是 JavaScript 在到达这层原型链之前已停止了搜索。
\4. Object => JavaScript 在到达这层原型链之前也已经停止了搜索。
使用 Mixin 在不相关对象之间添加共同行为
对于不相关的对象,更好的方法是使用mixins
。mixin
允许其他对象使用函数集合。
1 | let flyMixin = function(obj) { |
flyMixin
能接受任何对象,并为其提供fly
方法。flyMixin
能接受任何对象,并为其提供fly
方法。
1 | let bird = { |
了解立即调用函数表达(IIFE)
1 | (function () { |
函数声明后立即执行。函数不存放到任何变量当中
使用 IIFE 创建一个模块
一个自执行函数表达式
(IIFE
)通常用于将相关功能分组到单个对象或者是模块
中。例如,先前的挑战中定义了一个混合类
1 | function glideMixin(obj) { |
我们可以将这些mixins
分成以下一个模块:
1 | let motionModule = (function () { |
注意:一个自执行函数表达式
(IIFE
)返回了一个motionModule
对象。返回的这个对象包含了作为对象属性的所有mixin
行为。
模块
模式的优点是,所有的运动行为都可以打包成一个对象,然后由代码的其他部分使用。下面是一个使用它的例子:
1 | motionModule.glideMixin(duck); |