JS面向對象的本質不是基於類(class),而是基於構造函數(constructor)和原型對象(prototype)
創建對象(封裝)
最簡單的創建對象的方式就是通過Object的構造函數或者對象字面量,但這兩種方式在使用同一個接口創建多個對象時會產生大量重複代碼。
工廠模式
function createPerson(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
}
return o;
}
let person = createPerson('zhangsan', 20, 'FrontEnd Engineer');
工廠模式解決了創建多個相似對象的問題,但沒有解決對象識別的問題(即怎樣知道一個對象的類型)。
構造函數模式
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
}
}
let person = new Person('zhangsan', 20, 'FrontEnd Engineer');
要創建Person的新實例,必須使用new操作符。通過new調用構造函數會經歷一下4個步驟:
- 創建一個新對象
- 指定該對象的構造函數(constructor)為當前函數
- 將構造函數的作用域賦給新對象(this就指向了這個新對象)
- 如果該函數沒有返回對象,返回新對象
定義自定義構造函數可以確保實例被標識為特定類型,相比於工廠模式,這是一個很大的好處。但是構造函數的主要問題在於,其定義的方法會在每個實例上都創建一遍。
解決方案:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
console.log(this.name);
}
let person = new Person('zhangsan', 20, 'FrontEnd Engineer');
這樣雖然解決了相同邏輯的函數重複定義的問題,但全局作用域也因此被搞亂了。這個新問題可以通過原型模式來解決。
原型模式
function Person() {
}
Person.prototype.name = 'zhangsan';
Person.prototype.age = 20;
Person.prototype.job = 'FrontEnd Engineer';
Person.prototype.sayName = function() {
console.log(this.name);
}
let person = new Person('zhangsan', 20, 'FrontEnd Engineer');
缺點:原型中的所有屬性是被很多實例共享的,這種共享對於函數非常合適。
理解原型對象
無論什麼時候,只要創建了一個新函數,就會根據一組特定的規則為該函數創建一個prototype屬性,這個屬性指向函數的原型對象。在默認情況下,所有原型對象都會自動獲得一個constructor屬性,指回與之關聯的構造函數。
在自定義構造函數時,其原型對象默認只會獲得constructor屬性,其他所有方法都繼承自Object。每次調用構造函數創建一個新實例,該實例的內部[[Prototype]]指針就會被賦值為構造函數的原型對象。腳本中沒有訪問這個[[Prototype]]的特定方式,但FireFox、Safari和Chrome會在每個對象上暴露__proto__屬性,通過這個屬性可以訪問對象的原型。
function Person(name) {
this.name = name;
}
let person = new Person('xiaoming');
其他原型語法
function Person() {
}
Person.prototype = {
name: 'zhangsan',
age: 20,
job: 'FrontEnd Engineer',
sayName: function() {
console.log(this.name);
}
}
// 恢復constructor屬性,且設置不可枚舉
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
let person = new Person('zhangsan', 20, 'FrontEnd Engineer');
組合使用構造函數和原型模式
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
}
Person.prototype.sayName = function() {
console.log(this.name);
}
let person = new Person('zhangsan', 20, 'FrontEnd Engineer');
這種模式是目前在ECMAScript中使用最廣泛,認同度最高的一種創建自定義類型的方法。
寄生構造函數模式
基本思想是創建一個函數,該函數的作用僅僅是封裝創建對象的代碼,然後再返回新創建的對象;但從表面上看,這個函數又很像是典型的構造函數。除了使用new操作符並把使用的包裝函數叫做構造函數之外,這個模式跟工廠模式其實是一摸一樣的。
function Person(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
alert(this.name);
}
return o;
}
let person = new Person('zhangsan', 20, 'FrontEnd Engineer');
注意:使用這種模式返回的對象與構造函數或者構造函數的原型屬性之間沒有關係;也就是説,構造函數返回的對象與在構造函數外部創建的對象沒有什麼不同。(請優先使用其它模式)
繼承
原型鏈
ECMA-262把原型鏈定義為ECMAScript的主要繼承方式,其基本思想就是通過原型繼承多個引用類型的屬性和方法。
如果原型是另一個類型的實例,那就意味着這個原型本身有一個內部指針指向另一個原型,相應地另一個原型也有一個指針指向另外一個構造函數,這樣就在原型和實例之間夠早了一條原型鏈。這就是原型鏈的基本構思。
重寫原型對象實現繼承
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.clothesColor = ['read', 'blue', 'black'];
}
Person.prototype.sayName = function () {
console.log(this.name);
}
let person = new Person('zhangsan', 20, 'FrontEnd Engineer');
function Student(major) {
this.major = major;
}
Student.prototype = new Person('student', 18, 'student');
Student.prototype.getMajor = function () {
return this.major;
}
let student = new Student('English');
student.sayName()
console.log(student.getMajor());
console.log(student);
console.log(student instanceof Student); // true
console.log(student instanceof Person); // true
console.log(student instanceof Object); // true
默認情況下,所有引用類型都繼承自Object,這也是通過原型鏈實現的。
![]()
存在的問題:
- 原型中包含的屬性會在所用實例間共享
- 在創建子類時,不能向父類型的構造函數中傳遞參數
盜用構造函數實現繼承
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
this.getAge = function () {
var currentYear = new Date().getFullYear();
return currentYear - this.year;
};
}
function Truck(load, make, model, year) {
Car.call(this, make, model, year);
this.load = load;
}
var truck1 = new Truck(25, 'JieFang', 'DongFeng', 1960);
console.log(truck1.getAge()); // Uncaught TypeError: truck1.getAge is not a function
存在的問題:
借用構造函數解決了前面原型鏈繼承的問題,但是方法必須定義在構造函數中,而不是原型對象上,否則就報上面這個錯誤,但是這就無法實現函數的複用了。
組合繼承
使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。
function Truck(load, make, model, year) {
Car.call(this, make, model, year);
this.load = load;
}
Truck.prototype = new Car();
Truck.prototype.constructor = Truck;
var truck1 = new Truck(25, 'JieFang', 'DongFeng', 1960);
console.log(truck1.getAge());
存在的問題:調用了2次父類的構造方法,會存在一份多餘的父類實例屬性 。
原型式繼承
一種不涉及嚴格意義上構造函數的繼承方法。即使不自定義類型也可以通過原型實現對象之間的信息共享。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
本質上講,object()對傳入其中的對象執行了一次淺複製。
ES5通過新增Object.create()規範化了原型繼承:
var truck1 = Object.create(new Car('JieFang', 'DongFeng', 1960));
truck1.load = 25;
console.log(truck1.getAge());
在只有一個參數時,Object.create()與前面的object方法效果相同。
在沒有必要興師動眾地創建構造函數,而只想讓一個對象與另一個對象保持類似的情況下,原型式繼承是完全可以勝任的。注意:包含應用類型值的屬性始終都會共享相應的值
寄生式繼承
與原生式繼承非常相似,即使基於某個對象或某些信息創建一個對象,然後增強對象,最後返回對象。
function createAnother(original) {
var clone = object(original);
clone.sayHi = function() {
alert('hi');
}
return clone;
}
var person = {
name: 'Nicholas',
friends: ['shelby', 'Court', 'Van']
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // 'Hi'
寄生組合繼承
寄生組合繼承解決了組合繼承所存在的問題(兩次調用構造函數)。通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。不必為了子類型的原型而調用超類型的構造函數。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
// 用寄生式創建原型對象
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype); // 可使用Object.create()代替
prototype.constructor = subType;
subType.prototype = prototype;
}
function Truck(load, make, model, year) {
Car.call(this, make, model, year);
this.load = load;
}
inheritPrototype(Truck, Car);
var truck1 = new Truck(25, 'JieFang', 'DongFeng', 1960);
console.log(truck1.getAge());
如何實現多重繼承?
function Game(arg) {
this.name = 'lol';
this.skin = ['s'];
}
Game.prototype.getName = function() {
return this.name;
}
function Store() {
this.shop = 'steam';
}
Store.prototype.getPlatform = function() {
return this.shop;
}
function LOL(arg) {
Game.call(this, arg);
Store.call(this, arg);
}
LOL.prototype = Object.create(Game.prototype);
// LOL.prototype = Object.create(Store.prototype);
Object.assign(LOL.prototype, Store.prototype);
LOL.prototype.constructor = LOL;
// LOL繼承兩類
const game3 = new LOL();
多態
同一方法,在不同情況下,表現出不同的狀態
-
重載
JavaScript沒有真正意義上的重載,只能在同一個方法中傳遞不同的參數去模仿重載。
-
重寫(override)
在子對象中重寫父對象中的方法。