Javascript面向对象编程的三种方法

前言

虽然不同于传统的面向对象编程语言,但是Javascript是一门面向对象编程语言,支持基于原型的委托式继承。虽然这样,但是组织javascript代码的形式非常灵活,有函数式编程,模块化编程,面向对象编程等等,那么,Javascript如何实现面向对象编程呢?

一、构造函数法

最常见的方法。它用构造函数模拟”类”,在其内部用this关键字指代实例对象。所谓“构造函数”,就是专门用来生成“对象”的函数。它提供模板,作为对象的基本结构。一个构造函数,可以生成多个对象,这些对象都有相同的结构。
构造函数是一个正常的函数,但是它的特征和用法与普通函数不一样。下面就是一个构造函数:

1
2
3
function Cat() {
this.name = "加菲";
}

构造函数的最大特点就是,函数体内部使用了this关键字,代表了所要生成的对象实例。生成对象的时候,必需用new命令,调用Cat函数,使用new关键字实例化对象。

1
2
var cat1 = new Cat();
alert(cat1.name); //加菲

类的属性和方法,还可以定义在构造函数的prototype对象之上,例如可以在Cat这个“类”添加一个makeSound行为:

1
2
3
Cat.prototype.makeSound = function(){
alert("喵喵喵");
}

在对象中调用:

1
cat1.makeSound(); //喵喵喵

构造函数法的最大特点就是,函数体内部使用了this关键字,代表了所要生成的对象实例。生成对象的时候,必需用new命令,调用Cat函数。

new命令

上面代码通过new命令,让构造函数Cat生成一个实例对象,保存在变量cat1中。这个新生成的实例对象,从构造函数Cat继承了name属性。在new命令执行时,构造函数内部的this,就代表了新生成的实例对象,this.name表示实例对象的name属性,它的值是”加菲”。
使用new命令时,根据需要,构造函数也可以接受参数。

1
2
3
4
function Cat(n){
this.name = n;
};
var cat2 = new Cat("加菲2");

new命令本身就可以执行构造函数,所以后面的构造函数可以带括号,也可以不带括号。下面两行代码是等价的。

1
2
var cat3 = new Cat();
var cat4 = new Cat;

那么这里有个问题来了,如果忘了使用new命令,直接调用构造函数会发生什么?
这种情况下,构造函数就变成了普通函数,并不会生成实例对象。而且由于下面会说到的原因,this这时代表全局对象,将造成一些意想不到的结果。

1
2
3
4
5
6
7
8
9
function Cat(){
this.name = "加菲";
};

var cat = Cat(); //此时,Cat里面的this == window了
alert(cat.name);
// Uncaught TypeError: Cannot read property 'name' of undefined

alert(name); // 加菲

上面代码中,调用Cat构造函数时,忘了加上new命令。结果,name属性变成了全局变量,而变量cat变成了undefined。
这里又有个小提示来了,如果既忘记new,又忘记后面的小括号会怎么样,我们看代码:

1
2
3
4
5
6
function Cat(){
this.name = "加菲";
};

var cat = Cat; //此时,cat就是构造函数本身了
alert(cat.name);//哈哈,这里最搞笑,会弹出Cat,那是因为构造函数本身会有个名字属性name(另外还有:arguments,caller,length等),即Cat

因此,应该非常小心,避免出现不使用new命令、直接调用构造函数的情况。为了保证构造函数必须与new命令一起使用,一个解决办法是,在构造函数内部使用严格模式,即第一行加上‘use strict’。

1
2
3
4
5
6
7
8
function Cat(name){
"use strict";

this._name = name;
}

Cat();
// TypeError: Cannot set property '_name' of undefined

上面代码的Cat为构造函数,use strict命令保证了该函数在严格模式下运行。由于在严格模式中,函数内部的this不能指向全局对象,默认等于undefined,导致不加new调用会报错(JavaScript不允许对undefined添加属性)。

另一个解决办法,是在构造函数内部判断是否使用new命令,如果发现没有使用,则直接返回一个实例对象:

1
2
3
4
5
6
7
8
9
10
function Cat(name){
if (!(this instanceof Cat)) {
return new Cat(name);
}

this._name = name;
}

Cat(1)._name // 1
(new Cat(1))._name // 1

上面代码中的构造函数,不管加不加new命令,都会得到同样的结果。

new命令的原理

1、使用new命令时,它后面的函数调用就不是正常的调用,而是被new命令控制了。

2、new内部的流程是,先创造一个空对象,作为上下文对象,赋值给函数内部的this关键字。也就是说,this指的是一个新生成的空对象,所有针对this的操作,都会发生在这个空对象上。

3、构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作上下文对象(即this对象),将其“构造”为需要的样子。

4、如果构造函数的return语句返回的是对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回构造后的上下文对象。

1
2
3
4
5
6
7
function Cat(){
this.name = "加菲";
return "加菲";
};

(new Cat()) === "加菲"
// false

上面代码中,Cat是一个构造函数,它的return语句返回一个字符串常量。这时,new命令就会忽略这个return语句,返回“构造”后的this对象。
但是,如果return语句返回的是一个跟this无关的新对象,new命令会返回这个新对象,而不是this对象。这一点需要特别引起注意。

