面向對象編程 Object Oriented Programming
面向對象編程用對象把數據和方法聚合起來。
面向對象編程的優點
- 能寫出模塊化的代碼
- 能使得代碼更靈活
- 能提高代碼的可重用性
面向對象編程的原則
- 繼承(inheritance):子類/派生類從父類/基類/超類中派生,形成繼承結構
- 封裝(encapsulation):代碼的實現對用户不可見,例如調用
toUpperCase(),直接調用即可,不用考慮函數內部的實現 - 抽象(abstraction):抽象就是抽象出你正在嘗試做的事情的概念,而不是處理這個概念的具體表現形式
- 多態(polymorphism):多種形態
對象
創建對象
- 使用對象字面量創建:
let dog = {
name: "Mit",
numLegs: 2
}
- 使用
Object.create()創建:(所有的對象都來自某個類,對象是類的實例,也是一種層級結構)
class Animal {
/* code */
}
var myDog = Object.create(Animal);
console.log(Animal);
- 使用
new關鍵字:(當使用默認或者空構造函數方法,JS 會隱式調用對象的超類來創建實例)
class Animal { /* code here */ }
var myDog = new Animal();
console.log(Animal)
訪問對象屬性
let dog = {
name: "Mit",
numLegs: 2
}
console.log(dog.name); // Mit
對象方法 Method
對象可以有一個叫做 method 的特殊屬性。
方法屬性也就是函數。 這給對象添加了不同的行為。
let dog = {
name: "Spot",
numLegs: 4,
sayLegs: function() {
return "This dog has 4 legs."
}
};
dog.sayLegs();
this
this 代表上下文環境中引用的對象。
let dog = {
name: "Spot",
numLegs: 4,
sayLegs: function() {return "This dog has " + this.numLegs + " legs.";}
};
dog.sayLegs(); // This dog has 4 legs.
構造函數
constructors 是創建對象的函數。 函數給這個新對象定義屬性和行為。 可將它們視為創建的新對象的藍圖。
構造函數遵循一些慣例規則:
- 構造函數函數名的首字母大寫,這是為了方便區分構造函數(
constructors)和其他非構造函數。 - 構造函數使用
this關鍵字來給它將創建的這個對象設置新的屬性。 在構造函數裏面,this指向的就是它新創建的這個對象。 - 構造函數定義了屬性和行為就可創建對象,而不是像其他函數一樣需要設置返回值。
function Dog() {
this.name = "N";
this.color = "red";
this.numLegs = 2;
}
function Dog(name, color) {
this.name = name;
this.color = color;
this.numLegs = 4;
}
let terrier = new Dog("fsd","fa");
JS 中有很多內置對象類型,稱為“原生對象(native object)”。有些類型可以使用構造函數獲取一個實例,但是有些類型沒有也不需要構造函數。例如 Date 可以用 new 關鍵字創建對象,但是 Math 對象就不行。
如果對象類型是基本類型(primitive),使用構造函數通常不是最佳實踐:
let apple = new String('apple');
apple; // string
對於常規對象,最好使用對象字面量語法 {},而不是 new。例如:數組,函數,正則表達式等。
下列類型可以考慮構造函數:
new Date();
new Error();
new Map();
new Promise();
new Set();
new WeakSet();
new WeakMap();
使用構造函數創建對象
注意: 構造函數內的 this 總是指被創建的對象。
注意:通過構造函數創建對象的時候要使用 new 操作符。
function Dog() {
this.name = "Rupert";
this.color = "brown";
this.numLegs = 4;
}
let hound = new Dog();
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; // true
屬性
自身屬性 Own Property
請看下面的實例,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 的所有實例都將擁有這些屬性的獨立副本。
原型屬性 Prototype Property
所有 Bird 實例可能會有相同的 numLegs 值,所以在每一個 Bird 的實例中本質上都有一個重複的變量 numLegs。
當只有兩個實例時可能並不是什麼問題,但想象一下如果有數百萬個實例。 這將會產生許許多多重複的變量。
更好的方法是使用 Bird 的 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 屬性,這個屬性是屬於它所在的構造函數。
迭代屬性 Iterate Property
自身屬性是直接在對象上定義的。 而原型屬性在 prototype 上定義。
function Dog(name) {
this.name = name;
}
Dog.prototype.numLegs = 4;
let beagle = new Dog("Snoopy");
let ownProps = [];
let prototypeProps = [];
for (let property in beagle) {
if (beagle.hasOwnProperty(property)) {
ownProps.push(property); // let in 只能遍歷自身屬性
} else {
prototypeProps.push(property);
}
}
構造函數屬性 Constructor Property
在上一個挑戰中創建的實例對象 duck 和 beagle 都有一個特殊的 constructor 屬性:
let duck = new Bird();
let beagle = new Dog();
console.log(duck.constructor === Bird); // true
console.log(beagle.constructor === Dog); // true
需要注意到的是: constructor 屬性是對創建實例的構造函數的一個引用。 constructor 屬性的一個好處是可以通過檢查這個屬性來找出它是一個什麼對象。
注意: 由於 constructor 屬性可以被重寫,所以最好使用instanceof 方法來檢查對象的類型。
給對象添加屬性
一種更有效的方法就是給對象的 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; // false
duck.constructor === Object; // true
duck instanceof Bird; // 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
在 JS 中,原型是一種屬性可以被多個其他對象共享的對象。
對象的原型
來自同一原型的對象有相同的功能。
對象也可直接從創建它的構造函數那裏繼承其 prototype。 請看下面的例子:Bird 構造函數創建了一個 duck 對象:
function Bird(name) {
this.name = name;
}
let duck = new Bird("Donald");
duck 從 Bird 構造函數那裏繼承了它的 prototype。 你可以使用 isPrototypeOf 方法來驗證他們之間的關係:
Bird.prototype.isPrototypeOf(duck); // true
原型鏈 Prototype Chain
JavaScript 中所有的對象(除了少數例外)都有自己的 prototype。 而且,對象的 prototype 本身也是一個對象。
function Bird(name) {
this.name = name;
}
typeof Bird.prototype; // object
正因為 prototype 是一個對象,所以 prototype 對象也有它自己的 prototype! 這樣看來的話,Bird.prototype 的 prototype 就是 Object.prototype:
Object.prototype.isPrototypeOf(Bird.prototype);
hasOwnProperty 是定義在 Object.prototype 上的一個方法,儘管在 Bird.prototype 和 duck上並沒有定義該方法,但是依然可以在這兩個對象上訪問到。 這就是 prototype 鏈的一個例子。 在這個prototype 鏈中,Bird 是 duck 的 supertype,而 duck 是 subtype。 Object 則是 Bird 和 duck 實例共同的 supertype。 Object 是 JavaScript 中所有對象的 supertype,也就是原型鏈的最頂層。 因此,所有對象都可以訪問 hasOwnProperty 方法。
繼承 Inherit
根據以上所説的 DRY 原則,通過創建一個 Animal supertype(或者父類)來重寫這段代碼:
function Animal() { };
Animal.prototype = {
constructor: Animal,
describe: function() {
console.log("My name is " + this.name);
}
};
Animal 構造函數中定義了 describe 方法,可將 Bird 和 Dog 這兩個構造函數的方法刪除掉:
Bird.prototype = {
constructor: Bird
};
Dog.prototype = {
constructor: Dog
};
繼承使用 extends 關鍵字,例如 class B extends class A
class Animal { /* code */ }
class Bird extends Animal { /* code */ }
class Eagle extends Bird { /* code */ }
從超類繼承行為 Inherit Behaviors from a Supertype
這裏用到構造函數的繼承特性。 首先創建一個超類 supertype(或者叫父類)的實例。
let animal = new Animal();
此語法用於繼承時會存在一些缺點。相反,另外一種沒有這些缺點的方法來替代 new 操作:
let animal = Object.create(Animal.prototype);
Object.create(obj) 創建了一個新對象,並指定了 obj 作為新對象的 prototype。 回憶一下,之前説過 prototype 就像是創建對象的“配方”。 如果把 animal 的 prototype 設置為與 Animal構造函數的 prototype 一樣,那麼就相當於讓 animal 這個實例具有與 Animal 的其他實例相同的“配方”了。
第二個步驟:給子類型(或者子類)設置 prototype。 這樣一來,Bird 就是 Animal 的一個實例了。
Bird.prototype = Object.create(Animal.prototype);
請記住,prototype 類似於創建對象的“配方”。 從某種意義上來説,Bird 對象的配方包含了 Animal 的所有關鍵“成分”。
let duck = new Bird("Donald");
duck.eat();
duck 繼承了Animal 的所有屬性,其中包括了 eat 方法。
super 關鍵字可以借用父類中的功能。
class Animal {
constructor(color = 'yellow', energy = 100) {
this.color = color;
this.energy = energy;
}
isActive() {
if(this.energy > 0) {
this.energy -= 20;
console.log('Energy is decreasing, currently at:', this.energy)
} else if(this.energy == 0){
this.sleep();
}
}
sleep() {
this.energy += 20;
console.log('Energy is increasing, currently at:', this.energy)
}
getColor() {
console.log(this.color)
}
}
class Cat extends Animal {
constructor(sound = 'purr', canJumpHigh = true, canClimbTrees = true, color, energy) {
super(color, energy);
this.sound = sound;
this.canClimbTrees = canClimbTrees;
this.canJumpHigh = canJumpHigh;
}
makeSound() {
console.log(this.sound);
}
}
class Bird extends Animal {
constructor(sound = 'chirp', canFly = true, color, energy) {
super(color, energy);
this.sound = sound;
this.canFly = canFly;
}
makeSound() {
console.log(this.sound);
}
注意⚠️:如果在子類中不使用 super 關鍵字,一旦運行上面的程序,會得到 Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor. 錯誤。
重置構造屬性 Reset
當一個對象從另一個對象那裏繼承了其 prototype 時,那它也繼承了父類的 constructor 屬性。
請看下面的舉例:
function Bird() { }
Bird.prototype = Object.create(Animal.prototype);
let duck = new Bird();
duck.constructor // Bird
但是 duck 和其他所有 Bird 的實例都應該表明它們是由 Bird 創建的,而不是由 Animal 創建的。 為此,你可以手動將 Bird 的構造函數屬性設置為 Bird 對象:
Bird.prototype.constructor = Bird;
duck.constructor // Bird
繼承後添加方法
從超類構造函數繼承其 prototype 對象的構造函數,除了繼承的方法外,還可以擁有自己的方法。
除了從 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!。
重寫繼承的方法 Override
通過使用一個與需要重寫的方法相同的方法名,向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 的 prototype 鏈上尋找方法的過程:
duck=>eat()是定義在這裏嗎? 不是。Bird=>eat()是定義在這裏嗎? => 是的。 執行它並停止往上搜索。Animal=> 這裏也定義了eat()方法,但是 JavaScript 在到達這層原型鏈之前已停止了搜索。- Object => JavaScript 在到達這層原型鏈之前也已經停止了搜索
多態 Polymorphism
多態來源於希臘語,意思是“多種形態”。
例如門和自行車上都有鈴,但是它們的使用目的,形態都可能不同。
function ringTheBell(thing) {
console.log(thing.bell())
}
ringTheBell(door);
ringTheBell(bicycle);
例如 concat() 方法和 + 都可以起到字符串拼接的作用,但是它們的形態不同。
多態性允許開發人員構建具有完全相同功能的對象,即具有完全相同名稱、行為完全相同的函數。但同時,開發者可以在 OOP 結構的其他部分覆蓋共享功能的某些部分,甚至覆蓋完整功能。
舉例:
class Bird {
useWings() {
console.log('flying');
}
}
class Eagle {
useWings() {
super.useWings();
console.log('barely flying');
}
}
class Penguin extends Bird {
useWings() {
console.log('diving');
}
}
var baldEagle = new Eagle();
var kingPenguin = new Penguin();
baldEagle.useWings(); // flying barely flying
kingPenguin.useWings() // diving
使用 Mixin 在不相關對象之間添加共同行為
對於不相關的對象,更好的方法是使用 mixins。 mixin 允許其他對象使用函數集合。
let bird = {
name: "Donald",
numLegs: 2
};
let boat = {
name: "Warrior",
type: "race-boat"
};
let glideMixin = function(obj) {
obj.glide = function() {
console.log("glide");
}
};
glideMixin(bird);
glideMixin(boat);
私有化 Private
使屬性私有化最簡單的方法就是在構造函數中創建變量。 可以將該變量範圍限定在構造函數中,而不是全局可用。 這樣,屬性只能由構造函數中的方法訪問和更改。
在 JavaScript 中,函數總是可以訪問創建它的上下文。 這就叫做 closure(閉包)。
function Bird() {
let hatchedEgg = 10;
this.getHatchedEggCount = function() {
return hatchedEgg;
};
}
let ducky = new Bird();
ducky.getHatchedEggCount();
立即調用函數表達式(IIFE)
JavaScript 中的一個常見模式就是,函數在聲明後立刻執行:
(function () {
console.log("Chirp, chirp!");
})();
這是一個匿名函數表達式,立即執行並輸出 Chirp, chirp!。
請注意,函數沒有名稱,也不存儲在變量中。 函數表達式末尾的兩個括號()會讓它被立即執行或調用。 這種模式被叫做立即調用函數表達式(immediately invoked function expression) 或者IIFE。
一個立即調用函數表達式(IIFE)通常用於將相關功能分組到單個對象或者是 module 中。 例如,先前的挑戰中定義了兩個 mixins:
function glideMixin(obj) {
obj.glide = function() {
console.log("Gliding on the water");
};
}
function flyMixin(obj) {
obj.fly = function() {
console.log("Flying, wooosh!");
};
}
可以將這些 mixins 分成以下模塊:
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 行為。 module 模式的優點是,所有的運動相關的行為都可以打包成一個對象,然後由代碼的其他部分使用。 下面是一個使用它的例子:
motionModule.glideMixin(duck);
duck.glide();