JS 对象原型链详解

Javascript 是动态语言,本身不提供 class 实现。(在ES2015/ES6中引入了class关键字,但只是语法糖,JavaScript 仍然是基于原型的)。

谈到继承时,Javascript 只有一种结构:对象。每个对象都有一个私有属性(称之为[[Prototype]]),它持有一个连接到另一个称为 prototype 对象(原型对象)的链接。该 prototype 对象又具有一个自己的原型,层层向上直到一个对象的原型为 null 。(Object.getPrototypeOf(Object.prototype) === null; // true) 根据定义,null 没有原型,是这个原型链中的最后一个环节

原型基本概念

Object.prototype

Object.prototype 属性表示 Object 的原型对象,原型对象的作用是为其他对象提供共享的属性对象。

JavaScript 中几乎所有的对象都是 Object 的实例; 所有的对象都继承了Object.prototype 的属性和方法。

使用 构造函数(constructor) 去创建对象,该对象会隐式地引用构造函数的 prototype 属性。构造函数的 prototype 属性可以通过 constructor.prototype 引用,并且那些被添加到构造函数 prototype 的属性,会通过继承,被所有共用这个 prototype 的对象所共享。或者,也可以使用 Object.create 通过显式指定原型来创建新对象。

Object.prototype.constructor

返回创建实例对象的构造函数的引用。

var o = {};
o.constructor === Object; // true

function Tree(name) {
   this.name = name
}
var theTree = new Tree("Redwood")
theTree.constructor === Tree // true

prototype 被覆盖,那么也会影响到 constructor

function Mammal() {
  this.isMammal = 'yes'
}

function MammalSpecies(sMammalSpecies) {
  this.species = sMammalSpecies
}

MammalSpecies.prototype = new Mammal()
MammalSpecies.prototype.constructor === Mammal // true

但是 constructor 是可以被修改的,只有 true,1,和 "test" 不受影响,因为创建他们的是只读的原生构造函数。因此依赖一个对象的 constructor 属性并不总是安全的。

function Type() { }
var o = {}
o.constructor = Type
o.constructor // function Type() {}
o instanceof Type // false
o.toString() // [object Object]

Object.prototype._proto_

Object.prototype 的 __proto__ 属性是一个访问器属性(一个 getter 函数和一个 setter 函数),暴露了通过它访问的对象的内部 [[Prototype]] (一个对象或 null)。

警告:在现代浏览器中,为了操作属性的便利性,可以改变一个对象的 [[Prototype]] 属性,但是这种行为在每个 Javascript 引擎和浏览器中都是一个非常慢且影响性能的操作。不光是 obj.__proto__ = … 语句上,它还会影响所有继承自该 [[Prototype]] 的对象。建议创建一个新的且继承 [[Prototype]] 的对象,推荐使用 Object.create()

警告:当 Object.prototype.__proto__ 已被大多数浏览器厂商支持的今天,其存在和确切行为仅在 ECMAScript 2015 规范中被标准化为传统功能,以确保浏览器的兼容性,但是它已被不推荐使用。为了确保浏览器的兼容性,建议只使用 Object.getPrototypeOf()/Object.setPrototypeOf()。尽管如此,设置 [[Prototype]] 仍然是一个缓慢的操作,为了性能我们应该尽量避免使用。

var one = {x: 1}
one.__proto__ === Object.prototype // true
one.toString === one.__proto__.toString // true

.__proto__ 属性是 Object.prototype 一个简单的访问器属性,任何一个 _proto 的存取属性都继承于 Object.prototype,但是如果访问属性不是源自 Object.prototype 就不拥有 `.proto属性,譬如一个元素设置了其他的.proto_属性,将会覆盖原有的Object.prototype`。

简而言之, prototype 是用于类型的,而 __proto__ 是用于实例的(instances),两者功能一致。

Object.getPrototypeOf()

Object.getPrototypeOf(obj) 方法返回指定对象的原型(内部 [[Prototype]] 属性的值)。

var proto = {}
var obj = Object.create(proto)
Object.getPrototypeOf(obj) === proto // true

Object.setPrototypeOf()

Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null

Object.setPrototypeOf() 是ECMAScript 6最新草案中的方法,相对于 Object.prototype.__proto__ ,它被认为是修改对象原型更合适的方法。

Object.setPrototypeOf(obj, prototype)

继承与原型链

继承属性

创建函数时,Javascript 会为这个函数自动添加 prototype 属性,默认值是空对象。一旦你把这个函数作为 构造函数(constructor) 去创建一个对象,那么 Javascript 就会帮你创建该构造函数的实例,实例继承构造函数 prototype 的所有属性和方法(实例通过设置自己的 __proto__ 指向承构造函数的 prototype 来实现这种继承)。

我们已经知道 Javascript 对象都有一个指向原型对象的链接。当试图访问一个对象的属性时,它不仅会在该对象上查找,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或达到原型链的末尾。

让我们假设我们有一个对象 o, 其有自己的属性 a 和 b:
{a: 1, b: 2}
o 的原型 o.__proto__有属性 b 和 c:
{b: 3, c: 4}
最后, o.__proto__.__proto__ 是 null.
这就是原型链的末尾,即 null,
根据定义,null 没有__proto__.
综上,整个原型链如下: 
{a:1, b:2} ---> {b:3, c:4} ---> null

继承方法

任何函数都可以添加到对象上作为对象的属性。函数的继承与其他的属性继承没有差别,包括上面的“属性覆盖”。

当继承的函数被调用时,this 指向的是当前继承的对象,而不是继承的函数所在的原型对象。

var o = {
  a: 2,
  m: function(){
    return this.a + 1;
  }
};

console.log(o.m()); // 3
// 当调用 o.m 时,'this'指向了o.

var p = Object.create(o);
// p是一个对象, p.__proto__是o.

p.a = 4; // 创建 p 的自身属性a.
console.log(p.m()); // 5
// 调用 p.m 时, 'this'指向 p. 
// 又因为 p 继承 o 的 m 函数
// 此时的'this.a' 即 p.a,即 p 的自身属性 'a'

使用不同的方法来创建对象和生成原型链

使用普通语法创建对象

var o = {a: 1};
// o这个对象继承了Object.prototype上面的所有属性
// 所以可以这样使用 o.hasOwnProperty('a').
// hasOwnProperty 是Object.prototype的自身属性。
// Object.prototype的原型为null。
// 原型链如下:
// o ---> Object.prototype ---> null

var a = ["yo", "whadup", "?"];
// 数组都继承于Array.prototype 
// (indexOf, forEach等方法都是从它继承而来).
// 原型链如下:
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){
  return 2;
}
// 函数都继承于Function.prototype
// (call, bind等方法都是从它继承而来):
// f ---> Function.prototype ---> Object.prototype ---> null

