2.03.05 原型 与 原型链
1.构造函数的弊端
- 介绍:构造函数方法很好用,但是存在一个浪费内存的问题。我们以Dog对象为例:
function Dog(name, breed, weight) {
this.name = name;
this.breed = breed;
this.weight = weight;
this.bark = function() {
if (this.weight > 25) {
alert(this.name + " says WOOF!");
} else {
alert(this.name + " says woof!");
}
};
}
var fido = new Dog("Fido", "柴犬", 38);
var fluffy = new Dog("Fluffy", "贵宾", 30);
var spot = new Dog("Spot", "吉娃娃", 10);
console.log(fido.bark === fluffy.bark) // false
console.log(fido.bark === spot.bark) // false
console.log(fluffy.bark === spot.bark) // false
- 上面会返回false的原因是因为 fido实例对象、fliffy实例对象与spot实例对象里面的方法bark(这是个应用数据类型)有不同的地址
- 所以,构造函数的弊端就是:那就是对于每一个实例对象,bark()方法功能完全相同,但每个小狗对象都有自己的副本。每一次生成一个实例,都必须为重复的内容,多占用一些内存。这样会影响应用程序的性能,占用计算机资源。这可能是个大问题,在移动设备上尤其如此。
- 而要解决以上问题这是我们需要充分利用JavaScript的对象模型。JavaScript对象模型基于原型的概念,在这种模型中,可通过扩展其他对象(即原型对象)来创建对象。
2.解决构造函数的弊端
- 解决办法:
function Dog(name, breed, weight) {
this.name = name;
this.breed = breed;
this.weight = weight;
}
// 这里给小狗原型添加了属性和方法。
Dog.prototype.species = "犬科"
Dog.prototype.bark = function() {
if (this.weight > 25) {
alert(this.name + " says WOOF!");
}else {
alert(this.name + " says woof!");
}
};
Dog.prototype.run = function() {
alert(this.name + " Run!");
};
Dog.prototype.wag = function() {
alert(this.name + "Wag!");
};
// 像通常那样创建小狗对象。
var fido = new Dog("Fido", "柴犬", 38);
var fluffy = new Dog("Fluffy", "贵宾", 30);
var spot = new Dog("Spot", "吉娃娃", 10);
// 然后,像通常那样对每个小狗对象调用方法。每个小狗对象都从原型那里继承了这些方法。
fido.bark();
fido.run();
fido.wag();
fluffy.bark();
fluffy.run();
fluffy.wag();
spot.bark();
spot.run();
spot.wag();
console.log(fido.bark===fluffy.bark); //true
console.log(fido.bark===spot.bark); //true
console.log(fluffy.bark===spot.bark); //true
-
注意:添加到原型的方法中的this指向调用它的实例对象
-
图解:首先,需要创建小狗对象Fido、Fluffy和Spot的对象图,让它们继承新创建的小狗原型。为表示继承关系,我们将绘制从小狗实例到原型的虚线。注意,我们只将所有小狗都需要的方法和属性放在小狗原型中,因为所有小狗都将继承它们。对于所有随小狗对象而异的属性,如name,我们都将其都放在小狗实例中,因为每条小狗的这些属性都各不相同:
- 继承的工作原理
- Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。
- 继承的方法和属性并不包含在各个小狗对象中,而是包含在原型中,上面的例子如何让小狗发出叫声呢?这正是继承的用武之地。对象调用方法时,如果在对象中找不到,将在原型中查找它
- 首先,需要编写一些代码。例如,一个小狗对象调用方法bark的代码:
- 为执行这些代码,我们在实例fido中查找方法bark,但没有找到。
- 既然在实例fido中找不到方法bark,我们就沿继承链上移,在其原型中接着查找。
- 在小狗原型中查找,发现其中确实有方法bark。
- 最后,找到方法bark后,我们调用它,导致小狗对象fido发出叫声。
- 属性的情况也一样。如果我们编写了需要获取fido.name的代码,将从fido对象中获取这个值。如果要获取fido.species的值,将首先在对象fido中查找;在这里找不到后,将接着在小狗原型中查找
- 明白如何使用继承后,意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象上。便可以创建大量的实例对象了。例如上面的小狗对象都能发出叫声,但依赖于小狗原型提供的方法bark。原型模式实现的代码重用,不仅只需在一个地方编写代码,而且让所有小狗实例都在运行阶段使用同一个bark方法,从而避免了庞大的运行阶段开销。
console.log(fido.bark === fluffy.bark) // true
console.log(fido.bark === spot.bark) // true
console.log(fluffy.bark === spot.bark) // true
3.原型
1.基于原型的语言
- JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。(准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非对象实例本身。)
- 在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制,而是在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。
- 注意: 理解对象的原型(可以通过Object.getPrototypeOf(obj)或者已被弃用的__proto__属性获得)与构造函数的prototype属性之间的区别是很重要的。前者是每个实例上都有的属性,后者是构造函数的属性。也就是说,Object.getPrototypeOf(new Foobar())和Foobar.prototype指向着同一个对象。
2.原型的概念
- 在javascript中, 每个函数都自带一个特殊的属性叫作原型(prototype)。
- 这个prototype属性的数据类型是object,也就是对象,因此称为原型对象。
- 在javascript中,函数也是一个对象,所以函数可以有属性。
3.认识特殊属性————原型(prototype)
1. 特殊点1
- 构造函数实例出来的对象继承构造函数的prototype属性的所有属性和方法
function Dog(name,weight) {
this.name = name;
this.weight = weight;
}
var spot = new Dog("Spot",10);
console.log(spot); //这里打印了一个Dog的一个实例对象spot
// spot.bark(); //控制台反馈:spot.bark is not a function
// console.log(spot.bark); //undefined
/*
Dog.prototype.bark=function(){
console.log("say Woo woo!");
}
*/
//spot.bark(); //say Woo woo!
2.特殊点2
- 之所以有特殊点1,是因为特殊点2的存在
- 实例对象a在构造函数A构造出来的前提下,实例对象a在使用属性和方法的时候,会现在本身寻找,找不到就会从构造函数A的prototype中寻找
- 看看实例对象的内部
function Dog(name,weight) {
this.name = name;
this.weight = weight;
}
Dog.prototype.bark=function(){
console.log("say Woo woo!");
}
var spot = new Dog("Spot",10);
console.log(spot); //这里打印了一个Dog的一个实例对象spot
- 实例对象的内部打印:
Dog{
name: "Spot"
weight: 10
[[Prototype]]: Object{
bark: ƒ ()
constructor: ƒ Dog(name,weight)
[[Prototype]]: Object
}
}
- 以看到实例对象spot本身并没有bark()这个方法,只是它里面有个[[Prototype]],[[Prototype]]里面有个bark()这个方法
- 只是在Dog.prototype中添加了bark()方法,却被实例对象spot识别到了,所以说构造函数的prototype的属相和方法会被实例对象继承,但这些被继承的属性和方法虽被继承了,但他还是在构造函数的protitype里面,至于说spot为什么能识别到构造函数的prototype,就可以看看下面原型链的说法
- 重写实力对象的方法,不会影响原型
function Dog(name,weight) {
this.name = name;
this.weight = weight;
}
Dog.prototype.bark=function(){
console.log("say Woo woo!");
}
var spot = new Dog("Spot",10);
var spot2 = new Dog("SPOT2",10);
spot.bark=function(){
console.log("hahahaha");
}
spot.bark(); //hahahaha
spot2.bark(); //say Woo woo!
- 在任何情况下,都可重写原型的属性和方法,为此只需在对象实例中提供它们即可。这之所以可行,是因为JavaScript总是先在对象实例(即具体的spot对象)中查找属性;如果找不到,再在原型中查找。因此,要为对象spot定制方法bark,只需在其中包含自定义的方法bark。这样,JavaScript查找方法bark以便调用它时,将在对象spot中找到它,而不用劳神去原型中查找。
4.原型链
1.属性__proto__
- 每个对象都自带一个__proto__属性
- 这个属性用于指向他的构造函数的原型prototype,就是 实例对象.__proto__与构造函数.prototype相等
- 就是因为属性__proto__指向他的构造函数的原型prototype,所以实例对象能使用构造函数的原型prototype中的属性与方法,但这些属性与方法却不存在于实例对象身上
- 属性__proto__也称为隐式原型
- 这个属性现在被弃用了,但很多浏览器还支持这个效果,我们可以像不弃用之前那样正常使用
function Dog(name,weight) {
this.name = name;
this.weight = weight;
}
Dog.prototype.bark=function(){
console.log("say Woo woo!");
}
var spot = new Dog("Spot",10);
console.log(spot.__proto__ === Dog.prototype); //true
2.原型链
- 例子:
function Dog(name,weight) {
this.name = name;
this.weight = weight;
}
var spot = new Dog("Spot",10);
Dog.prototype.bark=function(){
console.log("say Woo woo!");
}
console.log(spot.__proto__ === Dog.prototype); //true
spot.bark(); //say Woo woo!
- 分析一下上面的继承:
- 就像上面看到的, spot的__proto__ 属性就是Dog.prototype。
- 但是这又有什么用呢? 好吧,当你访问 spot的一个属性, 浏览器首先查找 spot是否有这个属性.
- 如果 spot没有这个属性, 然后浏览器就会在spot的__proto__中查找这个属性(也就是 Dog.prototype).
- 如果 spot的__proto__有这个属性, 那么 spot的__proto__ 上的这个属性就会被使用.
- 否则, 如果 spot的__proto__ 没有这个属性, 浏览器就会去查找 spot的__proto__ 的__proto__ ,看它是否有这个属性.
- 默认情况下, 所有函数的原型属性的__proto__ 就是 window.Object.prototype. 所以 spot的__proto__ 的__proto__ (也就是 Dog.prototype 的__proto__ (也就是 Object.prototype)) 会被查找是否有这个属性.
- 如果没有在它里面找到这个属性, 然后就会在 spot的__proto__ 的__proto__ 的__proto__ 里面查找.
- 然而这有一个问题: spot的__proto__ 的__proto__ 的__proto__ 不存在.
- 最后, 原型链上面的所有的__proto__ 都被找完了, 浏览器所有已经声明了的__proto__ 上都不存在这个属性,然后就得出结论,这个属性是 undefined.
-
概念:由每个对象的__proto__ 这样一层一层向上查找,所形成的链式结构,叫做原型链,每个对象的__proto__ 都指向他们的构造函数的原型prototype。
-
原型链的尽头
- 因为在JavaScript中,function、object、数组等都是object对象类型,所以说js中万物皆对象,而这些都是Object这个构造函数(这个Object函数本身也是对象)实例出来的,所以原型链的尽头是Object.prototype
// 原型链的尽头:
console.log( Object.prototype.__proto__);// null
console.log( Object.prototype.__proto__ === null);// true
- JavaScript的内置的构造函数 的原型 的隐式原型
//Date 日期对象
//Array 数组对象
//String 字符串对象
//Number 数字对象
//Function 函数对象
//Object 对象(原始数据)
console.log( Object.prototype.__proto__);// null
console.log( Object.prototype.__proto__ === null);// true
console.log(Date.prototype.__proto__===Object.prototype);//true
console.log(Array.prototype.__proto__===Object.prototype);//true
console.log(String.prototype.__proto__===Object.prototype);//true
console.log(Number.prototype.__proto__===Object.prototype);//true
console.log(Function.prototype.__proto__===Object.prototype);//true
- 函数对象
- 概念:JavaScript中函数就是对象,普通“键值对”对象其原型对象连接到Object.prototype。函数对象会隐藏连接到Function.prototype(Function.prototype对象本身连接到Object.prototype)。
- 我们知道每个函数在创建时会配有一个prototype属性,而且prototype属性的值是一个拥有constructor属性且值为该函数的对象。
Function.prototype.OK="欧宽";
function Dog(name,weight) {
this.name = name;
this.weight = weight;
}
console.log(Dog.OK); //欧宽
console.log(Dog.__proto__===Function.prototype);//true
3.prototype与__proto__的区别
- prototype是函数的属性
- __proto__是对象属性,但函数也是对象,所以函数也有__proto__属性
- 函数可也拥有prototype,proto ;对象只拥有__proto__
function Dog(name,weight) {
this.name = name;
this.weight = weight;
}
var spot = new Dog("Spot",10);
console.log(Dog.__proto__===Function.prototype); //true
console.log(Dog.prototype!=undefined);//true
console.log(Dog.__proto__!=undefined);//true
console.log(spot.prototype); //undefined
console.log(spot.__proto__!=undefined);//true
5.其他杂项
- 往构造函数的原型添加方法也可以这么写:
Dog.prototype.wag = function() {
alert(this.name + "Wag!");
};
可以写成:
Dog.prototype={
constructor:Dog, //此处作用是为了指向构造函数Snake,不是指向Object
wag:function() {
alert(this.name + "Wag!");
}
}
- 给内置对象的原型添加方法
var arr=[1,2,3,1,1,2,3,5,7];
function unique(arr){
for(var i=0;i<arr.length;i++){ //遍历数组每一个元素
while((arr.indexOf(arr[i],i+1)) != -1){
//遍历arr[i]的后面的元素, 使用indexOf寻找arr[i]后面的元素是否与arr[i]相同
//当返回值!=-1时,证明后面的元素与arr[i]有相同的,就是重复的元素
//我们需要把它切掉
var index = arr.indexOf(arr[i], i+1); //重复元素的下标
arr.splice(index, 1); //把重复元素的元素切掉
}
}
}
unique(arr);
console.log("使用unique去重后得到 => "+arr); //使用unique去重后得到 => 1,2,3,5,7
var arr2=[1,2,3,1,1,2,3,5,7];
function unique2(){
for(var i=0;i<this.length;i++){
while((this.indexOf(this[i],i+1)) != -1){
var index = this.indexOf(this[i], i+1);
this.splice(index, 1);
}
}
}
Array.prototype.unique=unique2;
arr2.unique();
console.log("使用Array原型unique去重后得到 => "+arr2); //使用Array原型unique去重后得到 => 1,2,3,5,7
- 因为函数unique2在被arr2调用的时候this的指向arr2,所以arr2调用unique2可以不带参数
- constructor 属性
- 每个实例对象都从原型中继承了一个constructor属性,该属性指向了用于构造此实例对象的构造函数。
function Dog(name,weight) {
this.name = name;
this.weight = weight;
}
var spot = new Dog("Spot",10);
console.log(spot.constructor===Dog);//true
console.log(Dog.prototype.constructor===Dog);//true
- 因此有一个小技巧:你可以在 constructor 属性的末尾添加一对圆括号(括号中包含所需的参数),从而用这个构造器创建另一个对象实例。毕竟构造器是一个函数,故可以通过圆括号调用;只需在前面添加 new 关键字,便能将此函数作为构造器使用。
function Dog(name,weight) {
this.name = name;
this.weight = weight;
}
var spot = new Dog("Spot",10);
console.log(spot.constructor===Dog);//true
console.log(Dog.prototype.constructor===Dog);//true
var spot2 = new spot.constructor("SSSSSSSSpot",100000);
console.log(spot2); //Dog{name: "SSSSSSSSpot" , weight: 100000}
console.log(spot2.name);//SSSSSSSSpot
- prototype模式的验证方法
- 为了配合prototype属性,Javascript定义了一些辅助方法,帮助我们使用它
- isPrototypeOf(): 这个方法用来判断,某个proptotype对象和某个实例之间的关系。表示对象是否存在于另一个对象的原型链中。
function Dog(name,weight) {
this.name = name;
this.weight = weight;
}
var spot = new Dog("Spot",10);
console.log(Dog.prototype.isPrototypeOf(spot)); //true
- hasOwnProperty():每个实例对象都有一个hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承自prototype对象的属性。如果属性是在对象实例中定义的,这个方法将返回true。如果属性不是在对象实例中定义的,但能够访问它,就可认为它肯定是在原型中定义的。
function Dog(name,weight) {
this.name = name;
this.weight = weight;
}
var spot = new Dog("Spot",10);
Dog.prototype.owner="欧宽";
console.log(spot.hasOwnProperty("name"));// true
console.log(spot.hasOwnProperty("owner")); //false
- in运算符
- in运算符可以用来判断,某个实例是否含有某个属性,不管是不是本地属性。
- in运算符还可以用来遍历某个对象的所有属性。
function Dog(name,weight) {
this.name = name;
this.weight = weight;
}
var spot = new Dog("Spot",10);
Dog.prototype.owner="欧宽";
console.log("name" in spot); // true;
console.log("owner" in spot); // true;
for(var val in spot){
console.log(val);
}
/*
控制台输出:
name
weight
owner
*/
-
原型链的图解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uy9IqrVS-1649206160016)(./原型链图示.png)]function Dog(name,weight) {
this.name = name;
this.weight = weight;
}var spot = new Dog(“Spot”,10);
Dog.prototype.owner=“欧宽”;console.log(“name” in spot); // true;
console.log(“owner” in spot); // true;for(var val in spot){
console.log(val);
}
/*
控制台输出:
name
weight
owner
*/
5. 原型链的图解:
![请添加图片描述](https://img-blog.csdnimg.cn/7063b75cb69b472d9e7d34fe2d28b090.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lul5ZOyX29r,size_20,color_FFFFFF,t_70,g_se,x_16)