JavaScript面向对象(1)

Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。那么,如果我们要把”属性”(property)和”方法”(method),封装成一个对象,甚至要从原型对象生成一个实例对象,我们应该怎么做呢?

作为多年使用Java的我,首先开始思考如下问题:

  1. 在JavaScript中,类如何定义,如何创建对象?
  2. 在JavaScript中,基本类型有对应的对象形式吗?
  3. 在JavaScript中,数组和函数是对象吗?
  4. 在JavaScript中,有继承和接口吗?
  5. 在JavaScript中,如何使用函数这种特殊的对象?
  6. 在JavaScript中,闭包是什么?
  7. 在JavaScript中,有哪些奇技淫巧是Java所没有的?

类和对象

在JavaScript中,类的定义只是一个Map键值对,没有其他。

1
2
3
4
var person = {
name : "xiaohong",
age : 18
}

那么JavaScript中的类中没有方法吗?有!但是也是键值对。

1
2
3
4
5
6
7
var person = {
name : "xiaohong",
age : 18,
eat : function(){
alert("i like eat meat");
}
}

上面的person实际上已近是一个对象实例,我们可以这样使用

1
2
person.name;
person.eat();

那么问题来了,如何才能像Java中一样定义一个JavaScript的类呢?在JavaScript中定义类的方法除了上面的方法还可以使用下面几种方法。

构造函数法

这个方法是最经典的方法,用构造函数模拟“类”,在其内部可以用this关键字指代实例对象。

1
2
3
4
5
6
7
function Person(){
this.name="xiaohong";
this.age=18;
this.eat=function(){
alert("i like eat meat");
}
}

这样我们就可以像Java中的一样,创建该类的实例对象了

1
2
var person = new Person();
person.eat();

Object.create()

1
2
3
4
5
6
7
var person = {
name : "xiaohong",
age : 18,
eat : function(){
alert("i like eat meat");
}
}

我们来思考一下这个问题,一个类其实定义的是一个可以创建多个实例的模板,那么,上面的person即是模板又是对象,如果我们不把上面的person看成一个类的实例对象的话,而是模拟成一个类,我们可以使用Object.create()方法来创建多个实例对象。(其实JavaScript中的面向对象本身就是一种模拟而已)

1
2
3
4
5
6
7
8
9
10
var Person = {
name : "xiaohong",
age : 18,
eat : function(){
alert("i like eat meat");
}
}

var person = Object.create(Person);
person.eat();

是不是看起来特别别扭,模拟面向对象就是这样,因为在JavaScript中没有定义类的方法,所以,所有JavaScript中的类的定义都是通过函数来模拟的。

极简注意法

我们从上面类和对象的定义,已近知道JavaScript中可以通过var obj = {} 方法来定义一个对象,但是这个对象里面只能定义属性(方法也可以看成一种键值对属性),那么我们可以定义一个构造属性(构造方法)来创建这个对象的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var Person = {
createNew: function(){
var person = {}
person.name="xiaohong",
person.age=18,
person.eat=function(){
alert("i like eat meat");
}
return person;
}
}

var person = Person.createNew();
person.eat();

这种方法的好处是在实现面向对象的一些特征(比如,继承、封装、多态)方面能清晰表达。

基本类型的对象

JavaScript中的基本类型有6种:String,Number,Bolean,Symbol,undefined,null

Symbol很像Java中的UUID,Symbol()就相当于UUID.randomUUID()。

undefined和null这两种数据类型都只有一个值,就是它们的类型名。undefined用于表示变量未初始化,null用于表示对象为空。

Java中的基本数据类型都有对应的包装类,并可以自动装箱和拆箱,JavaScript也有类似的机制。

1
2
3
4
5
var str = new String("dp2px");
var num = new Number(3.1415);
var bool = new Boolean(true) ;

var sym = Symbol(); //特殊,不能 new

我们可以用typeof()来判断他们是不是对象。

那么JavaScript中的自动装箱和拆箱的过程是怎样的呢?

1
2
var hello ='hello';
var str = hello.slice(1); //str 的值是'ello'

我们知道字符串有一些操作函数,其实这个过程是对hello字符串进行了自动装箱,使用的是一个临时的String对象来操作的。

数组函数是对象吗?

在JavaScript中我们通常会这样定义一个数组

1
var arr = ["one", "two", "three"];

实际上JavaScript会把上面的代码转换为键值的形式

1
var arr = {"0":"one", "1":"two", "2", "three"}

那么函数是对象吗?

JavaScript中定义函数的方式有两种,一种是函数声明,一种是函数表达式

1
2
3
4
5
6
7
8
9
//函数声明,不会立即执行
function dowork(){
alert("i am work")
}

//函数表达式,会立即执行
var dowork = function(){
alert("i am work")
}

你可以发现两个函数都可以通过dowork()来执行,但是函数声明和函数表达式还是有很大区别的。

函数表达式就有点像Java中的匿名函数,声明的方式会在执行程序前加载,程序文件的任何地方都可以调用声明方式定义的函数,但是表达式方式定义的函数只有在执行到定义的地方才会加载,才能在之后来调用该函数。

1
2
3
4
5
var dowork = function work(){
alert("i am work");
}

dowork();

这个函数是函数表达式,dowork是它的函数名,如果函数内部需要递归调用自己就需要用到work这个名字了。

事实上在JavaScript中函数就相当于一个特殊的对象,可以作为对象的一个属性,而且这个对象可以直接被调用执行。这个特点在纯面向对象的Java语言中是不存在的。

