前端基本功(七):javascript中的繼承(原型、原型鏈、繼承的實現方式)

1. js的繼承機制

javascirpt沒有"子類"和"父類"的概念,也沒有"類"(class)和"實例"(instance)的區分,全靠一種很奇特的"原型鏈"(prototype chain)模式,來實現繼承。繼承意味着複製操做,然而 JavaScript 默認並不會複製對象的屬性,相反,JavaScript 只是在兩個對象之間建立一個關聯,這樣,一個對象就能夠經過委託訪問另外一個對象的屬性和函數,因此與其叫繼承,委託的說法反而更準確些。javascript

2. 什麼是原型、原型鏈

1. 分清楚prototype、__proto__、constructor
  1. prototype: 這是一個屬性,在javascript中的每個函數都會有一個prototype屬性,普通的對象是沒有這個屬性的。構造函數的prototype屬性的屬性值就是該構造函數而建立的實例的原型。
  2. __proto__: 每個JavaScript對象(除了null)都具備的一個屬性,叫 __proto__,這個屬性會指向該對象的原型。
  3. constructor: 每個原型都有一個constructor屬性指向關聯的構造函數。
function Person() {

}
var person = new Person();
console.log(person.__proto__ === Person.prototype); //true;

function Person() {

}
var person = new Person();
console.log(Person === Person.prototype.constructor); //true
//其實 person 中並無constructor 屬性,當不能讀取到constructor屬性時,會從 person 的原型也就是 Person.prototype中讀取,正好原型中有該屬性
console.log(person.constructor === Person); // true
複製代碼
2. 原型鏈

JavaScript中全部的對象都是由它的原型對象繼承而來。而原型對象自身也是一個對象,它也有本身的原型對象,這樣層層上溯,就造成了一個相似鏈表的結構,這就是原型鏈(prototype chain)。全部原型鏈的終點都是Object函數的prototype屬性,由於在JavaScript中的對象都默認由Object()構造。Objec.prototype指向的原型對象一樣擁有原型,不過它的原型是null,而null則沒有原型。java

3. js繼承的實現方式

  1. 原型鏈繼承: 拿父類實例來充當子類原型對象
function Super(){
    this.val = 1;
    this.arr = [1];
}
function Sub(){
    // ...
}
Sub.prototype = new Super();    // 核心

var sub1 = new Sub();
var sub2 = new Sub();
sub1.val = 2;
sub1.arr.push(2);
alert(sub1.val);    // 2
alert(sub2.val);    // 1

alert(sub1.arr);    // 1, 2
alert(sub2.arr);    // 1, 2
// 優勢: 簡單,易於實現
// 缺點: 1. 修改sub1.arr後sub2.arr也變了,由於來自原型對象的引用屬性是全部實例共享的。 2. 建立子類實例時,沒法向父類構造函數傳參
複製代碼
  1. 借用構造函數繼承:借父類的構造函數來加強子類實例,等因而把父類的實例屬性複製了一份給子類實例裝上了(徹底沒有用到原型)
function Super(val){
    this.val = val;
    this.arr = [1];

    this.fun = function(){
        // ...
    }
}
function Sub(val){
    Super.call(this, val);   // 核心
    // ...
}

var sub1 = new Sub(1);
var sub2 = new Sub(2);
sub1.arr.push(2);
alert(sub1.val);    // 1
alert(sub2.val);    // 2

alert(sub1.arr);    // 1, 2
alert(sub2.arr);    // 1

alert(sub1.fun === sub2.fun);   // false
// 優勢: 1. 解決了子類實例共享父類引用屬性的問題 2. 建立子類實例時,能夠向父類構造函數傳參 
// 缺點: 沒法實現函數複用,每一個子類實例都持有一個新的fun函數,太多了就會影響性能,內存爆炸。。
複製代碼
  1. 組合繼承(經常使用): 把實例函數都放在原型對象上,以實現函數複用。同時還要保留借用構造函數方式的優勢。
