1. 創(chuàng)建對(duì)象的三個(gè)方法
創(chuàng)建一個(gè)對(duì)象一般有三種方法:
- 字面量創(chuàng)建窑业,
var obj = {};
- 通過Object創(chuàng)建,
var obj = new Object();
- 通過構(gòu)造函數(shù)創(chuàng)建:
function Person(name, age) {
this.name = name;
this.age = age;
}
const jack = new Person('Jack', 18);
上面代碼中的 new 在執(zhí)行時(shí)會(huì)做四件事情:
- 在內(nèi)存中創(chuàng)建一個(gè)新的空對(duì)象枕屉。
- 讓 this 指向這個(gè)對(duì)象常柄。
- 執(zhí)行構(gòu)造函數(shù)的代碼。
- 返回這個(gè)對(duì)象(所以構(gòu)造函數(shù)不需要 return)。
2. 靜態(tài)成員與實(shí)例成員
function Person(name, age) {
this.name = name; // 實(shí)例成員
this.age = age;
this.sing = function () {
console.log('我在唱歌');
};
}
Person.height = 180; // 靜態(tài)成員
const jack = new Person('Jack', 22);
const lily = new Person('Lily', 22);
console.log(Person.height); // 180
console.log(jack.height); // undefined
console.log(jack.sing === lily.sing); // false 這里可以看出西潘,多個(gè)實(shí)例調(diào)用相同的方法卷玉,會(huì)造成內(nèi)存浪費(fèi)
上面代碼可以看出:
靜態(tài)成員只能使用構(gòu)造函數(shù)調(diào)用,不能通過實(shí)例調(diào)用喷市。
多個(gè)實(shí)例調(diào)用同樣的實(shí)例成員函數(shù)相种,會(huì)各自存儲(chǔ)一份,造成內(nèi)存浪費(fèi)品姓。
那么寝并,如何解決呢?
3. 原型和原型鏈
構(gòu)造函數(shù)存在內(nèi)存浪費(fèi)的情況腹备,可以通過原型對(duì)象上定義屬性衬潦、方法解決。構(gòu)造函數(shù)原型上的方法馏谨,能夠被構(gòu)造函數(shù)實(shí)例調(diào)用。
每一個(gè)構(gòu)造函數(shù)都有一個(gè) prototype
屬性附迷,指向一個(gè)對(duì)象惧互。這個(gè)對(duì)象的所有屬性和方法都會(huì)被構(gòu)造函數(shù)所擁有。
我們可以把那些不變的方法直接定義到 prototype 上喇伯,這樣所有的實(shí)例都可以共享這些方法喊儡。
代碼如下:
Person.prototype.say = function () {
console.log('我在說話');
};
jack.say();
lily.say();
console.log(jack.say === lily.say);
每個(gè)對(duì)象都會(huì)有一個(gè)__proto__
屬性,指向其構(gòu)造函數(shù)的原型對(duì)象稻据。也就是說艾猜,對(duì)象的__proto__
屬性和構(gòu)造函數(shù)的 prototype
屬性是等價(jià)的。
實(shí)例對(duì)象和原型對(duì)象都有一個(gè)constructor
屬性捻悯,指向構(gòu)造函數(shù)本身匆赃。
我們調(diào)用一個(gè)函數(shù)的屬性時(shí),編譯器會(huì)先看對(duì)象本身是否有這個(gè)屬性今缚,如果沒有就到對(duì)象的__proto__
屬性上去找算柳,如果還找不到,繼續(xù)找__proto__
的__proto__
屬性姓言,直到 null 為止瞬项。
把構(gòu)造函數(shù)、實(shí)例對(duì)象何荚、原型對(duì)象的關(guān)系用圖畫出來囱淋,如下:
這就是原型鏈。
其實(shí)構(gòu)造函數(shù)是Function
的實(shí)例餐塘,Function
的原型是一個(gè)對(duì)象妥衣,是Object
的實(shí)例。我們繼續(xù)拓展,把圖畫下來称鳞。如下:
4. 使用原型擴(kuò)展內(nèi)置對(duì)象方法
我們可以使用原型來擴(kuò)展內(nèi)置對(duì)象的方法涮较。比如,我們可以這樣擴(kuò)展數(shù)組的內(nèi)置方法:
Array.prototype.sum = function () {
let sum = 0;
for (let i = 0; i < this.length; i++) {
sum += this[i]; // prototype 里的 this 指向調(diào)用原型方法的那個(gè)對(duì)象冈止。
}
return sum;
};
console.log([2, 3, 4].sum()); // 9
5. 繼承
- 構(gòu)造函數(shù)繼承:只能繼承構(gòu)造函數(shù)里的屬性和方法
其實(shí)就是在子構(gòu)造函數(shù)里面調(diào)用父構(gòu)造函數(shù)(需要修改this指向)狂票,這樣就把父構(gòu)造函數(shù)里的屬性和方法放到了子構(gòu)造函數(shù)里了。
- 構(gòu)造函數(shù)繼承:只能繼承構(gòu)造函數(shù)里的屬性和方法
function Person(name, age) {
this.name = name;
this.age = age;
this.say = function () {
console.log('你好');
};
}
Person.prototype.sing = function () {
console.log('唱歌');
};
function Student(name, age, score) {
Person.call(this, name, age); // 由于Person里的this指向?yàn)镻erson的實(shí)例熙暴,這里修改this指向?yàn)镾tudent的實(shí)例
this.score = score;
}
const jack = new Student('Jack', 18, 100);
console.log(jack.name); // Jack
jack.say(); // 你好
jack.sing(); // 無結(jié)果闺属,Student沒有繼承Person原型上的方法
- 原型鏈繼承
通過上面的例子我們可以看出,構(gòu)造函數(shù)繼承并不能繼承原型鏈上的方法周霉。那我們?cè)趺床拍芾^承父構(gòu)造函數(shù)原型上的方法呢掂器?
我們可以把子構(gòu)造函數(shù)原型指向父構(gòu)造函數(shù)的原型。
- 原型鏈繼承
Student.prototype = Person.prototype; // - 原始版 缺陷:Student的原型改變導(dǎo)致Person原型改變俱箱,因此不可取
原型一樣国瓮,自然能夠繼承原型上的方法。
但是這樣一來就會(huì)出現(xiàn)問題狞谱,原型是個(gè)對(duì)象乃摹,是引用類型數(shù)據(jù),我們?cè)?strong>修改Student.prototype
會(huì)導(dǎo)致Person.prototype
的變化跟衅。這是不合理的孵睬。比如:
Student.prototype.dance = function () {
console.log('跳舞');
};
const jack = new Person('Jack', 18);
jack.dance(); // 跳舞 Person的原型上是沒有dance方法的。這里是因?yàn)槲覀償U(kuò)展了Student的原型導(dǎo)致Person原型變化
常用的解決辦法是伶跷,我們將子構(gòu)造函數(shù)的原型指向福構(gòu)造函數(shù)的一個(gè)實(shí)例掰读。
Student.prototype = new Person();
前面說過,調(diào)用一個(gè)函數(shù)的屬性時(shí)叭莫,編譯器會(huì)先看對(duì)象本身是否有這個(gè)屬性蹈集,如果沒有就到對(duì)象的__proto__
屬性上去找,如果還找不到雇初,繼續(xù)找__proto__
的__proto__
屬性雾狈,直到 null 為止。這樣一來抵皱,Student實(shí)例就能夠調(diào)用Person實(shí)例的方法善榛,從而調(diào)用Person原型的方法。
- 組合繼承
把構(gòu)造函數(shù)繼承和組合繼承結(jié)合起來就是組合繼承了呻畸。整體代碼如下:
- 組合繼承
function Person(name, age) {
this.name = name;
this.age = age;
this.say = function () {
console.log('你好');
};
}
Person.prototype.sing = function () {
console.log('唱歌');
};
function Student(name, age, score) {
Person.call(this, name, age);
this.score = score;
}
Student.prototype = new Person();
Student.prototype.constructor = Student; // 修改constructor指向
組合繼承調(diào)用了兩次父構(gòu)造函數(shù)移盆,且每創(chuàng)建一個(gè)子構(gòu)造函數(shù),都會(huì)生成一個(gè)父構(gòu)造函數(shù)實(shí)例伤为。還是不夠完美咒循。
- 寄生組合繼承
function Person(name, age) {
this.name = name;
this.age = age;
this.say = function () {
console.log('你好');
};
}
Person.prototype.sing = function () {
console.log('唱歌');
};
function Student(name, age, score) {
Person.call(this, name, age);
this.score = score;
}
Student.prototype = Object.create(Person.prototype); // Object.create(proto)創(chuàng)建一個(gè)對(duì)象据途,這個(gè)對(duì)象的__proto__屬性為proto
Student.prototype.constructor = Student; // 修改constructor指向
Student實(shí)例能夠調(diào)用Student原型上的方法,而Student原型又可以通過__proto__
獲取Person原型上的方法叙甸。這樣就實(shí)現(xiàn)了Student繼承Person原型上的方法颖医。
也可以自己寫一個(gè)類似 Object.create(proto)
的方法。
function objectCreate(proto) {
function Temp() {}
Temp.prototype = proto;
return new Temp();
}
Student.prototype = objectCreate(Person.prototype);
Student.prototype.constructor = Student; // 修改constructor指向
6. 類的本質(zhì)
當(dāng)然裆蒸,要實(shí)現(xiàn)繼承最好寫最好用的還是ES6里面的類熔萧。探究一下會(huì)發(fā)現(xiàn):
- 類的本質(zhì)還是函數(shù)。
- 類也有prototype屬性
- 類創(chuàng)建的實(shí)例僚祷,也有__proto__屬性佛致,且指向類的 prototype 屬性
- 類創(chuàng)建的實(shí)例的constructor指向類本身
- 類的prototype的constructor也是指向類本身
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
say() {
console.log('說話');
}
}
const jack = new Person('Jack', 18);
console.log(typeof Person); // function
console.log(Person.prototype); // {constructor: ?, say: ?}
console.log(jack.__proto__ === Person.prototype); // true
console.log(jack.constructor); // class Person{ ... }
console.log(Person.prototype.constructor); // class Person{ ... }
說白了,ES6 的類就是ES5繼承的語法糖辙谜。
參考資料:
傳智教育 - JavaScript進(jìn)階面向?qū)ο驟S6