JS原型與原型鏈

原型

什麼是原型?

在JavaScript中,函數能夠有屬性。每個函數都有一個特殊的屬性叫原型(prototype)。編程

在下面的代碼中爲函數p定義原型markdown

function P(){//構造函數
    this.x = x; //聲明私有屬性,並初始化爲參數x
}
P.prototype.x = 1;
var p1 = new P (10);  //實例化對象,並設置參數爲10
P.prototype.x = p1.x;  //設置原型屬性值爲私有屬性值
console.log(P.prototype.x);  //返回10
複製代碼

訪問原型

訪問原型對象有3種方法,簡單說明以下:函數

  • obj.__proto__
  • obj.constructor.prototype
  • Object.getPrototypeOf(obj)

其中,obj表示一個實例對象,constructor表示構造函數,__proto__是一個私有屬性,可讀寫,與prototype屬性相同,均可以訪問原型對象。Object.getPrototypeOf(obj)是一個靜態函數。參數爲示例對象,返回值是參數對象的原型對象。測試

下面建立一個空的構造函數,而後實例化,分別使用上述三種方法訪問實例對象的原型。ui

var F = function() {};//構造函數
var obj = new F();//實例化對象
var prototype1 = Object.getPrototypeOf(obj);//引用原型
var prototype2 = obj.__proto__;//引用原型
var prototype3 = obj.constructor.prototype;//引用原型
var prototype4 = F.prototype;
console.log(prototype1 === prototype2);  //true
console.log(prototype1 === prototype3);  //true
console.log(prototype1 === prototype4);  //true
console.log(prototype2 === prototype3);  //true
console.log(prototype2 === prototype4);  //true
console.log(prototype3 === prototype4);  //true
複製代碼

設置原型

設置原型對象有3種方法,簡單說明以下:this

  • obj.__proto__ = prototypeObj
  • Object.setPrototypeOf(obj,prototypeObj)
  • Object.create(prototypeObj)

其中,obj表示一個實例對象,prototypeObj表示原型對象。spa

下面代碼簡單演示利用上述三種方法爲對象直接設置原型:prototype

var proto = {name : "prototype"};  //原型對象
var obj1 = {};  //普通對象直接量
obj1.__proto__ = proto;  //設置原型
console.log(obj1.name);
var obj2 = {};  //普通對象直接量
Object.setPrototypeOf(obj2, proto);  //設置原型
console.log(obj2.name);
var obj3 = Object.create(proto);  //建立對象,並設置原型
console.log(obj3.name);
複製代碼

檢查原型

使用isPrototypeOf()方法能夠判斷該對象是否爲參數對象的原型。isPrototypeOf() 是一個原型方法,能夠在每一個實例對象上調用。設計

下面代碼簡單演示如何檢測原型對象:code

var F = function () {};  //構造函數
var obj = new F();  //實例化
var proto1 = Object.getPrototypeOf(obj);  //引用原型
console.log(proto1.isPrototypeOf(obj));  //true
複製代碼

也可使用下面代碼檢查不一樣類型的實例:

