白话详解JavaScript原型

原文:A Plain English Guide to JavaScript Prototypes



当初我刚学JavaScript的对象模型的时候,我完全被吓到了,而且觉得这个东西很不靠谱。因为这是我第一次接触基于原型的编程语言,所以我被JavaScript的原型特性弄得晕头转向。JavaScript对于原型的实现有其自己独特的一面,就是添加了函数构造器(function constructor),不过这并没能简化学习过程。我敢打赌你们中的大多数人也有类似经历。

但是当我用得多了之后,我不仅理解了JavaScript的对象模型,我也开始喜欢上其中的某些部分了。多亏了JavaScript,它令我发现了原型语言的优雅与灵活之处。我现在可以说非常痴迷于原型语言,因为在对象模型的实现方面,它比基于类的编程语言更加简单和灵活。




JavaScript里的原型(prototype)


大多数教程在解释JavaScript的对象的时候,往往是从构造函数(constructor functions)入手,我觉得这是错误的做法,因为这样做过早引入了太过于复杂的概念,而令JavaScript显得太难于理解。我们不如把这个问题先放在一边,先从原型的一些基础开始讲起。




原型链(prototype chain) ————也称为原型继承(prototype inheritance)


JavaScript的每个对象都有一个原型(prototype)。当一个信息到达一个对象时,JavaScript首先会在这个对象自身上面寻找这个属性,如果没有找到,那么这个单元信息会被发送到这个对象的原型,而这个原型本身又是另一个对象,所以上面的操作被再次执行,这个过程一直这样持续下去直到找到为止。这跟基于类的编程语言里的单继承是一样的。



你想要原型继承链有多长,它就可以有多长,完全由你决定。可是最好不要弄太长,不然你的代码会很难理解,也不好维护。




__proto__对象


理解JavaScript的原型链的最简单的方法莫过于通过__proto__属性。不过很遗憾的是__proto__本身并不在JavaScript的国际标准以内,至少在ES6之前都不是。所以在你的产品代码里不要使用它,但是不管怎么说作为研究学习用途来说,它可以帮助你很容易地理解原型。


// 先创建一个alien对象
>>> var alien = {
  kind: 'alien'
}


// 再创建一个person对象
>>> var person = {
  kind: 'person'
}


// 创建一个叫“zack”的对象
>>> var zack = {};

// 把alien赋值给zack的原型
>>> zack.__proto__ = alien;

// zack现在与alien关联了起来,它“继承”了alien的所有属性
>>> console.log(zack.kind);
alien

// 现在把zack的原型改为person对象
>>> zack.__proto__ = person;

// 那么现在zack与person关联了起来
>>> console.log(zack.kind);
person

现在你看到了,__proto__的用法非常简单明了。虽然不能在产品代码里直接使用它,不过上面这个例子已经很好地解释了JavaScript的对象模型的最基础的部分。


你可以通过下面的代码来验证一个对象是否是另一个对象的原型:
>>> console.log(person.isPrototypeOf(zack));
true


动态原型查找

你可以随时在一个对象的原型上面添加属性,而原型链查找总能按照你所预计的那样找到这个属性。

>>> var person = {}

>>> var zack = {}
>>> zack.__proto__ = person;


// 在这个时候zack上面还没有kind属性
>>> console.log(zack.kind);
undefined

// 现在给person添加kind属性
>>> person.kind = 'person';

// 现在,在zack上面能找到kind属性了,因为它在person上面找到了“kind”
>>> console.log(zack.kind);
person


在对象上面直接对属性进行添加/更新的操作并不会影响到该对象的原型

如果你在一个对象上面直接更新其原型已经有的一个属性,会怎样?

>>> var person = {
  kind: 'person'
}

>>> var zack = {}
>>> zack.__proto__ = person;

>>> zack.kind = 'zack';

>>> console.log(zack.kind);

zack
// zack现在有了自己的“kind”属性

>>> console.log(person.kind);
person
// person并没有被修改

注意,现在“kind”属性同时存在于person和zack上面。




Object.create

之前解释过,__proto__还不是一个被广泛支持的方法。所以目前来说,下一个最简单的操作原型属性的做法是通过Object.create()。它被纳入ES5标准里,就算是那些只支持更古老标准的浏览器和引擎,也可以通过es5-shim来升级到支持这个东东。

>>> var person = {
  kind: 'person'
}


// 创建一个新对象,它的prototype是person
>>> var zack = Object.create(person);

>>> console.log(zack.kind);
person

你甚至可以传给Object.create一个对象,来指定你想要在新创建的对象上面增添的属性。

>>> var zack = Object.create(person, {age: {value:  13} });
>>> console.log(zack.age);

13

