js繼承

簡單講講js的繼承,也是js的原型鏈問題的實際應用。markdown

原型

原型和原型鏈都是來源於對象而服務於對象的概念:app

JavaScript中一切引用類型都是對象,對象就是屬性的集合。函數

Array類型、Function類型、Object類型、Date類型、RegExp類型等都是引用類型。ui

原型與原型鏈

每個對象從被建立開始就和另外一個對象關聯,從另外一個對象上繼承其屬性,這個另外一個對象就是原型。this

當訪問一個對象的屬性時,先在對象的自己找,找不到就去對象的原型上找,若是仍是找不到,就去對象的原型的原型上找,如此繼續,直到找到爲止。若是在最頂層的原型對象也沒有找到,就返回undefined。 這條由對象及其原型組成的鏈就叫作原型鏈。spa

原型的意義

  • 原型鏈存在的意義就是繼承:訪問對象屬性時,在對象自己找不到,就在原型鏈上一層一層找。就是一個對象能夠訪問其餘對象的屬性。
  • 繼承存在的意義就是屬性共享:一是代碼重用,字面意思;二是可擴展,不一樣對象可能繼承相同的屬性,也能夠定義只屬於本身的屬性。

訪問原型鏈

__proto__屬性雖然在 ECMAScript 6 語言規範中標準化,可是不推薦被使用,如今更推薦使用 Object.getPrototypeOfprototype

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
    }
}
複製代碼

原型鏈示意圖

img

從上圖咱們能夠看出code

  • 引用類型都是對象,每一個對象都有原型對象。
  • 對象都是由構造函數建立,對象的__proto__屬性指向其原型對象,構造函數的prototype屬性指向其建立的對象實例的原型對象,因此對象的__proto__屬性等於建立它的構造函數的prototype屬性。
  • 全部經過字面量表示法建立的普通對象的構造函數爲Object
  • 全部原型對象都是普通對象,構造函數爲Object
  • 全部函數的構造函數是Function
  • Object.prototype沒有原型對象

簡單總結

原型鏈就是多個對象經過 __proto__ 的方式鏈接了起來的一個鏈表結構。orm

  • Object 是全部對象的父節點,全部對象均可以經過 __proto__ 找到它
  • Function 是全部函數的父節點,全部函數均可以經過 __proto__ 找到它
  • 函數的 prototype 是一個對象
  • 對象的 __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')
複製代碼

工廠模式很好理解,實例化一個對象,在把傳入的參數放入該對象,再返回。

img

缺點:沒法進行對象識別。因爲返回的對象都是由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
複製代碼

img

從打印中能夠看到 car1car 的區別。

構造函數模式可以很好的使用 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"
複製代碼

原型鏈繼承通俗易懂,利用原型鏈將兩個類串起來。

缺點

  • 要新增原型中屬性或方法,必需要先new一個實例, 函數沒法複用,形成內存的浪費。
  • 沒法多繼承
  • 建立子類實例時,沒法向父類構造函數傳參

借用構造函數

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,須要對該對象進行加強處理。最後將普通類的原型指向中間變量,這樣就只須要調用一次超類就能夠完成繼承。

繼承的總結

  • 在原型鏈繼承中,咱們又遇到了老對手引用類型值的共享問題。
  • 在借用構造函數進行繼承中,方法共享問題,這個老對手又出現了。
  • 按照建立對象的經驗,組合二者優勢的組合繼承將成爲最佳方式,可是咱們卻發現了超類會被調用兩次的問題。
  • 爲了解決超類被調用兩次的問題,寄生組合繼承成爲了最佳方案。
相關文章
相關標籤/搜索