JavaScript类和模块

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

构造函数

从某种意义上讲,定义构造函数即是定义类,所以构造函数名首字母要大写,而普通的函数都是首字母小写。

// 构造函数,首字母大写
// 注意,这里并没有创建并返回一个对象,仅仅是初始化
function Range(from , to) {
    // 添加2个属性,这2个属性是不可继承的,每个对象都拥有唯一的属性
    this.from = from;
    this.to   = to;
}

// 构造函数的原型对象
Range.prototype = {
    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 + ")"; }
};

// 直接调用构造函数
// 报错"TypeError: a is undefined",因为Range()没有返回值(即返回undefined)
var a = Range(1, 3);

// 通过new构造Range对象并给this赋值
var r = new Range(1, 3);
console.log(r.includes(2));     // true,2在范围内
r.foreach(console.log);         // 输出1 2 3
console.log(r);                 // 输出(1...3)

类的标识

原型对象是类的唯一标识,如果两个构造函数的prototype属性指向同一个原型对象,那么这两个构造函数创建的实例是属于同一类的。

构造函数通常用做类名,当使用instanceof运算符来检测对象是否属于某个类时会用到构造函数。

r instanceof Range

实际上instanceof运算符检查r是否继承自Range.prototype

constructor属性

任何JavaScript函数都可以用做构造函数,并且每个JavaScript函数(ECMAScript5中的Function.bind()方法返回的函数除外)都拥有一个prototype属性。这个属性的值是一个对象,这个对象包含唯一一个不可枚举属性constructor,constructor属性的值是一个函数

var F = function() {};
console.log(typeof F);                      // function
console.log(F.prototype.constructor === F)  // true,对于任意函数F.prototype.constructor == F

var o = new F();
console.log(typeof o);                      // object
console.log(o.constructor === F);           // true

但是在上面的例子Range()中,Range重新定义了prototype,所以创建对象的constructor属性将不再是Range(),而是直接使用Object.prototype.construtor,即Object()。

为了解决这个问题,可以在定义prototype时,显式指定constructor属性的值,如下:

Range.prototype = {
    constructor: Range, // 显式指定构造函数
    ...
};

属性、方法特性

在JavaScript中,属性和方法可以分为以下几种:

类别 含义
实例属性 它们是基于实例的属性或变量,用以保存独立对象的状态。
实例方法 它们是类的所有实例所共享的方法,由每个独立的实例调用。实例方法中使用this存取实例属性
类属性 这些属性是属于类的,而不是属于类的某个实例的。
类方法 这些方法是属于类的,而不是属于类的某个实例的。
function Complex(real, imaginary) {
    if(isNaN(real) || isNaN(imaginary))
        throw new TypeError();

    // 定义2个 “实例属性”
    this.r = real;
    this.i = imaginary;
}

// 定义2个 “实例方法”
Complex.prototype.add = function(that) {
    return new Complex(this.r + that.r, this.i + that.i);
};

Complex.prototype.toString = function() {
    return "{" + this.r + "," + this.i + "}";
};

// 定义2个 “类属性”
Complex.ZERO = new Complex(0, 0);
Complex.ONE  = new Complex(1, 0);

// 定义1个 “类方法”
Complex.equals = function(that) {
    return that != null &&
    that.constructor === Complex &&         // 判断相同类型
    this.r === that.r && this.i === that.i;
};

var c = new Complex(2, 3);      // 使用构造函数创建新的对象
var d = new Complex(c.i, c.r);  // 使用c的 “实例属性”
c.add(d).toString();            // "{5,5}",使用 “实例方法”

类的扩充

JavaScript中基于原型的继承机制是动态的:对象从原型继承属性,如果创建对象之后,原型的属性发生改变,会影响到继承这个原型的所有实例对象。

我们可以通过给原型对象添加新方法来扩充JavaScript类,如下所示:

