原型—-《你不知道的js》

圓滾滾的程式猿發表於2019-01-11

一、[[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()來構造一個新物件進行關聯。

來源:https://juejin.im/post/5c37f626e51d4551e533df16

相關文章