原型及原型鏈是javascript中非常重要的東西,對看別人源碼和自己設計框架和深入理解javascript這名語言特別有用。
原型及原型鏈名詞解釋
prototype 每一個函數都有一個prototype(原型)屬性,這個屬性是一個指針,指向了一個對象,而這個對象的用途就是包含可以由特定類型的所有實例共享的屬性和方法。
proto 所有引用類型(函數,數組,對象)都擁有__proto__屬性。JavaScript 只有一種結構:對象。每個實例對象( object )都有一個私有屬性(稱之爲 proto )指向它的構造函數的原型對象(prototype )。
proto 屬性是JavaScript 的非標準但許多瀏覽器實現的屬性__proto__
原型對象 擁有prototype屬性的對象,在定義函數時就被創建
prototype原型
原型是Javascript中的繼承的基礎,JavaScript的繼承就是基於原型的繼承。
Javascript的繼承機制基於原型,而不是Class類。
- 函數和對象的關係
function fun1(){};
let fun12 = function(){};
let fun13 = new Function('content','console.log(content)');
let obj1 = new fun1();
let obj2 = {};
let obj3 =new Object();
console.log(typeof Object); //function
console.log(typeof Function); //function
console.log(typeof Array); //function
// Object Function和 Array 都是函數
console.log(typeof obj1); //object
console.log(typeof obj2); //object
console.log(typeof obj3); //object
console.log(typeof fun1); //function
console.log(typeof fun2); //function
console.log(typeof fun3); //function
- 原型
函數也是一種對象。它是屬性的集合。每個函數都有一個屬性叫做prototype。
這個prototype的屬性值是一個對象(屬性的集合,再次強調!),默認的只有一個叫做constructor的屬性,指向這個函數本身。
原型既然作爲對象,屬性的集合,不可能就只弄個constructor來玩玩,肯定可以自定義的增加許多屬性。我們看下Object,它的prototype裏面,就有好幾個其他屬性。
下面我們來看一下自己定義函數的原型
function Person(){
}
Person對象會自動獲得prototyp屬性,而prototype也是一個對象,會自動獲得一個constructor屬性,該屬性正是指向Person對象。我們看一下Person的原型
自己自定義的方法的prototype中新增自己的屬性
function Person(){
}
Person.prototype.name = 'Jason';
Person.prototype.getYear = function(){
return 1989;
}
那原型對象是用來做什麼的呢?主要作用是用於繼承
我們來看下面的代碼
function Person(){
}
Person.prototype.name = 'Jason';
Person.prototype.getYear = function(){
return 1989;
}
let person = new Person();
console.log(person.name); //Jason
console.log(person.getYear()); //1989
我們來看一下增加了原型方法和屬性後的原型
從上面可以看出,通過給Person.prototype設置了一個函數對象的屬性,那有Person實例對象(person)出來的普通對象就繼承了這個屬性。具體是怎麼實現的繼承,就要講到下面的原型鏈了
- __proto__屬性
我們上面的例子,創建了person同時,自動生成一個__proto__屬性,該屬性指向Person的prototype,可以訪問到prototype內定義的getYear方法.
person.proto === Person.prototype //true
- prototype內屬性、方法是能夠共享
我們直接上代碼
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.personArray = [];
let person1 = new Person('Jason', 29);
let person2 = new Person('Peter', 5);
person1.personArray.push(1);
person2.personArray.push(2);
console.log(person1.personArray); //[1,2]
因此當代碼讀取某個對象的某個屬性的時候,都會執行一遍搜索,目標是具有給定名字的屬性,搜索首先從對象實例開始,如果在實例中找到該屬性則返回,如果沒有則查找prototype,如果還是沒有找到則繼續遞歸prototype的prototype對象,直到找到爲止,如果遞歸到object仍然沒有則返回undefine。同樣道理如果在實例中定義如prototype同名的屬性或函數,則會覆蓋prototype的屬性或函數(其它高級語言的重寫)。—-這就是Javascript的原型鏈。
原型鏈
原型鏈基礎
- JS在創建對象(不論是普通對象還是函數對象)的時候,都有一個叫做_ proto _的內置屬性,用於指向創建它的函數對象的原型對象prototype。
我們還是拿上面的例子
function Person(){
}
Person.prototype.name = 'Jason';
Person.prototype.getYear = function(){
return 1989;
}
let person = new Person();
console.log(person.__proto__ === Person.prototype) //true
- Person.prototype對象也有__proto__屬性,它指向創建它的函數對象(Object)的prototype
console.log(Person.prototype.__proto__=== Object.prototype) //true
- Object.prototype對象也有__proto__屬性,但它比較特殊,爲null
console.log(Object.prototype.__proto__) //null
4.這個有__proto__串起來的直到Object.prototype.__proto__爲null的鏈叫做原型鏈,因此person的原型鏈如下:
- 原型鏈中屬性查找
當查找一個對象的屬性時,JavaScript 會向上遍歷原型鏈,直到找到給定名稱的屬性爲止,到查找到達原型鏈的頂部 - 也就是 Object.prototype - 但是仍然沒有找到指定的屬性,就會返回 undefined
使用不同的方法來創建對象和生成原型鏈(此內容來自於MDN)
- 使用語法結構創建的對象
var o = {a: 1};
// o 這個對象繼承了 Object.prototype 上面的所有屬性
// o 自身沒有名爲 hasOwnProperty 的屬性
// hasOwnProperty 是 Object.prototype 的屬性
// 因此 o 繼承了 Object.prototype 的 hasOwnProperty
// Object.prototype 的原型爲 null
// 原型鏈如下:
// o ---> Object.prototype ---> null
var a = ["yo", "whadup", "?"];
// 數組都繼承於 Array.prototype
// (Array.prototype 中包含 indexOf, forEach 等方法)
// 原型鏈如下:
// a ---> Array.prototype ---> Object.prototype ---> null
function f(){
return 2;
}
// 函數都繼承於 Function.prototype
// (Function.prototype 中包含 call, bind等方法)
// 原型鏈如下:
// f ---> Function.prototype ---> Object.prototype ---> null
- 使用構造器創建的對象
在 JavaScript 中,構造器其實就是一個普通的函數。當使用 new 操作符 來作用這個函數時,它就可以被稱爲構造方法(構造函數)。
function Graph() {
this.vertices = [];
this.edges = [];
}
Graph.prototype = {
addVertex: function(v){
this.vertices.push(v);
}
};
var g = new Graph();
// g 是生成的對象,他的自身屬性有 'vertices' 和 'edges'。
// 在 g 被實例化時,g.[[Prototype]] 指向了 Graph.prototype。
- 使用 Object.create 創建的對象
ECMAScript 5 中引入了一個新方法:Object.create()。可以調用這個方法來創建一個新對象。新對象的原型就是調用 create 方法時傳入的第一個參數:
var a = {a: 1};
// a ---> Object.prototype ---> null
var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (繼承而來)
var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null
var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 因爲d沒有繼承Object.prototype
- 使用 class 關鍵字創建的對象
ECMAScript6 引入了一套新的關鍵字用來實現 class。使用基於類語言的開發人員會對這些結構感到熟悉,但它們是不同的。JavaScript 仍然基於原型。這些新的關鍵字包括 class, constructor,static,extends 和 super。
"use strict";
class Polygon {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
class Square extends Polygon {
constructor(sideLength) {
super(sideLength, sideLength);
}
get area() {
return this.height * this.width;
}
set sideLength(newLength) {
this.height = newLength;
this.width = newLength;
}
}
var square = new Square(2);
- 性能
在原型鏈上查找屬性比較耗時,對性能有副作用,這在性能要求苛刻的情況下很重要。另外,試圖訪問不存在的屬性時會遍歷整個原型鏈。
遍歷對象的屬性時,原型鏈上的每個可枚舉屬性都會被枚舉出來。要檢查對象是否具有自己定義的屬性,而不是其原型鏈上的某個屬性,則必須使用所有對象從 Object.prototype 繼承的 hasOwnProperty 方法。下面給出一個具體的例子來說明它:
console.log(g.hasOwnProperty('vertices'));
// true
console.log(g.hasOwnProperty('nope'));
// false
console.log(g.hasOwnProperty('addVertex'));
// false
console.log(g.__proto__.hasOwnProperty('addVertex'));
// true
hasOwnProperty 是 JavaScript 中唯一一個處理屬性並且不會遍歷原型鏈的方法。(譯者注:原文如此。另一種這樣的方法:Object.keys())
注意:檢查屬性是否爲 undefined 是不能夠檢查其是否存在的。該屬性可能已存在,但其值恰好被設置成了 undefined
原型的使用
原型的使用方式
- 通過給Person對象的prototype屬性賦值對象字面量來設定Person對象的原型
我們還是使用上面的例子
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype = {
getPersonInfo: function(){
let info = `my name is ${this.name}, and i am ${this.age} years old`;
return info;
},
}
let person = new Person('Jason', 29);
person.getPersonInfo();
- 在賦值原型prototype的時候使用function立即執行的表達式來賦值
Person.prototype = function () { } ();
它的好處就是可以封裝私有的function,通過return的形式暴露出簡單的使用名稱,以達到public/private的效果
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype = function(){
getPersonInfo = function(){
let info = `my name is ${this.name}, and i am ${this.age} years old`;
return info;
}
return {getPersonInfo:getPersonInfo}
}();
let person = new Person('Jason', 29);
person.getPersonInfo();
上述使用原型的時候,有一個限制就是一次性設置了原型對象,我們再來說一下如何分來設置原型的每個屬性
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.getPersonInfo = function(){
let info = `my name is ${this.name}, and i am ${this.age} years old`;
return info;
}
let person = new Person('Jason', 29);
person.getPersonInfo();
重寫原型
我們來重寫原型方法,在使用第三方JS類庫的時候,往往有時候他們定義的原型方法是不能滿足我們的需要,但是又離不開這個類庫,所以這時候我們就需要重寫他們的原型中的一個或者多個屬性或function,我們可以通過繼續聲明的同樣的
getPersonInfo代碼的形式來達到覆蓋重寫前面的getPersonInfo功能
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.getPersonInfo = function(){
let info = `my name is ${this.name}, and i am ${this.age} years old`;
return info;
}
Person.prototype.getPersonInfo = function(){
return 'I am new getPersonInfo method';
}
let person = new Person('Jason', 29);
person.getPersonInfo();//I am new getPersonInfo method
這樣,我們計算得出了後面函數的結果,但是有一點需要注意:那就是重寫的代碼需要放在最後,這樣才能覆蓋前面的代碼