與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的作用:當(dāng)我們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ū)ο笤?,我們稱為實例對象的隱式原型,即:
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,所以當(dāng)我們找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í)行完后,當(dāng)前棧被銷毀,而里面返回的值賦給了實例的屬性,而里面的變量標記會被清除掉,因此不存在變量提升。