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); // '成都','上海','北京';

推荐阅读更多精彩内容