浅谈原型

1、[[Prototype]]
JavaScript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。
思考下面代码:

var myobject = {
      a: 2
};
myObject.a; //2

当试图引用对象的属性时会触发[[Get]]操作,比如myObject.a。对于默认的[[Get]]操作来说,第一步就是检查对象本身是否有这个属性,如果有的话就使用它。但是如果a不在myObject中,就需要使用对象的[[Prototype]]链了。:

var anotherObject = {
      a:2
};
//创建一个关联到anotherObject的对象
var myObject = Object.create(anotherObject);
my.Object.a; //2

现在myObject对象的[[Prototype]]关联到了anotherObject。显然myObjext.a并不存在,但属性访问依然成功地找到了值2。但如果anotheObject中也找不到a并且[[Prototype]]链不为空的话。就会继续查找下去。这个过程会持续到找到匹配的属性名或者查找完整条[[Prototype]]链。如果是后者的话,[[Get]]操作的返回值是undefined。

引申到一个问题:哪里是[[Prototype]]的尽头呢?所有普通的[[Prototype]]链最终都会指向内置的Object.prototype。

2、类
为什么一个对象需要关联到另一个对象?
首先来了解[[Prototype]]"不是"什么。

JavaScript和面向类的语言不同,它并没有类来作为对象的抽象模型,JavaScript只有对象。但是JavaScript却有模仿类的行为,模仿类的行为利用了函数的一种特殊特性:所有的函数默认都会拥有一个名为prototype的共有并且不可枚举的属性,它会指向另一个对象。

function Foo(){}
Foo.prototype; // {}

这个对象通常被称为Foo的原型,因为是通过名为Foo.prototype的属性引用来访问它。

这个对象到底是什么?最直接的解释就是,这个对象是在调用new Foo()时创建的,最后会被关联到这个Foo.prototype对象上。

function Foo(){}
var a = new Foo();
Object.getPrototypeOf(a) === Foo.prototype; //true

调用new Foo()时会创建a,其中一步就是给a一个内部的[[Prototype]]链接,关联到Foo.prototype指向的那个对象。

在面向类的语言中,类可以被复制多次,就像用模具制作东西一样。之所以会这样是因为实例化一个类就意味着”把类的行为复制到物理对象中“,对于每一个新实例来说都会重复这个过程。

但是在JavaScript中,并没有类似的复制机制。不能创建一个类的多个实例,只能创建多个对象,它们[[Prototype]]关联的是同一个对象。但是在默认情况下并不会进行复制,因此这些对象之间并不会完全失去联系,它们是互相关联的。

new Foo()会生成一个新对象(a),这个新对象的内部链接[[Prototype]]关联的是Foo.prototype对象。这种机制通常被称为原型继承,它常常被视为动态语言版本的类继承。

继承意味着复制操作,JavaScript(默认)并不会复制对象属性。相反,JavaScript会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。

委托这个术语可以更加准确地描述JavaScript中对象的关联机制。

Foo.prototype还有另一个绝招:

function Foo(){}
Foo.prototype.constructor === Foo;//true
var a = new Foo();
a.constructor === Foo;//true

Foo.prototype默认有一个公有并且不可枚举的属性.condtructor,这个属性引用的是对象关联的函数。此外通过构造函数调用new Foo()创建的对象也有一个.constructor属性,指向”创建这个对象的函数“

看起来a.constructor === Foo为真意味着a的确有一个指向Foo.prototype.constructor默认指向Foo。其实是错的。实际上.constructor引用同样被委托给了Foo.prototype,而Foo.prototype.condtructor默认指向Foo。

举例来说,Foo.prototpe的.constructor属性只是Foo函数在声明时的默认属性,如果创建一个新对象并替换了函数默认的.prototype对象引用,那么新对象并不会自动获得.constructor属性。
思考下列代码:

function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!

a1并没有.constructor属性,所以它会委托[[Prototype]]链上的Foo.prototype。但是这个对象也没有.constructor属性,所以会继续委托,这次委托给了链的顶端Object.prototype。这个对象有.constructor属性,指向内置的Object()函数。

3、继承
思考下列代码:

function Foo(name){
    this.name = name;
}
Foo.prototype.myName = function (){
    return this.name;
};
function Bar(name, label){
    Foo.call(this, name);
    this.label = label;
}
//我们创建了一个新的 Bar.prototype 对象并关联到 Foo.prototype
Bar.prototype = Object.create(Foo.prototype);
// 注意! 现在没有 Bar.prototype.constructor 了
// 如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype.myLabel = function (){
    return this.label;
};
var a = new Bar("a", "obj a");
a.myName(); // "a"
a.myLabel(); // "obj a"

