紅皮書(Javascript高級程序設計)的第6章是關于面向對象中的構造函數的內容,看了第二遍,對一些重點做下筆記。
構造器模式
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
}
}
var person1 = new Person("johnson", "22", "frontend engineer")
按照慣例,構造函數的函數名總是以大寫字母開頭,非構造函數的普通函數則是以小寫字母開頭,我想這也是為了做個明顯的區分。
使用new 函數名()的形式的操作就是調用構造函數,而那些沒有new 操作符調用的就是普通調用
函數在ECMAScript中就是對象, 每當一個函數被定義,其實就是一個對象被初始化。如下代碼:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)"); //邏輯上等于 function() { alert(this.name) }
}
原型模式
每個函數被創建時都會有個原型屬性,這個原型其實也就是個包含屬性和方法的對象,同時也能被特殊引用類型的實例給訪問。這個對象(prototype)也就是原型在構造器(constructor)被調用時就被創建了。
function Person() {}
Person.prototype.name = "Johnson";
Person.prototype.age = "22";
Person.prototype.job = "frontend engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //"Johnson"
var person2 = new Person();
person2.sayName(); //"Johnson"
console.log(person1.sayName === person2.sayName); //true
所有的原型自動會得到一個叫構造器(constructor)的屬性并且這個屬性指向這個原型在的這個函數。Person.prototype.constructor指向Person。
當定義一個自定義的構造器,原型就默認獲得了constructor屬性。其他的方法比如函數的toString方法就從Object繼承(又是原型鏈)。每當用new操作符定義個新的實例(比如new Person()), 這個實例有一個內部指針指向這個構造器的原型。在ECMA-262第五版,也叫作[[prototype]]。如下圖:
當一個屬性在對象上能夠被讀取,有一種搜索規則來找到這個屬性。這個搜索最開始從對象自身開始搜索,如果找到這個屬性名這個屬性值就被返回了并且不會繼續往原型上找,當在對象自身不存在這個屬性,則會到原型對象上去找這個屬性,找到就返回。
如果一個屬性被加到對象實例上(帶new 操作符+函數名的實例)它會屏蔽原型上的同名屬性,也意味著它鎖住了訪問原型同名屬性的通道。即使用set方法把這個屬性設置成Null也只是設置在對象本身的屬性,并沒有恢復訪問原型同名屬性的通道。除非用delete方法刪除這個實例上的屬性才能夠重新訪問到原型上這個同名屬性的值。
function Person() {}
Person.prototype.name = "Johnson";
Person.prototype.age = "22";
Person.prototype.job = "frontend engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Jonas";
console.log(person1.name); //Jonas
console.log(person2.name); //Johnson
delete person1.name;
console.log(person1.name); //Johnson
遍歷對象的屬性
- for in
- Object.keys()
- Object.getOwnPropertyNames()
三者區別:
- for in能夠輸出自身以及原型鏈上可枚舉的屬性
var parent = Object.create(Object.prototype, {
a: {
value: 1,
writable: true,
enumerable: true,
configurable: true
}
});
var child = Object.create(parent, {
b: {
value: 2,
writable: true,
enumerable: true,
configurable: true
},
c: {
value: 3,
writable: true,
enumerable: false,
configurable: true
}
});
for (var key in child) {
console.log(key); // b a
}
- Object.keys()主要用來獲取對象自身可枚舉的屬性鍵,返回的是鍵名的數組。
function Person() {}
Person.prototype.name = 'Johnson';
Person.prototype.age = 29;
Person.prototype.job = "frontend engineer";
Person.prototype.sayName = function() {
console.log(this.name);
}
var keys = Object.keys(Person.prototype); //此處遍歷Person的原型對象
console.log(keys); //"name, age, job,sayName" 輸出原型對象的key
var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
console.log(p1keys); //"name, age" 此處只輸出自身可枚舉屬性,不輸出原型的屬性
- Object.getOwnPropertyNames()用來獲取對象自身的全部屬性,無論枚舉不可枚舉。
var keys = Object.getOwnPropertyNames(person.prototype);
console.log(keys); //"constructor(不可枚舉), name, age, job, sayName"
交替的原型句法
function Person() {}
Person.prototype = {
name: "Johnson",
age: 29,
job: "frontend engineer",
sayName: function() {
console.log(this.name);
}
}
var friend = new Person();
console.log(friend instanceof Object); //true
console.log(friend instanceof Person); //true
console.log(friend.constructor == Person); //false
console.log(friend.constructor == Object); //true
以上的復寫Person原型對象等同于創建了一個新的對象。但是構造器(constructor)屬性不再指向Person。按照前面說的,當一個函數被創建時,它的原型對象會被創建并且構造器屬性會被自動分配到原型對象中。但以上的寫法完全復寫了默認的原型對象,意味著構造器屬性等同于在一個新的對象中,而不是函數的原型對象中。(翻譯過來有點拗口,可以這么理解,原本這個構造函數是指向函數本身的,復寫之后指向的是全局Object)。
function Person() {}
Person.prototype = {
constructor: Person,
name: "Johnson",
age: 29,
job: "frontend engineer",
sayName: function() {
console.log(this.name);
}
}
但我們也能夠讓這個原型對象中的constructor重新指回Person。但是記住以這種方式回復構造器(constructor)屬性的指向會使得constructor屬性變為可枚舉(Enumerable)。原生的constructor屬性默認是不可枚舉的,所以你最好重新把它定義成不可枚舉,這里要用到Object.defineProperty()方法:
function Person() {}
Person.prototype = {
constructor: Person,
name: "Johnson",
age: 29,
job: "frontend engineer",
sayName: function() {
console.log(this.name);
}
}
var p1 = new Person();
for(var i in p1) {
console.log(i); // constructor, name, age, job, sayName
}
//ECMAScript 5 only - restore the constructor
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
});
var p2 = new Person();
for(var j in p2) {
console.log(j); //name, age, job, sayName
}
原型的動態特質
function Person() {}
var friend = new Person();
Person.prototype.sayHi = function() {
console.log('hi');
}
friend.sayHi(); //hi
基于之前在原型上查找屬性的慣例,在任何地方對原型做出修改都能直接反映到實例上,即使實例化操作在原型修改之前。也是因為實例和原型之間的連接只是個簡單的指針而不是拷貝,當sayHi()被調用時,實例上是第一次搜索,沒搜到,就繼續到原型上去搜索直到搜索到方法并返回。
[[Prototype]]指針在構造器被調用時被分配到,所以修改原型變成一個不同的對象會切斷構造器和原始的原型對象之間的聯系。要記住一點:** 實例有一個只指向原型的指針而不是構造器 **,思考如下:
function Person() {}
var friend = new Person();
Person.prototype = {
constructor: Person,
name: 'Johnson',
age: 29,
job: 'Frontend engineer',
sayName: function() {
console.log(this.name);
}
};
friend.sayName(); //error
當friend.sayName()被調用時,會發生錯誤,因為friend指向的原型沒有這個屬性。如下圖:
![U%6}O8T5]W5SC{WQ9YJ_C53.png](http://upload-images.jianshu.io/upload_images/1572265-592c3bfcbd49fa44.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
個人理解,實例化在重寫原型之前,切斷了實例和之前那個原型的連接,但看圖來說,我認為是實例還是指向之前的原型,只不過函數本身指向了新的原型對象。**注意:重寫原型在實例化之前,friend.sayName()方法還是執行的 **
寄生構造器模式
function Person(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
};
return o;
}
var friend = new Person('johnson', 29, 'Frontend engineer');
friend.sayName(); //johnson
console.log(friend.name); //johnson
console.log(friend instanceof Person); //false
寄生模式下,friend能夠訪問到函數內部對象的屬性。只是構造器和實例之間沒有任何聯系,所以instanceof不生效。
耐用模式
function Person(name, age, job) {
var o = new Object();
o.sayName = function() {
console.log(name);
}
return o;
}
var friend = Person('johnson', 29, 'Frontend engineer');
friend.sayName(); //johnson
原型鏈
一個簡單的例子:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
//inherit from SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
return this.subproperty;
};
var instance = new SubType();
console.log(instance.getSuperValue());
值得注意的是,getSuperValue()方法依舊在SuperType.prototype對象上而property在SubType.prototype對象上。SubType的原型現在是SuperType的實例,所以屬性property存在SubType的原型上。同時也注意instance.constructor指向SuperType,因為在SubType.prototype上的構造器屬性被復寫了。
調用instance.getSuperValue()經過3個步驟的搜索:
- 實例instance
- SubType.prototype
- SuperType.prototype
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
//inherit from SuperType
SubType.prototype = new SuperType();
var SuperTp = new SuperType();
console.log(SuperTp.getSuperValue()); //true
SubType.prototype.getSubValue = function() {
return this.subproperty;
};
//override existing method
SubType.prototype.getSuperValue = function() {
return false;
}
var instance = new SubType();
console.log(instance.getSuperValue()); //false
getSuperValue()方法被復寫,原型上的的getSuperValue()方法會被遮蔽。當getSuperValue()在SubType實例上被調用,會調用復寫的方法,但SuperType的實例還是會調用原型鏈上的方法。
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
//inherit from SuperType
SubType.prototype = new SuperType();
SubType.prototype = {
getSubValue: function() {
return this.subproperty;
},
someOtherMethod: function() {
return false;
}
}
var instance = new SubType();
console.log(instance.getSuperValue()); //error
SubType的原型被復寫成一個新的對象實例,所以原型鏈斷開,SubType和SuperType現在沒任何聯系。
構造器偷取
function SuperType() {
this.colors = ['red', 'yellow'];
}
SuperType.prototype.sayColor = function() {
console.log('1111');
};
function SubType() {
SuperType.call(this);
}
var instance = new SubType();
instance.colors.push('black');
console.log(instance.colors);// ['red', 'yellow', 'black']
instance.sayColor() //error
var instance2 = new SubType();
console.log(instance2.colors); //['red', 'yellow']
** 記住函數只是在一個特殊上下文中執行代碼的簡單對象。**
apply和call方法能夠被用作執行一個構造器在一個新創建的對象上。當構造器上的屬性包含引用時不會造成共享,各個實例有自己的屬性引用。好比以上的屬性的數組。此模式還支持傳遞參數。
缺點:缺點在以上代碼也很明顯,就是被繼承的函數上的原型方法不能夠被繼承的調用。
混合繼承
混合繼承結合了原型鏈和構造器偷取的優點。
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
//inherit properties
SuperType.call(this, name);
this.age = age;
}
//inherit methods
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
}
var instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
console.log(instance1.colors); //red, blue, green, black
instance1.sayName(); //Nicholas
instance1.sayAge(); //29
var instance2 = new SubType('Greg', 27);
console.log(instance2.colors); //red, blue, green
instance2.sayName(); //Greg
instance2.sayAge(); //27
這就解決了剛才構造器偷取不能訪問被繼承函數的原型方法的問題。并且最后的實例不會互相影響。
原型繼承
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');
var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');
console.log(person.friends); //[ 'Shelby', 'Court', 'Van', 'Rob', 'Barbie' ]
新對象將person對象作為自己的原型。person.friends不僅被person共享而且也被anotherPerson和yetAnotherPerson共享。這個代碼有兩個person的克隆。
var person = {
name: 'Nicholas',
friends: ['Shelly', 'Court', 'Van']
};
var anotherPerson = Object.create(person, {
name: {
value: 'Greg'
}
});
anotherPerson.friends.push('Rob');
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');
console.log(anotherPerson.name); //Greg
console.log(person.friends); //Shelly, Court, Van, Rob, Barbie
Object.create()方法 第一個參數作為原型對象,第二個參數則是附加的屬性,最后返回一個新對象。記住按照這種模式屬性中只要包含引用值,將會共享這個引用值,類似于原型方式。使用這種方法你不用去創建各種構造器。
寄生模式
function createAnother(original) {
var clone = object(original);
clone.sayHi = function() {
console.log('hi);
};
return clone;
}
var person = {
name: 'Nicholas',
friends: ['Shelly', 'Court', 'Van']
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //hi
這種模式雖然能在無構造器和自定義類型下實現但也不是很高效。
最后我們來說一下:
寄生混合模式
此方法是對之前** 混合繼承 **的一種改善,因為混合繼承會調用2次superType的構造器。一次是創建subType.prototype,另一次是在subtype的構造器中。
當SubType構造器被調用,SuperType的構造器也被調用了。你會發現,有兩個name和colors的集合屬性,一個在instance實例上一個在SubType的原型上。
解決調用兩次構造器的基本思想是代替調用supertype的構造器來分配給subType的原型。你只需要一個supetype的原型拷貝。
function inheritProtoype(subType, superType) {
var prototype = Object(superType.prototype); //create object
prototype.constructor = subType; //augment obect
subType.prototype = prototype; //assign object
}
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
}
var instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
console.log(instance1.colors); //red, blue, green, black
instance1.sayName(); //Nicholas
instance1.sayAge(); //29
var instance2 = new SubType('Greg', 27);
console.log(instance2.colors); //red, blue, green
instance2.sayName(); //Greg
instance2.sayAge(); //27
寄生混合模式還是很高效的。