Javascript 之 Prototype

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

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

内置对象的扩展 – 注意点

扩展内置对象是一个强大的功能,但不应当过度使用,因为对这些内置对象的修改会对使用者和维护者造成困惑。
另外随着各个浏览器的升级,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 来实例化一个对象时,这个对象会保存一个指向 functionprototype 属性的链接。
  • 对象的自有属性优先于 prototype 属性。
  • hasOwnProperty() 可以用来区分自有属性和 prototype 属性。
  • JavaScript 的查找本质是在 prototype 链上查找。
  • 内置的构造函数可以扩展,但扩展是需要注意不同的 JavaScript 环境,并在扩展前做好确认检查。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值