基本概念
先用构造函数创建一个对象
function Person() {}
var person = new Person();
person.name = 'Greg';
console.log(person.name) // Greg
在这个例子中,Person
就是一个构造函数,我们使用 new
创建了一个实例对象 person
。
prototype
首先来说说prototype
属性,不像每个对象都有__proto__
([[Prototype]]
)属性来标识自己所继承的原型,只有函数才有prototype
属性。
当你创建函数时,Javascript
会为这个函数自动添加prototype
属性,值是空对象。而一旦你把这个函数当作构造函数调用(即通过new
关键字调用),那么Javascript
就会帮你创建该构造函数的实例,实例继承构造函数prototype
的所有属性和方法(实例通过设置自己的__proto__
指向其构造函数的prototype
来实现这种继承)。
function Person() {}
Person.prototype.name = 'Greg';
var person1 = new Person();
var person2 = new Person();
console.log(person1.name); // Greg
console.log(person2.name); // Greg
函数的 prototype
属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 person1
和 person2
的原型。
你可以这样理解:每一个JavaScript
对象(null
除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。
那么我们该怎么表示实例与实例原型,也就是 person
和 Person.prototype
之间的关系呢,这时候我们就要讲到__proto__
__
proto__
这是每一个JavaScript
对象(除了null
)都具有的一个属性,叫__proto__
,这个属性会指向该对象的原型。
绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype
中,实际上,它是来自于 Object.prototype
,与其说是一个属性,不如说是一个 getter/setter
,当使用 obj.__proto__
时,可以理解成返回了 Object.getPrototypeOf(obj)
。
function Person() {}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true
既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?
constructor
指向实例倒是没有,因为一个构造函数可以生成多个实例,但是原型指向构造函数倒是有的,这就要讲到第三个属性:constructor
,每个原型都有一个 constructor
属性指向关联的构造函数。
为了验证这一点,我们可以尝试:
function Person() {}
console.log(Person === Person.prototype.constructor); // true
当获取 person.constructor
时,其实 person
中并没有 constructor
属性,当不能读取到constructor
属性时,会从 person
的原型也就是 Person.prototype
中读取,正好原型中有该属性:
var person = new Person();
console.log(person.constructor === Person); // true
console.log(person.hasOwnProperty('constructor')); // false
综上我们已经得出:
function Person() {}
var person = new Person();
console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
深入探究prototype与__
proto__
对象有[[prototype]]
属性,函数对象有prototype
属性,原型对象有constructor
属性。
其中,[[Prototype]]
作为对象的内部属性,是不能被直接访问的。所以为了方便查看一个对象的原型,Firefox和Chrome中提供了__proto__
这个非标准(不是所有浏览器都支持)的访问器(ECMA
引入了标准对象原型访问器Object.getPrototype(object)
)。
先创建一个构造函数
function Person() {}
函数的原型
console.log(Person.__proto__ === Function.prototype); // true
console.log(Person.constructor === Function); // true
console.log(typeof Person); // function
console.log(Person); // f Person() {}
console.log(Person.prototype.__proto__ === Object.prototype); // true;
console.log(Person.__proto__); // f () { [native code] }
console.log(Person.__proto__.__proto__ === Person.prototype.__proto__); // true
console.log(Person.__proto__.__proto__ === Object.prototype); // true
console.log(Person.prototype.constructor); // f Person() {}
console.log('============================================');
console.log(Function.__proto__ === Function.prototype); // true
console.log(Function.constructor === Function); // true
console.log(typeof Function); // function
console.log(Function); // f Function() { [native code] }
console.log(Function.prototype); // f () { [native code] }
console.log(Function.prototype.__proto__ === Object.prototype); // true
console.log(Function.__proto__); // f () { [native code] }
console.log(Function.__proto__.__proto__ === Function.prototype.__proto__); // true
console.log(Function.__proto__.__proto__ === Object.prototype); // true
console.log(Function.prototype.constructor); // f Function() { [native code] }
结果分析:
- 在
JavaScript
中有个Function
对象(类似Object
),这个对象本身是个函数;所有的函数(包括Function
,Object
)的原型(__proto__
)都是Function.prototype
。 -
Function
对象作为一个函数,就会有prototype
属性,该属性将对应function () {}
对象。 -
Function
对象作为一个对象,就有__proto__
属性,该属性对应Function.prototype
,也就是说,Function.__proto__ === Function.prototype
。 - 对于
Function
的原型对象Function.prototype
,该原型对象的__proto__
属性将对应Object {}
。 - 对于原型对象
Person.prototype
的constructor
,将对应Person
函数本身。
Object的原型
console.log(typeof Object); // function
console.log(Object); // ƒ Object() { [native code] }
console.log(Object.prototype); // Object {}
console.log(Object.prototype.__proto__); // null
console.log(Object.prototype.constructor); // ƒ Object() { [native code] }
console.log(Object.__proto__); // ƒ () { [native code] }
console.log(Object.__proto__ === Function.prototype); // true
console.log(Object.__proto__.prototype); // undefined
结果分析:
-
Object
对象本身是一个函数对象。 - 既然是
Object
函数,就肯定会有prototype
属性,所以可以看到Object.prototype
的值就是Object {}
这个原型对象。 - 反过来,当访问
Object.prototype
对象的constructor
这个属性的时候,就得到了Obejct
函数。 - 另外,当通过
Object.prototype.__proto__
获取Object
原型的原型的时候,将会得到null
,也就是说Object {}
原型对象就是原型链的终点了。 -
Object
对象作为一个对象,就有__proto__
属性,该属性对应Function.prototype
,也就是说,Object.__proto__ === Function.prototype
。
实例的原型
var will = new Person();
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(will.__proto__ === Person.prototype); // true
console.log(will.__proto__.__proto__ === Object.prototype); //true
console.log(will.constructor); // ƒ Person() {}
console.log(will.prototype); // undefined
console.log(Person.prototype.constructor === Person); // true
结果分析:
-
will.__proto__ === Person.prototype
,在JavaScript
中,每个函数都有一个prototype
属性,当一个函数被用作构造函数来创建实例时,该函数的prototype
属性值将被作为原型赋值给所有对象实例(也就是设置实例的__proto__
属性),也就是说,所有实例的原型引用的是函数的prototype
属性。 -
prototype
属性是函数对象特有的,如果不是函数对象,将不会有这样一个属性。 - 当通过
Person.prototype.__proto__
语句获取will对象原型的原型时候,将得到Object {}
对象,所有对象的原型都将追溯到Object {}
对象。 - 在
JavaScript的
原型对象中,还包含一个constructor
属性,这个属性对应创建所有指向该原型的实例的构造函数。will
对象本身并没有constructor
这个属性,但是通过原型链查找,找到了will
原型(will.__proto__
)的constructor
属性,并得到了Person
函数。
字面量对象的原型
var o = {};
console.log(o.constructor); // ƒ Object() { [native code] }
console.log(o.__proto__ === Object.prototype); // true
console.log(o.prototype); // undefined
结果分析:
- 字面量对象实质上就是Object构造函数的实例。
对比prototype和__
proto__
对于prototype
和__proto__
这两个属性有的时候可能会弄混,Person.prototype
和Person.__proto__
是完全不同的。
在这里对prototype
和__proto__
进行简单的介绍:
- 对于所有的对象,都有
__proto__
属性,这个属性对应该对象的原型 - 对于函数对象,除了
__proto__
属性之外,还有prototype
属性,当一个函数被用作构造函数来创建实例时,该函数的prototype
属性值将被作为原型赋值给所有对象实例(也就是设置实例的__proto__
属性)
图解实例
附上网上扣的一张图
对上图的总结
- 所有的对象都有
__proto__
属性,该属性对应该对象的原型 - 所有的函数对象都有
prototype
属性,该属性的值会被赋值给该函数创建的对象的__proto__
属性 - 所有的原型对象都有
constructor
属性,该属性对应创建所有指向该原型的实例的构造函数 - 函数对象和原型对象通过
prototype
和constructor
属性进行相互关联
重写原型
可以通过改变函数的prototype
属性或对象的__proto__
属性来重写原型,但会切断现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。
function Person() {}
var will = new Person();
Person.prototype = {
constructor: Person,
name: 'Greg',
sayName: function () {
console.log(this.name);
}
};
will.sayName(); // error
报错的原因就是因为will指向的原型中不包含以该名字命名的属性。
原型对象的问题
原型模式的最大问题是由其共享的本性所导致的。
function Person() {}
Person.prototype = {
constructor: Person,
friends: ['a', 'b']
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push('c');
console.log(person1.friends); // ["a", "b", "c"]
console.log(person2.friends); // ["a", "b", "c"]
console.log(person1.friends === person2.friends); //true
在此,Person.prototype
对象有一个名为friends
的属性,该属性包含一个字符串数组。然后创建两个Person
实例。接着修改person1.friends
引用的数组,向数组中添加一个字符串。由于friends
数组存在于Person.prototype
而非person1
中,所以此修改也会通过person2.friends
(与person1.friends
指向同一个数组)反映出来。
真的是继承吗?
最后是关于继承,前面我们讲到“每一个对象都会从原型‘继承’属性”,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》中的话,就是:
继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。