书中原文讨论比较深,这里只总结几个点。
- 原型机制其实就是关联对象,而前面介绍了几种方法,可以用来创建关联对象,但是都是一个。
- 如果是需要一个原型对象用来创建多个实例,而这个原型对象又需要与已有的原型对象关联,那么就需要下面的方式。
原型继承
- 图中b1可以继承Foo.prototype,也就是访问Foo.prototype包含的属性。
- 利用这种关系,可以实现两个构造函数的继承:
- 通过子类继承父类,也就是通过两个构造函数建立起联系。子类的实例可以访问父类原型上的属性。
代码实现:
function Father(){
this.property = true;
}
Father.prototype.getFatherValue = function(){
return this.property;
}
function Son(){
this.sonProperty = false;
}
//继承 Father
Son.prototype = new Father();//Son.prototype被重写,导致Son.prototype.constructor也一同被重写
Son.prototype.getSonVaule = function(){
return this.sonProperty;
}
var instance = new Son();
alert(instance.getFatherValue());//true
- 前面我们说过原型直接赋值会互相影响的问题,这里得到解决。
- 这是可能是比较常见的写法。但是,这种写法会有一个问题,就是new操作过程会有副作用,所以需要改为
Object.create()
// 直接引用,不是需要的结果,否则不需要Son
Son.prototype = Father.prototype
// 有副作用
Son.prototype = new Father();
// 修改为
Son.prototype = Object.create(Father.prototype);
当然,这里也可以用Object.setPrototypeOf()
后面的new Son()
也同样进行修改,避免new操作。
- 我的理解是,原型链继承其实找了个中间的人。
- 从上面这段代码中,
Son()
和Father()
都是构造函数,目的是让Son的实例对象能访问到Father的原型对象。这里的中间人就是Father的实例对象。 - 所以
Son.prototype = Object.create(Father.prototype)
,分成两段来看就是:中间人 = Object.create(Father.prototype);Son.prototype = 中间人
。
缺点
-
无法传参
子类型实例不能给父类型构造函数传参
-
引用类型属性共享
父类的所有引用类型的属性会被所有子类共享,更改一个子类的引用类型的属性值,其他子类也会受影响
借用构造函数继承
这实际上是new 操作和 call 方法结合实现的继承。
// 父类构造函数
function Parent() {
this.info = {
name: "yhd",
age: 19,
}
}
// 子类构造函数
function Child() {
// call方法调用父类
Parent.call(this)
}
// 创建子类实例
let child1 = new Child();
// 使用父类属性和方法
child1.info.gender = "男";
console.log(child1.info); // {name: "yhd", age: 19, gender: "男"};
let child2 = new Child();
console.log(child2.info); // {name: "yhd", age: 19}
大致过程:
- 相当于新建一个对象(实例),将
__proto__
赋值为子类的原型对象,子类作为它的方法执行。 - 子类执行的过程中会执行
Parent.call(this)
,这里的this指向新建的实例对象。 - 在call方法里面,将执行
this.Parent()
,也就是将Parent()
,作为this的方法进行调用。 - 所以最终是将
Parent()
作为Child()的实例对象的方法执行,为这个实例对象添加属性和方法,并不是真正意义上的继承。
优点
解决了原型链继承的引用类型的问题
缺点
-
占用内存
每个实例都拷贝一份父类的方法,占用内存大,尤其是方法过多的时候。
-
无法及时更新
而且该方法都不能及时作出更新
本质上来说,这里的继承其实是在函数执行的过程中发生的。并不是一直存在的联系。
组合继承
其实就是将原型链继承和借用构造函数继承结合起来。
思路
- 前面说了,每个实例都拷贝一份父类的方法,占用内存大,那把方法放到原型链上就可以了,通过原型链继承都共享同一个方法。
- 还有原型链的引用类型共享问题,将引用类型(除了函数)都放到构造函数中,通过执行构造函数,每个实例在生成自己的属性,避免了互相干扰。
特点
把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性
代码实现
function Parent(name) {
this.name = name
this.colors = ["red", "blue", "yellow"]
}
Parent.prototype.sayName = function () {
console.log(this.name);
}
function Child(name, age) {
// 继承父类属性
Parent.call(this, name)
this.age = age;
}
// 继承父类方法
Child.prototype = new Parent();
Child.prototype.sayAge = function () {
console.log(this.age);
}
let child1 = new Child("yhd", 19);
child1.colors.push("pink");
console.log(child1.colors); // ["red", "blue", "yellow", "pink"]
child1.sayAge(); // 19
child1.sayName(); // "yhd"
let child2 = new Child("wxb", 30);
console.log(child2.colors); // ["red", "blue", "yellow"]
child2.sayAge(); // 30
child2.sayName(); // "wxb"
这里就基本解决了上面的问题。
但是又有一个问题,就是需要进行new操作,会带来副作用。
而且这里的new操作不能改为Object.creact()
,因为对象的属性就是在执行构造函数的过程种产生的,改为Object.creact()
就失去这个作用。
ES6的类
在定义函数的时候使用extends父类,然后在构造函数中调用super方法继承了父类的所有属性和方法(实际是组合继承)
ES5代码实现:
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
转化为ES6的类:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
// 正确
var point = new Point(2, 3);
其实JS语法中的类只是原型机制的语法糖,只是对代码内容进行简化。
实际上没有真正的类,class仍然是通过原型机制实现的。
传统的类在声明时静态复制所有行为,而ES6的class并不是。如果修改了父类的方法,会影响到所有子类和实例。
提升:类不存在提升。
静态方法:如果在一个方法前面加static,那么该方法不会被实例继承,而是直接通过类来调用。
子类继承:使用extends和super