面向对象、构造函数、原型、原型链与继承

一、对象的定义
在ECMAScript-262中,对象被定义为“无序属性的集合,其属性可以包含基本值,对象或者函数”。
也就是说,在JavaScript中,对象无非就是由一些列无序的key-value对组成。其中value可以是基本值,对象或者函数。
// 这里的person就是一个对象
var person = {
name: 'Tom',
age: 18,
getName: function() {},
parent: {}
}
创建对象
我们可以通过new的方式创建一个对象。
var obj = new Object();
也可以通过对象字面量的形式创建一个简单的对象。
var obj = {};
当我们想要给我们创建的简单对象添加方法时,可以这样表示。
// 可以这样
var person = {};
person.name = "TOM";
person.getName = function() {
return this.name;
}
// 也可以这样
var person = {
name: "TOM",
getName: function() {
return this.name;
}
}
访问对象的属性和方法
假如我们有一个简单的对象如下:
var person = {
name: 'TOM',
age: '20',
getName: function() {
return this.name
}
}
当我们想要访问他的name属性时,可以用如下两种方式访问。
person.name
// 或者
person['name']
如果我们想要访问的属性名是一个变量时,常常会使用第二种方式。
二、工厂模式
使用上面的方式创建对象很简单,但是在很多时候并不能满足我们的需求。就以person对象为例。假如我们在实际开发中,不仅仅需要一个名字叫做TOM的person对象,同时还需要另外一个名为Jake的person对象,虽然他们有很多相似之处,但是我们不得不重复写两次。
var perTom = {
name: 'TOM',
age: 20,
getName: function() {
return this.name
}
};
var perJake = {
name: 'Jake',
age: 22,
getName: function() {
return this.name
}
}
很显然这并不是合理的方式,当相似对象太多时,大家都会崩溃掉。
我们可以使用工厂模式的方式解决这个问题。顾名思义,工厂模式就是我们提供一个模子,然后通过这个模子复制出我们需要的对象。我们需要多少个,就复制多少个。
var createPerson = function(name, age) {
// 声明一个中间对象,该对象就是工厂模式的模子
var o = new Object();
// 依次添加我们需要的属性与方法
o.name = name;
o.age = age;
o.getName = function() {
return this.name;
}
return o;
}
// 创建两个实例
var perTom = createPerson('TOM', 20);
var PerJake = createPerson('Jake', 22);
很显然,工厂模式帮助我们解决了重复代码上的麻烦,让我们可以写很少的代码,就能够创建很多个person对象。但是这里还有两个麻烦,需要我们注意。
第一个麻烦就是这样处理,我们没有办法识别对象实例的类型。使用instanceof可以识别对象的类型,如下例子:
var obj = {};
var foo = function() {}
console.log(obj instanceof Object); // true
console.log(foo instanceof Function); // true
因此在工厂模式的基础上,我们需要使用构造函数的方式来解决这个麻烦。
三、构造函数
在JavaScript中,new关键字可以让一个函数变得与众不同。
function demo() {
console.log(this);
}
demo(); // window
new demo(); // demo
为了能够直观的感受他们不同,建议大家动手实践观察一下。很显然,使用new之后,函数内部发生了一些变化,让this指向改变。那么new关键字到底做了什么事情呢。
// 先一本正经的创建一个构造函数,其实该函数与普通函数并无区别
var Person = function(name, age) {
this.name = name;
this.age = age;
this.getName = function() {
return this.name;
}
}
// 将构造函数以参数形式传入
function New(func) {
// 声明一个中间对象,该对象为最终返回的实例
var res = {};
if (func.prototype !== null) {
// 将实例的原型指向构造函数的原型
res.proto = func.prototype;
}
// ret为构造函数执行的结果,这里通过apply,将构造函数内部的this指向修改为指向res,即为实例对象
var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
// 当我们在构造函数中明确指定了返回对象时,那么new的执行结果就是该返回对象
if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
return ret;
}
// 如果没有明确指定返回对象,则默认返回res,这个res就是实例对象
return res;
}
// 通过new声明创建实例,这里的p1,实际接收的正是new中返回的res
var p1 = New(Person, 'tom', 20);
console.log(p1.getName());
// 当然,这里也可以判断出实例的类型了
console.log(p1 instanceof Person); // true
所以,为了能够判断实例与对象的关系,我们就使用构造函数来搞定。
var Person = function(name, age) {
this.name = name;
this.age = age;
this.getName = function() {
return this.name;
}
}
var p1 = new Person('Ness', 20);
console.log(p1.getName()); // Ness
console.log(p1 instanceof Person); // true
关于构造函数,就先记住下面这几个结论吧。
与普通函数相比,构造函数并没有任何特别的地方,首字母大写只是我们约定的小规定,用于区分普通函数;
new关键字让构造函数具有了与普通函数不同的许多特点,而new的过程中,执行了如下过程:
1.声明一个中间对象;
2.将该中间对象的原型指向构造函数的原型;
3.将构造函数的this,指向该中间对象;
4.返回该中间对象,即返回实例对象。
四、原型
虽然构造函数解决了判断实例类型的问题,但是,说到底,还是一个对象的复制过程。跟工厂模式颇有相似之处。也就是说,当我们声明了100个person对象,那么就有100个getName方法被重新生成。
这里的每一个getName方法实现的功能其实是一模一样的,但是由于分别属于不同的实例,就不得不一直不停的为getName分配空间。这就是工厂模式存在的第二个麻烦。
显然这是不合理的。我们期望的是,既然都是实现同一个功能,那么能不能就让每一个实例对象都访问同一个方法?
当然能,这就是原型对象要帮我们解决的问题了。
我们创建的每一个函数,都可以有一个prototype属性,该属性指向一个对象。这个对象,就是我们这里说的原型。
当我们在创建对象时,可以根据自己的需求,选择性的将一些属性和方法通过prototype属性,挂载在原型对象上。而每一个new出来的实例,都有一个proto属性,该属性指向构造函数的原型对象,通过这个属性,让实例对象也能够访问原型对象上的方法。因此,当所有的实例都能够通过proto访问到原型对象时,原型对象的方法与属性就变成了共有方法与属性。
我们通过一个简单的例子与图示,来了解构造函数,实例与原型三者之间的关系。
// 声明构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 通过prototye属性,将方法挂载到原型对象上
Person.prototype.getName = function() {
return this.name;
}
var p1 = new Person('tim', 10);
var p2 = new Person('jak', 22);
console.log(p1.getName === p2.getName); // true

