前言
写本《JavaScript简餐》系列文章的目的是记录在阅读学习《JavaScript高级程序设计(第4版)》一书时出现的各个知识点。虽是对读书的笔记和总结,但是希望它轻量、简洁、犀利,不会引起阅读疲劳,可以在碎片化时间和闲暇之余轻巧地沐浴一下知识点。每篇文章只针对一个小部分进行讲解式的梳理,来达到个人复习总结和分享知识的目的。
虽然使用Object构造函数或者对象字面量可以方便地创建对象,但这些方式也有明显的不足:创建具有同样接口的多个对象需要重复写很多代码。比如我要创建三个person对象:person1、person2、person3,那我需要像下面这样来创建。(以创建Object实例的方法为例)
let person1 = new Object(); //每个对象都有自己的属性和方法,显得很臃肿有没有!
person1.name = 'Jack';
person1.age = 29;
person1.job = 'Soft Engineer';
person1.sayname = function () {
console.log(this.name);
}
let person2 = new Object();
person2.name = 'Bob';
person2.age = 30;
person2.job = 'Soft Engineer';
person2.sayname = function () {
console.log(this.name);
}
let person3 = new Object();
person3.name = 'Lucy';
person3.age = 25;
person3.job = 'Soft Engineer';
person3.sayname = function () {
console.log(this.name);
}
像创建上面这样都有着同样的属性和方法的对象,需要创建三遍对象实例并且赋值属性,正如开头所说的一样,要重复编写很多代码。如果要创建成百上千个对象,那岂不是…很恐怖有木有!所以他来了。
一、工厂模式
工厂模式是一种设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。下面的例子展示了一种按照特定接口创建对象的模式。上代码:function createPerson (name, age, job) { // 接受三个参数
let obj = Object;
obj.name = name;
obj.age = age;
obj.job = job;
obj.sayname = function () {
console.log(this.name);
};
return obj; //这里返回一个创建好的对象
}
let person1 = createPerson('Jack', 29, 'SoftWare Engineer'); // 利用不同参数创建多个对象
let person2 = createPerson('Bob', 30, 'SoftWare Engineer');
let person3 = createPerson('Lucy', 25, 'SoftWare Engineer');
这里,函数createPerson()接受三个参数,根据这几个参数构建了一个包含Person信息的对象。可以用不同参数调用这个函数,每次都会返回包含属性和方法的对象,正如上面用不同参数创建了person1、person2、person3三个对象实例一样。这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题,即新创建的对象是什么类型的我们不知道。所以构造函数模式来了。
二、构造函数模式
ECMAScript中的构造函数是用于创建特定类型对象的。像Object就是一个原生构造函数。当然也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。前面的例子用构造函数模式可以这么写,就可以看作实例都是Person类型的了。function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayname = function() {
console.log(this.name);
}
}
let person1 = new Person('Jack', 29, 'SoftWare Engineer');
let person2 = new Person('Bob', 30, 'SoftWare Engineer');
let person3 = new Person('Lucy', 25, 'SoftWare Engineer');
这个例子中,Person()构造函数替代了createPerson()工厂函数。实际上Person()内部的代码跟createPerson()内部基本是一样的,不过有如下几个区别:
- 没有显示地创建对象。
- 属性和方法直接赋值给了this。
- 没有return。
console.log(person1.sayname == person2.sayname) // false
当然像下面这样把函数定义转移到构造函数外部是一个解决方法。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayname = sayname; //将方法转移到了外面(全局作用域)
}
function sayname() {
console.log(this.name);
}
这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱了,因为sayname()函数实际上只能在一个对象上调用。如果这个对象需要多个方法,那么就要在全局作用域下定义多个函数,这会导致自定义类型引用的代码不能很好的聚集在一起。所以原型模式来了。
三、原型模式
无论何时,只要创建一个函数,就会为这个函数创建一个prototype属性,这个属性的值是一个对象(像这样:prototype:{...})。其上面包含应该由特定引用类型的实例共享的属性和方法。实际上与这个prototype属性绑定的值也就是原型对象,是通过构造函数创建的对象的原型。(关于原型下篇文章会再来详细说一说)使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。原来在构造函数中直接赋值给对象实例的值,可以直接复制给它们的原型,如下所示:function Person() {};
Person.prototype.name = 'Jack';
Person.prototype.age = 29;
Person.prototype.job = 'SoftWare Engineer';
Person.prototype.sayname = function () {
console.log(this.name);
}
let person1 = new Person();
let person2 = new Person();
console.log(person1.sayname()); // 'Jack'
console.log(person2.sayname()); // 'Jack'
console.log(person1.sayname == person2.sayname) // true
这个例子中,所有属性和sayname()方法都直接添加到了Person的prototype属性上,构造函数体中什么也没有。但这样定义后,调用构造函数创建的新对象仍然拥有相应的属性和方法。与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。因此person1和person2访问的都是相同的属性和相同的sayName()函数。