原型对象
prototype属性
Javascript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。
这个对象的所有属性和方法,都会被构造函数的实例继承。
这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上。
function Person (name, age) { this.name = name this.age = age } console.log(Person.prototype) Person.prototype.type = 'human' Person.prototype.sayName = function () { console.log(this.name) } var p1 = new Person(...) var p2 = new Person(...) console.log(p1.sayName === p2.sayName) // => true
这时所有实例的 type 属性和 sayName() 方法,其实都是同一个内存地址,指向 prototype 对象,因此就提高了运行效率。
构造函数、实例、原型三者之间的关系
任何函数都具有一个 prototype 属性,该属性是一个对象。
function F () {} console.log(F.prototype) // => object F.prototype.sayHi = function () { console.log('你好啊!') }
构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数。
console.log(F.constructor === F) // => 得到的结果是true
通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 __proto__。
var instance = new F() console.log(instance.__proto__ === F.prototype) // => true
__proto__ 是非标准属性。
实例对象可以直接访问原型对象成员。
instance.sayHi() // => 输出:你好啊!
总结:
- 任何函数都具有一个 prototype 属性,该属性是一个对象
- 构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数
- 通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 __proto__
- 所有实例都直接或间接继承了原型对象的成员
探究:原型的指向是否可以改变
结论:
- 原型指向可以改变
实例对象的原型__proto__指向的是该对象所在的构造函数的原型对象
构造函数的原型对象(prototype)指向如果改变了,实例对象的原型(proto)指向也会发生改变
- 原型的指向是可以改变的
实例对象和原型对象之间的关系是通过__proto__原型来联系起来的,这个关系就是原型链
属性成员的搜索原则:原型链
了解了 构造函数-实例-原型对象 三者之间的关系后,接下来我们来解释一下为什么实例对象可以访问原型对象中的成员。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性
- 搜索首先从对象实例本身开始
- 如果在实例中找到了具有给定名字的属性,则返回该属性的值
- 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
- 如果在原型对象中找到了这个属性,则返回该属性的值
也就是说,在我们调用 person1.sayName() 的时候,会先后执行两次搜索:
- 首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。”
- 然后,它继续搜索,再问:“ person1 的原型有 sayName 属性吗?”答:“有。”
- 于是,它就读取那个保存在原型对象中的函数。
- 当我们调用 person2.sayName() 时,将会重现相同的搜索过程,得到相同的结果。
而这正是多个对象实例共享原型所保存的属性和方法的基本原理。
总结:
- 先在自己身上找,找到即返回
- 自己身上找不到,则沿着原型链向上查找,找到即返回
- 如果一直到原型链的末端还没有找到,则返回 undefined
原型链案例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>title</title> <script> //使用对象---->使用对象中的属性和对象中的方法,使用对象就要先有构造函数 //构造函数 function Person(name,age) { //属性 this.name=name; this.age=age; //在构造函数中的方法 this.eat=function () { console.log("吃好吃的"); }; } //添加共享的属性 Person.prototype.sex="男"; //添加共享的方法 Person.prototype.sayHi=function () { console.log("您好啊,怎么这么帅,就是这么帅"); }; //实例化对象,并初始化 var per=new Person("小明",20); per.sayHi(); //如果想要使用一些属性和方法,并且属性的值在每个对象中都是一样的,方法在每个对象中的操作也都是一样,那么,为了共享数据,节省内存空间,是可以把属性和方法通过原型的方式进行赋值 console.dir(per);//实例对象的结构 console.dir(Person);//构造函数的结构 //实例对象的原型__proto__和构造函数的原型prototype指向是相同的 //实例对象中的__proto__原型指向的是构造函数中的原型prototype console.log(per.__proto__==Person.prototype); //实例对象中__proto__是原型,浏览器使用的 //构造函数中的prototype是原型,程序员使用的 //原型链:是一种关系,实例对象和原型对象之间的关系,关系是通过原型(__proto__)来联系的 </script> </head> <body> </body> </html>
实例对象读写原型对象成员
读取:
- 先在自己身上找,找到即返回
- 自己身上找不到,则沿着原型链向上查找,找到即返回
- 如果一直到原型链的末端还没有找到,则返回 undefined
值类型成员写入(实例对象.值类型成员 = xx):
- 当实例期望重写原型对象中的某个普通数据成员时实际上会把该成员添加到自己身上
- 也就是说该行为实际上会屏蔽掉对原型对象成员的访问
引用类型成员写入(实例对象.引用类型成员 = xx):
- 同上
复杂类型修改(实例对象.成员.xx = xx):
- 同样会先在自己身上找该成员,如果自己身上找到则直接修改
- 如果自己身上找不到,则沿着原型链继续查找,如果找到则修改
- 如果一直到原型链的末端还没有找到该成员,则报错(实例对象.undefined.xx = xx)
更简单的原型语法
我们注意到,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype 。
为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:
function Person (name, age) { this.name = name this.age = age } Person.prototype = { type: 'human', sayHello: function () { console.log('我叫' + this.name + ',我今年' + this.age + '岁了') } }
在该示例中,我们将 Person.prototype 重置到了一个新的对象。
这样做的好处就是为 Person.prototype 添加成员简单了,但是也会带来一个问题,那就是原型对象丢失了 constructor 成员。
所以,我们为了保持 constructor 的指向正确,建议的写法是:
function Person (name, age) { this.name = name this.age = age } Person.prototype = { constructor: Person, // => 手动将 constructor 指向正确的构造函数 type: 'human', sayHello: function () { console.log('我叫' + this.name + ',我今年' + this.age + '岁了') } }
原生对象的原型
所有函数都有 prototype 属性对象。
- Object.prototype
- Function.prototype
- Array.prototype
- String.prototype
- Number.prototype
- Date.prototype
- …
练习:为数组对象和字符串对象扩展原型方法。
原型对象的问题
- 共享数组
- 共享对象
如果真的希望可以被实例对象之间共享和修改这些共享数据那就不是问题。但是如果不希望实例之间共享和修改这些共享数据则就是问题。
一个更好的建议是,最好不要让实例之间互相共享这些数组或者对象成员,一旦修改的话会导致数据的走向很不明确而且难以维护。
原型对象使用建议
- 私有成员(一般就是非函数成员)放到构造函数中
- 共享成员(一般就是函数)放到原型对象中
- 如果重置了 prototype 记得修正 constructor 的指向
原型对象的作用:共享数据,节省内存空间
__proto__与prototype区分
实例对象中有__proto__ 这个属性,叫原型,也是一个对象,这个属性是给浏览器使用,不是标准的属性→.→(proto----->可以叫原型对象)
构造函数中有prototype这个属性,叫原型,也是一个对象,这个属性是给程序员使用,是标准的属性------>prototype—>可以叫原型对象
- 实例对象的__proto__和构造函数中的prototype相等
- 又因为实例对象是通过构造函数来创建的,构造函数中有原型对象prototype,所以实例对象的__proto__指向了构造函数的原型对象prototype
小案例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>title</title> <script> function Person(name,age) { this.name=name; this.age=age; } //通过原型来添加方法,解决数据共享,节省内存空间 Person.prototype.eat=function () { console.log("吃凉菜"); }; var p1=new Person("小明",20); var p2=new Person("小红",30); console.log(p1.eat==p2.eat);//true console.dir(p1); console.dir(p2); </script> </head> <body> </body> </html>
案例:随机方块
补充:把局部变量变成全局变量方法:把局部变量给window
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>title</title> <script> //函数的自调用---自调用函数 // 一次性的函数--声明的同时,直接调用了 (function () { console.log("自调用函数"); })(); //页面加载后.这个自调用函数的代码就执行完了 // (function (形参) { // var num=10;//局部变量 // })(实参); // console.log(num); (function (win) { var num=10;//局部变量 //js是一门动态类型的语言,对象没有属性,点了就有了 win.num=num; })(window); console.log(num); //如何把局部变量变成全局变量? //↓↓↓↓↓↓↓↓↓↓ //把局部变量给window就可以了 </script> </head> <body> </body> </html>
案例实现
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>title</title> <style> .map{ width: 800px; height: 600px; background-color: #CCC; position: relative; } </style> </head> <body> <div class="map"></div> <script src="common.js"></script> <script> //产生随机数对象的 (function (window) { function Random() { } Random.prototype.getRandom=function (min,max) { return Math.floor(Math.random()*(max-min)+min); }; //把局部对象暴露给window顶级对象,就成了全局的对象 window.Random=new Random(); })(window);//自调用构造函数的方式,分号一定要加上 //产生小方块对象 (function (window) { //console.log(Random.getRandom(0,5)); //选择器的方式来获取元素对象 var map=document.querySelector(".map"); //食物的构造函数 function Food(width,height,color) { this.width=width||20;//默认的小方块的宽 this.height=height||20;//默认的小方块的高 //横坐标,纵坐标 this.x=0;//横坐标随机产生的 this.y=0;//纵坐标随机产生的 this.color=color;//小方块的背景颜色 this.element=document.createElement("div");//小方块的元素 } //初始化小方块的显示的效果及位置---显示地图上 Food.prototype.init=function (map) { //设置小方块的样式 var div=this.element; div.style.position="absolute";//脱离文档流 div.style.width=this.width+"px"; div.style.height=this.height+"px"; div.style.backgroundColor=this.color; //把小方块加到map地图中 map.appendChild(div); this.render(map); }; //产生随机位置 Food.prototype.render=function (map) { //随机产生横纵坐标 var x=Random.getRandom(0,map.offsetWidth/this.width)*this.width; var y=Random.getRandom(0,map.offsetHeight/this.height)*this.height; this.x=x; this.y=y; var div=this.element; div.style.left=this.x+"px"; div.style.top=this.y+"px"; }; //实例化对象 var fd=new Food(20,20,"green"); fd.init(map); console.log(fd.x+"===="+fd.y); })(window); </script> </body> </html>