簡單講講js的繼承,也是js的原型鏈問題的實際應用。markdown
原型和原型鏈都是來源於對象而服務於對象的概念:app
JavaScript中一切引用類型都是對象,對象就是屬性的集合。函數
Array類型、Function類型、Object類型、Date類型、RegExp類型等都是引用類型。ui
每個對象從被建立開始就和另外一個對象關聯,從另外一個對象上繼承其屬性,這個另外一個對象就是原型。this
當訪問一個對象的屬性時,先在對象的自己找,找不到就去對象的原型上找,若是仍是找不到,就去對象的原型的原型上找,如此繼續,直到找到爲止。若是在最頂層的原型對象也沒有找到,就返回undefined
。 這條由對象及其原型組成的鏈就叫作原型鏈。spa
__proto__
屬性雖然在 ECMAScript 6 語言規範中標準化,可是不推薦被使用,如今更推薦使用 Object.getPrototypeOf
,prototype
Object.getPrototypeOf(person) === person.__proto__
複製代碼
function getProperty(obj, propName) {
if(obj.hasOwnProperty(propName)) {
return obj[propName]
} else if (obj.__proto__ !== null) {
return getProperty(obj.__proto__, propName)
} else {
return undefined
}
}
複製代碼
從上圖咱們能夠看出code
__proto__
屬性指向其原型對象,構造函數的prototype
屬性指向其建立的對象實例的原型對象,因此對象的__proto__
屬性等於建立它的構造函數的prototype
屬性。原型鏈就是多個對象經過 __proto__
的方式鏈接了起來的一個鏈表結構。orm
Object
是全部對象的父節點,全部對象均可以經過 __proto__
找到它Function
是全部函數的父節點,全部函數均可以經過 __proto__
找到它__proto__
屬性指向原型, __proto__
將對象和原型鏈接起來組成了原型鏈在理解對象繼承以前得先弄明白建立對象這回事兒。對象
function createCar(color, passengers, brand){
var car = new Object();
car.color = color;
car.passengers = passengers;
car.brand = brand;
car.printBrand = function(){
console.log(this.brand)
}
return car;
}
const car = createCar('red', ['a','b'], 'benz')
複製代碼
工廠模式很好理解,實例化一個對象,在把傳入的參數放入該對象,再返回。
缺點:沒法進行對象識別。因爲返回的對象都是由Object對象實例化出來的,可是開發過程當中,須要建立不少種對象,確定會有進行對象識別的需求,工廠模式顯然沒法完成咱們這樣的訴求。咱們繼續探索。
function Car(color, passengers, brand){
this.color = color;
this.passengers = passengers;
this.brand = brand;
this.printBrand = function(){
console.log(this.brand)
}
}
const car1 = new Car('red', ['a','b'], 'benz');
const car2 = new Car('black', ['c','d'], 'BMW');
console.log(car1 instanceof Object); //true
console.log(car1 instanceof Car); //true
console.log(car2 instanceof Object); //true
console.log(car2 instanceof Car); //true
複製代碼
從打印中能夠看到 car1
與 car
的區別。
構造函數模式可以很好的使用 instanceof
進行對象的識別,Object
對象是全部對象的頂層對象類,全部的對象都會繼承他。對對象進行操做的各種方法就存放在Object對象裏面。function
實際上也是一個對象,從typeof
方法中能夠體現出來
缺點:可是沒法解決引用類型的建立問題,咱們每次對Car對象進行實例化的時候,都須要對printBrand方法進行建立,沒法複用,浪費內存。要解決只能把他放到全局做用域。可是在全局做用域中定義的函數通常來講只能被某個對象調用,這會讓全局做用域名存實亡。而且也會失去封裝性,咱們來想象一下,若是該對象中有不少方法,那會讓全局做用域充滿了單獨拎出來的方法,讓代碼可讀性變差。
function Car(){
}
car.prototype.color = "red";
car.prototype.passengers = ["a","b","c"];
car.prototype.brand = "benz";
car.prototype.printBrand = function () {
console.log(this.brand)
};
var car1 = new Car();
var car2 = new Car();
car1.color = "blue";
car1.passengers.push('d');
console.log(car1.brand); //["a","b","c","d"]
console.log(car2.brand); //["a","b","c","d"]
console.log(car1.color); // "bule"
console.log(car2.color); // "red"
複製代碼
這個模式利用了對象的原型,將基本參數掛載在原型上面。
缺點:省去了初始化參數,這一點有好有壞。最大的問題是對引用類型值的共享,car1和car2實例在實例化之後還會與Car類存在關係。若是對其賦值基本類型值的話,會在實例化的對象當中建立,而且調用時會首先在實例化對象中尋找。而對引用類型值進行操做的時候,會直接在原型對象的引用類型值上進行操做,因此會在全部實例中共享。
function Car(color,brand){
this.color = color;
this.brand = brand;
this.passengers = ["a","b","c"];
}
Car.prototype = {
constructor: Car,
printBrand: function () {
console.log(this.brand)
}
}
var car1 = new Car("red",'benz');
var car2 = new Car("blue","BMW");
car1.color = "blue";
car1.passengers('d');
console.log(car1.brand); //["a","b","c"]
console.log(car2.brand); //["a","b","c","d"]
複製代碼
利用原型自定義構造函數,每一個實例都會存在一份實例的副本,同時利用原型方法共享的特性,最大程度節省了內存,也提供了向構造函數中傳遞參數的功能。爲最佳實踐。
function OldCar(){
this.color = "red";
this.passengers = ['a','b','c']
}
OldCar.prototype.getOldColor = function(){
return this.color;
}
function NewCar(){
this.color = "blue";
}
NewCar.prototype = new OldCar();
var car = new NewCar();
var car2 = new OldCar();
console.log(car.getOldColor()); //"blue"
console.log(car.passengers) // [ 'a', 'b', 'c' ]
console.log(car2.getOldColor()); //"red"
複製代碼
原型鏈繼承通俗易懂,利用原型鏈將兩個類串起來。
缺點
function OldCar(name = 'default name'){
this.passengers = ['a','b','c'];
this.name = name
}
function NewCar(name){
OldCar.call(this, name);
}
複製代碼
基本思路就是在子類的構造函數的內部調用超類的構造函數。由於函數只是在特定的環境中執行代碼的對象。借用構造函數的方式能夠解決引用類型的問題。使用call()和apply()方法,在子類中調用超類。這樣每一個實例都會有本身的引用類型的副本了。
缺點:和構造函數建立對象一致的問題,方法都得在構造函數中定義,致使函數沒法複用,形成內存的浪費。
function OldCar(brand){
this.brand = brand;
this.passengers = ['a','b','c']
}
OldCar.prototype.getBrand = function(){
return this.brand;
}
function NewCar(name, color){
OldCar.call(this,name) //第一次調用
this.color = color;
}
NewCar.prototype = new OldCar(); //第二次調用
NewCar.prototype.constructor = NewCar; //加強
NewCar.prototype.getColor = function(){
return this.color;
}
複製代碼
組合繼承集借用構造函數方法和原型鏈繼承二者之長,複用了方法,也解決了引用類型的問題。
缺點:須要調用兩次超類的構造函數,第一次是OldCar.call(this,name)
,第二次是new OldCar()
。下一步咱們須要解決的是超類的兩次調用問題。
function A(){
}
A.prototype.name = 'py';
A.prototype.age = 12;
<!--等價於-->
A.prototype = {
name: 'py',
age: 12
}
A.prototype.constructor = A
複製代碼
上面的例子中,上半部分是最基本的對原型的賦值,而下班部分的對原型的賦值A的原型的構造函數會變成Object(先new Object而後再賦值參數),因此須要顯式的去加強構造函數。
爲了解決組合繼承的痛點,出現了寄生組合繼承。
function OldCar(brand){
this.brand = brand;
this.passengers = ['a','b','c']
}
OldCar.prototype.getBrand = function(){
return this.brand;
}
function NewCar(name,color){
OldCar.call(this,name)
this.color = color;
}
//繼承開始
var middleObj = Object.create(OldCar.prototype);
middleObj.constructor = NewCar;
NewCar.prototype = middleObj
//繼承結束
NewCar.prototype.getColor = function(){
return this.color;
}
複製代碼
function createObj(obj){
function Car(){};
Car.prototype = obj;
return new Car();
}
Object.create() 等價於 crateObj(),至關於對傳入的對象進行了一次淺複製。
複製代碼
那麼,咱們來看看繼承的過程當中發生了什麼。先對超類的原型進行一次淺複製。而後將中間對象的構造函數替換爲普通類。爲何要進行這一步?由於對超類的原型進行淺複製之後,中間對象的構造函數變成了Object,須要對該對象進行加強處理。最後將普通類的原型指向中間變量,這樣就只須要調用一次超類就能夠完成繼承。