不是原创
本文是阮一峰老师博客《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;
}
(一)、构造函数绑定
使用 call
或 apply
方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加入一行代码:
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
- 这里先将 Cat 的 prototype 对象指向一个 Animal 实例;
- Cat 的 prototype 对象的 constructor 属性,会因此指向 Animal;
- 但是由于 cat1 是用构造函数 Cat 生成的,需要手动纠正,将 Cat.prototype 对象的 constructor 值改回 Cat;
- 所以有了第二行的代码,让 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.prototype
的 constructor
重新指向 Cat
的同时,其实也把 Animal.prototype
的 constructor
属性更改(成指向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…