你不知道的JavaScript之原型

1.JavaScript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值。对象的[[Prototype]]链接可以为空,但是很少见。当我们通过各种语法进行属性查找时都会查找[[Prototype]]链,直到找到属性或查找完整条原型链。

2.所有普通的[[Prototype]]链最终都会指向内置的Object.prototype。由于所有的“普通”(内置,不是特定主机的扩展)对象都“源于”(或者说把[[Prototype]]链的顶端设置为)这个Object.prototype对象,所以它包含JavaScript中许多通用的功能。比如说.toString()和.valueOf()等。

3.属性屏蔽

myObject.foo = 'bar'; 

如果myObject对象中包含名为foo的普通数据访问属性,这条语句只会修改已有的属性值。

如果foo不是直接存在于myObject中,[[Prototype]]链就会被遍历,类似[[Get]]操作。如果原型链上找不到foo,foo就会被直接添加到myObject上。

如果foo不是直接存在于myObject中而是存在于原型链上层时会出现三种情况:

①如果在[[Prototype]]链上层存在名为foo的普通数据访问属性并且没有被标记为只读(writable: false),那就会直接在myObject中添加一个名为foo的新属性,它是屏蔽属性。

②如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable: false),那么无法修改已有属性或者在myObject上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误(TypeError)。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。

③如果在[[Prototype]]链上层存在foo并且它是一个setter,那就一定会调用这个setter。foo不会被添加到(或者说屏蔽于)myObject,也不会重新定义foo这个setter。

如果你希望在第二种和第三种情况下也屏蔽foo,那就不能使用=操作符来赋值,而是使用Object.defineProperty()来向myObject添加foo。

如果属性名foo既出现在myObject中也出现在myObject的[[Prototype]]链上层,那么就会发生屏蔽。myObject中包含的foo属性会屏蔽原型链上层的所有foo属性,因为myObject.foo总是会选择原型链中最底层的foo属性。

如果需要对屏蔽方法进行委托的话就不得不使用丑陋的显式伪多态。通常来说,使用屏蔽得不偿失,所以应当尽量避免使用。

4.小心隐式屏蔽

var anotherObject = {
      a: 2
};

var myObject = Object.create(anotherObject);

myObject.a++;// 隐式屏蔽

anotherObject.a;// 2
myObject.a;// 3

思考上述代码。尽管myObject.a++看起来应该(通过委托)查找并增加anotherObject.a属性,但是别忘了++操作符相当于myObject.a = myObject.a + 1。因此++操作首先会通过[[Prototype]]查找属性a并从anotherObject.a获取当前属性值2,然后给这个值加1,接着用[[Put]]将值3赋给myObject中新建的屏蔽属性a。

修改委托属性时一定要小心。如果想让anotherObject.a的值增加,唯一的办法是anotherObject.a++。

5.“类”函数

JavaScript模仿类的行为利用了函数的一种特殊特性:所有的函数默认都会拥有一个名为prototype的公有并且不可枚举的属性,它会指向另一个对象:

function Foo() {
      //...
}

Foo.prototype;// {}

这个对象通常被称为Foo的原型,因为我们通过名为Foo.prototype的属性引用来访问它。调用new Foo()时会创建对象a,其中的一步就是给a一个内部的[[Prototype]]链接,关联到Foo.prototype指向的那个对象。

实际上,绝大多数JavaScript开发者不知道的秘密是,new Foo()这个函数调用实际上并没有直接创建关联,这个关联只是一个意外的副作用。new Foo()只是间接完成了我们的目标:一个关联到其他对象的新对象。

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

6.构造函数

function Foo() {
      //...
}

var a = new Foo();

Foo.prototype默认有一个公有并且不可枚举的属性.constructor,这个属性引用的是对象关联的函数(本例中是Foo)。

上一段代码很容易让人认为Foo是一个构造函数,实际上,Foo和你程序中的其他函数没有任何区别。函数本身并不是构造函数,然而,当你在普通的函数调用前面加上new关键字之后,就会把这个函数调用变成一个“构造函数调用”。实际上,new会劫持所有普通函数并用构造对象的形式来调用它。

function NothingSpecial() {
     console.log('Dont mind me');
}

var a =  new NothingSpecial(); // Dont mind me

a;// {}

NothingSpecial只是一个普通的函数,但是使用new调用时,它就会构造一个对象并赋值给a。这个调用是一个构造函数调用,但是NothingSpecial本身并不是一个构造函数。

在JavaScript中对于”构造函数“最准确的解释是,所有带new的函数调用。

7.Constructor

对象的.constructor会默认指向一个函数,这个函数可以通过对象的.prototype引用。”constructor并不表示被构造“。

