JS 学习笔记--13---原型

时间:2022-08-24 22:39:35

 

 练习中使用的是IE11,如果有错误之处,还请各位朋友多多指教。本文关于原型难以描述,故多用代码展示

  原型是JS中一个很重要的概念,也是JS中一个难点,语言上难以描述,原型对象的属性和方法叫做原型属性和原型方法,构造函数中的属性和方法叫做实例属性和实例方法,它们的区别就是:对于一个对象的多个实例之间,它们的实例属性和实例方法是各不一样的,前面的面向对象中已经证明,而他们的原型属性和原型方法是一模一样的,完全相等的

构造函数方式声明原型对象:                                                                      

 1 //构造函数定义的成员变量时实例成员
 2     function Box(user,age){
 3         this.user=user;        //实例属性
 4         this.age=age;        
 5         this.run=function(){    //实例方法
 6             return this.user+" "+this.age+" "+"运行中...";
 7         }
 8     }
 9 
10 //原型声明,构造函数中什么也不写,然后通过prototype对象来添加属性和方法,调用都是一样的
11     function Box1(){};
12     Box1.prototype.user='abc';    //通过prototype定义的叫做  原型属性
13     Box1.prototype.age=22;        //下面这个方法叫做原型方法
14     Box1.prototype.run=function(){ return this.user+" "+this.age+" "+"运行中..."; };
15 
16     var box=new Box1();
17     alert(box.user);    // abc
18     alert(box.age);        // 22
19     alert(box.run());    // abc 22 运行中...
20 
21 //声明了两个对象   从下面的结果可以知道,这两个对象的引用不相等,但是他们中间的方法和属性是完全相等的  包括引用
22     var box1=new Box();
23     var box2=new Box();
24     alert(box1.user);    //abc
25     alert(box2.user);    //abc
26     alert(box1.age);    //22
27     alert(box2.age);    //22
28         
29     //如果是实例方法,不同的实例化,他们的方法的引用地址是不一样的,是唯一的
30     //但是原型对象,不同的实例化,他们的方法的引用地址也是一样的,共享的,大家都一样
31     alert(box1.run==box2.run);    // true
32     alert(box1==box2);    //false

 

JS 学习笔记--13---原型

JS 学习笔记--13---原型

 

1、  原型的作用主要作用

  用来共享一些属性和方法,我们每创建一个函数,都会自动的创建一个原型对象,原型对象是由函数下面的一个属性[__proto__]来指向的,这个属性是一个指针,它指向了原型对象的constructor属性,我们可以通过这两个属性就可以访问原型对象中的属性和方法了。

  constructor是一个构造属性,是可以获取构造函数的本身的,它的作用其实就是被原型指针[__proto__]定位,然后获取到构造函数的本身

1    alert(Box.prototype);    //访问方法的属性prototype
2     alert(box1.prototype);    //undefined        这个属性是一个对象,是访问不到的    
3     alert(box1.__proto__);    //object Object      低版本的IE可能打印不出来
4    alert(box1.constructor);    //function Box(){}; //构造属性
5     alert(Box.constructor);        // function Function(){...};    Box 本身就是一个Function类型
6     //alert(box1.prototype.constructor)    //error

  

  

2、isPrototypeOf() 方法

  判断一个实例对象是否是指向了该构造函数的原型对象,可以用 isPrototypeOf() 方法来判断,如果指向了返回为true,没有则返回为false,一切对象都是继承自Object对象

1     alert(Box.prototype.isPrototypeOf(box1)); //true    只要是实例化的,都指向了原型对象
2     alert(Box.prototype.isPrototypeOf(box2)); //true
3     alert(Object.prototype.isPrototypeOf(box1));//true  因为一切对象都是 Object 类型的,故指向了
4     var box=new Object();
5     alert(Box.prototype.isPrototypeOf(box)); // false    box对象是Object类型的对象,Box其实类似于是继承自Object类型

 

3、原型模型的执行流程:遵循JS中的就近原则

  先查找构造函数实例里面的方法和属性,即先查找实例属性,如果有,立即返回值或者执行对应的方法

  如果实例属性或者实例方法中没有,则去原型对象中查找相应的属性和方法,有就返回或执行方法,如果没有就返回undefined或者报错