这段代码的核心语句Bar.prototype = Object.create( Foo.prototype )。调用Objec.create()会凭空创建一个新对象并把新对象内部的[[Prototype]]关联到你指定的对象。

下面有两种常见的错误做法

// 和你想要的机制不一样!
Bar.prototype = Foo.prototype;
// 基本上满足你的需求, 但是可能会产生一些副作用 :(
Bar.prototype = new Foo();

第一种并不会创建一个关联到Bar.prototype的新对象,它只是让Bar.prototype直接引用Foo.prototype对象。因此当执行类似Bar.prototype.myLabel = ...的赋值语句时会直接修改Foo.prototype对象本身。
第二种的确会创建一个关联到Bar.prototype的新对象。但是它使用了Foo()的”构造函数调用“,如果foo有一些副作用的话,就会影响Bar()的后代。

因此,要创建一个合适的关联对象,我们必须使用Object.create()而不是使用具有副作用的Foo()。这样做唯一的缺点就是需要创建一个新对象然后把旧对象抛弃掉,不能直接修改已有的默认对象。

在ES6前,只能通过设置.proto属性来修改对象的[[Prototype]]关联,但是这个方法并不是标准并且无法兼容所有浏览器,ES6添加了辅助函数Object.setPrototypeOf(),可以标准并且可靠的方法来修改关联。

来比对一下:

// ES6 之前需要抛弃默认的 Bar.prototype
Bar.ptototype = Object.create( Foo.prototype );
// ES6 开始可以直接修改现有的 Bar.prototype
Object.setPrototypeOf( Bar.prototype, Foo.prototype );

忽略Object.create()方法带来的轻微性能损失,它实际上畀ES6及其之后的方法更短而且可读性更高。

假设有对象a,那么如何寻找对象a委托的对象呢?在传统的面向类环境中,检查一个实例的继承祖先通常被称为内省。

  1. 站在类的角度来判断:a instanceof Foo;//true
    instanceof回答的问题是:在a的整条[[Prototype]]链中是否有指向Foo.prototype的对象。但这个方法只能处理对象(a)和函数(Foo.prototype)之间的关系。
  2. Foo.prototype.isPrototypeOf(a); //true
    isPrototypeOf(..) 回答的问题是: 在 a 的整
    条 [[Prototype]] 链中是否出现过 Foo.prototype ?

我们也可以直接获取一个对象的[[Prototype]]链。在ES5中,标准的方法是:
Objcet.getPrototypeOf(a);
Object.getPrototypeOf( a ) === Foo.prototype; // true
绝大多数浏览器也支持一种非标准的方法来访问内部[[Prototype]]属性:
a.__proto__ === Foo.prototype;//true
这个奇怪的 .proto(在 ES6 之前并不是标准! ) 属性“神奇地” 引用了内部的[[Prototype]] 对象, 如果你想直接查找(甚至可以通过 .proto.ptoto... 来遍历)原型链的话, 这个方法非常有用。

和.constructor一样,.proto实际上并不存在与正在使用的对象中。实际上,它存在与内置的Objcet.prototype中,更像是一个getter/setter。

.proto 的实现大致上是这样的:

Object.defineProperty( Object.prototype,"__proto__", {
       get: function() {
              return Object.getPrototypeOf( this );
       },
       set: function(o) {
              // ES6 中的 setPrototypeOf(..)
              Object.setPrototypeOf( this, o );
              return o;
       }
} );

因此,访问a.proto时,实际上是调用了a.proto()。

4、对象关联
[[Prototype]]机制就是存在于对象中的一个内部链接,它会引用其他对象。通常来说,这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找它的[[Prototype]],以此类推,这一系列对象的链接被称为”原型链“

那[[Prototype]]机制的意义是什么?Object.create()的作用?

var foo = {
       something: function() {
              console.log( "Tell me something good..." );
       }
};
var bar = Object.create( foo );
bar.something(); // Tell me something good...

Object.create()会创建一个新对象(bar)并把它关联到指定的对象(foo)。

假设要调用bar.something(),如果bar中不存在cool()时这条语句可以正常工作的话,那这API设计可能不被维护软件的开发者理解。

可以这样设计:

var foo = {
      something: function() {
            console.log( "Tell me something good..." );
      }
};
var bar = Object.create( foo );
bar.doSomething = function() {
      this.something(); // 内部委托!
};
bar.doSomething(); // "Tell me something good..."

这里调用的bar.doSomething()是实际存在与bar中的,这就更加清晰。

你可能感兴趣的:(浅谈原型)