理解JavaScript的函数(二):原型对象
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操作符)共享的属性和方法。
有下面几点需要说明:
- 每个函数都有一个prototype属性,指向原型对象。
- 原型对象默认会有一个constructor属性,默认指向prototype属性所在的函数。
- 使用new操作符创建的函数实例有一个__proto__属性,指向原型对象。
- 代码读取实例对象的属性或者方法时,都会有一个查找对象属性的过程。
- 查找对象属性的过程是这样的:
- 在实例对象中(实例对象本身添加的属性和构造函数中给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的继承方式。
三、区分构造函数的prototype属性和实例对象的__proto__属性、
问题是由Object.setPrototypeOf这个方法引起的。
文档的定义是这样的:
/** * Sets the prototype of a specified object o to object proto or null. Returns the object o. * @param o The object to change its prototype. * @param proto The value of the new prototype or null. */ setPrototypeOf(o: any, proto: object | null): any;
那问题来了,setPrototypeOf设置的是prototype的值还是__proto__的值?(setPrototypeOf的简单介绍:https://www.cnblogs.com/wangtingnoblog/p/js_prototype_set_get.html)
先来说一下结果:setPrototypeOf设置的是对象的__proto__的值。
1 // 构造函数erson 2 function Person (name) { 3 this.name = name; 4 } 5 // 构造函数prototype属性对应的原型对象 6 Person.prototype.sayName = function() { 7 console.log(this.name); 8 }; 9 // 构造函数的prototype属性指向原型对象 10 console.log(Person.prototype); // Person { sayName: [Function] } 11 // 构造函数也是函数,函数也是对象,所以它也有__proto__属性,指向构造Person这个构造函数的构造函数的原型对象(有点烦) 12 console.log(Person.__proto__); // [Function] 13 // 调用Object.setPrototypeOf方法 14 Object.setPrototypeOf(Person, {new: 'happy'}); 15 // 发现构造函数并没有改变 16 console.log(Person.prototype); // Person { sayName: [Function] } 17 // 发现构造函数的__proto__属性改变了,指向了新定义的对象,而不是指向构造Person这个构造函数的构造函数的原型对象(有点烦) 18 console.log(Person.__proto__); // { new: 'happy' } 19 // 可以通过Person构造函数来取得构造函数对象的原型函数指向的对象的属性 20 console.log(Person.new); // happy
从上面可以看到,setPrototypeOf方法设置__proto__属性的值。
当然,一般情况我们也不会同时在一个对象上用到prototype属性和__proto__属性。
一般情况下,都是在构造函数中使用prototype属性,在实例对象上使用_proto__属性。
我们都知道,构造函数的prototype属性和实例对象的__proto__属性都指向同一个原型对象。
在实现继承时,我们使用的是构造函数.prototype = 某个对象,为什么不使用实例对象.__proto__ = 某个对象呢?
下面我们来看一下实例对象.__proto__ = 某个对象(Object.setPrototypeOf)这种方式给我们带来了什么。
- 切断了实例对象与原来的原型对象的联系。(如果是在实例对象上进行如此操作就要非常小心,因为它会切断与原来构造函数的原型对象的类型)
- 使得实例对象的属性搜索过程延伸到某个对象中。
1 a = {m: 'mm'}; 2 b = {n: 'nn'} 3 console.log('n' in a);// false 4 Object.setPrototypeOf(a, b); 5 console.log(a.hasOwnProperty('n')) // false 6 console.log('n' in a); // true 7 console.log(a.n); // nn
只有类型可以继承,实例对象是不可以继承的,在JS中,可以理解为只有构造函数可以继承,构造函数实例化出来的实例对象是不可以继承。
但是,使用setPrototypeOf可以进行一部分继承(只继承原型对象,不继承原型对象对应的构造函数),从根本上来说,它延长了属性搜索的区域。
参考: 《javascript高级程序设计》