【非原创】 JavaScript 面向对象编程 学习笔记

125 阅读8分钟

不是原创

本文是阮一峰老师博客《Javascript 面向对象编程》系列的学习笔记,非原创
原文指路,一共三篇:www.ruanyifeng.com/blog/2010/0…

面向对象编程

JavaScript 是一种基于对象 (object-based) 的语言,但是不是一种真正的面向对象编程 (object-oriented programming) 的语言,因为它的语法里没有类 (class)。

如果要把属性(property)和方法(method)封装成一个对象,可以有以下做法。

一、封装

(一)、生成实例对象的原始模式

var Cat = {
  name: '',
  color: ''
}

var cat1 = {};
  cat1.name = 'daMao';  
  cat1.color = 'yellow';

var cat2 = {};
  cat2.name = 'erMao';
  cat2.color = 'black';

优点:简单明了,把多个属性封装在一个对象里;
缺点:如果生成的实例较多,要多写很多重复代码;实例 cat 与原型 Cat 之间看不出联系。

(二)、原始模式的改进

写一个函数,解决上文的代码重复问题:

function Cat(name, color){
  return {
    name: name,
    color: color
  }
}

之后生成 Cat 的实例对象,相当于是调用函数

var cat1 = Cat('daMao', 'yellow')
var cat2 = Cat('erMao', 'black')

优点:解决了代码重复;
缺点:cat1 和 cat2 之间没有内在联系,不能反映出它们是同一个原型对象的实例。

(三)、构造函数模式

为了让 cat1 和 cat2 之间存在内在联系,也就是说解决从原型对象生成实例的问题,JavaScript 提供一个构造函数(Constructor)模式。
构造函数:是普通函数,内部使用了 this 变量。对构造函数使用 new 运算符可以生成实例,且 this 变量会绑定在实例对象上。
猫的原型对象可以写成:

function Cat(name, color){
  this.name = name;
  this.color = color;
}

然后生成实例可以写成:

var cat1 = new Cat('daMao', 'yellow');
var cat2 = new Cat('erMao', 'black');
alert(cat1.name); // daMao
alert(cat2.color); // yellow

此时的两个实例,都含有一个 constructor 属性,指向它们的构造函数 Cat

alert(cat1.constructor === Cat); // true
alert(cat2.constructor === Cat); // true

优点:让原型对象和实例们产生联系;
缺点:会造成内存浪费,对于每一个实例对象,属性和方法都是一样的,每次生成新的实例对象都有重复的属性和方法占用内存,不够环保高效。

(四)、构造函数模式的问题

针对上文说到的问题,用代码加以解释。

function Cat(name, color){
  this.name = name;
  this.color = color;
  this.type = 'feline'
  this.eat = function(){alert('seafood')}
}

new 方法生成实例对象:

var cat1 = new Cat('daMao','yellow');
var cat2 = new Cat('erMao', 'black');
alert(cat1.type); // feline
cat1.eat(); // seafood
alert(cat1.eat === cat2.eat); // false

两个实例的 type 属性和 eat() 方法在内存中生成了两次,但其实是同一信息,这样就造成了内存浪费,下一步的优化的思路是——让所有实例都指向同一个内存地址。

(五)、Prototype 模式

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

function Cat(name, color){
  this.name = name;
  this.color = color;
}
Cat.prototype.type = 'feline'
Cat.prototype.eat = function(){alert('seafood')}

此时再生成实例,共有的属性和方法就都存在同一个内存地址,指向 prototype 对象,提高了运行效率。

var cat1 = new Cat('daMao','yellow');
var cat2 = new Cat('erMao', 'black');
alert(cat1.type); // feline
cat1.eat(); // seafood
alert(cat1.eat === cat2.eat); // true

(六)、Prototype 模式的验证方法

为了配合 prototype 属性,JS定义了一些辅助方法。

1. isPrototypeOf()

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

