JS学习篇(四) 对象与继承

初学JavaScript(下称JS)的基本都知道,创建一个对象可以很容易:

var obj = {};

没错,上面代码就创建了一个空对象obj,接下来可以给它加多点料:

var person = {
    name: "Tom",
    age: 24
}

console.log(person.name);  //输出Tom
console.log(person.age);   //输出24

这里新创建了一个对象person,而它相比前面的空对象obj多了两个属性,分别是name和age,在后面的代码可以存取它们的值。

如果只会上面这一点的话,对于开发复杂的项目是不足够的,上面这种方法用来创建一个对象来使用固然十分简单,但是JS中对象的作用不仅限于此,下面将进一步讲述。


对象

在“JS学习篇(二) JavaScript七种数据类型”的笔记里也有提到过对象Object作为JS的七种基本类型之一,那么对象与其他几种类型对比起来有什么特别?

一个对象就是一系列属性的集合,一个属性包含一个键名和一个值,这也叫键值对。一个属性的值可以是函数,这种情况下属性也被称为方法。除了浏览器里面预定义的那些对象(例如Date、RegExp、String等等)之外,我们也可以定义自己的对象(就像上面那样)来使用。

上面这段话用来定义JS对象应该没什么问题,不过对象为啥要设计成这样,它这样设计的意义是什么呢?对象的作用是解决现实问题。对象也叫Object(物体),我们可以在代码中用对象来表示现实中任意的事物,例如用一个叫car的对象来表示一辆车,它可以拥有若干属性(品牌、型号、颜色、尺寸、速度等),它也可以拥有若干方法(前进,后退,加速,刹车灯),所有这些属性和方法都可以为这个car对象所用。

更进一步的,我或许需要在编程过程中操作多辆车,那我是不是用同样的方法再写一遍上面的属性和方法呢?当然没这个必要!这时候就需要使用抽象这种手法,简单点说就是把所有车(物体)的共同特征使用属性和方法来定义出来,形成一个构造函数,然后每次创建一辆车的时候调用这个构造函数即可,不同的车辆对象可以通过传递不同的参数值以区分它们,也可以先创建后修改。通过这样一种抽象方式,我们在代码中只需要写一遍代码(定义构造函数),以后在任何地方都可以简单地创建所需的对象。

上面提到抽象,其实这种设计方式在C++,Java这类编程语言中也十分普遍,在这些语言中使用对象前,需要定义一个类,这个类的定义其实就是抽象的过程,把所有对象的共同特性归纳出来并进行定义,然后通过类来创建一个对象,对象是类的实例。

C++和Java这种具有类概念的叫做基于类的面向对象语言,而JS则是一种基于原型的面向对象语言。

基于原型的语言(如 JavaScript)只有对象。基于原型的语言具有所谓原型对象的概念。原型对象可以作为一个模板,新对象可以从中获得原始的属性。任何对象都可以指定其自身的属性,既可以是创建时也可以在运行时创建,而且任何对象都可以作为另一个对象的原型,从而允许后者共享前者的属性。

单看上面这段话可能会有以下疑问:既然JS没有类那么对象是怎么创建的?基于原型又怎么理解?原型对象又是啥呀?后面逐一解答。

与C++和Java不同的是,JS中没有类这个概念,JS只有对象,JS对象是由构造函数创建的。

看下面代码,通过一个构造函数来定义对象的类型:

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}

单看这个语法看上去跟普通函数是一样的,但只有一个区别——this,通过this可以将传入函数的参数赋给特定的对象。

接下来可以使用构造函数来创建一个对象:

var mycar = new Car("Eagle", "Talon TSi", 1993);

console.log(mycar.make);   //输出"Eagle"
console.log(mycar.model);  //输出"Talon TSi"
console.log(mycar.year);   //输出1993

//也可以修改它们的值,例如:
mycar.year = 2000;

console.log(mycar.year);   //输出2000

上面代码的开头通过new关键字加上构造函数名和参数列表,可以很方便地创建一个对象mycar,然后可以用它来操作对象以及它们的属性了。

如果你想的话也可以创建更多对象,并赋给它们不同的初始值:

var kenscar = new Car("Nissan", "300ZX", 1992);
var vpgscar = new Car("Mazda", "Miata", 1990);

可以看到,通过Car这个构造函数可以很方便的创建更多的对象了。

现在回过头来,我们再看本文开头创建一个对象的简易方式:

