阅读 1056

理解原型其实是理解原型链

前言

原型和原型链,说是两个词,其实理解一个就可以了。这两个概念是同时存在的,不可能抛开一个去谈论另外一个,或者说这两个概念结合在一起才会发挥作用,甚至原型的存在是因为有原型链的存在,不在原型链上的原型只能称之为对象。

原型链

先来说说原型链是个什么东东,说起链我们现在脑海中描绘一下自己对链这个字的第一反应是什么,是社会我大哥的大金链?

是二哈的大狗链?
还是数据结构链表?

皮一下,下面我们正经说原型链,原型链从本质上来讲应该是个链表结构,也就是和上面的单链表有点像,我们把上图中的next换成__proto__属性,data换成键值对集合,这样经常在控制台输出对象的同学会不会有点熟悉的感觉?

举个栗子

下面举个庸俗的例子,有一个对象人,有move和sleep属性,人中又有男人具有sex属性,男人中又有一类人程序员具有code和hair(其值为less)属性,现在我们想用一个对象来表达程序员,那么这个对象应该同时具备人,男人,程序员的属性,我们用原型链来表达他们,就像这样

原型链具备的特征是能够从下往上查找属性,利于当我在要programmer对象中读取sex属性时,浏览器引擎会先在programmer对象中查找该属性,如果未查找到,那么通过其__proto__找到man对象,在man对象中去查找,在man对象中查找到了sex属性,并获取其值‘man’将其返回,就完成了一次属性查找。同理如果要通过programmer获取move,也是这样层层查找自身属性并通过__proto__往上查找。

程序员的睡觉时间与一般人不同,因此需要定义自己的sleep方法,直接在programmer对象上设置sleep属性,那么programmer对象就具有了自己的sleep属性,当通过程序员获取sleep属性时获取到的就是自己定义的sleep属性,也就是说同样都是person,此刻的programmer的sleep已不是peroson的sleep。

原型链的特征

通过上上面的例子,我们不难得出原型链具有的两个基本特征:

  1. 查找属性时可顺链向上查找
  2. 设置属性时只能设置当前对象的属性,而不会影响其上层链上的对象属性

第一点特征常常被人们称为继承,但是应该不能算是真正的继承,只能说在表现上与继承无异。真正意义上的继承是你从某处学会了某项能力,就算只有你一个人的时候你也是具备这项能力的,但是我们的原型链更应该是一个委托链,你可以通过这个委托链获取这个链上自你之后所有对象的能力,如果这个链发生变化你可能会失去某项能力。继承是对象本身具有这个能力或者特性,而原型委托是你及你身后的委托链具备这个能力。当然这对于对象的使用者我们来说是无所谓的,我们不必过分纠结到底是继承还是委托,但是了解事情的本质也是一件不错的事。

其实原型链具备这两点特征实际上是很自然而然的,这样的表现形式并没有太多刻意的违背正常逻辑的人为规定,我们只需稍微思考其在实际中的作用就能理解。

__proto__和prototype的关系

原型的英文是什么来着,嗯,prototype,只要说到原型就会被人们提起的一个词。那么它到底和原型有没有关系呢?这里我要说这个词虽然是原型的意思,其实它和原型并没有什么关系,骚年们以后不要直接在对象上去a.prototype了,这样你大多数情况下得到的只会是undefined(在函数对象上可以获取到值)。能在对象上直接获取其原型的是__proto__,你a.__proto__多数一般都能取到值,这个属性记录了该对象的原型对象地址。

prototype

这个词其实和原型链是有关系的,和原型真的一点关系没有,其作用是用来指定你使用new关键字调用函数的时候生成实例对象的原型(这个原型后面可能还藏着一条原型链)的。下面上代码

var person = {
  move: function() {
    console.log('moving')
  },
  sleep: function() {
    console.log('sleeping')
  }
};

function Man() {
  this.sex = 'man'
}

// 为new Man()得到的对象指定原型对象person
Man.prototype = person;

function Programmer() {
  this.hair = 'less';
  this.code = function() {
    console.log('coding')
  }
}

// 为new Programmer()得到的对象指定原型对象new Man()
Programmer.prototype = new Man();

var programmer = new Programmer();
console.log(programmer);
复制代码

从上面的代码的运行结果中我们不难看出,prototype的作用只是在特定场景下得到的对象的原型(还有其他多种指定对象原型的方式,下面另开小节说明),且这里指定的不仅仅是原型,当指定man为programmer的原型时,同时也意味着man的原型person及person的原型Object这一整个链都被指定给了programmer。