// 經過Super.call(this);繼承父類的基本屬性和引用屬性並保留能傳參的優勢;經過Sub.prototype = new Super();繼承父類函數,實現函數複用
function Super(){
    // 只在此處聲明基本屬性和引用屬性
    this.val = 1;
    this.arr = [1];
}
// 在此處聲明函數
Super.prototype.fun1 = function(){};
Super.prototype.fun2 = function(){};
//Super.prototype.fun3...
function Sub(){
    Super.call(this);   // 核心
    // ...
}
Sub.prototype = new Super();    // 核心

var sub1 = new Sub(1);
var sub2 = new Sub(2);
alert(sub1.fun === sub2.fun);   // true
// 優勢: 1. 不存在引用屬性共享問題 2. 可傳參 3. 函數可複用
// 缺點:(一點小瑕疵)子類原型上有一份多餘的父類實例屬性,由於父類構造函數被調用了兩次,生成了兩份,而子類實例上的那一份屏蔽了子類原型上的。。。又是內存浪費,比剛纔狀況好點,不過確實是瑕疵
複製代碼
  1. 寄生組合繼承(最佳方式):切掉了原型對象上多餘的那份父類實例屬性。
function beget(obj){   // 生孩子函數 beget:龍beget龍,鳳beget鳳。
    var F = function(){};
    F.prototype = obj;
    return new F();
}
function Super(){
    // 只在此處聲明基本屬性和引用屬性
    this.val = 1;
    this.arr = [1];
}
// 在此處聲明函數
Super.prototype.fun1 = function(){};
Super.prototype.fun2 = function(){};
//Super.prototype.fun3...
function Sub(){
    Super.call(this);   // 核心
    // ...
}
// 用beget(Super.prototype);切掉了原型對象上多餘的那份父類實例屬性
var proto = beget(Super.prototype); // 核心
proto.constructor = Sub;            // 核心
Sub.prototype = proto;              // 核心

var sub = new Sub();
alert(sub.val);
alert(sub.arr);
複製代碼
  1. 原型式:用生孩子函數獲得獲得一個「純潔」的新對象(「純潔」是由於沒有實例屬性),再逐步加強之(填充實例屬性)
function beget(obj){   // 生孩子函數 beget:龍beget龍,鳳beget鳳。
    var F = function(){};
    F.prototype = obj;
    return new F();
}
function Super(){
    this.val = 1;
    this.arr = [1];
}

// 拿到父類對象
var sup = new Super();
// 生孩子
var sub = beget(sup);   // 核心
// 加強
sub.attr1 = 1;
sub.attr2 = 2;
//sub.attr3...

alert(sub.val);     // 1
alert(sub.arr);     // 1
alert(sub.attr1);   // 1
//ES5提供了Object.create()函數,內部就是原型式繼承,IE9+支持
// 優勢:從已有對象衍生新對象,不須要建立自定義類型(更像是對象複製,而不是繼承。。
// 缺點: 1. 原型引用屬性會被全部實例共享,由於是用整個父類對象來充當了子類原型對象,因此這個缺陷無可避免 2. 沒法實現代碼複用(新對象是現取的,屬性是現添的,都沒用函數封裝,怎麼複用)
複製代碼
  1. 寄生式:給原型式繼承穿了個馬甲而已,建立新對象 -> 加強 -> 返回該對象,這樣的過程叫寄生式繼承,新對象是如何建立的並不重要。
// 有缺陷的寄生式繼承 + 不完美的組合繼承 = 完美的寄生組合式繼承
function beget(obj){   // 生孩子函數 beget:龍beget龍,鳳beget鳳。
    var F = function(){};
    F.prototype = obj;
    return new F();
}
function Super(){
    this.val = 1;
    this.arr = [1];
}
function getSubObject(obj){
    // 建立新對象
    var clone = beget(obj); // 核心
    // 加強
    clone.attr1 = 1;
    clone.attr2 = 2;
    //clone.attr3...

    return clone;
}

var sub = getSubObject(new Super());
alert(sub.val);     // 1
alert(sub.arr);     // 1
alert(sub.attr1);   // 1
// 優勢: 仍是不須要建立自定義類型
// 缺點: 沒法實現函數複用(沒用到原型,固然不行)
複製代碼
相關文章
相關標籤/搜索