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;
  };
}

另外我们也可以在 functionprototype 属性上进行相同的处理:

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&section=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('');
};

代码是不是简洁得出乎你的意料?

内置对象的扩展 – 注意点

扩展内置对象是一个强大的功能,但不应当过度使用,因为对这些内置对象的修改会对使用者和维护者造成困惑。
另外随着各个的升级,Script 环境也会对这些内置对象进行扩展,这就可能与你的扩展造成冲突。
目前有一些类库致力于在不同的 环境中提供一致的调用接口,这些类库被称之为 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 来实例化一个对象时,这个对象会保存一个指向 functionprototype 属性的链接。 对象的自有属性优先于 prototype 属性。 hasOwnProperty() 可以用来区分自有属性和 prototype 属性。 JavaScript 的查找本质是在 prototype 链上查找。 内置的构造函数可以扩展,但扩展是需要注意不同的 JavaScript 环境,并在扩展前做好确认检查。