var obj = {};

 这种创建对象的方式叫做对象字面量,类似于双引号对""创建一个字符串以及中括号对[]创建一个数组,只是不同的语法而已,这里使用大括号对{}来创建对象。其实这种语法所创建的对象也是调用了构造函数,它调用了Object这个构造函数,它类似于下面这样创建对象:

var obj = new Object();

事实上,JS中的所有对象都来自于Object,像上面这里创建的对象就是直接调用Object构造函数,那么它肯定是来自Object,它是Object构造函数的一个实例。在JS中有一个操作符instanceof可以用来判断一个对象是否一个构造函数的实例:

var obj = new Object();

console.log(obj instanceof Object);  //输出true

那么我上面自己定义的Car构造函数以及它的对象呢?可以在浏览器控制台试试下面的代码:

console.log(mycar instanceof Car);   //输出true

在看看上面说到所有JS对象都来自于Object,再试试下面的代码:

console.log(mycar instanceof Object);   //输出true

 这里同样输出true!说明mycar对象也是一个Object实例!说到这里,我开始有点好奇,再测试一行代码:

console.log(Car instanceof Object);   //输出true

好,这里基本印证了我的猜想,mycar对象是一个Car实例,同时Car构造函数也是一个Object实例,也就是说Car构造函数也是一个对象。要说明这是为什么,这将涉及面向对象编程的另一个概念——继承

 

继承

下面讲述的将是JS语言核心中的一个重点难点,继承是什么意思?同时作为一种面向对象语言,它是通过什么方式来进行继承的呢?

首先说说继承,虽然说JS中通过抽象出构造函数的方式来创建对象是一个方便的设计,但是现实世界中的问题往往是复杂多样的,不是说每样实体都定义一个构造函数这样方式不可行,而是不经济,试想一下在我已经有一个Car构造函数的前提下,如果我想要一个跑车的对象,这辆跑车的拥有普通车辆所没有的特性(额外的属性和方法),这时我可以选择创建一个Car对象,再动态给它加上新的属性和方法后再使用它;另外一种选择是,新定义一个跑车的构造函数,把重复的属性和方法从Car那边复制过来,然后添加新的属性和方法,再创建这个跑车对象。上面这两种方法都不够好,它存在一个问题,那就是重复定义,无论使用上面哪一种方法,我都需要重复多次定义相同的属性和方法才能使用新的跑车对象,这种方式要么造成代码冗余导致重用性不够高,要么导致程序的运行效率下降。这时候需要一个新的机制,那就是继承。

在JS中,继承就是基于上一个对象类型的属性方法,再定义一个新的构造函数,这个新构造函数所创建的对象将拥有上一个对象类型的属性和方法。那么具体怎么用代码实现呢?

下面利用一个Employee 的层级结构说明:

  • Employee 具有 name 属性(默认值为空的字符串)和 dept 属性(默认值为 "general")。
  • Manager 是 Employee的子类。它添加了 reports 属性(默认值为空的数组,以 Employee 对象数组作为它的值)。
  • WorkerBee 是 Employee的子类。它添加了 projects 属性(默认值为空的数组,以字符串数组作为它的值)。
  • SalesPerson 是 WorkerBee的子类。它添加了 quota 属性(其值默认为 100)。它还重载了 dept 属性值为 "sales",表明所有的销售人员都属于同一部门。
  • Engineer 基于 WorkerBee。它添加了 machine 属性(其值默认为空字符串)同时重载了 dept 属性值为 "engineering"。

虽然在JS中实际上没有父类和子类这种基于类的面向对象语言才有的术语,在JS中的构造函数与类有本质区别,因为JS的构造函数本质上也是一个对象,但类并不是一个对象,这里为了方便描述也使用了类相关的术语。

下面将用代码实现以上层级结构的继承:

//第一层
//定义Employee构造函数作为父类
function Employee () {
  this.name = "";
  this.dept = "general";
}

//第二层
//定义Manager构造函数作为Employee的子类
function Manager() {
  this.reports = [];
}
Manager.prototype = new Employee;

//定义WorkerBee构造函数作为Employee的子类
function WorkerBee() {
  this.projects = [];
}
WorkerBee.prototype = new Employee;

//定义SalesPerson构造函数作为WorkerBee的子类
function SalesPerson() {
   this.dept = 'sales';
   this.quota = 100;
}
SalesPerson.prototype = new WorkerBee;

//定义Engineer构造函数作为WorkerBee的子类
function Engineer() {
   this.dept = 'engineering';
   this.machine = '';
}
Engineer.prototype = new WorkerBee;

