一、[[Prototype]]
js中的物件有一個特殊的[[Prototype]]內建屬性,即對於其他物件的引用。幾乎所有的物件在建立時[[Prototype]]屬性都會被賦予一個非空值。物件的[[Prototype]]連結可以為空,但很少見。
var myObject = {
a:2
};
myObject.a;
// 2複製程式碼
當你試圖引用物件的屬性時會觸發[[Get]]操作,如myObject.a。對於預設的[[Get]]操作來說,第一步是檢查物件本身是否有這個屬性,若有就使用。
如果a不在myObject中,就需要使用物件的[[Prototype]]鏈了。
對於預設的[[Get]]操作來說,如果無法在物件本身找到需要的屬性,就會繼續訪問[[Prototype]]鏈:
var anotherObject = {
a:2
};
// 建立一個關聯到anotherObject的物件var myObject = Object.create(anotherObject);
myObject.a;
// 2複製程式碼
myObject物件的[[Prototype]]關聯到了anotherObject。myObject.a並不存在,但屬效能在anotherObject中找到2。
如果anotherObject也找不到a並且[[Prototype]]不為空,則繼續查詢下去。
這個過程會持續到找到匹配的屬性名或查詢完整條[[Prototype]]鏈。若是後者[[Get]]操作返回undefined。
使用for..in遍歷物件時原理和查詢[[Prototype]]鏈類似,任何可以通過原型鏈訪問到的屬性都會被列舉。使用in操作符來檢查屬性在物件中是否存在,同樣會查詢物件的整條原型鏈(無論物件是否可列舉):
var anotherObject = {
a:2
};
// 建立一個關聯到anotherObject的物件var myObject = Object.create(anotherObject);
for (var k in myObject) {
console.log("found: " + k);
}// found: a("a" in myObject);
// true複製程式碼
1、Object.prototype
所有普通的[[Prototype]]鏈最終對吼指向內建的Object.prototype。由於所有“普通”(內建)物件都“源於”Object.prototype,所以它包含了js中許多通用功能
2、屬性設定和遮蔽
myObject.foo = "far";
複製程式碼
如果myObject物件包含名為foo的普通資料訪問屬性,這條賦值語句只會修改已有的屬性值。
如果foo不是直接存在於myObject中[[Prototype]]鏈就會被遍歷,類似[[Get]]操作。如果原型鏈上找不到foo,foo就會被直接新增到myObject上。
如果foo存在於原型鏈上層,賦值語句myObject.foo = “bar”的行為就會有些不同。
如果屬性名foo既出現在myObject的[[Prototype]]鏈上層,則會發橫遮蔽。myObject中包含的foo屬性會遮蔽原型鏈上層的所有foo屬性,因為myObject.foo總是會選擇原型鏈中最底層的foo屬性。
下面直接分析如果foo不直接存在於myObject中而是存在於原型鏈上層時myObject.foo = “bar”會出現的三種情況。
1、如果在[[Prototype]]鏈上層存在名為foo的普通資料訪問屬性並且沒有被標記為只讀(writable:false)則直接在myObject中新增一個名為foo的屬性,它是遮蔽屬性。
2、如果在[[Prototype]]鏈上層存在foo,但被標記為只讀(writable:false),則無法需改已有屬性或在myObject上建立遮蔽屬性。如果在嚴格模式下,程式碼則會跑出一個錯誤。否則,這條賦值語句會被忽略。總之,不會發生遮蔽。
3、如果在[[Prototype]]鏈上層存在foo並且它是一個setter,則會呼叫這個setter。foo不會被新增到或遮蔽於myObject,也不會重新定義foo這個setter。
如果希望在第二種第三種情況下也遮蔽foo,就不能用=操作符來賦值,而使用Object.defineProperty(..)來向myObject新增foo。
通常應當儘量避免使用遮蔽
有些情況下會隱式產生遮蔽:
var anotherObject = {
a:2
};
var myObject = Object.create(anotherObject);
anotherObject.a;
// 2myObject.a;
// 2anotherObject.hasOwnProperty("a");
// truemyObject.a++;
// 隱式遮蔽anotherObject.a;
// 2myObject.a;
// 3myObject.hasOwnProperty("a");
// true複製程式碼
++操作首先會通過[[Prototype]]查詢屬性a並從anotherObject.a獲取當前屬性值2,然後給這個值加1,接著用[[Put]]將值3賦給myObject中新建的遮蔽屬性a
二、“類”
1、“類”函式
所有的函式預設都會擁有一個名為prototype的共有並且不可列舉的屬性,他會指向另一個物件:
function Foo() {
// ...
}Foo.prototype;
// {
}複製程式碼
這個物件通常被稱為Foo的原型。這個物件是在呼叫new Foo()時建立的,最後會被關聯到“Foo.prototype”物件上
function Foo() {
// ...
}var a = new Foo();
Object.getPrototypeOf(a) === Foo.prototype;
// true複製程式碼
呼叫new Foo()時會建立a,其中一步就是將a內部的[[Prototype]]連結到Foo.prototype所指向的物件。
2、“建構函式”
function Foo() {
// ...
}// constructor公有不可列舉的屬性Foo.prototype.constructor === Foo;
// truevar a = new Foo();
a.constructor === Foo;
// true複製程式碼
a本身沒有constructor屬性,雖然a.constructor確實指向Foo函式,但是這個屬性並不是表示a由Foo“構造”
實際上.constructor引用同樣被委託給了Foo.prototype,而Foo.prototype.constructor預設指向Foo。
a.constructor只是通過預設的[[Prototype]]委託指向Foo,這和“構造”毫無關係。
function Foo() {
/* .. */
}Foo.prototype = {
/* .. */
};
//建立一個新原型物件var a1 = new Foo();
a1.constructor === Foo;
// falsea1.constructor === Object;
// true複製程式碼
a1並沒有.constructor屬性,所以它會委託[[Prototype]]鏈上的Foo.prototype。但這個物件也沒有.constructor屬性(不過預設的Foo.prototype物件有這個屬性),所以它會繼續委託,這次會委託給委託鏈頂端的Object.prototype。這個物件有.constructor屬性,指向內建的Object(..)函式
當然你可以給Foo.prototype新增一個.constructor屬性,不過需要手動新增一個符合正常行為不可列舉的屬性。
function Foo() {
/* .. */
}Foo.prototype = {
/* .. */
};
//建立一個新原型物件// 需要在Foo.prototype上“修復”丟失的.constructor屬性// 新物件屬性起到Foo.prototype的作用// 關於defineProperty(..)Object.defineProperty( Foo.prototype, "constructor", {
enumerable: false, writable: true, configurable: true, value: Foo // 讓.constructor指向Foo
})複製程式碼
.constructor並不表示被構造,也不是一個不可變屬性。他是不可列舉的,但值是可寫的
1)建構函式還是呼叫
function NothingSpecial() {
console.log("Don't mind me!");
}var a = new NothingSpecial();
// Don't mind me!a;
// {
}複製程式碼
NothingSpecial只是一個普通的函式,但使用new呼叫時,就會構造一個物件並賦值給a。這個呼叫時一個建構函式呼叫,但NothingSpecial本身不是建構函式
函式不是建構函式,當且僅當使用new時,函式呼叫會變成“建構函式呼叫”
3、技術
function Foo(name) {
this.name = name;
}Foo.prototype.myName = function() {
return this.name;
}var a = new Foo("a");
var b = new Foo("b");
a.myName();
// "a"b.myName();
// "b"複製程式碼
這段diamante展示了另外兩種“面向類”的技巧:
1、this.name = name給每個物件,即a,b都新增了.name屬性
2、Foo.prototype.myName = …會給Foo.prototype物件新增一個屬性(函式)
在建立過程中,a、b的內部[[Prototype]]都會關聯到Foo.prototype上。當a、b中無法找到myName時,它會通過委託在Foo.prototype上找到
三、(原型)繼承
function Foo(name) {
this.name = name;
}Foo.prototype.myName = function() {
return this.name;
}function Bar(name, label) {
Foo.call(this, name);
}// 建立一個新的Bar.prototype物件並關聯到Foo.prototypeBar.prototype = Object.create(Foo.prototype);
// 核心部分// 注意現在沒有Bar.prototype.constructor了// 若你需要則手動修復Bar.prototype.myLabel = function() {
return this.label;
}var a = new Bar("a", "obj a");
a.myName();
// "a"a.myLabel();
// 'obj a'複製程式碼
建立一個新物件並把它關聯到我們希望的物件上有兩種常見的做法:
// 和你想要的機制不一樣Bar.prototype = Foo.prototype;
複製程式碼
此程式碼讓Bar.prototype直接引用Foo.prototype物件。因此當你執行型別Bar.prototype.myLabel = … 的賦值語句時會修改Foo.prototype物件本身
// 基本上滿足你的需求,但可能會產生一些副作用Bar.prototype = new Foo();
複製程式碼
此語句會建立一個關聯到Bar.prototype的新物件。但使用了Foo(..)的建構函式呼叫。如果函式Foo有一些副作用(如:寫日誌、修改轉態等)就會影響到Bar()的“後代”,後果不堪設想
要建立一個合適的關聯物件,最好使用Object.create(..)而不是具有徐作用的Foo(..)。但這需要建立一個新物件然後把舊物件拋棄掉,不能直接修改已有的預設物件。
// ES6之前需要拋棄預設的Bar.prototypeBar.prototype = Object.create(Foo.prototype);
// ES6可以直接修改現有的Bar.prototypeObject.setPrototypeOf(Bar.prototype, Foo.prototype);
複製程式碼
檢查“類”關係
內省(反射):檢查一個例項的繼承祖先(js的委託關聯)
function Foo() {
// ...
}Foo.prototype.blah = ...;
var a = new Foo();
複製程式碼
如何通過內省找出a的“祖先”(委託關聯)
1)a instanceof Foo;
在a的整條[[Prototype]]鏈中是否有指向Foo.prototype的物件
缺點:只能處理物件(a)和函式(帶.prototype引用的Foo)之間的關係
如果使用內建的.bind(..)函式來生成一個硬繫結函式的話,該函式沒有.prototype屬性。在這樣的函式上使用instanceof的話目標函式的.prototype會代替硬繫結函式的.prototype。
function isRelated(o1, o2) {
function F() {
} F.prototype = o2;
return o1 instanceof F;
}var a = {
};
var b = Object.create(a);
isRelated(b, a);
// true複製程式碼
2)判斷[[Prototype]]反射的方法
Foo.prototype.isPrototypeOf(a);
// true複製程式碼
在a的整條[[Prototype]]鏈,中是否出現過Foo.prototype
此方法不需要間接引用函式(Foo),它的.prototype屬性會被自動訪問
// 非常簡單:b是否出現在c的[[Prototype]]鏈中b.isPrototypeOf(c);
複製程式碼
這個方法並不需要使用函式(“類”),它直接使用b和c之間的物件引用來判斷他們的關係。換而言之,語言內建的isPrototypeOf(..)函式就是我們的isRelatedTo(..)函式
我們也可以直接獲取一個物件[[Prototype]]鏈,在ES5中標準方法:
Object.getPrototypeOf(a);
Object.getPrototypeOf(a) === Foo.prototype;
// truea.__proto__ === Foo.prototype;
// true 大多數瀏覽器也支援這種方法複製程式碼
.__proto__存在於內建的Object.prototype中(不可列舉)
Object.defineProperty(Object.prototype, "__proto__", {
get: function() {
return Object.getPrototypeOf(this);
}, set: function(o) {
Object.setPrototypeOf(this, o);
return 0;
}
})複製程式碼
因此,訪問(獲取值)a.__proto__時,實際上是呼叫了a.proto()(呼叫getter函式)。雖然getter函式存在於Object.prototype物件中,但它的this指向物件a,所以Object.getPrototypeOf(a)結果相同。
.__proto__是可設定屬性,但通常來說不需要修改已有物件的[[Prototype]],。
在特殊情況下需要設定函式預設.prototype物件的[[Prototype]],讓它引用其他物件(除了Object.prototype)這樣可避免使用全新物件替換預設物件。此外最好把[[Prototype]]物件關聯看做是隻讀特性,從而增加程式碼可讀性
四、物件關聯
1、建立關聯
var foo = {
something:function() {
console.log("Tell me sometihng good ...");
}
};
var bar = Object.create(foo);
bar.something();
// Tell me sometihng good ...複製程式碼
Object.create(..)會建立一個新物件(bar)並把它關聯到我們指定的物件(foo),這樣可以避免不必要的麻煩。
Object.create(..)的polyfill程式碼
Object.create(..)是在ES5中新增的函式,所以在ES5之前的環境中要支援這個功能的話就需要使用一段簡單的polyfill程式碼。
if (!Object.create) {
Object.create = function(o) {
function F(){
} F.prototype = o;
return new F();
}
}複製程式碼
這段polyfill程式碼使用了一個一次性函式F,我們通過改寫它的.prototype屬性使其指向想要關聯的物件,然後再使用new F()來構造一個新物件進行關聯。