首先,定义一个类
function Animal (name) { // 属性 this.name = name || 'Animal'; // 实例方法 this.sleep = function(){ console.log(this.name + '正在睡觉'); } } // 原型方法 Animal.prototype.eat = function(food) { console.log(this.name + '正在吃:' + food); };
1. 原型链继承
思想: 利用原型链来实现继承,超类的一个实例作为子类的原型
function Dog(){ } Dog.prototype = new Animal(); Dog.prototype.name = '狗'; var dog = new Dog(); console.log(dog.name); // 狗 dog.eat('狗粮'); // 狗正在吃:狗粮 dog.sleep(); // 狗正在睡觉 console.log(dog instanceof Animal); // true console.log(dog instanceof Dog); // true
特点:
- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例;
- 父类新增原型方法/原型属性,子类都能访问到;
- 简单,易于实现;
缺点:
- 可以在Dog构造函数中,为Dog实例增加实例属性。如果要新增原型属性和方法,则必须放在new Animal()这样的语句之后执行;
- 无法实现多继承;
- 来自原型对象的所有属性被所有实例共享;
- 创建子类实例时,无法向父类构造函数传参;
2. 构造函数继承
思想: 通过使用call、apply方法在新创建的对象上执行构造函数,用父类的构造函数来增加子类的实例
function Dog(name){ Animal.call(this); this.name = name || '汪汪'; } var dog = new Dog(); console.log(dog.name); // 汪汪 dog.sleep(); // 汪汪正在睡觉 console.log(dog instanceof Animal); // false console.log(dog instanceof Dog); // true
特点:
- 解决了方法1中,子类实例共享父类引用属性的问题;
- 创建子类实例时,可以向父类传递参数;
- 可以实现多继承(call多个父类对象);
缺点:
- 实例并不是父类的实例,只是子类的实例;
- 只能继承父类的实例属性和方法,不能继承原型属性/方法;
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能;
3. 实例继承
思想: 为父类实例添加新特性,作为子类实例返回
function Dog(name){ var instance = new Animal(); instance.name = name || '汪汪'; return instance; } var dog = new Dog(); console.log(dog.name); // 汪汪 dog.sleep(); // 汪汪正在睡觉 console.log(dog instanceof Animal); // true console.log(dog instanceof Dog); // false
特点:
- 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果;
缺点:
- 实例是父类的实例,不是子类的实例;
- 不支持多继承;
4. 拷贝继承
function Dog(name){ var animal = new Animal(); for(var p in animal){ Dog.prototype[p] = animal[p]; } Dog.prototype.name = name || '汪汪'; } var dog = new Dog(); console.log(dog.name); // '汪汪' dog.sleep(); // 汪汪正在睡觉 console.log(dog instanceof Animal); // false console.log(dog instanceof Dog); // true
特点:
- 支持多继承;
缺点:
- 效率较低,内存占用高(因为要拷贝父类的属性);
- 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到);
5. 原型式继承
思想:采用原型式继承并不需要定义一个类!!!,传入参数obj,生成一个继承obj对象的对象
var animal = { name: "汪汪", age: 2, } function F(o) { function C() {} C.prototype = o; return new C(); } var dog = F(animal) console.log(dog.name); // 汪汪 console.log(dog.age); // 2
特点:
- 直接通过对象生成一个继承该对象的对象;
缺点:
- 不是类式继承,而是原型式基础,缺少了类的概念;
6. 组合继承(推荐)
思想: 利用构造继承和原型链组合
function Dog(name){ Animal.call(this); this.name = name || '汪汪'; } Dog.prototype = new Animal(); Dog.prototype.constructor = Dog; var dog = new Dog(); console.log(dog.name); // 汪汪 dog.sleep(); // 汪汪正在睡觉 console.log(dog instanceof Animal); // true console.log(dog instanceof Dog); // true
特点:
- 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法;
- 既是子类的实例,也是父类的实例;
- 不存在引用属性共享问题;
- 可传参;
- 函数可复用;
缺点:
- 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了);
7. 寄生式继承
思想: 原型式+工厂模式 ,解决了组合继承两次调用构造函数的问题; 创建一个仅仅用于封装继承过程的函数,然后在内部以某种方式增强对象,最后返回对象;
//临时中转函数 function obj(o) { function Animal() {} Animal.prototype = o; return new Animal(); } //寄生函数 function create(o){ var Dog = obj(o); // 可以对f进行扩展 Dog.sleep = function(){ console.log(this.name + '在睡觉') } return Dog; } var mydog = { name: '汪汪', age: 1, }; var dog = create(mydog); console.log(dog.name); // 汪汪 dog.sleep(); // 汪汪在睡觉
特点:
- 原型式继承的一种拓展
缺点:
- 依旧没有类的概念
8. 寄生组合继承(推荐)
思想: 结合寄生式继承和组合式继承,完美实现不带两份超类属性的继承方式
// 该实现没有修复constructor function Dog(name){ Animal.call(this); this.name = name || '汪汪'; } (function(){ // 创建一个没有实例方法的类 var Super = function(){}; Super.prototype = Animal.prototype; //将实例作为子类的原型 Dog.prototype = new Super(); })(); var dog = new Dog(); console.log(dog.name); // 汪汪 dog.sleep(); // 汪汪正在睡觉 console.log(dog instanceof Animal); // true console.log(dog instanceof Dog); // true
补充:
// 定义一个类 function Animal (name) { // 属性 this.name = name || 'Animal'; // 实例方法 this.sleep = function(){ console.log(this.name + '正在睡觉'); } //实例引用属性 this.features = []; } function Dog(name){} Dog.prototype = new Animal(); var tom = new Dog('Tom'); var kissy = new Dog('Kissy'); console.log(tom.name); // "Animal" console.log(kissy.name); // "Animal" console.log(tom.features); // [] console.log(kissy.features); // [] tom.name = 'Tom-New Name'; tom.features.push('eat'); //针对父类实例值类型成员的更改,不影响 console.log(tom.name); // "Tom-New Name" console.log(kissy.name); // "Animal" //针对父类实例引用类型成员的更改,会通过影响其他子类实例 console.log(tom.features); // ['eat'] console.log(kissy.features); // ['eat'] // 原因分析: // 1. 执行tom.features.push,首先找tom对象的实例属性(找不到),那么去原型对象中找,也就是Animal的实例。发现有,那么就直接在这个对象的features属性中插入值。 // 2. 在console.log(kissy.features); 的时候。同上,kissy实例上没有,那么去原型上找。 // 3. 刚好原型上有,就直接返回,但是注意,这个原型对象中features属性值已经变化了。
特点:
- 堪称完美;
缺点:
- 实现较为复杂;
9. ES6中的继承(强烈推荐)
思想: ES6中通过 Class ‘类’ 这个语法糖实现继承和Java等面向对象的语言在实现继承上已经非常相似,当然只是语法层面相似,本质当然依旧是通过原型实现的。
ES6实现继承是通过关键字extends、super来实现继承,和面向对象语言Java一样。
class Animal{ constructor(name,age){ this.name = name || 'Animal'; this.age = age this.hobbies = ['睡觉','吃饭'] } /* 下面的写法就等同于把方法挂在原型上 */ static say(){ // 加了static 静态方法,只给类用的方法 console.log('你好'); } // 方法和方法之间不用加逗号 getname(){ console.log(this.name); } } class Dog extends Animal{ /* 在子类constructor中添加属性的小技巧 专属于子类的属性写在参数的前面,父类的属性放在后面 这样一来,就能直接使用...arg ...arg 把函数中的多余的参数放入数组中体现出来。 */ constructor(food,...arg){ super(...arg); // 等同于调用父类,把多余的参数(和父类一样的属性)放到super中,达到继承父类属性的目的 /* 在继承里,写constructor必须写super super下面才能使用this,super有暂存死区(super上面不能使用this,用了就报错) */ this.food = food } static sayState(){ console.log("我在睡觉") } sayAge(){ console.log("我今年" + this.age + '岁'); } sayFood(){ console.log('我在吃'+this.food) } } var dog1 = new Dog('骨头',"汪汪",2); Animal.say() // 你好 Dog.sayState(); // 我在睡觉 dog1.getname(); // 汪汪 dog1.sayAge(); // 我今年2岁 dog1.sayFood(); // 我在吃骨头 var dog2 = new Dog('狗粮',"旺财",1); dog2.hobbies.push("玩耍"); dog2.getname(); // 旺财 dog2.sayAge(); // 我今年1岁 dog2.sayFood(); // 我在吃狗粮 console.log(dog1.hobbies) // ['睡觉','吃饭'] console.log(dog2.hobbies) // ['睡觉','吃饭','玩耍']