1. 創(chuàng)建對象的三個(gè)方法
創(chuàng)建一個(gè)對象一般有三種方法:
- 字面量創(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í)會做四件事情:
- 在內(nèi)存中創(chuàng)建一個(gè)新的空對象。
- 讓 this 指向這個(gè)對象。
- 執(zhí)行構(gòu)造函數(shù)的代碼。
- 返回這個(gè)對象(所以構(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)用相同的方法,會造成內(nèi)存浪費(fèi)
上面代碼可以看出:
靜態(tài)成員只能使用構(gòu)造函數(shù)調(diào)用,不能通過實(shí)例調(diào)用。
多個(gè)實(shí)例調(diào)用同樣的實(shí)例成員函數(shù),會各自存儲一份,造成內(nèi)存浪費(fèi)。
那么,如何解決呢?
3. 原型和原型鏈
構(gòu)造函數(shù)存在內(nèi)存浪費(fèi)的情況,可以通過原型對象上定義屬性、方法解決。構(gòu)造函數(shù)原型上的方法,能夠被構(gòu)造函數(shù)實(shí)例調(diào)用。
每一個(gè)構(gòu)造函數(shù)都有一個(gè) prototype 屬性,指向一個(gè)對象。這個(gè)對象的所有屬性和方法都會被構(gòu)造函數(shù)所擁有。
我們可以把那些不變的方法直接定義到 prototype 上,這樣所有的實(shí)例都可以共享這些方法。
代碼如下:
Person.prototype.say = function () {
console.log('我在說話');
};
jack.say();
lily.say();
console.log(jack.say === lily.say);
每個(gè)對象都會有一個(gè)__proto__屬性,指向其構(gòu)造函數(shù)的原型對象。也就是說,對象的__proto__屬性和構(gòu)造函數(shù)的 prototype 屬性是等價(jià)的。
實(shí)例對象和原型對象都有一個(gè)constructor屬性,指向構(gòu)造函數(shù)本身。
我們調(diào)用一個(gè)函數(shù)的屬性時(shí),編譯器會先看對象本身是否有這個(gè)屬性,如果沒有就到對象的__proto__屬性上去找,如果還找不到,繼續(xù)找__proto__的__proto__屬性,直到 null 為止。
把構(gòu)造函數(shù)、實(shí)例對象、原型對象的關(guān)系用圖畫出來,如下:

這就是原型鏈。
其實(shí)構(gòu)造函數(shù)是Function的實(shí)例,Function的原型是一個(gè)對象,是Object的實(shí)例。我們繼續(xù)拓展,把圖畫下來。如下:

4. 使用原型擴(kuò)展內(nèi)置對象方法
我們可以使用原型來擴(kuò)展內(nèi)置對象的方法。比如,我們可以這樣擴(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è)對象。
}
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ù)繼承并不能繼承原型鏈上的方法。那我們怎么才能繼承父構(gòu)造函數(shù)原型上的方法呢?
我們可以把子構(gòu)造函數(shù)原型指向父構(gòu)造函數(shù)的原型。
- 原型鏈繼承
Student.prototype = Person.prototype; // - 原始版 缺陷:Student的原型改變導(dǎo)致Person原型改變,因此不可取
原型一樣,自然能夠繼承原型上的方法。
但是這樣一來就會出現(xiàn)問題,原型是個(gè)對象,是引用類型數(shù)據(jù),我們再修改Student.prototype會導(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í),編譯器會先看對象本身是否有這個(gè)屬性,如果沒有就到對象的__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ù),都會生成一個(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è)對象,這個(gè)對象的__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里面的類。探究一下會發(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