在JavaScript中的继承的实质就是子代可以拥有父代公开的一些属性和方法,在js编程时,我们一般将相同的属性放到父类中,然后在子类定义自己独特的属性,这样的好处是减少代码重复。继承是面向对象的基础,是代码重用的一种重要机制。
——此文整理自 《jQuery 开发从入门到精通》 ,这是本好书,讲的很详细,建议购买阅读。
继承的作用
实现继承的主要作用是:
① 子类实例可以共享超类属性和方法。
② 子类可以覆盖和扩展超类属性和方法。
继承的分类
在JavaScript中是不支持类的概念,使用构造器机制来实现类的特性。
JavaScript中类的继承不止一种,主要包括:类继承(构造函数继承),原型继承,实例继承,复制继承,克隆继承,混合继承,多重继承等。
类继承
类继承也叫构造函数继承,其表现形式是在子类中执行父类的构造函数。实现本质:比如把一个构造函数A的方法赋值为另一个构造函数B,然后调用该方法,使构造函数A在构造函数B内部执行,这是构造函数B就拥有了构造函数A中定义的属性和方法。这就是B类继承A类。示例如下:
function A(x){ this.x = x; this.say = function() { console.log(this.x + ' say'); } } function B(x,y) { this.m = A; // 把构造函数A作为一个普通函数引给临时方法m() this.m(x); // 把当前参数作为值传给构造函数A,并且执行 delete this.m; // 清除临时方法 this.y = y; this.call = function(){ console.log(this.y); } } // 测试类继承 var a = new A(1); var b = new B(2,3); a.say(); // 1 say b.say(); // 2 say b.call(); // 3
上面的实现方式很巧妙对吧,但是这种设计方式太随意,缺乏严密性。严禁的设计模式应该考虑到各种可能存在的情况和类继承关系中的互相耦合性。所以一般我们尽可能把子类自身的方法放在原型里去实现。下面这种创建方式会更好:
function A(x){ this.x = x; this.say = function() { console.log(this.x + ' say'); } } function B(x,y){ this.y = y; A.call(this,x); // 在构造函数B内调用超类A,实现绑定,用call来实现借用 } B.prototype = new A(); // 设置原型链,建立继承关系 B.prototype.constructor = B; // 恢复B的原型对象的构造函数为B,如果不操作就会指向A B.prototype.gety = function(){ // 给类B添加新的方法 return this.y; } // 测试 类继承 var a = new A('A'); var b = new B('Bx','By'); a.say(); // A say b.say(); // Bx say console.log(b.gety()); // By console.log(B.prototype.__proto__ === A.prototype); // true; console.log(b.constructor === B); // true 这里也可以把B中的 B.prototype.constructor = B; 去除,结果为false
在js中实现类继承,需要设置3点:
① 在子类构造函数结构体内,使用函数call()调用父类构造函数,把子类的参数传递给调用函数如上面的例子:A.call(this,x) 这样子类可以继承父类的所有属性和方法。
② 在子类和父类之间建立原型链,如上例:B.prototype = new A() 为了实现类的继承必须保证他们原型链上的上下级关系。即设置子类的prototype 属性指向父类的一个实例即可。
③ 恢复子类原型对象的构造函数, 如上例:B.prototype.constructor = B
在类继承中,call() 和 apply() 方法被频繁使用,它们之间的功能和用法都是相同的,唯一区别就是第2个参数类型不同。如果深入,请看前面的文章:https://www.jb51.net/article/166093.htm
此处需要提一下,就是类的构造函数中的成员,一般称之为本地成员,而类的原型成员就是类的原型中的成员,此处我们只考虑原型中的成员继承。本地成员继承可以用call 和 apply。下面我们来看下类继承的原型成员的继承封装函数:
在函数体内,首先定义一个空函数F(),用来实现功能中转,把它的原型指向父类的原型,把空函数的实例传递给子类的原型,这样就避免了直接实例化父类引发的内存问题,因为实际开发中,父类可能很大,实例化后,会占用很大一部分内存。
// 定义一个继承函数 function extend(Sub,Sup){ // 有两个入口参数,Sub是子类,Sup是父类 var F = function(){}; // 建立一个临时的构造函数 F.prototype = Sup.prototype; // 把临时构造函数的原型指向父类的原型 Sub.prototype = new F(); // 实例化临时类,此处相当于把子类的原型指向父类的实例 Sub.prototype.constructor = Sub; // 恢复子类的构造函数 Sub.sup = Sup.prototype; // 在子类中定义一个本地属性存储超类原型,可避免子类和超类耦合 if(Sup.prototype.constructor === Object.prototype.constructor) { // 检测超类构造器是否为Object构造器 Sup.prototype.constructor = Sup; } } // 下面定义两个类用来测试上面的继承函数 function A(x,y) { this.x = x; this.y = y; } A.prototype.add = function() { return (this.x-0) + (this.y-0); // 此处-0 的目的是确保字符串类型可转成数值型 } A.prototype.minus = function() { return this.x - this.y; } function B(x,y) { A.apply(this,[x,y]); } // 开始实现类继承中的原型成员继承 extend(B,A); // 为了不与A类中的代码耦合可以单独为B定义一个同名的add B.prototype.add = function() { return B.sup.add.call(this); // 避免代码耦合 } // 测试继承 var b = new B(1,2); console.log(b.minus()); // -1 console.log(b.add()); // 3
原型继承