有继承和接口吗?

说到这个继承,我们首先得看一下JavaScript的prototype属性是什么。

1
2
3
4
5
6
7
8
9
function Person(name){
this.name=name;
}

var p1 = new Person('xiaoming');
var p2 = new Person('xiaohong');

alert(p1.name);
alert(p2.name);

我们发现,每一个实例对象,都有自己的属性和方法,假设我们在JavaScript中要在类中共享数据,就很难做到了。

JavaScript中有一个prototype,我们可以将所有需要共享的属性和方法放到prototype对象中。将不需要共享的属性和方法放在构造函数中。

1
2
3
4
5
6
7
8
9
10
function Person(name){
this.name=name;
}
Person.prototype={age:28}

var p1 = new Person('xiaoming');
var p2 = new Person('xiaohong');

alert(p1.age);
alert(p2.age);

我们会发现p1.age和p2.age都是28.

由于所有实体类对象都共享了一个prototype对象,而实例对象就好像继承了prototype对象一样,这就是JavaScript继承机制的设计思想。

继承的五种方法

现在有一个动物类和一个猫类,如何来使猫类继承动物类呢?

1
2
3
4
5
6
7
8
function Animal(){
this.species="动物"
}

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

构造函数绑定

第一种方法也是最简单的方法,使用call或apply方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:

1
2
3
4
5
6
7
8
function Cat(name, color){
Animal.apply(this, arguments);
this.name=name;
this.color=color;
}

var cat1 = new Cat("大黄", "yellow");
alert(cat1.species);

prototype模式

第二种方法更常见,使用prototype属性。

如果”猫”的prototype对象,指向一个Animal的实例,那么所有”猫”的实例,就能继承Animal了。

1
2
3
4
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大黄", "yellow");
alert(cat1.species);

任何一个对象的prototype对象都有一个constructor属性,指向它的构造函数。
如果没有”Cat.prototype = new Animal();”这一行,Cat.prototype.constructor是指向Cat的;加了这一行以后,Cat.prototype.constructor指向Animal。

每一个实例也有一个constructor属性,默认调用prototype的constructor属性。

1
alert(cat1.constructor == Cat.prototype.constructor); // true

所以如果替换了prototype对象就要纠正constructor的指向。

直接继承prototype

第三种方法是对第二种方法的改进。由于Animal对象中,不变的属性都可以直接写入Animal.prototype。所以,我们也可以让Cat()跳过 Animal(),直接继承Animal.prototype。

1
2
3
4
5
6
7
8
function Animal(){}
Animal.prototype.species="动物";

Cat.prototype=Animal.prototype;
Cat.prototype.constructor=Cat;

var cat1 = new Cat("大黄", "yellow");
alert(cat1.species);

与前一种方法相比,这样做的优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。缺点是 Cat.prototype和Animal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会反映到Animal.prototype。

所以,上面这一段代码其实是有问题的。请看第二行

1
Cat.prototype.constructor = Cat;

这一句实际上把Animal.prototype对象的constructor属性也改掉了!

利用空对象作为中介

由于”直接继承prototype”存在上述的缺点,所以就有第四种方法,利用一个空对象作为中间继承对象。

1
2
3
4
var F = function(){}
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;

F是空对象,所以几乎不占内存。这时,修改Cat的prototype对象,就不会影响到Animal的prototype对象。

我们将上面的方法,封装成一个函数,便于使用。

1
2
3
4
5
6
7
function extend(Child, Parent){
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}

使用的时候,方法如下

1
2
3
extend(Cat, Animal);
var cat1 = new Cat("大黄", "yellow");
alert(cat1.species);

另外

1
Child.uber = Parent.prototype;

意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是”向上”、”上一层”。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。

拷贝继承

上面是采用prototype对象,实现继承。我们也可以换一种思路,纯粹采用”拷贝”方法实现继承。简单说,如果把父对象的所有属性和方法,拷贝进子对象,不也能够实现继承吗?这样我们就有了第五种方法。

首先,还是把Animal的所有不变属性,都放到它的prototype对象上。

1
2
function Animal(){}
Animal.prototype.species="动物";

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

1
2
3
4
5
6
7
8
function extend(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对象。

1
2
3
extend(Cat, Animal);
var cat1 = new Cat("大黄", "yellow");
alert(cat1.species);

前面我们看到了5中构造函数实现继承,对于非构造函数对象如何继承呢?

1
2
3
4
5
6
7
var Chiness={
nation:'中国'
}

var Doctor{
career:'医生'
}

怎样才能让”医生”去继承”中国人”

object方法

1
2
3
4
5
6
7
8
function object(o){
function F(){}
F.prototype = o;
return new F();
}

var Doctor = object(Chinese);
Doctor.career = '医生';

这个object()函数,其实只做一件事,就是把子对象的prototype属性,指向父对象,从而使得子对象与父对象连在一起。

浅拷贝

除了使用”prototype链”以外,还有另一种思路:把父对象的属性,全部拷贝给子对象,也能实现继承。

1
2
3
4
5
6
7
8
9
10
11
12
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 = '医生';
alert(Doctor.nation);

但是,这样的拷贝有一个问题。那就是,如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。

深拷贝

所谓”深拷贝”,就是能够实现真正意义上的数组和对象的拷贝。它的实现并不难,只要递归调用”浅拷贝”就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

var Doctor = deepCopy(Chinese);