String.prototype.trim = String.prototype.trim || function() {
    if(!this) return this;                  // 空字符串不做处理
    return this.replace(/^\s+|\s+$/g, "");
};

对象类型的判断

使用instanceof运算符

instanceof运算符的右操作数是构造函数,但计算过程实际上是检测了对象的继承关系,而不是检测创建对象的构造函数。

还可以使用isPrototypeOf()方法来判断原型链上是否存在某个特定的原型对象。

Range.prototype.isPrototypeOf(r);

这种方法的缺点是:

在两个不同框架页面创建的两个数组继承自两个相同但相互独立的原型对象,其中一个框架页面中的数组不是另一个框架页面的Array()构造函数的实例,instanceof运算符结果是false。

使用constructor属性

构造函数是类的公共标识,所以最直接的方法是使用constructor属性,如下:

function typeAndValue(x) {
    if(x == null) return "";    // null和undefined没有构造函数
    switch(x.constructor) {
        // 原始类型
        case Number: return "Number: " + x;
        case String: return "String: " + x;

        // 内置类型
        case Date: return "Date: " + x;
        case RegExp: return "RegExp: " + x;

        // 自定义类型
        case Complex: return "Complex: " + x;
    }
}

这种方式的缺点同使用instanceof一样。

使用构造函数名称

一个函数里的Array()构造函数和另一个窗口中的Array()构造函数是不相等的,但是它们的名字是一样的。所以可以通过构造函数名来判断对象类型。

function type(o) {
    var t, c, n;    // type, class, name

    // 处理null值的特殊情形
    if(o === null) return "null"

    // 处理NaN,NaN和它自身不相等
    if(o !== o) return "nan";

    // 原始类型处理:Number, String, Boolean
    if((t = typeof o) !== "object") return t;

    // 内置类型处理:Date, RegExp
    if((c = classof(o)) !== "Object") return c;

    // 自定义类型处理
    if(o.constructor && typeof o.constructor === "function" &&
        (n = o.constructor.getName()))
            return n;

    return "Object";
}

function classof(o) {
    return Object.prototype.toString.call(o).slice(8, -1);
}

Function.prototype.getName = function() {
    if("name" in this) return this.name;
    return this.name = this.toString().match(/function\s*([^(]*)\(/))[1]);
};

此种方式的缺点是:

如果函数是匿名函数,则getName()返回空字符串,无法进行类型判断。

鸭式辩型

上面提到的各种技术都有些问题,规避掉这些问题的办法是:不要关注”对象的类是什么”,而是关注”对象能做什么”

下面给出一个判断对象是否实现了参数列出的方法:

function quacks(o /*, ... */) {
    for(var i=1; i < arguments.length; i++) {
        var arg = arguments[i];
        switch(typeof arg) {
            case "string":
                if(typeof o[arg] !== "function") return false;
                continue;
            case "function":
                arg = arg.prototype;    // 进入下一个case
            case "object":
                for(var m in arg) {
                    if(typeof arg[m] !== "function") continue;  // 跳过不是方法的属性
                    if(typeof o[m] !== "function") return false;
                }
        }
    }

    return true;
}

这个函数有2点局限性:

  • 只是通过函数名来判断函数是否存在,而没有关注细节信息(函数参数、参数类型等)。
  • 不能应用于内置类型,因为内置类型的方法是不可枚举的。

JavaScript中的面向对象技术

枚举类型的实现


function enumeration(namesToValues) {
    var enumeration = function() { throw "Can‘t Instantiate Enumeration"; }

    var proto = enumeration.prototype = {
        constructor: enumeration,
        toString: function() { return this.name; },
        valueOf:  function() { return this.value; },
        toJSON:   function() { return this.name; }
    };

    enumeration.values = [];

    for(name in namesToValues) {
        // e使用enumeration的原型对象,即与enumberation是同一类型
        var e = inherit(proto);
        e.name = name;
        e.value = namesToValues[name];

        // 定义枚举值
        enumeration[name] = e;
        enumeration.values.push(e);
    }

    enumeration.foreach = function(f, c) {
        for(var i=0; i < this.values.length; i++)
            f.call(c, this.values[i]);
    };

    return enumeration;
}

