JavaScript原型

原型概述

JavaScript中的对象有一个特殊的[[Prototype]]内置属性,它是对于其他对象的引用。当查找对象的属性和方法时,如果在该对象本身不存在时,就会去[[Prototype]](浏览器的实现为属性proto)所指的对象(也成为对象原型)中查找,正是对象和对象原型的这种单向的关系,将对象与对象有层次地关联起来。由于几乎每个对象都有原型(通过Object.create(null)创建的对象没有原型,一般的原型链的终点Object.prototype没有原型),这些对象通过proto就形成了我们常说的原型链。原型链是JavaScript中非常重要的一环。

原型链中属性查找规则

由于对象与对象原型之间是由单向的引用关联的,所以,如果对象原型的属性或方法发生改变时,对象取得的处于原型对象的该属性或方法也将发生改变。对象读取属性和方法的规则为:如果对象中不存在,则去对象原型中寻找,还不存在,再去对象原型的对象原型中查找,直至原型链的终点Object.prototype(一般都是它)。

上述查找方法适用于一般的获取操作,如:.操作获取、for(var property in obj)(该方法只能获取属性,不能获取方法)。另外,如果只想在对象本身查找,而不去原型链中查找的话,就要使用Object.getOwnPropertyNames(o)返回非继承属性的名字,或者使用Object.hasOwnProperty(propname)检查该属性是否是继承的。

原型链中属性的赋值规则

考虑到对象和对象原型的关系,我们在给对象属性和方法赋值时,不应该去修改对象原型中对象的属性和方法。所以,在给对象属性和方法赋值时,一般会在对象中添加相应的属性和方法,而不是去修改原型对象中的属性和方法,这样就保证了原型的改变不会影响原型对象(原型对象改变倒是可能影响原型),实例如以下代码。为什么说一般呢?因为假如给对象a添加属性name,其原型对象中也有name属性,但是却被标记为只读(writable:false)时,那么就无法给a添加name属性,严格模式下会报错。

1
2
3
4
var a = { name: 'a' };
var b = Object.create(a, { name: { value: 'b', writable: true, enumerable: true, configurable: true }}); //
b.name; //'b' b对象有name属性和__proto__(指向a)
a.name; //'a' a对象有name属性和__proto__(指向Object.prototype)

获取/检查原型链

  • Object.create(proto,descriptions),创建指定原型对象的对象。
  • Object.getPrototypeOf(o),返回对象o的原型,一般都等于o.proto
  • a.isPrototypeOf(b),检查a是不是b的原型链中的一员。
  • o.proto,指向o的原型对象,也可以使用o.proto.proto获取o原型的原型,依次类推。也可以通过该属性修改原型对象。但不同的浏览器实现不同。
  • a instanceof Object,检查a的原型链中是否有Object.prototype。a和Object只是示例,此方法常用于检查前者是否是后者使用new构造出的函数。

  • Object.setPrototypeOf(b,a),ES6中的新方法,将b的原型对象设为a,相当于b.proto = a;

原型继承

上面讲了很多关于原型和原型链的基础知识,熟悉原型链并熟练应用原型链是JavaScript的基本功。特别是使用运行原型链实现“继承”。
对象o的原型对象是o.proto,而函数Foo有一个prototype属性,指向使用new Foo()产生的对象f的f.proto

函数/prototype/new

每个函数都有一个属性prototype(且只有函数有,对象没有),该属性指向一个对象,虽然很多地方都称其为该函数的原型,但是它的真正作用是:在var f = new Foo()时,会产生一个对象f(不是函数),f.proto指向Foo.prototype。在我看来,将Foo.prototype说为Foo函数的原型是不准确的,会造成很多误会,而Foo的原型对象(即Foo.proto)其实为Funciton.prototype原型对象。所以,此处应该多加注意。

那么,接下来,我们看看var f = new Foo()到底做了什么。在我们执行new操作时,会调用Foo.prototype.constructor()函数,而该函数指向Foo,即:Foo.prototype.constructor 等于 Foo,调用Foo函数本身。并在调用完成后返回this对象,所以在Foo函数中指定this.name = 'f'的话,返回的对象就会拥有name属性,且name属于对象本身。另外,new操作会将f.proto指向Foo.prototype对象。

一般情况下,Foo.prototype中由一个构造constructor和一个指向Objcet.prototype的原型对象组成(不考虑多层原型链的情况下)。

原型’继承’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function A(name){
this.name = name;
}
A.prototype.myName = function(){
return this.name;
}

function B(name, label){
this.name = name;
this.label = label
}

B.prototype = Object.create(A.prototype); //此处使用B.prototype = new A();并不好,会调用A()函数,可能产生副作用。

B.prototype.myLabel = function(){
return this.label;
}

var b = new B('b', 'label b');
b.myName(); //'a'
b.myLabel(); //'label b'

b.__proto__ === B.prototype; //true
B.prototype.__proto__ === A.prototype; //true
b.__proto__.__proto__ === A.prototype; //true
A.prototype.__proto__ === Object.prototype; //true
b.__proto__.__proto__.__proto__ === Object.prototype; //true

上面例子中将B的prototype的原型指向A的prototype,这样b就形成了原型链,依次指向B.prototype、A.prototype、Object.prototype。而B的prototype的原型指向A的prototype的方法主要有;

  1. B.prototype = Object.create(A.prototype); B.prototype中只有一个原型属性proto,很干净纯粹;
  2. B.prototype = new A(); 会执行A()函数。另外,如果使用B.prototype = new A(‘a’); 会造成b.proto不等于B.prototype。因为b.proto没有name属性。
  3. Object.setPrototypeOf( B.prototype, A.prototype ); 这个是ES6中提供的原生方法,当然是最好的。

原生函数

前面说过,只有函数才有prototype属性,然而为什么Object、Array、Function等都有prototype呢,是的因为他们也是函数,并且可以使用new操作符来构造相应的对象。我们都知道Array、Function这些静态对象也都是对象,他们的原型链中自然也有Object.prototype。其关系为:Array.prototype.proto = Function.prototype.proto = Object.prototype;

1
2
3
4
5
6
Array.prototype; //[]
Function.prototype; //function () {}
Object.prototype; // Object {} 对象,包含constructor、toString等
Function.prototype.__proto__; // Object.prototype,包含constructor、toString等

Object.__proto__; //function () {}, 即Function.prototype;因为Object也是函数