分享

理解JavaScript的函数(二):原型对象

 印度阿三17 2019-02-17

0:前提知识

  在函数上下文中,this的指向有很多需要注意的地方:

  • 如果函数是作为一个实例对象的方法被调用,this操作符指向该实例。
  • 如果函数是作为构造函数(使用new操作符)被调用,this操作符指向正在被构造的对象(也就是实例)。

也就是说,如果函数是作为构造函数的,构造函数中的this指向实例对象,构造函数中定义的方法(不要使用箭头函数)中使用到的this也指向实例对象。

 1 function Person (name) {
 2     this.name = name; // 如果函数是作为构造函数被调用,this操作符指向该实例。
 3     this.sayHello = function() {
 4         // 如果函数是作为一个实例对象的方法被调用,this操作符指向该实例。
 5         console.log(this.name); // wangting
 6         // 如果函数是作为一个实例对象的方法被调用,this操作符指向该实例。
 7         console.log(this === person1) // true
 8     }
 9     this.sayThis = function() {
10         // 打印出此时的this,这里需要特别注意的是,实例对象的原型对象上的属性不在this对象上
11         console.log(this); // Person {name: 'wangting',sayHello: [Function],sayThis: [Function],age: '25' }
12     }
13 }
14 Person.prototype.sayName = function() {
15     // 如果函数是作为一个实例对象的方法被调用,this操作符指向该实例。
16     console.log(this.name); // wangting
17     // 如果函数是作为一个实例对象的方法被调用,this操作符指向该实例。
18     console.log(this === person1); // true
19 };
20 
21 let person1 = new Person('wangting');
22 // 在实例对象上添加属性(相当与在this对象上添加属性)
23 person1.age = '25';
24 person1.sayHello();
25 person1.sayName();
26 person1.sayThis();

 

一言以蔽之,

  • 构造函数和原型对象中使用到的this都指向实例对象。
  • 在构造函数和原型对象中给this添加属性,就是给实例对象添加属性。
  • 原型对象上添加属性不在this对象上。
  • 在构造函数和原型对象中获取this的属性,就是在获取实例对象的属性。
  • 我们称实例对象上的属性其实包含在实例对象本身添加的属性(如person1.age = '25';)和在构造函数中给this添加的属性(如this.name = name;)

一、原型对象的定义

每个函数都有一个prototype属性(原型属性),它是一个指针,指向一个对象,叫作原型对象。

这个对象保存着由函数创建的所有实例(使用new操作符)共享的属性和方法。

有下面几点需要说明:

  1. 每个函数都有一个prototype属性,指向原型对象。
  2. 原型对象默认会有一个constructor属性,默认指向prototype属性所在的函数。
  3. 使用new操作符创建的函数实例有一个__proto__属性,指向原型对象。
  4. 代码读取实例对象的属性或者方法时,都会有一个查找对象属性的过程。
  5. 查找对象属性的过程是这样的:
    • 在实例对象中(实例对象本身添加的属性和构造函数中给this添加的属性)查找,如果找到,则返回该属性的值。
    • 如果没有找到,就去实例对象的_proto_属性指向的原型对象上查找,如果找到,则返回该属性的值。
    • 过程结束没有找到属性的话就报error。

看下面的代码:

 1 function Person (name) {
 2     this.name = name;
 3 }
 4 Person.prototype.age = '25';
 5 Person.prototype.name = 'wangying';
 6 Person.prototype.sayName = function() {console.log(this.name)};
 7 
 8 let person1 = new Person('wangting');
 9 person1.job = 'IT Engineer';
 10 
11 console.log(person1.job); // IT Engineer
12 console.log(person1.age); // 25
13 console.log(person1.name); // wangting
14 person1.sayName(); // wangting

job属性的查找过程: person1实例对象本身(找到)

age属性的查找过程: person1实例对象本身(没找到) ==》person1实例对象的prototype属性指向的原型对象(找到)

name属性的查找过程:  person1实例对象本身(找到)

sayName方法的查找过程: person1实例对象本身(没找到) ==》person1实例对象的prototype属性指向的原型对象(找到)

 用图形表示:

可以理解为,在使用构造函数创建好实例对象后,实例对象与构造函数已经没有直接的联系了,但是,实例对象与原型对象之间一直保持类型,并且在搜索过程中也会去原型对象中寻找。

我们来看一下更复杂的例子,在例子中会有属性覆盖的应用。

 1 function Person (name) {
 2     this.name = name; // 给实例对象上添加属性name(这个属性会被实例对象本身添加的name属性覆盖)
 3     this.age = '25'; // 给实例对象上添加属性age
 4     // 给实例对象上添加方法saySex
 5     this.saySex = function() {
 6         // 获取实例对象的_proto_属性指向的原型对象上的属性sex的值(注意,这里的sex属性并不在实例对象即this对象上)
 7         console.log(this.sex); // male
 8     }
 9 }
