我理解的繼承,是為了解決程式碼重複浪費空間與編寫精力的問題,如有兩個物件,
// person1
var person1={
name:'tom',
say(){
console.log(this.name);
}
}
// person2
var person2={
name:'jerry',
say(){
console.log(this.name);
}
}
複製程式碼
這兩個物件都有相同的name
屬性和say()
方法,只是name
屬性值不同,造成了程式碼的重複浪費,因此提出了節省程式碼的方式:
工程模式
function person(name){
var obj=new Object();
obj.name=name;
obj.say=function(){
console.log(obj.name);
};
return obj;
}
var person1=person("tom");
var person2=person("jerry");
複製程式碼
建構函式
function Person(name){
this.name=name;
this.say=function(){
console.log(this.name);
};
}
var person1=new Person("tom");
var person2=new Person("jerry");
複製程式碼
其實兩種方式大同小異,因為在使用new操作符來建立一個例項物件時,產生了以下4個步驟:
- 新建一個物件:
var obj=new Object();
- 將建構函式中的
this
指向該物件,對該物件進行賦值obj.name='name
- 將該物件的
__proto__
指向建構函式的原型obj.__proto__=Person.prototype
- 返回該物件
其中1、2、4步就是工程模式中的步驟,只是多了第3步。
這兩種產生物件的方法,雖然節省了程式碼的書寫量,但在記憶體上仍然消耗相同的空間,每建立一個新的例項物件仍然要建立新的屬性和方法。所以就有了原型。
原型
(1)首先,js裡所有的函式都有一個prototype
屬性,該屬性是一個物件;同時js裡面所有的物件(除去基本型別number,string,boolean,null和undefined
之外的所有)都有一個__proto__
屬性,所以一個函式有prototype
和__proto__
兩個屬性,可以通過console.dir(fn)
檢視。
function Person(name){
this.name=name;
}
console.dir(Person);
複製程式碼
(2)prototype
裡有個構造器constructor
,指向的就是該建構函式,所有的物件都是由建構函式例項化得到的,現在我們來看一下剛才講new操作符時的第3個步驟:
- 將該物件的
__proto__
指向建構函式的原型obj.__proto__=Person.prototype
用圖來表示就是:
__proto__
屬性都指向建構函式的prototype
物件,所以可以把共享的方法寫在prototype
裡,這樣只需要建立一個方法就可以了。
function Person(name){
this.name=name;
}
Person.prototype.say=function(){
console.log(this.name);
}
var p1=new Person("tom");
p1.say(); // tom
var p1=new Person("jerry");
p1.say(); // jerry
複製程式碼
原型鏈
有了原型的概念,先給出原型鏈的概念:例項物件在使用屬性或者呼叫方法時,如果自己沒有,則會往上一級級查詢prototype
物件,直到找到為止,如果最終也找不到則報錯,就拿上面講的,p1自己沒有say方法,但是原型物件裡面有該方法,所以可以呼叫。
原型鏈繼承
有了原型鏈的概念,我們就可以實現繼承了,即讓子類建構函式的prototype
指向父類的一個例項物件。這樣通過原型鏈的查詢就可以繼承到父類的方法,我們通常需要繼承的都是方法。
function Person(name){
this.name=name;
}
Person.prototype.say=function(){
console.log(this.name);
}
// 子類建構函式
function Student(name){
this.name=name;
}
// 將子類新增到原型鏈中
Student.prototype=new Person("tom");
// 子類自己的原型方法必須在改變原型指向後新增
Student.prototype.play=function(){
console.log(this.name+" play");
}
var s1=new Student("jerry");
s1.say(); // 原型鏈上的方法 jerry
s1.play(); // 自己原型上的方法 jerry play
// this一直指向都是s1,跟例項物件tom沒有關係
複製程式碼
例項方法、靜態方法、原型方法和內部方法
function Fn(){
// 例項方法,只能通過例項物件.的形式呼叫
this.work=function(){
console.log("work");
}
// 內部方法 只能內部呼叫
function learn(){}
};
// 靜態方法,只能通過函式名.的形式呼叫
Fn.say=function(){
console.log("say")
}
// 原型方法,只能例項.的形式呼叫
Fn.prototype.play=function(){
console.log("play")
}
Fn.say(); // say
Fn.play(); // 報錯
Fn.work(); // 報錯
var f1=new Fn();
f1.say(); // 報錯
f1.play(); // play
f1.work(); // work
複製程式碼
我們可以使用console.dir(Person)
檢視一下:
其實這個問題是我在面試頭條的時候暴露出來的,感謝面試小哥哥為我講解,當時是有兩個問題,怎麼判斷是陣列,怎麼讓不是陣列的元素呼叫splice方法,然後我就回答成了:
[1,2,3].isArray()
Array.splice.call(obj)
複製程式碼
完美搞錯,真感謝那個面試小哥哥還耐心地給我講解(捂臉羞愧)。
其實列印以下建構函式console,dir(Array)
就可以看到
isArray
是靜態方法,splice
是原型方法,所以正確的應該是:
Array.isArray([1,2,3]);
[].splice.call(obj); // []是Array的一個例項化物件
複製程式碼
instanceof操作符
l instanceof R 就是判斷l的原型鏈上是否有R.prototype
s1 instanceof Student // true
s1 instanceof Person // true
複製程式碼
缺點
父類原型上的引用屬性會被子類們共享,一個子類更改了,其餘的也會被更改;
子類例項無法向父類建構函式傳參
建構函式繼承
建構函式可以解決向父類建構函式傳參的問題,但沒有辦法繼承父類原型上的方法。
function Person(name){
this.name=name;
}
Person.prototype.say=function(){
console.log("say");
}
function Student(name,age){
Person.call(this,name);
this.age=age;
}
var s1=new Student("xixi",12);
s1.name; // xixi
s1.age; // 12
s1.say(); // 報錯
複製程式碼
組合繼承
即使用建構函式來繼承屬性,使用原型來繼承原型方法
function Person(name){
this.name=name;
}
Person.prototype.say=function(){
console.log("say");
}
function Student(name,age){
// 繼承屬性
Person.call(this,name);
this.age=age;
}
// 繼承方法
Student.prototype=new Person();
var s1=new Student("xixi",12);
s1.name; // xixi
s1.age; // 12
s1.say(); // say
複製程式碼