构造函数
构造函数主要用于初始化新对象。按照惯例,构造函数名第一个字母都要大写。
构造函数有别于其它函数在于它使用new操作符来调用生成一个实例对象。换句话说,如果一个函数使用new操作符来调用,则将其称为构造函数。
function User(name, age) {
this.name = name;
this.age = age;
}
// 调用
var jenemy = new User('jenemy', 25);
jenemy.name; // jenemy
与函数调用和方法调用的不同点在于,构造函数调用是将一个全新的对象作为this变量的值,并隐式返回这个新对象作为调用的结果。
typeof User; // function
typeof jenemy; // object
在JavaScript中每一个对象都有一个constructor属性指向创建这个对象的函数,函数同样也是一个对象。因此有
Person.constructor === Function; // true
jenemy.constructor === User; // true
如果调用构造函数时忘记使用new操作符,那么构造函数将作为一个普通函数调用,此时this将会被绑定到全局对象中。
var xiaolu = User('xiaolu', 25);
xiaolu.name; // Uncaught TypeError: Cannot read property 'name' of undefined
window.name; // xiaolu
window.age; // 25
更加健壮的方式是无论如何调用都按构造函数来工作。
function User(name, age) {
if (!(this instanceof User)) {
return new User(name, age);
}
this.name = name;
this.age = age;
}
var jenemy = new User('jenemy', 25);
var xiaolu = User('xiaolu', 25);
jenemy instanceof User; // true
xiaolu instanceof User; // true
这种模式虽然能够解决问题,但带来了另外一个问题:执行了二次User()
函数的调用,因此代价有点高。此外,如果参数是可变的,这种方式也很难适用。
这里可以使用ES5的Object.create()
来解决上述问题。
function User(name, age) {
var self = this instanceof User ? this : Object.create(User.prototype);
self.name = name;
self.age = age;
return self;
}
Object.create()
方法是创建一个拥有指定原型和若干个指定属性的对象。这里需要注意的是它的第二个参数和Object.defineProperties()
的第二个参数是一样的。
由于Object.create()
只有在ES5才是有效的,因此需要对Object.create()
进行Polyfill
if (typeof Object.create != 'function') {
// 使用匿名函数封装所有私有变量
Object.create = (function() {
function Temp() {};
// 更加安全的引用Object.prototype.hasOwnProperty
var hasOwn = Object.prototype.hasOwnProperty;
return function(O) {
// 如果 O 不是 Object 或者 null,抛出一个 TypeError 异常
if (typeof O != 'object') {
throw TypeError('Object prototype may only be an Object or null');
}
Temp.prototype = O;
var obj = new Temp();
Temp.prototype = null; // 释放临时对象资源
// 如果存在参数 Properties,而不是undefined,那么就把自身属性添加到 obj 上
if (arguments.length > 1) {
var Properties = Object(arguments[1]);
for (var prop in Properties) {
if (hasOwn.call(Properties, prop)) {
obj[prop] = Properties[prop];
}
}
}
return obj;
};
}) ();
}
上述polyfill实现了Object.create()
的所有功能,其实这里只需要每一个参数就可以了,因此可以简化一下
if (Object.create != 'function') {
Object.create = function(O) {
function Temp() {};
if (typeof O != 'object') {
throw TypeError('Object prototype may only be an Object or null');
}
Temp.prototype = O;
return new Temp();
};
}
原型(prototype)
在JavaScript中prototype属性保存了引用类型所有实例方法的真正所在。拿数组操作来讲,push()
方法实际上是保存在prototype名下,只不过是通过其对象的实例访问罢了。
// 实例化一个数组对象
var person = new Array();
// 调用实例化方法push
person.push('jenemy');
// 同样也可以直接调用Array.prototype.push方法。
// 同时注意将this指向当前person数组对象
Array.prototype.push.call(person, 'xiaolu');
person.length; // 2
无论什么时候,只要我们创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。
在上面介绍构造函数的时候提到过每个对象都有一个constructor属性指向创建它的函数。因此所有原型对象都会默认获得一个constructor属性指向prototype属性所在函数的指针。然后,当调用构造函数实例化一个新对象后,该实例内部会有一个标准的指针 [[Prototype]] 指向构造函数的实例对象。虽然没有一个标准的方式去访问 [[Prototype]],但 firefox、Safari 和 Chrome在每个对象上都支持一个属性__proto__
。
注意一点的是__proto__
实际上只存在于构造函数实例与构造函数原型之间,而不是存在于实例与构造函数之间。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function() {
return this.name;
}
var jenemy = new Person('jenemy', 25);
var xiaolu = new Person('xiaolu', 25);
jenemy.name; // jenemy
xiaolu.getName(); // xiaolu
为了便于理解各对象之间的关系,我们将其图形化展示:
|------------------------1---------------------------|
v |
|-----------------| |---------------------| |
| Person | ----->| Person.prototype | |
|-----------------| | |---------------------| |
| prototype |·|------------- | constructor |·|--
|-----------------| | |---------------------|
| name | String | | | getName | Function |
|-----------------| | |---------------------|
| age | String | 2
|-----------------| |
|---------------------------|
| |
|-----------------| | |-------------------| |
| jenemy | | | xiaolu | |
|-----------------| | |-------------------| |
| [[Prototype]] |·|------------| | [[prototype]] |·|-|
|-----------------| |-------------------|
| name | 'jenemy' | | name | 'xiaolu' |
|-----------------| |-------------------|
| age | 25 | | age | 25 |
|-----------------| |-------------------|
然后我们再用代码来验证一下图形所展示的对象之间的关系
// 验证线路1
Person.prototype.constructor === Person; // true
// 验证线路2
Person.prototype.isPrototypeOf(jenemy); // true
Object.getPrototypeOf(xiaolu) === Person.prototype; // true
jenemy.__proto__ === Person.prototype; // true
// 由于jenemy是由new Person()后得到的实例化对象,因此有
jenemy.constructor === Person; // true
这里的Object.isPrototypeOf()
方法用于检查传入的对象是否是传入对象的原型。而Object.getPrototypeOf()
方法返回指定对象的原型(也就是该对象的内部属性[[Prototype]]的值)。
上面我们有使用__proto__
来获取对象的原型,但并不是所有的JavaScript环境都支持通过它来获取对象的原型,因此官方给出了一个标准解决方案就是使用Object.getPrototypeOf()
方法。另外需要注意的是,拥有null原型的对象没有这个特殊的__proto__
属性。
function Person(name, age) {
this.name = name;
this.age = age;
}
var jenemy = new Person('jenemy', 25);
var empty = Object.create(null);
'__proto__' in jenemy; // true
'__proto__' in empty; // false
Object.getPrototypeOf(empty); // null
由于Object.getPrototypeOf()
方法是ES5中提供的方法,对于那些没有提供ES5 API的环境,也可以利用__proto__
属性来实现Object.getPrototypeOf()
函数。
if (typeof Object.getPrototypeOf === 'undefined') {
Object.getPrototypeOf = function(obj) {
var t = typoef obj;
if (!obj || (t!== 'object' && t!== 'function')) {
throw new TypeError('not an object');
}
return obj.__proto__;
}
}
在创建原型属性时,我们同样可以使用对象字面量语法:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
getName: function() {
return this.name;
}
}
Person.prototype.constructor === Person; // false
Person.proottype.constructor; // Object
然后,我们发现这里Person.prototype.constructor
并没有指向创建它的构造函数,而是指向了Object
,原因在于我们重写了Person.prototype
对象,导致Person.prototype
指向了Object
,面Object.prototype.constructor
本来就指向'Object'。解决办法是手机将Person.prototype.constructor
指向Person
。
Person.prototype = {
constructor: Person,
getName: function() {
return this.name;
}
}
Person.prototype.constructor === Person; // true
获取对象的属性列表
对于一个对象的属性遍历,最先想到的就是使用in
操作符在for-in
循环中使用。通过in
操作符可以访问实例中和原型中的可枚举的属性。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.prop1 = 1;
var jenemy = new Person('jenemy', 25);
jenemy.addr = 'shanghai';
'name' in jenemy; // true
'prop1' in jenemy; // true
'addr' in jenemy; // true
另一个就是使用ES5提供的Object.keys()
方法,它会返回一个由给定对象的所有可枚举自身属性的属性名组成的数组,数组中属性名的排列顺序和使用for-in
循环遍历该对象时返回的顺序一致(两者的主要区别是for-in
会枚举原型链中的属性)。
Object.keys(jenemy); // ["name", "age", "addr"]
最后一个是使用ES5提供的Object.getOwnPropertyNames()
方法,它会返回对象所有的实例属性,包括可枚举的和不可枚举的属性。
Object.getOwnPropertyNames(jenemy); // ["0", "1", "2", "length"]
这里length
属性是Object对象的一个不可枚举的属性,因此会被输出。
区分实例属性和原型属性
在JavaScript中我们可以任意修改对象的属性和方法,甚至可以修改内置原型方法。当然,我们不是不建议修改内置对象方法和属性,这样会导致依赖该方法或者属性的其它调用者发生无法预期的结果。
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性。也就是说,它会阻止我们访问原型中的那个属性,但不会修改那个属性。当然,使用delete
操作符可以完全删除实例属性,从而重新访问原型中的属性。
使用hasOwnProperty()
方法可以检测一个属性是存在于实例中,还是存在于原型中。只有属性存在于实例中时才会返回true。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function() {
return this.name;
}
var jenemy = new Person('jenemy', 25);
jenemy.getName(); // jenemy
jenemy.hasOwnProperty('getName'); // false
// 自定义实例属性
jenemy.getName = function() {
return '我的名字叫' + this.name + '我会屏蔽掉原型中的属性值。';
}
jenemy.getName(); // 我的名字叫jenemy我会屏蔽掉原型中的属性值。
jenemy.hasOwnProperty('getName'); // true
delete jenemy.getName;
jenemy.getName(); // jenemy
jenemy.hasOwnProperty('getName'); // false
参考
-《JavaScript高级程序设计》(第3版)
-《JavaScript面向对象精要》
-《Effective JavaScript》