一、原型鏈
之前接觸過很多面向對象的語言比如Java、Object-C等等,語言設計思想都差不多,但是JavaScript是我見過最獨特的,尤其是通過原型鏈來實現繼承
js原型理解附圖如下:
二、創建對象的幾種方式及對比
1.對象字面量與創建Object實例方式
var person = {
name:'Zhangsan',
age:20,
gender: 'male',
sayName: function(){
console.log(this.name);
}
}
var person = new Object();
person.name = 'Zhangsan';
person.age = 20;
person.gender = 'male';
person.sayName = function(){
console.log(this.name);
}
缺點:使用同一個接口創建很多對象,會產生大量的重複代碼
2.工廠模式
因爲ECMAScript沒有類的概念,所以用函數來封裝創建對象的細節
function createPerson(name,age,gender){
var o = new Object();
o.name = name;
o.age = age;
o.gender = gender;
o.sayName = function(){
console.log(this.name);
}
return o;
}
var person1 = createPerson('Zhangsan',20,'male');
var person2 = createPerson('Lisi',24,'male');
console.log(person1 instanceof Object); //true
缺點:工廠模式解決了創建多個相似對象的重複代碼問題,但沒有解決對象類型識別的問題
3.構造函數模式
function Person(name, age, gender){
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = function(){
console.log(this.name);
}
}
var person1 = new Person('Zhangsan',20,'male');
var person2 = new Person('Lisi',24,'male');
console.log(person1 instanceof Person); //true
console.log(person1 instanceof Object); //true
console.log(person1.constructor == Person); //true
console.log(person1.sayName == person2.sayName); //false
以這種方式調用構造函數會經歷以下四步:
1. 創建一個新對象
2. 將構造函數的作用域賦給新對象
3. 執行構造函數中的代碼
4. 返回新對象
任何函數,只要通過new操作符來調用,那它就可以作爲構造函數;如果不通過new操作符來調用,那它就是普通函數
var person = new Person('Zhangsan',20,'male');
person.sayName(); //'Zhangsan'
Person('Lisi',24,'male');
window.sayName(); //'Lisi'
var o = new Object();
Person.call(o,'Wangwu',22,'female');
o.sayName(); //'Wangwu'
缺點:每個方法都要在每個實例上重新創建一遍,上述例子中的sayName方法就會在每個實例中重新創建一遍
function Person(name, age, gender){
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = sayName;
}
function sayName(){
console.log(this.name);
}
var person1 = new Person('Zhangsan',20,'male');
var person2 = new Person('Lisi',24,'male');
上述代碼將sayName()函數的定義轉移到構造函數外部,這樣構造函數中的的sayName是一個指向函數的指針,因此person1、person2就共享了在全局作用域定義的同一個sayName()函數。這麼做雖然解決了問題但卻帶來了新問題:如果對象需要定義很多方法那麼就需要定義很多全局函數,那麼自定義的引用類型就絲毫沒有封裝性可言了
4.原型模式
4.1原型對象
每個函數都有一個prototype屬性,這個屬性是一個指針指向一個對象,而這個對象包含可以由特定類型的所有實例共享的屬性和方法。使用原型對象的好處就是讓所有對象實例共享它所包含的屬性和方法
function Person(){
}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.gender = 'male';
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //Nicholas
var person2 = new Person();
person2.sayName(); //Nicholas
console.log(Person.prototype.isPrototypeOf(person1));//true
console.log(Person.prototype.isPrototypeOf(person2));//true
console.log(Object.getPrototypeOf(person1) == Person.prototype);//true
console.log(Object.getPrototypeOf(person1).name);//Nicholas
當代碼讀取某個對象的某個屬性時,先從對象實例本身開始搜索,如果找到給定名稱的屬性則返回該屬性的值;如果沒找到則搜索其指針指向的原型。當爲對象實例添加一個屬性時,則這個屬性就會屏蔽原型對象中保存的同名屬性
person1.name = 'Zhangsan';
console.log(person1.name); //Zhangsan
console.log(person1.hasOwnProperty('name')); //true
console.log(person2.hasOwnProperty('name')); //false
delete person1.name;
console.log(person1.name); //Nicholas
4.2原型對象賦值
function Person(){
}
Person.prototype = {
name:'Nicholas',
age:29,
gender:'male',
sayName:function(){
console.log(this.name);
}
}
var person = new Person();
console.log(person instanceof Object); //true
console.log(person instanceof Person); //true
console.log(person.constructor == Person); //false
console.log(person.constructor == Object); //true
Person.prototype.constructor = Person;
console.log(person.constructor == Person); //true
使用原型對象賦值操作是會覆蓋原型對象中的constructor屬性,就會切斷原型與構造函數之間的關聯
function Person(){
}
var person = new Person();
Person.prototype = {
name:'Nicholas',
age:29,
gender:'male',
sayName:function(){
console.log(this.name);
}
}
person.sayName(); //error
缺點:由於原型中的所有屬性和方法都是共享的,所以對於引用類型屬性問題就比較突出
function Person(){
}
Person.prototype = {
constructor:Person,
name:'Nicholas',
age:29,
gender:'male',
love:['swimming','running'],
sayName:function(){
console.log(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.love.push('playing games');
console.log(person1.love); //["swimming", "running", "playing games"]
console.log(person2.love); //["swimming", "running", "playing games"]
console.log(person1.love == person2.love); //true
5.組合使用構造函數和原型模式
function Person(name,age,gender){
this.name = name;
this.age = age;
this.gender = gender;
this.love = ['swimming','running'];
}
Person.prototype = {
constructor:Person,
sayName:function(){
console.log(this.name);
}
}
var person1 = new Person('Zhangsan',20,'male');
var person2 = new Person('Lisi',24,'male');
person1.love.push('playing games');
console.log(person1.love); //["swimming", "running", "playing games"]
console.log(person2.love); //["swimming", "running"]
console.log(person1.love == person2.love); //false
這種模式是使用最廣泛、認同度最高的一種創建自定義類型的方法
6.寄生構造函數模式
function SpecialArray(name,age,gender){
var array = new Array();
array.push.apply(array,arguments);
array.toPipedString = function(){
return this.join('|');
}
return array;
}
這種模式可以用來爲原生引用類型做擴展,寄生構造函數模式返回的對象與構造函數或者與構造函數原型之間沒有關係,因此不能依賴instanceof操作符來確定對象類型
三、繼承
1.原型鏈
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function (){
return this.property;
}
function SubType(){
this.subproperty = false;
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
return this.subproperty;
}
var instance = new SubType();
console.log(instance.getSuperValue()); //true
原型鏈雖然很強大,可以用它來實現繼承,但它也存在一些問題,其中,最主要的問題來自包含引用類型值的原型;第二個問題是創建子類型的實例時不能向超類的構造函數中傳遞參數。
function SuperType(){
this.colors = ['red','yellow','blue'];
}
function SubType(){
}
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push('green');
console.log(instance1.colors); //["red", "yellow", "blue", "green"]
var instance2 = new SubType();
console.log(instance2.colors); //["red", "yellow", "blue", "green"]
2.借用構造函數
借用構造函數用於解決原型鏈中包含引用類型值所帶來的問題
function SuperType(){
this.colors = ['red','yellow','blue'];
}
function SubType(){
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push('green');
console.log(instance1.colors); //["red", "yellow", "blue", "green"]
var instance2 = new SubType();
console.log(instance2.colors); //["red", "yellow", "blue"]
問題:方法都在構造函數中定義,因此函數複用無從談起,而且在超類原型中定義的方法對子類而言也是不可見的,結果所有類型都只能使用構造函數模式
3.組合繼承
function SuperType(name){
this.name = name;
this.colors = ["red", "yellow", "blue"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name, age){
SuperType.call(this,name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}
var instance1 = new SubType('Zhangsan',20);
instance1.colors.push('green');
console.log(instance1.colors); //["red", "yellow", "blue", "green"]
instance1.sayName(); //Zhangsan
instance1.sayAge(); //20
var instance2 = new SubType('Lisi', 24);
console.log(instance2.colors); //["red", "yellow", "blue"]
instance2.sayName(); //Lisi
instance2.sayAge(); //24
組合繼承避免了原型鏈和借用構造函數的缺陷,融合了他們的優點,成爲JavaScript中最常用的繼承模式
4.原型式繼承
var person = {
name : 'Zhangsan',
colors : ["red", "yellow", "blue"]
}
var anotherPerson = Object.create(person);
anotherPerson.name = "Lisi";
anotherPerson.colors.push("green");
var otherPerson = Object.create(person);
otherPerson.name = "Wangwu";
otherPerson.colors.push("black");
console.log(person.colors); //["red", "yellow", "blue", "green", "black"]
console.log(person.name); //Zhangsan
這種繼承方式在想讓一個對象與另一個對象保持類似的情況下是完全可以勝任的
5.寄生式繼承
var person = {
name : 'Zhangsan',
colors : ["red", "yellow", "blue"]
}
var anotherPerson = Object.create(person);
anotherPerson.sayHi = function(){
console.log("hi");
}
anotherPerson.sayHi();
使用寄生式繼承不能做到函數複用而降低效率
6.寄生組合式繼承
組合繼承最大的問題就在於無論什麼情況下都會調用兩次超類型構造函數:一次是在創建子類型原型的時候,另一次是在子類型構造函數內部。子類型最終回報寒潮類型對象的全部實例屬性,但是我們不得不在調用子類型構造函數時重寫這些屬性(處理引用類型共用問題)
組合式繼承代碼如下:
function SuperType(name){
this.name = name;
this.colors = ["red", "yellow", "blue"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name, age){
SuperType.call(this,name); //第二次調用
this.age = age;
}
SubType.prototype = new SuperType(); //第一次調用
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}
寄生組合式繼承代碼如下:
function SuperType(name){
this.name = name;
this.colors = ["red", "yellow", "blue"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name, age){
SuperType.call(this,name);
this.age = age;
}
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}