1     var box1=new Box();
2     var box2=new Box();    
3     box1.name='kkk';    //给对象box1添加一个实例属性,
4     alert(box1.name);    //访问的是box1的实例属性,box2是访问不到的,因为原型属性中也没有
5     alert(box2.name);
6     box1.user='jjj';    //其实是实例属性,并没有重写原型属性的值
7     alert(box1.__proto__.user);//abc    访问原型属性中的值
8     alert(box1.user);//jjj    访问的是实例属性中的值
9     alert(box2.user);//abc    box2 中不存在实例属性 user  就返回的是原型属性,它访问不到box1中的实例属性,因为他们之间共享的只是原型属性和方法

 

4、属性的删除和修改
  可以通过 delete 关键字来删除实例属性和原型属性,删除和修改原型属性可以通过两种方式:(1) 通过实例对象的指针修改:box1.__proto__.age; (2) 定义原型属性一样用构造函数修改 prototype: Box.prototype.age;

  构造函数中是改了就生效,不管何时声明的对象,而在后面的字面量形式中只有在声明实例对象之前该才有效(详细的见后面字面量形式创建中)

1     delete box1.user;    //删除对象box1中的实例属性
2     alert(box1.user);    //abc    因为前面删除了实例属性中的user属性,返回的就是原型属性中的user属性
3     //delete box1.__proto__.age;    //通过这种方式可以删除原型对象中的属性,但是别这样弄,牵一发而动全身
4     //delete Box.prototype.age;    //也是删除了原型对象中的属性 age
5     alert(box2.age);    //undefined     因为前面删除了原型属性中的age属性
6     box1.__proto__.age=33;    //修改了原型属性中的值
7     alert(box2.age);    //33
8     Box.prototype.age=44;    //修改了原型属性中的值
9     alert(box2.age);    //44


5、hasProperty() 方法和 in 操作符

  判断某个对象是否拥有某个实例属性,可以通过 hasOwnProperty() 方法来测试,有就返回true,否则返回false

  in 操作符可以判断对象中是否包含某个属性,不管这个属性是原型属性还是实例属性,包含则返回true

  可以通过 hasOwnProperty() 方法和 in 操作符 共同来判断某个对象是否包含某个原型属性
 

    var box1 = new Box();
    var box2 = new Box();

 1    box1.name='jjj';
 2     alert(box1.hasOwnProperty('name'));    //true
 3     alert(box2.hasOwnProperty('name'));    //false        box2 并没有实例属性name
 4     alert(box1.hasOwnProperty('user'));    //false        user 属性是原型属性,不是实例属性
 5 
 6     alert('name' in box1);    //false
 7     alert('user' in box1);    //true    原型属性
 8     box1.name='jjj';
 9     alert('name' in box1);    //true    实例属性
10 
11 //通过 hasOwnProperty() 方法和 in 操作符 共同来判断某个对象是否包含某个原型属性
12 function checkPropo(object,element){    //判断的是在某个对象中的属性,故要将对象和属性传递过来
13         if(!object.hasOwnProperty(element)){    //先判断是否存在实例属性,如果存在就返回false
14             if(element in object){    //如果存在就返回true
15                 return true;    
16             }else{
17                 return false;
18             }
19         }else{
20             return false;
21         }
22         //上面的代码可以用一个表达式来表示:return !object.hasOwnProperty(element)&& (element in object);
23     }
24     
25     alert(checkPropo(box1,'name'));    //false
26     box1.name='name';
27     alert(checkPropo(box1,'name'));    //false        虽然有属性 name,但是这是一个实例属性
28     alert(checkPropo(box1,'user'));    //true        //原型属性 user 是存在的


字面量形式创建原型对象:                                                      

 

 1     function Box(){};
 2     Box.prototype={        // 通过字面方法来创建原型属性和方法,这样有点封装的感觉
 3         user:'abc',
 4         age:123,
 5         run:function(){return this.user+" "+this.age+" 运行中...";}
 6     }
 7 
 8 //创建两个对象
 9     var box1=new Box();
10     var box2=new Box();
11     //运行结果是一样的
12     alert(box1.run());    //abc 123 运行中...
13     alert(box2.run());    //abc 123 运行中...
14     alert(box1.run==box2.run);    //true


6、字面量方式创建原型对象注意的问题一:构造属性的指向

  字面量创建的方式 用constructor 属性指向的是Object对象,而不是实例本身,但是构造函数方式创建的这相反;

  如果想让constructor指向实例[Box],可以采用强制指向的方式

 1     var box1 = new Box();
 2 
 3     alert(box1.constructor);    //function Object() { [native code] }
 4     alert(box1.constructor == Box);        //false
 5     alert(box1.constructor == Object);    //true
 6     alert(box1 instanceof Box);        //true
 7     alert(box1 instanceof Object);    //true
 8     alert(Box.constructor);    //function Function...
 9     alert(Box.prototype);    //object Object    //使用构造函数名(对象名)访问prototype
