详解JavaScript基于面向对象之创建对象(2)
本文深入解析JavaScript中基于面向对象创建对象的多种模式。详细对比了纯原型方式的局限性,重点推荐混合构造函数与原型方式,并介绍动态原型模式。通过代码示例说明如何解决属性共享问题,帮助开发者选择最合适的对象创建策略,避免常见陷阱。
JavaScript创建对象概述
承接前文《详解JavaScript基于面向对象之创建对象(1)》,继续探讨相关技术。
原型方式及其局限性
4、原型方式 每个函数都拥有一个名为prototype(原型)的属性,该属性是一个对象,用于包含可由特定类型的所有实例共享的属性和方法。从逻辑上理解,通过调用构造函数创建的那个对象的原型对象,即为prototype。使用原型方式的主要优势在于,它允许所有对象实例共享其中包含的属性和方法。这意味着无需在构造函数内部定义对象信息,而是可以直接将这些信息添加到原型中。 原型方式利用对象的prototype属性,将其视为创建新对象所依赖的原型。具体实现时,首先使用空构造函数来设定函数名,随后将所有属性和方法直接赋予prototype属性。以下是对前述示例的重写代码:
function Car() { };
//将所有的属性的方法都赋予prototype属性
Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.showColor = function() {
return this.color;
};
var Car1 = new Car();
var Car2 = new Car();
document.write(Car1.showColor()+"<br/>");//输出:blue
document.write(Car2.showColor());//输出:blue
在上述代码中,首先定义了构造函数Car(),其内部没有任何代码。随后的几行代码通过给Car的prototype属性添加属性来定义Car对象的属性。当调用new Car()时,原型的所有属性会被立即赋予新创建的对象,这意味着所有Car实例存放的都是指向showColor()函数的指针。从语义角度看,所有属性看起来都属于一个对象,从而解决了前面工厂方式和构造函数方式存在的问题。 此外,使用这种方式,还可以利用instanceof运算符检查给定变量指向的对象的类型:
代码如下:
<span style="font-size:18px;">document.write(Car1 instanceof Car); //输出:true</span> 原型方式看似是一个不错的解决方案,但实际上并不尽如人意。首先,该构造函数没有参数。使用原型方式时,无法通过给构造函数传递参数来初始化属性的值,因为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() {
return this.color;
};
var Car1 = new Car();
var Car2 = new Car();
Car1.drivers.push("Bill");
document.write(Car1.drivers+"<br/>");//输出:Mike,John,Bill
document.write(Car2.drivers);//输出 :Mike,John,Bill
在上述代码中,属性drivers是指向Array对象的指针,该数组中包含两个名"Mike"和 "John"。由于 drivers是引用值,Car的两个实例都指向同一个数组。这意味着给Car1.drivers添加值 "Bill",在 Car2.drivers 中也能看到。输出这两个指针中的任何一个,结果都是显示字符串 "Mike,John,Bill"。由于创建对象时有这么多问题,你一定会想,是否有种合理的创建对象的方法呢?答案是有,需要联合使用构造函数和原型方式。 5、混合的构造函数/原型方式(推荐使用) 混合使用构造函数方式和原型方式,就可像用其他程序设计语言一样创建对象。这种概念非常简单,即用构造函数定义对象的所有非函数属性,用原型方式定义对象的函数属性(方法)。结果是,所有函数都只创建一次,而每个对象都具有自己的对象属性实例。我们重写了前面的例子,代码如下:
function Car(Color,Doors,Mpg) {
this.color = Color;
this.doors = Doors;
this.mpg = Mpg;
this.drivers = new Array("Mike","John");
};
Car.prototype.showColor = function() {
return this.color;
};
var Car1 = new Car("red",4,23);
var Car2 = new Car("blue",3,25);
Car1.drivers.push("Bill");
document.write(Car1.drivers+"<br/>");//输出:Mike,John,Bill
documnet.write(Car2.drivers);//输出:Mike,John
现在就更像创建一般对象了。所有的非函数属性都在构造函数中创建,意味着又能够用构造函数的参数赋予属性默认值了。因为只创建showColor()函数的一个实例,所以没有内存浪费。此外,给Car1的drivers数组添加 "Bill" 值,不会影响到Car2的数组,所以输出这些数组的值时,Car1.drivers 显示的是 "Mike,John,Bill",而 Car2.drivers 显示的是 "Mike,John"。因为使用了原型方式,所以仍然能利用 instanceof运算符来判断对象的类型。 这种方式是ECMAScript采用的主要方式,它具有其他方式的特性,却没有他们的副作用。不过,有些开发者仍觉得这种方法不够完美。 6、动态原型方式 对于习惯使用其他语言的开发者来说,使用混合的构造函数/原型方式感觉不那么和谐。毕竟,定义类时,大多数面向对象语言都对属性和方法进行了视觉上的封装。请考虑下面的 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(Color,Doors,Mpg) {
this.color = Color;
this.doors = Doors;
this.mpg = Mpg;
this.drivers = new Array("Mike","John");
//如果Car对象中的_initialized为undefined,表明还没有为Car的原型添加方法
if (typeof Car._initialized == "undefined") {
Car.prototype.showColor = function() {
return this.color;
};
Car._initialized = true; //设置为true,不必再为prototype添加方法
}
}
var Car1 = new Car("red",4,23);//生成一个Car对象
var Car2 = new Car("blue",3,25);
Car1.drivers.push("Bill");//向Car1对象实例的drivers属性添加一个元素
document.write(Car1.drivers+"<br/>");//输出:Mike,John,Bill
document.write(Car2.drivers);//输出:Mike,John
方式选择与建议
直到检查typeof Car._initialize是否等于"undefined"之前,这个构造函数都未发生变化。这行代码是动态原型方法中最重要的部分。如果这个值未定义,构造函数将用原型方式继续定义对象的方法,然后把 Car._initialized设置为true。如果这个值定义了(它的值为 true时,typeof 的值为Boolean),那么就不再创建该方法。简而言之,该方法使用标志(_initialized)来判断是否已给原型赋予了任何方法。该方法只创建并赋值一次,传统的 OOP开发者会高兴地发现,这段代码看起来更像其他语言中的类定义了。 我们应该采用哪种方式呢? 如前所述,目前使用最广泛的是混合的构造函数/原型方式。此外,动态原型方式也很流行,在功能上与构造函数/原型方式等价。可以采用这两种方式中的任何一种。不过不要单独使用经典的构造函数或原型方式,因为这样会给代码引入问题。总之JS是基于面向对象的一门客户端脚本语言,我们在学习它的面向对象技术的时候要的留意JS与其他严谨性高的程序语言的不同。也要正确使用JS创建对象的合理的方式,推荐使用构造函数与原型方式的混合方式创建对象实例。这样可以避免许多不必要的麻烦。
以上就是JavaScript基于面向对象之创建对象的全部内容,希望对大家的学习有所帮助。
JS语法错误会阻止加载吗?区分SyntaxError与资源加载失败
2026-11-05 16:08:46
JavaScript常见SyntaxError语法错误排查与修复方法
2026-11-05 13:58:44
JS中slice、substring和substr截取字符串的区别
2026-11-05 13:43:47
JS 转 TS 手动迁移最佳实践:从配置 tsconfig 到消除 any 类型
2026-11-05 11:53:32
为什么JS数组push后得到的是数字而不是数组?
2026-11-05 09:59:18