var proto = Object.prototype;
console.log(proto.isPrototypeOf({}));  //true
console.log(proto.isPrototypeOf([]));  //true
console.log(proto.isPrototypeOf(//)); //true
console.log(proto.isPrototypeOf(function () {}));  //true
console.log(proto.isPrototypeOf(null));  //false
複製代碼

原型屬性和私有屬性

原型屬性能夠被全部實例訪問,而私有屬性只能被當前實例訪問。

在下面示例中,演示如何定義一個構造函數,併爲實例對象定義私有屬性:

function F () {  //聲明一個構造函數
    this.a = 1;  //爲構造類型聲明一個私有屬性
    this.b = function () {  //爲構造類型聲明一個私有屬性
        return this.a;
    };
}
var e = new F();  //實例化構造函數
console.log(e.a);  //調用實例對象的屬性a,返回1
console.log(e.b());  //調用實例對象的方法b,提示1
複製代碼

構造函數 f 中定義了兩個私有屬性,分別是屬性 a 和 方法b()。當構造函數實例化後,實例對象繼承了構造函數的私有屬性。此時能夠在本地修改實例對象的屬性 a 和方法 b()。

e.a = 2;
console.log(e.a);
console.log(e.b());
複製代碼

若是給構造函數定義了與原型屬性同名的私有屬性,則私有屬性會覆蓋原型屬性值。

若是使用 delete 運算符刪除私有屬性,則原型屬性會被訪問。在上面示例的基礎上刪除私有屬性,則會發現能夠訪問原型屬性。

私有屬性能夠在實例對象中被修改,不一樣實例對象之間不會相互干擾:

function F () {  //聲明一個構造類型
    this.a = 1;  //爲構造類型聲明一個私有屬性
}
var e = new F ();  //實例e
var g = new F ();  //實例g
console.log(e.a);  //返回值爲1,說明它繼承了構造函數的初始值
console.log(g.a);  //返回值爲1,說明它繼承了構造函數的初始值
e.a = 2;  //修改實例e的屬性a的值
console.log(e.a);  //返回值爲2,說明e的屬性a的值改變了
console.log(g.a);  //返回值爲1,說明g的屬性a的值沒有受影響
複製代碼

上面示例演示了若是使用私有屬性,則實例對象之間就不會相互影響。可是若是但願統一修改實例對象中包含的私有屬性值,就須要一個個的修改,工做量會很大。

原型屬性將會影響全部實例對象,修改任何原型屬性值,則該構造函數的全部實例都會看到這種變化,這樣就省去了私有屬性修改的麻煩。

function F () {};  //聲明一個構造函數
F.prototype.a = 1;  //爲構造類型聲明一個私有屬性
var e = new F ();  //實例e
var g = new F ();  //實例g
console.log(e.a);  //返回值爲1,說明它繼承了構造函數的初始值
console.log(g.a);  //返回值爲1,說明它繼承了構造函數的初始值
F.prototype.a = 2;  //修改原型屬性值
console.log(e.a);  //返回值爲2,說明實例e的屬性a的值改變了
console.log(g.a);  //返回值爲2,說明實例g的屬性a的值改變了
複製代碼

在上面示例中,原型屬性值會影響全部實例對象的屬性值,對於原型方法也是如此。原型屬性或原型方法能夠在構造函數結構體內定義。

function f () {};  //聲明一個空的構造類型
f.prototype.a = 1;  //在結構體外爲構造類型聲明一個原型屬性
f.prototype.b = function () {  //在結構體外爲構造類型聲明一個原型方法
    return f.prototype.a;  //返回原型屬性值
}
複製代碼

prototype 屬性屬於構造函數,因此必須使用構造函數經過點語法來調用 prototype 屬性,再經過 prototype 屬性來訪問原型對象

利用對象原型與私有屬性之間的這種特殊關係能夠設計如下有趣的演示效果:

function P (x, y, z) {  //構造函數
    this.x = x;  //聲明私有屬性x並賦值參數x的值
    this.y = y;  //聲明私有屬性y並賦值參數y的值
    this.z = z;  //聲明私有屬性z並賦值參數z的值
}
P.prototype.del = function () {  //定義原型方法
    for (var i in this) {  //遍歷本地對象,刪除實例內的全部屬性和方法
        delete this[i];
    }
}
P.prototype=new P (1, 2, 3);  //實例化,並把實例對象傳遞給原型對象
var p1 = new P (10, 20, 30);  //實例化構造函數p爲p1
console.log(p1.x);  //返回10,私有屬性x的值
console.log(p1.y);  //返回20,私有屬性y的值
console.log(p1.z);  //返回30,私有屬性z的值
p1.del();  //調用原型方法刪除全部私有屬性
console.log(p1.x);  //返回1,原型屬性x的值
console.log(p1.y);  //返回2,原型屬性x的值
console.log(p1.z);  //返回3,原型屬性x的值
複製代碼

上面示例定義了構造函數 P,聲明瞭 3 個私有屬性,並實例化構造函數,把實例對象賦值給構造函數的原型對象。同時定義了原型方法 del(),該方法將刪除實例對象的全部私有屬性和方法。最後,分別調用屬性 x、y 和 z,返回的是私有屬性值,調用方法 del(),刪除全部私有屬性,再次調用屬性 x、y 和 z,則返回的是原型屬性值。

應用原型

利用原型爲對象設置默認值。當原型屬性與私有屬性同名時,刪除私有屬性後訪問原型屬性,便可以把原型屬值做爲初始化默認值。

function P (x) {  //構造函數
    if (x) {  //若是參數存在,則設置屬性,該條件是關鍵
        this.x = x;  //使用參數初始化私有屬性x的值
    }
}
P.prototype.x = 0;  //利用原型屬性,設置私有屬性x的默認值
var p1 = new P();  //實例化一個沒有帶參數的對象
console.log(p1.x);  //返回0,即顯示私有屬性的默認值
var p2 = new P(1);  //再次實例化,傳遞一個新的參數
console.log(p2.x);  //返回1,即顯示私有屬性的初始化值
複製代碼

利用原型間接實現本地數據備份。把本地對象的數據徹底賦值給原型對象,至關於爲該對象定義一個副本,也就是備份對象。當對象屬性被修改時,就能夠經過原型對象來恢復本地對象的初始值。

function P (x) {  //構造函數
    this.x = x;
}
P.prototype.backup = function () {  //原型方法,備份本地對象的數據到原型對象中
    for (var i in this) {
        P.prototype[i] = this[i];
    }
}
var p1 = new P(1);  //實例化對象
p1.backup;  //備份實例對象中的數據
p1.x = 10;  //改寫本地對象的屬性值
console.log(p1.x);  //返回10,說明屬性值已經被改寫
p1 = P.prototype;  //恢復備份
console.log(p1.x);  //返回1,說明對象的屬性值已經被恢復
複製代碼

利用原型還能夠爲對象屬性設置"只讀"特性,這在必定程度上能夠避免對象內部被任意修改的問題。下面實例演示瞭如何根據平面上兩點座標來計算它們之間的距離。構造函數P用來設定定位座標,當傳遞兩個參數值時,會返回以參數爲座標值的點。若是省略參數則默認點爲圓點(0,0)。而在構造函數L中經過傳遞的兩點座標對象計算它們的距離。

function P (x, y) {  //求座標點構造函數
    if (x) this.x = x;  //初始x軸值
    if (y) this.y = y;  //初始y軸值
    P.prototype.x = 0;  //默認x軸座標
    P.prototype.y = 0;  //默認y軸座標
}
function L (a, b) {  //求兩點距離構造函數
    var a = a;  //參數私有化
    var b = b;  //參數私有化
    var w = function () {  //計算x軸距離,返回對函數引用
        return Math.abs(a.x - b.x);
    }
    var h = function () {  //計算y軸距離,返回對函數引用
        return Math.abs(a.y - b.y);
    }
    this.length = function () {  //計算兩點距離,調用私有方法w()和h()
        return Math.sqrt(w() * w() + h() * h());
    }
    this.b = function () {  //獲取起點座標對象
        return a;
    }
    this.e = function () {  //獲取終點座標對象
        return b;
    }
}
var p1 = new P (1, 2);  //實例化P構造函數,聲明一個點
var p2 = new P (10, 20);  //實例化P構造函數,聲明另外一個點
var l1 = new L (p1, p2);  //實例化L構造函數,傳遞兩個對象
console.log(l1.length());  //返回20.12461179749811,計算兩點距離
l1.b().x = 50;  //不經意改動方法b()的一個屬性爲50
console.log(l1.length());  //返回43.86342439892262,說明影響兩點距離值
複製代碼

在測試中會發現,若是無心間修改了構造函數 L 的方法 b() 或 e() 的值,則構造函數 L 中的 length() 方法的計算值也隨之發生變化。這種動態效果對於須要動態跟蹤兩點座標變化來講,是很是必要的。可是,這裏並不須要當初始化實例以後,隨意的被改動座標值。畢竟方法 b() 和 e() 與參數 a 和 b 是沒有多大聯繫的。

爲了不由於改動方法 b() 的屬性 x 值會影響兩點距離,能夠在方法 b() 和 e() 中新建一個臨時性的構造類,設置該類的原型爲 a,而後實例化構造類並返回,這樣就阻斷了方法 b() 與私有變量 a 的直接聯繫,它們之間僅是值得傳遞,而不是對對象 a 的引用,從而避免由於方法 b() 的屬性值變化而影響私有對象 a 的屬性值。

this.b = function () {  //方法b()
    function temp () {};  //臨時構造類
    temp.prototype = a;  //把私有對象傳遞給臨時構造類的原型對象
    return new temp();  //返回實例化對象,阻斷直接返回a的引用關係
}
this.e = function () {  //方法e()
    function temp () {};  //臨時構造類
    temp.prototype = a;  //把私有對象傳遞給臨時構造類的原型對象
    return new temp();  //返回實例化對象,阻斷直接返回a的引用關係
}
複製代碼

還有一種方法是在給私有變量 w 和 h 賦值時,不是賦值函數,而是函數調用表達式,這樣私有變量 w 和 h 存儲的時值類型數據,而不是對函數結構的引用,從而就再也不受後期相關屬性值的影響。

function l (a, b) {  //求兩點距離構造函數
    var a = a;  //參數私有化
    var b = b;  //參數私有化
    var w = function () {  //計算x軸距離,返回函數表達式的計算值
        return Math.abs(a.x - b.x);
    } ()
    var h = function () {  //計算y軸距離,返回函數表達式的計算值
        return Math.abs(a.y - b.y);
    } ()
    this.length = function () {  //計算兩點距離,直接使用私有變量 w 和 h 來計算
        return Math.sqrt(w() * w() + h() * h());
    }
    this.b = function () {  //獲取起點座標
        return a;
    }
    this.e = function () {  //獲取終點座標
        return b;
    }
}
複製代碼

原型鏈

在JavaScript中,實例對象在讀取屬性時老是先檢查私有屬性。若是存在,則會返回私有屬性值;不然就會檢索prototype原型;若是找到同名屬性,則返回prototype原型的屬性值。

prototype原型容許引用其餘對象。若是在prototype原型中沒有找到指定的屬性,則JavaScript將會根據引用關係,繼續檢索prototype原型對象的prototype原型,依次類推。

原型鏈解決主要問題是繼承問題.

下面示例演示了對象屬性查找原型的基本方法和規律:

function A (x) {  //構造函數A
    this.x = x;
}
A.prototype.x = 0;  //原型屬性x的值爲0
function B (x) {  //構造函數B
    this.x = x;
}
B.prototype = new A (1);  //原型對象爲構造函數a的實例
function C (x) {  //構造函數c
    this.x = x;
}
C.prototype = new B(2);  //原型對象爲構造函數b的實例
var d = new C(3);  //實例化構造函數C
console.log(d.x);  //調用實例對象d的屬性x,返回值爲3
delete d.x;  //刪除實例對象的私有屬性x
console.log(d.x);  //調用實例對象d的屬性x,返回值爲2
delete C.prototype.x;  //刪除c類的原型屬性x
console.log(d.x);  //調用實例對象d的屬性x,返回值爲1
delete B.prototype.x;  //刪除b類的原型屬性x
console.log(d.x);  //調用實例對象d的屬性x,返回值爲0
delete A.prototype.x;  //刪除a類的原型屬性x
console.log(d.x);  //調用實例對象d的屬性x,返回值爲undefined
複製代碼

原型鏈可以幫助用戶更清楚的認識 JavaScript 面向對象的繼承關係,如圖所示。

74328923.png

在JavaScript中,一切都是對象,函數時第一型。Function和Object都是函數的實例。構造函數的父原型指向Function的原型,Function.prototype的原型時Object的原型,Object的原型也指向Function的原型,Object.prototype是全部原型的頂層。

Function.prototype.a = function () {  //Function原型方法
    console.log("Function");
}
Object.prototype.a = function () {  //Object原型方法
    console.log("Object");
}
function F () {  //構造函數f
    this.a = "a";
}
F.prototype = {  //構造函數f的原型方法
    w : function () {
        console.log("w");
    }
}
console.log(F instanceof Function);  //返回true,說明f是Function的實例
console.log(F.prototype instanceof Object);  //返回true,說明f的原型也是對象
console.log(Function instanceof Object);  //返回true,說明Function是Object的實例
console.log(Function.prototype instanceof Object);  //true,說明Function是Object的實例
console.log(Object instanceof Function);  //返回true,說明Object 是Function的實例
console.log(Object.prototype instanceof Function);  //false,說明Object.prototype是原型頂
複製代碼

原型繼承

原型繼承是一種簡化的繼承機制,也是JavaScript原生支持的繼承模式。在原型繼承中,類和實例概念被淡化了,一切都從對象的角度來考慮。原型繼承不在須要使用類來定義對象的解構,直接定義對象,並被其餘對象引用,這樣就造成了一種繼承關係,其中引用對象被成爲原型對象。JavaScript可以根據原型鏈來查找對象之間的這種繼承關係。

function A (x) {  //A類
    this.x1 = x;  //A的私有屬性x1
    this.get1 = function () {  //A的私有方法get1()
        return this.x1;
    };
}
function B(x) {  //B類
    this.x2 = x;  //B的私有屬性x2
    this.get2 = function () {  //B的私有方法get2()
        return this.x2 + this.x2;
    };
}
B.prototype = new A (1);  //原型對象繼承A的實例
function C (x) {  //C類
    this.x3 = x;  //C的私有屬性x3
    this.get3 = function () {  //C的私有方法get3()
        return this.x3 * this.x3;
    };
}
C.prototype = new B (2);  //原型對象繼承B的實例
複製代碼

在上面示例中,分別定義了 3 個構造函數,而後經過原型鏈把它們串聯在一塊兒,這樣 C 就可以繼承 B 和 A 函數的成員,而 B 可以繼承 A 的成員。

prototype最大的特色就是可以容許對象實例共享原型對象的成員。所以,若是把某個對象做爲一個類型的原型,那麼這個對象的類型也能夠做爲那些以這個對象爲原型的實例的父類。

var b = new B (2);  //實例化B
var c = new C (3);  //實例化C
console.log(b.x1);  //在實例對象b中調用A的屬性x1,返回1
console.log(c.x1);  //在實例對象c中調用A的屬性x1,返回1
console.log(c.get3());  //在實例對象c中調用C的方法get3(),返回9
console.log(c.get2());  //在實例對象c中調用B的方法get2(),返回4
複製代碼

基於原型的編程是面向對象編程的一種特定形式。在這種編程模型中,不須要聲明靜態類,而是經過複製已經存在的原型對象來實現繼承關係的。所以,基於原型的模型沒有類的概念,原型繼承中的類僅是一種模式,或者說是沿用面向對象編程的概念。

原型繼承的優勢是結構簡練,使用簡便,可是也存在如下幾個缺點:

  • 每一個類型只能有一個原型,因此它不支持多重繼承
  • 不能友好的支持帶參數的父類
  • 使用不靈活。在原型聲明階段實例化父類,並把它做爲當前類型的原型,這限制了父類實例化的靈活性,沒法肯定父類實例化的時機和場合
  • prototype屬性固有的反作用

擴展原型方法

JavaScript 容許經過 prototype 爲原生類型擴展方法,擴展方法能夠被全部對象調用。例如,經過 Function.prototype 爲函數擴展方法,而後爲全部函數調用。

爲 Function 添加一個原型方法 method,該方法能夠爲其餘類型添加原型方法。

Function.prototype.method = function (name, func) {
    this.prototype[name] = func;
    return this;
};
複製代碼

下面利用 method 擴展方法爲 Number 擴展一個 int 原型方法。該方法能夠對浮點數進行取整。

Number.method('int', function () {
    return Math[this < 0 ? 'ceil' : 'floor'] (this);
});
console.log((-10 / 3).int());  //-3
複製代碼

Number.method 方法可以根據數字的正負來判斷是使用 Math.ceil 仍是 Math.floor,這樣就不須要每次都編寫上面的代碼。

經過爲原生的類型擴展方法,能夠大大提升 JavaScript 編程靈活性。可是在擴展基類時務必當心,避免覆蓋原生方法。建議在覆蓋以前先肯定是否已經存在該方法。

Function.prototype.method = function (name, func) {
    if (!this.prototype[name]) {  //檢測是否已經存在同名屬性
        this.prototype[name] = func;
        return this;
    }
};
複製代碼

另外,可使用 hasOwnProperty 方法過濾原型屬性或者私有屬性。

相關文章
相關標籤/搜索