JavaScript 创建类和对象的一般性套路
还记得吗,面向对象语言的四大基本准则。
抽象
封装
继承
多态
JavaScript 语言满足以上四点,所以完全能够胜任面向对象编程的要求。一般来说,在 JavaScript 中要创建新的对象,通常有以下几种办法。
对象字面量
动态绑定方式
工厂方式
构造函数方式(我是说真正的函数,而不是隶属于类代码块里面的构造函数)
原型方式
其他(指混合了以上三种方法的方案,也不可小觑)
你要知道 JavaScript 是一门相当灵活的编程语言,所以甚至不需要用到 class
关键字也能完美创建自定义对象。而且 JavaScript 是基于原型而不是基于类的编程语言,在 JavaScript 中是对象继承对象而不是继承类。以下实例大量引用来自 W3cSchool 的代码。
对象字面量
使用对象字面量创建对象在语法上极为简单。它基于字典实现。
var person={"name":"Xiaoming","age":100,"sex":"male"};
如果你想要绑定函数的话。属性名可以不加引号,但是如果是保留字就必须加上。
var person = {
"name":"Xiaoming",
"age":100,
"sex":"male",
"selfIntro":function(){
console.log("My name is "+this.name+" and I am "+age+" years old");
}
};
使用这种方式可以创建一个单独独立的对象,而且当然具有复用性(封装的特点)。但是缺陷是对比像 Java 这样以类为基础的语言,它完全没有继承的概念,也就是说是你虽然可以复用该对象却不能大量生产该对象的同类型对象。
动态绑定
JavaScript 是一门动态的编程语言,这为类结构的书写奠定了基调,毕竟类的本质就是封装么。在 JavaScript 中,有一个超好用的 object 内置对象,它是所有 JavaScript 对象的超类,你可以以它为基础进行扩展,进而达到类结构的构建。
var oCar = new Object;
oCar.color = "blue";
oCar.doors = 4;
oCar.mpg = 25;
oCar.showColor = function() {
alert(this.color);
};
如果你不介意代码变得又臭又长,还可以继续写(我能接着写完这个屏幕)。但是这样做和对象字面量一样有一个致命的缺陷,就是它每次只能创建一个对象,却不能达到批量创建对象的作用。
工厂函数
想要实现批量 JavaScript 对象的批量生产,你需要工厂函数来帮忙。本来函数就是一段代码的封装体,这样一来,只要以这段函数为模版构造对象,就能达到批量生产的效果了,而且这些新做出来对象全都具有相似的属性和功能。
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();
看到了吗,在函数内部定义了一个内部函数,但是从封装类的角度看,这个函数的地位实际上和其他变量一样,都是作为 createCar 对象的属性罢了。注意到外层函数,也就是所谓的封装类,它返回一个新的 oTempcar 对象,这才是最后两行之所以能顺利创建实例的关键。在这之后,你想创建多少由这个该对象为模版的对象都可以,如果你需要的话。有人说可以为函数添加参数,以提升新对象的可扩展性,完全没问题,只要在外层函数参数调用处写好即;有人说可以为了保持代码美观整洁,还可以在外层函数外定义对象的方法属性,同样完全没问题。这里不再赘述。
构造函数
JavaScript 的构造函数可以单独出现,这和包括 Python 在内的一些语言较为不同。这样的设计我自认为较为小巧、灵活。构造函数和前面讲的工厂函数的区别在于,构造函数重点在于构造,它存在之目的即在于创建模版,使用 this
关键字表明特点。最后构造函数没有返回值,这一点和我们通常写的以 class
字眼包裹的那种类倒有几分神似。哦对了,为了迎合类的一般性标准,JavaScript 的构造函数,函数名最好大写。
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);
再说一遍,构造函数没有返回值(也没有所谓的 return
)。和工厂函数不同的是,工厂函数没有创建对象,配合使用 this
和 new
关键字达到构造新对象的效果。
基于原型
JavaScript 是基于原型的编程语言。基于原型构建对象利用了prototype
属性,它使您有能力向对象添加属性和方法。或者你可以把 prototype
理解为外层函数的子对象,事实上确是如此(每个函数都有一个 prototype
子对象~)。prototype
表示该函数的原型,也表示一个类的成员的集合。
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();
调用 new Car()
时,原型的所有属性都被立即赋予要创建的对象,意味着所有 Car 实例存放的都是指向 showColor()
函数的指针。从语义上讲,所有属性看起来都属于一个对象,因此解决了前面两种方式存在的问题。
使用基于原型定义的对象有缺陷吗?当然。参考上面的例子,相当于类的那个函数并没有传入任何参数,即使传入了也用不了,因为之后用 prototype
进行绑定时是忽略那个函数之参数的。另外,如果遇到内层包括封装体的话,一旦改变一个实例对象的该封装体的某个属性的值(有些绕口),以该模版创建的别的对象也会受到影响,同时改变它们的那个属性的属性值。下面是一个例子。
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"
原因在于,使用 prototype
绑定的是 new Array("Mike", "John")
,它和函数封装体一样也是一个对象,也封装类对象属性。事实上,不管该模版创建了多少对象实例,它们只共享同一个 new Array("Mike", "John")
对象,之后不管通过那个对象改变了这个作为属性的对象的值,其他对象会跟着改变。这和 Java 的静态变量倒有些相像。
构造函数 + 基于原型
所谓取长补短,方能相得益彰。使用构造函数与基于原型两者结合的方案构造对象,把变量属性和方法属性分离,将变量属性写在构造函数里,将方法属性,也就是函数属性,用基于原型的思路搞定。
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"
这样做的直接好处就是,方法属性在内存中只创建一次,避免了内存浪费。同时,因为构造函数的介入,又可以调用参数了,岂不美哉。我要说这样创建出来的对象,才和像 Java 之类的强调类的编程语言写出来的对象基本没差了。当然,仅仅从实用性角度看。
动态的基于原型
上面讲到,结合使用构造函数和基于原型,已经基本上可以实现像 Java 这样的独立的、个性化的对象构建了。但是有批评者认为,那种方法虽然实用却不够美观,因为把方法和变量属性分开了。_(:_」∠)_
这些专家认为如果结合使用构造函数和基于原型的话,势必导致代码分散,也就是说会出现明明是完成一个封装类的代码,却出现在了两个不同的代码块中。这样不和谐哦。那怎么办呢?又想节省内存,函数只加载一次。
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;
}
}
恍然大悟……原来是在构造函数内部做一次条件判断,如果该方法没有被初始化,就加载它,否则忽略之。说到底靠的还是 _initialized
这个小不点儿,实在是相见恨晚。动态的基于原型,其实就是以构造函数为主体,封装住包含基于原型的条件判断。
Object.getOwnPropertyNames 方法
Object.getOwnPropertyNames()
方法返回一个数组,其中包含直接在给定对象上找到的所有属性。暂且记住这个方法,留作后用。
const object1 = {
a: 1,
b: 2,
c: 3
};
console.log(Object.getOwnPropertyNames(object1));
// expected output: Array ["a", "b", "c"]
我们可以用它进行分析对象原型的属性所有情况。
console.log(Object.getOwnPropertyNames(Object.prototype));
// Log all properties belong to Object's prototype.
/**
* The properties belongs to Object's prototype.
*/
Array ["toString", "toLocaleString", "valueOf", "hasOwnProperty", "propertyIsEnumerable", "isPrototypeOf", "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__", "__proto__", "constructor"]
让我们分析看看咱们自定义的类的原型之属性。
- 对象字面量
console.log(Object.getOwnPropertyNames(person.prototype));
// raise an error, since oCar does not have an prototype (undefined)
console.log(Object.getOwnPropertyNames(person));
// output: Array ["name", "age", "sex", "selfIntro"]
- 无任何封装的动态绑定
console.log(Object.getOwnPropertyNames(oCar.prototype));
// raise an error, since oCar does not have an prototype (undefined)
console.log(Object.getOwnPropertyNames(oCar);
// output: Array ["color", "doors", "mpg", "showColor"]
- 使用工厂函数
console.log(Object.getOwnPropertyNames(createCar.prototype));
// output: Array ["constructor"]
console.log(Object.getOwnPropertyNames(createCar));
// output: Array ["arguments", "caller", "length", "name", "prototype"]
- 使用构造函数
console.log(Object.getOwnPropertyNames(Car.prototype));
// output: Array ["constructor"]
console.log(Object.getOwnPropertyNames(Car.prototype));
// output: Array ["arguments", "caller", "length", "name", "prototype"]
- 构造函数 + 基于原型
console.log(Object.getOwnPropertyNames(Car.property));
// raise an error, since oCar does not have an prototype (undefined)
console.log(Object.getOwnPropertyNames(Car));
// Array ["arguments", "caller", "length", "name", "prototype"]
- 基于原型创建
console.log(Object.getOwnPropertyNames(Car.property));
// raise an error, since oCar does not have an prototype (undefined)
console.log(Object.getOwnPropertyNames(Car));
// output: Array ["arguments", "caller", "length", "name", "prototype"]
- 动态的基于原型
console.log(Object.getOwnPropertyNames(Car.property));
// raise an error, since oCar does not have an prototype (undefined)
console.log(Object.getOwnPropertyNames(Car));
// Array ["arguments", "caller", "length", "name", "prototype"]
可以看到,除了字面量对象和动态绑定外,只要和函数有关,Object.getOwnPropertyNames()
方法返回的都是相同的东西,即 Array ["arguments", "caller", "length", "name", "prototype"]
,请把握这一点,这也是所有函数的共性。(从这里也体会到 JavaScript 中函数作为对象的强大了吧……)
class 关键字
class
关键字是 ES6 引入的最重要的一个特性。使用该特性可以写出简约风格的封装类,使用 class
关键字的内部实现机制实际上是构造函数和基于原型结合的对象创建方案。毕竟这是一个语法糖~~
一段用构造函数与基于原型写成的封装类。
function User(name) {
this.name = name;
}
User.prototype.sayHi = function() {
alert(this.name);
}
let user = new User("John");
user.sayHi();
如果使用 class
关键字可以写得更优雅。
class User {
constructor(name) {
this.name = name;
}
sayHi() {
alert(this.name);
}
}
let user = new User("John");
user.sayHi();
可以说,只要条件允许(浏览器支持的话),如果要写封装类就应该用 class
关键字。它不但糅合了之前方案的所有优点,而且写起来也比较简单。
同样的,我们使用 Object.getOwnPropertyNames()
方法对自定义类进行测试。
console.log(Object.getOwnPropertyNames(User.property));
// Array ["constructor", "sayHi"]
console.log(Object.getOwnPropertyNames(User));
// output: Array ["length", "name", "prototype"]
如果使用 class
关键字来创建对象,从效果上完全等价于结合使用构造函数与基于原型进行对象的构建,这是针对构造出来的对象而言的。从上面的例子中我们看出,那个用来创建对象的模版(构造函数~严格类)拥有的属性是不同的。且不看每个例子的第一块,就看第二块,你发现在基于构造函数创建对象的方案中,因为是函数,所以它们具有属性 "arguments", "caller", "length", "name", "prototype"
,而这里不再是函数了,是类,具有的属性是不一样的,分别是 "length", "name", "prototype"
。函数模版比类模版多了 arguments
和 caller
属性。可以这样理解,class
描述的类,在前者的基础上封装,同时舍去了一些和严格意义上的类无关的东西。想 arguments
、caller
这样作为属性对象,虽然在源代码中看不见,但却是在运行时隐式声明的。
使用 class
关键字创建对象,有几点提请注意。
JavaScript 的
class
代码块内只存在方法,包括一般方法和构造方法,不存在直接写在代码块中内的变量,但是你可以使用prototype
打破这个限制如果非要不可的话能且只能用
new
关键字来调用constructor()
构造方法如果没有显式声明
constructor()
,JavaScript 会自动创建一个无参数构造方法constructor() {}
对于用
class
声明的类而言,其自动使用use strict
(严格模式)为了更好的实现封装,你自己实现需要
get(..)
、set(..)
方法为了实现类函数级别的方法,而不是将方法附加到 prototype 上,请使用
static
关键字(在 ES6 中和class
一同被引入)
通常我们要使用某个封装对象,不考虑复用性则优先考虑对象字面量。如果想要构建类体系,包括继承(extends
关键字在 ES6 和 class
一同引入)等特性的话,最好使用 class
关键字来创建对象。现在比较新的浏览器都支持。