一、對(duì)象
ECMA-262把對(duì)象定義為:”無序?qū)傩缘募厦觯鋵傩钥梢园局敌叻础?duì)象或者函數(shù)“布朦;
每個(gè)對(duì)象的屬性或者方法都有一個(gè)名字,每個(gè)名字都映射到一個(gè)值(key: value)昼窗;
創(chuàng)建一個(gè)對(duì)象最簡單的辦法就是創(chuàng)建一個(gè)Object實(shí)例
可以為對(duì)象添加屬性和方法
var person = new Object()
person.name = 'zcy';
person.age = '22';
person.sayName = function() {
console.log(this.name);
}
上述例子用對(duì)象字面量語法可以寫成
var person = {
name: 'zcy',
age: '22',
sayName: function () {
console.log(this.name); //this指向?qū)ο蟊旧? },
};
person.sayName(); //zcy
ECMAScript中有兩種屬性是趴,數(shù)據(jù)屬性和訪問器屬性,如果要修改屬性的默認(rèn)特征澄惊,必須要使用Object.defineProperty()
方法
具體參考https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
二唆途、構(gòu)造函數(shù)
構(gòu)造函數(shù)本身也是一個(gè)函數(shù),與普通函數(shù)相同掸驱,不過為了規(guī)范將首字母大寫肛搬,構(gòu)造函數(shù)與普通函數(shù)的區(qū)別就是可以用new生成構(gòu)造函數(shù)的實(shí)例,普通函數(shù)是直接調(diào)用無法創(chuàng)建實(shí)例毕贼,像原生構(gòu)造函數(shù)Object()温赔、Array()等,在運(yùn)行時(shí)會(huì)自動(dòng)出現(xiàn)在執(zhí)行環(huán)境中鬼癣。此外也可以創(chuàng)建自定義的構(gòu)造函數(shù)陶贼,從而定義自定義對(duì)象類型的屬性和方法。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.sayName = function () {
console.log(this.name);
};
this.job = job;
}
var person1 = new Person('zcy', '22', 'web');
person1.sayName();//構(gòu)造函數(shù)的this指向被創(chuàng)建的對(duì)象
構(gòu)造函數(shù)和普通函數(shù)的唯一區(qū)別就是調(diào)用它們的方式不同待秃,構(gòu)造函數(shù)通過new調(diào)用拜秧,普通函數(shù)直接調(diào)用,不過任何函數(shù)只要通過new操作符來調(diào)用锥余,那么它就是構(gòu)造函數(shù)腹纳,而任何函數(shù)不通過new調(diào)用痢掠,那它和普通函數(shù)也不會(huì)有什么不同驱犹。
例如上述例子直接調(diào)用Person()函數(shù),函數(shù)里的對(duì)象和方法將掛到全局(window)上足画;
constructor
每一個(gè)被構(gòu)造出來的對(duì)象都有一個(gè)constructor屬性
constructor返回創(chuàng)建實(shí)例對(duì)象時(shí)構(gòu)造函數(shù)的引用雄驹,就是說 p.constructor
返回它的構(gòu)造函數(shù)的引用Person
;
function Person(age) {
this.age = age;
}
var p = new Person(50);
p.constructor === Person; // true
構(gòu)造函數(shù)的缺點(diǎn)就是每個(gè)方法都要在每個(gè)實(shí)例上重新創(chuàng)建一遍。
var person1 = new Person();
var person2 = new Person();
person1和person2都有一個(gè)名為sayName()的方法淹辞,但是這兩個(gè)方法不是同一個(gè)Function實(shí)例医舆,他們是不相等的;
可以把sayName()理解為:
this.sayName = new Function(console.log(this.name))
所以每個(gè)Person實(shí)例都會(huì)有不同的Function實(shí)例象缀,如果一直以這種方式創(chuàng)建蔬将,會(huì)導(dǎo)致不同的作用域鏈和標(biāo)識(shí)符解析;
如果一直創(chuàng)建對(duì)象就會(huì)一直創(chuàng)建Function實(shí)例央星,浪費(fèi)內(nèi)存霞怀,好在這些問題可以通過使用原型來解決;
三莉给、原型
我們創(chuàng)建的每個(gè)函數(shù)都有一個(gè)prototype
屬性毙石,也是就原型屬性廉沮,無論什么時(shí)候,只要?jiǎng)?chuàng)建了一個(gè)新函數(shù)徐矩,就會(huì)根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個(gè)prototype屬性滞时,這個(gè)屬性是一個(gè)指針,指向一個(gè)該函數(shù)的原型對(duì)象滤灯,而這個(gè)對(duì)象的用途是包含可以由特定類型的所有實(shí)例共享的屬性和方法坪稽。
按照字面意思來理解,prototype就是通過調(diào)用構(gòu)造函數(shù)而創(chuàng)建的那個(gè)對(duì)象實(shí)例的原型對(duì)象
在默認(rèn)情況下鳞骤,所有的原型對(duì)象都會(huì)自動(dòng)獲得一個(gè)constructor(構(gòu)造函數(shù))屬性刽漂,這個(gè)屬性是一個(gè)指向prototype屬性所在函數(shù)的指針;
Person.prototype.constructor === Person //tue
掛在原型上的方法和屬性弟孟,實(shí)例出來的對(duì)象訪問的都是同一組屬性和同一個(gè)方法
所以這個(gè)就是比單純使用構(gòu)造函數(shù)更好的地方
構(gòu)造函數(shù)的Person的prototype屬性指向該構(gòu)造函數(shù)原型對(duì)象贝咙,該原型對(duì)象的constructor指向構(gòu)造函數(shù)本身
[[Prototype]]和 __proto__
當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例后,該實(shí)例的內(nèi)部包含一個(gè)指針(內(nèi)部屬性)拂募,指向構(gòu)造函數(shù)的原型對(duì)象庭猩,這個(gè)指針叫[[Prototype]]
[[Prototype]]和__proto__有什么關(guān)系呢?
其實(shí)[[prototype]]和__proto__意義相同陈症,均表示對(duì)象的內(nèi)部屬性蔼水,其值指向?qū)ο笤汀G罢咴谝恍伎稀⒁?guī)范中表示一個(gè)對(duì)象的原型屬性趴腋,后者則是在瀏覽器實(shí)現(xiàn)中指向?qū)ο笤汀?br>
需要注意的一點(diǎn)是,這個(gè)連接存在于構(gòu)造出來的實(shí)例person1
與構(gòu)造函數(shù)的原型對(duì)象Person.prototype
中论咏,而不存在實(shí)例與構(gòu)造函數(shù)中优炬;
所以person1.__proto__指向構(gòu)造person1的構(gòu)造函數(shù)的原型
構(gòu)造函數(shù)的原型對(duì)象prototype上也有__proto__屬性,其指向構(gòu)造Person的構(gòu)造函數(shù)的原型對(duì)象
也就是Object的原型對(duì)象
原型對(duì)象上有isPrototypeOf
和hasOwnProperty
方法厅贪,他們的作用分別是
Person.prototype.isPrototypeOf(person1)
true
判斷對(duì)象person1的內(nèi)部指針__proto__是不是指向Person
person1.play = 'happy';
person1.hasOwnProperty("play");
true
person1.hasOwnProperty("sayName");
false
判斷屬性是否是對(duì)象自身的屬性蠢护,如果是自身屬性則返回true,是原型上的則返回false
ECMAScript還提供了一個(gè)方法getPrototypeOf
养涮,用來判斷對(duì)象__proto__指向
Object.getPrototypeOf(person1) == Person.prototype
true
person1的__proto__指向構(gòu)造函數(shù)的原型對(duì)象
當(dāng)我們?nèi)ピL問一個(gè)對(duì)象的屬性時(shí)葵硕,會(huì)執(zhí)行一次搜索,如果在該對(duì)象找到了這個(gè)屬性贯吓,則返回懈凹,如果沒找到,則順著該對(duì)象的__proto__向構(gòu)造該對(duì)象的原型對(duì)象上查找悄谐,找到則返回介评,沒找到繼續(xù)沿著__proto__向上查找,直到查找到Object.__proto__都還沒找到尊沸,原型鏈的頂端是null威沫,沒找到就返回undefined贤惯,這就是原型鏈
比如:當(dāng)調(diào)用person1的sayName()方法時(shí),問:“person1有sayName屬性嗎”棒掠,回答沒有孵构,然后繼續(xù)向上搜索,通過查找person1.__proto__指向person1構(gòu)造函數(shù)的prototype對(duì)象烟很,該原型對(duì)象上有sayName屬性颈墅,則返回該屬性。
最后梳理一下各種名詞的解釋
//Person是一個(gè)構(gòu)造函數(shù)
function Person() {}
//構(gòu)造函數(shù)的prototype屬性指向該構(gòu)造函數(shù)的原型對(duì)象
Person.prototype.sayName = function () {
console.log('hello');
};
var person1 = new Person();
//實(shí)例化出來的對(duì)象的constructor指向構(gòu)造函數(shù)
person1.constructor == Person; //true
//構(gòu)造函數(shù)的原型對(duì)象上有constructor屬性雾袱,指向構(gòu)造函數(shù)本身
Person.prototype.constructor == Person; //true
//每個(gè)實(shí)例化出來的對(duì)象有__proto__屬性恤筛,指向構(gòu)造該對(duì)象的構(gòu)造函數(shù)的原型對(duì)象,通過該屬性實(shí)現(xiàn)原型鏈
person1.__proto__ == Person.prototype; //true
//person1.__proto__.__proto__就是構(gòu)造函數(shù)原型對(duì)象的__proto__芹橡,其指向構(gòu)造該構(gòu)造函數(shù)的原型對(duì)象毒坛,也就是Object.prototype
person1.__proto__.__proto__ == Person.prototype.__proto__; //true
Person.prototype.__proto__ == Object.prototype; // true
//最終Object.prototype.__proto__指向null,所以說Object.prototype就是原型鏈的頂端,不存在構(gòu)造Object的構(gòu)造函數(shù)的原型對(duì)象
Object.prototype.__proto__ == null; // true
那么使用原型的方式構(gòu)造函數(shù)就沒有缺點(diǎn)了嗎林说,不是的煎殷,當(dāng)我們?cè)谠蛯?duì)象上定義包含引用類型的值的時(shí)候,就會(huì)出現(xiàn)問題
function Person() {}
Person.prototype.friends = ['zcy', 'zcy2'];
var person1 = new Person();
var person2 = new Person();
person1.friends.push('zcy3');
console.log(person2.friends);//['zcy', 'zcy2', 'zcy3']腿箩;
person1.friends == person2.friends; //true
當(dāng)我們?cè)谠蜕隙x一個(gè)數(shù)組豪直,并且實(shí)例化兩個(gè)對(duì)象時(shí),這個(gè)兩個(gè)對(duì)象的friends屬性指向同一個(gè)數(shù)組的引用珠移,當(dāng)修改其中數(shù)組的值會(huì)影響到另外一個(gè)弓乙;
為了解決這個(gè)問題,我們可以組合使用構(gòu)造函數(shù)模式和原型模式
function Person() {
this.friends = ['zcy1', 'zcy2'];
}
Person.prototype.name = 'zcy';
var person1 = new Person();
var person2 = new Person();
person1.friends.push('zcy3');
console.log(person2.friends); //['zcy', 'zcy2']
console.log(person1.friends == person2.friends); //false
四钧惧、繼承
1. 原型鏈繼承
使用原型鏈實(shí)現(xiàn)繼承的基本思想是讓一個(gè)引用類型繼承另一個(gè)引用類型的屬性和方法;
要實(shí)現(xiàn)原型鏈繼承就是讓一個(gè)構(gòu)造函數(shù)的原型屬性指向另一個(gè)構(gòu)造函數(shù)的實(shí)例對(duì)象
Children.prototype = new Parent()暇韧;
原型鏈繼承就是子構(gòu)造函數(shù)Children()創(chuàng)建出來的實(shí)例對(duì)象也會(huì)繼承父構(gòu)造函數(shù)Parent()的屬性和方法,當(dāng)我們?cè)谕ㄟ^子構(gòu)造函數(shù)Children()構(gòu)造出來的對(duì)象children上訪問一個(gè)屬性時(shí)垢乙,當(dāng)children沒有該屬性锨咙,本應(yīng)該去訪問其構(gòu)造函數(shù)Children()的原型對(duì)象,但是此時(shí)Children原型對(duì)象指向父Parent的實(shí)例追逮,所以先去Parent實(shí)例(parent)上查找,如果parent上也沒有粹舵,會(huì)通過parent.__proto__
去父構(gòu)造函數(shù)的原型對(duì)象Parent.prototype上查找钮孵,所以這就通過原型鏈實(shí)現(xiàn)了繼承;
注意:children.constructor == Parent;//true
眼滤,這是因?yàn)樽訕?gòu)造函數(shù)的原型屬性指向父構(gòu)造函數(shù)的實(shí)例巴席,而這個(gè)實(shí)例的constructor就等于Parent;
//構(gòu)造函數(shù)a
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperTypeValue = function () {
return this.property;
};
//構(gòu)造函數(shù)b
function SubType() {
this.subproperty = false;
}
//讓構(gòu)造函數(shù)b的原型屬性指向構(gòu)造函數(shù)a的實(shí)例
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subpropertys;
};
//通過構(gòu)造函數(shù)b構(gòu)造出來的對(duì)象上有構(gòu)造函數(shù)a的方法诅需,這就實(shí)現(xiàn)了繼承
var instance = new SubType();
instance.getSuperTypeValue(); //true
instance.__proto__ == SubType.prototype; //true
SubType.prototype.__proto__ == SuperType.prototype; //true
instance.constructor == SuperType; //true
注意:給原型添加方法的代碼一定要放在替換原型語句之后
SubType.prototype = new SuperType();
SubType.prototype.aaa = 'aaa';
不能使用對(duì)象字面量創(chuàng)建原型方法漾唉,這樣做就會(huì)重寫原型鏈
SubType.prototype = new SuperType();
SubType.prototype = {
getSubValue: function () {},
};
原型鏈繼承存在的問題:
- 使用原型鏈?zhǔn)降睦^承無法實(shí)現(xiàn)多繼承荧库,一個(gè)子構(gòu)造函數(shù)沒辦法同時(shí)繼承兩個(gè)父構(gòu)造函數(shù)(一般在js開發(fā)中多繼承用的不多)
- 使用原型鏈?zhǔn)降睦^承和使用原型模式給構(gòu)造函數(shù)添加屬性相同,當(dāng)我們?cè)谠玩溕咸砑右粋€(gè)帶引用類型的屬性時(shí)赵刑,比如在父類上有個(gè)數(shù)組分衫,通過父類繼承了一個(gè)子類,子類實(shí)例化的對(duì)象給數(shù)組添加元素般此,再用子類實(shí)例化一個(gè)對(duì)象2蚪战,對(duì)象2的數(shù)組會(huì)有剛剛新添加那條數(shù)據(jù);
- 還有就是原型鏈繼承沒辦法向父構(gòu)造函數(shù)中傳遞參數(shù)
2. 借用構(gòu)造函數(shù)繼承
借用構(gòu)造函數(shù)繼承的思想式铐懊,在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用父類型的構(gòu)造函數(shù)邀桑,并通過apply()或者call()改變this的指向,將子類的this指向父類構(gòu)造函數(shù)的函數(shù)體內(nèi)科乎,這樣就能實(shí)現(xiàn)向父類構(gòu)造函數(shù)傳遞參數(shù)并且在子類中執(zhí)行了父類的代碼之后在子類的每個(gè)實(shí)例都會(huì)具有自己的屬性副本壁畸;
function Father(name) {
this.name = name;
this.color = ['1', '2', '3'];
}
function Children() {
//繼承了父類屬性,并且還傳遞了參數(shù)
Father.call(this, 'zcy');
//實(shí)例化自身屬性
this.age = 22;
}
var children1 = new Children();
children1.color.push('4');
console.log(children1.name); //zcy
console.log(children1.age); //22
var children2 = new Children();
console.log(children2.color); // ['1', '2', '3']
借用構(gòu)造函數(shù)繼承可以實(shí)現(xiàn)多繼承茅茂,并且解決了共享的問題瓤摧,每次在子類中調(diào)用Father在內(nèi)存中分配的地址都不同;
存在的問題:
- 子類只能繼承父類構(gòu)造函數(shù)內(nèi)的方法和屬性玉吁,無法繼承父類原型對(duì)象上的屬性和方法照弥;
- 和構(gòu)造函數(shù)模式問題相同,屬性和方法沒辦法復(fù)用进副,每次都會(huì)構(gòu)造出新的對(duì)象这揣,浪費(fèi)內(nèi)存
3.組合式繼承
組合式繼承有時(shí)候也叫偽經(jīng)典繼承,指的是將原型鏈繼承和借用構(gòu)造函數(shù)繼承的思想整合到一起影斑,使用原型鏈繼承實(shí)現(xiàn)對(duì)屬性和方法的繼承给赞,通過借用構(gòu)造函數(shù)模式來實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承,這樣既可以通過在原型上定義方法實(shí)現(xiàn)了函數(shù)的復(fù)用矫户,又能保證每個(gè)實(shí)例都有它自己的屬性片迅,指向不同的內(nèi)存空間,互不影響
function Father(name) {
this.name = name;
this.color = ['1', '2', '3'];
}
Father.prototype.sayName = function () {
return this.name;
};
function Children(name, age) {
//繼承了父類屬性皆辽,并且還傳遞了參數(shù)
Father.call(this, name);
//實(shí)例化自身屬性
this.age = age;
}
//將子類的原型屬性指向父類的實(shí)例
Children.prototype = new Father();
//并且讓子類原型上的constructor指向子類自身
Children.prototype.constructor = Children;
Children.prototype.sayAge = function () {
return this.age;
};
var children1 = new Children('zcy', '22');
children1.color.push('4');
console.log(children1.color); //['1', '2', '3', '4']
console.log(children1.sayAge()); //22
console.log(children1.sayName()); //zcy
var children2 = new Children('vvv', '23');
console.log(children2.color); //['1', '2', '3'] 實(shí)例不受影響
console.log(children2.sayAge()); //23
console.log(children2.sayName()); //vvv
可以實(shí)現(xiàn)多繼承柑蛇,可以實(shí)現(xiàn)傳參
實(shí)現(xiàn)繼承時(shí)為何總是要修正constructor的指向呢?
constructor其實(shí)沒有什么用處驱闷,只是JavaScript語言設(shè)計(jì)的歷史遺留物耻台。由于constructor屬性是可以變更的,所以未必真的指向?qū)ο蟮臉?gòu)造函數(shù)空另,只是一個(gè)提示盆耽。不過,從編程習(xí)慣上,我們應(yīng)該盡量讓對(duì)象的constructor指向其構(gòu)造函數(shù)摄杂,以維持這個(gè)慣例坝咐。
這個(gè)方式的缺點(diǎn):
- 父類的構(gòu)造函數(shù)在這種繼承下被調(diào)用了兩次,第一次是在子類的構(gòu)造函數(shù)當(dāng)中析恢,使用call或者apply的時(shí)候墨坚,第二次調(diào)用在設(shè)置原型的時(shí)候,子.prototype = new 父()氮昧;假如父類的構(gòu)造函數(shù)很消耗性能框杜,那么整個(gè)繼承的性能就行下降
- 子類構(gòu)造出來的實(shí)例,它的屬性在實(shí)例中和原型中各存在一份袖肥,占用多余內(nèi)存
4.原型繼承
原型繼承的思想是借助原型可以基于已有的對(duì)象創(chuàng)建新對(duì)象咪辱,同時(shí)還不必因此創(chuàng)建自定義類型
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
在object()函數(shù)內(nèi)部,先創(chuàng)建一個(gè)臨時(shí)性的構(gòu)造函數(shù)椎组,然后將傳入的對(duì)象作為這個(gè)構(gòu)造函數(shù)的原型油狂,最后返回這個(gè)臨時(shí)構(gòu)造函數(shù)的實(shí)例,從本質(zhì)上講寸癌,object()函數(shù)對(duì)傳入的對(duì)象執(zhí)行了一次淺復(fù)制专筷;
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var person = {
name: 'zcy',
friends: ['1', '2', '3'],
};
//在通過object()函數(shù)返回的F()構(gòu)造函數(shù)的實(shí)例中,它的__proto__屬性指向傳入的對(duì)象
var children1 = object(person);
children1.__prpto__ == person; //true
children1.name = 'Grge';
children1.friends.push('4');
var children2 = object(person);
children2.__prpto__ == person; //true
children2.friends.push('5');
console.log(children2.name); //zcy 繼承自person
console.log(person.friends); // ['1', '2', '3', '4', '5'] 共享屬性
所以就實(shí)現(xiàn)了新對(duì)象繼承person對(duì)象蒸苇,不過包含引用類型的屬性始終都會(huì)共享相應(yīng)的值磷蛹,就像使用原型模式一樣
ES5提供的Object.create()方法規(guī)范化了原型繼承模式,該方法接受兩個(gè)參數(shù)
- 一個(gè)用作新對(duì)象原型的對(duì)象
- 為新對(duì)象定義的格外屬性的對(duì)象
var person = {
name: 'zcy',
friends: ['1', '2', '3'],
};
var children1 = Object.create(person, {
age: {
value: '22',
},
});
children1.__proto__ == person; //true
console.log(children1.age); //22
console.log(children1.friends); //['1', '2', '3']
5溪烤、class繼承
ES6中的Class可以通過extends實(shí)現(xiàn)繼承味咳,這比ES5中通過修改原型鏈實(shí)現(xiàn)繼承要清晰和方便很多;
class Parent {
//constructor方法默認(rèn)返回實(shí)例對(duì)象檬嘀,即this指向被實(shí)例化的對(duì)象
constructor(name, age) {
this.name = name;
this.age = age;
this.friends = ['1', '2', '3'];
}
sayName() {
return this.name + this.age;
}
}
class Children extends Parent {
//子類的constructor里必須調(diào)用super方法槽驶,否則會(huì)報(bào)錯(cuò),this關(guān)鍵字只能在super方法之后使用
constructor(name, age, city) {
//super方法的作用就是讓子類繼承父類的this對(duì)象鸳兽,然后對(duì)其進(jìn)行加工掂铐,子類本身沒有this
super(name, age);
this.city = city;
}
sayName() {
//繼承父類的方法通過super.xxx
return super.sayName() + this.city;
}
}
var children1 = new Children('zcy', 22, 'sz');
children1.sayName(); //zcy22sz
children1.friends.push('4');
var parent1 = new Parent();
console.log(parent1.friends); //['1', '2', '3']
通過子類構(gòu)建出來的對(duì)象既是子類的實(shí)例也是父類的實(shí)例;
大多數(shù)瀏覽器的ES5實(shí)現(xiàn)之中揍异,每一個(gè)對(duì)象都有proto屬性全陨,指向?qū)?yīng)的構(gòu)造函數(shù)的prototype屬性。Class作為構(gòu)造函數(shù)的語法糖蒿秦,同時(shí)有prototype屬性和proto屬性烤镐,因此同時(shí)存在兩條繼承鏈。
- 子類的
__proto__
屬性棍鳖,表示構(gòu)造函數(shù)的繼承,總是指向父類。 - 子類prototype屬性的
__proto__
屬性渡处,表示方法的繼承镜悉,總是指向父類的prototype屬性
Children.__proto__ == Parent
Children.prototype.__proto__ === Parent.prototype
所以就可以實(shí)現(xiàn)上述組合式繼承中子類既可以實(shí)現(xiàn)繼承父類的方法屬性,又可以繼承原型上的方法和屬性
也就是既可以通過在原型上定義方法實(shí)現(xiàn)了函數(shù)的復(fù)用医瘫,又能保證每個(gè)實(shí)例都有它自己的屬性侣肄,指向不同的內(nèi)存空間,互不影響醇份。