JavaScript中可以自定义构造函数,从而自定义对象类型的属性和方法,构造函数本身也是函数,只不 过可以用来创建对象。
1.自定义构造函数
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = function () {
console.log(this.name);
}
}
var person1 = new Person('zhangsan', 29, 'male');
var person2 = new Person('lisi', 19, 'female');
person1.sayName(); // zhangsan
person2.sayName(); // lisi
在这个案例中,Person()构造函数代替了 createPerson()工厂函数。实际上,Person()内部的代码跟 createPerson()基本是一样的,只是有如下区别。
没有显式地创建对象。
属性和方法直接赋值给了 this。
没有 return。
另外,要注意函数名 Person 的首字母大写了。
按照惯例,构造函数名称的首字母都是要大写的, 非构造函数则以小写字母开 头。这是从面向对象编程语言那里借鉴的,有助于在 ECMAScript 中区分构造函数和普通函数。毕竟 ECMAScript 的构造函数就是 能创建对象的函数。
2.创建Person实例
要创建 Person 的实例,应使用 new 操作符。以这种方式调用构造函数会执行如下操作。
var person1 = new Person('zhangsan', 29, 'male');
var person2 = new Person('lisi', 19, 'female');
(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]] 特性被赋值为构造函数的 prototype 属性。
(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。 person1 和 person2 分别保存着 Person 的不同实例。所有对象都会从它的原型上继承一个 constructor 属性,这两个对象的constructor 属性指向 Person,如下所示:
console.log(person1.constructor === Person); // true
console.log(person2.constructor === Person); // true
3.instanceof
constructor 本来是用于标识对象类型的。不过,一般认为 instanceof 操作符是确定对象类型更可靠的方式。
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。或 者说判断一个对象是某个对象的实例。
前面案例中的每个对象都是 Object 的实例,同时也是 Person 的实例,如下面调用
//instanceof 操作符的结果所示:
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
定义自定义构造函数可以确保实例被标识为特定类型,相比于工厂模式,这是一个很大的好处。在这个 案例中,person1 和 person2 之所以也被认为是 Object 的实例,是因为所有自定义对象都继承自 Object。
4.使用函数表达式自定义构造函数
构造函数不一定要写成函数声明的形式。赋值给变量的函数表达式也可以表示构造函数:
var Person = function (name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = function () {
console.log(this.name);
};
}
var person1 = new Person("zhangsan", 29, "male");
var person2 = new Person("lisi", 27, "female");
person1.sayName(); // zhangsan
person2.sayName(); // lisi
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
5.构造函数的问题
构造函数虽然有用,但也不是没有问题。构造函数的主要问题在于,其定义的方法会在每个实例上都创 建一遍。因此对前面的案例而言,person1 和 person2 都有名为 sayName()的方法,但这两个方法不是同 一个 Function 实例。我们知道,ECMAScript 中的函数是对象,因此每次定义函数时,都会初始化一个对 象。逻辑上讲,这个构造函数实际上是这样的:
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = new Function("console.log(this.name)"); // 逻辑等价
}
这样理解这个构造函数可以更清楚地知道,每个 Person 实例都会有自己的 Function 实例用于显示 name 属性。当然了,以这种方式创建函数会带来不同的作用域链和标识符解析。但创建新 Function实例的机 制是一样的。因此不同实例上的函数虽然同名却不相等,如下所示: console.log(person1.sayName === person2.sayName); // false
因为都是做一样的事,所以没必要定义两个不同的 Function 实例。况且,this 对象可以把函数与对象的 绑定推迟到运行时。 要解决这个问题,可以把函数定义转移到构造函数外部:
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = sayName;
}
function sayName() {
console.log(this.name);
}
var person1 = new Person("zhangsan", 29, "male");
var person2 = new Person("lisi", 27, "female");
person1.sayName(); // zhangsan
person2.sayName(); // lisi
在这里,sayName()被定义在了构造函数外部。在构造函数内部,sayName 属性等 于全局 sayName()函数。因为这一次 sayName 属性中包含的只是一个指向外部函 数的指针,所以 person1 和 person2共享了定义在全局作用域上的 sayName()函 数。这样虽然解决了相同逻辑的函数重复定义的问题,但全局作用域也因此被搞乱 了,因为那个函数实际上只能在一个对象上调用。如果这个对象需要多个方法,那么 就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。 这个新问题可以通过原型模式来解决。