面向对象编程

面向对象编程

介绍

面向对象编程是软件开发过程的几种主要方法之一。

顾名思义,面向对象编程将代码组织成对象定义。这些有时被称为类,它们将数据和相关行为组合在一起。数据是对象的属性,行为(或函数)是方法。

对象结构能够在程序中灵活使用,比如对象可以通过调用数据并将数据传递给另一个对象的方法来传递信息。此外,新对象可以从基类(或父类)接收或继承所有功能,这有助于减少重复代码。

对于编程方法的选择取决于几个因素,其中包括问题的类型、如何构造数据以及算法等。这里介绍 JavaScript 中面向对象的编程原则。

对象

1
2
3
4
5
let duck = {
name: "Aflac",
numLegs: 2,
sayName: function() {return "The name of this duck is " + this.name + ".";}
};

构造函数

构造函数遵循一些惯例规则:

  • 构造函数函数名的首字母最好大写,这是为了方便我们区分构造函数和其他非构造函数。
  • 构造函数使用this关键字来给它将创建的这个对象设置新的属性。在构造函数里面,this指向的就是它新创建的这个对象。
  • 构造函数定义了属性和行为就可创建对象,而不是像其他函数一样需要设置返回值。
1
2
3
4
5
function Dog(){
this.name = "Wangcai";
this.color = "green";
this.numLegs = 4;
}

创建对象

通过构造函数创建对象的时候要使用new操作符。因为只有这样,JavaScript 才知道要给Bird这个构造函数创建一个新的实例blueBird。如果不使用new操作符来新建对象,那么构造函数里面的this就无法指向新创建的这个对象实例,从而产生不可预见的错误。

1
2
3
4
5
6
7
8
function Bird() {
this.name = "Albert";
this.color = "blue";
this.numLegs = 2;
// 构造函数里面的 "this" 总是指向新创建的实例。
}

let blueBird = new Bird();

每个对象不相同,使用传入参数的构造函数可以方便的创建不同的对象

1
2
3
4
5
6
7
function Dog(name,color) {
this.numLegs = 4;
this.name = name;
this.color = color;
}

var terrier = new Dog("旺财","green");

instance of

凡是通过构造函数创建出的新对象,都叫做这个构造函数的实例。JavaScript 提供了一种很简便的方法来验证这个事实,那就是通过instanceof操作符。instanceof允许你将对象与构造函数之间进行比较,根据对象是否由这个构造函数创建的返回true或者false。以下是一个示例:

1
2
3
4
5
6
7
8
9
let Bird = function(name, color) {
this.name = name;
this.color = color;
this.numLegs = 2;
}

let crow = new Bird("Alexis", "black");

crow instanceof Bird; // => true

使用原型属性来减少重复代码

所有Bird实例可能会有相同的numLegs值,所以在每一个Bird的实例中本质上都有一个重复的变量numLegs

当只有两个实例时可能并不是什么问题,但想象一下如果有数百万个实例,这将会产生许许多多重复的变量。

这里有一个更好的方法可以解决上述问题,那就是使用Bird原型原型是一个可以在所有Bird实例之间共享的对象。以下是一个在Bird prototype中添加numLegs属性的示例:

1
Bird.prototype.numLegs = 2;

现在所有的Bird实例都拥有了共同的numLegs属性值。

由于所有的实例都可以继承原型上的属性,所以可以把原型看作是创建对象的 “配方”。

请注意:duckcanary原型Bird构造函数原型Bird.prototype的一部分。JavaScript 中几乎所有的对象都有一个原型属性,这个属性是属于它所在的构造函数的一部分。

迭代所有属性

两种属性: 自身属性和原型属性。自身属性是直接在对象上定义的。而原型属性是定义在prototype上的:

1
2
3
4
5
function Bird(name) {
this.name = name; // 自身属性
}
Bird.prototype.numLegs = 2; // 原型属性
let duck = new Bird("Donald");

duck自身属性和原型属性分别添加到ownProps数组和prototypeProps数组里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
let ownProps = [];
let prototypeProps = [];

for (let property in duck) {
if(duck.hasOwnProperty(property)) {
ownProps.push(property);
} else {
prototypeProps.push(property);
}
}

console.log(ownProps); // 输出 ["name"]
console.log(prototypeProps); // 输出 ["numLegs"]

了解构造函数属性

