也来谈谈继承

继承,代码复用的一种模式。和其它高级程序语言相比,javascript有点点不一样,它是一门纯面向对象的语言,在JS中,没有类的概念,但也可以通过原型(prototype)来模拟对象的继承和多态。

原型与实例

  • 一切对象都是Object的实例,一切函数都是Function的实例
  • 构造函数通过 prototype 属性访问原型对象
  • 实例对象通过 [[prototype]] 内部属性访问原型对象,浏览器实现了 proto 属性用于实例对象访问原型对象
  • Object 是构造函数,既然是函数,那么就是Function的实例对象;Function是构造函数,但Function.prototype是对象,既然是对象,那么就是Object的实例对象

关系判断

  • instanceof 判断是否为另一个对象的实例
  • isPrototypeOf() 判断一个对象是否存在于另一个对象的原型链上 Child.isPrototypeOf(Parent) //true
  • Object.getPrototypeOf() ES6中新增的方法,用于获取子类的父类 Object.getPrototypeOf(Child) == Parent //true

继承类型

  • 引用对象继承:子引用类型继承父引用类型,然后通过子引用类型生成的实例对象,具有父引用类型的特性。
  • 实例对象继承:而实例对象继承,继承得到的对象都具有父实例对象的所有属性和方法,其实就是指对象的复制和克隆。

引用对象继承

  1. 原型 (C.prototype = new P())
  2. 构造函数 (P.apply(this, arguments))
  3. 原型 + 构造函数 (使用原型链实现对原型中的属性方法的继承,使用构造函数实现实例属性的继承)
  4. 共享原型(子类与父类共享同一个原型)
  5. 临时原型(使用中间类)
  6. 临时原型 + 构造函数(完美、nodejs的继承方式、ES5版本)

原型继承

让子类的的原型等于父类的实例,从而继承父类的所有属性和原型

function Parent(){
this.name = 'father';
this.friends = ['A','B'];
}

Parent.prototype.say = function(){
log(this.name, this.friends)
}

function Person(){}

Person.prototype = new Parent();
Person.prototype.constructor = Person;

var p1 = new Person();
var p2 = new Person();

log(p1.name);
log(p2.name);
p1.name = 'jack';
p2.name = 'mak';
log(p1.name);
log(p2.name);
log(p1.say())
log(p2.say())
log('====================')
log(p1.friends);
log(p2.friends);
p1.friends.push('C');
p1.friends.push('D');
log(p1.friends);
log(p2.friends);
log(p1.say())
log(p2.say())

缺点:

  1. 不能向父类的构造函数中传递参数
  2. 父类中的引用类型属性会被实例共享
  3. 需要修正实例的constructor指向,否则会指向父类

构造函数继承

在子类的构造函数中使用 Parent.call(this, …args) 或 Parent.apply(this,[args]) 来继承父类的属性,并向父类的构造函数传参

function Parent(name){
this.name = name;
this.friends = ['A','B'];
}

Parent.prototype.say = function(){
log(this.name, this.friends)
}

function Person(){
Parent.apply(this, arguments);
this.age = 23;
}

var p1 = new Person('nameA');
var p2 = new Person('nameB');
log(p1.name); //nameA
log(p2.name); //nameB

p1.friends.push('C');
p2.friends.push('D');
log(p1.friends) //["A", "B", "C"]
log(p2.friends) //["A", "B", "D"]

log(p1.say()) // Uncaught TypeError: p1.say is not a function
  • 优点:子类可以向父类的构造函数中传参,子类实例中的引用类型属性互不干扰
  • 缺点:子类实例无法访问父类的原型(无法复用父类原型中的方法)

原型+构造函数继承

为解决纯原型继承不能给父传参和纯构造函数继承不能继承父类原型的缺点,把二者结合起来实现

function Parent(name){
this.name = name;
this.friends = ['A','B'];
log('Parent constructor excuted');
}

Parent.prototype.say = function(){
log(this.name, this.friends)
}

