原型与原型链
JavaScript中的原型
在JavaScript中,函数可以有属性。每个函数都有一个特殊的属性叫作原型(prototype)
,如下面代码显示:
js
function Test () {}
console.log(Test.prototype);
// {
// constructor: ƒ Test()
// __proto__: Object
// }
正如上面所示,函数Test
拥有一个默认的原型prototype
属性。现在我们可以添加一些属性到Test的原型上面:
js
Test.prototype.name = '张三';
Test.prototype.age = 20;
console.log(Test.prototype);
// {
// age: 20
// name: "张三"
// constructor: ƒ Test()
// __proto__: Object
// }
并且,我们可以通过Test的实例化对象来访问到Test的原型属性:
js
var test1 = new Test(),
test2 = new Test();
console.log(test1.name); // '张三'
console.log(test2.age); // age
那么,原型到底是什么呢?
原型prototype
其实是function对象
的一个属性;- 它也是一个对象;
- 它是构造函数构造出来的对象的公共祖先;
- 所有被该构造函数构造出来的对象都可以继承原型上的属性和方法。
constructor
原型上的构造器constructor
指向构造函数本身。
js
function Test () {}
console.log(Test.prototype.constructor === Test); // true
__proto__
实例化对象的__proto__
中存储的是构造函数的原型,它拿的是构造函数的原型的引用。
js
console.log(test.__proto__ === Test.prototype); // true
当然,我们并不推荐直接访问对象的__proto__属性
来获取原型,ES6推荐我们使用Object.getPrototypeOf
来获取原型。
js
console.log(Object.getPrototypeOf(test) === Test.prototype); // true
那么,这个__proto__
是如何来的呢?
众所周知,当构造函数被实例化时,会在该构造函数内部生成隐式的this
,并且在this里面会添加一个__proto__属性
,它的值就是该构造函数的原型,最后把this
返回给实例化对象。
js
function Test () {
var this = {
__proto__: Test.prototype
}
return this;
}
如上所示,在__proto__
里面存储的是Test
的原型prototype
的引用。
我们做一个实验:
js
// exm1:
Car.prototype.name = 'Benz';
function Car () {}
var car = new Car();
console.log(car.name); // 'Benz'
// 在实例化之后,尝试修改原型对象上的name属性
Car.prototype.name = 'BMW';
console.log(car.name); // 'BMW'
console.log(Car.prototype); // {name: "BMW", constructor: ƒ}
// exm2:
Car.prototype.name = 'Benz';
function Car () {}
var car = new Car();
console.log(car.name); // 'Benz'
// 在实例化之后,尝试将原型对象指向一个新的引用。
Car.prototype = {
constructor: Car,
name: 'Mazda'
}
console.log(car.name); // 'Benz'
console.log(Car.prototype); // {name: "Mazda", constructor: ƒ}
在上面两个例子中,我们可以很清晰地看出来,实例化对象的__proto__属性
存储的仅是构造函数的原型prototype
的引用,它们并不是同一个东西。相当于下面的情况:
js
// exm1:
var prototype = {
name: 'Benz'
}
var __proto__ = prototype;
console.log(__proto__.name); // 'Benz'
// 在完成赋值之后,再修改prototype中的name值
prototype.name = 'BMW';
console.log(__proto__.name); // 'BMW'
// exm2:
var prototype = {
name: 'Benz'
}
var __proto__ = prototype;
console.log(__proto__.name); // 'Benz'
// 在完成赋值之后,prototype指向另一个引用
prototype = {
name: 'Mazda'
}
console.log(__proto__.name); // 'Benz'
原型链
在JavaScript中,每个对象都拥有一个原型对象prototype
,对象以其原型为模板、从原型中继承方法和属性。而原型对象prototype
也可能拥有原型,并从中继承方法和属性,一层一层、以此类推,这种关系被称为原型链(prototype chain)。
原型链的特点:
- 沿着
__proto__
一层一层地查找__proto__
; - 终点是
Object.prototype
; - 原型链上的增删改只能是自身的增删改。
当我们访问一个对象(obj)中的某个属性(name)时,浏览器首先查找obj中是否存在这个属性,如果obj中没有,浏览器就会在obj.__proto__
中查找这个属性;如果obj.__proto__
中也没有,则会继续查找obj.__proto__.proto__
……当所有的__proto__
中都无法找到name属性
时,这个属性就是undefined
。
js
Professor.prototype.tSkill = 'Java';
function Professor () {}
var professor = new Professor();
Teacher.prototype = professor;
function Teacher () {
this.mSkill = 'JS/JQ';
}
var teacher = new Teacher();
Student.prototype = teacher;
function Student () {
this.pSkill = 'HTML/CSS';
}
var student = new Student();
console.log(student.pSkill); // 'HTML/CSS'
console.log(student.mSkill); // 'JS/JQ'
console.log(student.tSkill); // 'Javs'
console.log(student.skill); // undefined
注意:并非所有对象都继承于Object.prototype
,继承自Object.prototype
的对象都会拥有一个toString()方法
。看下面的例子:
js
// Object.create(null)生成的对象不会有任何属性,包括__proto__
// 所以它并没继承于Object.prototype
var obj = Object.create(null);
console.log(obj); // { No Properties }
// console.log(obj.toString); // Uncaught TypeError: obj.toString is not a function
// 但是它又是一个实实在在的对象,我们可以给它添加任意属性
obj.name = 'test';
console.log(obj); // {name: "test"}
// 甚至,我们还可以给它指定一个原型
Object.setPrototypeOf(obj, Object.prototype);
console.log(obj.toString); // ƒ toString() { [native code] }
console.log(obj instanceof Object); // true
修改原型上的属性
js
function Teacher () {
this.mSkill = 'JS/JQ';
this.students = {
alibaba: 10086,
tencent: 10010
}
this.number = 100;
}
var teacher = new Teacher();
Student.prototype = teacher;
function Student () {
this.pSkill = 'HTML/CSS';
}
var student = new Student();
console.log(student.students); // {alibaba: 10086, tencent: 10010}
console.log(student.number); // 100
// 修改原始值
student.number ++;
console.log(student.number); // 101
console.log(teacher.number); // 100
// 相当于 student.number = student.number + 1;
// 第一个student.number意味着给student对象增加一个number属性
// 而第二个student.number是从原始链上查找到的值 100
// 所以student会变成 { pSkill: 'HTML/CSS', number: 101 }
// 而teacher则不会变
console.log(student); // { pSkill: 'HTML/CSS', number: 101 }
// 修改引用值
student.students.baidu = 10000;
console.log(student.students); // {alibaba: 10086, tencent: 10010, baidu: 10000}
console.log(teacher.students); // {alibaba: 10086, tencent: 10010, baidu: 10000}