前言
本文是《JavaScript高級程序設計》中第六章的面向?qū)ο蟪绦蛟O計的幾個 構造函數(shù)模式的筆記
理解對象
// ...
創(chuàng)建對象
// ...
構造函數(shù)模式
先來看看典型的構造函數(shù):
function Person(name,age,job){
this.name=name;
this.age=age;
this.sayName=function(){
console.log(this.name);
};
}
var person1=new Person("小明",20);
由上邊可以直接看出構造函數(shù)模式的特點:
- 直接將屬性和方法賦給了 this 對象;
- 沒有 return 語句
- 沒有 顯式創(chuàng)建對象
優(yōu)點:
直觀,方便,符合OO編程直覺上的使用
存在的問題:
每個方法都會在實例上重新創(chuàng)建一遍,導致了每一個person1 / person2 等實例之間的sayName()方法其實是不相等的
person1.sayName==person2.sayName
//false
原型模式
無論什么時候,只要創(chuàng)建了一個新函數(shù),就會根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個protoype屬性,這個屬性指向函數(shù)的原型對象.
那么:
function Person(){}
Person.prototype.name="小明";
Person.prototype.age=20;
Person.prototype.sayName=function(){
console.log(this.name);
};
var person1=new Person();
但是這樣寫太麻煩了,想想每次要增加一個屬性,就得寫一長串的Person.prototype.xxx
所以可以簡略為:
function Person(){}
Person.prototype={
name:"小明",
age:20,
sayName:function(){
console.log(this.name);
}
};
但是要注意的是, 這種做法本質(zhì)上 是 完全重寫了默認的prototype 對象,因此 constructor屬性也就變成了指向Object構造函數(shù)的constructor屬性,不再指向Person函數(shù).
雖然 instanceof 操作符仍能返回正確結果,但constructor無法確定對象類型:
var friend=new Person();
friend instanceof Object //true
friend instanceof Person //true
friend.constructor == Person //false
friend.constructor == Object //true
//我的疑問:
//既然instanceof能確定對象類型,那為什么還要把constructor顯式轉(zhuǎn)為Person?
所以最好要加上 constructor:Person
function Person(){}
Person.prototype={
constructor:Person,
name:"小明",
age:20,
sayName:function(){
console.log(this.name);
}
};
注意的是,當以這種方式重設constructor屬性會導致它的 [[Enumerable]]特性被設置為true.
在默認情況下,原生的constructor屬性是不可枚舉的,因此在兼容ES5的情況下,使用Object.defineProperty()
Object.defineProperty(Person.prototype,"constructor",{
enumerable:false,
value:Person
});
//我的疑問:
//那假如我不把它設置為false又會怎樣?
但是原型模式存在著一個很大的問題, 就是其共享的本性!
原型中所有屬性都是被實例共享的,對于包含引用類型值(非基礎類型)的屬性來說,就很糟糕:
function Person(){}
Person.prototype={
constructor:Person,
name:"小明",
age:20,
friends:["sb","nb"]
};
var person1=new Person();
var person2=new Person();
person1.friends.push("Van");
console.log(person1.friends); //"sb,nb,Van"
console.log(person2.friends); //"sb,nb,Van"
console.log(person1.friends===person2.friends); //true
組合使用構造函數(shù)模式和原型模式
既然 構造函數(shù)模式的實例的方法和屬性沒有存在共享性,而原型模式的方法能保持方法的引用,那么結合起來就是 ,利用構造函數(shù)模式的方式來使得屬性沒有共享性,再利用原型模式使方法都能共用!
function Person(name,age){
this.name=name;
this.age=age;
this.friends=["sb","nb"];
}
Person.prototype={
constructor:Person,
sayName:function(){
console.log(this.name);
}
};
var person1=new Person("xm",20);
動態(tài)原型模式
但問題還是有的, 在其他OO開發(fā)人員看到這樣獨立的構造函數(shù)和原型的時候,第一感覺是懵逼的, 所以 動態(tài)原型模式 應運而生.
它把所有信息都封裝在構造函數(shù)中 , 然后在構造函數(shù)中初始化原型,這樣能夠同時保證之前組合方式的優(yōu)點.
function Person(name,age){
this.name=name;
this.age=age;
if(typeof this.sayName != "function"){
Person.prototype.sayName=function(){
console.log(this.name);
}
}
//這只會在sayName()方法不存在的情況下才會添加到原型
//一旦添加到原型之后,再次創(chuàng)建實例的時候,原型已經(jīng)存在了這個方法.
//只需要用if檢查一個方法之后全部寫到prototype中即可,不用每一個方法都寫一個if
}
寄生構造函數(shù)模式
跟簡單工廠模式只有 調(diào)用時new的差別:
簡單工廠:
function createPerson(name,age){
var o =new Object();
o.name=name;
o.age=age;
o.sayName=function(){
//...
}
return o;
}
var person1=createPerson("a",20);
寄生構造函數(shù)模式:
function createPerson(name,age){
var o =new Object();
o.name=name;
o.age=age;
o.sayName=function(){
//...
}
return o;
}
var person1=new createPerson("a",20);
這個模式可以在特殊情況下為對象創(chuàng)建構造函數(shù),例如:
我們想創(chuàng)建一個具有額外方法的特殊數(shù)組,因為不能簡單粗暴地直接修改Array構造函數(shù),所以可以使用一下模式:
function SpecialArray(){
var values= new Array();
values.push.apply(values,arguments);
values.toPipedString=function(){
return this.join("|");
};
return values;
}
var colors=new SpecialArray("red","blue","green");
colors.toPipedString();
// "red|blue|green"
穩(wěn)妥構造函數(shù)模式
- 不使用
this - 不使用
new操作符調(diào)用構造函數(shù)
function Person(name, age, job) {
var o = new Object();
// private members
var nameUC = name.toUpperCase();
// public members
o.sayName = function() {
alert(name);
};
o.sayNameUC = function() {
alert(nameUC);
};
return o;
}
var person = Person("Nicholas", 32, "software Engineer");
person.sayName(); // "Nicholas"
person.sayNameUC(); // "NICHOLAS"
alert(person.name); // undefined
alert(person.nameUC); // undefined
除了調(diào)用本身提供的sayName()方法外,沒有別的方式可以訪問到其數(shù)據(jù)成員.故而這種方式提供了良好的安全性,適合在某些安全執(zhí)行環(huán)境中.
繼承
如何繼承
原型鏈
直接上代碼好理解
//先用 組合構造函數(shù)模式和原型模式 創(chuàng)建兩個類型:
function Animal(){
this.name="animal";
}
Animal.prototype.getName=function(){
return this.name;
}
function Dog(){
this.age=2;
}
//用Animal的實例重寫Dog的原型
//Dog 將會獲得Animal全部的屬性和方法
//這就實現(xiàn)了繼承
Dog.prototype=new Animal();
Dog.prototype.getAge=function(){
return this.age;
}
var adog=new Dog();
adog.getAge(); //2
但是你有沒有注意到 , 這樣實現(xiàn)繼承的時候,Dog的原型是一個Animal 的實例.
當Animal中存在一個引用值類型的屬性時, 會出現(xiàn)什么問題?
function Animal(){
this.colors=["red","blue","green"];
}
function Dog(){}
Dog.prototype=new Animal();
Dog.prototype.constructor=Dog;
var adog=new Dog();
adog.colors.push("black");
var bdog=new Dog();
console.log(bdog);
// "red,blue,green,black"
由上面代碼可以看出,這導致了每個實例都對父類的引用值類型產(chǎn)生了共享性.
所以一般很少會單獨使用原型鏈來實現(xiàn)繼承.
那么 , 接下來祭出 構造函數(shù)
借用構造函數(shù)
function Animal(){
this.colors=["red","blue"];
}
function Dog(){
Animal.call(this);
}
var adog=new Dog();
adog.colors.push("black");
var bdog=new Dog();
console.log(bdog);
//"red,blue"
除了能解決 引用值類型的共享性問題之外, 相對于 單獨使用原型鏈還有一個很大的優(yōu)勢 , 也就是能在子類型構造函數(shù)中向 父類構造函數(shù)傳遞參數(shù)
function Animal(name){
this.name=name;
}
function Dog(){
Animal.call(this,"dog");
//dog 屬性
this.age=2;
}
var adog=new Dog();
adog.name //"dog"
adog.age //2
按照之前的 通過構造函數(shù)/原型鏈 來創(chuàng)建對象 的經(jīng)驗就知道 , 單獨使用某一種模式都會顧此失彼,最好就是雙劍合璧,那就是 組合繼承. 這也是最常用的繼承模式.
組合繼承
function Animal(name){
this.name=name;
this.colors=["red","blue"];
}
Animal.prototype.sayName=function(){
console.log(this.name);
}
function Dog(name,age){
Animal.call(this,name);
this.age=age;
}
Dog.prototype=new Animal();
Dog.prototype.constructor=Dog;
Dog.prototype.sayAge=function(){
console.log(this.age);
}
var adog=new Dog("a",2);
adog.colors.push("black");
adog.colors; //"red,blue,black"
adog.sayName(); // "a"
adog.sayAge(); // 2
var bdog=new Dog("b",3);
bdog.colors; //"red,blue"
bdog.sayName(); //"b"
bdog.sayAge(); //3
這樣adog和bdog 就能分別擁有自己的屬性,也能同時使用相同的方法.
原型式繼承
借助原型基于已有對象來創(chuàng)建新對象,同時不必因此創(chuàng)建自定義類型.
function object(o){
function F(){}
F.prototype=o;
return new F();
}
很顯然這跟上邊 使用原型鏈實現(xiàn)繼承的方式非常相似 , 它們之間 在調(diào)用的時候就差了一個 new,這是因為函數(shù)object()把傳進去的實例直接賦給了 臨時函數(shù)的 prototype.
ES5 中 新增了一個 Object.create() 方法規(guī)范花了原型式繼承
注意,這種原型式繼承,當包含著引用類型值時會時鐘共享相應的值,跟原型鏈實現(xiàn)繼承一樣
寄生式繼承
function createAnother(original){
var clone =object(original);
clone.sayHi=function(){
console.log("hi");
}
return clone;
}
也就是通過淺拷貝原有對象,然后在內(nèi)部以某種方式增強對象,最后返回對象.
這跟單用構造函數(shù)模式類似, 但由于不能做到函數(shù)復用而降低效率.
寄生組合式繼承
現(xiàn)在先回顧一下組合繼承:
function Animal(name){
this.name=name;
this.colors=["red","blue"];
}
Animal.prototype.sayName=function(){
console.log(this.name);
}
function Dog(name,age){
Animal.call(this,name); //第二次: 創(chuàng)建Dog實例時候
this.age=age;
}
Dog.prototype=new Animal(); //第一次 : 創(chuàng)建子類型原型的時候
Dog.prototype.constructor=Dog;
Dog.prototype.sayAge=function(){
console.log(this.age);
}
var adog=new Dog("a",2);
雖說組合繼承這是最常用的繼承模式,但它有一些不足,就是無論什么情況下,都會調(diào)用兩次父類的構造函數(shù).
那么如何解決呢
接下來就是寄生組合式繼承登場了
function Animal(name){
this.name=name;
this.colors=["red","blue"];
}
Animal.prototype.sayName=function(){
console.log(this.name);
}
function Dog(name,age){
Animal.call(this,name);
this.age=age;
}
Dog.prototype=object(Animal.prototype);
Dog.prototype.constructor=Dog;
Dog.prototype.sayAge=function(){
console.log(this.age);
}
var adog=new Dog("a",2);
抽象出邏輯就是:
function inheritPrototype(subType,superType){
var prototype=object(superType.prototype);
prototype.constructor=subType;
subType.prototype=prototype;
}
等價于
function Animal(name){
this.name=name;
this.colors=["red","blue"];
}
Animal.prototype.sayName=function(){
console.log(this.name);
}
function Dog(name,age){
Animal.call(this,name);
this.age=age;
}
// Dog.prototype=object(Animal.prototype);
// Dog.prototype.constructor=Dog;
inheritPrototype(Dog,Animal);
Dog.prototype.sayAge=function(){
console.log(this.age);
}
var adog=new Dog("a",2);
確定原型和實例的關系?
以上面的 adog 為例子 :
有兩個方法:
1. 使用 instanceof
adog instanceof Object //true
adog instanceof Animal //true
adog instanceof Dog //true
2. 使用 isPrototypeOf()
Object.prototype.isPrototypeOf(adog) //true
Animal.prototype.isPrototypeOf(adog) //true
Dog.prototype.isPrototypeOf(adog) //true