1.函数的prototype属性
每个函数都有一个prototype属性,它默认是一个空的Object的实例对象(即称为:原型对象),而原型对象中有一个属性constructor,它指向函数对象。这说明(构造)函数和它的原型对象相互引用。
Type表示的是构造函数,它其中有个属性prototype,而这个属性指向这个函数的prototype对象,而这个对象有个属性constructor,它是指回这个构造函数Type的,即是相互引用关系。
1.1 代码体验
<script> // 1. 每个函数都有一个prototype属性,它默认指向一个Object空对象(即称为:原型对象) // (1) 内置对象(构造函数) // 打印结果是object,但不是空对象,原因是系统内部先前已为其添加了各种属性,但它本身原始是空对象 console.log(Date.prototype); // (2) 自定义对象(构造函数) function Fun() { }; console.log(Fun.prototype); // 默认指向Object的空实例对象(没有程序员自己指定的属性) // 2. 原型对象中有一个属性constructor(默认内置的属性),它指向函数对象 console.log(Date.prototype.constructor === Date); // true console.log(Fun.prototype.constructor === Fun); //true </script>
注意:任何一个函数都有prototype属性,只是我们平时一般只有在使用构造函数时才会提及使用它而已,切勿误解。
2. 显示原型和隐式原型
① 每个函数function都有一个prototype,即显式原型
② 每个实例对象都有一个__proto__,可称为隐式原型
③ 对象的隐式原型的值为其对应构造函数的显示原型的值(显式原型===隐式原型)
2.1 代码体验
<script> // 1. 定义构造函数 function Fn() { // 内部执行语句:this.prototype={} }; // (1)每个函数function都有一个prototype,即显式原型属性,默认指向一个空的object对象 console.log(Fn.prototype); // 2. 创建一个实例对象 let fn = new Fn(); // 内部执行语句:this.__proto__ = Fn.prototype // (2)每个实例对象都有一个__proto__,即隐式原型属性 console.log(fn.__proto__); // (3)对象的隐式原型的值为其对应的构造函数的显式原型的值 console.log(fn.__proto__ === Fn.prototype); // true // 3. 给显式原型(原型对象) 添加属性(一般是方法) Fn.prototype.test = function() { console.log('test()'); }; // 4. 通过实例对象可调用原型对象的方法 fn.test(); // 打印结果 test() </script>
总结:
① 函数的prototype属性:在定义函数时自动添加的,默认值是个空的Object的实例对象
② 对象的__proto__属性:创建对象时自动添加的,默认值为构造函数的prototype属性值
③ 程序员能直接操作显式原型(添加属性(一般为方法)==》实例对象可访问),但一般不直接操作隐式原型。
2.2 内存结构图
为了方便了解原型,以及显式和隐式原型的区别,可以结合下面图片加强理解。
3. 手写数组函数
在了解了原型的相关知识后,我们可以对前面所学的一些数组方法,自己手写函数封装一样功能的方法,从而更好的理解原型的概念和使用。
<script> // 给内置构造函数Array的原型对象上添加自己书写的方法,这样使用数组时就可以调用该方法了 // (1) 过滤方法 Array.prototype.myFilter = function(callback) { let narr = []; for (let i = 0; i < this.length; i++) { if (callback(this[i])) { narr.push(this[i]) } } return narr; }; // (2) 映射方法 Array.prototype.myMap = function(callback) { let narr = []; for (let i = 0; i < this.length; i++) { narr.push(callback(this[i])); callback() }; return narr; }; // (3) 累计方法 // num表示初始值 Array.prototype.myReduce = function(callback, num) { // sum表示计算结束后的返回值。 let sum = num; for (let i = 0; i < this.length; i++) { sum = callback(sum, this[i]) }; return sum } let arr1 = [11, 22, 33, 44, 55, 66, 77, 88, 99, 123, 456]; // 由于上面我给内置对象Array的原型对象上添加了3个新方法,所以下面的实例对象就可以直接使用 let arr2 = arr1.myFilter(r => r % 2 === 0); // 从原数组中过滤出一个只含奇数的新数组 console.log(arr2); let arr3 = arr1.myMap(r => r + 1); // 从原数组中映射出一个各元素的值均大1的新数组 console.log(arr3); let arr4 = arr1.myReduce((a, b) => { // 将原数组各元素的值累加为一个值(即求和) return a + b }, 0); console.log(arr4); // 1074 </script>
4. 实际应用
正因为我们程序员可以手动给函数的原型对象添加属性(方法),所以我们在实际开发中,创建构造函数时,如果需要创建大量实例化对象,那么把方法添加到函数的原型对象prototype中,这样可以减少内存空间,从而大大提高性能。
<script> function Student(name, age, sex) { // 给构造函数添加属性,要使用this,这个this是指向实例化对象的 // 普通属性我们可以直接添加即可,属性值以形参的形式传入,这样具有扩展性 this.name = name this.age = age this.sex = sex }; // 方法不直接添加,而是将其添加在函数的原型对象上,这样函数的所有实例对象自动拥有原型中的方法, // 从而可以减少内存空间,从而提高性能 Student.prototype.sayHi = function() { console.log(`我叫${this.name},今年${this.age}岁,性别是${this.sex}`); }; Student.prototype.study = function(time) { console.log(`我叫${this.name},我每天学习${time}小时`); }; // 创建多个实例化对象时,直接使用原型方法,并且因为使用了this指向,所以打印的内容都是活泛的 let s1 = new Student("张三", 22, "男"); s1.sayHi(); s1.study(10); let s2 = new Student("李四", 34, "男"); s2.sayHi(); s1.study(8); let s3 = new Student("王燕", 29, "女"); s3.sayHi(); s3.study(12); </script>