Mmear's 😘.

JavaScript面向对象编程

字数统计: 2.3k阅读时长: 9 min
2018/04/07 Share

面向对象思想

面向对象思想是基于现实世界的一种抽象建模方式,它包含封装继承多态等概念;

”C++是半面向对象半面向过程语言,因为,虽然它实现了类的封装,继承和多态,但存在非对象性质的全局变量和函数;Java则是完全的面向对象语言,它通过类的形式组织函数和变量,使之不能脱离对象而存在。“

上述基于类的面向对象语言通过类(class)这个方式来实现面向对象编程要求,类是一种对事物属性和功能的抽象模型,通过类可以生成实例(instance),各实例是类的具体化,互相独立;类之间可以通过(继承inheritance)来实现进一步的抽象;子类对父类中同名方法的重写和调用称为多态(polymorphism)(不止),从而增加类间的灵活性;

1
2
3
4
5
6
7
8
Class Human(){
private String name;
private String sex;
public Human(name, sex){...}
public void sayName(){...}
};
...
Human man = new Human('Bob','Male');

JavaScript是一门基于原型的面向对象语言,没有”类”这个概念,一切均为对象;为了实现面向对象思想,可以采用通过原型模式模拟类的方法;

创建对象

在基于类的面向对象语言中,对象只能通过实例化new一个类来实现;

JavaScript中没有类,但有new操作符,JS中可以对任何函数使用new操作符来得到一个对象,被执行操作的函数称为”构造函数”;其次,还可以通过对象字面量直接创建一个对象;ES5中还提供了Object.create方法来构建一个具有自定原型的对象:试想不依靠函数,从一个对象衍生出另一个对象,这便是Object.create的用处所在;

1
2
3
4
5
6
7
8
9
10
11
var Person = {
init: function(name) {
this.name = name;
},
sayName: function() {
return this.name;
}
}
var bob = Object.create(Person);
bob.init('Bob');
console.log(bob.sayName());//Bob

关于new

当一个函数被创建时,这个函数对象会做一件事:

1
2
3
this.prototype = {
constructor: this
}

新的函数对象被赋予一个prototype属性,它是一个包含constructor属性且属性为该函数对象的对象;当对一个函数使用new操作符时,函数的原型将被用来关联一个对象,如果new是一个方法,它的执行过程看起来就像这样:

1
2
3
4
5
6
7
8
Function.prototype.new = function() {
//产生一个对象,它关联着构造函数对象的原型
var obj = Object.create(this.prototype);
//将构造函数的this绑定至这个新对象
var other = this.apply(obj, arguments);
//若构造函数已经返回了一个对象,抛弃obj,否则返回新对象
return (other && typeof other == 'object' ? other : obj);
}

可以看出,new操作的内部其实还是基于原型模式来创建对象,虽然它表面上看起来是通过实例化一个’类’创建对象;

抽象

模拟类,就是模拟一种抽象方式:通过原型创建的对象能够访问它所关联原型的属性,加之new操作符绑定this的特性,一般会将类的属性设置放入构造函数,而方法则放入函数原型中:

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name) { //又称组合构造
this.name = name;
this.family = ['father', 'mother'];
}
Person.prototype.sayName = function() {
return this.name;
}
Person.prototype.addFamily = function(...member) {
for(var i = member.length, j = 0; j < i; j++){
this.family.push(member[j]);
}
}

继承

一般的继承中,子类构造函数需要调用父类的构造函数:

1
2
3
4
function sub(){
sup.apply(this, arguments);
//再执行自己的属性赋值
}

这样虽然能够调用父类的构造函数,但父类原型的属性和方法并不能访问,子类与父类之间并没有明显的联系;而建立这层联系的方法便是原型;

组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Person(name) {
this.name = name;
}
//添加父类原型方法
Person.prototype.sayName = function() {
return this.name;
};

function Student(name, school) {
Person.apply(this, name); //调用父类构造函数
this.school = school;
}
//连接原型
Student.prototype = Object.create(Person.prototype);
//修复constructor指向
Student.prototype.constructor = Student;
//添加子类原型方法
Student.prototype.saySchool = function() {
return this.school;
}
//伪多态,重写父类方法
Student.prototype.sayName = function() {
console.log("I am ");
//使用父类中的同名方法
console.log(Person.sayName.apply(this));
}

通过Object.create函数,创建出了一个以Person.prototype[[prototype]]属性的对象,并赋值给Student.prototype,这样便可以通过原型链访问到父类原型上的属性和方法了;同时,每个实例也会拥有各自独立的属性;

原型继承

在一个纯粹的原型模式中,我们会摒弃类,转而专注于对象。基于原型的继承相比于类的继承在概念上更为简单:一个新对象可以继承一个旧对象的属性。
—————《JavaScript精粹》