// 使用4个值创建枚举对象
var Coin = enumeration({Penny: 1, Nickel: 5, Dime: 10, Quarter: 25});
var c = Coin.Dime;
c instanceof Coin;              // true
c.constructor == Coin;          // true
Coin.Quarter + 3*Coin.Nickel;   // 调用valueOf()函数 

对象的值比较

JavaScript的相等运算符比较对象时,比较的是引用而不是值。

我们可以自定义值比较函数,可分为2步实现:

  • 类型比较
  • 属性值比较
// 判断2个集合是否相等
Set.prototype.equals = function(that) {
    // 一些次要情况的快捷处理
    if(this === that) return true;

    // 判断参数是否是集合类型
    if(! (that instanceof Set)) return false;

    // 判断2个集合大小是否相等
    if(this.size() != that.size()) return false;

    // 逐个判断每个元素的值
    try {
        this.foreach(function(v) {
                                    if(!that.contains(v))
                                        // 通过抛异常来终止foreach循环
                                        throw false; });
        return true;
    } catch(x) {
        if(x === false) return false;
        throw x;    // 重新抛出异常
    }
};

方法借用(borrowing)

一个函数可以赋值给2个属性,然后作为2个方法来调用它。把一个类的方法用到其他的类中的做法称为”方法借用”。

私有变量

在经典的面向对象编程中,允许声明类的”私有”实例字段,这些私有实例字段只能被类的实例方法访问,在类的外部是不可见的。

在JavaScript中可以通过将变量(或参数)闭包在一个构造函数内来模拟实现私有实例字段。

// 将Range类的端点进行简单封装
function Range(from, to) {
    this.from = function() { return from; }
    this.to   = function() { return to;   }
}

但需要注意的是,这种封装技术占用更多的内存,并且运行速度更慢

重载构造函数

我们可以通过重载构造函数来执行不同的初始化方法,注意:重载后,原始的构造函数不再可用。

下面给出重载Set()构造函数的代码:

function Set() {
    this.values = {};
    this.n = 0;

    if(arguments.length == 1 && isArrayLike(arguments[0]))
        this.add.apply(this, arguments[0]);
    else if(arguments.length > 0)
        this.add.apply(this, arguments);
}

子类(subclass & superclass)

JavaScript的对象可以从类的原型对象中继承属性。如果O是类B的实例,B是A的子类,那么O也一定从A中继承了属性。

定义子类

// 定义Set的子类,它的成员不能是null和undefined
function NonNullSet() {
    // 直接调用父类的构造函数
    Set.apply(this, arguments);
}

// 将NonNullSet设置为Set的子类
NonNullSet.prototype = inherit(Set.prototype)
// 设置constructor属性
NonNullSet.prototype.constructor = NonNullSet;

// 重写add()方法,不接收null和undefined
NonNullSet.prototype.add = function() {
    for(var i=0; i < arguments.length; i++)
        if(arguments[i] == null)
            throw new Error("Can‘t add null or undefined to a NonNullSet.‘");

    // 调用父类的add()方法
    return Set.prototype.add.apply(this, arguments);
};

对象组合

面向对象编程中有一条设计原则:组合优于继承

下面使用组合代替继承:

function NonNullSet(set) {
    // 存储集合属性
    this.set = set;
}

NonNullSet.prototype.add = function() {
    for(var i=0; i < arguments.length; i++)
        if(arguments[i] == null)
            throw new Error("Can‘t add null or undefined to a NonNullSet.‘");

    // 使用存储的集合对象
    return this.set.add.apply(this.set, arguments);
};

抽象类

JavaScript中也可以模拟实现抽象类,如下:

