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键值对,没有其他。

var person = {
    name : "xiaohong",
    age  : 18
}

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

var person = {
    name : "xiaohong",
    age : 18,
    eat : function(){
        alert("i like eat meat");
    }
}

上面的person实际上已近是一个对象实例,我们可以这样使用, 这种方式我们称它为字面量对象

person.name;
person.eat();

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

构造函数法

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

function Person(){
    this.name="xiaohong";
    this.age=18;
    this.eat=function(){
        alert("i like eat meat");
    }
}

这样我们就可以像Java中的一样,创建该类的实例对象了,这种方式我们称它为函数对象

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

Object.create()

var person = {
    name : "xiaohong",
    age : 18,
    eat : function(){
        alert("i like eat meat");
    }
}

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

var Person = {
    name : "xiaohong",
    age : 18,
    eat : function(){
        alert("i like eat meat");
    }
}

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

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

事实上,在JavaScript中函数就是对象,上面提到了字面量对象是连接到 Object.prototype 的, 函数对象是连接到 Function.prototype 的(而Function对象本身也是连接到 Object.prototype 的)。

极简主义法

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

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也有类似的机制。

var str = new String("dp2px");
var num = new Number(3.1415);
var bool = new Boolean(true) ;

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

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

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

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

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

数组函数是对象吗

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

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

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

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

那么函数是对象吗?

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

//函数声明,不会立即执行
function dowork(){
    alert("i am work")
}

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

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

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

var dowork = function work(){
    alert("i am work");
}

dowork();

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

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

JavaScript中的this

在JavaScript中除了函数声明的参数之外,还有两个附加参数 thisarguments, 而这个 this 参数和 Java 中的不太一样,在JavaScript中函数一共有四种调用模式,在每个调用模式下 this 存在很大差异。

方法调用模式

当一个函数被定义为一个对象的属性时,这个函数被称为方法(注意方法和函数的区别),当一个方法被调用的时候,this被绑定到这个方法所在的对象上,通常使用 . 语法来调用方法。

var obj = {
    value : '0',
    increment: function(inc){
        this.value += typeof inc === 'number' ? inc : 1;
    }
}

obj.increment();
document.writeln(obj.value);  //结果为 1

obj.increment(2);
document.writeln(obj.value);  //结果为 2

这个 this 的绑定过程发生在实际调用方法的时候 obj.increment() , 这里的 increment() 方法可以访问所属对象的上下文,因此被称为公共方法

函数调用模式

当一个函数直接被调用(而不是作为对象的方法)时, this 被绑定到全局对象(这是语言设计上的一个错误),这样就导致了一个问题,问题如下:

var obj = {
    value : '0';
}

obj.increment = function(inc){

    var helper = function(){
        this.value += typeof inc === 'number' ? inc : 1;    //this.value 不能访问obj的value
    }
}

如上代码中 obj 对象绑定了一个方法 increment, 在 increment 方法内有一个函数 helper, 此时 helper 的 this并不是绑定的obj对象而是 helper自己。 所以上面的 this.value 实际上是 helper 的 value。如果要访问上面的 obj 的 value 要通过如下方式(注意下面的that是约定的名称)

var obj = {
    value : '0'
}
obj.increment = function(inc){
    that = this;
    var helper = function(){
        that.value += typeof inc === 'number' ? inc : 1;    
    } 
    helper();
}
obj.increment(2);
document.writeln(obj.value);

构造器调用模式

JavaScript是一门基于原型继承的语言。Java的继承实际上是一种类型的扩展,但是JavaScript不存在直接扩展一个Class,因为不存在Class这种类型。

JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( __proto__ ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

如果一个函数通过 new 关键字调用,将创建一个隐藏连接到该函数的 prototype 成员的新对象,同时 this 将会绑定在这个新对象上。

var Quo = function(string){
    this.status = string;
};

Quo.prototype.get_status = function(){
    return this.status; // this 绑定到了新对象,可以访问Quo的属性
};

var myQuo = new Quo("confused");
document.writeln(myQuo.get_status());

apply和call调用模式

apply和call方法的第一个参数都是this的指向对象,第二个参数有些差别:

call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面 obj.myFun.call(db,'成都', ... ,'string' )。 apply 的所有参数都必须放在一个数组里面传进去 obj.myFun.apply(db,['成都', ..., 'string' ])

例如下面代码:

var name = 'DP2PX', age = 28;
var obj = {
    name: '水寒',
    objAge: this.age,
    myFun: function(){
        console.log(this.name + "年龄" + this.age);
    }
}

obj.objAge;   // 28
obj.myFun();  // 水寒年龄 undefined

上面已经分析了原因, objAge: this.age 这里的 this 指向 window, 而 console.log(this.name + "年龄" + this.age) 这里的 this 指向 obj.

接下来我们使用 apply 和 call 方法来将 this 指向另一个对象。

var name = 'DP2PX', age = 28;
var obj = {
    name: '水寒',
    objAge: this.age,
    myFun: function(){
        console.log(this.name + "年龄" + this.age, "来自" + fm + "去往" + t);
    }
}

var db = {
    name = "小强",
    age = 18
}

obj.objAge;   // 28
obj.myFun();  // 水寒年龄 undefined
obj.myFun.call(db, '西安', '北京');   //小强18来自西安去往北京
obj.myFun.apply(db, ['西安', '北京']); //小强18来自西安去往北京
obj.myFun.bind(db, '西安', '北京')();  //小强18来自西安去往北京 bind返回的是一个函数,必须调用

有继承和接口吗

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

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对象中。将不需要共享的属性和方法放在构造函数中。

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继承机制的设计思想。

继承的五种方法

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

function Animal(){
    this.species="动物"
}

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("大黄", "yellow");
alert(cat1.species);  //动物

prototype模式

第二种方法更常见,使用prototype属性。当一个函数对象被创建时,Function 构造器产生的函数对象会运行类似这样一些代码:

this.prototype = {constructor: this};

新函数对象被赋予一个 prototype 属性,其值是包含一个 constructor 属性且属性值为该新函数对象。

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

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属性。

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

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

直接继承prototype

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

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。

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

Cat.prototype.constructor = Cat;

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

利用空对象作为中介

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

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

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

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

function extend(Child, Parent){
    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    Child.uber = Parent.prototype;
}

使用的时候,方法如下

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

另外

Child.uber = Parent.prototype;

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

拷贝继承

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

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

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

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

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 对象。

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

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

var Chiness={
    nation:'中国'
}

var Doctor{
    career:'医生'
}

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

object方法

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

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

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

浅拷贝

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

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

深拷贝

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

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);