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 仍然基于原型。这些新的关键字包括 class
, constructor
,static
,extends
和 super
。
"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
。