共计 11723 个字符,预计需要花费 30 分钟才能阅读完成。
面向对象编程
本系列文章是基于 freecodecamp 网站前端训练营练习的演绎总结
创立一个根本的 JavaScript 对象
咱们在生活中每天都可见到的事物:比方汽车、商店以及小鸟等。它们都是对象:即人们能够察看和与之互动的实体事物。
这些物体的性质是什么?汽车有轮子。商店销售物品。鸟儿有翅膀。
这些特色,或者说是属性定义了一个对象由什么形成的。须要留神的是:那些类似的对象能够领有雷同的属性,然而这些属性可能会有不同的值。举个例子:所有的汽车都有轮子,但并不是所有汽车的轮子个数都是一样的。
JavaScript 中的对象能够用来形容事实世界中的物体,并赋予他们属性和行为,就像它们在事实世界中的对应物一样。上面是应用这些概念来创立一个 duck
对象的示例:
let duck = {
name: "Aflac",
numLegs: 2
};
这个 duck
对象有两组键值对:一个是 name 属性,它的值是 Aflac;另一个是 numLegs 属性,它的值是 2。
应用点符号来拜访对象的属性
上一个挑战创立了一个具备各种属性的对象。当初你会看到如何拜访这些属性的值。上面是一个示例:
let duck = {
name: "Aflac",
numLegs: 2
};
console.log(duck.name);
咱们能够用“点号表示法”来拜访对象的属性,duck
前面加上点号以及属性名 name
,来拜访到 Aflac
。
在对象上创立办法
对象能够有一个叫做 method 的非凡属性。
办法属性也就是函数。这给对象增加了不同的行为。以下就是一个带有办法属性的 duck 示例:
let duck = {
name: "Aflac",
numLegs: 2,
sayName: function() {return "The name of this duck is" + duck.name + ".";}
};
duck.sayName();
示例增加了 sayName
办法,函数返回蕴含 duck
名字的一个句子。留神:这个办法在返回语句中应用 duck.name
的形式来获取 name
的属性值
应用 this 关键字进步代码重用性
在上一个挑战中咱们理解了该如何给 duck
对象设置一个办法。而后在 return
语句里,咱们通过应用“点号表示法”duck.name
来获取 name
的属性值:
sayName: function() {return "The name of this duck is" + duck.name + ".";}
尽管这是拜访对象属性的无效办法,然而这里有一个陷阱。如果变量名产生了扭转,那么援用了原始名称的任何代码都须要更新。在一个简短的对象定义中,这并不是问题,然而如果对象有很多对其属性的援用,那么产生谬误的可能性就更大了。
咱们能够应用 this
关键字来防止这一问题:
let duck = {
name: "Aflac",
numLegs: 2,
sayName: function() {return "The name of this duck is" + this.name + ".";}
};
this
是一个很简单的知识点,而下面那个例子也只是应用它的一种办法而已。在以后的上下文环境中,this
指向的就是与这个办法有关联的 duck
对象。 如果把对象的变量名改为 mallard
,那应用 this
后就没有必要在代码中找到所有指向 duck
的局部。这样能够使得代码更具备可读性和复用性。
定义构造函数
Constructors
是创建对象的函数。函数给这个新对象定义属性和行为。可将它们视为创立的新对象的蓝图。
以下就是一个构造函数的示例:
function Bird() {
this.name = "Albert";
this.color = "blue";
this.numLegs = 2;
}
这个构造函数定义了一个 Bird
对象,其属性 name、color
和 numLegs
的值别离被设置为 Albert、blue 和 2。构造函数遵循一些常规规定:
- 构造函数函数名的首字母大写,这是为了不便咱们辨别构造函数(
constructors
)和其余非构造函数。 - 构造函数应用
this
关键字来给它将创立的这个对象设置新的属性。在构造函数外面,this
指向的就是它新创建的这个对象。 - 构造函数定义了属性和行为就可创建对象,而不是像其余函数一样须要设置返回值。
应用构造函数创建对象
在上一个挑战中,创立了一个 Bird 构造函数:
function Bird() {
this.name = "Albert";
this.color = "blue";
this.numLegs = 2;
}
let blueBird = new Bird();
留神: 构造函数内的 this 总是指被创立的对象。
留神: 通过构造函数创建对象的时候要应用 new
操作符。因为只有这样,JavaScript 才晓得要给 Bird 这个构造函数创立一个新的实例:blueBird
。如果不应用 new 操作符来新建对象,那么构造函数外面的 this
就无奈指向新创建的这个对象实例,从而产生不可预感的谬误。当初 blueBird
这个实例就继承了 Bird
构造函数的所有属性,如下:
blueBird.name;
blueBird.color;
blueBird.numLegs;
由构造函数创立的实例也和其余对象一样,它的属性能够被拜访和批改:
blueBird.name = 'Elvira';
blueBird.name;
扩大构造函数以接管参数
上一个挑战中 Bird
构造函数运行得不错。然而,留神到没有:所有通过Bird
构造函数创立进去的实例 Birds
都主动的取名为 Albert
,色彩都是蓝色,还都有两条腿。如果你想要新创建进去的小鸟们领有不同的名字和色彩要怎么办呢?当然,手动的去批改每一个小鸟实例本人的属性也是能够实现的,只是会减少很多无谓的工作量:
let swan = new Bird();
swan.name = "Carlos";
swan.color = "white";
如果你写了一个程序来追踪一个鸟舍外面的几百只甚至几千只不同的小鸟。你将会破费很多工夫去创立所有的小鸟实例并给它们的属性一一批改为不同的值。为了加重创立不同 Bird
对象的工作量,你能够给你的 Bird 设置为能够接管参数的构造函数:
function Bird(name, color) {
this.name = name;
this.color = color;
this.numLegs = 2;
}
而后将值通过参数的形式传递给 Bird
构造函数来定义每一个惟一的小鸟实例:let cardinal = new Bird("Bruce", "red");
这给 Bird
的 name
和 color
属性别离赋值为 Bruce 和 red 色。但 numLegs 属性依然设置为 2。cardinal
有以下这些属性:
cardinal.name
cardinal.color
cardinal.numLegs
这样一来构造函数就变得很灵便了。当初能够在创立每个 Bird
实例时间接定义属性,这是 JavaScript 构造函数十分实用的用法之一。它们依据独特或类似的属性和行为将对象演绎为一组,并可能主动的创立各自实例
应用 instanceof 验证对象的构造函数
但凡通过构造函数创立出的新对象,这个对象都叫做这个构造函数的 instance。JavaScript 提供了一种很简便的办法来验证这个事实,那就是通过 instanceof
操作符。instanceof
容许你将对象与构造函数之间进行比拟,依据对象是否由这个构造函数创立的返回 true
或者 false
。以下是一个示例:
let Bird = function(name, color) {
this.name = name;
this.color = color;
this.numLegs = 2;
}
let crow = new Bird("Alexis", "black");
crow instanceof Bird;
instanceof
办法会返回 true
。
如果一个对象不是应用构造函数创立的,那么 instanceof
将会验证这个对象不是构造函数的实例:
let canary = {
name: "Mildred",
color: "Yellow",
numLegs: 2
};
canary instanceof Bird;
instanceof
办法会返回 false。
理解自有属性
请看上面的实例,Bird
构造函数定义了两个属性:name
和 numLegs
:
function Bird(name) {
this.name = name;
this.numLegs = 2;
}
let duck = new Bird("Donald");
let canary = new Bird("Tweety");
name
和 numLegs
被叫做 本身属性,因为它们是间接在实例对象上定义的。这就意味着 duck
和 canary
这两个对象别离领有这些属性的独立正本。事实上,Bird
的所有实例都将领有这些属性的独立正本。上面的代码将 duck
的所有本身属性都存到一个叫作 ownProps
的数组外面:
let ownProps = [];
for (let property in duck) {if(duck.hasOwnProperty(property)) {ownProps.push(property);
}
}
console.log(ownProps);
控制台将显示值 ["name", "numLegs"]
。
应用原型属性来缩小反复代码
所有 Bird
实例可能会有雷同的 numLegs
值,所以在每一个 Bird
的实例中实质上都有一个反复的变量 numLegs。
当只有两个实例时可能并不是什么问题,但设想一下如果有数百万个实例。这将会产生许多反复的变量。
这里有一个更好的办法能够解决上述问题,那就是应用 Bird’s
的 prototype
。prototype
是一个能够在所有 Bird
实例之间共享的对象。以下是一个在 Bird prototype
中增加 numLegs
属性的示例:
Bird.prototype.numLegs = 2;
当初所有的 Bird 实例都领有了独特的 numLegs 属性值。
console.log(duck.numLegs);
console.log(canary.numLegs);
因为所有的实例都能够继承 prototype
上的属性,所以能够把 prototype
看作是创建对象的 “ 配方 ”。请留神:duck
和 canary
的 prototype
属于 Bird
的构造函数,即 Bird
的原型 Bird.prototype
。JavaScript 中简直所有的对象都有一个 prototype
属性,这个属性是属于它所在的构造函数。
迭代所有属性
当初你曾经理解了两种属性: 本身属性和 prototype
属性。本身属性是间接在对象上定义的。而 prototype
属性是定义在 prototype
上的。
function Bird(name) {this.name = name; //own property}
Bird.prototype.numLegs = 2; // prototype property
let duck = new Bird("Donald");
这个示例会通知你如何将 duck
的本身属性和 prototype
属性别离增加到 ownProps
数组和 prototypeProps
数组外面:
let ownProps = [];
let prototypeProps = [];
for (let property in duck) {if(duck.hasOwnProperty(property)) {ownProps.push(property);
} else {prototypeProps.push(property);
}
}
console.log(ownProps);
console.log(prototypeProps);
console.log(ownProps)
将在控制台中显示 ["name"]
,console.log(prototypeProps)
将显示 ["numLegs"]
。
理解构造函数属性
在上一个挑战中创立的实例对象 duck
有一个非凡的 constructor
属性:
let duck = new Bird();
let beagle = new Dog();
console.log(duck.constructor === Bird);
console.log(beagle.constructor === Dog);
这两次 console.log
调用都将在控制台中显示 true。
须要留神到的是这个 constructor
属性是对创立这个实例的构造函数的一个援用。constructor
属性的一个益处是能够通过查看这个属性来找出它是一个什么对象。上面是一个例子,来看看是怎么应用的:
function joinBirdFraternity(candidate) {if (candidate.constructor === Bird) {return true;} else {return false;}
}
留神: 因为 constructor 属性能够被重写,所以最好应用 instanceof 办法来查看对象的类型。
将原型更改为新对象
到目前为止,你曾经能够独自给 prototype
增加属性了:
Bird.prototype.numLegs = 2;
须要增加多个属性的,这未免会显得拖沓。
Bird.prototype.eat = function() {console.log("nom nom nom");
}
Bird.prototype.describe = function() {console.log("My name is" + this.name);
}
一种更无效的办法就是给对象的 prototype
设置为一个曾经蕴含了属性的新对象。这样一来,所有属性都能够一次性增加进来:
Bird.prototype = {
numLegs: 2,
eat: function() {console.log("nom nom nom");
},
describe: function() {console.log("My name is" + this.name);
}
};
更改原型时,记得设置构造函数属性
手动设置一个新对象的原型有一个重要的 副作用 。 它革除了 constructor 属性! 此属性能够用来查看是哪个构造函数创立了实例,但因为该属性已被笼罩,它当初给出了谬误的后果:
duck.constructor === Bird;
duck.constructor === Object;
duck instanceof Bird;
按程序,这些表达式会返回 false
、true
和 true
。
为了解决这个问题,但凡手动给新对象从新设置过原型对象的,都别忘记在原型对象中定义一个 constructor
属性:
Bird.prototype = {
constructor: Bird,
numLegs: 2,
eat: function() {console.log("nom nom nom");
},
describe: function() {console.log("My name is" + this.name);
}
};
理解对象的原型来自哪里
就像人们从父母那里继承基因一样,对象也可间接从创立它的构造函数那里继承其 prototype
。请看上面的例子:Bird
构造函数创立了一个 duck
对象:
function Bird(name) {this.name = name;}
let duck = new Bird("Donald");
duck
从 Bird
构造函数那里继承了它的 prototype
。你能够应用 isPrototypeOf
办法来验证他们之间的关系:
Bird.prototype.isPrototypeOf(duck);
这将返回 true
理解原型链
JavaScript 中所有的对象(除了多数例外)都有本人的 prototype
。而且,对象的 prototype
自身也是一个对象。
function Bird(name) {this.name = name;}
typeof Bird.prototype;
正因为 prototype
是一个对象,所以 prototype
对象也有它本人的 prototype
!这样看来的话,Bird.prototype
的 prototype
就是 Object.prototype
:
Object.prototype.isPrototypeOf(Bird.prototype);
这有什么作用呢?你可能还记得咱们在之前挑战中学到的 hasOwnProperty
办法:
let duck = new Bird("Donald");
duck.hasOwnProperty("name");
hasOwnProperty
是定义在 Object.prototype
上的一个办法,只管在 Bird.prototype
和 duck
上并没有定义该办法,然而咱们仍然能够在这两个对象上拜访到。这就是 prototype
链的一个例子。在这个 prototype
链中,Bird
是 duck
的 supertype
,而 duck
是 subtype
。Object
则是 Bird
和 duck
实例独特的 supertype
。Object
是 JavaScript
中所有对象的 supertype
,也就是原型链的最顶层。因而,所有对象都能够拜访 hasOwnProperty
办法。
应用继承防止反复
有一条准则叫做:Don’t Repeat Yourself。常以缩写模式 DRY 呈现,意思是“不要本人反复”。编写反复代码会产生的问题是:任何扭转都须要去多个中央修复所有反复的代码。这通常意味着咱们须要做更多的工作,会产生更高的出错率。
请察看上面的示例,Bird
和 Dog
共享 describe
办法:
Bird.prototype = {
constructor: Bird,
describe: function() {console.log("My name is" + this.name);
}
};
Dog.prototype = {
constructor: Dog,
describe: function() {console.log("My name is" + this.name);
}
};
咱们能够看到 describe
办法在两个中央反复定义了。依据以上所说的 DRY 准则,咱们能够通过创立一个 Animal
supertype
(或者父类)来重写这段代码:
function Animal() {};
Animal.prototype = {
constructor: Animal,
describe: function() {console.log("My name is" + this.name);
}
};
Animal
构造函数中定义了 describe
办法,可将 Bird
和 Dog
这两个构造函数的办法删除掉:
从超类继承行为
在上一个挑战中,咱们创立了一个 Animal
超类(supertype
),用来定义所有动物共有的行为:
function Animal() {}
Animal.prototype.eat = function() {console.log("nom nom nom");
};
在这一节以及下一节挑战中咱们将学习如何在 Bird
和 Dog
中重用 Animal's
中的办法,而无需从新定义它们。这里咱们会用到构造函数的继承个性。这一节挑战中咱们学习第一步:创立一个超类 supertype
(或者叫父类)的实例。你曾经学会了一种创立 Animal
实例的办法,即应用 new 操作符:
let animal = new Animal();
此语法用于继承时会存在一些毛病,这些毛病对于以后咱们这个挑战来说太简单了。相同,咱们学习另外一种没有这些毛病的办法来代替 new
操作:
let animal = Object.create(Animal.prototype);
Object.create(obj)
创立了一个新对象,并指定了 obj
作为新对象的 prototype
。回顾一下,咱们之前说过 prototype
就像是创建对象的 “配方”。如果咱们把 animal
的 prototype
设置为与 Animal's
构造函数的 prototype
一样,那么就相当于让 animal
这个实例的配方与 Animal
其余实例的配方一样了。
animal.eat();
animal instanceof Animal;
instanceof
办法会返回 true
将子辈的原型设置为父辈的实例
在上一个挑战中,咱们学习了从超类(或者叫父类)Animal
继承其行为的第一个步骤:创立一个 Animal
的新实例。
这一节挑战咱们将学习第二个步骤:给子类型(或者子类)设置 prototype。
这样一来,Bird
就是 Animal
的一个实例了。
Bird.prototype = Object.create(Animal.prototype);
请记住,prototype
相似于创建对象的“配方”。从某种意义上来说,Bird
对象的配方蕴含了 Animal
的所有要害“成分”。
let duck = new Bird("Donald");
duck.eat();
duck
继承了 Animal
的所有属性,其中包含了 eat
办法。
重置一个继承的构造函数属性
当一个对象从另一个对象那里继承了其 prototype
时,那它也继承了父类的 constructor
属性。
请看上面的举例
function Bird() {}
Bird.prototype = Object.create(Animal.prototype);
let duck = new Bird();
duck.constructor
然而 duck
和其余所有 Bird
的实例都应该表明它们是由 Bird
创立的,而不是由 Animal
创立的。为此,你能够手动把 Bird's
的 constructor
属性设置为 Bird
对象:
Bird.prototype.constructor = Bird;
duck.constructor
继承后增加办法
从超类构造函数继承其 prototype
对象的构造函数,除了继承的办法外,还能够领有本人的办法。
请看举例:Bird
是一个构造函数,它继承了 Animal
的 prototype
:
function Animal() {}
Animal.prototype.eat = function() {console.log("nom nom nom");
};
function Bird() {}
Bird.prototype = Object.create(Animal.prototype);
Bird.prototype.constructor = Bird;
除了从 Animal
构造函数继承的行为之外,还须要给 Bird
对象增加它独有的行为。这里,咱们给 Bird
对象增加一个 fly()
函数。函数会以一种与其余构造函数雷同的形式增加到 Bird's
的 prototype
中:
Bird.prototype.fly = function() {console.log("I'm flying!");
};
当初 Bird
的实例中就有了 eat()
和 fly()
这两个办法:
let duck = new Bird();
duck.eat();
duck.fly();
duck.eat()
将在控制台中显示字符串 nom nom nom
,duck.fly()
将显示字符串 I'm flying!
。
重写继承的办法
在上一个挑战中,咱们学习了一个对象能够通过援用另一个对象的 prototype
来继承其属性和行为(或办法):
ChildObject.prototype = Object.create(ParentObject.prototype);
而后,ChildObject
将本人的办法链接到它的 prototype
中:
ChildObject.prototype.methodName = function() {...};
咱们还能够重写继承的办法。以同样的形式 – 通过应用一个与须要重写的办法雷同的办法名,向 ChildObject.prototype
中增加办法。请看上面的举例:Bird
重写了从 Animal
继承来的 eat()
办法:
function Animal() {}
Animal.prototype.eat = function() {return "nom nom nom";};
function Bird() {}
Bird.prototype = Object.create(Animal.prototype);
Bird.prototype.eat = function() {return "peck peck peck";};
如果你有一个实例:let duck = new Bird();
,而后你调用了 duck.eat()
,以下就是 JavaScript 在 duck’s
的 prototype
链上寻找办法的过程:
duck
=>eat()
是定义在这里吗?不是。Bird
=>eat()
是定义在这里吗?=> 是的。执行它并进行往上搜寻。Animal
=> 这里也定义了eat()
办法,然而 JavaScript 在达到这层原型链之前已进行了搜寻。Object
=>JavaScript
在达到这层原型链之前也曾经进行了搜寻。
应用 Mixin 在不相干对象之间增加独特行为
正如你所见,行为是能够通过继承来共享的。然而,在有些状况下,继承不是最好的解决方案。继承不适用于不相干的对象,比方 Bird
和 Airplane
。尽管它们都能够航行,然而 Bird
并不是一种 Airplane
,反之亦然。
对于不相干的对象,更好的办法是应用 mixins。mixin 容许其余对象应用函数汇合。
let flyMixin = function(obj) {obj.fly = function() {console.log("Flying, wooosh!");
}
};
flyMixin
能承受任何对象,并为其提供 fly
办法。
let bird = {
name: "Donald",
numLegs: 2
};
let plane = {
model: "777",
numPassengers: 524
};
flyMixin(bird);
flyMixin(plane)
这里的 flyMixin
接管了 bird
和 plane
对象,而后将 fly
办法调配给了每一个对象。当初 bird
和 plane
都能够航行了:
bird.fly();
plane.fly();
控制台将显示字符串 Flying, wooosh!
两次,每 .fly()
调用都会显示。
留神察看 mixin
是如何容许雷同的 fly
办法被不相干的对象 bird
和 plane
重用的。
应用闭包爱护对象内的属性不被内部批改
在上一次挑战中,bird 有一个公共属性 name。公共属性的定义就是:它能够在 bird 的定义范畴之外被拜访和更改
bird.name = "Duffy";
因而,代码的任何中央都能够轻松地将 bird
的 name 属性更改为任意值。想想明码和银行账户之类的货色,如果代码库的任何局部都能够轻易扭转他们。那么将会引起很多问题。
使属性私有化最简略的办法就是在构造函数中创立变量。能够将该变量范畴限定在构造函数中,而不是全局可用。这样,属性只能由构造函数中的办法拜访和更改。
function Bird() {
let hatchedEgg = 10;
this.getHatchedEggCount = function() {return hatchedEgg;};
}
let ducky = new Bird();
ducky.getHatchedEggCount();
这里的 getHatchedEggCount
是一种特权办法,因为它能够拜访公有属性 hatchedEgg
。这是因为 hatchedEgg
是在与 getHatchedEggCount
雷同的上下文中申明的。在 JavaScript 中,函数总是能够拜访创立它的上下文。这就叫做 closure
。
理解立刻调用函数表白(IIFE)
JavaScript 中的一个常见模式就是,函数在申明后立即执行:
(function () {console.log("Chirp, chirp!");
})();
这是一个匿名函数表达式,立刻执行并输入 Chirp, chirp!
。
请留神,函数没有名称,也不存储在变量中。函数表达式开端的两个括号()会让它被立刻执行或调用。这种模式被叫做立刻调用函数表达式(immediately invoked function expression) 或者IIFE。
应用 IIFE 创立一个模块
一个立刻调用函数表达式(IIFE)通常用于将相干性能分组到单个对象或者是 module 中。例如,先前的挑战中定义了一个 mixin:
function flyMixin(obj) {obj.fly = function() {console.log("Flying, wooosh!");
};
}
咱们能够将这些 mixin 分成以下模块:
let motionModule = (function () {
return {flyMixin: function(obj) {obj.fly = function() {console.log("Flying, wooosh!");
};
}
}
})();
留神:一个立刻调用函数表达式(IIFE)返回了一个 motionModule
对象。返回的这个对象蕴含了作为对象属性的所有 mixin 行为。module 模式的长处是,所有的静止相干的行为都能够打包成一个对象,而后由代码的其余局部应用。上面是一个应用它的例子:
motionModule.glideMixin(duck);
duck.glide();