深入理解原型物件和原型鏈

call_me_R發表於2018-04-12

frontend/javascript/prototype_object_chain

原型物件和原型鏈在前端的工作中雖然不怎麼顯式的使用到,但是也會隱式的使用了,比如使用的jquery,vue等啦。在進入正題的時候,我們還是需要明白什麼是__proto__prototype等知識點,主要講解建構函式,這篇博文大多是問答形式進行...

原文請戳這裡

問答環節

Javascript建立物件的方式?

也許你會說出工廠模式、建構函式模式、原型模式、組合使用建構函式和原型模式、動態原型模式、寄生建構函式模式和穩妥建構函式這些,但是我們可以對他們進行以下歸類--屬於函式建立物件。

我們可以簡單的將建立物件的方式分為三種:函式建立物件、字面量建立、Object建立。當然,也可以只是分成兩類:函式建立物件和字面量建立物件,因為new Object()中的Object是本身就是一個函式。

Object // f Object(){ [native code] }
複製程式碼

什麼是prototype?

function(注意是function哦)定義的物件有一個prototype屬性,prototype屬性又指向了一個prototype物件,注意prototype屬性與prototype物件是兩個不同的東西,要注意區別。用虛擬碼表示如下:

var function{
	prototype: prototype{} // function的prototype屬性指向prototype物件
}
複製程式碼

注意上面說的是function裡面才會有prototype屬性,而我們new出來的物件裡面是沒有的哦。

# function
function Fun(name){
	this.name = name;
}

Fun.prototype // {constructor:f}

var fun = new Fun('嘉明');
fun.prototype // undefined

# Object
Object.prototype // {constructor:f,__defineGetter__:f,...}

var object = new Object();
object.prototype // undefined

# 字面量,字面量可以理解沒有prototype啦
var jack = {};
jack.prototype // undefined
複製程式碼

__proto__是什麼?

在官方的es5種,定義了一個名叫做[[prototype]]的屬性,每個物件(除了null)都擁有這樣一個屬性,這個屬性是一個指標,它指向一個名叫做原型物件的記憶體堆。而原型物件也是一個物件,因此又含有自己的[[prototype]]屬性,又指向下一個原型物件,終點指向我們的Object.prototype物件。

注意⚠️ 這裡使用的是[[prototype]],而並非__proto__。可是他們是同一個東西哈:[[prototype]]是官方所定義的屬性,而__proto__是瀏覽器(就是任性)自己對[[prototype]]所做的實現。

分三種情況來說物件內部的__proto__

  1. 使用字面量定義一個普通物件: var foo = {}
  2. 建立一個函式: function Foo(){};包含Object()啦
  3. 建立物件例項: var foo = new Foo();

情況一:{}

var foo = {};
foo.__proto__; // {}
foo.__proto__ === Object.prototype; // true
foo.hasOwnProperty('prototype'); // false 函式才有prototype屬性
foo.hasOwnProperty('__proto__'); // false
Object.prototype.hasOwnProperty('__proto__'); // true
複製程式碼

程式碼的最後一行,一個是返回了false,另一個是true。⚠️因為它並不存在於foo物件(foo.__proto__)或者Foo.prototype(Foo.prototype.__proto__)或者Foo(Foo.__proto__)中【下面情況二和三會有程式碼驗證】,實際上,它是來自於Object.prototype,與其說是一個屬性,不如說是一個getter/setter。

情況二:function Foo(){}

1. function Foo(){};
2. Foo.__proto__; // [Function]
3. Foo.__proto__ === Object.prototype; // false
4. Foo.__proto__.__proto__ === Object.prototype; // true
5. Foo.prototype.__proto__ === Object.prototype; // true 函式的原型物件指向
6. Foo.__proto__ == Foo.prototype; //false
7. Foo.hasOwnProperty('__proto__'); // false
8. Foo.hasOwnProperty('prototype'); // true
複製程式碼

在函式中,通過上面程式碼2,3,4,5可以知道Foo.__proto__可以理解為指向了Foo.prototype(廣義上理解),而實際上兩個又有差別(狹義上,第6點可以說明,歡迎補充)。然後就是每個函式都有一個預設的prototype屬性,其指向函式的原型物件。

情況三:物件例項 new Foo()

function Foo(){};
var foo = new Foo();
foo.__proto__; // Foo {}
foo.__proto__ === Foo.prototype ; true
foo.hasOwnProperty('prototype'); false
foo.hasOwnProperty('__proto__'); false
複製程式碼

上面可知,例項中是沒有prototype這個屬性的,對比上面的三種情況,也說明了只有在函式中才預設建立了prototype屬性,而且指向了相應的函式原型物件。

constructor是什麼?

在javascript語言中,constructor屬性是專門為function而設計的,它存在於每一個function的prototype屬性中,這個constructor儲存了指向function的一個引用。

function F(){
	// some code
}

# javascript內部會執行如下的動作
# 1.為該函式新增一個原型(即prototype)屬性
# 2.為prototype物件額外新增一個constructor屬性,並且該屬性儲存指向函式F的一個引用
複製程式碼

物件的例項中也有一個constructor屬性(從prototype那裡獲取的),每一個物件例項都可以通過constructor物件訪問它的建構函式,如下:

var f = new F();
f.constructor === F; // true
f.constructor === F.prototype.constructor; // true
複製程式碼

既然可以訪問例項的型別f.constructor,那麼我們就可以對例項進行特殊的處理啦:

if(f.constructor == F){
	// some code
}
複製程式碼