创建的实例对象duckbeagle都有一个特殊的constructor属性:

1
2
3
4
5
let duck = new Bird();
let beagle = new Dog();

console.log(duck.constructor === Bird); //输出 true
console.log(beagle.constructor === Dog); //输出 true

注意:
由于constructor属性可以被重写(在下面两节挑战中将会遇到),所以使用instanceof方法来检查对象的类型会更好

将原型更改为新对象

到目前为止,你已经可以单独给prototype添加属性了:

1
Bird.prototype.numLegs = 2;

这将在添加许多属性的时候变得单调乏味。

一种更有效的方法就是给对象的prototype设置为一个已经包含了属性的新对象。这样一来,所有属性都可以一次性添加进来:

1
2
3
4
5
6
7
8
9
Bird.prototype = {
numLegs: 2,
eat: function() {
console.log("nom nom nom");
},
describe: function() {
console.log("My name is " + this.name);
}
};

更改原型时,记得设置构造函数属性

手动给新对象重新设置原型对象,会产生一个重要的副作用:删除了constructor属性!

为了解决这个问题,凡是手动给新对象重新设置过原型对象的,都别忘记在原型对象中定义一个constructor属性:

1
2
3
4
5
6
7
8
9
10
Bird.prototype = {
constructor: Bird, // 定义 constructor 属性
numLegs: 2,
eat: function() {
console.log("nom nom nom");
},
describe: function() {
console.log("My name is " + this.name);
}
};

了解对象的原型来自哪里

就像人们从父母那里继承基因一样,对象也可直接从创建它的构造函数那里继承其原型。请看下面的例子:Bird构造函数创建了一个duck对象:

1
2
3
4
5
function Bird(name) {
this.name = name;
}

let duck = new Bird("Donald");

duckBird构造函数那里继承了它的原型,你可以使用isPrototypeOf方法来验证他们之间的关系:

1
2
Bird.prototype.isPrototypeOf(duck);
// 返回 true

了解原型链

JavaScript 中所有的对象(除了少数例外)都有自己的原型。而且,对象的原型本身也是一个对象。

1
2
3
4
5
function Bird(name) {
this.name = name;
}

typeof Bird.prototype; // => object

正因为原型是一个对象,所以原型对象也有它自己的原型!这样看来的话,Bird.prototype原型就是Object.prototype

1
2
 Object.prototype.isPrototypeOf(Bird.prototype);
// 返回 true

这有什么作用呢?你可能还记得我们在上一个挑战中学到的hasOwnProperty方法:

1
2
let duck = new Bird("Donald");
duck.hasOwnProperty("name"); // => true

hasOwnProperty是定义在Object.prototype上的一个方法,尽管在Bird.prototypeduck上并没有定义该方法,但是我们依然可以在这两个对象上访问到。这就是一个原型链。

在这个原型链中,Bird构造函数是父级duck子级Object则是Bird构造函数和duck实例共同的父级

Object是 JavaScript 中所有对象的父级,也就是原型链的最顶层。因此,所有对象都可以访问hasOwnProperty方法。

从超类继承行为

创建一个Animal 超类,用来定义所有动物共有的行为:

1
2
3
4
function Animal() { }
Animal.prototype.eat = function() {
console.log("nom nom nom");
};

如何给BirdDog重写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
2
animal.eat(); // 输出 "nom nom nom"
animal instanceof Animal; // => true

将子辈的原型设置为父辈的实例

我们学习了从超类(或者叫父类) Animal继承其行为的第一个步骤:创建一个Animal的实例。

这一节挑战我们将学习第二个步骤:给子类型(或者子类)设置原型。这样一来,Bird就是Animal的一个实例了。

1
Bird.prototype = Object.create(Animal.prototype);

请记住,原型类似于创建对象的“配方”。从某种意义上来说,Bird对象的配方包含了Animal构造函数的所有关键“成分”。

1
2
let duck = new Bird("Donald");
duck.eat(); // 输出 "nom nom nom"

duck继承了Animal构造函数的所有属性,其中包括了eat方法。

  • 相当于java中: person类—student类—李四

重置继承的构造函数属性

当一个对象从另一个对象那里继承了其原型,那它也继承了父类的 constructor 属性。

1
2
3
4
function Bird() { }
Bird.prototype = Object.create(Animal.prototype);
let duck = new Bird();
duck.constructor // function Animal(){...}