10     alert(box1.__proto__);    //object Object    //使用对象实例访问prototype的指针
11 
12 // 如果想让constructor指向实例[Box],可以采用强制指向的方式
13     function Box(){};
14     Box.prototype={        
15         constructor:Box,    //强制原型对象来指向Box
16         user:'abc',
17         age:123,
18         run:function(){return this.user+" "+this.age+" 运行中...";}
19     }
20 
21     //创建两个对象
22     var box1=new Box();
23     var box2=new Box();
24 
25     //强制constructor指向Box的时候,返回结果如下
26     alert(box1.constructor == Box);        //true
27     alert(box1.constructor == Object);    //false

  之所以出现上面的原因是因为:通过Box.prototype={..};方式创建的时候,都创建一个新的对象,而每次创建一个函数,都会同时创建它自己的prototype,那么这个新的对象也就会自动获取它自己的constructor属性,这样新对象的constructor属性重写了Box实例的constructor属性,因此会执行新的对象,而这个新的对象又没有指定构造函数,故默认的就是Object

7、字面量形式创建原型对象的问题二:原型属性的重写

  原型的声明是有先后顺序的,所以,重写的原型会切断之前的原型,故应该注意避免此种情况的发生

 1 var box2=new Box();    
 2 Box.prototype={    //字面量方式不存在修改原型属性,直接是覆盖掉原来的,因为每次都是创建一个新的对象
 3     user:444    //这里不会保留之前原型的任何信息
 4                 //把原来的原型对象和构造函数对象之间的关系给切断了
 5 }
 6 //字面量方式创建,只要声明了,以后随便怎么重写原型对象,已经创建的实例对象的值不会改变
 7     var box1=new Box();    
 8 
 9     alert(box1.user);    // 444    重写中赋值为444
10     alert(box1.age);    //undefined        因为第二次重写中没有这个属性
11 
12     alert(box2.age);//123    因为在对象的定义是发生在用字面量形式重写原型属性之前,故以后的原型属性的修改和box2无关


9、通过原型模式扩展类型的方法

  原型对象不仅仅可以在自定义对象的情况下使用, 而 ECMAScript 内置的引用类型都可以使用这种方式,并且内置的引用类型本身也使用了原型。

  通过prototype来添加方法,访问的时候要用这种类型的变量点这个方法,和访问系统内置的方法是一样的,但是最好不要用这种方式来添加方法,因为可能会存在命名冲突的问题,特别是在代码量大的时候,容易照成命名冲突问题。

 1     String.prototype.addString=function(s){        //传递一个参数过来,
 2         return '【' + s + '】';
 3     }
 4 
 5     alert('abc'.addString('111'));// 【111】
 6 
 7     String.prototype.addString=function(){    //可以不进行传参,通过this来代表当前调用这个方法 的字符串
 8         return this+"被添加了!";
 9     }
10 
11     alert("abcd".addString());    // abcd被添加了!


原型模式创建对象的缺点以及采用的方式:                          

  原型模式创建对象也有自己的缺点, 它省略了构造函数传参初始化这一过程, 带来的缺点就是初始化的值都是一致的。而原型最大的缺点就是它最大的优点,那就是共享。

  原型中所有的属性都能被很多实例共享,共享对于函数非常适合,对于包含基本值的属性页还可以,但是如果属性包含引用类型就一定存在问题,见代码中

 1 function Box(){};
 2     Box.prototype={
 3         constructor:Box,
 4         user:'abc',
 5         age:22,
 6         family:['哥哥','姐姐','妹妹'],
 7         run:function(){
 8             return this.user+" "+this.age+" 运行中...";
 9         }
10     }
11 
12     //var box=new Box(123);    //缺点之一就是不能够传递参数
13 
14     var box1=new Box();    //缺点二就是原型的共享性,这也是它最大的优点
15     alert(box1.family);    //哥哥,姐姐,妹妹
16     box1.family.push('弟弟');    //在第一个实例后修改了引用类型,保持了共享,但本质上不希望它共享
17     alert(box1.family);    //哥哥,姐姐,妹妹,弟弟
18 
19     var box2=new Box();
20     alert(box2.family);    //哥哥,姐姐,妹妹,弟弟    共享了box1引用类型添加后的原型


