首页 > 代码库 > 《你不知道的JavaScript》 原型

《你不知道的JavaScript》 原型

1 [[Prototype]]

[[Prototype]]是对其他对象的引用,几乎所有对象在创建时[[Prototype]]属性会被赋予非空值。

var myObject = {
      a:2
}

myObject.a;  //  2  

 

引用对象属性时会触发[[Get]]操作,它会检查对象本身是否有这个属性,如果有就使用它,但a不在myObject,需要使用对象的[[Prototype]]链。

使用for in遍历对象时原理和查找[[Prototype]]链类似,任何可以通过原型链访问到的属性都会被枚举。使用in操作符来检查属性在对象是否存在也会查找对象的整条原型链。

var anotherObject = {
      a:2
}

var myObject = Object.create( anotherObject  );

for(var k in myObject){
    console.log( k );
}

 

 

1.1 Object.prototype

所有普通的[[Prototype]]链最终都指向内置的Object.prototype。由于所有的“普通”(内置,不少特定主机的扩展)对象都源于这个Object.prototype,所以它包含JavaScript许多通用功能。

1.2 属性设置和屏蔽

myObject.foo = "bar";

如果foo不是直接存在于myObject,[[Prototype]]链会被遍历,如果原型链找不到foo,foo会被添加到myObject。

如果foo不直接存在于myObject而是存在于原型链上层时:

  1. 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性并且没有被标记为只读(writeable:false),直接在myObject添加foo属性,它是屏蔽属性
  2. 如果在[[Prototype]]链上存在foo,但它被标记为只读,将无法修改已有属性或在myObject上创建屏蔽属性。在严格模式中会抛出错误,否则忽略此语句。
  3. 如果在[[Prototype]]链上存在foo且它是一个setter,那一定会调用这个setter。foo不会被添加到myObject,也不会重新定义foo。

如果细微第二种和第三种情况也会屏蔽foo,不能使用=赋值,而是使用Object.defineProperty()向myObject添加foo。

var anotherObject = {
      a:2
}

var myObject = Object.create( anotherObject  );

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

anotherObject.hasOwnProperty("a");  // true
myObject.hasOwnProperty("a");  //  false

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

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

myObject.hasOwnProperty("a");  //  true

 

++操作相当于myObject.a = myObject.a + 1。++操作先会通过[[Prototype]]查找属性a并从anotherObject.a获取当前属性值2,然后给它加1,接着用[[Put]]将3赋值给myObejct的新建屏蔽属性a。

如果想让anotherObjecg的值增加,唯一的办法是anotherObject.a++。

2 "类"

2.1 "类"函数

function Foo() {}

Foo.prototype;  //  {}

 

这个对象在调用new Foo()时创建,最后被关联到“Foo点prototype”。

 

function foo() {}

var a = new foo();

foo.prototype == Object.getPrototypeOf(a)  //  true

 

调用new foo()创建a,其中一步是将a的[[Prototype]]连接到foo.prototype所指向的对象

 

 

2.2 “构造函数”

function Foo() {}

Foo.prototype.constructor === Foo;  // true

var a = new Foo();

a.constructor === Foo;  //  true

 

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

 

当在普通的函数调用前面加上new后,就会把这个函数调用变成一个“构造函数调用”。new会劫持普通函数并用构造对象的形式聊调用它。

function NothingSpecial() {
    console.log("Don‘t mind me");
}

var a = new NothingSpecial();

a; // {}

 

NothingSpecial只是普通函数,但使用new调用时,它会构造一个对象并赋值给 a

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

2.3 技术 

a.constructor === Foo为真,不代表a有指向Foo的.constructor属性。.constructor引用同样被委托给Foo.prototype,foo.prototype.constructor默认指向foo。a.constructor只是通过默认[[prototype]]委托指向Foo。

Foo.prototype的.constructor属性只是Foo函数在声明时的默认属性。如果你创建一个新对象并替换了函数默认的.prototype对象引用,新对象不会自动获得.constructor属性。

function Foo() { /*...*/ }

Foo.prototype = { /*...*/ }

var a1 = new Foo();

a1.constructor === Foo;  //  false
a1.constructor === true  //  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"

 

调用Object.create()会凭空创建一个“新”对象并把新对象内部的[[Prototype]]关联到你指定的对象(Foo.prototype)。

Bar.prototype = Object.create(Foo.prototype) 的意思是“创建一个新的Bar.prototype对象并把它关联到Foo.prototype”。

 

//错误做法

//  和想要的机制不一样
Bar.prototype = Foo.prototype;

//可能产生副作用
Bar.prototype = new Foo();

 

Bar.prototype = Foo.prototype 只是让Bar.prototype直接引用Foo.prototype对象。因此执行如Bar.prototype.mylabel = ...的赋值语句会直接修改Foo.prototype对象本身。

Bar.prototype = new Foo() 会创建关联到Bar.prototype的新对象,但它使用的是构造函数调用,如果函数Foo有一些副作用(如写日志、修改状态、注册到其他对象、给this添加数据属性等),就会影响到Bar()的“后代”。

 

检查“类关系

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

function Foo( ) {
    //  ...
}

Foo.prototype.blash = ...;

var a = new Foo();

 

第一种方法: a instanceof Foo ;  //  true

instanceof 判断a的整条[[Prototype]]链是否有指向 Foo.prototype的对象。但这个方法只能处理对象和函数的关系。

第二种方法: Foo.prototype.isPropertyOf( a );  //  true 

4 对象关联

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

4.1创建关联

var foo = {
    something: function() {
        console.log("Tell me someting goos.");
    }
}

var bar = Object.create( foo );

bar.something( );   //  Tell me someting goos.

 

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

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

 

Object.create的polyfill代码

Object.create是ES5新增的函数,在ES5前的环境要支持这个功能要使用这段代码,它实现了Obejct.create的部分功能

if(! Object.create) {
    Object.create = function(o) {
        function F() {};
        F.prototype = o;
        return new F();
    }
}

 

这段代码使用了一个一次性函数F,通过改写它的.prototype使其指向想要关联的对象,然后再使用new F()构造一个新对象进行关联。Object.create的第二个参数指定需要添加到新对象的属性名和这些属性的属性描述符,但ES5之前的版本无法模拟属性描述符。

 

4.2 关联关系是必备的

假设要调用myObject.cool(),如果myObject中不存在cool()也能正常工作,这API会很“神奇”。

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

var myObject = Object.create( anotherObject );

myObject.cool( );   //  cool!

 

 

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

var myObject = Object.create( anotherObject );

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

myObject.doCool( );  

 

 这里强调的myObject.doCool()是实际存在于myObject的,让API设计更加清晰。从内部来说,这种实现遵循的是委托设计模式,通过[[Prototype]]委托到anotherObject.cool()。

 

内部委托比起直接委托可以让API接口设计更加清晰。

 

《你不知道的JavaScript》 原型