但是duck和其他所有Bird的实例都应该表明它们是由Bird创建的,而不是由Animal创建的。为此,你可以手动把Bird的 constructor 属性设置为Bird对象:

1
2
Bird.prototype.constructor = Bird;
duck.constructor // function Bird(){...}

继承后添加方法

1
2
3
4
5
6
7
8
9
10
11
function Animal() { }
Animal.prototype.eat = function() {
console.log("nom nom nom");
};
function Bird() { }
Bird.prototype = Object.create(Animal.prototype);
Bird.prototype.constructor = Bird;
//添加方法
Bird.prototype.fly = function() {
console.log("I'm flying!");
};

重写继承的方法

一个对象可以通过复制另一个对象的原型来继承其属性和行为(或方法):

1
ChildObject.prototype = Object.create(ParentObject.prototype);

然后,ChildObject将自己的方法链接到它的原型中,我们可以像这样访问:

1
ChildObject.prototype.methodName = function() {...};

我们还可以重写继承的方法。以同样的方式——通过使用一个与需要重写的方法相同的方法名,向ChildObject.prototype中添加方法。

请看下面的举例:Bird重写了从Animal继承来的eat()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Animal() { }
Animal.prototype.eat = function() {
return "nom nom nom";
};
function Bird() { }

// 继承了 Animal 的所有方法
Bird.prototype = Object.create(Animal.prototype);

// Bird.eat() 重写了 Animal.eat() 方法
Bird.prototype.eat = function() {
return "peck peck peck";
};

如果你有一个实例:let duck = new Bird();,然后你调用了duck.eat(),以下就是 JavaScript 在duck原型链上寻找方法的过程:

\1. duck => 这里定义了 eat() 方法吗?没有。

\2. Bird => 这里定义了 eat() 方法吗?=> 是的。执行它并停止往上搜索。

\3. Animal => 这里也定义了 eat() 方法,但是 JavaScript 在到达这层原型链之前已停止了搜索。

\4. Object => JavaScript 在到达这层原型链之前也已经停止了搜索。

使用 Mixin 在不相关对象之间添加共同行为

对于不相关的对象,更好的方法是使用mixinsmixin允许其他对象使用函数集合。

1
2
3
4
5
let flyMixin = function(obj) {
obj.fly = function() {
console.log("Flying, wooosh!");
}
};

flyMixin能接受任何对象,并为其提供fly方法。flyMixin能接受任何对象,并为其提供fly方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let bird = {
name: "Donald",
numLegs: 2
};

let plane = {
model: "777",
numPassengers: 524
};

flyMixin(bird);
flyMixin(plane);
//这里的flyMixin接收了bird和plane对象,然后将fly方法分配给了每一个对象。现在bird和plane都可以飞行了:
bird.fly(); // 输出 "Flying, wooosh!"
plane.fly(); // 输出 "Flying, wooosh!"

了解立即调用函数表达(IIFE)

1
2
3
4
(function () {
console.log("Chirp, chirp!");
})(); // 这是一个立即执行的匿名函数表达式
// 立即输出 "Chirp, chirp!"

函数声明后立即执行。函数不存放到任何变量当中

使用 IIFE 创建一个模块

一个自执行函数表达式IIFE)通常用于将相关功能分组到单个对象或者是模块中。例如,先前的挑战中定义了一个混合类

1
2
3
4
5
6
7
8
9
10
function glideMixin(obj) {
obj.glide = function() {
console.log("Gliding on the water");
};
}
function flyMixin(obj) {
obj.fly = function() {
console.log("Flying, wooosh!");
};
}

我们可以将这些mixins分成以下一个模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let motionModule = (function () {
return {
glideMixin: function (obj) {
obj.glide = function() {
console.log("Gliding on the water");
};
},
flyMixin: function(obj) {
obj.fly = function() {
console.log("Flying, wooosh!");
};
}
}
}) (); // 末尾的两个括号导致函数被立即调用

注意:一个自执行函数表达式IIFE)返回了一个motionModule对象。返回的这个对象包含了作为对象属性的所有mixin行为。

模块模式的优点是,所有的运动行为都可以打包成一个对象,然后由代码的其他部分使用。下面是一个使用它的例子:

1
2
motionModule.glideMixin(duck);
duck.glide();