深入理解JavaScript原型链:prototype、getPrototypeOf与__proto__的区别与最佳实践

作者:函数茶馆

时间:2026-10-29

浏览:599

本文深入解析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之间的关系可图示如下:

深入理解prototype, getPrototypeOf和__proto__

图中箭头代表继承关系。访问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)。关系如图所示:

深入理解prototype, getPrototypeOf和__proto__

获取对象优先使用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__的深入学习,希望有所帮助。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章