是的,这个方法有点绕弯子,可是没有别的办法,你可以参见这里


译者注:
按照上面介绍的方法创建对象后,有一点需要注意,age属性被添加到zack对象上:
>>> console.log(zack.age);
13

可是,在Firebug(2.0.6版本)里输出zack对象,并不会有age的信息:
>>> console.log(zack);
Object { kind="person"}

如果显式地修改age,也不会改变这个情况:
>>> zack.age = 27;
>>> console.log(zack);

Object { kind="person"}

或者添加另一个属性给zack:
>>> zack.height=0;
>>> console.log(zack);

Object { height=0, kind="person"}
可是在Chrome的Developer Tool里面,却是正常的:
>    console.log(zack);
    Object {age: 13,kind: "person"}

Object.getPrototype

你可以通过Object.getPrototypeOf来获得一个对象的原型。
>>> var zack = Object.create(person);
>>> Object.getPrototypeOf(zack);

Object { kind="person"}

注意,没有类似Object.setPrototype这样的东西。




构造函数(Constructor Function)

在JavaScript里,构造函数始终是用来构造原型链的主要方法。构造函数的广为盛行主要是因为它的本意就是用来创建新类型,同时它也是创建类型唯一的方法。还有另外一个重要的考量就是,很多JavaScript引擎都对构造函数做了特别的优化。

不幸的是构造函数也很令人头痛,在我看来它是多数新手觉得JavaScript难于掌握的主要原因。不过它确实也是这个语言的很重要一个环节,我们不得不深入理解。

作为构造器的函数

在JavaScript里,一般来说你会这样创建一个函数的实例:

>>> function Foo(){}

>>> var foo = new Foo();

// 现在foo是Foo的一个实例
>>> console.log(foo instanceof Foo);
true

本质上讲,当跟new关键字一起使用时,函数的行为类似工厂,就是说创建新对象。而这些新创建的对象会跟函数的原型关联起来,迟一些再回来细说。所以在JavaScript的语境里,我们称这个对象为这个函数的一个实例。


“this”被隐式赋值

当使用new的时候,JavaScript会“悄悄地”创建一个叫“this”的引用,来指向新创建的对象。在函数执行的最后,它也会悄悄地返回这个引用。

当我们这样做的时候:

function Foo() {
  this.kind = 'foo';
}

var foo = new Foo();
foo.kind;	//=> "foo"

实际在后面发生的事情更像是这样:

function Foo() {
  var this = {}; // 当然,这样使用this是无效的,只是为了演示
  this.__proto__ = Foo.prototype;
  
  this.kind = 'foo';
  
  return this;
}


但是要记住,只有在你使用new的时候,“this”才会指向新创建的对象,否则在函数里使用“this”将会是指向一个全局变量(译者注:其实就是window对象)。


函数原型属性(function prototype)

JavaScript里面的每一个函数都有一个特别的属性,叫:“prototype”。

>>> function Foo(){
}

>>> Foo.prototype;
Foo {}


它的名字就有些令人费解,更匪夷所思的是,其实这个“prototype”属性并不是函数的真正的原型(__proto__)。

>>> Foo.__proto__ === Foo.prototype;
false

因为人们会用“原型”(prototype)这个词来称呼不同的东西,所以这样一来也就产生了非常多的误会和混乱。我觉得为了避免混淆,当我们想表达函数的“prototype”属性时,应该称之为“函数原型属性”(the function prototype),而不要仅仅称其为“原型”(prototype)。

函数的prototype属性指向着一个对象,而当用new创建新对象时,新对象的prototype属性就被赋值为这个对象——也就是函数的prototype属性指向的东东。很迷惑,是吧?用个例子来解释或许会好些:

>>> function Person(name) {
  this.name = name;
}


// 记住,person函数有一个叫prototype的属性,它其实是个对象,我们可以在这个prototype对象上曾加属性
>>> Person.prototype.kind = 'person';

// 我们用new创建一个新对象
>>> var zack = new Person('Zack');

// 新对象的原型其实指向person.prototype
>>> zack.__proto__ == Person.prototype;
true

// 在新创建的对象上,我们可以访问Person.prototype定义过的属性
>>> zack.kind;
"person"


译者注:
注意isPrototypeOf()方法与instanceof操作符在用法上的区别,尤其是他们的操作对象:
>>> Person.prototype.isPrototypeOf(zack);
true
>>> Person.isPrototypeOf(zack);
false

>>> zack instanceof Person;
true
>>> zack instanceof Person.prototype;
TypeError: invalid 'instanceof' operand Person.prototype


基本上,JavaScript的对象模型的所有重点都在这里了。彻悟__proto__与function.prototype之间的密切关系会给你带来无穷无尽的乐趣和满足,不过也可能相反。


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值