@
原型鏈
JavaScript的對象通過其身上原型這一點特征,來實現與傳統的面向對象編程語言截然不同的繼承機制
prototype)原型是什么?
- 在JS中大多數情況下創建的對象,都擁有一個原型對象,創建的對象,從其原型為模板、來繼承方法和屬性。而原型對象本身都有可能擁有原型,並從中獲取方法和屬性,以此類推,這種關系便被稱為原型鏈,原型鏈的關系解釋了為何一個對象身上會擁有本不屬於他的屬性和方法。
- 所有擁有原型的對象的最頂層便是Object對象的prototype
prototype長什么樣子
先復制如下代碼放入到瀏覽器的控制台回車查看結果:
function test(){}
console.log(test.prototype);
放入到控制台打印的結果為:
這里我把其中的關系做了個圖來方便理解,忽略其他屬性,專注於constructor和__proto__屬性
- __proto__== prototype 這兩個是一樣的,你調用test.prototype和調用test.___proto___是一樣的
-
通過上面我們發現當調用test.prototype時,會出現兩個屬性:constructor、__proto__
- constructor: 該屬性指向了用於構造此實例對象的構造函數,在上例中就為function test(){}
-
我們會發現test.prototype.__proto__屬性的引用指向Object的原型對象
-
Object的原型對象里面同樣包含了:constructor、__proto__屬性,只不過Object的__proto__屬性的值為NULL,這跟我們前面說的:所有擁有原型的對象的最頂層便是Object對象的prototype,往后就沒有原型對象了,所以才會出現為NULL的情況
添加屬性
現在讓我們在test的原型上添加一個name屬性
test.prototype.name = 'zhang';
function test(){}
console.log(test.prototype);
我們可以發現在test的原型上多一個名為name的屬性
我們繼續來添加屬性,這次我們要實例化test的對象,需要用到new關鍵字
var obj = new test();
然后我們試着在obj對象里面添加一些屬性,然后再看整體的原型結構是什么樣子的
test.prototype.name = 'zhang';
function test(){}
var obj = new test();
obj.age = 13;
console.log(test.prototype);
這里我把其中的關系做了個圖來方便理解,忽略其他屬性
目前的結構為:obj對象里面有一個age屬性,而test.prototype原型上一個name屬性
訪問原型屬性 初探原理
正常情況下,當我們訪問一個對象里不存在的屬性時,會返回undefined
下面這一段代碼讓我們來看看結果是什么:
test.prototype.name = 'zhang';
function test(){
this.age = 12;
}
var obj = new test();
console.log(obj.name);
通過控制台打印我們發現結果為“zhang”
對於這個結果我們應該有疑惑比如:
- 為什么在test對象里面並沒有定義名為name的屬性,可是實例化之后我們卻可以訪問,並獲取其結果,為什么不是undefined
我們可能想到了於上文的原型有關,讓我們再來看段代碼:
console.log(obj.__proto__);
console.log(test.prototype);
讓我們來看看打印結果
可以發現obj.__proto__和test.prototype的打印結果是一致的
到這里我們應該也有些眉頭了
原型鏈的查找
- 當我們訪問obj.name時,瀏覽器首先查找test對象本身是否有這個name屬性,如果有就會直接拿來進行使用,例:
-
如果obj沒有這個name屬性,那么瀏覽器就會從obj的__proto__中查找這個屬性,在這里的obj.__proto__等同於test.prototype
-
如果obj.__proto__上有這個name屬性,那么就會獲取他,就如上例打印所示,在test.prototype上面有一個name屬性值,那么我們打印obj.name 就會取到test.prototype上面的屬性值
-
如果test.prototype對象上也沒有name屬性值,那么我們就會繼續往上一個__proto__上去找具有name屬性值的prototype,比如obj.proto.proto,在本例中,第三層就已經是Object對象了,在實際的例子中可以會有多層
-
如果Object.prototype上面也沒有name屬性,那么最終就會返回undefined,如下圖所示:
上面的關系用一張圖總結
理解原型對象
有如下代碼,我們來看看
function test(){
this.name = 12;
}
var obj = new test();
console.log(obj.name);
console.log(obj.toString());
這個toString()方法是哪里來的納?結合我們上文理解其實不難想到,應該是原型鏈中某一個環節里面的方法
這這個例子中有如下過程:
- 瀏覽器首先檢查obj對象里面是有可以使用的toString()方法
- 如果沒有可以使用的toString()方法,瀏覽器會查看obj對象的原型對象(即test構造函數的prototype屬性),是否有可用的toString()方法
- 如果也沒有,瀏覽器會繼續往上尋找,在本例中就為obj.proto.proto(即Object的prototype屬性)
- 我們在Object的prototype屬性里面找到了我們要使用的toString()方法,於是我們就會看到這個方法被調用的結果
注意:原型鏈中的方法和屬性沒有被復制到其他對象——它們被訪問需要通過前面所說的“原型鏈”的方式。
沒有官方的方法用於直接訪問一個對象的原型對象——原型鏈中的“連接”被定義在一個內部屬性中,在 JavaScript 語言標准中用
[[prototype]]
表示。然而,大多數現代瀏覽器還是提供了一個名為__proto__
(前后各有2個下划線)的屬性,其包含了對象的原型。你可以嘗試輸入實例化對象.__proto__
和實例化對象.__proto__.__proto__
,看看代碼中的原型鏈是什么樣的! ---以上引自MDN
如何定義一個可以被繼承的屬性或者方法?
我們來看一下Object對象
我們可以發現他的prototype里面就有我們之前案例的toString方法,但是在他的prototype外面有一些類似於create等方法
我們來段代碼:
function test(){
}
var obj = new test();
console.log(obj.toString());
console.log(obj.create());
結果如下:
系統提示我們obj.create並不是一個函數,但是obj.toString是可以輸出結果的,他們之間在這里的區別只有toString是在Object的原型里面定義的,而create方法是Object對象內部定義的方法,只有他自己是可以訪問的
那么講到這里,我們就可以定義的原型的方法和屬性被繼承
function test(){
}
test.prototype.name = '我要被繼承了';
test.prototype.toInt = function(){
console.log('我是toInt');
}
var obj = new test();
console.log(obj.name);
console.log(obj.toInt());
我們成功的利用test上定義的原型方法和屬性,使obj對象成功的獲取到
原型查找、重寫的就近原則
如果我們的代碼是這樣的,會出現什么結果納?
function test(){
}
Object.prototype.name = '我是Object的name屬性'
test.prototype.name = '我是test的name屬性';
test.prototype.toSring = function(){
console.log('我是test原型上面的toString方法');
}
var obj = new test();
console.log(obj.name);
console.log(obj.toSring());
- 我們先是在Object.prototype的原型對象上定義了名為name的屬性,並賦值
- 我們又在test.prototype的原型對象上也定義了名為name的屬性,並賦值
- 我們在已知Object的原型對象上有一個名為toString的方法,但是我們又缺心眼的在test.prototype的原型的對象上也定義了一個toString方法
- 那么當我們調用obj.name、obj.toString() 結果是怎么樣的納
結果是我們的name和toString全部執行的test原型對象里面的定義的方法和屬性,這里就引出了原型鏈查找時的就近原則
Object是在test原型鏈的上層,所以當我們調用name時會先使用test原型對象的屬性,如果沒有再往上進行查找
同時這里也出現了我們定義的方法和Object原型對象里面的定義的方法,名字相同的情況下,這種情況下,我們在原型鏈底層定義的與頂層同名的函數時,底層的方法會覆蓋頂層同名的函數(也稱為重寫),所以當調用同名函數時,按照原型鏈的就近規則,我們會取離得最近的原型對象里面的同名函數(這里的頂層可以理解為樹的根節點,也就是Object)
在這里我們如果直接調用Object.toString()方法
是不出現test定義的toString,因為我們是直接越過了底層原型鏈進行的頂層原型鏈的調用
構造函數里面的this指向問題
現在讓我們回歸到test構造函數里面的來看看this指向的問題
函數和構造函數他們之間其實沒有什么區別,但是一個可以實例化一個不可以是為什么?
其實主要是看new這個關鍵字,讓我們來看如下的代碼:
function test() {
console.log(this);
this.name = 'name';
}
test.prototype.age = 123;
var obj = new test();
讓我們來看看結果:
這里的打印的this是一個對象,對象里面的prototype屬性有我們在test原型上定義的age屬性和constructor,這里我畫個圖來方便大家理解
- this首先是一個對象,當你在test()構造函數內部用this.屬性定義屬性會放在圖中藍色區域,對象結果的name的同一層
- 而__proto__對象里面放的就是test原型鏈里面定義的屬性比如:age
- 這里在原型鏈尋找中,當我們沒有在test里面找到age屬性時,就會隱式的調用__proto__屬性里面的age屬性
- 我們打印obj會發現其結構是與this相同的,其實在我們new 對象時,就已經把這個this的值,傳給了new 對象時要賦值的變量
this是哪里來的
這個this是哪里來的納?
其實當我們new 對象時,在構造函數內部會有隱式的幾部操作
function test() {
// var this = {
// __proto__: test.prototype
// }
console.log(this);
this.name = 'name';
// return this;
}
test.prototype.age = 123;
var obj = new test();
當我們看到這幾部操作時,我想就都明白了為什么我們在構造函數內部定義屬性需要this來進行賦值
一些關於原型的常用操作
Object.create
- Object.create(參數) 可以指定的參數(原型對象)創建一個新的對象
示例如下:
function test() {
}
test.prototype.age = 123;
var obj = new test();
var obj2 = Object.create(test);
console.log(obj);
console.log(obj2);
我們可以發現Obj2的__proto__指向的原型對象為obj,而obj的原型對象為test,create相當於對指定的原型對象下面再添加一層鏈的關系
這里Object.create(null)的參數是可以為null的
我們可以發現obj2是沒有原型的,所以這也是文章開始時說的大部分對象都是有原型的
constructor 屬性
- 每個實例對象都從原型中繼承了一個constructor屬性,該屬性指向了用於構造此實例對象的構造函數。
案例如下:
function test() {
}
test.prototype.age = 123;
var obj = new test();
console.log(obj.constructor);
這里我們看到obj.constructor返回的是test構造函數
- 第一個輸出是因為我們創建了對象,打印出了param參數,但是因為沒有參數傳遞,所以為undefined
- 第二次是我們的打印的constructor
- 這里返回的constructor因為是一個對象,所以我們可以在后面加上()來實例化這個對象,這里我們實例化了他,並傳遞了參數12132,所以他第三次打印出了參數,因為已經成功到創建了對象
原型的增刪改查
-
查
- 這里我們可以在原型底層實例化對象中,直接.屬性或者方法,讓瀏覽器隱式的幫我們查找,也可以直接顯示的查找他們的__proto__屬性
-
改
-
構造函數.prototype.屬性/方法 = 值
-
-
增
-
構造函數.prototype.屬性/方法 = 值 如果嫌一個個賦值太麻煩,那么我們可以直接賦值一個對象 構造函數.prototype = { key:vlaue, name:'1123' }
-
-
刪
-
delete 構造函數.prototype.屬性/方法
-