JS中的类与继承
1.面向过程与面向对象
-
面向过程
按照传统流程编写一个一个函数来解决需求的方式,即为面向过程变成方式
-
面向对象
把需求抽象成一个对象,然后针对这个对象分析其特征(属性)与动作(方法),这个过程就是面向对象编程。其中,这个对象就是我们所说的类,把这些需求整理放在一个对象里的过程,就称之为封装的过程。Javascript这种解释性的弱类型语言没有经典强类型中那种通过class等关键字实现的类的封装方式,js中都是通过一些特性模仿实现的。
2.封装
2.1 创建一个类
首先声明一个函数保存在一个变量里,然后在这个函数(变量)内部通过对this(函数内部自带的一个变量,用于指向当前对象)变量添加属性或者方法来实现对类添加属性或方法。如:
var Book = function (id, name, price){
this.id = id;
this.name = name;
this.price = price;
}
也可在类的原型上添加属性和方法(2种方式)
- 为原型对象属性赋值
Book.prototype.display = function (){
//动作方法
}
- 将一个对象赋值给类的原型对象
Book.prototype = {
display: function(){}
}
这里说一下constructor
constructor是一个属性,当创建一个函数或者对象时,都会为其创建一个原型对象prototype,在prototype对象中又会像函数中创建this一样创建一个constructor属性,那么,constructor属性指向的就是拥有整个原型对象的函数或对象,例如以上例子中constructor属性指向的就是Book类对象。
2.2 属性与方法封装
类似以前学校中所学私有、公有属性和方法,js类中的属性和方法也有所区别:
//私有属性与私有方法、特权方法,对象共有属性/方法,构造器
var Book = function (id, name, price){
//私有属性
var num = 1;
//私有方法
function checkId(){}
//特权方法
this.getName = function () {};
this.getPrice = function () {};
this.setName = function () {};
this.setPrice = function () {};
//对象公有属性
this.id = id;
//对象公有方法
this.copy = function () {};
//构造器
this.setName(name);
this.setPrice(price);
}
//类静态公有属性(对象不能访问)
Book.isChinese = true;
//类静态公有方法(对象不能访问)
Book.resetTime = function () {
console.log("new Time");
};
Book.prototype = {
//公有属性
isJSBook : false,
//公有方法
display: function () {}
}
- 私有属性、方法
由于js的函数级作用域,申明在函数内部的变量和方法是外部访问不到的,由此特性,可创建类的私有属性和方法 - 对象共有属性、方法
在函数内部通过this创建属性和方法,在类创建对象时,每个对象自身都会拥有一份并且外部可以访问到,因此通过this创建的可看做是对象共有属性、方法。 - 特权方法
通过this创建的方法,不但可以访问这些对象的公有属性和方法,还能访问到类(创建时)或对象自身的私有属性和方法,由于这些方法权利比较大,所以可看做特权方法。 - 构造器
由于js的函数级作用域,申明在函数内部的变量和方法是外部访问不到的,由此特性,可创建类的私有属性和方法 - 类的静态共有属性、方法
在类外面通过点语法定义的属性和方法(通过new关键字创建新对象时,这些属性和方法执行不到。故新创建的对象无法获取,但是可以通过类来使用) - 共有属性、方法
类通过prototype创建的属性或者方法再类实例的对象中是可以通过this访问到的,这些称为共有属性、方法
参考如下测试结果:
var b = new Book(10,"Javascript设计模式",50);
console.log(b.num); //undefined
console.log(b.isJSBook); //false
console.log(b.id); //11
console.log(b.isChinese); //undefined
2.3 创建对象的安全模式
实际创建对象的过程中,我们常常会忘记new这一步,试想如果直接执行
var book = Book("Javascript", 2014,'js');
console.log(book.name); //undefined
没有通过new进行实例化,为什么会是这样的的结果呢?
首先,new关键字的作用可以看做是对当前对象的this不停地赋值,如果没有new,则是直接执行Book这个函数,并且是在全局作用域中执行的,由此可想而知,此时的this指向的当前对象必定是全局变量window了。由于Book函数中并无返回值,故执行结果为undefined。
知道了上面的原因,那么如何避免以上问题呢?
可参考如下安全模式:
var Book = function (title, time, type) {
//判断执行过程中this是否是当前这个对象(如果是,说明是new创建的)
if(this instanceof Book){
this.title = title;
this.time = time;
this.type = type;
}else {
//否则重新创建这个对象
return new Book(title, time, type);
}
}
var book = Book("Javascript", 2014,'js');
3.继承
因为Javascript中并没有继承这一现有的机制,使得继承的实现存在更多地可能性可多样性,那这里的继承又可以怎么实现呢?
3.1 子类的原型对象-类式继承
//声明父类
function SuperClass(){
this.superValue = true;
};
//为父类添加共有方法
SuperClass.prototype.getSuperValue = function () {
return this.superValue;
};
//声明子类
function SubClass() {
this.subValue = false;
}
//继承父类
SubClass.prototype = new SuperClass();
//为子类添加公有方法
SubClass.prototype.getSubValue = function () {
return this.subValue;
}
这里为什么要将第一个类的实例赋值给第二个类的原型呢?
类的原型对象的作用就是为类的原型添加共有方法,但类不能直接访问这些属性和方法,需要通过原型prototype来访问。我们实例化一个父类的时候,新创建的对象复制了父类构造函数内的属性和方法,并且将原型_proto_指向了父类的原型对象,这样,新创建的对象可直接访问父类原型上的属性和方法,也可访问从父类构造函数中复制的属性和方法,这样又赋给了子类的原型,则员子类原型也有了以上权限。由此实现类式继承。
instanceof是通过判断对象的prototype链来确定这个对象是否是某个类的实例,而不关心对象与类的自身结构
var instance = new SubClass();
console.log(instance.getSuperValue()); //true
console.log(instance.getSubValue()); //false
console.log(instance instanceof SuperClass); //true
console.log(instance instanceof SubClass); //true
console.log(SubClass instanceof SuperClass); //false
console.log(SubClass.prototype instanceof SuperClass); //true
console.log(instance instanceof Object); //true
所创建的所有对象都是Object的实例
以上有两个问题存在:
①由于子类通过其原型prototype对父类实例化,继承了父类,如果父类中的共有属性是引用类型,就会在自类中被所有实例共用,因此一个子类的实例更改子类原型从父类构造函数中继承来的共有属性就会直接影响到其他子类
②由于子类实现的继承是靠其原型prototype对父类的实例化实现的,因此在创建父类的时候,是无法向父类传递参数的,因而在实例化负累的时候也无法对父类构造函数内的属性进行初始化。
3.2 创建即继承-构造函数继承
//声明父类
function SuperClass(id){
this.books = ['Javascript', 'html', 'css'];
this.id = id;
};
//父类申明原型方法
SuperClass.prototype.showBooks = function () {
console.log(this.books);
};
//声明子类
function SubClass(id) {
//继承父类
SuperClass.call(this, id);
}
//创建第一个子类的实例
var instance1 = new SubClass(10);
//创建第二个子类的实例
var instance2 = new SubClass(11);
instance1.books.push("设计模式");
console.log(instance1.books); //['Javascript', 'html', 'css', '设计模式']
console.log(instance1.id); //10
console.log(instance2.books); //['Javascript', 'html', 'css']
console.log(instance2.id); //11
instance1.showBooks(); //typeError
==SuperClass.call(this, id)==这条语句是构造函数式继承的精华,用call临时更改函数作用环境,对SuperClass调用就是讲子类中的变量再父类中执行一遍,由于父类中是给this绑定属性的,因此子类自然也就继承了父类的共有属性。
由于这种继承没有涉及原型,所以父类的原型方法是不会被继承的(放在构造函数内才会被继承),这又是一个问题。
3.3 组合继承
//声明父类
function SuperClass(name){
this.name = name;
this.books = ['Javascript', 'html', 'css'];
};
//父类原型共有方法
SuperClass.prototype.getName = function () {
console.log(this.name);
};
//声明子类
function SubClass(name,time) {
//构造函数式继承父类name属性
SuperClass.call(this, name);
this.time = time;
}
//类式继承 子类原型继承父类
SubClass.prototype = new SuperClass();
//子类原型方法
SubClass.prototype.getTime = function () {
console.log(this.time);
}
这样将原型继承和构造函数继承组合起来用,构造函数继承使子类的实例中更改父类继承下来的引用类型属性,不会影响到其他实例,且子类实例化过程中能够将参数传递给父类构造函数中
测试结果如下:
var instance1 = new SubClass("js book",2014);
instance1.books.push("设计模式");
console.log(instance1.books); //['Javascript', 'html', 'css', '设计模式']
instance1.getName(); //js Book
instance1.getTime(); //2014
var instance2 = new SubClass("css book",2013);
console.log(instance2.books); //['Javascript', 'html', 'css']
instance2.getName(); //css Book
instance2.getTime(); //2013
这种方式解决了前面所说的问题,但还存在另一个问题:
我们在构造函数继承时执行了一遍父类构造函数,而在实现子类原型的类式继承时又执行了一遍。因此父类构造函数调用了两遍。所以这个方案还是有瑕疵。
摘抄自张容铭的Javascript 设计模式。加深理解类、原型与继承。