创建对象,刚开始我觉得是一件非常简单的事情,就一行代码 var person = {...}
。然而,在我重头学习创建对象后,我发现事情并没有想象中的那么简单。
创建对象,不只是那一行代码那么简单,他还有好几种模式,而且,各种模式之间那种层层递进,不断迭代的关系,让我觉得妙不可言。
创建 Object 实例
我们知道,在 JS 中,所有对象都是 Object
的实例。因此,创建对象
其实就是创建 Object 实例
。
创建 Object 实例,有两种方法:
一种是 使用 new 操作符后接 Object 构造函数:
var person = new Object();
person.name = 'zhang3';
person.age = 29;
一种是对象字面量表示法:开发人员更加青睐这种,简洁:
var person = {
name : "zhang3",
age : 29
};
上面两种方式在创建单个对象的时候,问题不大。但是如果创建很多个相似的对象的时候,会产生大量重复代码:
var person1 = {...};
var person2 = {...};
var person3 = {...};
...
工厂模式
为了解决对象字面量在创建多个相似对象,会产生大量重复代码的问题,人们开始使用下面这样的函数,我们称之为工厂模式
:
function createPerson(name, age){
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function() {
console.log(this.name); // 注意,这里使用的是 this.name
};
return o;
}
var person1 = createPerson("zhang3", 29);
var person2 = createPerson("li4", 27);
优点:解决了创建多个相似对象的问题。
不足:无法解决对象识别的问题。(就是说没有类型,只知道他是个对象,不知道它是谁谁谁的实例)
构造函数模式
原生的构造函数,如 Object、Array,我们可以使用这些构造函数创建对象。其实,我们也可以创建自定义构造函数,从而自定义对象类型的属性和方法:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function(){
console.log(this.name);
};
}
var person1 = new Person("zhang3", 29);
var person2 = new Person("li4", 27);
与工厂模式的不同:
- 首字母大写
- 没有显式的创建一个对象
- 直接将属性和方法赋值给this
- 没有 return 语句
- 使用 new ,而不是调用
之所以有这些不同,依然能达到相同的功能,主要是 new 操作符
的作用:
- 创建一个对象
var person1 = new Object();
- 将构造函数的作用域赋值给新对象
person1.__proto__ = Person.prototype;
- 执行构造函数的代码
Person.call(person1);
- 返回新对象
return person1
优点:可以将实例标识为特定类型。
不足:每个方法要在每个实例上都要创建一遍,方法应该是共享的。
改进:将函数挪到构造函数外面,如下:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName(){
console.log(this.name);
}
var person1 = new Person("Nicholas", 29);
var person2 = new Person("Greg", 27);
依然不足:其一,全局作用域定义的函数只能被某个对象调用;其二,如果这样的函数有好些个,都写在外面,没有封装性。
原型模式
每个函数都有一个 prototype
的属性,它是一个指针,指向函数的原型对象。
函数的原型对象的用途是包含所有实例共享的属性和方法
。
因此,我们可以不在构造函数中定义对象实例的信息,而是将这些信息添加到函数的原型对象中:
function Person(){};
Person.prototype.name = "zhang3";
Person.prototype.age = 29;
Person.prototype.sayName = function(){
console.log(this.name)
};
var person1 = new Person();
person1.sayName(); //"zhang3"
var person2 = new Person();
person2.sayName(); //"zhang3"
person1.sayName == person2.sayName; // true
可以看到 person1 与 person2 的 sayName 方法,访问的是同一个函数。
原型对象有个 constractor
属性,指向构造函数, 构造函数,原型对象,实例之间的关系如下图:
简写:函数的 prototype
属性,指向的是一个原型对象,原型对象也是对象,因此,可以直接将一个对象赋值给 prototype
,不用每次都写 Person.prototype.x = xxx
了:
function Person(){}
Person.prototype = {
name : "zhang3",
age : 29,
sayName : function () {
console.log(this.name)
}
};
问题:每创建一个函数,就会同时创建它的 prototype
对象,这个对象会自动获得 constractor
属性。而简写的方式,相当于是重写了原型对象,就不会默认有 constractor
属性了,因此需要手动将它指定回去。
手动指定:
function Person(){}
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
sayName : function () {...}
};
注意 constructor 的 [[Enumerable]] 会被设置为 true ,默认情况下 这个属性是不可枚举的。
function Person(){}
Person.prototype = {
name : "Nicholas",
age : 29,
sayName : function () {...}
};
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
不足:其一,每个实例都取得相同的属性值,实际情况中,我们是需要自定义每个实例的属性的,这一点可以通过传参解决。其二,最大的问题是,引用类型的共享。
function Person(){}
Person.prototype = {
constructor : Person,
name : "zhang3",
age : 29,
friends : ["aa", "bb"],
sayName : function () {...}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("cc");
console.log(person1.friends); // "aa,bb,cc"
console.log(person2.friends); // "aa,bb,cc"
console.log(person1.friends === person2.friends); // true
组合 构造函数模式 和 原型模式
原型模式的不足之处在于无法传参,可以通过构造函数解决;
构造函数的不足之处在于方法的重复定义,可以通过原型模式解决;
于是,可以将这两种模式组合起来使用,既满足传参,自定义实例属性,又满足方法共享:
function Person(name, age){
this.name = name;
this.age = age;
this.friends = ["aa", "bb"];
}
Person.prototype = {
constructor : Person,
sayName : function () {...}
};
var person1 = new Person('Nicholas', 29,);
var person2 = new Person('Greg', 27);
person1.friends.push("cc");
console.log(person1.friends); // "aa,bb,cc"
console.log(person2.friends); // "aa,bb"
console.log(person1.friends === person2.friends); // false
组合构造函数模式和原型模式,是目前 JS 中使用最广泛,认同度最高的一种创建自定义对象的方法。
到这,如何创建对象的几种模式应该差不多了。其实,还有三种模式,个人感觉可能只是使用场景的不同,才需要使用到他们,下面一起接着来看看。
动态原型模式
个人感觉这种模式,只是将属性定义和方法定义写在了一起而已。(如果你知道它的妙用,欢迎留言告诉我)
function Person(name, age) {
this.name = name;
this.age = age;
if(typeof this.sayName != 'function') { // 只需要检测其中一个方法
Person.prototype.sayName = function() {
console.log(this.name)
}
// 后面还可以定义其他的方法
}
}
寄生构造函数模式
这种模式的基本思想是:创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后返回新创建的对象。
function Person(name, age) {
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function(){...};
return o;
}
var friend = new Person("zhang3", 29);
friend.sayName(); //"zhang3"
从 Person
函数来看,发现,跟工厂模式是一模一样的。只不过是使用了 new 操作符调用 Person。前面说过,new 操作符会返回一个新对象,而 Person 函数末尾的 retrun 会覆盖 new 的返回值。
用途一:可以扩展已有的构造函数
function SpecialArray(){
var values = new Array();
values.push.apply(values, arguments);
values.toPipedString = function() {
return this.join("|");
};
return values;
}
var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipedString()); // "red|blue|green"
疑问:上面如果不使用 new 操作符,直接调用,好像也能达到类似的效果:
var colors2 = SpecialArray("red", "blue", "green");
console.log(colors2.toPipedString()); //"red|blue|green"
那它跟工厂模式有啥区别?这点我比较迷惑,期待大家留言,让我学习下。
而且这种模式创建出来的实例的原型并不是 SpecialArray,可以通过 instanceof 来检验,所以书上也建议说如果有其他模式可以使用,最好不要使用这种模式。
稳妥的构造函数模式
所谓稳妥对象,就是没用公共属性,其方法也不引用实用 this。适合在一些对安全要求比较高的环境中。
function Person(name, age) {
var o = new Object();
... // 定义私有变量和函数,没有公用属性
o.sayName = function() {
// 这里与工厂模式区别,不使用 this
console.log(name)
};
return o;
}
var friend = Person("Nicholas", 29);
作用域安全的构造函数
我们知道,构造函数也是函数,也能直接调用。只不过,在没了 new 操作符后,直接调用,函数中的 this 很有可能是全局环境(例如 window),因此,有可能将函数中的属性和方法定义到全局环境中,可能造成污染。
因此,可以像下面这样,先判断 this 是不是构造函数的实例,这样就锁定了可以调用构造函数的作用域了。
function Person(name, age) {
if (this instanceof Person) {
this.name = name;
this.age = age;
} else {
return new Person(name, age,);
}
}