alert(Cat.prototype.isPrototypeOf(cat1)); // true
alert(Object.prototype.isPrototypeOf(cat1)); // true
alert(Date.prototype.isPrototypeOf(cat1)); // false
2. hasOwnProperty()

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

alert(cat1.hasOwnProperty('name'))  // true
alert(cat1.hasOwnProperty('type'))  // false

由于 name 是实例对象自有的属性,所以返回 true,而 type 是继承自 prototype 对象的属性,hasOwnProperty 判断返回 false

3. in 运算法

可以用 in 运算符来判断,某个实例对象是否含有某个属性,不区分自有 / 非自有属性

alert('name' in cat1)  // true
alert('type' in cat1)  // true

in 还可以用来遍历某个对象的所有属性

for(var prop in cat1){ alert('cat1['+prop+']='+cat1[prop]) }  

二、对象之间的继承

可以分成“构造函数的继承”和“非构造函数的继承”。对象之间的继承,可以有物种方法。
假设有一个“动物”对象的构造函数,和一个“猫”对象的构造函数,想让“猫”继承“动物”

function Animal(){this.species = "Animal";}

function Cat(name, color){
  this.name = name;
  this.color = color;
}

(一)、构造函数绑定

使用 callapply 方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加入一行代码:

function Cat(name, color){
  Animal.apply(this, arguments);
  this.name = name;
  this.color = color;
}
var cat1 = new Cat('daMao', 'yellow');
alert(cat1.species); // Animal

(二)、prototype 模式

还可以使用 prototype 属性继承,如果猫的prototype对象,指向Animal的实例,那么所有猫的实例也就能继承Animal了。

Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

var cat1 = new Cat('daMao', 'yellow');
alert(cat1.species); // Animal
  1. 这里先将 Cat 的 prototype 对象指向一个 Animal 实例;
  2. Cat 的 prototype 对象的 constructor 属性,会因此指向 Animal;
  3. 但是由于 cat1 是用构造函数 Cat 生成的,需要手动纠正,将 Cat.prototype 对象的 constructor 值改回 Cat;
  4. 所以有了第二行的代码,让 Cat 的 prototype 对象的 constructor 属性重新指向 Cat 这里是编程要遵循的一个重点:如果替换了 prototype 对象,那么下一步一定要为新的 prototype 对象加上 constructor 属性,让这个属性指回原来的构造函数,即:
o.prototype = {};
o.prototype.constructor = o;
//  如果替换了 prototype 对象,那么下一步一定要为新的 prototype 对象加上 constructor 属性,将这个属性指回原来的构造函数

(三)、直接继承 prototype

直接继承 prototype 是 prototype 模式的改进。由于对象中,不变的属性可以直接写入 Animal.prototype,所以可以让 Cat() 跳过 Animal(),直接继承 Animal.prototype,即:

function Animal(){}  // 先将Animal对象改写
Animal.prototype.species = 'Animal'

然后将 Cat 的 prototype 对象指向 Animal 的 prototype 对象,完成继承

Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;
var cat1 = new Cat('daMao', 'yellow');
alert(cat1.species);  // Animal

优点:效率高,不用执行和建立 Animal 实例,节省了一部分内存
缺点:Cat.prototype 和 Animal.prototype 现在都指向同一个对象,任何对 Cat.prototype 的修改都会反映到 Animal.prototype。Line 2 中将 Cat.prototypeconstructor 重新指向 Cat 的同时,其实也把 Animal.prototypeconstructor 属性更改(成指向Cat)了。

(四)、使用空对象作为中介

鉴于上文中,直接继承 prototype 存在的缺点,优化出第四种方法,利用空对象作为中介的继承方法。

var F = function(){};
F.prototype = Animal.prototype;
Cat.prototype = new F()
Cat.prototype.constructor = Cat;

由于 F 是空对象,所以几乎不占内存。而且此时修改 Cat.prototype 的 constructor 属性,也不会影响到 Animal.prototype。

