13.核心JavaScript笔记:类和模块

本系列内容由ZouStrong整理收录

整理自《JavaScript权威指南(第六版)》,《JavaScript高级程序设计(第三版)》

每个JavaScript对象都是属性的集合,相互之间没有任何联系

在JavaScript中可以定义对象的类,让一类对象都共享某些属性

在JavaScript中,类的实现是基于原型继承机制的;如果两个实例都从同一个原型对象继承属性,我们称他们为同一个类的实例,并且它们往往都是由同一个构造函数创建的

尽管写法类似,并且可以模拟出很多经典的"类特性"(封装、继承、多态),但是JavaScript中的类和基于原型的继承与传统的java的类和基于类的继承有很大不同,因此我们更喜欢称JavaScript中的类为——类型

JavaScript中类的一个重要特征就是“动态可继承”

一. 类和原型

在JavaScript中,类的所有实例都从同一个原型对象上继承属性,因此,原型对象是类的核心

之前定义过一个inherit()函数,返回一个新函数,新函数继承自一个指定的原型对象

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

如果定义了一个原型对象,然后通过inherit()函数创建一个继承自它的对象,这样就定义了一个JavaScript类,通常,类的实例还需要进一步的初始化,通常是通过定义一个函数来创建并初始化这个新对象

下面实现了一个简单的JavaScript类,给一个表示“值的范围”的类定义了原型对象,还定义了一个“工厂函数”用以创建并初始化类的实例

//这个工厂方法返回一个新的“范围对象”
function range(from,to){
    //使用inherit()函数创建对象,并继承自在下面定义的原型对象
    //原型对象作为函数的一个属性存储,并定义所有范围对象所共享的方法
    var r = inherit(range.methods);
    //存储新的“范围对象”的起始位置和结束位置
    //这两个属性是不可继承的,每个对象都拥有唯一的属性
    r.from = from;
    r.to = to;
    //返回新创建的对象
    return r;
}
//原型对象定义方法,这些方法被每个“范围对象”所继承
range.methods = {
    //如果x在范围内,则返回true,反之返回false
    includes:function(x){
        return this.from<=x && x<=this.to;
    },
    //对于范围内的每个整数都调用一次f函数
    foreach:function(f){
        for(var x=Math.ceil(this.from);x<=this.to;x++){
            f(x);
        }
    },
    toString:function(){
        return "("+this.from+"-"+this.to+")";
    }
}
var obj = range(1,10);
obj.includes(6);   //true
obj.foreach(console.log);   //1,2,3,4,5,6,7,8,9,10
console.log(r);    //"(1-10)"

上面的代码有问题???????????????????

这段代码定义了一个工厂方法range(),用来创建新的范围对象

我们给range()函数定义了一个属性range.methods,用以快捷的存放定义类的原型对象,from和to属性不是共享的,也是不可继承的

range.methods中的可共享、可继承的方法都用到了from和to属性,而且使用了this关键字来指代调用这个方法的对象
,任何类的方法都可以通过this来读取对象的属性

二. 类和构造函数

上面定义类的方式不常用,因为没有定义构造函数,构造函数是用来初始化新创建的对象的

  • 使用new调用构造函数会自动创建一个新对象(构造函数初始化这个新对象的状态)
  • 构造函数的prototype属性会成为新对象的原型
  • 通过同一个构造函数创建的所有对象都继承自一个相同的对象,因此它们都是同一个类的成员

使用构造函数代替工厂函数

function Range(from, to) {
	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 + ")"; 
	}
};
var r = new Range(1,3);
r.includes(2);   //true
r.foreach(console.log);   // 输出 1 2 3
console.log(r);  // 输出 (1...3)

从某种意义上讲,定义构造函数就是定义类,因此构造函数的首字母要大写,并且首字母大写,也可以传递这样一种信息:这是构造函数,请使用new调用

使用构造函数时,没有显示创建新对象,因为在调用构造函数之前就已经创建了新对象,通过this关键字可以获取这个新对象,构造函数只不过是通过this初始化新对象而已

构造函数甚至不必返回这个新对象,构造函数会自动创建新对象,然后将构造函数作为这个对象的方法调用一次,然后返回新对象