function Child(name, age){
Parent.call(this, name);
this.age = age;
log('Child constructor excuted');
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

Child.prototype.see = function(){
log(this.age);
}
Child.prototype.getAge = () => this.age;

var c1 = new Child('jack',12);
c1.friends.push('F');

log(c1.say()); //jack ["A", "B", "F"]
log(c1.see()); //12

var c2 = new Child('jack',42);
c2.friends.push('D');

log(c2.say()); //jack ["A", "B", "D"]
log(c2.see()); //42

缺点:子类初次实例化时会多调用一个父类的构造函数(第一次创建子类原型和子类实例化时)

共享原型

子类和父类共用一个原型

function inherit(C, P){
C.prototype = P.prototype;
}

function Parent(name){
this.name = name;
this.friends = ['A','B'];
log('Parent constructor excuted');
}

Parent.prototype.say = function(){
log(this.name, this.friends)
}

function Child(name, age){
this.age = age;
log('Child constructor excuted');
}

Child.prototype.say=()=>log('child',this.name);

inherit(Child, Parent);

var c1 = new Child('pake', 44);
c1.name = 'jack';

log(c1.name)
log(c1.say())

缺点:子类实例化时,父类构造函数接收不到参数, 子类原型如果改变也会影响到父类原型

临时原型

使用一个纯净类继承父类的原型,再将纯净类的实例设置为子类的原型,如此子类继承了父类的原型和纯净类的构造函数,再修正一下子类的构造函数为子类本身,这样子类的原型改动就不影响父类。

function inherit(C, P){
function F(){}
F.prototype = P.prototype;
C.prototype = new F(); //继承父类的原型,但不继承父类的构造函数
C.super = P.prototype; //便于子类访问父类的原型
C.prototype.constructor = C; //重置构造函数为本身
return new F();
}

function Parent (age){
this.name = 'parent';
this.age = age || 50;
}

Parent.prototype.say = function(){
log(this.name, this.age)
}

function Child (name, age){
this.name = name;
}
inherit(Child, Parent)

var c1 = new Child('jack', 10)
log(c1.name) // jack
log( c1.say() ) // jack undefined

缺点:只继承了父类的原型,子类实例化时一样不能给父类的构造函数传参

临时原型 + 构造函数

在子类的构造函数中调用父类的构造函数,修复了上面 临时原型 不能传递参数给父类构造函数的问题

 function inherit(C, P){
function F(){}
F.prototype = P.prototype;
C.prototype = new F(); //继承父类的原型,但不继承父类的构造函数
C.super = P.prototype; //便于子类访问父类的原型
C.prototype.constructor = C; //重置构造函数为本身
return new F();
}

function Parent (age){
this.name = 'parent';
this.age = age || 50;
}

Parent.prototype.say = function(){
log(this.name, this.age)
}

function Child (name, age){
Parent.call(this, age);
this.name = name;
}
inherit(Child, Parent)

var c1 = new Child('jack', 10)
log(c1.name) // jack
log( c1.say() ) // jack 10

// 这种方法是最合适,也是用得最广的继承模式
另一种相同的实现,ES5的实现
function Shape() {
this.x = 0;
this.y = 0;
}

Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info("Shape moved.");
};

function Rectangle() {
Shape.call(this); //call super constructor.
}

Rectangle.prototype = Object.create(Shape.prototype);

var rect = new Rectangle();

log(rect instanceof Rectangle); //true.
log(rect instanceof Shape); //true.

rect.move(); //"Shape moved."

实例对象继承

  1. 原型实例
  2. 克隆(深拷贝、浅拷贝)
  3. 借用和绑定

原型实例

创建一个继承父类原型的实例对象,这也是ES5中Object.create()的简单实现

function object(P){
var F = function(){};
F.prototype = P;
return new F();
}

function Parent(name) { this.name = name || 'Adam'; }
Parent.prototype.say = function(){ log(this.name) };

var c1 = new Parent('jack');
var c2 = object(c1);
c2.name = 'mark';

log(c1.say())
log(c2.say())

克隆

浅拷贝只能拷贝值类型的数据,对于引用类型,只会拷贝引用地址,如果有引用类型,多个拷贝对象会共用同一个引用类型的数据,造成混乱。

function clone(parent, child){
var i, child = child || {};
for(i in parent){
parent.hasOwnProperty(i) && (child[i] = parent[i]);
}

return child;
}

var o1 = { arr: [1,2,4], name:'jack'};
var o2 = clone(o1);
o2.arr.push(3);
log(o1.arr); //[1, 2, 4, 3]
log(o2.arr); //[1, 2, 4, 3]

两个拷贝对象共用同一个引用类型,会相互影响

深拷贝

function cloneDeep(parent, child){
var i, child = child || {};

for(i in parent){
if(parent.hasOwnProperty(i)){
if(typeof parent[i]==='object'){
child[i] = Array.isArray(parent[i]) ? []:{};
cloneDeep(parent[i], child[i]);
}else{
child[i] = parent[i];
}
}
}

return child;
}

var o3 = { arr: [1,2,4], name:'jack'};
var o4 = cloneDeep(o3);
o4.arr.push(3);
log(o3.arr);//[1, 2, 4]
log(o4.arr);//[1, 2, 4, 3]

两个对象引用不同的引用地址,互不影响

借用和绑定

使用 call / apply / bind 复用对象上的方法

var parent = {
name:'parent',
say:function(msg){
log(this.name, msg)
}
}

var child = {
name:'child'
}

// 复用父类的say方法
parent.say.call(child, 'hello'); //child hello
parent.say.bind(child, 'world')(); //child world

// bind的内部实现
if(typeof Function.prototype.bind === 'undefined'){
Function.prototype.bind = function(context){
var _this = this, slice = Array.prototype.slice, args = slice.call(arguments, 1);
return function(){
return _this.apply(context, args.concat(slice.call(arguments)));
}
}
}

// 借用Array中的slice方法
// Array.prototype.slice.call() 与 [].prototype.slice.call() 区别: 前者写法长,但少一个创建数组实例的开销

ES6的继承方式

使用 extends 关键字,来实现继承

class Parent {
static name = 'parent'; //静态属性
work = 'aa'; //实例属性
}
class Child extends Parent {
constructor(age){
super(age); // 在此必须先调用super(),否则子类实例化时会报错,因为子类没有自己的this对象,而是继承父类的this对象
this.age = age;
}
say(){
log('child say', this.age);
}
// 通过 static 关键字标识类的静态方法
static work(){
log('child working')
}
}
Child.work(); //child working
var c1 = new Child(16);
log(c1.say());//child say 16

// 如果子类中没有 constructor 方法,ES6 会默认添加这个方法

constructor(...args){
super(...args);
}

class A extends Object{}
A.__proto__ === Object //true

class B extends Array{}
var arrB = new B();
b.push(1); //[1]
b.length; //1
  • ES5继承 是先创建子类的实例对象this,再向this对象中添加父类的方法
  • ES6继承 是先创造父类的实例对象this, 再用子类的构造函数修改this
  • Parent 可以是任意函数(只要有prototype属性)