// 定义一个抽象方法
function abstractmethod() { throw new Error("abstract method"); }

// 定义一个抽象类
function AbstractSet() { throw new Error("Can‘t instantiate abstract classes"); }
AbstractSet.prototype.contains = abstractmethod;

// 定义一个非抽象子类(重定义了contains()方法)
// SingletonSet是只读的,只包含一个成员
var SingletonSet = AbstractSet.extend(
    function SingletonSet(member) { this.member = member; },
    {
        contains: function(x) { return x === this.member; },
        size: function() { return 1; }
    }
);

ECMAScipt5中的类

ECMAScipt5给属性特性增加了方法支持(getter、setter、可枚举性、可写性和可配置性),而且增加了对象可扩展性的限制。

定义不可变的类

// Range的属性都是只读的
function Range(from, to) {
    var props = {
        // writable, configurable属性值都为false
        from: { value: from, enumerable: true, writable: false, configurable: false },
        to:   { value: to,   enumerable: true, writable: false, configurable: false }
    };

    if(this instanceof Range)   // 如果作为构造函数来调用
        Object.defineProperties(this, props);
    else                        // 否则,作为工厂方法来调用
        return Object.create(Range.prototype, props);
}

封装对象状态变量

getter和setter方法可以更健壮地将状态变量封装起来,并且这2个方法是无法删除的。

function Range(from, to) {
    function getFrom() { return from; }
    function getTo()   { return to;   }

    // 设置getter
    Object.defineProperties(this, {
        from: { get: getFrom, enumerable: true, configurable: false },
        to:   { get: getTo,   enumerable: true, configurable: false }
    });
}

防止类的扩展

Object.preventExtensions()可以将对象设置为不可扩展的,即不可添加新属性。

Object.seal()不只将对象设置为不可扩展,同时还将属性设置为不可配置。

模块

一般来讲,模块是一个独立的JavaScript文件。模块文件可以包含一个类定义、一组相关的类、一个实用函数库的代码。

使用对象作为命名空间

在模块创建过程中避免污染全局变量的一种方法是使用一个对象作为命名空间。

var collections;        // 声明一级命名空间"collections"
if(!collections)
    collections = {};

collections.sets = {};  // 声明二级命名空间"sets"

当使用模块文件时,可将模块内的命名空间直接导入到全局命名空间中,如:

var Set = collections.sets;
// 可以直接使用Set来使用collections.sets
// ...

使用函数作为命名空间

在一个函数中定义的变量和函数都属于函数的局部成员,在函数的外部是不可见的。实际上,可以将这个函数作用域用做模块的私有命名空间。

var collections;
if(!collections)
    collections = {};
collections.sets = {};

// 使用函数做命名空间
// 立即执行立即得到定义
(function namespace() {

    collections.sets.AbstractSet = AbstractSet;
    collections.sets.NonNullSet  = NonNullSet;
    // ...

}());
时间: 2024-10-13 12:34:17

JavaScript类和模块的相关文章

Javascript类和模块(二)

鸭式辩型:像鸭子一样走路,游泳并且呱呱叫的鸟就是鸭子.对于Javascript程序员来说,这句话可以理解为"如果一个对象可以像鸭子一样走路.游泳并且嘎嘎叫,就认为这个对象是鸭子,哪怕它并不是从鸭子类的原型对象继承而来的". toJSON()方法:这个方法是由JSON.stringify()自动调用的.JSON格式用于序列化良好的数据结构,而且可以处理Javascript原始值,数组和纯对象.它和类无关,当对一个对象执行序列化操作时,它会忽略对象的原型和构造函数. 使用闭包来封装的状态一

第九章:Javascript类和模块

