第二篇 关于对象
写在前面:这次来说下 JavaScript 中对象的创建,文中提到的方法均较为常用,其他方法请自行百度。
2.1 创建对象
2.1.1 工厂模式
工厂模式是一种较为简单的设计模式,开发人员将创建对象的具体细节封装在一个函数中。
// 工厂模式
function F(name, age) {
let o = {};
o.name = name;
o.age = age;
o.arr = ['a', 1, 2];
return o
}
let p1 = F('Devin', 34)
p1.arr.push(5)
let p2 = F('Joker', 22)
p2.arr.push(10)
console.log(p1.name, p2.name) //Devin Joker
console.log(p1.age, p2.age) //34 22
console.log(p1.arr, p2.arr) //[ 'a', 1, 2, 5 ] [ 'a', 1, 2, 10 ]
工厂模式解决了创建多个类似对象的问题,但却没有解决对象识别的问题。
2.1.2 构造器模式
诸如Object和Array等原生构造器一样,我们可以创建自定义构造器去生成自定义对象。
// 构造器模式
function F(name, age) {
this.name = name;
this.age = age;
this.arr = ['a', 1, 2];
}
let p1 = new F('Devin', 34)
p1.arr.push(5)
let p2 = new F('Joker', 22)
p2.arr.push(10)
console.log(p1.name, p2.name) //Devin Joker
console.log(p1.age, p2.age) //22 22
console.log(p1.arr, p2.arr) //[ 'a', 1, 2, 5 ] [ 'a', 1, 2, 10 ]
与工厂模式相比,我们可以发现构造器函数存在以下几点不同:
- 没有显式地创建对象
- 直接将属性和方法赋给了this对象
- 没有return语句
使用构造器模式时,需要注意必须使用new操作符。
构造器模式虽然好用,但也存在问题,即它没创建一个对象,所有的属性都要重新创建一边,某些情况下这种做法是并不可取的。
// 构造器模式
function F(name) {
this.name = name;
this.sayName = function () {
return this.name
};
}
let p1 = new F('Devin')
let p2 = new F('Joker')
console.log(p1.sayName()) //Devin
console.log(p2.sayName()) //Joker
console.log(p1.sayName === p2.sayName) //false
上面的代码中sayName函数只是单纯的返回当前对象中的名字,因此重复创建这个函数本身并没有太大的意义,如果可以将sayName函数放入到一个共享的环境中就好了。我们一般会想到将sayName函数放入全局环境中,这种做法可行。
function sayName() {
return this.name
}
function F(name) {
this.name = name;
this.sayName = sayName;
}
console.log(p1.sayName === p2.sayName) //false
这样就很好的解决了浪费内存的问题,但是新的问题又来了,如果有很多个这样的函数,那就不得不在全局作用域中创建很多共享函数,这么做会增加风险,且构造器本身也就没有封装性可言了。好在,这些问题我们可以用过原型模式去解决。
2.1.3 原型模式
我们创建的每个函数都有一个prototype属性,这个属性指向一个对象,这个对象包含所有实例共享的属性和方法。
// 原型模式
function F() {}
F.prototype.name = 'Joker';
F.prototype.age = 22;
let p1 = new F()
let p2 = new F()
console.log(p1,p2) //{ } { }
console.log(p1.name, p2.name) //Joker Joker
p1.name = 'Devin';
p1.age = 34;
console.log(p1.name, p2.name) //Devin Joker
console.log(p1.age, p2.age) //34 22
我们创建了两个不同的实例,从打印结果中可以看出,他们本身是两个空对象,但是访问各自的name属性,均可以打印出Joker,可见,在它们本身不具备该属性时,他们会到constructor的prototype中去找,当为他们定义相同的属性时,实例本身的属性会屏蔽原型上面的属性。
原型模式也有不足之处,其根本来源是由于原型的共享性所致。
// 原型模式
function F() {}
F.prototype.arr = ['a', 1, 2];
let p1 = new F()
let p2 = new F()
p1.arr.push(5)
p2.arr.push(10)
console.log(p1.arr, p2.arr) //[ 'a', 1, 2, 5, 10 ] [ 'a', 1, 2, 5, 10 ]
console.log(F.prototype.arr) //[ 'a', 1, 2, 5, 10 ]
对于prototype中的引用类型,实例中均采用指针的方式对其进行访问。任何实例对其修改都会影响到所有其他的实例对象。因此,在实际生产中,也很少单独使用原型模式。
2.1.4 组合使用原型模式和构造器模式
见题知意,就是将原型模式和构造器模式组合在一起使用。
// 构造器与原型组合模式
function F(name, age) {
this.name = name;
this.age = age;
this.arr = [1, 2, 4]
}
F.prototype.sayName = function () {
return this.name
}
let p1 = new F('Devin', 34)
p1.arr.push(5)
let p2 = new F('Joker', 22)
p2.arr.push(10)
console.log(p1) //{ name: 'Devin', age: 34, arr: [ 1, 2, 4, 5 ] }
console.log(p2) //{ name: 'Joker', age: 22, arr: [ 1, 2, 4, 10 ] }
console.log(p1.sayName()) //Devin
console.log(p2.sayName()) //Joker
在这个例子中,实例属性都是在构造函数中定义的,而共享属性均在prototype属性中。可以看到,修改各自的arr并不会影响到其他实例对象。
2.1.5 其他方法
出上述几种方法以外,还有动态原型模式、寄生构造函数模式、稳妥构造函数模式,这里不做介绍。