根据“委托理论”,我们只需要定义对象之间的关系,无需使用繁杂的伪类过程;继承曾经被看作是类与类之间的关系,现在,我们只需注重对象之间的关系,毕竟[[prototype]]属性的本质就是一个对象的引用;这种编码风格又称为”对象关联”(OLOO, objects linked to ohter objects)土爆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//创建字面量对象而不是函数
var Person = {
init: function(name) {
this.name = name;
},
sayName: function() {
return this.name;
}
}
//将二者建立链接
var Student = Object.create(Person);
Student.setUp = function(name, school) {
this.init(name);
this.school = school;
}
Student.saySchool = function() {
return this.school;
}
//创建子衍生对象
var bob = Object.create(Student);
bob.setUp('Bob', 'Hope School');
console.log(bob.sayName());
console.log(bob.saySchool());

委托理论的特点在于:

  • 数据成员一般储存在委托者(如bob),而不是委托对象(如Student)上;
  • 委托行为要尽量避免在不同的[[prototype]]上出现同名方法,即不推荐重写方法
  • 以原型链为基础,对于自身没有的方法会委托[[prototype]]链寻找,委托最好在内部实现,而不要暴露出去;

委托理论中,对象与对象之间的关系非常简单,就是一个__proto__的联系,而这个联系可以是各个方向的;其次,并不需要模仿类中的行为(new,构造函数,原型等等);

这种模式的缺点也很简单,对象创建后需要手动初始化、不推荐重写方法相当于减弱了多态性,另外,还是无法解决私有变量的问题;

Simple Inheritance

jQuery 之父 John Resig 在搏众家之长之后,用不到 30 行代码便实现了自己的 SimpleInheritance:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
(function(){
//initializing用于控制类的初始化
//fnTest返回一个正则比表达式,用于检测函数中是否含有_super,当然浏览器如果不支持的话就返回一个通用正则表达式
var initializing = false,
fnTest = /xyz/.test(function(){xyz;}) ?
/\b_super\b/ : /.*/;
//所有类的基类Class,这里的this一般是window对象
this.Class = function(){};
//对基类添加extend方法,用于从基类继承
Class.extend = function(prop){
//保存当前类的原型
var _super = this.prototype;
initializing = true;
//将父类的一个空实例作为子类的原型,initializing用于控制避免父类初始化
var prototype = new this();
initializing = false;
//将参数prop中赋值到prototype中,这里的prop中一般是包括init函数和其他函数的对象
for(var name in prop){
//对于函数重写并出现调用父类同名函数,需要特殊处理,处理后可以在子函数中使用this._super()调用父类同名函数
prototype[name] = typeof prop[name] == "function"
&& typeof _super[name] == "function"
&& fnTest.test(prop[name])?
(function(name,fn){
return function(){
//为了避免覆盖对象的_super属性(如果有的话
var tmp = this._super;
//添加父类函数
this._super = _super[name];
//调用子类函数
var res = fn.apply(this,arguments);
//复原_super属性
tmp && (this._super = tmp);
return res;
}
})(name,prop[name]) : prop[name];
}
//注意这个内部Class变量和外部Class是不同的
//当new一个对象时,调用了init函数
function Class(){
if(!initializing && this.init){
this.init.apply(this,arguments);
}
}
//给子类设置原型
Class.prototype = prototype;
Class.prototype.constructor = Class;
//设置子类的extend方法,使得子类也可以通过extend方法被继承
Class.extend = arguments.callee;
return Class;
}
})();

这个立即执行函数给全局对象window添加了一个属性Class,它是一个闭包,引用着原有函数的活动对象,通过它能够实现类的继承,并能简单地调用父类方法;同时,每次执行Class.extend函数也有可能产生闭包(for循环中),此时的闭包引用着的活动对象有着变量_super[name]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var Human = Class.extend({
init: function (age, name) {
this.age = age;
this.name = name;
},
say: function () {
console.log("I am a human");
}
});
var Man = Human.extend({
init: function (age, name, height) {
this._super(age, name); //调用父类同名方法
this.height = height;
},
say: function () {
this._super();
console.log("I am a man");
}
});
var man = new Man(21, 'bob', '191');
man.say();

总结

其实写这一部分的时候还是有些懵逼,封装、继承、多态的特性还是有些模糊;以后在这方面理解深入后再更新~

CATALOG
  1. 1. 面向对象思想
  2. 2. 创建对象
    1. 2.1. 关于new
    2. 2.2. 抽象
  3. 3. 继承
    1. 3.1. 组合继承
    2. 3.2. 原型继承
    3. 3.3. Simple Inheritance
  4. 4. 总结