使用构造器

在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符 来作用这个函数时,它就可以被称为构造方法(构造函数)。

function Graph() {
  this.vertices = []
  this.edges = []
}

Graph.prototype = {
  addVertex: function(v) {
    this.vertices.push(v)
  }
}

var g = new Graph()
// g是生成的对象,他的自身属性有'vertices'和'edges'.
// 在g被实例化时,g.__proto__指向了Graph.prototype.

使用 Object.create

ECMAScript 5 中引入了一个新方法:Object.create(),调用这个方法创建新对象,新对象的原型就是调用 create 方法时传入的第一个参数:

var a = {a: 1}
// a ---> Object.prototype ---> null

var b = Object.create(a)
// b ---> a ---> Object.prototype ---> null

var c = Object.create(b)
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null)
// d --> null
console.log(d.hasOwnProperty); // undefined, 因为d没有继承Object.prototype

使用 class 关键字

ECMAScript 6 引入了一套新的关键字用来实现 class。使用基于类语言的开发人员会对这些结构感到熟悉,但它们是不同的。JavaScript 仍然基于原型。这些新的关键字包括 classconstructorstaticextendssuper

"use strict"

class Polygon {
  constructor(height, width) {
    this.height = height
    this.width = width
  }
}

class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength)
  }
  get area() {
    return this.height * this.width
  }
  set sideLength(newLength) {
    this.height = newLength
    this.width = newLength
  }
}

var square = new Square(2)

遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来,要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从 Object.prototype 继承的 hasOwnProperty 方法。

console.log(g.hasOwnProperty('vertices')) // true
console.log(g.hasOwnProperty('addVertex')) // false
console.log(g.__proto__.hasOwnProperty('addVertex')) // true

hasOwnProperty 是 Javascript 中唯一处理属性的东西,并且不支持原型链。

一个错误的实践是,扩展 Javascript 的内置原型,扩展内置原型的唯一理由是为了支持 Javascript 引擎的新特性,如 Array.forEach

小结

现在我们已经知道大致了解到了原型链的机制了,当你执行:

var o = new Foo()

JavaScript 实际上执行的是:

var o = new Object()
o.__proto__ = Foo.prototype
Foo.call(o)

然后当你执行:

o.someProp

它检查 o 是否具有 someProp属性。如果没有,它会查找 Object.getPrototypeOf(o).someProp,如果仍旧没有,它会继续查找 Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp

results matching ""

    No results matching ""