alert(Animal.prototype.constructor); // Animal

这个方法可以封装成⬇️ :

function extend(Child, Parent){
  var F = function(){};
  F.prototype = Parent.prototype;
  Child.prototype = new F();
  Child.prototype.constructor = Child;
  Child.uber = Parent.prototype; 
  // 给子对象设一个 uber 属性,这个属性直接指向父对象的 prototype 属性
}

使用的时候方法如下:

extend(Cat, Animal);
var cat1 = new Cat('daMao', 'yellow');
alert(cat1.species);  // Animal

这个 extend 函数,就是 YUI (Yahoo! User Interface)库实现继承的方法。

(五)、拷贝继承

prototype对象是实现继承的一种方式,也可以换一种思路,用纯拷贝的方式继承。如果把父对象的所有属性和方法,都拷贝到子对象中,也可以实现继承。
首先,把 Animal 的所有固定属性和方法,放到 prototype 对象上。

function Animal(){};
Animal.prototype.species = 'Animal';

然后写一个函数,实现属性拷贝的目的。

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 对象的属性,一一拷贝给了子对象的 prototype 对象。使用的时候写成以下形式:

extend2(Cat, Animal);
var cat1 = new Cat('daMao', 'yellow')
alert(cat1.species); // Animal

三、非构造函数的继承

(一)、什么是非构造函数的继承

举例:有两个普通对象,它们都不是构造函数,无法使用构造函数实现继承;一个对象是中国人,一个对象是医生
问:怎样才能让医生继承中国人,生成一个中国医生?

var Chinese = {nation: 'China'};
var Doctor = {career: 'Doctor'};

(二)、object() 方法

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

这个 object() 函数,把子对象的 prototype 属性,指向父对象,从而使得子对象与父对象连在一起。
使用的时候,先在父对象的基础上,生成子对象:

var Doctor = object(Chinese)

然后加上子对象本身的属性

Doctor.career = 'Doctor'

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

alert(Doctor.nation); // China

(三)、浅拷贝

除了使用 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 = 'Doctor';
alert(Doctor.nation); // Chinese

但是这样的拷贝有一个问题,如果父对象的属性等于数组或另一个对象,那么实际上子对象获得的只是一个内存地址,不是真正的拷贝,会存在父对象被篡改的可能性。 例如,给 Chinese 对象添加出生地属性,值是一个数组:

Chinese.birthPlaces = ['NE','SE','NW','SW'];

通过 extendCopy() 函数,Doctor 继承了 Chinese,然后我们给他添加一个出生地城市:

var Doctor = extendCopy(Chinese)
Doctor.birthPlaces.push('Jilin')

这时 Chinese 的出生地被更改了:

alert(Doctor.birthPlaces); // NE,SE,NW,SW,Jilin
alert(Chinese.birthPlaces); // NE,SE,NW,SW,Jilin

所以extendCopy()只是拷贝基本类型的数据,这种拷贝也叫做浅拷贝

(四)、深拷贝

要想拷贝复杂的数据类型(对象、数组等),需要递归调用浅拷贝,也就是深拷贝

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

这样使用的时候,把 Chinese 传给 deepCopy 函数,给 Doctor 加一个属性也不会修改父对象了。

var Doctor = deepCopy(Chinese);
Chinese.birthPlaces = ['NE','SE','NW','SW'];
Doctor.birthPlaces.push('Jilin');
alert.(Doctor.birthPlaces);  // NE,SE,NW,SW,Jilin
alert(Chinese.birthPlaces); // NE,SE,NW,SW

目前 jQuery 库使用的就是这种继承方法。

原文地址 | Reference List

一、封装: www.ruanyifeng.com/blog/2010/0…
二、构造函数的继承:www.ruanyifeng.com/blog/2010/0…
三、非构造函数的继承: www.ruanyifeng.com/blog/2010/0…