11、组合构造函数 + 原型模式

  这种方式能够很好的解决传参和引用共享的问题,是创建对象比较好的方法

 1 function Box(user,age){    //构造函数,这里面声明一些会变的属性和引用类型的属性
 2         this.user=user;
 3         this.age=age;
 4         this.family=['哥哥','姐姐','妹妹'];
 5     }
 6 
 7     Box.prototype={    //原型模式
 8         constructor:Box,
 9         run:function(){
10             return this.user+" "+this.age+" 运行中...";
11         }
12     }
13 
14     //下面可以看出通过构造函数总写一些自己会变的属性等,即使值改变了也不会被共享出去
15     var box1=new Box('abc',22);
16     alert(box1.run());    // abc 22 运行中...
17     alert(box1.family);    //哥哥,姐姐,妹妹    
18     box1.family.push("弟弟");
19     alert(box1.family);    //哥哥,姐姐,妹妹,弟弟
20 
21     var box2=new Box('jack',33);
22     alert(box2.family);    //哥哥,姐姐,妹妹    并没有共享对象box1中的引用类型
23     alert(box2.run());    //jack 33 运行中...        和box1 中的输出结果是不一样的


11、动态原型模型

  将构造函数和原型模型封装在一起,也能够解决共享的问题,但是要注意两个问题,一是资源的浪费,还有就是要注意,不可以再使用字面量的方式重写原型,因为会切断实例和新原型之间的联系

  在下面的代码中,当第一次调用构造函数时,run()方法发现不存在,然后初始化原型。当第二次调用, 就不会初始化, 并且第二次创建新对象, 原型也不会再初始化了。 这样及得到了封装, 又实现了原型方法共享,并且属性都保持独立

function Box(user,age){
        this.user=user;
        this.age=age;
        this.family=['哥哥','姐姐','妹妹'];

        if(typeof this.run != 'function'){//判断this.run是否存在,因为执行一次后类型返回值就为为function
            alert("原型初始化开始");//没有判断之前,每new一次Box都会执行一次,
            Box.prototype.clss='person';
            Box.prototype.run=function(){
                return this.user+" "+this.age+" 运行中...";
            };
            alert("原型初始化结束");
        }
    }

    var box1=new Box('abc',22);    
    var box2=new Box('jack',33);
//也可以判断原型中任意一个属性的类型返回值,是否等于undefined,若等于原型就还没有创建 //因为原型中的属性一般都是一开始就有特定的值的,除非故意赋值为undefined[这样没意义了] function Box(user,age){ this.user=user; this.age=age; this.family=["哥哥","姐姐","妹妹"]; if(typeof this.clss == "undefined"){ alert("原型初始化开始");//没有判断之前,每new一次Box都会执行一次, Box.prototype.clss="person"; Box.prototype.run=function(){ return this.user+" "+this.age+" 运行中..."; }; alert("原型初始化结束"); } } var box1=new Box('abc',22); var box2=new Box('jack',33);


12、寄生构造函数

  寄生构造函数,其实就是工厂模式+构造函数模式。这种模式比较通用,但不能确定对象关系,所以,在可以使用之前所说的模式时,不建议使用此模式;在什么情况下使用寄生构造函数比较合适呢?假设要创建一个具有额外方法的引用类型。由于之前说明不建议直接 String.prototype.addstring,可以通过寄生构造的方式添加

    function myString(string){
        var str = new String(string);
        str.addString = function(){
            return this + ",被添加了!";//this 指的是对象str下的值string
        }  
        return str;  
    }

    var box = new myString("abcd");
    alert(box.addString());


13、稳妥构造函数

  在一些安全的环境中, 比如禁止使用 this 和 new, 这里的 this 是构造函数里不使用 this,这里的 new 是在外部实例化构造函数时不使用 new。这种创建方式叫做稳妥构造函数。

 1     function Box(name, age) {
 2         var obj = new Object();
 3         obj.name = name;
 4         obj.age = age;
 5         obj.run = function () {
 6             return name + age + '运行中...';
 7         };
 8         return obj;
 9     }
10 
11     var box1 = Box('Lee', 100);
12     alert(box1.run());
13 
14     var box2 = Box('Jack', 200);
15     alert(box2.run());

 

用的最多的应该是组合原型模式+构造函数以及动态原型模式