(过年了,祝大家新年好!) 第6章详细介绍了javascript对象,每个javascript对象都是一个属性集合,相互之间没有任何联系.在javascript中也可以定义对象的类,让每个对象都共享某些属性,这种“共享”的特性是非常有用的.类的成员或实例都包含一些属性,用以存放它们的状态,其中有些属性定义了它们的行为(通常称为方法).这些行为通常是由类定义的,而且为所有实例所共享.例如,假如有一个名为complex的类用来表示复数,同时还定义了一些复数运算.一个complex实例应当包含复数的实

Javascript类和模块(一)

原型对象是类的唯一标识:当且仅当两个对象继承自同一个原型对象时,它们才是属于同一个类的实例.而初始化对象的状态的构造函数则不能作为类的标识,两个构造函数的prototype属性可能指向同一个原型对象.那么这两个构造函数创建的实例是属于同一个类的. 一个典型的面向对象的js程序: functionRange(from,to){ this.from=from; this.to=to; } Range.prototype={ includes:function(x){ return this.from

javascript基础知识-类和模块

在JavaScript中可以定义对象的类,让每个对象都共享这些属性. 在JavaScript中,类的实现是基于其原型继承机制的.如果两个实例都从同一个原型对象上继承了属性,我们就说它们是同一个类的实例. JavaScript中有一个重要特性是"动态可继承"(dynamically extendable),笔记之后再做. 为什么要定义类:定义类是模块开发和重用代码的有效方式之一. 1)类和原型 在JavaScript中,类的所有实例对象都从一个原型对象上继承属性.因此,源性对象是类的核心

JavaScript权威指南第09章 类和模块

类和模块 类的实现是基于继承机制的,如果两个类继承同一个原型对象,那么也就是说,也就意味着使用同一个构造函数来实例化,它们是同一个类的实例. 9.1类和原型 在javascript中,类的实例都是从同一个原型对象上继承属性,所以原型对象是类的核心. 9.2类和构造函数 构造函数是初始化新创建对象的函数. 构造函数的prototype属性被用作新对象的原型,因此,同一个构造函数的实例化的对象都继承自同一个对象. 9.3javasript中的java式的类继承 实例字段 实例方法 类字段 类方法 9

201506300917_《Javascript权威指南(第六版)——类和模块、定义类三步法、定义简单类的函数 》(P200-210)

一. 类和模块 1. 类的实现基于原型继承机制的. 二. 类和原型 三. 类和构造函数 1. 构造函数是用来初始化新创建的对象的. 2. 使用new,所以,构造函数只要初始化对象状态即可. 3. 构建构造函数即是定义类,所以首字母要大写. 4. 四. 构造函数和类的标识 五. constrctor属性 1. constructor属性的值是一个函数对象 例如: var F = function() {}; //这是一个函数对象: var p = F.prototype;  //原型对象 var

实现javascript下的模块组织

前面的话 java有类文件.Python有import关键词.Ruby有require关键词.C#有using关键词.PHP有include和require.CSS有@import关键词,但是对ES5版本的javascript来说,javascript通过script标签引入代码的方式显得杂乱无章,语言自身毫无组织和约束能力,人们不得不用命令空间等方式人为地约束代码,以求达到安全和易用的目的.本文将详细介绍javascript中的模块组织 反模式 反模式(Anti-Pattern)指没有使用任何

你需要知道的 JavaScript 类(class)的这些知识

作者: Dmitri Pavlutin译者:前端小智来源:dmitripavlutin 点赞再看,养成习惯 本文 GitHub https://github.com/qq44924588... 上已经收录,更多往期高赞文章的分类,也整理了很多我的文档,和教程资料.欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西. JavaScript 使用原型继承:每个对象都从原型对象继承属性和方法. 在Java或Swift等语言中使用的传统类作为创建对象的蓝图,在 JavaScript 中不

javascript类继承

function extend(subClass, superClass) { var f = function() {}; f.prototype = superClass.prototype; subClass.prototype = new f(); subClass.superClass = superClass.prototype; } var parent = function (name, age) { this._name = name; this._age = age; };