原型物件和原型鏈在前端的工作中雖然不怎麼顯式的使用到,但是也會隱式的使用了,比如使用的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__
:
- 使用字面量定義一個普通物件: var foo = {}
- 建立一個函式: function Foo(){};
包含Object()啦
- 建立物件例項: 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();
複製程式碼
上面程式碼中,相關的關係如下圖
實驗環節
小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,您可自行驗證哈,改善啥的):
原文請戳這裡