本来想写一篇关于如何定义类或者对象生成的文章,但是仔细比较之后,还是用高级教程来讲解比较合理,所以直接把文章转发到这里了。
更多内容请参见: 。
注:核心。
使用预定义的对象只是面向对象语言的一部分; 它的真正威力在于能够创建您自己的专门类和对象。
有许多创建对象或类的方法。
工厂方式 原始方式
由于对象属性可以在对象创建后动态定义,因此许多开发人员在首次引入对象属性时会编写如下代码:
var oCar = new Object; oCar.color = "blue"; oCar.doors = 4; oCar.mpg = 25; oCar.showColor = function() { alert(this.color); };
DIY
在上面的代码中,创建了对象 car。 然后赋予它一些属性:它是蓝色的,有四扇门,每加仑可行驶 25 英里。 最后一个属性实际上是一个指向函数的指针,这意味着该属性是一个方法。 执行这段代码后,对象car就可以使用了。
不过,这里有一个问题,就是可能需要创建多个car实例。
解决方案:工厂方法
为了解决这个问题,开发人员创建了工厂函数 ( ),用于创建并返回特定类型的对象。
例如,()可用于封装前面列出的创建汽车对象的操作:
function createCar() { var oTempCar = new Object; oTempCar.color = "blue"; oTempCar.doors = 4; oTempCar.mpg = 25; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; } var oCar1 = createCar(); var oCar2 = createCar();
DIY
在这里,第一个示例中的所有代码都包含在 () 函数中。 此外,还有一行额外的代码返回 car () 作为函数值。 调用此函数将创建一个新对象,为其提供所有必要的属性,并复制我们之前解释过的汽车对象。 因此,通过这种方法,我们可以轻松创建具有完全相同属性的汽车对象的两个版本(oCar1 和 oCar2)。
将参数传递给函数
我们还可以修改 () 函数,将每个属性的默认值传递给它,而不是简单地给属性一个默认值:
function createCar(sColor,iDoors,iMpg) { var oTempCar = new Object; oTempCar.color = sColor; oTempCar.doors = iDoors; oTempCar.mpg = iMpg; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; } var oCar1 = createCar("red",4,23); var oCar2 = createCar("blue",3,25); oCar1.showColor(); //输出 "red" oCar2.showColor(); //输出 "blue"
DIY
通过向 () 函数添加参数,可以为要创建的汽车对象的 color、doors 和 mpg 属性赋值。 这使得两个对象具有相同的属性但属性值不同。
在工厂函数之外定义对象的方法
尽管日益形式化,但创建对象的方法却被忽视了,其规范化到今天仍然是反对的。 部分是出于语义原因(它看起来不像将 new 运算符与构造函数一起使用那么正式),部分是出于功能原因。 功能原因是必须以这种方式创建对象的方法。 在前面的示例中,每次调用 () 时,都会创建 new (),这意味着每个对象都有自己的 () 版本。 事实上,每个对象共享相同的功能。
有些开发者通过在工厂函数之外定义对象的方法,然后通过属性指向该方法来避免这个问题:
function showColor() { alert(this.color); } function createCar(sColor,iDoors,iMpg) { var oTempCar = new Object; oTempCar.color = sColor; oTempCar.doors = iDoors; oTempCar.mpg = iMpg; oTempCar.showColor = showColor; return oTempCar; } var oCar1 = createCar("red",4,23); var oCar2 = createCar("blue",3,25); oCar1.showColor(); //输出 "red" oCar2.showColor(); //输出 "blue"
DIY
在上面重写的代码中,() 是在 () 之前定义的。 在 () 内部,为对象分配一个指向现有 () 函数的指针。 从功能上来说,这解决了重复创建函数对象的问题; 但从语义上讲,该函数看起来不像对象的方法。
所有这些问题导致了开发人员定义的构造函数的出现。
构造方法
创建构造函数就像创建工厂函数一样简单。 第一步是选择类名,即构造函数的名称。 按照约定,该名称的首字母大写,以区别于首字母通常为小写的变量名称。 除了这个区别之外,构造函数看起来很像工厂函数。 考虑以下示例:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.showColor = function() { alert(this.color); }; } var oCar1 = new Car("red",4,23); var oCar2 = new Car("blue",3,25);
DIY
下面解释一下上面代码和工厂方法的区别。 首先,对象不是在构造函数内部创建的,而是使用了this关键字。 使用new运算符构造函数时,在执行第一行代码之前就创建了一个对象,并且只能用this来访问该对象。 然后你可以直接分配这个属性,默认情况下它是构造函数的返回值(不必显式使用运算符)。
现在,使用 new 运算符和类名 Car 创建对象更像是通常创建对象的方式。
您可能会问,这种方式在管理功能方面是否与之前的方式存在同样的问题? 是的。
与工厂函数一样,构造函数重新生成函数,为每个对象创建该函数的单独版本。 然而,与工厂函数类似,构造函数也可以被外部函数覆盖,同样,这样做没有语义意义。 这就是下面要讨论的原型方法的优点所在。
原型
该方法利用了对象的属性,可以将对象视为创建新对象的原型。
在这里,类名首先使用空构造函数设置。 然后,所有属性和方法都直接分配给属性。 我们重写前面的例子,代码如下:
function Car() { } Car.prototype.color = "blue"; Car.prototype.doors = 4; Car.prototype.mpg = 25; Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car(); var oCar2 = new Car();
DIY
在此代码中,首先定义构造函数 (Car),无需任何代码。 接下来的几行代码通过向 Car 的属性添加属性来定义 Car 对象的属性。 当调用 new Car() 时,原型的所有属性立即分配给要创建的对象,这意味着所有 Car 实例都存储指向 () 函数的指针。 从语义上讲,所有属性似乎都属于一个对象,从而解决了前两种方法的问题。
此外,通过这种方式,可以使用运算符来检查给定变量所指向的对象的类型。 因此,以下代码将输出 TRUE:
alert(oCar1 instanceof Car); //输出 "true"
原型设计问题
原型设计方法看起来是一个很好的解决方案。 不幸的是,它并不完全是应有的样子。
首先,这个构造函数没有参数。 使用原型方法,无法通过向构造函数传递参数来初始化属性的值,因为Car1和Car2的color属性都等于“blue”,doors属性都等于4,mpg 都等于25。这意味着在创建对象后必须更改属性的默认值,这很烦人,但还没有结束。 当属性指向对象而不是函数时,真正的问题就会出现。 函数共享不会造成问题,但对象很少被多个实例共享。 考虑以下示例:
function Car() { } Car.prototype.color = "blue"; Car.prototype.doors = 4; Car.prototype.mpg = 25; Car.prototype.drivers = new Array("Mike","John"); Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car(); var oCar2 = new Car(); oCar1.drivers.push("Bill"); alert(oCar1.drivers); //输出 "Mike,John,Bill" alert(oCar2.drivers); //输出 "Mike,John,Bill"
DIY
在上面的代码中,属性是一个指向包含两个名字“Mike”和“John”的Array对象的指针。 由于它们是引用值,因此 Car 的两个实例都指向同一个数组。 这意味着将值“Bill”添加到 oCar1。 在 oCar2 中也可以看到。 打印这两个指针中的任何一个都会产生字符串“Mike,John,Bill”。
面对这么多的对象创建问题,你一定想知道,有没有一种合理的方法来创建对象呢? 答案是肯定的,你需要一起使用构造函数和原型。
混合构造函数/原型方法
结合构造函数和原型,可以像任何其他编程语言一样创建对象。 这个概念很简单,就是用构造函数定义对象的所有非函数属性,用原型定义对象的函数属性(方法)。 结果是所有函数都被创建一次,而每个对象都有自己的对象属性实例。
我们重写前面的例子,代码如下:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.drivers = new Array("Mike","John"); } Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car("red",4,23); var oCar2 = new Car("blue",3,25); oCar1.drivers.push("Bill"); alert(oCar1.drivers); //输出 "Mike,John,Bill" alert(oCar2.drivers); //输出 "Mike,John"
DIY
现在它更像是创建一个普通的对象。 所有非函数属性都是在构造函数中创建的,这意味着可以使用构造函数参数再次为属性分配默认值。 由于只创建了 the() 函数的一个实例,因此不会浪费任何内存。 另外,将“Bill”值添加到oCar1数组中不会影响oCar2数组,因此在输出这些数组的值时,oCar1. 显示“Mike,John,Bill”,而 oCar2. 显示“迈克,约翰”。 因为使用了原型方法,所以仍然可以使用运算符来判断对象的类型。
这种方法是采用的主要方法,它具有其他方法的特性,但没有副作用。 然而,一些开发人员仍然认为这种方法并不完美。
动态原型法
对于习惯其他语言的开发人员来说,使用混合构造函数/原型方法感觉不太和谐。 毕竟,大多数面向对象语言在定义类时都会直观地封装属性和方法。 考虑以下 Java 类:
class Car { public String color = "blue"; public int doors = 4; public int mpg = 25; public Car(String color, int doors, int mpg) { this.color = color; this.doors = doors; this.mpg = mpg; } public void showColor() { System.out.println(color); } }
Java已经很好的封装了Car类的所有属性和方法,所以当你看到这段代码时,你就知道它要实现什么功能。 它定义了对象的信息。 混合构造函数/原型方法的批评者认为,在构造函数内部查找属性并在构造函数外部查找方法是不合逻辑的。 因此,他们设计了一种动态原型方法来提供更友好的编码风格。
动态原型方法的基本思想与混合构造函数/原型方法相同,即在构造函数内部定义非功能属性,而使用原型属性定义功能属性。 唯一的区别是对象方法的分配位置。 这是用动态原型方法重写的 Car 类:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.drivers = new Array("Mike","John"); if (typeof Car._initialized == "undefined") { Car.prototype.showColor = function() { alert(this.color); }; Car._initialized = true; } }
DIY
该构造函数在检查 Car. 等于“”。 这行代码是动态原型方法中最重要的部分。 如果这个值未定义,构造函数会继续以原型的方式定义对象的方法,然后设置Car。 为真。 如果定义了该值(当其值为 true 时, 的值为 is),则永远不会创建该方法。 简而言之,该方法使用 flags() 来判断原型是否已被赋予任何方法。 该方法仅创建和分配一次,传统的 OOP 开发人员会很高兴地发现这段代码看起来更像其他语言中的类定义。
混合工厂方法
当以前的方法无法应用时,此方法通常是一种解决方法。 它的目的是创建仅返回另一种对象的新实例的假构造函数。
这段代码看起来与工厂函数非常相似:
function Car() { var oTempCar = new Object; oTempCar.color = "blue"; oTempCar.doors = 4; oTempCar.mpg = 25; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; }
DIY
与经典方式不同,这种方式使用了 new 运算符,使其看起来像一个真正的构造函数:
var car = new Car();
由于 new 运算符是在 Car() 构造函数内部调用的,因此第二个 new 运算符(位于构造函数外部)将被忽略,构造函数内部创建的对象将被传递回变量 car。
这种方法与有关对象方法内部管理的经典方法存在相同的问题。 强烈建议:除非绝对必要,否则避免使用此方法。
哪一条路
如前所述,最广泛使用的一种是混合构造函数/原型方法。 此外,动态原始方法很流行,并且在功能上等同于构造函数/原型方法。 这两种方式都可以使用。 但不要单独使用经典的构造函数或原型方法,因为这会给代码带来问题。
例子
关于对象的有趣的事情之一是它们用于解决问题的方式。 最常见的问题之一是字符串连接的性能。 与其他语言类似,中的字符串是不可变的,即它们的值不能更改。 考虑以下代码:
var str = "hello "; str += "world";
事实上,这段代码在幕后执行的步骤如下:
创建一个存储“hello”的字符串。 创建一个字符串来存储“world”。 创建一个字符串来存储连接的结果。 将 str 的当前内容复制到结果中。 将“世界”复制到结果中。 更新 str 使其指向结果。
每次完成字符串连接时都会执行步骤 2 到 6,这使得该操作非常消耗资源。 如果这个过程重复数百甚至数千次,可能会导致性能问题。 解决方案是使用 Array 对象来存储字符串,然后使用 join() 方法(使用空字符串参数)创建最终字符串。 想象一下用以下代码替换前面的代码:
var arr = new Array(); arr[0] = "hello "; arr[1] = "world"; var str = arr.join("");
这样,数组中引入多少个字符串并不重要,因为连接操作仅在调用 join() 方法时发生。 此时,需要执行的步骤如下:
创建字符串来存储结果 将每个字符串复制到结果中的适当位置
虽然这个解决方案很好,但还有更好的方法。 问题是,这段代码并没有准确反映它的意图。 为了更容易理解,可以将功能封装成一个类:
function StringBuffer () { this._strings_ = new Array(); } StringBuffer.prototype.append = function(str) { this._strings_.push(str); }; StringBuffer.prototype.toString = function() { return this._strings_.join(""); };
这段代码中首先要注意的是属性,它是私有属性。 它只有两个方法,() 和 () 方法。 ()方法有一个参数,它将参数追加到字符串数组中,()方法调用数组的join方法,并返回实际连接的字符串。 要将一组字符串与一个对象连接起来,可以使用以下代码:
var buffer = new StringBuffer (); buffer.append("hello "); buffer.append("world"); var result = buffer.toString();
DIY
下面的代码可以用来测试对象和传统字符串连接方法的性能:
var d1 = new Date(); var str = ""; for (var i=0; i < 10000; i++) { str += "text"; } var d2 = new Date(); document.write("Concatenation with plus: " + (d2.getTime() - d1.getTime()) + " milliseconds"); var buffer = new StringBuffer(); d1 = new Date(); for (var i=0; i < 10000; i++) { buffer.append("text"); } var result = buffer.toString(); d2 = new Date(); document.write("
Concatenation with StringBuffer: " + (d2.getTime() - d1.getTime()) + " milliseconds");
DIY
此代码对字符串连接进行两次测试,第一个使用加号,第二个使用类。 每个操作连接 10000 个字符串。 日期值d1和d2用于确定操作需要何时完成。 请注意,创建 Date 对象时,如果没有参数,则为该对象赋予当前日期和时间。 要计算连接操作花费的时间,请相互减去日期的毫秒表示形式(由 () 方法返回)。 这是衡量性能的常用方法。 该测试的结果表明,与使用加号相比,使用类比可节省 50% - 66% 的时间。