“约见”面试官系列之常见面试题第四十二篇之原型和原型链(建议收藏)
原型和原型鏈的理解:(面試題)
- 原型:每個函數(shù)都有 prototype 屬性,該屬性指向原型對象;使用原型對象的好處是所有對象實例共享它所包含的屬性和方法。
- 原型鏈:主要解決了繼承的問題;每個對象都擁有一個原型對象,通過__proto__ 指針指向其原型對象,并從中繼承方法和屬性,同時原型對象也可能擁有原型,這樣一層一層,最終指向 null。
原型的作用:
1.數(shù)據(jù)共享 節(jié)約內(nèi)存內(nèi)存空間
2.實現(xiàn)繼承
注意:函數(shù)也是一個對象,對象不一定是函數(shù)。(對象有__proto__屬性,函數(shù)有prototype屬性)此處說明,方便大家理解下文。
下面我將舉例說明為什么要使用原型
例1:
每次使用構(gòu)造函數(shù)Person()實例化出對象的時候,就會給每個實例對象的eat()方法和sleep()方法開辟空間。可是當(dāng)實例化許多對象的時候,就會浪費(fèi)大量的空間,因為每個實例對象的eat()方法和sleep()的功能都是一樣的,所以我們沒必要為每個實例對象添加eat()方法和sleep()方法。
這時原型就派上用場了,看下面經(jīng)過改造的例子:
?
eat()方法和sleep()被添加到了Person()構(gòu)造函數(shù)的原型(prototype)上了。因此只有一份eat()方法和sleep()方法。當(dāng)實例對象需要使用這些方法的時候就在自己的__proto__屬性中找到并調(diào)用這些方法實現(xiàn)相應(yīng)的功能。
現(xiàn)在我們來捋一下構(gòu)造函數(shù),實例對象,原型對象之間的關(guān)系。
如以下代碼和圖所示:
Person()構(gòu)造函數(shù)的prototype屬性是一個對象,實例對象p1的__proto__屬性也是一個對象,并且prototype對象和__proto__對象的指向相同。那么我們再回過頭來理解一下為什么添加到原型的方法可以是共享的。因為prototype對象和__proto__對象的指向相同,所以將eat()方法和sleep()添加到Person()構(gòu)造函數(shù)的prototype屬性上之后,實例對象就可以通過自己__proto__屬性去訪問eat()方法和sleep()了。
console.dir(Person);console.dir(p1);console.log(typeof p1.__proto__);//objectconsole.log(typeof Person.prototype);//objectconsole.log(p1.__proto__ === Person.prototype);//true__proto__指向該對象所在的構(gòu)造函數(shù)的原型對象。
實例對象和構(gòu)造函數(shù)之間沒用直接的關(guān)系。原型對象與實例對象之間用原型(__proto__)關(guān)聯(lián),這種關(guān)系叫做原型鏈。
我是這樣理解原型鏈的(可能不是很準(zhǔn)確)我向我爸要零花錢,我爸也沒有錢,那么我就向我奶奶要,奶奶要是也沒有,就繼續(xù)找別人要。
那么原型的指向可以改變嗎?答案是可以的。
舉個例子:
?
function Person(name) {
this.name=name;
}
Person.prototype.eat=function () {
console.log(this.name+"吃東西");
};
Person.prototype.sleep=function () {
console.log(this.name+"睡覺");
}
function Student(school) {
this.school=school;
}
Student.prototype.write=function () {
console.log("寫作業(yè)");
}
Student.prototype=new Person("小華");//改變Student()構(gòu)造函數(shù)的指向,讓Student()構(gòu)造函數(shù)的原型對象指向Person的實例對象
var s1=new Student("某某高中");
s1.eat();//小華吃東西
s1.sleep();//小華睡覺
s1.write();//Uncaught TypeError: s1.write is not a function,因為Student()的原型的指向改變,所以找不到write()方法
console.dir(Student);
console.dir(s1);
__proto__指向該對象所在的構(gòu)造函數(shù)的原型對象。如上圖所示:Studend()構(gòu)造函數(shù)的原型(prototype)指向了Person()的實例對象(new Person("小華")),所以Studend()的實例對象s1的__proto__也指向了Person()的實例對象((new Person("小華"))。而實例對象((new Person("小華"))的__proto__指向了其所在的構(gòu)造函數(shù)Person()的原型對象在這個原型對象中,找到了eat()方法和sleep()方法。
從這個例子中,可以發(fā)現(xiàn),利用原型可以實現(xiàn)繼承。面向?qū)ο蟮木幊陶Z言中有(class)類的概念,但是JavaScript不是面向?qū)ο蟮恼Z言,所以js中沒有類(class)(ES6中實現(xiàn)了class),但是js可以模擬面向?qū)ο蟮乃枷刖幊?#xff0c;js中通過構(gòu)造函數(shù)來模擬類的概念。
改變原型的指向可以實現(xiàn)方法的繼承。借用構(gòu)造函數(shù)繼承,主要解決屬性的問題
?
組合繼承就是指:將改變原型的指向和借用構(gòu)造函數(shù)兩者結(jié)合在一起實現(xiàn)繼承。?
一、原型模式
我們創(chuàng)建的每一個函數(shù)都有一個prototype(原型)屬性,這個屬性指向的是通過調(diào)用構(gòu)造函數(shù)來創(chuàng)建出來的對象實例的原型對象,這個原型對象可以讓所有對象實例共享它所包含的屬性和方法。
function Person () {}Person.prototype.name = "xiao";Person.prototype.sayName = function () {alert('this.name')}var person1 = new Person();var person2 = new Person();person1.sayName()console.log(person1.name == person2.name) // "true"上面的例子當(dāng)中我們創(chuàng)建了一個構(gòu)造函數(shù)Person,并通過它的prototype屬性在它的原型對象中定義了name屬性并賦值,然后通過調(diào)用構(gòu)造函數(shù)Person實例化出來兩個對象實例,通過打印出來的值我們可以得知,person1和person2共享了原型對象中的屬性和方法。
構(gòu)造函數(shù),原型對象和對象實例的關(guān)系
我們知道每個函數(shù)都有一個prototype屬性指向函數(shù)的原型對象。在默認(rèn)情況下,所有原型對象都有一個constructor(構(gòu)造函數(shù))屬性,這個屬性指向了prototype屬性所在的函數(shù),比如前面的例子中,Person.prototype.constructor就指向Person。
另外,當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個新實例后,該實例的內(nèi)部將包含一個__porto__屬性(僅在Firefox、Safari、Chrome中支持),這個屬性指向的就是構(gòu)造函數(shù)的原型對象。由此我們可以得出以下圖示的結(jié)論:
通過代碼來驗證:
# 實例和原型對象之間的關(guān)系console.log(person.__proto__ == Person.prototype) // true# 也可以通過isPrototypeOf()和ES5中的Object.getPrototypeOf()方法來判斷console.log(Person.prototype.isPrototypeOf(person1)) // trueconsole.log(Object.getPrototypeOf(person) === Person.prototype) // true# 原型對象和構(gòu)造函數(shù)的關(guān)系console.log(Person.prototype.constructor == Person) // true二、原型鏈
通過前面我們對構(gòu)造函數(shù),對象實例和原型對象三者關(guān)系的描述可知,實例都包含了指向原型對象的內(nèi)部指針。
那么假如現(xiàn)在我們有兩個構(gòu)造函數(shù)A跟B,我們讓構(gòu)造函數(shù)A的原型對象等于構(gòu)造函數(shù)B的實例,根據(jù)前面的推論,這個時候A的原型對象就包含指向B的原型對象的指針,再假如又有一個構(gòu)造函數(shù)C,讓A的原型對象等于C的實例,上述關(guān)系依舊成立,以此類推便形成了實例與原型的鏈條,即原型鏈,它主要作為JS中實現(xiàn)繼承的主要方法。
原型鏈的基本實現(xiàn)
function SuperType() {this.property = true;}SuperType.prototype.getSuperValue = function() {return this.property;}# 繼承了SuperTypeSubType.prototype = new SuperType();SubType.prototype.getSubValue = function() {return this.subproperty;}var instance = new SubType();console.log(instance.SuperValue()); // true在上面的代碼中,我們沒有使用SubType默認(rèn)的原型,而是將SuperType的實例賦給它,重寫了SubType的原型對象;這樣一來SubType.prototype的內(nèi)部便具有一個指向SuperType原型的指針,原來存在于SuperType的實例中的所有屬性和方法,現(xiàn)在也存在于SubType.prototype中了。
instance同理,還要注意的是由于SubType的原型指向了SuperType的原型,而SuperType的原型的constructor屬性指向的是SuperType構(gòu)造函數(shù),那么instance.constructor也就指向了SuperType
原型搜索機(jī)制:
當(dāng)訪問一個實例屬性或方法時,在通過原型鏈實現(xiàn)繼承的情況下,首先會在實例中搜索該屬性,在沒有找到屬性或方法時,便會沿著原型鏈繼續(xù)往上搜索,直到原型鏈末端才會停下來。
這里還有一個重要的點,事實上所有引用類型默認(rèn)都繼承了Object,而這個繼承也是通過原型鏈實現(xiàn)的,也就是說,所有函數(shù)的默認(rèn)原型都是Object的實例,這也是所有自定義類型都會繼承toString()、valueOf()等默認(rèn)方法的根本原因。
Object.prototype的原型
既然所有類型默認(rèn)都繼承了Object,那么Object.prototype又指向哪里呢,答案是null,我們可以通過下面的代碼打印試試看:
console.log(Object.prototype.__proto__ === null) // truenull即沒有值,也就是說屬性或方法的查找到Object.prototype就結(jié)束了。
本面試題為前端常考面試題,后續(xù)有機(jī)會繼續(xù)完善。我是歌謠,一個沉迷于故事的講述者。
歡迎一起私信交流。
“睡服“面試官系列之各系列目錄匯總(建議學(xué)習(xí)收藏)?
總結(jié)
以上是生活随笔為你收集整理的“约见”面试官系列之常见面试题第四十二篇之原型和原型链(建议收藏)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 自己动手写操作系统——开发环境搭建
- 下一篇: 前端学习(2169):vue-route