本文深入解析JavaScript中prototype、getPrototypeOf和__proto__三个核心概念的区别与联系。重点阐述ES5标准API Object.getPrototypeOf的推荐用法,分析__proto__非标准属性的兼容性与性能隐患,并探讨构造函数调用模式及Object.create的安全实现,帮助开发者建立正确的原型继承认知。
深入理解prototype, getPrototypeOf和__proto__
一、深入理解prototype, getPrototypeOf和__proto__
prototype、getPrototypeOf和__proto__是三种访问原型链的方式。由于命名相似,初学者容易混淆它们的具体用途。
三者的使用场景如下:
- C.prototype通常用于为构造函数C定义其原型继承对象。例如执行C.prototype = xxx后,通过new C()创建的对象将拥有xxx作为其原型。此外,直接访问obj.prototype也能获取该对象的原型。
- getPrototypeOf是ES5标准中获取对象原型的规范方法,调用Object.getPrototypeOf(obj)即可返回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两个方法。当使用new User()创建新对象u时,u的原型会被自动指向User.prototype,即u.__proto__ === User.prototype返回true。
User函数、User.prototype与实例对象u之间的关系可图示如下:

图中箭头代表继承关系。访问u的属性时,引擎首先查找u自身属性;若未找到,则沿原型链向上查找。
例如调用u.checkPassword()时,由于该方法定义在原型上,u自身不存在该属性,引擎便会在u的原型(即User.prototype)中查找。查找路径为u -> u.__proto__(User.prototype)。
前文提到,getPrototypeOf是ES5中获取对象原型的标准方法,因此:
Object.getPrototypeOf(u) === User.prototype; // true
部分环境同时提供了非标准的__proto__属性作为替代方案。在环境不支持ES5标准getPrototypeOf时,可暂时使用__proto__。可通过以下代码检测环境是否支持__proto__:
u.__ proto __ === User.prototype; // true
在JavaScript中,类的概念由构造函数(User)和用于实例间共享方法的原型对象(User.prototype)共同构成。构造函数负责初始化每个对象特有的属性(如name和password),而原型对象负责存储所有实例共享的方法(如checkPassword和toString)。关系如图所示:

获取对象优先使用Object.getPrototypeOf,而非__proto__
二、获取对象优先使用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__
三、绝不要修改__proto__
与Object.getPrototypeOf仅用于读取不同,__proto__作为属性还可以被赋值,从而修改对象的原型继承链。
然而,严禁修改__proto__,原因如下:
- 首先是便携性问题。由于并非所有JavaScript执行环境都支持__proto__,使用它会导致代码在不支持该属性的环境中无法运行。
- 其次是性能问题。现代JavaScript引擎会对对象属性的存取进行大量优化,因为这些操作极为频繁。修改__proto__会改变对象的整个继承结构,导致许多优化失效。
- 最重要的是可靠性问题。改变__proto__可能完全改变对象的原型继承链。若程序其他部分依赖原有继承链,将引发不可预料的错误。通常应保持原型继承链的稳定。
若需为新创建的对象指定原型,应使用ES5提供的Object.create方法。对于未实现ES5的环境,可提供一个不依赖__proto__的Object.create实现。
解决__proto__兼容问题,让构造函数不再依赖new关键字
四、解决__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被赋值给全局对象。
若将构造函数声明为严格模式(strict mode):
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错误。因为在严格模式下,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环境中,需先实现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__的深入学习,希望有所帮助。