10 // 给实例对象的原型对象添加age属性,这个属性会被构造函数中添加的age属性覆盖
11 Person.prototype.age = '55';
12 // 给实例对象的原型对象添加sex属性
13 Person.prototype.sex = 'male';
14 Person.prototype.sayName = function() {
15     // 获取实例对象上(实例对象本身添加的)的name属性
16     console.log(this.name); // wangting
17     // 获取实例对象上(构造函数中添加的)的age属性
18     console.log(this.age); // 25
19     // 获取实例对象的_proto_属性指向的原型对象上的属性sex的值(注意,这里的sex属性并不在实例对象即this对象上)
20     console.log(this.sex); // male
21 };
22 
23 let person1 = new Person('othername');
24 // 在实例对象上添加属性(相当与在this对象上添加属性)
25 person1.name = 'wangting';
26 person1.saySex();
27 person1.sayName();
28 // 打印出person1的this的值
29 (function () {console.log(this)}).call(person1) // { name: 'wangting', age: '25', saySex: [Function] }
30 // 打印出person1的原型对象的值
31 console.log(person1.__proto__) // { age: '55', sex: 'male', sayName: [Function] }

 

分析

  • 实例对象本身添加的属性会覆盖通过构造函数添加的属性(如第25行在实例对象本身添加的name属性覆盖了第2行在构造函数中添加的name属性)
  • 构造函数中添加的属性会覆盖原型对象上的属性(如第3行在构造函数中添加的age属性,覆盖了第11行在原型对象上添加的age属性)
  • 实例对象上添加的属性和构造函数中添加的属性都在this对象上(第29行,name属性来自实例对象本身,它覆盖了构造函数中定义的name属性,age属性来自构造函数,saySex方法来自构造函数)
  • 原型对象上添加的属性不在this对象上。
  • 原型对象上的属性只包含原型对象本身中添加的属性,如age属性等于55,sex属性。
  • 虽然this对象中不包含sex属性(第29行打印出的this中没有sex属性),但是通过this.sex同样可以获取sex属性的值,这是实例对象的获取属性的搜索过程决定的。
  • 查看第18行,这里是在原型对象中调用this.age,虽然原型对象中定义了age属性,但是它还是获取到this对象上的值,这也是实例对象的获取属性的搜索过程决定的。

总结:实例对象的搜索过程可以理解为: 实例对象本身定义的属性 =》 构造函数中定义的属性 ==》 原型对象中定义的属性。(实例对象本身定义的属性和构造函数中定义的属性共同组成this对象的属性)

二、原型链

通过上面的例子,我们知道,获取实例对象的属性有一个搜索过程,

先在实例对象中查找,然后去实例对象的__proto__属性对应的原型对象上查找。

如果,原型对象中也有一个__proto__属性,那么,在查找完原型对象没有找到属性的情况下,系统会继续查找原型对象的__proto__属性对应的对象,这就形成了搜索属性的链,这个搜索链是由另外一个链组成的,它叫做原型链。
原型对象中的__proto__属性指向某个对象,外在表现就是该原型对象本身是某个引用类型(构造函数)的实例对象。

看下面的代码:

 1 // Person
 2 function Person (name) {
 3     this.name = name;
 4 }
 5 Person.prototype.sayName = function() {
 6     console.log(this.name);
 7 };
 8 // Student
 9 function Student(school) {
10     this.school = school;
11 }
12 // 继承Person
13 Student.prototype = new Person('wangting');
14 Student.prototype.saySchool = function() {
15     console.log(this.school);
16 }
17 
18 let studenta = new Student('NT');
19 // 实例对象的属性
20 console.log(studenta.school); // NT
21 // 实例对象的__proto__属性对应的原型对象的属性(继承自Person)
22 console.log(studenta.name); // wangting
23 // 实例对象的__proto__属性对应的原型对象的方法
24 studenta.saySchool(); // NT
25 // 实例对象的__proto__属性对应的原型对象的__proto__属性对应的原型对象(继承自Person)
26 studenta.sayName(); // wangting
27 // 打印this对象
28 (function() {console.log(this)}).call(studenta); // { school: 'NT' }
29 // 打印实例对象的原型对象
30 console.log(studenta.__proto__); // { name: 'wangting', saySchool: [Function] }
31 // 打印实例对象的原型对象的原型对象(继承Person)
32 console.log(studenta.__proto__.__proto__); // { sayName: [Function] }

 

分析:

  • 第13行,实例对象直接赋值给原型对象实现继承
  • school属性的搜索过程: 对象实例本身的属性(没有) ==》 对象实例的构造函Student(有)
  • saySchool方法的搜索过程: 对象实例本身的方法(没有) ==》 对象实例的构造函数Student(没有) ==》 实例对象的原型对象的本身(这里把原型对象理解成新的实例对象)(有)
  • name属性的搜索过程: 对象实例本身的属性(没有) ==》 对象实例的构造函数Student(没有) ==》 实例对象的原型对象的本身(没有) ==》 实例对象的原型对象的构造函数Person(有)
  • sayName方法的搜索过程: 对象实例本身的属性(没有) ==》 对象实例的构造函数Student(没有) ==》 实例对象的原型对象的本身(没有) ==》 实例对象的原型对象的构造函数Person(没有) ==》 实例对象的原型对象的原型对象
  • 第30行,打印出的是实例对象的原型对象,可以看到,它有一个name属性,来自Person构造函数(也就是说,name属性不是来自原型对象本身,而是来自原型对象的构造函数)

上面就是使用原型对象实现继承。这也typescript中class的extends的继承方式。

 

参考: 《javascript高级程序设计》

来源:http://www./content-1-116401.html

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多