avaScript對每個創(chuàng)建的對象都會設(shè)置一個原型,指向它的原型對象。
當我們用obj.xxx訪問一個對象的屬性時,JavaScript引擎先在當前對象上查找該屬性,如果沒有找到,就到其原型對象上找,如果還沒有找到,就一直上溯到Object.prototype對象,最后,如果還沒有找到,就只能返回undefined。
例如,創(chuàng)建一個Array對象:
var arr=[1, 2, 3];
其原型鏈是:
arr ----> Array.prototype ----> Object.prototype ----> null
Array.prototype定義了indexOf()、shift()等方法,因此你可以在所有的Array對象上直接調(diào)用這些方法。
當我們創(chuàng)建一個函數(shù)時:
function foo() {
return 0;
}
函數(shù)也是一個對象,它的原型鏈是:
foo ----> Function.prototype ----> Object.prototype ----> null
由于Function.prototype定義了apply()等方法,因此,所有函數(shù)都可以調(diào)用apply()方法。
很容易想到,如果原型鏈很長,那么訪問一個對象的屬性就會因為花更多的時間查找而變得更慢,因此要注意不要把原型鏈搞得太長。
構(gòu)造函數(shù)
除了直接用{ ... }創(chuàng)建一個對象外,JavaScript還可以用一種構(gòu)造函數(shù)的方法來創(chuàng)建對象。它的用法是,先定義一個構(gòu)造函數(shù):
function Student(name) {
this.name=name;
this.hello=function () {
alert('Hello, ' + this.name + '!');
}
}
你會問,咦,這不是一個普通函數(shù)嗎?
這確實是一個普通函數(shù),但是在JavaScript中,可以用關(guān)鍵字new來調(diào)用這個函數(shù),并返回一個對象:
var xiaoming=new Student('小明');
xiaoming.name; // '小明'
xiaoming.hello(); // Hello, 小明!
注意,如果不寫new,這就是一個普通函數(shù),它返回undefined。但是,如果寫了new,它就變成了一個構(gòu)造函數(shù),它綁定的this指向新創(chuàng)建的對象,并默認返回this,也就是說,不需要在最后寫return this;
新創(chuàng)建的xiaoming的原型鏈是:
xiaoming ----> Student.prototype ----> Object.prototype ----> null
也就是說,xiaoming的原型指向函數(shù)Student的原型。如果你又創(chuàng)建了xiaohong、xiaojun,那么這些對象的原型與xiaoming是一樣的:
xiaoming ↘
xiaohong -→Student.prototype ----> Object.prototype ----> null
xiaojun ↗
用new Student()創(chuàng)建的對象還從原型上獲得了一個constructor屬性,它指向函數(shù)Student本身:
xiaoming.constructor===Student.prototype.constructor; // true
Student.prototype.constructor===Student; // true
Object.getPrototypeOf(xiaoming)===Student.prototype; // true
xiaoming instanceof Student; // true
看暈了吧?用一張圖來表示這些亂七八糟的關(guān)系就是:
另外,函數(shù)Student恰好有個屬性prototype指向xiaoming、xiaohong的原型對象,但是xiaoming、xiaohong這些對象可沒有prototype這個屬性,不過可以用__proto__這個非標準用法來查看。紅色箭頭是原型鏈。注意,Student.prototype指向的對象就是xiaoming、xiaohong的原型對象,這個原型對象自己還有個屬性constructor,指向Student函數(shù)本身。
現(xiàn)在我們就認為xiaoming、xiaohong這些對象“繼承”自Student。
不過還有一個小問題,注意觀察:
xiaoming.name; // '小明'
xiaohong.name; // '小紅'
xiaoming.hello; // function: Student.hello()
xiaohong.hello; // function: Student.hello()
xiaoming.hello===xiaohong.hello; // false
xiaoming和xiaohong各自的name不同,這是對的,否則我們無法區(qū)分誰是誰了。
xiaoming和xiaohong各自的hello是一個函數(shù),但它們是兩個不同的函數(shù),雖然函數(shù)名稱和代碼都是相同的!
如果我們通過new Student()創(chuàng)建了很多對象,這些對象的hello函數(shù)實際上只需要共享同一個函數(shù)就可以了,這樣可以節(jié)省很多內(nèi)存。
要讓創(chuàng)建的對象共享一個hello函數(shù),根據(jù)對象的屬性查找原則,我們只要把hello函數(shù)移動到
xiaoming、xiaohong這些對象共同的原型上就可以了,也就是Student.prototype:
修改代碼如下:
function Student(name) {
this.name=name;
}
Student.prototype.hello=function () {
alert('Hello, ' + this.name + '!');
};
用new創(chuàng)建基于原型的JavaScript的對象就是這么簡單!
忘記寫new怎么辦
如果一個函數(shù)被定義為用于創(chuàng)建對象的構(gòu)造函數(shù),但是調(diào)用時忘記了寫new怎么辦?
在strict模式下,this.name=name將報錯,因為this綁定為undefined,在非strict模式下,this.name=name不報錯,因為this綁定為window,于是無意間創(chuàng)建了全局變量name,并且返回undefined,這個結(jié)果更糟糕。
所以,調(diào)用構(gòu)造函數(shù)千萬不要忘記寫new。為了區(qū)分普通函數(shù)和構(gòu)造函數(shù),按照約定,構(gòu)造函數(shù)首字母應(yīng)當大寫,而普通函數(shù)首字母應(yīng)當小寫,這樣,一些語法檢查工具如jslint將可以幫你檢測到漏寫的new。
最后,我們還可以編寫一個createStudent()函數(shù),在內(nèi)部封裝所有的new操作。一個常用的編程模式像這樣:
function Student(props) {
this.name=props.name || '匿名'; // 默認值為'匿名'
this.grade=props.grade || 1; // 默認值為1
}
Student.prototype.hello=function () {
alert('Hello, ' + this.name + '!');
};
function createStudent(props) {
return new Student(props || {})
}
這個createStudent()函數(shù)有幾個巨大的優(yōu)點:一是不需要new來調(diào)用,二是參數(shù)非常靈活,可以不傳,也可以這么傳:
var xiaoming=createStudent({
name: '小明'
});
xiaoming.grade; // 1
如果創(chuàng)建的對象有很多屬性,我們只需要傳遞需要的某些屬性,剩下的屬性可以用默認值。由于參數(shù)是一個Object,我們無需記憶參數(shù)的順序。如果恰好從JSON拿到了一個對象,就可以直接創(chuàng)建出xiaoming。
繼承
在傳統(tǒng)的基于Class的語言如Java、C++中,繼承的本質(zhì)是擴展一個已有的Class,并生成新的Subclass。
由于這類語言嚴格區(qū)分類和實例,繼承實際上是類型的擴展。但是,JavaScript由于采用原型繼承,我們無法直接擴展一個Class,因為根本不存在Class這種類型。
但是辦法還是有的。我們先回顧Student構(gòu)造函數(shù):
function Student(props) {
this.name=props.name || 'Unnamed';
}
Student.prototype.hello=function () {
alert('Hello, ' + this.name + '!');
}
以及Student的原型鏈:
現(xiàn)在,我們要基于Student擴展出PrimaryStudent,可以先定義出PrimaryStudent:
function PrimaryStudent(props) {
// 調(diào)用Student構(gòu)造函數(shù),綁定this變量:
Student.call(this, props);
this.grade=props.grade || 1;
}
但是,調(diào)用了Student構(gòu)造函數(shù)不等于繼承了Student,PrimaryStudent創(chuàng)建的對象的原型是:
new PrimaryStudent() ----> PrimaryStudent.prototype ----> Object.prototype ----> null
必須想辦法把原型鏈修改為:
new PrimaryStudent() ----> PrimaryStudent.prototype ----> Student.prototype ----> Object.prototype ----> null
這樣,原型鏈對了,繼承關(guān)系就對了。新的基于PrimaryStudent創(chuàng)建的對象不但能調(diào)用PrimaryStudent.prototype定義的方法,也可以調(diào)用Student.prototype定義的方法。
如果你想用最簡單粗暴的方法這么干:
PrimaryStudent.prototype=Student.prototype;
是不行的!如果這樣的話,PrimaryStudent和Student共享一個原型對象,那還要定義PrimaryStudent干啥?
我們必須借助一個中間對象來實現(xiàn)正確的原型鏈,這個中間對象的原型要指向Student.prototype。為了實現(xiàn)這一點,參考道爺(就是發(fā)明JSON的那個道格拉斯)的代碼,中間對象可以用一個空函數(shù)F來實現(xiàn):
// PrimaryStudent構(gòu)造函數(shù):
function PrimaryStudent(props) {
Student.call(this, props);
this.grade=props.grade || 1;
}
// 空函數(shù)F:
function F() {
}
// 把F的原型指向Student.prototype:
F.prototype=Student.prototype;
// 把PrimaryStudent的原型指向一個新的F對象,F(xiàn)對象的原型正好指向Student.prototype:
PrimaryStudent.prototype=new F();
// 把PrimaryStudent原型的構(gòu)造函數(shù)修復(fù)為PrimaryStudent:
PrimaryStudent.prototype.constructor=PrimaryStudent;
// 繼續(xù)在PrimaryStudent原型(就是new F()對象)上定義方法:
PrimaryStudent.prototype.getGrade=function () {
return this.grade;
};
// 創(chuàng)建xiaoming:
var xiaoming=new PrimaryStudent({
name: '小明',
grade: 2
});
xiaoming.name; // '小明'
xiaoming.grade; // 2
// 驗證原型:
xiaoming.__proto__===PrimaryStudent.prototype; // true
xiaoming.__proto__.__proto__===Student.prototype; // true
// 驗證繼承關(guān)系:
xiaoming instanceof PrimaryStudent; // true
xiaoming instanceof Student; // true
用一張圖來表示新的原型鏈:
注意,函數(shù)F僅用于橋接,我們僅創(chuàng)建了一個new F()實例,而且,沒有改變原有的Student定義的原型鏈。
如果把繼承這個動作用一個inherits()函數(shù)封裝起來,還可以隱藏F的定義,并簡化代碼:
function inherits(Child, Parent) {
var F=function () {};
F.prototype=Parent.prototype;
Child.prototype=new F();
Child.prototype.constructor=Child;
}
這個inherits()函數(shù)可以復(fù)用:
function Student(props) {
this.name=props.name || 'Unnamed';
}
Student.prototype.hello=function () {
alert('Hello, ' + this.name + '!');
}
function PrimaryStudent(props) {
Student.call(this, props);
this.grade=props.grade || 1;
}
// 實現(xiàn)原型繼承鏈:
inherits(PrimaryStudent, Student);
// 綁定其他方法到PrimaryStudent原型:
PrimaryStudent.prototype.getGrade=function () {
return this.grade;
};
小結(jié)
JavaScript的原型繼承實現(xiàn)方式就是:
定義新的構(gòu)造函數(shù),并在內(nèi)部用call()調(diào)用希望“繼承”的構(gòu)造函數(shù),并綁定this;
借助中間函數(shù)F實現(xiàn)原型鏈繼承,最好通過封裝的inherits函數(shù)完成;
繼續(xù)在新的構(gòu)造函數(shù)的原型上定義新方法。
ES6的class繼承
新的關(guān)鍵字class從ES6開始正式被引入到JavaScript中。class的目的就是讓定義類更簡單。
我們先回顧用函數(shù)實現(xiàn)Student的方法:
function Student(name) {
this.name=name;
}
Student.prototype.hello=function () {
alert('Hello, ' + this.name + '!');
}
如果用新的class關(guān)鍵字來編寫Student,可以這樣寫:
class Student {
constructor(name) {
this.name=name;
}
hello() {
alert('Hello, ' + this.name + '!');
}
}
比較一下就可以發(fā)現(xiàn),class的定義包含了構(gòu)造函數(shù)constructor和定義在原型對象上的函數(shù)hello()(注意沒有function關(guān)鍵字),這樣就避免了Student.prototype.hello=function () {...}這樣分散的代碼。
最后,創(chuàng)建一個Student對象代碼和前面章節(jié)完全一樣:
var xiaoming=new Student('小明');
xiaoming.hello();
class繼承
用class定義對象的另一個巨大的好處是繼承更方便了。想一想我們從Student派生一個PrimaryStudent需要編寫的代碼量?,F(xiàn)在,原型繼承的中間對象,原型對象的構(gòu)造函數(shù)等等都不需要考慮了,直接通過extends來實現(xiàn):
class PrimaryStudent extends Student {
constructor(name, grade) {
super(name); // 記得用super調(diào)用父類的構(gòu)造方法!
this.grade=grade;
}
myGrade() {
alert('I am at grade ' + this.grade);
}
}
注意PrimaryStudent的定義也是class關(guān)鍵字實現(xiàn)的,而extends則表示原型鏈對象來自Student。子類的構(gòu)造函數(shù)可能會與父類不太相同,例如,PrimaryStudent需要name和grade兩個參數(shù),并且需要通過super(name)來調(diào)用父類的構(gòu)造函數(shù),否則父類的name屬性無法正常初始化。
PrimaryStudent已經(jīng)自動獲得了父類Student的hello方法,我們又在子類中定義了新的myGrade方法。
ES6引入的class和原有的JavaScript原型繼承有什么區(qū)別呢?實際上它們沒有任何區(qū)別,class的作用就是讓JavaScript引擎去實現(xiàn)原來需要我們自己編寫的原型鏈代碼。簡而言之,用class的好處就是極大地簡化了原型鏈代碼。
千鋒教育HTML5教學(xué)部 :鄭若凡
拷貝和淺拷貝是經(jīng)常在面試中會出現(xiàn)的,主要考察你對基本類型和引用類型的理解深度。我在無數(shù)次的面試中,應(yīng)聘者還沒有一個人能把這個問題回答情況,包括很多機構(gòu)的培訓(xùn)老師。這篇文章會讓你把基本類型和引用類型的區(qū)別搞得清清楚楚,搞清楚這兩者的區(qū)別,你對任何編程語言的都不怕,因為,這不是js一門語言,是任何編程語言中都需要掌握的知識,而且,在任何編程語言中,兩者都是一樣的。
深拷貝和淺拷貝主要是針對對象的屬性是對象(引用類型)
一、基本類型和引用類型的區(qū)別
1、先了解內(nèi)存
任何編程語言的內(nèi)存分區(qū)幾乎都是一樣的
內(nèi)存是存儲數(shù)據(jù)的,不同類型的數(shù)據(jù)要存儲在不同的區(qū)域,即分類存放,不同的區(qū)域作用和功能也不一樣。就像你家里的衣柜一樣,也分了不同的區(qū)域:如掛西裝的區(qū)域,放襪子的區(qū)域等等,我相信每個人都會把這兩個東西放在不同的區(qū)域。要不然,當你西裝革履地參加一個高檔的宴會,手塞在褲兜里,掏出來一只臭襪子,是不是很尷尬?。?!哈哈?。?!
以下為內(nèi)存的分區(qū)圖。內(nèi)存分為四個區(qū)域:棧區(qū)(堆棧),堆區(qū),全局靜態(tài)區(qū),只讀區(qū)(常量區(qū)和代碼區(qū))。
https://blog.csdn.net/jiang7701037/article/details/98728249
2、基本類型和引用類型在內(nèi)存上存儲的區(qū)別
現(xiàn)在只看棧區(qū)和堆區(qū),不管其它區(qū)域,也假定只是局部變量。
以上函數(shù)testf在調(diào)用時,
1)、 定義局部變量 age,由于age是局部變量,所以在棧中申請內(nèi)存空間,起名為age,又由于給age賦的值250是基本類型,所以,值直接存儲在棧中。
2)、定義局部變量arr,由于arr是局部變量,所以在棧中申請空間,但是arr的內(nèi)存中存儲的是什么?由于給arr賦的值不是基本類型,而是引用類型(new出來的),所以,先在堆中申請空間存放數(shù)據(jù) 12,23,34,。再把堆區(qū)的地址賦給arr。
3、到底什么是基本類型和引用類型
1)、基本類型:就是值類型,即在變量所對應(yīng)的內(nèi)存區(qū)域存儲的是值,如:上面的age變量所對應(yīng)的內(nèi)存存儲的就是值250.
2)、引用類型:就是地址類型。
何為地址:地址就是編號,要地址何用,就是為了容易找到。每個人的家里為什么要有一個唯一的地址,就是在郵寄時,能夠找到你家。
比如:我們最早的超市存包的格子,每個格子都有個編號,你存包時,服務(wù)員會把你的東西放在某個格子里,再把這個格子的編號給你(一個牌子)。你購物完畢取包時,直接給服務(wù)員你的牌子(有編號),服務(wù)員根據(jù)你的編號就會找到你的包。這個編號就是格子的地址。內(nèi)存也是一樣的,每個內(nèi)存都有一個編號,方便cpu查找。要不然,浩瀚的內(nèi)存海洋,cpu要找到數(shù)據(jù)靠啥找。
以上的變量arr就是引用類型,arr所對應(yīng)的內(nèi)存中存儲著地址,真正的數(shù)據(jù)是在地址對應(yīng)的內(nèi)存區(qū)域里,就像,你填寫簡歷時,會在簡歷的那張紙上寫上你家的地址。簡歷上寫你家地址的地方就相當于arr。而你家是根據(jù)這個地址可以找到的。簡歷上寫你家地址的地方就相當于引用著你家(可以想象一根無形的線牽引著你家,在簡歷上的這根無形的線,順藤摸瓜就能找到你家)。所以叫做引用類型。
二、基本類型和引用類型在賦值時內(nèi)存的變化
你可以認為,賦值就是在拷貝。
1、基本類型:
2、引用類型:
如果給arr[0]賦值的話,arr1[0]的值也會發(fā)生變化,因為,arr和arr1保存著相同的地址,它門兩個引用的數(shù)據(jù)是共享的。就像你在很多地方(簡歷的那張紙,戶口本上的那張紙)會寫上你的家庭地址。這么多張紙都引用著你家。根據(jù)一張紙上找到你家,給你家放上一百萬的現(xiàn)金(數(shù)據(jù)改變了,相當于arr[0]=10),再根據(jù)另外一張紙的地址也找到了你家,你發(fā)現(xiàn)你一百萬在(不要給我說被人拿了)
如果在上面的基礎(chǔ)上增加一句代碼:arr[0]=10;那么內(nèi)存將會有如下變化:
三、基本類型和引用類型作為函數(shù)參數(shù)的區(qū)別(這個可以不看)
1、基本類型作為函數(shù)的參數(shù)
2、引用類型作為函數(shù)的參數(shù):
四、深拷貝和淺拷貝:
終于說到了深拷貝和淺拷貝。
其實在第二點已經(jīng)說到了拷貝,所謂拷貝,就是賦值。把一個變量賦給另外一個變量,就是把變量的內(nèi)容進行拷貝。把一個對象的值賦給另外一個對象,就是把一個對象拷貝一份。
1、基本類沒有問題,
因為,基本類型賦值時,賦的是數(shù)據(jù)(所以,不存在深拷貝和淺拷貝的問題)。
如:
Var x=100;
Var y=x; //此時x和y都是100;
如果要改變y的值,x的值不會改變。
2、引用類型有問題
因為,引用類型賦值時,賦的值地址(就是引用類型變量在內(nèi)存中保存的內(nèi)容),強烈建議把前面的第二點(基本類型和引用類型在賦值時內(nèi)存的變化)多看幾遍,以保證理解深刻。這樣,一勞永逸,以后在碰到任何跟引用類型有關(guān)的話題(如:繼承時,父類的屬性是引用類型)都沒有問題。
如:
var arr1=new Array(12,23,34)
Var arr2=arr1;//這就是一個最簡單的淺拷貝
如果要改變arr2所引用的數(shù)據(jù):arr2[0]=100時,那么arr1[0]的值也是100。
原因就是 arr1和arr2引用了同一塊內(nèi)存區(qū)域(以上的第二點中有體現(xiàn))。
這是最簡單的淺拷貝,因為,只是把arr1的地址拷貝的一份給了arr2,并沒有把arr1的數(shù)據(jù)拷貝一份。所以,拷貝的深度不夠
3、用json對象的方式(也是引用類型)來演示淺拷貝和深拷貝
1)、定義一個json對象(對象的屬性也是對象)
2)、把該對象p進行復(fù)制一份
· (一)淺拷貝
在控制臺中打印的結(jié)果(p和p2的books[0]都變成了“四國”):
內(nèi)存:
(二) 深拷貝(初步)
在控制臺中打印的結(jié)果(只有p2的books[0]變成了“四國”)
內(nèi)存:
(三)深拷貝(最終)
3.1、深拷貝_如果屬性都是json對象,那么用遞歸的方式
//如果對象的屬性是對象(引用類型),屬性的屬性也是引用類型,即層層嵌套很多.怎么辦,只能遞歸
//如下對象,要復(fù)制:
3.2、深拷貝_如果屬性是數(shù)組等非鍵值對的對象
就得單獨處理:要么給數(shù)組增加一個自我復(fù)制的函數(shù)(建議這樣做),要么單獨判斷。
```
小白,知道怎么制作登陸模塊么?”
“大概能想明白,簡單的登陸主要是用戶名、密碼兩個輸入框,復(fù)雜點的可能會加上驗證碼和手機驗證。用戶填寫好信息以后,點擊確定后把用戶輸入的信息通過POST方式發(fā)送給做登陸處理的PHP頁面。做登陸處理的PHP頁面判斷如果用戶名密碼正確,則返回登陸正確的JSON字符串,如果用戶名密碼錯誤則返回對應(yīng)的錯誤信息JSON字符串?!?/p>
“嗯,基本沒有問題,不過有一個情況你沒有考慮?!?/p>
“什么情況?”,小白不解的問。
“假如用戶輸入的用戶名為空或者長度不符合要求,還有沒有必要向PHP頁面發(fā)送登陸驗證?”
“哦!我知道了,沒必要!不符合要求的數(shù)據(jù)發(fā)送到后臺PHP頁面進行判斷肯定是登陸不了的,所以需要提前通過JavaScript進行判斷,如果填寫的格式不對,可以通過JavaScript提示用戶哪里有問題,等用戶信息填寫正確后再向PHP發(fā)送登陸驗證的請求,這樣不但用戶體驗好服務(wù)器壓力也小?!?/p>
老朱說:“說的不錯,那你現(xiàn)在布局一個簡單的用戶登錄,然后咱試著做個驗證判斷。”
小白用了幾分鐘就做完了,跟老朱說道:“布局做好了,我們現(xiàn)在應(yīng)該怎么做呢?”
“我們現(xiàn)在只判斷用戶名和密碼的長度,用戶名最短長度是2,密碼最短長度是6。之前定義對象的方法還記得吧?你現(xiàn)在試著定義一個對象,并讓這個對象包含判斷判斷用戶名和密碼的方法。”
“這個感覺沒什么頭緒啊,給我1個小時的時間可以么?”
“沒問題,我相信你!一定要用對象來做啊,不要用函數(shù)?!?/p>
“朱哥,做好了,你看看我寫的這個有什么問題么?”
老朱仔細看了小白寫的代碼后,非常高興,說道:“小白!我現(xiàn)在已經(jīng)對你刮目相看了,看來你這幾天沒少做JS對象的練習(xí)啊!我非常欣賞你這里每個方法中最后的return this用法。通過這個方法就可以通過鏈式方法使用對象里面的各個方法了,你還給checkOBJ對象設(shè)定了一個ispass屬性來判斷是否驗證通過,真的很不錯?!?/p>
小白聽了以后,內(nèi)心相當?shù)母吲d,說道:“這幾天晚上沒事我都會看看JS的一些基礎(chǔ)知識介紹,正好最近好好研究了一下return的用法。不過我這里用alert提醒用戶是不是用戶體驗很不好??!”
“沒關(guān)系的,小白!我們今天主要還是熟悉對象的操作,你已經(jīng)把JS對象的使用方法又提高了一層,看來不用幾天我們就可以學(xué)習(xí)JS模擬類的操作了。”
“嗯,我也很期待繼續(xù)提高呢~”
“小白,今天我們做的用戶登錄驗證還有很多需要完善的地方,比如提示用戶的時候能不能直接把焦點定位到有問題的文本框,能不能不用alert而使用昨天我們說到的彈層等等。前端用戶體驗是非常重要的,這一點我們一定不能忽視,所以我還是希望你今天能夠繼續(xù)把用戶登錄驗證完善完善,也當是做一個練習(xí)?!?/p>
老朱準備走的時候回頭又跟小白說了一句:“噢!對了,今天為什么要用一個對象來做判定而不是用幾個函數(shù)判定,這個你也好好考慮考慮!”
想學(xué)H5的朋友可以關(guān)注老爐,您的關(guān)注是我持續(xù)更新《小白HTML5成長之路》的動力!
*請認真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。