图示
通过图示我们可以看出,构造函数的prototype与所有实例对象的proto都指向原型对象。而原型对象的constructor指向构造函数。
除此之外,还可以从图中看出,实例对象实际上对前面我们所说的中间对象的复制,而中间对象中的属性与方法都在构造函数中添加。于是根据构造函数与原型的特性,我们就可以将在构造函数中,通过this声明的属性与方法称为私有变量与方法,它们被当前被某一个实例对象所独有。而通过原型声明的属性与方法,我们可以称之为共有属性与方法,它们可以被所有的实例对象访问。
当我们访问实例对象中的属性或者方法时,会优先访问实例对象自身的属性和方法。
function Person(name, age) {
this.name = name;
this.age = age;
this.getName = function() {
console.log('this is constructor.');
}
}
Person.prototype.getName = function() {
return this.name;
}
var p1 = new Person('tim', 10);
p1.getName(); // this is constructor.
在这个例子中,我们同时在原型与构造函数中都声明了一个getName函数,运行代码的结果表示原型中的访问并没有被访问。
我们还可以通过in来判断,一个对象是否拥有某一个属性/方法,无论是该属性/方法存在与实例对象还是原型对象。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function() {
return this.name;
}
var p1 = new Person('tim', 10);
console.log('name' in p1); // true
in的这种特性最常用的场景之一,就是判断当前页面是否在移动端打开。
isMobile = 'ontouchstart' in document;
// 很多人喜欢用浏览器UA的方式来判断,但并不是很好的方式
更简单的原型写法
根据前面例子的写法,如果我们要在原型上添加更多的方法,可以这样写:
function Person() {}
Person.prototype.getName = function() {}
Person.prototype.getAge = function() {}
Person.prototype.sayHello = function() {}
... ...
除此之外,我还可以使用更为简单的写法。
function Person() {}
Person.prototype = {
constructor: Person,
getName: function() {},
getAge: function() {},
sayHello: function() {}
}
这种字面量的写法看上去简单很多,但是有一个需要特别注意的地方。Person.prototype = {}实际上是重新创建了一个{}对象并赋值给Person.prototype,这里的{}并不是最初的那个原型对象。因此它里面并不包含constructor属性。为了保证正确性,我们必须在新创建的{}对象中显示的设置constructor的指向。即上面的constructor: Person。
五、原型链
原型对象其实也是普通的对象。几乎所有的对象都可能是原型对象,也可能是实例对象,而且还可以同时是原型对象与实例对象。这样的一个对象,正是构成原型链的一个节点。因此理解了原型,那么原型链并不是一个多么复杂的概念。
我们知道所有的函数都有一个叫做toString的方法。那么这个方法到底是在哪里的呢?
先随意声明一个函数:
function foo() {}
那么我们可以用如下的图来表示这个函数的原型链。

原型链


图片1.png

其中foo是Function对象的实例。而Function的原型对象同时又是Object的实例。这样就构成了一条原型链。原型链的访问,其实跟作用域链有很大的相似之处,他们都是一次单向的查找过程。因此实例对象能够通过原型链,访问到处于原型链上对象的所有属性与方法。这也是foo最终能够访问到处于Object原型对象上的toString方法的原因。
基于原型链的特性,我们可以很轻松的实现继承。
六、继承
我们常常结合构造函数与原型来创建一个对象。因为构造函数与原型的不同特性,分别解决了我们不同的困扰。因此当我们想要实现继承时,就必须得根据构造函数与原型的不同而采取不同的策略。
我们声明一个Person对象,该对象将作为父级,而子级cPerson将要继承Person的所有属性与方法。
function Person(name, age) {
this.name = name;
this.age = age;
}

Person.prototype.getName = function() {
return this.name;
}
首先我们来看构造函数的继承。在上面我们已经理解了构造函数的本质,它其实是在new内部实现的一个复制过程。而我们在继承时想要的,就是想父级构造函数中的操作在子级的构造函数中重现一遍即可。我们可以通过call方法来达到目的。
// 构造函数的继承
function cPerson(name, age, job) {
Person.call(this, name, age);
this.job = job;
}
而原型的继承,则只需要将子级的原型对象设置为父级的一个实例,加入到原型链中即可。
// 继承原型
cPerson.prototype = new Person(name, age);
// 添加更多方法
cPerson.prototype.getLive = function() {}

原型链
以上方式即为组合继承,也称伪经典继承,指的是将原型链和借用构造函数的技术组合到一起,其背后的思路是使用原型链实现对原型属性的继承,通过借用构造函数来实现实例属性的继承。这是js中最常用的继承模式。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,961评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,444评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,009评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,082评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,101评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,271评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,738评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,395评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,539评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,434评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,481评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,160评论 3 317
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,749评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,816评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,038评论 1 256
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,548评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,140评论 2 341