1
2
3
4
5
6
7
function Cat(){
this.sex = 1;
return { sex: 2};
};

(new Cat()).sex
// 2

上面代码中,构造函数Cat的return语句,返回的是一个新对象。new命令会返回这个对象,而不是this对象。
new命令简化的内部流程,可以用下面的代码表示:

1
2
3
4
5
6
7
8
9
function _new(/* constructor, param, ... */) {
var args = [].slice.call(arguments);
var constructor = args.shift();
var context = Object.create(constructor.prototype);
var result = constructor.apply(context, args);
return (typeof result === 'object' && result != null) ? result : context;
}

var actor = _new(Person, "张三", 28);

由于篇幅过大,我们这里暂时只延伸到new关键字,对于另外几个和面向对象密切相关的关键字将在以后的文章中继续讨论,例如:thisinstanceofprototype等。

二、Object.create()法

为了解决”构造函数法”的缺点,更方便地生成对象,Javascript的国际标准ECMAScript第五版(目前通行的是第三版),提出了一个新的方法Object.create()。

用这个方法,“类”就是一个对象,不是函数。

1
2
3
4
var Cat = {
name: "加菲",
makeSound: function(){ alert("喵喵喵"); }
};

然后,直接用Object.create()生成实例,不需要用到new:

1
2
3
var cat = Object.create(Cat);
alert(cat.name); // 加菲
cat.makeSound(); // 喵喵喵

目前,各大浏览器的最新版本(包括IE9)都部署了这个方法。如果遇到老式浏览器,可以用下面的代码兼容。

1
2
3
4
5
6
7
if (!Object.create) {
Object.create = function (o) {
function F() {}
F.prototype = o;
return new F();
};
}

这种方法比”构造函数法”简单,但是不能实现私有属性和私有方法,实例对象之间也不能共享数据,对”类”的模拟不够全面。

三、极简主义法(我的定义是闭包法)

荷兰程序员Gabor de Mooij提出了一种比Object.create()更好的新方法,他称这种方法为”极简主义法”(minimalist approach)。
首先,它也是用一个对象模拟”类”。在这个类里面,定义一个对象createNew(),用来生成实例:

1
2
3
4
5
var Cat = {
createNew: function(){
// some code here
}
};

然后,在createNew()里面,定义一个实例对象,把这个实例对象作为返回值:

1
2
3
4
5
6
7
8
var Cat = {
createNew: function(){
var cat = {};
cat.name = "加菲";
cat.makeSound = function(){ alert("喵喵喵"); };
return cat;
}
};

使用的时候,调用createNew()方法,就可以得到实例对象:

1
2
var cat1 = Cat.createNew();
cat1.makeSound(); // 喵喵喵

这种方法的好处是,容易理解,结构清晰优雅,符合传统的”面向对象编程”的构造,因此可以方便地实现面向对象的”继承、多态“等特性。

继承

让一个类继承另一个类,实现起来很方便。只要在前者的createNew()方法中,调用后者的createNew()方法即可。
先定义一个Animal类:

1
2
3
4
5
6
7
var Animal = {
createNew: function(){
var animal = {};
animal.sleep = function(){ alert("睡懒觉"); };
return animal;
}
};

然后,在Cat的createNew()方法中,调用Animal的createNew()方法:

1
2
3
4
5
6
7
8
var Cat = {
createNew: function(){
var cat = Animal.createNew();
cat.name = "大毛";
cat.makeSound = function(){ alert("喵喵喵"); };
return cat;
}
};

这样得到的Cat实例,就会同时继承Cat类和Animal类。

1
2
var cat1 = Cat.createNew();
cat1.sleep(); // 睡懒觉

私有属性和私有方法

在createNew()方法中,只要不是定义在cat对象上的方法和属性,都是私有的。

1
2
3
4
5
6
7
8
var Cat = {
createNew: function(){
var cat = {};
var sound = "喵喵喵";
cat.makeSound = function(){ alert(sound); };
return cat;
}
};

上例的内部变量sound,外部无法读取,只有通过cat的公有方法makeSound()来读取:

1
2
3
var cat1 = Cat.createNew();
alert(cat1.sound); // undefined
alert(cat1.makeSound()); // 喵喵喵

数据共享

有时候,我们需要所有实例对象,能够读写同一项内部数据。这个时候,只要把这个内部数据,封装在类对象的createNew()方法的外面即可:

1
2
3
4
5
6
7
8
9
var Cat = {
sound : "喵喵喵",
createNew: function(){
var cat = {};
cat.makeSound = function(){ alert(Cat.sound); };
cat.changeSound = function(x){ Cat.sound = x; };
return cat;
}
};

现在生成两个实例对象:

1
2
3
var cat1 = Cat.createNew();
var cat2 = Cat.createNew();
cat1.makeSound(); // 喵喵喵

这时,如果有一个实例对象,修改了共享的数据,另一个实例对象也会受到影响:

1
2
cat2.changeSound("啦啦啦");
cat1.makeSound(); // 啦啦啦

感谢您的阅读,有不足之处请在评论为我指出。

参考资料

[1]:javascript标准参考教程
[2]:Javascript定义类(class)的三种方法

版权声明:本文为博主原创文章,未经博主允许不得转载。本文地址 http://yangyuji.github.io/2015/07/02/javascript-class/