javascript面向对象

一、封装

在es6之前,javascript是没有类(class)的概念的,但是又确实是一种基于对象的语言,所以通常情况下我们都是把属性和方法封装成一个对象,可以直接从对象生成一个实例对象,下面我们来看看具体实现的方法。

1、生成实例对象的原始模式

假设我们把人看作是一个原型对象,他有名字和年龄两个属性。

var People = {
   name:'',
   age:''
}

现在我们根据这个原型对象生成两个实例

var people1 = {};
  people.name = 'people1';
  people.age = 12;

var people2 = {};
  people.name = 'people2';
  people.age = 13;

这个就是最原始的封装,把两个属性封装在一个对象里面。但是这样的写法有两个缺点,一是生成实例太多,写起来就很麻烦。二是实例与原型之间看不出来有任何联系。

2、原始模式的改进

写一个函数解决代码重复的问题

function People(name,age){
  return {
    name:name,
    age:age
    }
  }

然后生成实例对象,等于是在调用函数:

var people1 = People('people1',12);
var people2 = People('people2',13);

但是这种方法问题是,people1和people2之间没有什么联系,看不出来是同一个原型的实例。

3、构造函数的模式

为了解决从原型对象生成实例的问题javascript提供了一个构造函数,使用new运算符就能生成实例,并且this变量会绑定到实例对象上
比如:上面人的原型对象现在就可以这样写了。

function People (name,age){
  this.name = name;
  this.age = age;
  }

现在就可以生成实例对象了:

var people1 = new ('people1',12);
var people2 = new('people2',13);
console.log(people1.name); // people1

这时people1和people2会自动含有一个constructor属性,指向它们的构造函数。

console.log(people1.constructor == People) // true
console.log(people2.constructor == People) // true

4、构造函数模式的问题

构造函数很好用,但是存在一个消耗内存的问题。
下面为People对象添加一个不变的属性height,那么People原型对象就变成这样:

function People (name,age){
  this.name = name;
  this.age = age;
  this.height = 170;
  }

还是采用同样的方法生成实例:

var people1 = new ('people1',12);
var people2 = new('people2',13);
console.log(people1.height); // 170

表面上看没什么问题,但是实际上这样做有一个很大的弊端。那就是对于每一个实例对象,height属性都是一样的内容,每生成一个实例都会为重复的内容,多占用一些内存。这样既不环保,也缺乏效率。

console.log(people1.height == people2.height) // false

能不能让height属性在内存中只生成一次,然后所有实例都指向那个内存地址呢?答案是可以的。

5、Prototype模式

javascript规定,每一个构造函数都有prototype属性,指向另外一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。
这就意味着,我们可以把那些不变的属性和方法直接定义在prototype对象上。

function People (name,age){
  this.name = name;
  this.age = age;
  }