.constructor并不是一个不可变属性。它是不可枚举的,但是它的值是可写的。此外,你可以给任意[[Prototype]]链中的任意对象添加一个名为constructor的属性或者对其进行修改,你可以任意对其赋值。

结论:一些随意的对象属性引用,比如a1.constructor,实际上是不被信任的,它们不一定会指向默认的函数引用。

.constructor是一个非常不可靠并且不安全的引用。通常来说要尽量避免使用这些引用。

8.(原型)继承

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
= Object.create(Foo.prototype);// 将Bar.prototype对象关联到Foo.prototype
Bar.prototype.constructor = Bar;// 手动修复.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)。调用Object.create(..)会凭空创建一个”新“对象并把新对象内部的[[Prototype]]关联到你指定的对象。

声明funciton Bar() {...}时,和其他函数一样,Bar会有一个.prototype关联到默认的对象,但是这个对象并不是我们想要的Foo.prototype。因此我们创建了一个新对象并把它关联到我们希望的对象上,直接把原始的关联对象抛弃掉。

这样做唯一的缺点就是需要创建一个新对象然后把旧对象抛弃掉,不能直接修改已有的默认对象。ES6添加了辅助函数Object.setPrototypeOf(..),可以用标准并且可靠的方法来修改关联。

// ES6之前需要抛弃默认的Bar.prototype
Bar.prototype = Object.create(Foo.prototype);

// ES6开始可以直接修改现有的Bar.prototype
Object.setPrototypeOf(Bar.prototype, Foo.prototype);

9.检查“类”关系

在传统的面向类环境中,检查一个实例(JavaScript中的对象)的继承祖先(JavaScript中的委托关联)通常被称为内省(或者反射)。

function Foo() {
      //...
}

Foo.prototype.blah = ...;

var a = new Foo();

我们如何通过内省找出a的“祖先”(委托关联)呢?

①第一种方法是站在“类”的角度来判断:

a instanceof Foo;// true

instanceof操作符回答的问题是:在a的整条[[Prototype]]链中是否有Foo构造函数的prototype属性指向的对象。

可惜,这个方法只能处理对象(a)和函数(带.prototype引用的Foo)之间的关系。

②第二种判断[[Prototype]]反射的方法,它更加简洁:

Foo.prototype.isPrototypeOf(a);// true

注意,在本例中,我们实际上并不关心(甚至不需要)Foo,我们只需要一个可以用来判断的对象(本例中是Foo.prototype)就行。isPrototypeOf(..)回答的问题是:在a的整条[[Prototype]]链中是否出现过Foo.prototype。这个方法并不需要函数(“类”),它直接使用对象引用来判断它们的关系。

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

.__proto__实际上并不存在于你正在使用的对象中,它存在于内置的Object.prototype上。

.__proto__看起来很像一个属性,但是实际上它更像一个getter/setter,它的实现大致上是这样的:

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

我们只有在一些特殊情况下(前面讨论过)需要设置函数默认.prototype对象的[[Prototype]],让它引用其他对象。这样可以避免使用全新的对象替换默认对象。此外,最好把[[Prototype]]对象关联看作是只读特性,从而增加代码的可读性。

10.对象关联

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

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)。

Object.create(null)会创建一个拥有空(或者说null)[[Prototype]]链接的对象,这个对象无法进行委托。由于这个对象没有原型链,所以instanceof操作符无法进行判断,因此总是会返回false。这些特殊的空[[Prototype]]对象通常被称作“字典”,它们完全不会受到原型链的干扰,因此非常适合用来存储数据。

Object.create(..)的第二个参数指定了需要添加到新对象中的属性名以及这些属性的属性描述符。

var myObject = Object.create(anotherObject, {
      b: {
            enumerable: false,
            writable: true,
            configurable: false,
            value: 3
      }
});

当你给开发者设计软件时,假设要调用myObject.cool(),如果myObject中不存在cool()时这条语句也可以正常工作的话,那你的API设计就会变得很“神奇”,对于未来维护你软件的开发者来说这可能不太好理解。

但是你可以让你的API设计不那么“神奇”,同时仍然能发挥[[Prototype]]关联的威力:

var anotherObject = {
      cool: function() {
            console.log('cool!');
      }
};

var myObject = Object.create(anotherObject);

myObject.doCool = function() {
         this.cool();// 内部委托!
};

myObject.doCool();// 'cool!'

这里我们调用的myObject.doCool()是实际存在于myObject中的,这可以让我们的API设计更加清晰(不那么“神奇”)。换句话说,内部委托比起直接委托可以让API接口设计更加清晰。

posted @ 2016-10-07 14:08  Mr.czar  阅读(214)  评论(0)    收藏  举报