面向?qū)ο箨P(guān)鍵知識(shí)點(diǎn)匯總
先來了解一些基礎(chǔ)概念,才能更好的理解知識(shí)。
對(duì)象的定義
ECMA-262把對(duì)象定義為:“無序?qū)傩缘募?,其屬性可以包含基本值、?duì)象或者函數(shù)。
數(shù)據(jù)屬性
4個(gè)特性
- [[Configurable]]:默認(rèn)true 表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性。
- [[Enumerable]]: 默認(rèn)true 表示能否通過for-in 循環(huán)返回屬性
- [[Writable]]: 默認(rèn)true 表示能否修改屬性的值。
- [[Value]]: 默認(rèn)值為undefined, 屬性的數(shù)據(jù)值,讀和寫入都在這個(gè)位置。
以上屬性一般通過Object.defineProperty()方法來修改,接受三個(gè)參數(shù):屬性所在的對(duì)象、屬性的名字和一個(gè)描述符對(duì)象。其中描述符就是上面四個(gè)特性。
訪問器屬性
4個(gè)特性
- [[Configurable]]:默認(rèn)true 表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性。
- [[Enumerable]]: 默認(rèn)true 表示能否通過for-in 循環(huán)返回屬性
- 訪問器屬性不能直接定義,必須使用 Object.defineProperty()來定義。
定義多個(gè)屬性 以及讀取屬性的特性
Object.defineProperties()方法
在調(diào)用 Object.defineProperty()方法時(shí),如果不指定,configurable、enumerable 和
writable 特性的默認(rèn)值都是 false。
Object.getOwnPropertyDescriptor()方法
接受兩個(gè)參數(shù):屬性所在的對(duì)象和要讀取其描述符的屬性名稱。
var book = {};
Object.defineProperties(book, {
_year: {
value: 2004
},
edition: {
value: 1
},
year: {
get: function(){
return this._year;
},
set: function(newValue){
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
alert(descriptor.value); //2004
alert(descriptor.configurable); //false
alert(typeof descriptor.get); //"undefined"
var descriptor = Object.getOwnPropertyDescriptor(book, "year");
alert(descriptor.value); //undefined
alert(descriptor.enumerable); //false
alert(typeof descriptor.get); //"function"
工廠模式
用函數(shù)來封裝以特定接口創(chuàng)建對(duì)象的細(xì)節(jié)
function createPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function () {
alert(this.name);
};
return o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
作用:解決了創(chuàng)建多個(gè)相似對(duì)象的問題
缺陷:沒有解決對(duì)象識(shí)別的問題(即怎么知道一個(gè)對(duì)象的類型)
構(gòu)造函數(shù)模式
new 一個(gè)構(gòu)造函數(shù)通常會(huì)經(jīng)歷一下四個(gè)步驟
- 創(chuàng)建一個(gè)新對(duì)象
- 將構(gòu)造函數(shù)的作用域賦給新對(duì)象(因此this就指向了這個(gè)新對(duì)象)
- 執(zhí)行構(gòu)造函數(shù)中的代碼(為這個(gè)新對(duì)象添加屬性)
- 返回新對(duì)象
由于構(gòu)造函數(shù)內(nèi)部定義的屬性和方法,通過實(shí)例化后都有自己的作用域鏈和辨識(shí)符解析。為了讓屬性和函數(shù)能夠更好的公用,這時(shí)候原型模式就要登場(chǎng)了。
isPrototypeOf()的作用
當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例后,該實(shí)例的內(nèi)部包含一個(gè)指針(內(nèi)部屬性),指向構(gòu)造函數(shù)的原型對(duì)象。ECMA-262中管這個(gè)指針叫[[Prototype]]。在腳本中沒有標(biāo)準(zhǔn)的方式訪問[[Prototype]],但瀏覽器(Firefox,Safari,chrome)在每個(gè)對(duì)象上都支持一個(gè)屬性/proto/;我們要知道的是,這個(gè)屬性對(duì)腳本則是完全不可見的。可以通過isPrototypeOf()方法來確定對(duì)象之間是否存在這種關(guān)系。從本質(zhì)上講,如果[[Prototype]]指向調(diào)用isPrototypeOf()方法的對(duì)象(Person.prototype),那么這個(gè)方法就返回true
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
Person.prototype.isPrototypeOf(person1)//true
ECMAScript 5 增加了一個(gè)新方法 Object.getPrototypeOf(),在所有支持的實(shí)現(xiàn)中,這個(gè)方法返回[[Prototype]]的值。例如
Object.getPrototypeOF(person1) === Person.prototype // true
hasOwnProperty()的使用場(chǎng)景
當(dāng)我們從一個(gè)構(gòu)造函數(shù)實(shí)例一個(gè)對(duì)象后,如果在這個(gè)實(shí)例上創(chuàng)建一個(gè)屬性或者方法,正好與這個(gè)實(shí)例的原型上的屬性或者方法同名,那么訪問時(shí)就會(huì)屏蔽原型上的屬性或者方法。當(dāng)然這樣做不會(huì)修改原型上的屬性或者方法。如果要恢復(fù)指向原型,那么使用delete操作符則可以完全刪除實(shí)例屬性。 使用 hasOwnProperty()方法可以檢測(cè)一個(gè)屬性是存在于實(shí)例中,還是存在于原型中。不要忘記這個(gè)方法是從Object繼承來的,所以只在給定屬性存在于對(duì)象實(shí)例中時(shí),才會(huì)返回true.
in 操作符的作用
in 操作符只要通過對(duì)象能夠訪問到屬性就會(huì)返回true. ,hasOwnProperty()只在屬性存在于實(shí)例中時(shí)才返回 true,因此只要 in 操作符返回 true 而hasOwnProperty()返回 false,就可以確定屬性是原型中的屬性。
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person = new Person();
alert(hasPrototypeProperty(person, "name")); //true
person.name = "Greg";
alert(hasPrototypeProperty(person, "name")); //false
for-in的特殊
在使用 for-in 循環(huán)時(shí),返回的是所有能夠通過對(duì)象訪問的、可枚舉的(enumerated)屬性,其中既包括存在于實(shí)例中的屬性,也包括存在于原型中的屬性。屏蔽了原型中不可枚舉屬性(即將[[Enumerable]]標(biāo)記為 false 的屬性)的實(shí)例屬性也會(huì)在 for-in 循環(huán)中返回,因?yàn)楦鶕?jù)規(guī)定,所有開發(fā)人員定義的屬性都是可枚舉的——只有在 IE8 及更早版本中例外。
Object.keys()
獲取對(duì)象上所有可枚舉的實(shí)例屬性,返回一個(gè)字符串?dāng)?shù)組。
Object.getOwnPropertyNames()
如果是想得到所有的實(shí)例屬性,包括不可枚舉的,就能使用Object.getOwnPropertyNames() 能夠獲取到不可枚舉的constructor屬性。
constructor屬性什么情況下會(huì)指向Object
function Person(){
}
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
為了減少重復(fù)代碼,我們經(jīng)常用一個(gè)包含所有屬性和方法的對(duì)象字面量來重寫整個(gè)原型對(duì)象。代碼如上所示。這里會(huì)產(chǎn)生一個(gè)情況,就是constructor屬性不再指向Person。為什么會(huì)這樣呢?我們知道,每創(chuàng)建一個(gè)函數(shù),就會(huì)同時(shí)創(chuàng)建它的prototype對(duì)象,這個(gè)對(duì)象也會(huì)自動(dòng)獲得constructor屬性。而我們上面這樣的寫法,本質(zhì)上完全重寫了默認(rèn)的prototype對(duì)象,因此constructor屬性也就變成了新對(duì)象的constructor屬性(指向Object構(gòu)造函數(shù)),不再指向Person函數(shù)。所以為什么我們用instanceof并不能準(zhǔn)確的判別實(shí)例的構(gòu)造函數(shù)。如下代碼
var p = new Person()
console.log(p instanceof Object)//true
console.log(p instanceof Person)//true
console.log(p.constructor === Person)//false
console.log(p.constructor === Object)//true
有下面兩種方法將constructor設(shè)置回來
function Person(){
}
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
以上這種重設(shè)constructor屬性會(huì)導(dǎo)致它的[[Ennumerable]]特性設(shè)置為true,所以我們可以用下面這種方法;
//重設(shè)構(gòu)造函數(shù),只適用于 ECMAScript 5 兼容的瀏覽器
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
組合構(gòu)造函數(shù)模式和原型模式
原型模式的缺點(diǎn)就是在共享時(shí)屬性和方法后,要是修改了內(nèi)容值,那么同一個(gè)構(gòu)造函數(shù)出來的實(shí)例就會(huì)都受到影響。例如下面代碼
function Person(){
}
Person.prototype = {
constructor: Person,
friends : ["Shelby", "Court"]
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"
alert(person1.friends === person2.friends); //true
所以組合使用構(gòu)造函數(shù)模式與原型模式就是大家普遍用到的方法,我們想要定義自己的屬性和方法,那么就在構(gòu)造函數(shù)中定義即可。例如下面代碼:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
什么是動(dòng)態(tài)原型模式
意思就在初始化實(shí)例的時(shí)候就檢測(cè)判斷是否原型中存在你想要的屬性或者方法,要是沒有,就可以在原型上定義你想要的屬性或方法。如一下代碼:
function Person(name, age, job){
//屬性
this.name = name;
this.age = age;
this.job = job; if (typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName()
什么是寄生構(gòu)造函數(shù)模式
這個(gè)模式跟工廠模式幾乎是一樣的。通過在一個(gè)構(gòu)造函數(shù)內(nèi)部創(chuàng)建一個(gè)新的對(duì)象(數(shù)組、字符串),然后你可以隨意在這個(gè)定義的新對(duì)象上添加屬性和方法,最后return這個(gè)新的對(duì)象。例如一下代碼:
function SpecialArray(){
//創(chuàng)建數(shù)組
var values = new Array();
//添加值
values.push.apply(values, arguments);
//添加方法
values.toPipedString = function(){
return this.join("|");
};
//返回?cái)?shù)組
return values;
}
var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"
這樣做的優(yōu)點(diǎn)是我們可以很輕松在原生對(duì)象上創(chuàng)建一個(gè)自己想要的方法和屬性。但是返回的對(duì)象與構(gòu)造函數(shù)或者與構(gòu)造函數(shù)的原型屬性之間沒有關(guān)系。這樣我們不能通過instanceof操作符確定對(duì)象類型。
什么是穩(wěn)妥構(gòu)造函數(shù)模式
所謂穩(wěn)妥對(duì)象,指的是沒有公共屬性,而且其方法也不引用this對(duì)象。一是新創(chuàng)建對(duì)象的實(shí)例方法不引用this;二是不使用new操作符調(diào)用構(gòu)造函數(shù)。
繼承
什么是原型鏈
我們先來弄懂一句話: 每個(gè)構(gòu)造函數(shù)都有一個(gè)原型對(duì)象,原型對(duì)象都包含一個(gè)指向構(gòu)造函數(shù)的指針,而實(shí)例都包含一個(gè)指向原型對(duì)象的內(nèi)部指針。這就是相當(dāng)于構(gòu)建一個(gè)原型鏈的基石,整個(gè)原型鏈就是由這樣一個(gè)基石累計(jì)起來。
為什么實(shí)例中很少單獨(dú)使用原型鏈?
主要有下面兩個(gè)原因
- 引用類型值的原型屬性會(huì)被所有實(shí)例共享
- 在創(chuàng)建子類型的實(shí)例時(shí),不能向超類型的構(gòu)造函數(shù)中傳遞參數(shù)。
比如下面的例子中我們實(shí)現(xiàn)簡單的繼承
function Father(){
this.colors = ["red","blue","green"];
}
function Son(){}
Son.prototype = new Father();
var instance1 = new Son();
instance1.colors.push('black');
console.log(instance1.colors);// red blue,green,black
var instance2 = new Son();
console.log(instance2.colors);// red blue,green,black
上面例子我們可以看見兩個(gè)實(shí)例共享了一個(gè)方法,這是我們不愿意看到的地方,那么有什么方法能夠解決這個(gè)問題呢?
借用構(gòu)造函數(shù)能否解決上面兩個(gè)問題?
什么是借用構(gòu)造函數(shù)? 就是在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型的構(gòu)造函數(shù)。如下面代碼:
function Father(){
this.colors = ['red','blue','green'];
}
function Son(){
//繼承 或者使用apply
Father.call(this)
}
var instance1 = new Son();
instance1.colors.push('black');
console.log(instance1.colors);//red blue,green,black
var instance2 = new Son();
console.log(instance2.colors);// red blue,green
怎么傳遞參數(shù)? 就是在調(diào)用父類構(gòu)造函數(shù)時(shí),傳遞參數(shù)。具體見下面代碼
function Father(name) {
this.name = name;
}
function Son(){
Father.call(this,'pan');
this.age = 28;
}
var instance = new Son()
console.log(instance.name)//pan
console.log(instance.age)//28
但是借用構(gòu)造函數(shù)也用的不多,為什么呢?
- 方法都在構(gòu)造函數(shù)中定義,函數(shù)復(fù)用就沒有施展的地方了。
- 而且超類型的原型中定義的方法,對(duì)子類型而言也是不可見的。
組合繼承能夠解決什么?
上面兩種繼承都有自己的缺點(diǎn)和優(yōu)點(diǎn),那么我們是否可以取長補(bǔ)短,把兩家結(jié)合在一起?組合繼承就是用來解決這個(gè)問題的。思路就是使用原型鏈實(shí)現(xiàn)對(duì)原型屬性和方法的繼承,而通過借用構(gòu)造函數(shù)來實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承。這樣,即通過在原型上定義方法實(shí)現(xiàn)了函數(shù)復(fù)用,又能夠保證每個(gè)實(shí)例都有它自己的屬性。
function Father(name) {
this.name = name;
this.colors = ['red','blue','green'];
}
Father.prototype.sayName = function(){
console.log(this.name)
}
function Son(name,age){
Father.call(this,name);
this.age = age;
}
//繼承的實(shí)現(xiàn)
Son.prototype = new Father();
Son.prototype.constructor = Son;
Son.prototype.sayAge = function(){
console.log(this.age);
}
var instance1 = new Son('pan',28);
instance1.colors.push('black');
console.log(instance1.colors);// red ,blue,green,black
instance1.sayName();//pan
instance1.sayAge();//28
var instance2 = new Son('lin',26);
console.log(instance2.colors);// red ,blue,green
instance2.sayName();//lin
instance2.sayAge();//26
上面的例子我們看到取長補(bǔ)短的優(yōu)勢(shì),即可以定義自己的屬性和方法,又可以使用相同的方法。這是我們常用的繼承模式,并且instanceof和isPrototypeOf也能夠用于識(shí)別基于組合繼承創(chuàng)建的對(duì)象。
原型式繼承
我們先來看一個(gè)Object.create()的前身的寫法,借助原型可以基于已有的對(duì)象創(chuàng)建新對(duì)象,同時(shí)還不必因此創(chuàng)建自定義類型。
function object(o) {
function F(){}
F.prototype = o;
return new F();
}
在一個(gè)function 內(nèi)部創(chuàng)建一個(gè)臨時(shí)性的構(gòu)造函數(shù),然后重寫這個(gè)構(gòu)造函數(shù)的原型,然后在返回這個(gè)構(gòu)造函數(shù)的新的實(shí)例。不知道你又沒有看出什么不好的地方?jīng)]?從本質(zhì)上講,object()對(duì)傳入其中的對(duì)象執(zhí)行一次淺復(fù)制。
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
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"
我們用新的方法Object.create()來重新規(guī)范上門的例子。這個(gè)方法接收兩個(gè)參數(shù):一個(gè)作用新對(duì)象原型的對(duì)象和一個(gè)為新對(duì)象定義額外屬性的對(duì)象。
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); //"Shelby,Court,Van,Rob,Barbie"
Object.create()方法的第二個(gè)參數(shù)與Object.defineProperties()方法的第二個(gè)參數(shù)格式相同:每個(gè)屬性都是通過自己的描述符定義的。以這種方式指定的任何屬性都會(huì)覆蓋原型對(duì)象上的同名屬性。例如:
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
console.log(anotherPerson.name); //"Greg"
如果只是簡單的繼承,那么原型模式就能勝任,但是想要?jiǎng)?chuàng)建構(gòu)造函數(shù),那么引用類型值的會(huì)共享這個(gè)坑是不能避免的。
寄生式繼承又能解決什么問題?
都是寄生,自然和構(gòu)造函數(shù)的寄生類似,創(chuàng)建一個(gè)僅用于封裝繼承過程的函數(shù),該函數(shù)在內(nèi)部以某種方式來增前對(duì)象,然后再返回這個(gè)對(duì)象。
function createAnother(original){
var clone = object(original);
clone.sayHi = function(){
console.log('hi')
}
return clone;
}
接下里我們運(yùn)用上面這個(gè)函數(shù)
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
新對(duì)象anotherPerson不僅具有person的所有屬性和方法,而且還有自己的sayHi()方法。但是此寄生式繼承不能復(fù)用函數(shù),所以效率不是很高。
終極boss 寄生組合式繼承
我們前面說的組合繼承是常用的模式,但是也有一個(gè)缺點(diǎn):那就是會(huì)調(diào)用兩次超類型構(gòu)造函數(shù)。什么意思?我們來看代碼;
function Father(name) {
this.name = name;
this.colors = ['red','blue','green'];
}
Father.prototype.sayName = function(){
console.log(this.name)
}
function Son(name,age){
Father.call(this,name);//第二次調(diào)用Father()
this.age = age;
}
//繼承的實(shí)現(xiàn)
Son.prototype = new Father();//第一次調(diào)用Father()
Son.prototype.constructor = Son;
Son.prototype.sayAge = function(){
console.log(this.age);
}
var instance1 = new Son('pan',28);
instance1.colors.push('black');
console.log(instance1.colors);// red ,blue,green,black
instance1.sayName();//pan
instance1.sayAge();//28
上面代碼中兩個(gè)注釋很好的反應(yīng)了這兩次構(gòu)造函數(shù)。那么寄生組合式繼承就能夠很好的優(yōu)化這個(gè)問題。通過Object.create()來很好的實(shí)現(xiàn),只需要修改一處即可。
將Son.prototype = new Father();
替換成
Son.prototype = Object.create(Father.prototype)
高效率的體現(xiàn)就在只調(diào)用了一次Father構(gòu)造函數(shù),可以避免在Son.prototype上面創(chuàng)建不必要的、多余的屬性。與此同時(shí),原型鏈還能保持不變。并且還能正常使用instanceof和isPrototypeOf()。
多繼承又如何實(shí)現(xiàn)?
上面的寄生組合式繼承只是一個(gè)單繼承,那多繼承又該如何實(shí)現(xiàn)?
我們可以用混入的方式來實(shí)現(xiàn)多個(gè)對(duì)象的繼承
//第一個(gè)構(gòu)造函數(shù)
function Father(name) {
this.name = name;
this.colors = ['red','blue','green'];
}
Father.prototype.sayName = function(){
console.log(this.name)
}
//第二個(gè)構(gòu)造函數(shù)
function FatherBrother(hobby){
this.hobby = hobby
}
function Son(name,age,hobby){
Father.call(this,name);
FatherBrother.call(this,hobby)
this.age = age;
}
//繼承的實(shí)現(xiàn)
Son.prototype = Object.create(Father.prototype);
//混合
Object.assign(Son.prototype,FatherBrother.prototype)
//重新指定constructor
Son.prototype.constructor = Son;
Son.prototype.sayAge = function(){
console.log(this.age);
}
var instance1 = new Son('pan',28,'play');
instance1.colors.push('black');
console.log(instance1.colors);// red ,blue,green,black
instance1.sayName();//pan
instance1.sayAge();//28
instance1.hobby;//play
這里關(guān)鍵點(diǎn)是:Object.assign 會(huì)把 FatherBrother原型上的函數(shù)拷貝到 Son原型上,使 Son 的所有實(shí)例都可用 FatherBrother 的方法。
整個(gè)創(chuàng)建對(duì)象的演變過程
工廠模式(簡單的添加屬性和方法) -->構(gòu)造函數(shù)模式(優(yōu)點(diǎn):可以創(chuàng)建內(nèi)置對(duì)象實(shí)例。缺點(diǎn):成員無法復(fù)用。)-->原型模式(結(jié)合構(gòu)造函數(shù)模式和原型模式兩方的優(yōu)點(diǎn),使用構(gòu)造函數(shù)定義實(shí)例屬性,而使用原型定義共享的屬性和方法。)-->原型式繼承(優(yōu)點(diǎn):不必預(yù)先定義。缺點(diǎn):會(huì)存在重寫原型)-->寄生式繼承(優(yōu)點(diǎn):效率高,不必多次調(diào)用超類型構(gòu)造函數(shù)。缺點(diǎn):復(fù)用率不高)-->寄生組合式繼承(集寄生式繼承和組合繼承的優(yōu)點(diǎn)與一身。)
如果大神您想繼續(xù)探討或者學(xué)習(xí)更多知識(shí),歡迎加入QQ一起探討:854280588
參考文章
- 紅皮書第三版
