JavaScript的类及面向对象编程

在JavaScript中,的实现是基于其原型继承机制的。假设两个实例都从同一个原型对象上继承了属性,我们说它们是同一个类的实例。

假设熟悉C++、Java这样的强类型面向对象编程语言。比方类的封装、继承、多态特性,你会发现JavaScript中的类与它们有非常大不同。一个重要的特性就是动态可继承性。往往弱化对象的类型,强化对象的功能。定义类是模块开发和代码重用的的有效方式之中的一个。以下对JavaScript中的类及面向对象编程作个介绍。

1、类和原型

在JavaScript中,类的全部实例对象都从同一个原型对象上继承属性,原型对象是类的核心。例如以下样例中的inherit()函数,返回一个新创建的对象。后者继承自某个原型对象。

function inherit(p) {
    if (p == null) {
        throw TypeError();
    }
    if (Object.create) {
        return Object.create(p);
    }
    var t = typeof p;
    if (t !== "object" && t !== "function") {
        throw TypeError();
    }
    function f() {};
    f.prototype = p;
    return new f();
}

假设定义一个原型对象。例如以下样例中的range.methods,然后通过inherit()函数创建一个继承自它的对象。例如以下样例中的r对象,这样就定义了一个JavaScript类,通常类的实例还须要进一步的初始化,通常是通过定义一个函数来创建并初始化这个对象,例如以下样例中的range()工厂函数,实现一个能表示值的范围的类。

function range(from, to) {
    var r = inherit(range.methods);
    r.from = from;
    r.to = to;
    return r;
}
range.methods = {
    includes: function(x) {
        return this.from <= x && x <= this.to;
    },
    foreach: function(f) {
        for (var x = Math.ceil(this.from); x <= this.to; ++x) {
            f(x);
        }
    },
    toString: function() {
        return "(" + this.from + "..." + this.to + ")";
    }
};
var r = range(1, 3); // 创建一个范围对象
r.inlcudes(2); // true
r.foreach(console.log); // 1 2 3
console.log(r); // (1...3)

2、类和构造函数

上面样例中的自己定义类是通过工厂函数实现的,可是普通情况下,类要定义自己的构造函数。然后通过关键字new来创建新的对象,改写上面的样例例如以下:

function Range(from, to) {
    this.from = from;
    this.to = to;
}
Range.prototype = {
    constructor: Range,
    includes: function(x) {
        return this.from <= x && x <= this.to;
    },
    foreach: function(f) {
        for (var x = Math.ceil(this.from); x <= this.to; ++x) {
            f(x);
        }
    },
    toString: function() {
        return "(" + this.from + "..." + this.to + ")";
    }
};
var rg = new Range(1, 3);

JavaScript中,定义构造函数即是定义类,类名首字母要大写,而普通的函数和方法都是首字母小写,其原型是prototype,这是一个强制命名规则,对构造函数的调用会自己主动使用这个prototype作为新对象的原型。prototype属性的值是一个对象,这个对象包括唯一一个不可枚举属性constructor属性。constructor属性的值是一个函数对象即这个类。

当自己定义prototype时。这个新定义的原型对象不包括有constructor属性。能够通过如上样例中的构造函数反向引用来实现。

3、类和类型

instanceof运算符和isPropertyOf()方法。能够用来检測对象是否属于指定的类名,假设对象包括constructor属性的话,也能够用这个属性来推断对象的类型。另外,使用构造函数的名字也能够作为类标识符。上面几个检測对象的类的各种技术多少都会有些问题,至少在clientJavaScript中是如此。

解决的方法就是规避掉这些问题,不要关注对象的类是什么,而是关注对象能做什么。这就是“鸭式辩型”思路。

4、定义类

JavaScript中的类设计三种不同的对象,它们是构造函数、原型和实例。定义类,能够分三步走,从上面的样例中也看出来了。第一步先定义一个构造函数,并设置初始化新对象的实例属性。第二步給构造函数的prototype对象定义实例的方法。第三步給构造函数定义类字段和类属性。

JavaScript中基于原型的继承机制是动态的,对象从其原型继承属性。假设创建对象之后原型的属性发生改变。也会影响到继承这个原型的全部实例对象,这意味着我们能够通过给原型对象加入新方法来扩充JavaScript类。

5、面向对象编程

类中,我们经常会定制一些方法,如类型转换方法。比較方法,有时还会涉及到方法借用。

在经典的面向对象编程中,经常须要将对象的某个状态封装或隐藏在对象内,仅仅有通过对象的方法才干訪问这些状态,对外值暴露一些重要的状态变量能够直接读写,为了实现这个目的,相似Java/C++的编程语言同意声明类的private实例字段,这些私有实例字段仅仅能被类的实例去訪问,且在类的外部是不可见的。我们能够通过将变量或參数闭包在一个构造函数内来模拟实现私有实例字段,调用构造函数会创建一个实例。为了做到这一点,须要在构造函数内部定义一个函数。并将这个函数赋值给新创建对象的属性。但须要注意的是,这样的封装技术造成了很多其它系统开销,使用闭包来封装类的状态的类一定会比不使用封装类的状态变量的等价类运行速度更慢,并占用很多其它内存。

有时候,我们希望对象的初始化有多种方式,有一个方法能够实现。通过重载这个构造函数让它依据传入參数arguments的不同来运行不同的初始化语法。

JavaScript中继承。子类的prototype要继承自父类的prototype,子类能够作一些有别与父类的特殊处理,如重写父类方法,作一些扩展等,实现这样的需求,还能够使用还有一种技术:组合。组合是编程中广为人知的一种设计原则。有时候实现起来优于继承。

JavaScript中也能够像C++/Java中那样定义抽象类。提供一些通用的接口,方法是在构造函数中不作不论什么事情,仅仅是抛出一个异常,说明这个类是抽象类,不能实例化。

6、模块

将代码组织到类中的一个重要原因是让代码更加模块化,能够在不同的场景中实现代码的重用。但类不是唯一的模块化代码的方式。一般来讲,模块是一个独立的JavaScript文件。模块化的目标是支持大规模的程序开发,不同的模块必须避免改动全局运行上下文,应当尽可能少地定义全局标识。

在模块创建过程中,避免污染全局变量的一种方法是使用一个对象作为命名空间。它将函数和值作为命名空间对象属性存储起来。而不是全局函数和变量。依照约定,模块的文件名称应当和命名空间匹配。

var animals = {};

如上。这个animals对象是模块的命名空间,全部的属性都能够加入到animals对象中,也能够通过animals对象訪问这些属性,对应地文件名称为animals.js。

模块对外导出一些公用API,但模块的实现往往须要一些辅助性API。不须要在模块外部可见,这时就能够使用模块函数,即将函数作用域作为模块的私有命名空间,由于在一个函数中定义的变量和函数都属于函数的局部成员。在函数的外部是不可见的,这样在模块外部也就不可见了。

假设想让代码在一个私有命名空间中运行。仅仅要把代码放到例如以下格式的花括号里就能够了,是一个马上运行的匿名函数:

(function(){...}())

posted on 2017-08-14 21:34  wgwyanfs  阅读(177)  评论(0编辑  收藏  举报

导航