原型与原型链

前端开发
2020年02月17日
501

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}