簡話 prototype 和 __proto__

就那發表於2018-07-16

在js中,prototype和__proto__是很難理解的兩個知識點,一是它倆名字差不多,容易混淆;二是在工作中能直接用到這兩個知識點的地方可能也不是特別多,但是這兩個知識點卻是非常重要的,尤其在實現繼承方面。

一、理解這倆名字

在我學習它倆的時候,最大的障礙就是名字,第一印象就感覺它倆是一個東西。如果以這種印象作為前提的話,就很容易走入一個迷宮。所以怎麼理解prototype和__proto__呢?

我個人理解的是:prototype指的是擴充套件; __proto__指的鏈子(原型鏈),所以我覺得如果用__chain__來替代__proto__能更好的理解這個鏈子,或者把proto兩邊的下劃線想象作鏈條,看到鏈條就想起原型鏈。

二、鏈子__proto__鏈到了哪

先來個例子:

function Dog(age){
    this.age = age;
}
Dog.prototype.name = "狗";

var dog = new Dog(3);
複製程式碼

此時dog是Dog的一個例項,我們可以列印下dog看看:

簡話 prototype 和 __proto__
發現dog私有屬性只有age,列印下dog.name也能得到"狗"。這是為什麼呢?dog除了私有屬性age外,還有個__proto__,點開dog的__proto__看一下:
簡話 prototype 和 __proto__
發現dog的__proto__中有了name這個屬性,那列印dog.name是不是就返回的這個name呢?

實際就是返回的這個name,js中訪問一個物件的屬性,會遵循一個規則:先在這個物件本身找私有屬性,如果找到了就返回,如果找不到就沿著原型鏈往上找,直到最頂層null還找不到就會返回undefined

那dog.__proto__為什麼會有name這個屬性呢?這是因為在構建dog例項的時候,會把dog的__proto__指向它的建構函式的prototype,簡單說就是例項的鏈子會鏈到建構函式的擴充套件上,驗證一下:

console.log(dog.__proto__ === Dog.prototype) //true
複製程式碼

一個例項的鏈子預設會往上鍊接到它的建構函式的擴充套件,並且能訪問這個擴充套件上面的屬性,除非手動改變這個鏈子連結的擴充套件

function Dog(age){
    this.age = age;
}
Dog.prototype.name = "狗";

function Cat(){
    this.type = "cat";
}
Cat.prototype.say = "miao~";

var dog = new Dog(3);
//手動改變了dog的鏈子
dog.__proto__ = Cat.prototype;
console.log(dog.name); //undefined
console.log(dog.say); //miao~
console.log(dog.age); //3
console.log(dog.type); //undefined

//手動改變__proto__指向Cat.prototype 還會出現下面的情況
console.log(dog instanceof Dog); //false
console.log(dog instanceof Cat); //true
複製程式碼

雖然把dog的鏈子鏈到了Cat的擴充套件上了,但是列印了dog.type還是undefined,這說明屬性只能在鏈子所鏈的擴充套件上找,跟是誰的擴充套件沒有很大的關聯。這種情況很類似使用Object.create()來生成的例項:

function Cat(){
    this.type = "cat";
}
Cat.prototype.say = "miao~";
var dog = Object.create(Cat.prototype, {
    age:{
        value:10
    }
})
console.log(dog.age); //10
console.log(dog.say); //miao~
conosle.log(dog.type); //undefined

//生成的例項的鏈子指向了 Object.create傳入的第一個引數
console.log(dog.__proto__ === Cat.prototype); //true
複製程式碼

三、誰具有預設的prototype

js只有函式預設擁有prototype屬性,由建構函式構造出來的例項預設是不具有擴充套件的,除非手動給這個例項加上擴充套件:

function Dog(age){
    this.age = age;
}
Dog.prototype.name = "狗";

var dog = new Dog(3);
console.log(dog.prototype) //undefined
//給dog新增一個叫prototype的屬性
dog.prototype = {
    say:"wangwang~";
}
複製程式碼

手動給例項新增prototype,並沒有什麼實際意義,就只是給一個物件新增一個屬性,如上給dog新增了prototype,跟以下類似:

dog = {
    age:3,
    prototype:{
        say:"wangwang~"
    },
    prototype2:{
        say:"wangwangwang~"
    }
}
複製程式碼

四、prototype裡面都有啥

以一個普通的函式為例,函式的prototype首先會有constructor屬性,constructor指向了這個函式本身:

function test(a) {
    console.log(a)
}

console.log(test.prototype.constructor === test); //true
複製程式碼

還會有__proto__屬性,為了方便我們把函式的prototype稱為fnPrototype,fnPrototypey也是個例項物件,它也會有建構函式,因此它的鏈子也會鏈到了構造它的函式的擴充套件

其實fnPrototypey是Object的一個例項,它的鏈子鏈到Object的擴充套件上,我們可以驗證下:

function test(a) {
    console.log(a)
}

var fnPrototype = test.prototype;
console.log(fnPrototype.__proto__ === Object.prototype); //true
複製程式碼

我們平時用到prototype比較多的情況會有:給Array、String、Function或者Object等js的源生構造器重寫或者新增擴充套件方法和屬性:

Array.prototype.replaceAll = function(){
    //code...
}
String.prototype.trim = function(){
    //code...
}
Function.prototype.bindFn = function(){
    //code...
}
Object.prototype.__type = "object";
複製程式碼

這些js源生的構造器也會有prototype屬性,每個構造器會有自己的一些對應方法,他們構造出來的例項的鏈子會鏈到這些擴充套件上,因此例項就能使用這些方法和屬性。 Function.prototype是個比較特殊的情況,它實際是一個源生封裝的函式,但是實現了__proto__連結到了Object.prototype,Object.prototype.__proto__指向了null,所以說js中的一切皆物件(雖然Object也是一個函式,這裡就不糾結到底是函式層級高還是物件層級高了,就認為Object層級是最高的吧):

console.log(Function.prototype.__proto__ === Object.prototype); //true
複製程式碼

簡話 prototype 和 __proto__

五、總結

簡單總結一下,例項的鏈子會連結到建構函式的擴充套件。訪問例項的屬性,先在本身找,找不到就沿著鏈子往上找鏈的擴充套件,再在這個擴充套件上找,還找不到就沿著這個擴充套件鏈子再往上找。

相關文章