一、深入理解prototype, getprototypeof和_ proto _
prototype,getpropertyof和 _ proto _ 是三个用来访问prototype的方法。它们的命名方式很类似因此很容易带来困惑。
它们的使用方式如下:
c.prototype: 一般用来为一个类型建立它的原型继承对象。比如c.prototype = xxx,这样就会让使用new c()得到的对象的原型对象为xxx。当然使用obj.prototype也能够得到obj的原型对象。 getpropertyof: object.getpropertyof(obj)是es5中用来得到obj对象的原型对象的标准方法。 _ proto _: obj._ proto _是一个非标准的用来得到obj对象的原型对象的方法。为了充分了解获取原型的各种方式,以下是一个例子:
function user(name, passwordhash) { this.name = name; this.passwordhash = passwordhash; } user.prototype.tostring = function() { return [user + this.name + ]; }; user.prototype.checkpassword = function(password) { return hash(password) === this.passwordhash; }; var u = new user(sfalken, 0ef33ae791068ec64b502d6cb0191387);
user函数拥有一个默认的prototype属性,该属性的值是一个空对象。在以上的例子中,向prototype对象添加了两个方法,分别是tostring和checkpassword。当调用user构造函数得到一个新的对象u时,它的原型对象会被自动赋值到user.prototype对象。即u.prototype === user.prototype会返回true。
user函数,user.prototype,对象u之间的关系可以表示如下:
上图中的箭头表示的是继承关系。当访问u对象的某些属性时,会首先尝试读取u对象上的属性,如果u对象上并没有这个属性,就会查找其原型对象。
比如当调用u.checkpassword()时,因为checkpassword定义在其原型对象上,所以在u对象上找不到该属性,就在u的原型上查找,查找顺序是u-> u.prototype(user.prototype)。
前面提到过,getprototypeof方法是es5中用来得到某个对象的原型对象的标准方法。因此:
object.getprototypeof(u) === user.prototype; // true
在一些环境中,同时提供了一个非标准的_ proto _ 属性用来得到某个对象的原型对象。当环境不提供es5的标准方法getprototypeof方法时,可以暂时使用该属性作为替代。可以使用下面的代码测试环境中是否支持_ proto _:
u.__ proto __ === user.prototype; // true
所以在javascript中,类的概念是由构造函数(user)和用于实例间共享方法的原型对象(user.prototype)共同完成的。构造函数中负责构造每个对象特有的属性,比如上述例子中的name和password属性。而其原型对象中负责存放所有对象共有的属性,比如上述例子中的checkpassword和tostring方法。就像下面这张图表示的那样:
二、获取对象优先使用object.getprototypeof,而不是_ proto _
在es5中引入了object.getprototypeof作为获取对象原型对象的标准api。但是在很多执行环境中,也提供了一个特殊的_ proto _属性来达到同样的目的。
因为并不是所有的环境都提供了这个_ proto _属性,且每个环境的实现方式各不相同,因此一些结果可能不一致,例如,对于拥有null原型的对象:
// 在某些环境中 var empty = object.create(null); // object with no prototype _proto _ in empty; // false (in some environments) // 在某些环境中 var empty = object.create(null); // object with no prototype _proto_ in empty; // true (in some environments)
所以当环境中支持object.getprototypeof方法时,优先使用它。即使不支持,也可以为了实现一个:
if (typeof object.getprototypeof === undefined) { object.getprototypeof = function(obj) { var t = typeof obj; if (!obj || (t !== object && t !== function)) { throw new typeerror(not an object); } return obj._proto_; }; }
上述代码首先会对当前环境进行检查,如果已经支持了object.getprototypeof,就不会再重复定义。
三、绝不要修改_ proto _
和object.getprototypeof相比,_ proto _的特殊之处还体现在它能够修改一个对象的原型继承链。因为它是一个属性,除了执行获取它的操作外,还能够对它进行设置。
但是,绝不要修改_ proto _。原因如下:
首先,最显而易见的原因就是便携性。因为不是所有的javascript执行环境都支持这一属性,所以使用了_ proto _ 之后,代码就不能在那些不支持_ proto _的环境中运行了。 其次,是性能上的考虑。现在的javascript引擎的实现都会针对对象属性的存取作出大量的优化,因为这些操作是最常用的。当修改了对象的_ proto _后,就相当于修改了对象的整个继承结构,这样做回导致很多优化都不再可用。 最后,最重要的原因是需要保证程序的可靠性。因为改变_ proto _属性后,对象的原型继承链也许会被完全地改变。当程序中有其他代码依赖于原来的继承链时,就会出现不可意料的错误。通常而言,原型继承链需要保持稳定。当需要为一个新创建的对象赋予一个原型对象时,可以使用es5提供的object.create方法。对于未实现es5标准的环境,可以给出来一个不依赖于_ proto _的object.create方法的实现。
四、解决 _ proto _兼容问题, 让构造函数不再依赖new关键字
在将function当做构造函数使用时,需要确保该函数是通过new关键字进行调用的。
function user(name, passwordhash) { this.name = name; this.passwordhash = passwordhash; }
如果在调用上述构造函数时,忘记了使用new关键字,那么:
var u = user(baravelli, d8b74df393528d51cd19980ae0aa028e); u; // undefined this.name; // baravelli this.passwordhash; // d8b74df393528d51cd19980ae0aa028e
可以发现得到的u是undefined,而this.name以及this.passwordhash则被赋了值。但是这里的this指向的则是全局对象。
如果将构造函数声明为依赖于strict模式:
function user(name, passwordhash) { use strict; this.name = name; this.passwordhash = passwordhash; } var u = user(baravelli, d8b74df393528d51cd19980ae0aa028e); // error: this is undefined
那么在忘记使用new关键字的时候,在调用this.name= name的时候会抛出typeerror错误。这是因为在strict模式下,this的默认指向会被设置为undefined而不是全局对象。
那么,是否有种方法能够保证在调用一个函数时,无论使用了new关键字与否,该函数都能够被当做构造函数呢?下面的代码是一种实现方式,使用了instanceof操作:
function user(name, passwordhash) { if (!(this instanceof user)) { return new user(name, passwordhash); } this.name = name; this.passwordhash = passwordhash; } var x = user(baravelli, d8b74df393528d51cd19980ae0aa028e); var y = new user(baravelli, d8b74df393528d51cd19980ae0aa028e); x instanceof user; // true y instanceof user; // true
以上的if代码块就是用来处理没有使用new进行调用的情况的。当没有使用new时,this的指向并不是一个user的实例,而在使用了new关键字时,this的指向是一个user类型的实例。
另一个更加适合在es5环境中使用的实现方式如下:
function user(name, passwordhash) { var self = this instanceof user ? this : object.create(user.prototype); self.name = name; self.passwordhash = passwordhash; return self; }
object.create方法是es5提供的方法,它能够接受一个对象作为新创建对象的prototype。那么在非es5环境中,就需要首先实现一个object.create方法:
if (typeof object.create === undefined) { object.create = function(prototype) { function c() { } c.prototype = prototype; return new c(); }; }
实际上,object.create方法还有接受第二个参数的版本,第二个参数表示的是在新创建对象上赋予的一系列属性。
当上述的函数确实使用了new进行调用时,也能够正确地得到返回的新建对象。这得益于构造器覆盖模式(constructor override pattern)。该模式的含义是:使用了new关键字的表达式的返回值能够被一个显式的return覆盖。正如以上代码中使用了return self来显式定义了返回值。
当然,以上的工作在某些情况下也不是必要的。但是,当一个函数是需要被当做构造函数进行调用时,必须对它进行说明,使用文档是一种方式,将函数的命名使用首字母大写的方式也是一种方式(基于javascript语言的一些约定俗成)。
以上就是针对javascript的prototype,getprototypeof和__proto__进行的深入学习,希望对大家的学习有所帮助。