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

1、[[Prototype]]

js中的對象有一個特殊的[[Prototype]]內置屬性,即對於其餘對象的引用。幾乎全部的對象在建立時[[Prototype]]屬性都會被賦予一個非空值。對象的[[Prototype]]連接能夠爲空,但不多見。瀏覽器

var myObject = {
    a:2
};
myObject.a; // 2
複製代碼

當你試圖引用對象的屬性時會觸發[[Get]]操做,如myObject.a。對於默認的[[Get]]操做來講,第一步是檢查對象自己是否有這個屬性,如有就使用。bash

若是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。ui

若是anotherObject也找不到a而且[[Prototype]]不爲空,則繼續查找下去。this

這個過程會持續到找到匹配的屬性名或查找完整條[[Prototype]]鏈。如果後者[[Get]]操做返回undefined。spa

使用for..in遍歷對象時原理和查找[[Prototype]]鏈相似,任何能夠經過原型鏈訪問到的屬性都會被枚舉。使用in操做符來檢查屬性在對象中是否存在,一樣會查找對象的整條原型鏈(不管對象是否可枚舉):prototype

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
複製代碼

一、Object.prototype

全部普通的[[Prototype]]鏈最終對吼指向內置的Object.prototype。因爲全部「普通」(內置)對象都「源於」Object.prototype,因此它包含了js中許多通用功能日誌

二、屬性設置和屏蔽

myObject.foo = "far";
複製代碼

若是myObject對象包含名爲foo的普通數據訪問屬性,這條賦值語句只會修改已有的屬性值。code

若是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"會出現的三種狀況。

一、若是在[[Prototype]]鏈上層存在名爲foo的普通數據訪問屬性而且沒有被標記爲只讀(writable:false)則直接在myObject中添加一個名爲foo的屬性,它是屏蔽屬性。

二、若是在[[Prototype]]鏈上層存在foo,但被標記爲只讀(writable:false),則沒法需改已有屬性或在myObject上建立屏蔽屬性。若是在嚴格模式下,代碼則會跑出一個錯誤。不然,這條賦值語句會被忽略。總之,不會發生屏蔽。

三、若是在[[Prototype]]鏈上層存在foo而且它是一個setter,則會調用這個setter。foo不會被添加到或屏蔽於myObject,也不會從新定義foo這個setter。

若是但願在第二種第三種狀況下也屏蔽foo,就不能用=操做符來賦值,而使用Object.defineProperty(..)來向myObject添加foo。

一般應當儘可能避免使用屏蔽

有些狀況下會隱式產生屏蔽:

var anotherObject = {
    a:2
};
var myObject = Object.create(anotherObject);
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty("a"); // true
myObject.a++; // 隱式屏蔽
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty("a"); // true
複製代碼

++操做首先會經過[[Prototype]]查找屬性a並從anotherObject.a獲取當前屬性值2,而後給這個值加1,接着用[[Put]]將值3賦給myObject中新建的屏蔽屬性a

2、「類」

一、「類」函數

全部的函數默認都會擁有一個名爲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所指向的對象。

二、「構造函數」

function Foo() {
    // ...
}
// constructor公有不可枚舉的屬性
Foo.prototype.constructor === Foo; // true
var 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; // false
a1.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時,函數調用會變成「構造函數調用」

三、技術

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展現了另外兩種「面向類」的技巧:

一、this.name = name給每一個對象,即a,b都添加了.name屬性

二、Foo.prototype.myName = ...會給Foo.prototype對象添加一個屬性(函數)

在建立過程當中,a、b的內部[[Prototype]]都會關聯到Foo.prototype上。當a、b中沒法找到myName時,它會經過委託在Foo.prototype上找到

3、(原型)繼承

function Foo(name) {
    this.name = name;
}
Foo.prototype.myName = function() {
    return this.name;
}
function Bar(name, label) {
    Foo.call(this, name);
}
// 建立一個新的Bar.prototype對象並關聯到Foo.prototype
Bar.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.prototype
Bar.prototype = Object.create(Foo.prototype); 
// ES6能夠直接修改現有的Bar.prototype
Object.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; // true
a.__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]]對象關聯看作是隻讀特性,從而增長代碼可讀性

4、對象關聯

一、建立關聯

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()來構造一個新對象進行關聯。

相關文章
相關標籤/搜索