1. Prototype 属性
JavaScript 中的 function
本质就是一个 object
对象,它本身包含了一些方法(apply(),call())和一些属性(length
, constructor
),这其中还包含一个名为 prototype
的属性。
当你定义了一个 function 后,你就能访问到这个 prototype
属性,它的初始值是一个”空”的 object
对象:
function foo() { ... }
typeof foo.prototype;
// "object"
你可以随意设定这个对象,给它加上属性或者方法,但是这不会对这个 function
本身造成任何影响,除非你把它最为构造函数来使用。
使用 prototype 来添加方法和属性
当使用 new
来实例化一个对象时,在 function
内可以通过 this
关键字来对这个对象进行成员追加:
function Gadget(name, color) {
this.name = name;
this.color = color;
this.whatAreYou = function () {
return ‘I am a ‘ + this.color + ‘ ‘ + this.name;
};
}
另外我们也可以在 function
的 prototype
属性上进行相同的处理:
Gadget.prototype.price = 100;
Gadget.prototype.rating = 3;
Gadget.prototype.getInfo = function () {
return ‘Rating: ‘ + this.rating + ‘, price: ‘ + this.price;
};
prototype
对象甚至可以被整个替换:
Gadget.prototype = {
price: 100,
rating: ... /* 其他成员... */
};
2. 使用 prototype 的方法和属性
prototype
上添加的方法和属性都能够在实例化后的对象上调用:
var newtoy = new Gadget(‘webcam‘, ‘black‘);
newtoy.name;
// "webcam"
newtoy.color;
// "black"
newtoy.whatAreYou();
// "I am a black webcam"
newtoy.price;
// 100
newtoy.rating;
// 3
newtoy.getInfo();
// "Rating: 3, price: 100"
object
对象在 Javascript 中都是以引用方式传递的,所以 prototype
并非在每个实例对象中保存一份。当你改变 prototype
时,所有的实例对象都能立即“察觉”这些变动。假设我们再增加一个新的方法:
Gadget.prototype.get = function (what) {
return this[what];
};
前文的 newtoy 虽然在此前已经被实例化,但他仍然能使用到这个新方法:
newtoy.get(‘price‘);
// 100
newtoy.get(‘color‘);
// "black"
自有属性 与 prototype 属性
先前我们定义过一个 getInfo() 方法,它修改为以下的方式后也能获得同样的输出结果:
Gadget.prototype.getInfo = function () {
return ‘Rating: ‘ + Gadget.prototype.rating + ‘, price: ‘ + Gadget.prototype.price;
};
这个原由需要从头说起,先来看 newtoy 对象是怎样实例化的:
var newtoy = new Gadget(‘webcam‘, ‘black‘);
当你访问 newtoy 的某个属性的时候(这里假设是 newtoy.name),JavaScript 引擎会搜索它的所有名为 name 的属性,如果发现了就返回它的值:
newtoy.name;
// "webcam"
当你访问 rating 属性时情况变了,JavaScript 引擎在 newtoy 上找不到名为 rating 的属性,然后他就会到 newtoy 的构造函数(Gadget) 的 prototype
属性上继续查找:
newtoy.rating;
// 3
以下的代码验证了这一点:
newtoy.constructor === Gadget;
// true
newtoy.constructor.prototype.rating;
// 3
我们知道每个 object 对象都有一个构造函数,那么作为 object
对象的 prototype
也必然存在一个构造函数。这就形成了一个 prototype chain (prototype 链),这个链的最上层就是内置的 Object() 对象。 要验证这一点很容易,newtoy 没有 toString() 方法,它的 prototype
上也没有,但是你却能调用 newtoy.toString() ,因为 object
对象有这个方法:
newtoy.toString();
// "[object Object]"
自有属性复写 prototype 属性
当自有属性与 prototype
属性重名时,自有属性优先:
function Gadget(name) {
this.name = name;
}
Gadget.prototype.name = ‘mirror‘;
var toy = new Gadget(‘camera‘);
toy.name;
// "camera"
使用 hasOwnProperty() 可以知道某个属性知否是自有属性:
toy.hasOwnProperty(‘name‘);
// true
我们把自有属性 name 删了再瞧瞧什么情况:
delete toy.name;
// true
toy.name;
// "mirror"
toy.hasOwnProperty(‘name‘);
// false
Enumerating properties
使用 for-in 语句能够遍历出一个对象的所有属性:
虽然 for-in 也适用于数组,但建议遍历数组时采用 for,遍历对象时采用 for-in。
var params = {
productid: 666,
section: ‘products‘
};
var url = ‘http://example.org/page.php?‘,
i,
query = [];
for (i in params) {
query.push(i + ‘=‘ + params[i]);
}
url += query.join(‘&‘);
以上代码输出:
http://example.org/page.php?productid=666§ion=products
以下几点需要注意:
- 并非所有属性能够在 for-in 中遍历到,譬如
constructor
属性等等。所有能遍历到的属性称为 enumerable。你可以用 propertyIsEnumerable() 方法来区分,在 ES5 中你甚至可以自定义哪些属性是 enumerable。 - Prototype 链上的 enumerable 属性也会被遍历到。
- 由 Prototype 链上而来的 enumerable 属性,被传入 propertyIsEnumerable() 方法时总返回
false
。
通过实例来看一下:
function Gadget(name, color) {
this.name = name;
this.color = color;
this.getName = function () {
return this.name;
};
}
Gadget.prototype.price = 100;
Gadget.prototype.rating = 3;
var newtoy = new Gadget(‘webcam‘, ‘black‘);
for (var prop in newtoy) {
console.log(prop + ‘ = ‘ + newtoy[prop]);
}
// 输出
name = webcam
color = black
getName = function () {
return this.name;
}
price = 100
rating = 3
再试一下 propertyIsEnumerable() 方法:
// 自有属性
newtoy.propertyIsEnumerable(‘name‘);
// true
// 内置属性
newtoy.propertyIsEnumerable(‘constructor‘);
// false
// prototype 链上的属性
newtoy.propertyIsEnumerable(‘price‘);
// false
// 改变调用对象后
newtoy.constructor.prototype.propertyIsEnumerable(‘price‘);
// true
isPrototypeOf()
isPrototypeOf() 可以用来确认某个对象是否另一个对象的 prototype
。
var monkey = {
hair: true,
feeds: ‘bananas‘,
breathes: ‘air‘
};
function Human(name) {
this.name = name;
}
Human.prototype = monkey;
var george = new Human(‘George‘);
monkey.isPrototypeOf(george);
// true
那么当你对 prototype
一无所知时,怎么办呢?对于支持 ES5 的环境,你可以使用 getPrototypeOf() 方法。
> Object.getPrototypeOf(george).feeds;
"bananas"
> Object.getPrototypeOf(george) === monkey;
true
如果遇到不支持 ES5 的环境,你可以使用 __proto__ 这个特殊的属性。
__proto__ 连接
前文提到当你访问一个非自有属性时, 引擎通过 prototype
继续查找:
var monkey = {
feeds: ‘bananas‘,
breathes: ‘air‘
};
function Human() {}
Human.prototype = monkey;
var developer = new Human();
developer.feeds = ‘pizza‘;
developer.hacks = ‘JavaScript‘;
developer.feeds;
// "pizza"
developer.breathes;
// "air"
在现今许多 JavaScript 环境中,都是通过一个名为 __proto__ 的属性来实现的:
developer.__proto__ === monkey;
// true
需要注意的是 __proto__ 与 prototype 是不同的,__proto__ 是一个实例对象的属性,而 prototype
是一个构造函数的属性。
typeof developer.__proto__;
// "object"
typeof developer.prototype;
// "undefined"
typeof developer.constructor.prototype;
// "object"
你应当仅在调试环境中使用这个 __proto__ 属性来获取信息
3. 内置对象的扩展
内置的构造函数(诸如:Array,String 以及 Object)都可以通过 prototype
来进行扩展。见示例:
Array.prototype.inArray = function (needle) {
for (var i = 0, len = this.length; i < len; i++) {
if (this[i] === needle) {
return true;
}
}
return false;
};
var colors = [‘red‘, ‘green‘, ‘blue‘];
colors.inArray(‘red‘);
// true
colors.inArray(‘yellow‘);
// false
上面的方法与 apply() 等函数的灵活结合可以写出非常高效的代码。譬如我们为 String 增加一个字符串反转的函数:
String.prototype.reverse = function () {
return Array.prototype.reverse.apply(this.split(‘‘)).join(‘‘);
};
代码是不是简洁得出乎你的意料?
内置对象的扩展 – 注意点
扩展内置对象是一个强大的功能,但不应当过度使用,因为对这些内置对象的修改会对使用者和维护者造成困惑。
另外随着各个浏览器的升级,JavaScript 环境也会对这些内置对象进行扩展,这就可能与你的扩展造成冲突。
目前有一些类库致力于在不同的 JavaScript 环境中提供一致的调用接口,这些类库被称之为 shims 或 polyfills。
自行对内置对象的扩展应当谨慎,以下提供一种比较保险的做法:
if (typeof String.prototype.trim !== ‘function‘) {
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g,‘‘);
};
}
" hello ".trim();
// "hello"
Prototype 陷阱
当你操作 prototype
时应当牢记以下两点:
- 如果你替换了整个
prototype
对象,那么你也打断了 prototype 链 - prototype.constructor 是不可靠的
非常抽象难懂对不对,看个例子你就明白了:
function Dog() {
this.tail = true;
}
var benji = new Dog();
此时如果你扩展 Dog(),prototype 链能保证已经实例化的 benji 能够使用到新的扩展:
Dog.prototype.say = function () {
return ‘Woof!‘;
};
benji.say();
// "Woof!"
benji.constructor === Dog;
// true
接着我们整个替换掉 Dog() 的 prototype 会怎样呢?
Dog.prototype = {
paws: 4,
hair: true
};
typeof benji.paws;
// "undefined"
benji.say();
// "Woof!"
typeof benji.__proto__.paws;
// "undefined"
typeof benji.__proto__.say;
// "function"
早先实例化的 benji 对象无法访问到扩展成员(paws, hair),但它仍然能调用替换前的成员。
那么对于新的实例对象是什么情况呢?
var lucy = new Dog();
lucy.say();
// TypeError: lucy.say is not a function
lucy.paws;
// 4
typeof lucy.__proto__.say;
// "undefined"
typeof lucy.__proto__.paws;
// "number"
这个新的实例对象显然可以调用到最新的扩展方法,如果你检查一下 constructor
属性你会发现返回值为 Object(),而不是预期的 Dog()。
lucy.constructor;
// function Object() { [native code] }
benji.constructor;
// function Dog() {
// this.tail = true;
// }
你可以在整个替换 prototype
属性后强行改正 constructor
属性来避免发生这样的困扰。
function Dog() { ... }
Dog.prototype = { ... };
new Dog().constructor === Dog;
// false
// 强行改正 constructor
Dog.prototype.constructor = Dog;
new Dog().constructor === Dog;
// true
强烈建议每次整个替换
prototype
属性后强行设置一下constructor
属性。
4. 总结
- 所有
function
都有一个名为prototype
的属性,初始化时他只是一个空对象。 - 你可以在这个
prototype
对象上增加属性或方法,也可以整个替换掉它。 - 当你使用一个
function
来实例化一个对象时,这个对象会保存一个指向function
的prototype
属性的链接。 - 对象的自有属性优先于
prototype
属性。 - hasOwnProperty() 可以用来区分自有属性和
prototype
属性。 - JavaScript 的查找本质是在 prototype 链上查找。
- 内置的构造函数可以扩展,但扩展是需要注意不同的 JavaScript 环境,并在扩展前做好确认检查。