與java武通、c++相同霹崎,JavaScript 也是一門面向?qū)ο蟮某绦蛘Z言。對象類型冶忱,是具有一系列相同的特征的事物的高度抽象尾菇,比如說人,每一個人有名字朗和,會說話,會吃飯等簿晓,人就是一種對象類型眶拉。 如何來定義這種對象類型,描述其屬性特征呢憔儿?
傳統(tǒng)方式:通過function關(guān)鍵字來定義一個對象類型
function People(name) {
this.name = name
}
People.prototype.toSay= function () {
alert("我的名字是:" + this.name)
}
People.prototype.toEat= function () {
alert("我吃飯")
}
var p = new People("小明")
p.toSay(); // 我的名字是小明
上面的代碼里剃袍,我們定義People這種類型骇窍,它的屬性特征有name、toSay、toEat 伴挚。然后我們以People為模板new出來一個p的實例對象。剛接觸js時缩滨,可能會疑惑呐萨,function聲明的不是函數(shù)么,怎么又變成定義對象類型底哥?prototype是什么咙鞍?
其實在js中,函數(shù)本身也是一個對象趾徽。這種對象有點特殊续滋,它的作用是定義了對象類型,可以說是數(shù)據(jù)結(jié)構(gòu)模板孵奶。 而prototype是它的一個屬性疲酌,稱為對象原型,其本質(zhì)也是一個對象了袁,包含constructor和其他屬性成員朗恳。constructor默認指向自身構(gòu)造函數(shù)。
所以聲明People的時候载绿,程序自動People對象添加了prototype屬性僻肖,并且讓prototype.constructor指向了People,即函數(shù)本身卢鹦。所以上面的例子等同于下面的寫法:
function People(name) {
this.name = name
}
var proto = {
constructor : People,
toSay: function (name) {
alert("我的名字是:" + name)
},
toEat: function() {
alert("我吃飯")
}
}
People.prototype = proto // 指定People的Prototype屬性
prototype的作用:當我們new一個實例對象p時臀脏,程序根據(jù)對象類型People的原型prototype劝堪,將原型所定義的屬性(constructor除外)復(fù)制給新的實例對象p,并執(zhí)行了一次prototype.constructor 所指向的構(gòu)造函數(shù)揉稚,對實例對象p進行初始化秒啦。
實例對象p有兩種屬性:實例屬性、原型屬性
實例屬性: 構(gòu)造方法里定義的
原型屬性: 在原型prototype里定義
hasOwnProperty方法可以幫我們區(qū)分
p.hasOwnProperty("name"); // true
p.hasOwnProperty("toSay"); // false,因為這個屬性是原型上定義的
問題1:為什么我們不直接都在構(gòu)造函數(shù)里面定義呢搀玖?
function People(name) {
this.name = name
this.toSay = function() {
alert("我的名字是:" + this.name)
}
this.toEat = function() {
alert("我吃飯")
}
}
答: 這個主要考慮內(nèi)存管理余境,因為函數(shù)是內(nèi)存中的一個對象,也就是說灌诅,toSay或toEat都是對象占有一定內(nèi)存芳来。寫在構(gòu)造函數(shù)里面,每new一個實例對象猜拾,都會執(zhí)行一次構(gòu)造函數(shù)即舌,都會重新創(chuàng)建一個函數(shù)對象,賦給新的實例對象的屬性上挎袜。結(jié)果就是每一個實例對象的toSay或toEat屬性都對應(yīng)各自的函數(shù)對象顽聂,而這些函數(shù)功能都是一樣的,我們創(chuàng)建了一大堆重復(fù)的函數(shù)對象盯仪。使用prototype不會紊搪,因為大家共享一個prototype對象。
問題2: 為什么name不是直接定義在原型prototype上呢全景?
答:每個人名字不同耀石,如果定義在prototype上,大家名字就一樣了爸黄,其中一個改變了name值娶牌,都會影響到其他實例對象。
注意:實例對象是沒有prototype屬性馆纳,所以你不可以用實例對象為模板new一個新的實例對象來诗良,只能用函數(shù)對象為模板來創(chuàng)建。
var p1 = new People(''小明")鲁驶; // 正確鉴裹,函數(shù)對象的prototype的constructor指定構(gòu)造方法
var p2 = new p1("小王") ; // error ,實例對象沒有prototype钥弯,找不到構(gòu)造方法
各大瀏覽器廠商給實例對象實現(xiàn)了一個 __proto__ 屬性径荔,指向?qū)ο笤停覀兎Q為實例對象的隱式原型脆霎,即:
var p1 = new People("小明")
p1.__proto__ === People.prototype // true
但我們要避免使用這個屬性总处, 這個屬性作用我猜測是瀏覽器提供給我們方便調(diào)試的時候用的。
問題1:People.prototype是一個對象睛蛛,這個對象是什么鹦马?
答:Object胧谈, js所有對象默認繼承js內(nèi)置對象Object。
問題2: js中荸频,怎么實現(xiàn)對象的繼承菱肖?
答:js的繼承是通過對象原型prototype來實現(xiàn)的。
// 父類型
function Animal(name) {
this.name= name
this.hasFoot = true
this.color = ["orange", ''black"]
}
Animal.prototype = {
constructor: Animal,
voice: function(word) {
console.info(word)
}
}
// 子類型 Cat
function Cat() {}
Cat.prototype = new Animal("cat"); // Cat.prototype.constructor是Animal
Cat.prototype.constructor = Cat; // 我們將構(gòu)造函數(shù)指定回來旭从,因為我們可以在構(gòu)造擴展其它屬性
上面的代碼稳强,我們就實現(xiàn)了Cat的對象類型是繼承了Animal對象類型,所以我們可以看到:
var cat1 = new Cat()
cat1.hasFoot // true
cat1.color // ["orange","black"]
cat1.toString // function toString() { [native code] }
hasFoot和悦、與footNum都是從父類型annimal繼承過來的退疫,而toString為什么有呢,其實是這樣鸽素,Cat繼承了Animal褒繁,而Animal默認繼承了Object,所以當我們找cat1的toString屬性是付鹿,發(fā)現(xiàn)自身實例屬性沒有澜汤,發(fā)現(xiàn)原型上也沒有定義蚜迅,那程序就會尋所繼承的父對象的實例屬性舵匾,父對象的原型屬性,這樣一步步找下去谁不,這就是JS的原型鏈坐梯。所以就是:
cat1.__proto__ === Cat.prototype // true
cat1.__proto__.__proto__ === Animal.prototype // true ,因為Cat.prototype是一個Animal的實例對象
上面的程序設(shè)計存在一個問題,有的貓只有一種顏色刹帕,有貓身上的顏色有三種吵血,橘、白偷溺、黑蹋辅。顯然從Animal繼承過來的顏色只有不能滿足這種情況
var cat2 =new Cat()
cat2.color.push("white");
cat1.color // ["orange", "black", "white"]
//原因是因為,color是來自Cat.prototype挫掏,cat1和cat2共享一個prototype侦另,你改變了cat2,cat1的color原型屬性就會受到影響
面對這種情況尉共,我們的Cat對象類型應(yīng)該這么寫:
function Cat() {
Animal.call(this) // 這樣就可以將原型的實例屬性變成自身的實例屬性
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
var cat1 = new Cat()
var cat2 = new Cat()
cat1.color === cat2.color // false
雖然的方式解決了問題褒傅,但是還是有個缺點,調(diào)用了兩次Animal構(gòu)造函數(shù)袄友。第一次是指定Cat.prototype殿托,第二次是Cat自身構(gòu)造函數(shù)中主動調(diào)用。我們更想要的是剧蚣,指定了prototype支竹,new實例時旋廷,構(gòu)造函數(shù)就不要再調(diào)用Animal()了。此時我們需要一個工具來完成
// 工具extend
function extend(super, suber) {
var proto = Object.create(super.prototype)
proto.constructor = suber
suber.prototype = proto
}
function Cat() {
Animal.call(this);
}
extend(Animal, Cat)
上面的這種方式唾戚,將指定Cat.prototype從通過new Animal()換成直接Object.creat(Animal.prototype),這樣就避免了Animal() 構(gòu)造函數(shù)的執(zhí)行柳洋。
實際上,這方式是最高效的方式叹坦。
對比其他面向?qū)ο箝_發(fā)語言(如: java)熊镣,js通過function定義對象類型,容易讓人不理解募书。ES6新規(guī)范推出class和extends關(guān)鍵字來實現(xiàn)面向?qū)ο缶幊獭?br> ES6方式:用class關(guān)鍵字定義對象類型绪囱,用extends關(guān)鍵字實現(xiàn)繼承
const private2 = Symbol('I am symbol value')
class A {
a1 = '1' // ES7 實例屬性,需要new實例來訪問, ES6規(guī)定class沒有靜態(tài)屬性莹捡,只有靜態(tài)方法所以只能在constructor中定義屬性
static a2 = '2' // ES7的靜態(tài)屬性鬼吵,直接 A.a2 訪問,不需要new實例
getA1() {
return this.a1 // this指向new實例
}
static getA2() {
return ‘2’ // 靜態(tài)方法
}
constructor(name) {
//一定要有構(gòu)造方法篮赢,如果沒有默認生成空構(gòu)造方法
this.a3 = '3' // 這里定義實例屬性
this.name = name
}
// 私有方法寫法
publicMethod() {
private1() // 私有方法1齿椅,可以寫在class體外
private2() // 利用Symbol值來定義
}
[private2]() {
// 這里是私有方法
}
}
const private1 = function() { // 這里也是私有方法,但別export出去}
// 最后export class
export default A
class關(guān)鍵字會讓我們更清晰設(shè)計一個對象類型启泣,實際上,這只是語法糖:
- A 的實質(zhì)還是一個function
- 對屬性的定義是實例屬性涣脚,而對方法的定義是定義在原型上
// 通過extends繼承
class B extends A{
constructor() {
// 一定要在構(gòu)造函數(shù)的第一句調(diào)用super
super() // 這是調(diào)用父類的構(gòu)造方法
this.b1 = '11'
this.b2 = super.a1 // super直接調(diào)用時指向父類構(gòu)造方法,范圍屬性時寥茫,指向父類實例遣蚀,或調(diào)用父類靜態(tài)方法
}
}
我們可以知道,實際上A纱耻、B都是兩個對象類型芭梯,B繼承A。ES6的class作為語法糖也提供了prototype和proto兩個屬性弄喘;
let instanceA = new A()
let instanceB = new B()
A.prototype // Object
instanceA.__proto__ //即A.prototype 還是Object
B.prototype // A的實例對象玖喘,并且constructor指定為ClassB
instanceB.__proto__ //B.prototype
instanceB.__proto__.__proto__ // 即A.prototype ,即Object
// es6提供對class 的__proto__的訪問
A.__proto__ // A本質(zhì)是函數(shù),函數(shù)也是對象蘑志,A是Object的實例累奈,實例對象__proto__是對象類型的原型,這里是 [native code]
B.__proto__ // B繼承A卖漫,B.prototype是A的實例费尽,B是A的實例,所以B.__proto__ === A.prototype
對于class本來就是讓我們能夠避開傳統(tǒng)function的不容易理解的語義羊始,我們實際中盡量不要去使用proto,很多時候把自己給繞暈了旱幼。另外,這里補充一句:
class內(nèi)部定義的變量是不能存在變量提升的突委,也就是說你用了var也是不存在變量提升柏卤。因為他是一個語法糖冬三,我們new一個實例時才會走進構(gòu)造函數(shù)棧,執(zhí)行完后缘缚,當前棧被銷毀勾笆,而里面返回的值賦給了實例的屬性,而里面的變量標記會被清除掉桥滨,因此不存在變量提升窝爪。