js中实现继承和传统的面向对象语言中有所不同:传统的面向对象语言的继承由类来实现,而在js中,是通过构造原型来实现的,原型与如下几个术语有关:
①构造函数:在构造函数内部拥有一个prototype属性,这个属性指向原型。在js中,构造函数和函数是属于一个概念范畴,都是引用类型,都可以实例化为对象。唯一不同的地方是使用上的不同,用new关键字来调用函数就能让这个函数变成一个构造函数,这一点很好理解,因为在像java、C#这中类C语言中构造函数就是和方法是同名的。而如果要实例化一个类,那么就用new来构造一个类的实例。prototype保存一个指针,指向这个构造函数的原型。实现继承后,原型中保存的引用类型(像Arrya、Function等)会被所有实例化后的对象所共享。原型是js中实现继承的重要方式,通过构造原型链,可以实现代码复用,后面会用代码来阐述这一点。
②原型:在原型内部保存一个constructor的属性,这个属性保存一个指针,指向构造函数。
③对象(实例):在实例内部保存一个[[prototype]]的指针,指向原型,通过这个[[prototype]],所有实例就可以访问原型中的所有属性和方法。
在继承上面需要注意的是原型中保存的基础类型的属性在继承后能被重写(由于js动态添加特性),这个是没有问题的(如string、number、bool等),问题出在引用类型的属性(和函数)上,如果在原型中定义了一个Array,那么在所有的实例中将共享这个属性,那么,在一个实例中对这个Array所做的任何改变都会体现到其他实例中,这就破坏了面向对象的程序设计中对封装的要求。所以,下面介绍一个如果去有效的建立继承,这里我记录两个最为理想的例子。
第一个:
'use strict';
function Person(name,age) {
this.name = name;
this.age = age;
this.address = ["a", "b"];
}
Person.prototype.getName = function() {
return this.name;
}
Person.prototype.favoriteColor = ["red", "blue"];
function Student(name, age) {
Person.call(this, name, age);
}
Student.prototype = new Person("parents", 60);
var student1 = new Student("xp", 20);
var student2 = new Student("xz", 30);
student2.address[2] = "c";
student1.favoriteColor[3] = "green";
console.log(student1.address);//['a','b']
console.log(student2.address);//['a','b','c']
console.log(student1.getName());//'xp'
console.log(student2.getName());//'xz'
console.log(student1.favoriteColor);//['red','green',blue']
console.log(student2.favoriteColor);//['red','green',blue']
上面这个代码说明了一个问题:使用构造原型链和借用构造函数这种组合的方式来实现继承的关键要点是在原型中声明函数,而在构造函数中声明属性,这种的意义在于原型中声明的属性或方法会被子类共享,如果属性是基础类型的那么不存在问题,但是如果声明的是引用类型的,那么在其中一个子类中对这个属性做出的修改会延续到其他的子类中,这样就破坏了封装性。我们要做的是在构造函数中声明属性,在原型中声明方法,这样就避免了上述出现的问题。
下面介绍第二种方式:
首先定义两个函数:
function object(o) { function F() { } F.prototype = o.prototype; return new F(); } function inheritPrototype(subType, superType) { var prototype = object(superType.prototype); prototype.constructor = subType; subType.prototype = prototype; }
一个函数是object(),这个函数接受一个构造函数的原型,object内部先定义一个构造函数,然后将传入的原型赋给这个定义好的构造函数的原型。
另一个函数是inheritPrototype,从字面意思上来讲就是继承原型。这个函数接受两个构造函数作为参数,在函数内部,使用object()函数返回一个实例化后的构造函数,接着将这个构造函数当作一个原型,并将为这个构造函数添加一个constructor的属性(constructor是原型中的属性,因为object函数返回的是一个构造函数,并没有constructor这个属性,所以要手动设置一个),并将constructor属性的值手动设置为传入的第一个参数(可以看出传入的第一个参数是作为子类,另一个是基类)。然后将第一个参数的原型设置为这个构造函数。这个函数基本解决了上面第一种所遇到的各种不好的情况:
"use strict"; function object(o) { function F() { } F.prototype = o; return new F(); } function inheritPrototype(subType, superType) { var prototype = object(superType.prototype); prototype.constructor = subType; subType.prototype = prototype; } function BaseClass(name, age) { this.name = name; this.age = age; this.favoriteColor = ["red", "green"]; //新添加的属性 } BaseClass.prototype.sayName = function () { console.log(this.name); }; function DerivedClass(name, age, job) { this.job = job; BaseClass.call(this, name, age); } inheritPrototype(DerivedClass, BaseClass);//替换DerivedClass.prototype=new BaseClass(); DerivedClass.prototype.sayHi = function () { console.log(this.job); } var derived1 = new DerivedClass("bob", 16, "SoftWare Engineer"); derived1.favoriteColor.push("black"); console.log(derived1.favoriteColor);//["red","green","blacjk"] derived1.sayName();//bob var derived2 = new DerivedClass("loryn", 17, "doctor"); console.log(derived2.favoriteColor);//["red","green"]
首先解决的是构造函数的使用次数,这个优化后的只使用了一次构造函数:
BaseClass.call(this, name, age);
然后解决的是原型链的问题:
inheritPrototype(DerivedClass, BaseClass);//替换DerivedClass.prototype=new BaseClass();
在inheritPrototype函数中调用object函数,object函数直接将传入的原型赋给新创建的这个构造函数的原型。而在inheritPrototype函数中完成了两个构造函数之间的继承关系。