第一段代码里,我们随便命名了一个原型对象,然后让新对象继承,第二段代码使用了protorype属性,这是强制的;调用构造函数,会自动将构造函数的prototype属性作为新对象的原型

1. 构造函数和类的标识

原型对象是类的唯一标识,当且仅当两个对象继承自同一个原型时,它们才是同一个类的实例

初始化对象状态的构造函数则不能作为类的标识,只有两个构造函数的prototype属性指向同一个原型时,它们创建的实际才属于同一个类

虽然构造函数不像原型那么基础,但是构造函数是类的公有标识,构造函数的名字通常就是类名

a instanceof A

但是,instanceof运算符并不会检查a是否由A构造函数初始化的,而回检查a是否继承自A.prototype,

function A(){}
function B(){}
var obj = {};
A.prototype = obj;
B.prototype = obj;
var a = new A();
var b  =new B();
a instanceof A;  //true
b instanceof A; //true

2. constructor属性

第二个例子完全将Range.prototype定义为一个新的对象,其实没有必要

因为每个函数都可以用作构造函数,并且调用构造函数是需要用prototype属性的,因此每个函数(bind()方法返回的函数除外)都自动有一个prototype属性

这个属性是一个对象,因此可以直接为该对象添加属性,而不用重写该对象

Range.prototype.includes = function(){};

原型对象包含唯一一个不可枚举属性constructor,该属性返回函数对象

var F = function(){};
var p = F.prototype;
var c = p.constructor;    //c===F

构造函数的原型中存在预先定义好的constructor属性,所以对象继承的constructor属性均指向它们的构造函数,由于构造函数是类的“公共标识”,因此这个constructor属性为对象提供了类

var o = new F();
o.constructor = F;

第二个例子完全将Range.prototype定义为一个新的对象,重写了原型对象,导致这个对象不包含constructor属性

补救措施1:显式设置构造函数

Range.prototype = {
	constructor:Range,
	fun1:fun1,
	.....
};

补救措施2:不要重写原型对象,而是扩展预定义的原型对象

Range.prototype.fun1 = fun1;
Range.prototype.fun2 = fun2;
....

三. Java式的类继承

在Java中,属性和方法分为以下几种

  • 实例属性:基于实例的属性或变量,用于保存独立对象的状态
  • 实例方法:属于所有实例所共享的,由每个独立的实例调用
  • 类属性:属于类的属性,而不是属于类的某个实例
  • 类方法:属于类的方法,而不是属于类的某个实例

JavaScript和Java的一个不同之处在于,JavaScript中的函数也是一种值,因此属性和方法之间没有什么本质的区别,当一个属性是函数的时候,它就是方法,反之,它就是普通的属性

但是我们仍然可以模拟出Java中的四种类成员类型,JavaScript中的类牵扯三种不同的对象,他们和下面三种类成员非常相似

  • 构造函数对象:任何添加到构造函数上的属性都是类属性和类方法
  • 原型对象:原型对象的属性被所有实例继承,因此在原型对象上定义的函数,就是实例方法
  • 实例对象:每个实例都是独立的对象,直接在实例上定义的属性不会被共享,这就是实例属性

在JavaScript中定义类的步骤可以分为三步

  • 定义一个构造函数,并设置初始化新对象的实例属性
  • 给构造函数的prototype对象定义实例方法
  • 给构造函数定义类属性和类方法

封装起来

function defineClass(constructor,methods,statics){
    if(methods){
        constructor.prototype.methods = methods;
    }
    if(statics){
         constructor.statics = statics;              
    }
    return constructor;
}

四. 类的扩充

JavaScript基于原型的继承是动态的

对象从原型继承属性,如果创建对象之后,原型的属性发生了变化,都会影响到继承这个原型的所有实例对象,这意味着我们可以通过给原型对象添加新方法来扩充JavaScript类

所以可以给任意内置对象的原型添加方法,从而使相应的实例对象可以调用这些方法,但是在ECMAScript5之前,这些新增的方法都是可枚举的,无法将他们设为不可枚举

五. 类和类型

...

你可能感兴趣的:(JavaScript)