__proto__

上面的运行结果中我们可以看到,在每个对象上都有一个属性__proto__,这个属性不是我们指定的,而且这只是在大多数浏览器中这个属性名是__proto__,这个属性名的作用就是记录对象的原型指向。虽然不一定每个浏览器中都是这个属性名,但是相同的是他们必然都有一个属性用来记录对象的原型。当我们要获取一个对象的原型时应该使用ES的标准API: Object.getPrototypeOf()或者Reflect.getPrototypeOf()(ES6),来获取。

原型链关系图

话说本来只是随便画画的,结果就成了你上面看到那个样子,让我们大家一起来找茬,发现有哪个等式不成立的欢迎在评论区打脸。另表达下个人的对JS中的对象起点观念,我认为是Object.prototype指向的这个对象,不认为是null,不接受反驳(傲娇脸)。关于这一点,这里解释一下,在JS中所有对象的原型链追到最后应该都是Object.prototype指向的对象(通过使用下面的指定原型对象为null的方式的对象除外),上图中所表现的想表明的是具备实际使用意义的原型链起点。

Object.prototype.__proto__的值是null也就是表明Object.prototype对象是存在这个属性的,因为它不是undefined,但是这个值应该被理解为一个原型链的结束符号更为准一些,使用这个符号是为了能够更好的实现JS引擎,而不是语言具备的标准特性。

指定对象原型的几种方式

总结了下指定对象的原型的几种方法,大体可分为非标准操作,标准API操作,特定场景操作。为了方便举例,我们设定一个场景,对象a有name属性,其值为a,对象b有color属性,其值为red,现在要求将a指定为b的原型。

非标准操作

这个是最简单粗暴的方式,直接设置对象的__proto__属性,像下面这样

var a = {name: 'a'};
var b = {id: 'b'};
console.log('a的原型是Object.prototype', Object.getPrototypeOf(a) === Object.prototype);    //true
a.__proto__ = b
console.log('a的原型是b', Object.getPrototypeOf(a) === b);  //true
复制代码

代码传送门

这种方式虽然很简单,但是一般不建议在生产代码中使用,这种写法存在兼容性上的问题,这个没有在标准中规定的属性只是靠各浏览器厂商之间的默契维持,兼容性可想而知。其次这种方式在代码的可维护性上不是很好,毕竟不是谁都知道这个属性__proto__(虽然觉得搞前端的同学应该都知道)。

标准API操作

下面来介绍几个指定对象的API(水字数)。

Object.setPrototypeOf

这个是Object对象的一个静态方法,使用方式如下

var a = {name: 'a'};
var b = {id: 'b'};
console.log('a的原型是Object.prototype', Object.getPrototypeOf(a) === Object.prototype);    //true
Object.setPrototypeOf(b);
console.log('a的原型是b', Object.getPrototypeOf(a) === b);  //true
复制代码

这个方法使用简单,兼容性好,指定对象的原型首推使用这个方法。

Reflect.setPrototypeOf

此API的方式同上,没什么好说的,只是这个ES6标准中提供的方法。按照阮老师的说法,Reflect对象应该会将Object上定义的一些对象操作方法都接收过来。

Object.create

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。 下面上代码

var b = {id: 'b'};
var c = Object.create(b, {
    name: {
    	value: 'a'     
    }
});
console.log('a的原型是b', Object.getPrototypeOf(c) === b);  //true
复制代码

使用Object.create()方法会得到一个指定属性的新对象,这个方法的第一个参数可以指定新得到对象的原型,第二个参数可以指定对象属性值等。

如果你想得到一个纯净的的对象(没有原型),可以在上面三个API使用时指定原型对象那个参数传入null

特定场景操作

将使用new关键词调用函数创建实例对象,通过指定函数的prototype属性来指定对象的方式放在特定场景操作,是因为这种方式不具备上面几种方式的灵活性,不能随时随地的修改对象的原型,使用起来也比较麻烦,怎么用大家应该都懂,这里就不多说了。。

结论

通常我们在谈论原型的时候,应该都是在谈论这种设计模式,这应该是一种思想,一种解决问题的方式,我们对它的理解不应该仅仅停留在对机制的理解上。这种模式的优点在于你只需要在原型上的添加某个属性,指向该原型的所有对象都会具有这个属性,而不用一个一个的去给这些对象添加这个属性。

关注下面的标签,发现更多相似文章
评论