JavaScript 原型、原型链、继承
参考文章:
[1] https://blog.youkuaiyun.com/qq_42019025/article/details/80708446
1. 原型是什么?解释原型链
在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象。这个对象的用途是包含特定类型的所有实例共享的属性和方法,即这个原型对象是用来给实例共享属性和方法的。
而每个实例内部都有一个指向原型对象的指针,也就是__proto__,它的指向和生成该实例的构造函数有关,即指向构造函数的prototype对象。
当函数经过new调用时,这个函数就成为了构造函数,返回一个全新的实例对象,这个实例对象有一个__proto__属性,指向构造函数的原型对象。
如果让原型对象等于另外一个类型的实例,该类型的实例的__proto__又指向另一个构造函数原型对象,假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条,也就是所谓的原型链,当这条链条到 object 的时候就到头了,object 的 __proto__ 指向 null。
2. JS实现继承的6种方式
推荐阅读:https://www.cnblogs.com/Grace-zyy/p/8206002.html
2.1 原型链继承
核心: 将父类的实例作为子类的原型
function Father(){
this.colors = ['blue','yellow'];
}
function Son(){
}
// Son 继承了 Father
Son.prototype = new Father();
Son.prototype.sleep = function(){
console.log("睡觉")
}
var instance1 = new Son();
var instance2 = new Son();
instance1.colors.push('black')
console.log(instance1.colors) // [ 'blue', 'yellow', 'black' ]
console.log(instance2.colors) // [ 'blue', 'yellow', 'black' ]
console.log(instance1 instanceof Son) // true
console.log(instance1 instanceof Father) // true
instance1.sleep() // 睡觉
instance2.sleep() // 睡觉
优点:
- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例。
- 父类新增原型方法、原型属性,子类都能访问到。
- 简单易用,只要修改子类的原型属性指向父类的实例就行。
缺点:
- 无法实现多继承。(每个构造函数只能指定一个原型对象)
- 来自原型对象的所有引用类型值的属性是所有实例共享的。(例如数组)
- 创建子类实例时,无法向父类构造函数传参。(因为在创建实例之前,就需要给子类的构造函数指定原型实例对象,此时可以传参,但是会影响将来生成的所有实例)
2.2 借用构造函数
核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
function Father(name){
this.name = name;
this.colors = ['red','yellow'];
}
function Son(name){
// 继承了 Father,同时还传递了名字这个参数
Father.call(this, name);
// 实例属性
this.age = 29;
}
var instance1 = new Son('Nicholas');
var instance2 = new Son('John');
console.log(instance1.name); // Nicholas
console.log(instance2.name); // John
instance1.colors.push('blue');
console.log(instance1.colors, instance2.colors)
// [ 'red', 'yellow', 'blue' ] [ 'red', 'yellow' ]
优点:
- 解决了2.1中,子类实例共享父类引用属性的问题(每个属性都会复制一份,父类的引用属性不会被共享)。
- 创建子类实例时,可以向父类传递参数(传递多个也可以)
- 可以实现多继承(call多个父类对象,就继承了多个父类对象的属性和方法)
缺点:
- 实例并不是父类的实例,只是子类的实例(本质上只是一种复制,实例对象保存了父类实例属性和方法的副本,子类的原型并没有发生改变)
- 只能继承父类的实例属性和方法,不能继承父类的原型属性/方法
- 无法实现函数复用(函数都是单独复制的一份),每个子类都有父类实例函数的副本,影响性能。
2.3.组合继承
核心:通过利用原型链继承和借用构造函数继承实现了组合继承。
通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Father(name){
this.colors = ['blue','yellow'];
this.name = name;
}
Father.prototype.sayName = function(){
console.log(this.name);
}
function Son(name, age){
Father.call(this, name);
this.age = age;
}
Son.prototype = new Father();
Son.prototype.constructor = Son;
Son.prototype.sayAge = function(){
console.log(this.age);
}
var instance1 = new Son('Nicholas', 29);
instance1.colors.push('black');
console.log(instance1.colors); // [ 'blue', 'yellow', 'black' ]
instance1.sayName(); // Nicholas
instance1.sayAge(); // 29
var instance2 = new Son('John', 26);
console.log(instance2.colors); // [ 'blue', 'yellow' ]
instance2.sayName(); // John
instance2.sayAge(); // 26
优点:
- 弥补了方式2.2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
- 既是子类的实例,也是父类的实例,可以用 instanceof 方法来检测,因为使用了原型继承。
- 不存在引用属性共享问题,因为 call 方法把父类属性都复制了一份,包括引用属性,实例中包含该属性,就不会到原型链上去寻找。
- 可向父类构造函数传参(通过call 方法)
- 函数可复用
缺点:
- 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
2.4 原型式继承
Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
var father = {
name:'Nicholas',
colors:['blue','yellow']
}
var son1 = Object.create(father)
son1.name = "Greg"
son1.colors.push('black')
var son2 = Object.create(father)
son2.name = "Linda"
son2.colors.push('red')
console.log(son1.colors) // [ 'blue', 'yellow', 'black', 'red' ]
console.log(son2.colors) // [ 'blue', 'yellow', 'black', 'red' ]
console.log(son1.name) // Greg
console.log(son2.name) // Linda
// Object.create 内部做的事情
Object.create = function (obj) {
function F() {}
F.prototype = obj;
return new F();
};
2.5 寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路
var father = {
name:'Nicholas',
colors:['blue','yellow']
}
// 进行了一次浅复制
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function createClone(original){
var clone = object(original); // 通过调用函数来创建一个新的对象
clone.sayHi = function(){ // 使用某种方式来增强这个对象
console.log('hi');
}
return clone; // 返回这个对象
}
var clone1 = createClone(father) // 克隆了原对象的所有实例方法属性,并增加了自己的方法
clone1.sayHi() // hi
这个例子中的代码基于 father 这个原型对象返回了一个新对象,clone,这个新对象不仅有 father 的所有属性和方法,而且还有自己的sayHi方法。使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点与构造函数模式类似。
2.6 寄生组合式继承
使用了寄生继承和组合继承进行结合的方法。
优点:解决了组合继承中调用两次父类构造函数的问题。
缺点:构造比较复杂。
function inheritPrototype(subType, superType){
var p = Object.create(superType.prototype)
p.constructor = subType
subType.prototype = p
}
function Father(name){
this.name = name
this.colors = ["red","blue","green"]
}
Father.prototype.sayName = function(){
console.log(this.name)
}
function Son(name,age){
Father.call(this,name)
this.age = age
}
inheritPrototype(Son, Father)
Son.prototype.sayAge = function () {
alert(this.age)
}
说白了就是让子类继承于父类的原型,避免在生成子类的实例的时候重复调用当前父类的构造函数。