不過別這樣操作,因為constructor是不穩定的(見下文物件中的constructor的作用是什麼呢?),一般不會採取上面的這種操作,而是通過instanceof

if(f instanceof F){
	// some code
}
複製程式碼

物件中的constructor的作用是什麼呢?

這裡推薦賀師俊前輩的回答,原文複製如下:

constructor屬性不影響任何javascript的內部屬性。instanceof檢測物件的原型鏈,通常你是無法修改的(不過某些引擎通過私有的__proto__屬性暴露出來)。

constructor其實沒有什麼用,只是javascript語言設計的歷史遺留物。由於constructor屬性是可以變更的,所以未必真的指向物件的建構函式,只是一個提示。不過,從程式設計習慣上,我們應該儘量讓物件的constructor指向其建構函式,以維持這種習慣。

例子解析:

delete Object.prototype.constructor; // true
({}).constructor; // undefined
({}) instanceof Object; // true
複製程式碼

原型鏈的最高指向?

《javascript高階程式設計》中有說到所有函式的預設原型都是Object的例項,因此預設原型都會包含一個內部指標,指向Object.prototype。那麼原型的最高指向就是Object了嘛?你可以理解是Object,可是我認為是null。

Object.prototype.__proto__; // null
Object.prototype.__proto__===null ; // true
複製程式碼

最高指向是Object/null,無傷大雅。

例項和原型的關係?

當讀取例項的屬性時,如果找不到例項的屬性,就會查詢與物件關聯的原型的屬性,如果還是查詢不到,就查詢原型的原型,一直到頂級為止。

function Person(){
}
Person.prototype.name = "嘉明";

var person = new Person();

person.name = "jiaming";
console.log(person.name); // "jiaming"

// 刪除例項的屬性後
delete person.name;
console.log(person.name); // "嘉明"

// 追加一個疑問  在__proto__中加屬性會覆蓋原來的嘛
person.__proto__.name = "嘉";
console.log(person.name); // "嘉"   證明成功,不建議這樣修改,畢竟__proto__是瀏覽器廠商實現的,非標準的

// 再追加一個疑問 __proto__新增的屬性或者方法是放在物件的原型上的嘛
var another_person = new Person();
console.log(another_person.name); // "嘉"  證明是放在物件的原型上的
複製程式碼

原型的原型呢?

屬性或者方法在自己的原型上沒有找到的話,那就要跑到原型上去找啦。之前有提到過所有函式的預設原型都是Object的例項,因此預設原型都會包含一個內部指標,指向Object.prototype。

那麼一個建構函式function Person(){}就存在這樣的一個關係,例項出來的var person = new Person()person通過__proto__指向建構函式的原型Person.prototype,然後建構函式的原型指向Object的原型,即是Person.prototype.__proto__指向Object.prototype

總結下唄

嗯,還是針對建構函式來說哈,將上面提到的知識點彙總一下啦。上面都是純文字說明,下面就配上圖片好好理解下。

先上相關程式碼:

function Person(name){
	this.name = name;
}
Person.prototype.sayName = function(){
	console.log(this.name);
}

var person = new Person("嘉明");
person.age = 12;
person.sayName(); // "嘉明"
console.log(person.name.toString()); // "嘉明"

var another_person = new Person("jiaming");
another_person.sayName();
複製程式碼

上面程式碼中,相關的關係如下圖

frontend/javascript/prototype_chain_summerize_img01

實驗環節

小demo是使用canvas畫出小星光,程式碼如下:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>canvas</title>
    <style>
        body{
            margin: 0;
            padding: 0;
            position: relative;
        }
        #myCanvas{
            position: absolute;
            left: 50%;
            top: 50%;
            background: #000;
            margin-left: -300px;
            margin-top: -150px;
        }
    </style>
</head>
<body>
    <canvas id="myCanvas" width="600" height="300" style="border: 1px solid #000;">

    </canvas>
    <script src="path/to/canvas.js"></script>
</body>
</html>
複製程式碼
window.onload = function(){
    var c = document.getElementById('myCanvas');

    var grd = ""; // 漸變的顏色

    // 上下文
    var context = c.getContext("2d");
    
    if(context){
        // x,y,r 座標和半徑
        function Star(x,y,r){
            this.x = x;
            this.y = y;
            this.r = r;
            this.init(this.x,this.y,this.r);
        }
        
        // 繪製星星
        Star.prototype.init = function(x,y,r){
        
        
            context.beginPath();
        
            // 漸變顏色
            grd = context.createRadialGradient(x,y,r-2,x,y,r+2)
            grd.addColorStop(0, 'white');
            grd.addColorStop(1, 'yellow');
            context.fillStyle=grd;
        
            //  畫圓
            context.arc(x,y,r,0,2*Math.PI);
        
            // 填充顏色
            context.fill();
        
            context.closePath();
        
        
        }
        
        // 建立星星
        for(var i = 0; i < 300; i++){
            var x = Math.floor(Math.random()*600);
            var y = Math.floor(Math.random()*300);
            var r = Math.floor(Math.random()*3)+2;
            new Star(x,y,r)
        }
    }else{
        var div = document.createElement("div");
        div.innerHTML = "您的瀏覽器不支援canvas,請升級瀏覽器!";
        document.getElementsByTagName("body")[0].appendChild(div);
    }
}
複製程式碼

實現的簡單效果如下圖哈(ps,您可自行驗證哈,改善啥的):

frontend/javascript/prototype_chain_demo

原文請戳這裡

相關文章