理解JavaScript原型的一个入门指南(译文)

当我第一次接触到关于JavaScript对象模型的知识,我的反应是震惊的,充满了不可思议.那是我第一次接触基于原型的语言,我被它的原型特征完全搞糊涂了.我不理解把原型加在JavaScript方法的构造函数组成上有啥不同,我敢打赌,你们中的大多数人也曾有过类似的经历.

但是当我经常使用JavaScript之后,我不仅理解了它的对象模型,而且也爱上了它这部分特性.多亏了JavaScript,我才能发现基于原型语言的典雅与灵活.正因为他们比基于类的语言更简单且更灵活,我现在更倾向于原型语言.

JavaScript的原型

许多指南介绍JavaScript对象时都以方法的构造函数开始,我认为这是个误区,因为他们在一开始就介绍复杂的内容会让JavaScript看起来很难,一开始就理解的很困惑.让我们后续介绍这个,我们先从原型基础说起.

原型链

在JavaScript中,每一个对象都有原型(prototype)属性,当有一个消息指向对象,JavaScript会先尝试在其寻找对应的属性,
如果没有找到,这条消息会被传递到对象的原型上,并依次传递下去.这种工作机制有点像基于类的语言的单亲继承.
原型链可以指向任何你想指向的对象,但是一般来说,太长的原型链会让你的代码理解起来很困难并且很难维护.

proto‘对象

理解JavaScript原型链最简单的就是先理解proto属性,不幸的是,proto属性并不是JavaScript标准的一部分,
至少在ES6之前如此.因此你不能在项目代码中应用它,但不管怎么说,它让解释原型变得更简单.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// let's create an alien object
var alien = {
kind: 'alien'
}
// and a person object
var person = {
kind: 'person'
}
// and an object called 'zack'
var zack = {};
// assign alien as the prototype of zack
zack.__proto__ = alien
// zack is now linked to alien
// it 'inherits' the properties of alien
console.log(zack.kind); //=> ‘alien’
// assign person as the prototype of zack
zack.__proto__ = person
// and now zack is linked to person
console.log(zack.kind); //=> ‘person’

如你所见, proto属性通俗易懂,也很好用.即使我们不能在项目代码中使用它,我也认为这些例子对理解JavaScript对象模型起到很大的帮助.

你可以像如下这样检查一个对象是否是另一个对象的原型:

1
2
console.log(alien.isPrototypeOf(zack))
//=> true

原型查找是动态的

你可以在任何时刻给一个对象的原型添加属性,原型链可以如预期那样找到这个新属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var person = {}
var zack = {}
zack.__proto__ = person
// zack doesn't respond to kind at this point
console.log(zack.kind); //=> undefined
// let's add kind to person
person.kind = 'person'
// now zack responds to kind
// because it finds 'kind' in person
console.log(zack.kind); //=> 'person'

给对象新增/修改的属性,将不会再指向原型

如果你给对象添加一个在原型上已经存在的属性,会发生什么呢?我们来看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var person = {
kind: 'person'
}
var zack = {}
zack.__proto__ = person
zack.kind = 'zack'
console.log(zack.kind); //=> 'zack'
// zack now has a 'kind' property
console.log(person.kind); //=> 'person'
// person has not being modified

注意,属性’kind’现在同时存在于person与zack.

Object.create

正如我说的,由于分配给对象原型的proto属性并不被良好支持,因此下一个最简单的方式是使用Object.create().
这个在ES5中已经支持,但是在一些老的浏览器或者引擎中需要使用es5-shim来转换.

1
2
3
4
5
6
7
8
var person = {
kind: 'person'
}
// creates a new object which prototype is person
var zack = Object.create(person);
console.log(zack.kind); // => ‘person

你可以使用Object.create方法去创建一个新对象,并将另一个对象及一些特殊属性当参数传递进去.

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