在说明以上代码之前,先创建上面各个构造函数的对象:

var jim = new Employee; // 如构造函数无须接受任何参数,圆括号可以省略。
// jim.name is ''
// jim.dept is 'general'

var sally = new Manager;
// sally.name is ''
// sally.dept is 'general'
// sally.reports is []

var mark = new WorkerBee;
// mark.name is ''
// mark.dept is 'general'
// mark.projects is []

var fred = new SalesPerson;
// fred.name is ''
// fred.dept is 'sales'
// fred.projects is []
// fred.quota is 100

var jane = new Engineer;
// jane.name is ''
// jane.dept is 'engineering'
// jane.projects is []
// jane.machine is ''

可以看到,除了Employee父类所创建的对象jim拥有name和dept属性以外,下面所有继承的子类所创建对象都拥有这两个属性,而且属性值也是继承过来的,也就是说子类继承父类的属性。除此之外,fred和jane两个对象还分别在他们构造函数中谁知了dept属性,使其的值分别为'sales'和'engineering',这种子类重设父类同名属性的方式叫做覆盖

通过上面构造函数定义实现继承的代码发现,它比之前定义一个构造函数时多了一行代码,提炼多出代码如下所示:

Manager.prototype = new Employee;

WorkerBee.prototype = new Employee;

SalesPerson.prototype = new WorkerBee;

Engineer.prototype = new WorkerBee;

发现每定义一个子类的构造函数就需要设置一次改构造函数的prototype属性,这个属性所指向的对象就是上面提到过的原型对象,可以看到每个prototype属性都是通过new加上父类构造函数名来创建,创建的是一个父类对象,这个对象里面包含里父类的所有属性。

通过设置子类构造函数的prototype属性,那么子类构造函数所创建的对象就拥有了父类对象的所有属性了吗?要理解这个过程好像还缺少一个关键步骤连接起来,我们再看看使用new来创建一个对象时,JS解释器在背后究竟发生了什么操作,下面以WorkerBee的对象为例:

var mark = new WorkerBee;

1. JS解释器发现new操作符后,创建一个通用的JS对象,然后将作为this关键字的值传递给WorkerBee构造函数;

2. 该构造函数显式地设置projects属性的值为空数组[],然后隐式地将对象内部隐藏的[[Prototype]]属性设置为WorkerBee.prototype的值,这个操作等于将新对象的__proto__属性设置为WorkerBee.prototype,这时候实际上新对象已经拥有了Employee的所有属性了。

3.最后将新创建的WorkerBee对象赋值给mark变量。

从以上步骤得知,实际上mark对象自身只有一个在WorkerBee构造函数中显式定义的projects属性,这个叫自身属性,而其他继承而来的属性,都在继承链的__proto__属性上。实际上JS有一种机制,当请求属性的值时,JavaScript 将首先检查对象自身中是否存在属性的值,如果有,则返回该值。如果不存在,JavaScript会检查原型链(使用内置的 [[Prototype]] 属性)。如果原型链中的某个对象包含该属性的值,则返回这个值。如果没有找到该属性,JavaScript 则认为对象中不存在该属性。这样,mark对象中将具有如下的属性和对应的值:

mark.name = "";
mark.dept = "general";
mark.projects = [];

通过上面的分析得知,实际上不止WorkerBee,整个层级结构都是这样方式去实现继承,从而生成以下继承链(从左往右代表继承链从下到上):

jane —— Engineer —— WorkerBee —— Employee —— Object

fred —— Engineer —— WorkerBee —— Employee —— Object

mark —— WorkerBee —— Employee —— Object

sally —— Manager —— Employee —— Object

通过代码测试jane对象所代表的原型继承链:

console.log(jane.__proto__ == Engineer.prototype);
//输出true
console.log(jane.__proto__.__proto__ == WorkerBee.prototype);
//输出true
console.log(jane.__proto__.__proto__.__proto__ == Employee.prototype);
//输出true
console.log(jane.__proto__.__proto__.__proto__.__proto__ == Object.prototype);
//输出true
jconsole.log(ane.__proto__.__proto__.__proto__.__proto__.__proto__ == null);
//输出true

在此验证了前面提到的JS中的所有对象都来自于Object,而Object的原型则为null,所以最后一个__proto__值与null相等。

 

上面是目前我对JS对象与继承的总结,目前只能算是有一个初步的认识,实际上还有其他方法去创建和实现继承,暂时不在这里介绍。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值