2.03.05 原型与原型链

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.解决构造函数的弊端

  1. 解决办法:
    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,我们都将其都放在小狗实例中,因为每条小狗的这些属性都各不相同:
    请添加图片描述

  1. 继承的工作原理
  • 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.原型的概念

  1. 在javascript中, 每个函数都自带一个特殊的属性叫作原型(prototype)。
  2. 这个prototype属性的数据类型是object,也就是对象,因此称为原型对象。
  3. 在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中寻找
  1. 看看实例对象的内部
    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,就可以看看下面原型链的说法
  1. 重写实力对象的方法,不会影响原型
    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.原型链

  1. 例子:
    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.
  1. 概念:由每个对象的__proto__ 这样一层一层向上查找,所形成的链式结构,叫做原型链,每个对象的__proto__ 都指向他们的构造函数的原型prototype。

  2. 原型链的尽头

  • 因为在JavaScript中,function、object、数组等都是object对象类型,所以说js中万物皆对象,而这些都是Object这个构造函数(这个Object函数本身也是对象)实例出来的,所以原型链的尽头是Object.prototype
    // 原型链的尽头:
    console.log( Object.prototype.__proto__);// null
    console.log( Object.prototype.__proto__ === null);// true

  1. 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
  1. 函数对象
  2. 概念:JavaScript中函数就是对象,普通“键值对”对象其原型对象连接到Object.prototype。函数对象会隐藏连接到Function.prototype(Function.prototype对象本身连接到Object.prototype)。
  3. 我们知道每个函数在创建时会配有一个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.其他杂项

  1. 往构造函数的原型添加方法也可以这么写:
    Dog.prototype.wag = function() {
        alert(this.name + "Wag!"); 
    }; 

    可以写成:

    Dog.prototype={
        constructor:Dog,     //此处作用是为了指向构造函数Snake,不是指向Object
        wag:function() {
            alert(this.name + "Wag!");
        }
    }
  1. 给内置对象的原型添加方法
    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可以不带参数
  1. 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
  1. prototype模式的验证方法
  • 为了配合prototype属性,Javascript定义了一些辅助方法,帮助我们使用它
  1. 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
  1. 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
  1. 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
    */
  1. 原型链的图解:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值