在前端開發(fā)中市殷,我們離不開面向過程和面向?qū)ο筮@兩種開發(fā)模式渗鬼。面向過程是指分析出實(shí)現(xiàn)需求所需要的步驟,通過函數(shù)一步一步地實(shí)現(xiàn)這些步驟迷殿,接著依次調(diào)用儿礼。面向?qū)ο笫侵笇⒄麄€(gè)需求按功能、特性劃分庆寺,將這些存有共性的部分封裝成對(duì)象蚊夫,創(chuàng)建了對(duì)象不是為了完成某一個(gè)步驟,而是描述某個(gè)事物在解決問題的步驟中的行為懦尝。對(duì)于復(fù)雜的項(xiàng)目知纷,我們經(jīng)常會(huì)用到面向?qū)ο蟮姆绞絹斫M織我們的代碼。前人早已給我們總結(jié)了幾種常用的繼承方式陵霉,熟悉這幾種繼承方式琅轧,對(duì)于我們的日常開發(fā)會(huì)有很大的裨益。
在 ES5 中踊挠,JS 沒有 class 類乍桂,JS 中的繼承是通過原型的方式來實(shí)現(xiàn)的。每個(gè)構(gòu)造函數(shù)都有一個(gè)原型對(duì)象效床,原型對(duì)象都包含一個(gè)指向構(gòu)造函數(shù)的指針睹酌,而實(shí)例都會(huì)有一個(gè)原型對(duì)象的指針。先來看一個(gè)簡單的示例圖剩檀。
所有的引用類型都繼承自 Object.prototype憋沿,而且是通過原型鏈實(shí)現(xiàn)的。這些對(duì)象都會(huì)有 Object 具有的一些默認(rèn)方法沪猴。我們?cè)跒g覽器控制臺(tái)里先輸入“Object”辐啄,可以發(fā)現(xiàn) Object 的類型是 function采章,然后再輸入“Object.prototype”,可以發(fā)現(xiàn)這個(gè)一個(gè)對(duì)象壶辜,而且里面就放在我們平時(shí)常用的一些方法悯舟,比如 hasOwnProperty、propertyIsEnumerable士复、toLocaleString图谷、toString 等翩活。既然 Object.prototype 也是一個(gè)對(duì)象阱洪,那它的原型是什么呢?我們?cè)佥斎搿癘bject.prototype.proto”菠镇,則返回為 null冗荸。由此可見,所有對(duì)象的原型鏈最頂層是 null利耍。
高能預(yù)警
“__proto__”這個(gè)屬性已從 Web 標(biāo)準(zhǔn)中刪除蚌本,雖然一些瀏覽器目前仍然支持它,但也許會(huì)在未來的某個(gè)時(shí)間停止支持隘梨,請(qǐng)盡量不要使用該特性程癌。為了更好的支持,建議只使用 Object.getPrototypeOf()轴猎。 而且“__proto__”這個(gè)屬性的兼容性方面 IE 只有到了 11 的版本才支持嵌莉。而 getPrototypeOf 的方法在 IE9 就已經(jīng)支持了。
通過現(xiàn)代瀏覽器的操作屬性的便利性捻脖,可以改變一個(gè)對(duì)象的 [[Prototype]] 屬性, 這種行為在每一個(gè) JavaScript 引擎和瀏覽器中都是一個(gè)非常慢且影響性能的操作锐峭,使用這種方式來改變和繼承屬性是對(duì)性能影響非常嚴(yán)重的,并且性能消耗的時(shí)間也不是簡單的花費(fèi)在 obj.__proto__ = ... 語句上, 它還會(huì)影響到所有繼承來自該 [[Prototype]] 的對(duì)象可婶,如果你關(guān)心性能沿癞,你就不應(yīng)該在一個(gè)對(duì)象中修改它的 [[Prototype]]。相反, 創(chuàng)建一個(gè)新的且可以繼承 [[Prototype]] 的對(duì)象矛渴,推薦使用 Object.create()椎扬。
原型鏈繼承
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function () {
return this.name;
};
Person.prototype.getAge = function () {
return this.age;
};
function Student() {}
Student.prototype = new Person("junjun", 20);
Student.prototype.study = function () {
return this.name + " is studying";
};
var student1 = new Student();
var student2 = new Student();
console.log(student1.getName()); //junjun
console.log(student1.study()); //junjun is studying
console.log(student2.getName()); //junjun
console.log(student2.study()); //junjun is studying
Student.prototype.name = "lili";
console.log(student1.getName()); //lili
console.log(student1.study()); //lili is studying
console.log(student2.getName()); //lili
console.log(student2.study()); //lili is studying
student1.name = "weihua";
console.log(student1.getName()); //weihua
console.log(student1.study()); //weihua is studying
console.log(student2.getName()); //lili
console.log(student2.study()); //lili is studying
console.log(Student.prototype.constructor === Student); //false
console.log(Student.prototype.constructor === Person); //true
console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //true
console.log(student1 instanceof Student); //true
console.log(Object.prototype.isPrototypeOf(student1)); //true
console.log(Person.prototype.isPrototypeOf(student1)); //true
console.log(Student.prototype.isPrototypeOf(student1)); //true
要點(diǎn):子類的原型等于父類的實(shí)例。
特點(diǎn):實(shí)現(xiàn)簡單具温;可繼承構(gòu)造函數(shù)的屬性和方法蚕涤,也可繼承原型的屬性和方法。
缺點(diǎn):子類實(shí)例共享父類屬性桂躏,父類屬性修改后會(huì)影響所有實(shí)例钻趋;在創(chuàng)建子類的實(shí)例時(shí),不能向父類的構(gòu)造函數(shù)中傳遞參數(shù);在給子類原型添加新的屬性和方法要在繼承后定義剂习。
借用構(gòu)造函數(shù)
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function () {
return this.name;
};
Person.prototype.getAge = function () {
return this.age;
};
function Student(name, age) {
Person.call(this, name, age);
this.job = "study";
}
Student.prototype.study = function () {
return this.name + " is studying";
};
var student1 = new Student("lili", 19);
var student2 = new Student("huahua", 20);
console.log(student1.getName()); //Uncaught TypeError: student1.getName is not a function
console.log(student1.study()); //lili is studying
console.log(student2.getName()); //Uncaught TypeError: student1.getName is not a function
console.log(student2.study()); //huahua is studying
console.log(Student.prototype.constructor === Student); //true
console.log(Student.prototype.constructor === Person); //false
console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //false
console.log(student1 instanceof Student); //true
console.log(Object.prototype.isPrototypeOf(student1)); //true
console.log(Person.prototype.isPrototypeOf(student1)); //false
console.log(Student.prototype.isPrototypeOf(student1)); //true
要點(diǎn):父類的構(gòu)造函數(shù)在子類構(gòu)造函數(shù)中執(zhí)行蛮位。
特點(diǎn):可以向父類傳參较沪,且不會(huì)有原型屬性共享的問題;可以繼承多個(gè)構(gòu)造函數(shù)屬性
缺點(diǎn):只能繼承父類的實(shí)例屬性和方法失仁,不能繼承原型屬性和方法尸曼;每個(gè)新實(shí)例都有父類構(gòu)造函數(shù)的副本,臃腫
組合繼承
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function () {
return this.name;
};
Person.prototype.getAge = function () {
return this.age;
};
function Student(name, age) {
Person.call(this, name, age);
this.job = "study";
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
Student.prototype.study = function () {
return this.name + " is studying";
};
var student1 = new Student("lili", 19);
var student2 = new Student("huahua", 20);
console.log(student1.getName()); //lili
console.log(student1.study()); //lili is studying
console.log(student2.getName()); //huahua
console.log(student2.study()); //huahua is studying
console.log(Student.prototype.constructor === Student); //true
console.log(Student.prototype.constructor === Person); //false
console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //true
console.log(student1 instanceof Student); //true
console.log(Object.prototype.isPrototypeOf(student1)); //true
console.log(Person.prototype.isPrototypeOf(student1)); //true
console.log(Student.prototype.isPrototypeOf(student1)); //true
要點(diǎn):父類的構(gòu)造函數(shù)在子類構(gòu)造函數(shù)中執(zhí)行萄焦。并將子類的原型賦值為父類的實(shí)例控轿,之后將該實(shí)例的 constructor 指向子類構(gòu)造函數(shù)。使用原型鏈實(shí)現(xiàn)對(duì)原型方法的繼承拂封,而通過借用構(gòu)造函數(shù)來實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承茬射。
特點(diǎn): 可以繼承父類實(shí)例和原型中的屬性和方法。
缺點(diǎn): 在使用子類創(chuàng)建實(shí)例對(duì)象時(shí)冒签,其原型中會(huì)存在兩份相同的屬性/方法在抛。會(huì)調(diào)用兩次超類型構(gòu)造函數(shù):一次是在創(chuàng)建子類型原型的時(shí)候,另一次是在子類型構(gòu)造函數(shù)內(nèi)部萧恕。
原型式繼承
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function () {
return this.name;
};
Person.prototype.getAge = function () {
return this.age;
};
function CreateObj(obj, name, age) {
function F() {
this.name = name;
this.age = age;
}
F.prototype = obj;
F.prototype.study = function () {
return this.name + " is studying";
};
return new F();
}
var person = new Person();
var student1 = CreateObj(person, "huahua", 16);
var student2 = CreateObj(person, "weiwei", 14);
console.log(student1.getName()); //huahua
console.log(student1.study()); //huahua is studying
console.log(student2.getName()); //weiwei
console.log(student2.study()); //weiwei is studying
console.log(student1.__proto__); //Person 實(shí)例
console.log(student1.__proto__.constructor === Person); //true
console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //true
要點(diǎn):用一個(gè)函數(shù)包裝一個(gè)對(duì)象刚梭,然后返回這個(gè)函數(shù)的調(diào)用,這個(gè)函數(shù)就變成了個(gè)可以隨意增添屬性的實(shí)例或?qū)ο笃彼簟bject.create()就是這個(gè)原理朴读。
特點(diǎn):類似于復(fù)制一個(gè)對(duì)象,用函數(shù)來包裝走趋。
缺點(diǎn):原型上的屬性和方法都是共享的衅金,所有后面對(duì)原型上的父類實(shí)例修改會(huì)影響所有實(shí)例;
寄生式繼承
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function () {
return this.name;
};
Person.prototype.getAge = function () {
return this.age;
};
function Student(name, age) {
Person.call(this, name, age);
this.job = "study";
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.study = function () {
return this.name + " is studying";
};
var student1 = new Student("lili", 19);
var student2 = new Student("huahua", 20);
console.log(student1.getName()); //lili
console.log(student1.study()); //lili is studying
console.log(student2.getName()); //huahua
console.log(student2.study()); //huahua is studying
console.log(Student.prototype.constructor === Student); //false
console.log(Student.prototype.constructor === Person); //true
console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //true
console.log(student1 instanceof Student); //true
console.log(Object.prototype.isPrototypeOf(student1)); //true
console.log(Person.prototype.isPrototypeOf(student1)); //true
console.log(Student.prototype.isPrototypeOf(student1)); //true
要點(diǎn):主要思想就是不需要為了指定子類型的原型而去調(diào)用超類型的構(gòu)造函數(shù)吆视,我們需要的無非是超類型的原型副本典挑。這樣就會(huì)斷絕父類修改對(duì)子類的影響。
特點(diǎn):父類的修改不會(huì)影響到子類的實(shí)例啦吧。且能共享父類的方法您觉。
ES6 中的繼承
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
getName() {
return this.name;
}
getAge() {
return this.age;
}
}
class Student extends Person {
constructor(name, age, job) {
super(name, age);
this.job = job;
}
study() {
return this.name + " is studying";
}
}
var student1 = new Student("lili", 19);
var student2 = new Student("huahua", 20);
console.log(student1.getName()); //lili
console.log(student1.study()); //lili is studying
console.log(student2.getName()); //huahua
console.log(student2.study()); //huahua is studying
console.log(Student.prototype.constructor === Student); //true
console.log(Student.prototype.constructor === Person); //false
console.log(student1 instanceof Object); //true
console.log(student1 instanceof Person); //true
console.log(student1 instanceof Student); //true
console.log(Object.prototype.isPrototypeOf(student1)); //true
console.log(Person.prototype.isPrototypeOf(student1)); //true
console.log(Student.prototype.isPrototypeOf(student1)); //true
特點(diǎn):除了子類原型的構(gòu)造函數(shù)不同,其他和寄生繼承實(shí)現(xiàn)的效果一致