JavaScript原型與原型鏈分析

itsmikej發表於2015-03-21

JavaScript沒有類的概念,但幾乎所有的東西又是基於物件的,同時也能實現繼承,這就是js跟其他OOP語言最大的不同之處,這也是js最難理解的一塊。下面我來說說我個人的理解。

首先從建立物件說起,一般會有下面幾種方法:

1.建立一個Object例項,然後給它新增屬性和方法。

var person() = new Object();
person.name = 'mikej';
person.sayName = function(){
  alert(this.name);
}

2.也可以這樣寫:

var parson = {
  name : 'mikej',
  sayName : function(){
    alert(this.name);
  }
}

3.這兩種建立物件的方法很簡單,但都有缺陷,使用同一個模式建立物件的時候,會產生大量重複程式碼。於是就有了工廠模式:

function createPerson(name){
  var p = new Object();
  p.name=name;
  p.sayName = function(){
    alert(this.name);
  };
  return p;
}
var p1 = createPerson('mikej');
var p2 = createPerson('tom');

這樣就可以無限建立物件了。

4.還有一種方法,跟工廠模式異曲同工,叫做建構函式模式:

function Person(name){
  this.name=name
  this.sayName = function(){
   alert(this.name);
  }
  this.say = function(){
    alert('hi');
  }
}
var p1 = new Person('mikej');
var p2 = new Person('tom');

這裡有幾個值得關注的地方:沒有顯示的建立物件、函式名Person使用的是大寫字母P(這是必須的)、p1和p2中都有一個constructor(建構函式)屬性,指向Person。同時p1和p2既是Object的例項,也是Person的例項。

alert(p1.constructor == Person); //true
alert(p1 instanceof Object); //true
alert(p1 instanceof Person); //true

//5.11更新:以一個phper的角度看的話,之前很容易將建立物件的流程想成這樣,Person就是一個“類”,然後用new Person('mikej')例項化了這個類,並且傳入引數。但實際上並不是這樣的,建立的流程應該是這樣:首先,建立一個空物件,然後用apply方法,第一個引數是這個空物件,第二個引數是上下文的引數,這樣Person中的this就會指向這個物件,也就是p1。

var p1 = new Person('mikej');
//上面程式碼就相當於
var p1 = {};
Person.apply(p1, ['mikej']);

建構函式模式看上去很好,但是它有一個弊端就是浪費記憶體,接上例

alert(p1.say == p2.say) //false

.為了避免這個缺陷,可是使用原型模式來建立物件,js中的每個物件都有一個prototype屬性用來指向另外一個物件,這個物件的所有屬性和方法都會被建構函式的例項繼承,是共享的,這就意味著,我們可以把那些不變的屬性和方法,定義到prototype物件上。

function Person(name){
  this.name = name;
}
//Person的原型物件
Person.prototype = {
  say: function(){
    alert('hi');
  },
  sayName: function(){
    alert(this.name);
  }
};
var p1 = new Person("mikej");
var p2 = new Person("tom");
p1.sayName();
p2.sayName();
//下面就可以看出方法實現了共享
alert(P1.say == P2.say) //true
alert(P1.sayName == P2.sayName) //true

再來擴充套件一下上面的例子,使用原型來實現繼承。

function Person(name){
  this.name = name;
}

Person.prototype = {
  say: function(){
    alert('hi');
  },
  sayName: function(){
    alert(this.name);
  }
};

function Programmer(){
  this.say = function(){
    alert('im Programmer, my name is ' + this.name);
  }
}

Programmer.prototype = new Person('mikej');
//手動修正建構函式
Programmer.prototype.constructor = Programmer;
var p1 = new Programmer();

console.dir(Programmer.prototype.constructor);//Programmer
console.dir(p1.constructor);//Programmer
console.dir(p1);

Programmer的原型指向了Person的一個例項,那麼所有的Programmer的例項都能繼承Person和Person的原型了。

這裡會有一個問題。

預設原型物件裡有一個constructor屬性,指向它的建構函式。而每一個例項也有一個constructor屬性,會預設呼叫prototype物件的constructor屬性。

假設沒有Programmer.prototype = new Person('mikej');

Programmer.prototype.constructor是指向Programmer的。p1的構造也指向Programmer

alert(Programmer.prototype.constructor == Programmer) //true
alert(p1.constructor == Programmer) //true

但有了這句Programmer.prototype = new Person('mikej');之後,

Programmer.prototype.constructor就指向了Object,也就是Person.prototype指向的物件的構造。p1.constructor也指向了Object。但p1明明是建構函式Programmer生成的,這就造成了繼承的混亂,所以我們必須手動修正建構函式,也就是下面這程式碼。

Programmer.prototype.constructor = Programmer;

好了,現在我們再來看看原型鏈:

console.dir(p1);

這句程式碼的結果是

prototype

可以看出,p1是Programmer的例項,Programmer的原型是Person,Person的原型是Object,再網上就是JS的物件了。這就是原型鏈,也就是說,JavaScript的繼承是基於原型鏈來實現的。

相關文章