是的,你需要传递对象有一点绕,但这就是它的用法.如果你想了解更多,点击这里

Object.getPrototype

你可以使用Object.getPrototypeOf方法去获取对象的原型

1
2
var zack = Object.create(person);
Object.getPrototypeOf(zack); //=> person

注意没有所谓的Object.setPrototype.

构造器函数(Constructor Functions)

在JavaScript中,构造器函数用的最多的地方就是用在构造原型链上.
构造器函数的流行来自于这样一个事实:它是唯一的一种针对构造类型最原始的方式.
当然还有其他一个重要的事实是其他很多引擎已经高度支持构造器函数.

不幸的是,这让人很困惑.在我看来,这是我觉得JavaScript让菜鸟很困惑的一个主要原因.
因此这也是这门语言中很大的一个需要我们去很好理解并掌握的模块.

函数作为构造器

JavaScript中,你可以像下面这样创建一个函数的实例:

1
2
3
4
5
6
function Foo(){}
var foo = new Foo();
//foo is now an instance of Foo
console.log(foo instanceof Foo ) //=> true

像这样使用’new’关键字,看似用工厂模式新建一个函数,实际上表示创建一个新对象.这个新对象的原型会指向这个函数,
这里以后再详述.因此在JavaScript中,我们把’this’称作函数实例.

‘this’是隐式分配的

当我们使用’new’关键字时,JavaScript以’this’关键字的形式注入一个指向新对象的隐式引用.而且,在函数最后,将这个隐式引用返回.
当我们像下面这样时:

1
2
3
4
5
6
function Foo() {
this.kind = ‘foo’
}
var foo = new Foo();
foo.kind //=> ‘foo’

在这个场景后面,又像是在做下面这件事情:

1
2
3
4
5
6
7
8
function Foo() {
var this = {}; // this is not valid, just for illustration
this.__proto__ = Foo.prototype;
this.kind = ‘foo’
return this;
}

但是请记住,当只有使用’new’关键字时,才将隐式的’this’分配到新对象上.如果你忘记使用’new’关键字,’this’将指向全局对象(global object).
忘记使用’new’关键字会引起一系列bug,因此不要忘记使用’new’关键字.

有一个我比较喜欢的约定是当这个函数要被当成函数构造器使用的时候,有效利用函数的第一个字母(译者注:一般我习惯首字母大写),
这样可以很直观的发现你忘记了’new’关键字.

函数的原型属性(The ‘function prototype’)

在JavaScript中,每一个函数都有一个特殊的属性:’prototype’.

1
2
3
4
function Foo(){
}
Foo.prototype

尽管听起来可能很费解,这个’prototype’属性并不是函数的原型.

1
Foo.__proto__ === Foo.prototype //=> false

当然,使用’prototype’表示不一样的东西会引起人们很多疑问.
我认为当表示函数的’prototype’属性的时候,称之为’函数的原型属性’(‘the function prototype’)而不仅仅是原型
可以做到更好的区分.

当使用’new’关键字创建对象, 新对象的原型会指向函数的原型属性(‘prototype’ property).
困惑吧?下面的例子可以更好的解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(name) {
this.name = name;
}
// the function person has a prototype property
// we can add properties to this function prototype
Person.prototype.kind = ‘person’
// when we create a new object using new
var zack = new Person(‘Zack’);
// the prototype of the new object points to person.prototype
zack.__proto__ == Person.prototype //=> true
// in the new object we have access to properties defined in Person.prototype
zack.kind //=> person

上面就是关于JavaScript对象模型你最需要了解的一些知识,理解proto及function.prototype的关系会给你无尽的乐趣及满足,
当然也许并不会.

最后,如果有不对的地方,或者你有什么不解,请留言联系.
(本文译自:A Plain English Guide to JavaScript Prototypes)



money☜☜☜ wechat 『『『 reward 点击扫码打赏 ~~~ ^_^ 』』』alipay ☞☞☞ money