People.prototype.height = 170;
People.prototype.say = function(){console.log('hello)};

生成实例。

var people1 = new ('people1',12);
var people2 = new ('people2',13);
console.log(people1.height); // 170
console.log(people1.say()); // hello

这时所有实例的height属性和say方法,其实都是一个内存地址,指向prototype对象,因此就提高了运行效率。

console.log(people1.height == people2.height) // true

6 、Prototype模式的验证方法

为了配合prototype属性,javascript定义了一些方法辅助我们使用它们。

6.1、isPrototypeOf()

这个方法用来判断,某个prototype对象和某个实例对象之间的关系

console.log(People.prototype.isPrototypeOf(people1)); //true
console.log(People.prototype.isPrototypeOf(people2)); //true

6.2、 hasOwnProperty()

每个实例对象都有一个hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承自prototype对象的属性。

console.log(people1.hasOwnProperty("name")); // true
console.lot(people1.hasOwnProperty("height")); // false

二、构造函数的继承

我们已经知道了如何封装属性和方法,以及如何从原型对象上生成实例,
但是如何实现对象之间的继承呢?
比如现在有一个‘人’对象的构造函数。

function People (){
  this.name = 'people';
}

还有一个‘中国人’对象的构造函数。

function Chinese (height) {
  this.height = height ;
}

怎么才能使‘中国人’继承‘人’呢?

1.构造函数绑定

第一种方法也是最简单的办法,使用apply或者call方法,将父对象的构造函数绑定在子对象上。

function Chinese (height) {
  People.call(this, arguments);
  this.height = height ;
}
var chinese1 = new Chinese();
console.log(chinese.name); // people

2、prototype模式

这种方法更常见,使用prototype属性
如果‘中国人’的prototype对象指向People实例,那么所有的‘中国人’的实例,就能继承People了。

Chinese.prototype = new People();
Chinese.prototype.constructor = Chinese;
var chinese1 = new Chinese();
console.log(chinese.name); // people

代码的第一行,我们将Chinese的prototype对象指向一个People实例。

Chinese.prototype = new People();

它相当于完全删除了prototype对象原先的值,然后赋了一个新的值。第二行代码又是在干什么呢?

Chinese.prototype.constructor = Chinese;

原来任何一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有‘Chinese.prototype = new People()’这一句话,Chinese.prototype.constructor是指向Chinese的,现在是指向People的。

console.log(Chinese.prototype.constructor == People); // true

一次在加上Chinese.prototype.constructor = Chinese这句以后,Chinese.prototype.constructor指向了Chinese;

console.log(Chinese.prototype.constructor == Chinese); // true

更重要的是每一个实例也有constructor属性,默认调用prototype对象的constructor属性。

console.log(chinese1.constructor == Chinese.prototype.constructor); // true

因此在运行‘Chinese.prototype = new People();’之后,chinese1.constructor也指向了People。

console.log(chinese1.constructor == People); //true

这显然会造成继承链的混乱,people1明明是People生成的,因此我们必须手动纠正,将Chinese.prototype对象的constructor值改为Chinese。这是很重要的一点,编程时一定要遵守。

3、直接继承prototype

第三种方法是对第二种方法的改进。在People对象中,不变的属性可以直接写入People.prototype。所以我们可以让Chinese跳过People直接继承People.prototype.
实现方式如下:

function People(){};
PeoPle.prototype.name = 'people';

然后将将Chinese的prototype对象指向People.prototype,这样就完成了继承。

Chinese.prototype = Chinese.prototype;
Chinese.prototype.constructor = Chinese;
var chinese1 = new Chinese();
console.log(chinese.name); // people

与前一种方法相比,这样做的优点是效率高(不用执行和建立People实例),就比较省内存,缺点就是Chinese.prototype和People.prototype都指向了同一个对象,那么任何对Chinese.prototype的修改也同时反映到了People.prototype上。
所以上面的这一段代码是有问题的。

Chinese.prototype.constructor = Chinese;

这一句话实际上也把People.prototype对象的constructor属性也改了。

console.log(People.prototype.constructor ); // Chinese

4、利用空对象作为中介

由于直接继承prototype存在上述的缺点,所以我们可以用一个空对象作为中介。

var F = function(){};
F. prototype = People.prototype;
Chinese.prototype = new F();
Chinese.prototype.constructor = Chinese;

F 是一个空对象,几乎不占内存。这时修改Cat的prototype对象就不会影响到People的prototype对象。

console.log(Chinese.prototype.constructor ); // Chinese

我们将上面的方法封装成一个方法,便于调用。

function extend(Child,Parent){
  var F = function(){};
  F. prototype = Parent.prototype;
  Chind.prototype = new F();
  Child.prototype.constructor = Chind;
  Child.uber = Parent.prototype;  
}

使用方式如下

extend(Chinese,People);
var chinese1 = new Chinese();
console.log(chinese.name); // people

另外说明一点,函数最后一行

  Child.uber = Parent.prototype;  

意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。

5、拷贝继承

上面采用prototype对象实现继承,我们可以换一种思路,纯粹采用拷贝方法实现继承。简单说如果把父对象的所以属性和方法拷贝进子对象,也能实现继承。

function extend2(Child,Parent){
  var p = Parent.prototype;
  var c = Child.prototype;
  for(var i in p){
  c[i] = p[i];
  c.uber = p;
  }
}

这个函数的作用,就是将父对象的prototype对象中的属性,一一拷贝给Child对象的prototype对象。
使用的时候,这样写:

extend(Chinese,People)
var chinese1 = new Chinese();
console.log(chinese.name); // people

三、非构造函数的继承

前面提到的都是构造函数实现的继承,那么不使用构造函数,怎么能实现继承呢?

1、什么是非构造函数的继承

比如我有个对象叫“中国人”。

var Chinese = {
  nation:'中国'
}

还有一个对象叫“医生”。

var Doctor = {
  career:'医生'
}

那要怎样才能让“医生”去继承“中国人”,也就是说,要怎样才能去生成一个中国医生。这两个都是普通对象不是构造函数,无法用构造函数的方式实现继承。

2、object()方法

function object(o){
  function F (){}
  F.prototype = o;
  returen new F();
}

这个object函数只做一件事,就是把子对象的prototype指向父对象,从而使得子对象与父对象连在一起。使用的时候先在父对象的基础上生成子对象:

var Doctor = object(Chinese);

在加上子对象本身的属性

Doctor.career = '医生';

这时子对象已经继承了父对象的属性了。

console.log(Doctor.nation); // 中国

3、浅拷贝

除了使用‘prototype链’以外,我们可以把父对象的属性全部拷贝给子对象,也能实现继承。
请看下面这个函数 :

function extendCopy(p){
  var c = {};
  for (var i in p){
    c[i] = p[i]
    }
  c.uber = p;
  return c;
}

使用的时候,这样写:

var Doctor = extendCopy(Chinese);
Doctor.career = '医生';
console.log(Doctor.nation); // 中国

但是这样拷贝有一个问题。那就是如果父对象的属性等于数组或是另外一个对象,那么实际是,子对象获取的只是一个内存地址,而不是真正的拷贝,因此存在父对象被篡改的可能。
请看,现在给Chinese添加一个‘出生地’属性,它是一个数组。

Chinese.birthPlaces = ['成都','上海','北京'];

通过extendCopy()函数,Doctor继承了Chinese。

var Doctor = extendCopy(Chinese);

然后我们为Doctor的出生地添加一个城市:

Doctor.birthPlaces.push('厦门');

发生了什么事?Chinese的‘出生地’也被改掉了!

console.log(Doctor.birthPlaces); // '成都','上海','北京','厦门';
console.log(Chinese.birthPlaces); // '成都','上海','北京','厦门';

所以,extendCopy()函数只是拷贝基本类型的数据,我们把这种拷贝叫做浅拷贝。

4、深拷贝

所谓深拷贝,就是能够实现真正意义上的数组和对象的拷贝。实现并不难,只要递归调用浅拷贝就行了。

function deepCopy(p,c){
  var c = c || {};
  for(var i in p){
    if(typeof p[i] == 'object'{
      c[i] = (p[i].constructor == Array) ? [] : {};
      deep(p[i],c[i]);
    }else { 
      c[i] = p[i];
    }
  }
  return c;
}

使用的时候这样写:

var Dcotor = deepCopy(Chinese);

现在,给父对象加一个属性,值为数组。然后,在子对象上修改这个属性:

Chinese.birthPlaces = ['成都','上海','北京'];
Doctor.birthPlaces.push('厦门');

这时父对象就不受影响了。

console.log(Doctor.birthPlaces);// '成都','上海','北京','厦门';
console.log(Chinese.birthPlaces); // '成都','上海','北京';

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

推荐阅读更多精彩内容