• JavaScript 【面向对象的程序设计】创建对象


    以下为学习《JavaScript 高级程序设计》》(第 3 版) 所做笔记。

    目录:

      1、使用Object构造函数或者对象字面量创建对象

      2、工厂模式

      3、构造函数模式

        ① 将构造函数当作函数

        ② 构造函数的问题

      4、原型模式

        ① 理解原型对象

        ② 原型与 in 操作符

        ③ 更简单的原型语法

        ④ 原型的动态性

        ⑤ 原生对象的原型

        ⑥ 原型对象的问题

      5、组合使用构造函数模式和原型模式

      6、动态原型模式

      7、寄生构造函数模式

      8、稳妥构造函数模式

    使用Object构造函数或者对象字面量创建对象

    缺点:使用同一个接口创建很多对象,会产生大量的重复代码

     1 <script>
     2     //使用 Object 构造函数创建单个对象
     3     var person = new Object();
     4     person.name = "xiaoxu";
     5     person.age = 20;
     6     person.a = function(){
     7         console.log("今tia很开心");
     8     }
     9     console.log( person.name ); //输出:xiaoxu
    10     person.a();                 //输出:今tia很开心
    11     //使用对象字面量创建单个对象
    12     var dog = {
    13         name : "旺财",
    14          age : 2,
    15         word : function(){
    16             console.log("汪汪");
    17         }
    18     }
    19     console.log( dog.name );    //输出:旺财
    20     dog.word();                 //输出:汪汪
    21 </script>

    工厂模式

     优点:解决了创建多个相似对象的问题

     缺点:没有解决对象识别的问题(即怎么知道一个对象的类型)

     1 <script>
     2     function createPerson(name, age, job){
     3         var o = new Object();
     4         o.name = name;
     5         o.age = age;
     6         o.sex = sex;
     7         o.sayName = function(){
     8             console.log( o.name );
     9         };
    10         return o;
    11     }
    12     //函数 createPerson() 能够根据接受的参数来构建一个包含所有必要信息的 Person 对象
    13     var person1 = createPerson( "小许", 20, "male" );
    14     var person2 = createPerson( "乐乐", 1, "female" );
    15 </script>

    构造函数模式

     优点:创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型(这是胜过工厂模式的地方)

     1 <script>
     2 /*
     3     创建构造函数: 1、没有显示地创建对象
     4                 2、直接将属性和方法赋给了 this 对象
     5                 3、没有 return 语句
     6                 4、构造函数始终以一个大写字母开头
     7 */
     8     function Person(name, age, sex){
     9         this.name = name;
    10         this.age = age;
    11         this.sex = sex;
    12         this.sayName = function(){
    13             console.log( this.name );
    14         };
    15     }
    16 /*
    17     创建构造函数新实例时,必须使用 new 操作符。使用 new 操作符调用构造函数实际上会经历 4 个步骤:
    18     1、创建1个新对象
    19     2、将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
    20     3、执行构造函数中的代码(为这个新对象添加属性)
    21     4、返回新对象
    22 */
    23     var person1 = new Person( "xiaoxu", 20, "male" );
    24     var person2 = new Person( "旺财", 1 ,"female" );
    25 
    26     //函数实例都有一个 constructor(构造函数)属性,该属性指向函数
    27     console.log( person1.constructor == Person );      //输出:true
    28     console.log( person2.constructor == Person );      //输出:true
    29 
    30     //本例中创建的所有对象既是 object 的实例,也是 Person 的实例
    31     //创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型(这是胜过工厂模式的地方)
    32     console.log( person1 instanceof Object );       //输出:true
    33     console.log( person1 instanceof Person );       //输出:true
    34     console.log( person2 instanceof Object );       //输出:true
    35     console.log( person2 instanceof Person );       //输出:true
    36 </script>

    将构造函数当作函数

       使用 new 操作符调用函数,函数就当作构造函数使用。不使用 new 操作符调用,函数就当作普通函数使用。不使用 new 操作符调用,函数的属性和方法都被添加给 window 对象。

     1 <script>
     2 //以这种方式定义的构造函数是定义在 Global 对象(在浏览器中是 window 对象)中的。
     3     function Person(name){
     4         this.name = name;
     5         this.sayName = function(){
     6             console.log( this.name );
     7         }
     8     }
     9 //使用 new 操作符调用
    10     //当作构造函数调用
    11     var person1 = new Person("xiaoxu");
    12     person1.sayName();      //输出:xiaoxu
    13 //不使用 new 操作符调用
    14     //当作普通函数调用
    15     Person("乐乐");
    16     window.sayName();       //输出:乐乐
    17     //在另一个对象的作用域中调用
    18     var o  = new Object();
    19     Person.call(o, "Sammi");
    20     o.sayName();            //输出:Sammi
    21 </script>

    构造函数的问题

      使用构造函数的主要问题是每个方法都要在每个实例上都要创建一遍。不同实例上的同名函数是不相等的,但是没有必要创建 2 个完成相同任务的 Function对象。可以通过把函数定义转移到构造函数外部来解决这个问题,但是这样又有2个缺点:①在全局作用域中定义的函数实际上只能被某个对象调用 ②如果对象需要定义很多方法,那么需要定义很多全局函数,那么这个自定义的引用类型就毫无封装性可言了。这个问题可通过使用原型模式来解决。

     1 <script>
     2     //使用构造函数的主要问题是每个方法都要在每个实例上都要创建一遍。
     3     function Person(name, age, job){
     4         this.name = name;
     5         this.age = age;
     6         this.job = job;
     7         /*
     8         this.sayName = function(){
     9             console.log( this.name );
    10         }*/
    11         //下面与上面声明函数在逻辑上是等价的
    12         //ECMAScript 中的函数是对象,因此每定义一个函数也就是实例化了一个对象,
    13         this.sayName = new Function( "console.log(this.name)" );
    14     }
    15     //不同实例上的同名函数是不相等的
    16     var person1 = new Person();
    17     var person2 = new Person();
    18     console.log( person1.sayName == person2.sayName );  //输出:false
    19 
    20     //创建 2 个完成同样任务的 Function 实例没有必要,可以通过把函数定义转移到构造函数外部来解决这个问题。
    21     function Person2(name, age){
    22         this.name = name;
    23         this.age = age;
    24         //将 sayName 属性设置成等于全局的 sayName 函数
    25         this.sayName = sayName;
    26     }
    27     function sayName(){
    28         console.log( this.name );
    29     }
    30     var person1 = new Person2();
    31     var person2 = new Person2();
    32     //由于 sayName 包含的是一个指向函数的指针,因此 person1 和 person2 对象就共享了在全局作用域中定义的 sayName()函数
    33     console.log( person1.sayName == person2.sayName );   //输出:true
    34 </script>

    原型模式

     优点:可以让所有对象实例共享它所包含的属性和方法

     1 <script>
     2     function Person(){
     3     }
     4 
     5     /*
     6         创建的每个函数都有 1 个 prototype(原型)属性,这个属性是一个指针,指向一个对象,
     7     这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
     8         prototype 就是通过调用构造函数而创建的那个对象的实例的原型对象,使用原型对象可以让
     9     所有对象实例共享它所包含的属性和方法。
    10     */
    11     Person.prototype.name = "xiaoxu";
    12     Person.prototype.age = 22;
    13     Person.prototype.sayName = function(){
    14         console.log( this.name );
    15     };
    16 
    17     var person1 = new Person();
    18     person1.sayName();      //输出:xiaoxu
    19     var person2 = new Person();
    20     person2.sayName();      //输出:xiaoxu
    21     
    22     //将 sayName() 方法和所有属性直接添加到了 Person 的 prototype 属性中,构造函数变成了空函数。
    23     console.log( person1.sayName == person2.sayName );  //输出:true
    24 </script>

    理解原型对象

      以上面使用的 Person 构造函数和 Person.prototype 创建实例的代码为例, 下图展示了各个对象之间的关系。Person.prototype.constructor 指向 Person。

      在所有实现中都没有办法访问到 [[Prototype]],但可以通过 isPrototypeOf() 方法来确定对象之间是否存在这种关系。使用 Object.getPrototypeOf() 返回 [[Prototype]] 的值。

    1     //isPrototypeOf()
    2     //如果[[Prototype]]指向调用 isPrototype() 方法的对象(Person.prototype), 那么这个方法就返回 true。
    3     console.log( Person.prototype.isPrototypeOf( person1 ) );   //输出:true
    4     console.log( Person.prototype.isPrototypeOf( person2 ) );   //输出:true
    5     //Object.getPrototypeOf()
    6     console.log( Object.getPrototypeOf[person1] == Person.prototype );  //输出:false
    7     console.log( Object.getPrototypeOf( person1 ).name );               //输出: xiaoxu

      当代码读取某个对象的某个属性时,先从对象实例本身开始搜索给定名字的属性,如果找到则返回属性的值,如果找不到则在原型对象属性中查找给定名字的属性,如果找到则返回属性的值。比如:在调用 person1.sayName() 时,先查找实例 person1 有无 sayName 属性,若实例 person1 无, 则继续搜索 person1 的原型有无 sayName 属性。

     1 <script>
     2     function Person(){
     3     }
     4     Person.prototype.name = "xiaoxu";
     5     var person1 = new Person();
     6     var person2 = new Person();
     7     //可以通过对象实例访问保存在原型中的值,但是不能通过对象实例重写原型中的值。
     8     //在实例中添加一个属性,该属性与实例原型中的一个属性同名,那么实例的属性将会屏蔽原型中的属性
     9     person1.name = "zeng";
    10     console.log( person1.name );    //输出:zeng
    11     console.log( person2.name );    //输出:xiaoxu
    12     //使用 delete 操作符可以完全删除实例属性,删除之后可以重新访问原型中的属性
    13     delete person1.name;
    14     console.log( person1.name );    //输出:xiaoxu
    15 </script>

      使用 hasOwnProperty() 可以判断访问的是实例属性还是原型属性。

     1 <script>
     2     function Person(){
     3     }
     4     Person.prototype.name = "xiaoxu";
     5     var person1 = new Person();
     6     var person2 = new Person();
     7     person1.name = "zeng";
     8     //实例属性返回 true
     9     console.log( person1.hasOwnProperty("name"));   //输出:true
    10     //原型属性返回 false
    11     console.log( person2.hasOwnProperty("name"));   //输出:false
    12 </script>

    原型与 in 操作符

     1 <script>
     2     function Person(){
     3     }
     4     Person.prototype.name = "xiaoxu";
     5     Person.prototype.age = 22;
     6     Person.prototype.sayName = function(){
     7         console.log( this.name );
     8     };
     9     var person1 = new Person();
    10     var person2 = new Person();
    11     person1.name = "li";
    12 
    13 //单独使用 in 操作符
    14     //in 操作符只要通过对象能够访问到属性就返回 true,无论是实例属性还是原型属性
    15     //下面person1.name是实例属性,person2.name是原型属性
    16     console.log( "name" in person1 );               //输出:true
    17     console.log( person1.hasOwnProperty("name") );  //输出: true
    18     console.log( "name" in person2 );               //输出:true
    19     console.log( person2.hasOwnProperty("name") );  //输出: false
    20     //同时使用 hasOwnProperty()方法和 in操作符可以确定属性是存在对象中还是存在原型中
    21     function hasPrototypeProperty( object, name ){
    22         //为原型属性则返回 true, 否则返回 false
    23         return !object.hasOwnProperty(name) && (name in object);
    24     }
    25     console.log( hasPrototypeProperty( person1, "name" ) ); //输出:false
    26     console.log( hasPrototypeProperty( person2, "name" ) ); //输出:true
    27 
    28 //在 for-in 循环中使用 in 操作符
    29     //返回所有能够通过对象访问的、可枚举(enumerated)属性
    30     //所有开发人员定义的属性都是可枚举的
    31     //屏蔽了原型中不可枚举属性(即将 [[Enumerable]])标记为 false 属性)的实例属性也会在 for-in 循环中返回
    32     var o = {
    33         //原型的 toSrting() 方法的不可枚举属性[[Enumerable]]为false, 这里创建同名为toString()的实例属性,屏蔽了原型中的同名属性
    34         toString : function(){
    35             return "My Object";
    36         }
    37     };
    38     for( var prop in o ){
    39         if( prop == "toString" ){
    40             console.log( "Found toString" );
    41         }
    42     }   //输出:Found toString
    43     
    44     //Object.keys()
    45     //取得对象上所有可枚举的实例属性,返回一个数组
    46     var keys = Object.keys( Person.prototype);  //取得原型对象上所有可枚举的属性
    47     console.log( keys );        //输出:(3) ["name", "age", "sayName"]
    48     var person3 = new Person();
    49     person1.name = "dongxu";
    50     var person1Keys = Object.keys( person1 );   //取得实例对象person1上所有可枚举的属性
    51     console.log( person1Keys ); //输出:["name"]
    52 
    53     //Object.getOwnPropertyNames()
    54     //得到所有实例属性,无论其是否可枚举
    55     var keys = Object.getOwnPropertyNames( Person.prototype );
    56     console.log( keys );        //输出:(4) ["constructor", "name", "age", "sayName"]
    57     //注意:结果中包含不可枚举的 constructor 属性
    58 
    59     //Object.keys() 跟 Object.getOwnPropertyNames() 方法都可以用来替代 for-in 循环
    60 </script>

    更简单的原型语法

     1 <script>
     2     function Person(){
     3     }
     4     /*
     5     Person.prototype.name = "xiaoxu";
     6     Person.prototype.age = 22;
     7     Person.prototype.sayName = function(){
     8         console.log( this.name );
     9     };
    10     */
    11     //如上,每添加一个属性和方法都要敲一遍 Person.prototype, 为减少输入,可将 Person.prototype 设置为等于一个对象字面量形式的新对象
    12     Person.prototype = {
    13         name : "xiaoxu",
    14         age : 22,
    15         sayName : function(){
    16             console.log( this.name );
    17         }
    18     };
    19     //此时本质上完全重写了默认的 prototype 对象,因此 constructor 属性变成了新对象的 constructor 属性,constructor 属性不再指向 Person
    20     var person1 = new Person();
    21     console.log( person1 instanceof Object );   //输出:true
    22     console.log( person1 instanceof Person );   //输出:true
    23     console.log( person1.constructor instanceof Object );   //输出:true
    24     console.log( person1.constructor instanceof Person );   //输出:false
    25 </script>

    如果 constructor 的值很重要,可以特意将它设置回适当的值。

     1 <script>
     2     function Person(){
     3     }
     4     Person.prototype = {
     5         constructor : Person,
     6         name : "xiaoxu",
     7         age : 22,
     8         sayName : function(){
     9             console.log( this.name );
    10         }
    11     };
    12     //重设 constructor 属性会导致它的 [[Enumerable]]特性被设置为 true,默认情况下原生的constructor 属性是不可枚举的。
    13     //如果使用兼容 ECMAScript 5 的 JavaScript 引擎,可以用 Object.defineProperty() 重设 constructor
    14     Object.defineProperty( Person.prototype, "constructor",{
    15         enumerable : false,
    16         //重设 constructor 属性指向 prototype 所在函数的指针
    17         value : Person
    18     });
    19 </script>

    重设之前的 constructor

    重设之后的 constructor

     

    原型的动态性

     可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来。

    1 <script>
    2     function Person(){}
    3     var p1 = new Person();
    4     Person.prototype.sayHi = function(){
    5         console.log( "hi" );
    6     };
    7     p1.sayHi();     //输出:hi
    8 </script>

    但是,如果是重写整个原型对象,情况就不一样了,重写相当于将原型改为另一个对象就等于切断了构造函数与最初原型之间的联系。实例中的指针仅指向原型,而不指向构造函数。

     1 <script>
     2     function Person(){}
     3     var p1 = new Person();
     4     Person.prototype = {
     5         constructor : Person,
     6         name : "xiaoxu",
     7         sayName : function(){
     8             console.log( this.name );
     9         }
    10     }
    11     p1.sayName();   //报错 Uncaught TypeError: p1.sayName is not a function
    12 </script>

    重写原型对象之前:

     

     重写原型对象之后:

    原生对象的原型

     所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。

    1 <script>
    2     console.log( Array.prototype.sort );        //输出:ƒ sort() { [native code] }
    3     console.log( String.prototype.substring );  //输出:ƒ substring() { [native code] }
    4 </script>

    通过原生对象的原型,不仅可以取得所有默认方法的引用,而且可以定义新方法。还可以修改原生对象的原型。但是不建议在产品化的程序中修改原生对象的原型。

    1 <script>
    2     //给基本包装类型 String 添加一个名为 someFn() 的方法
    3     String.prototype.someFn = function( text ){
    4         return this+text;
    5     };
    6     var msg = "hello";
    7     console.log(msg.someFn("123")); //输出:hello123
    8 </script>

    原型对象的问题

     所有实例在默认情况下都取得相同的属性。原型中所有属性是被很多实例共享的。因此,很少有人单独使用原型模式。

     1 <script>
     2     function Person(){}
     3     Person.prototype = {
     4         constructor : Person,
     5         name : "xiaoxu",
     6         age : 22,
     7         friends : ["mona", "nana"],
     8         sayName : function(){
     9             console.log( this.name );
    10         }
    11     }
    12     var p1 = new Person();
    13     var p2 = new Person();
    14     //修改了 p1.friends 引用的数组之后 p2.friends 引用的数组也被修改了,因为实例共享一个数组
    15     p1.friends.push( "zeng" );
    16     console.log( p1.friends );  //输出:(3) ["mona", "nana", "zeng"]
    17     console.log( p1.friends );  //输出:(3) ["mona", "nana", "zeng"]
    18     console.log( p1.friends == p2.friends );    //输出:true
    19 </script>

    组合使用构造函数模式和原型模式

    是在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。这是用来定义引用类型的一种默认模式。

     1 <script>
     2     //构造函数用于定义实例属性
     3     function Person(name, age){
     4         this.name = name;
     5         this.friends = ["mona", "nana"];
     6     }
     7     //原型模式用于定义方法和共享的属性
     8     Person.prototype = {
     9         constructor : Person,
    10         sayName : function (){
    11             console.log( this.name );
    12         }
    13     }
    14     var p1 = new Person("xiaoxu", 22);
    15     var p2 = new Person("Sammi", 33);
    16     //数组friends是在构造函数中定义的,所以实例之间不共享数组friends, p1.friends跟p2.friends分别引用不同的数组。
    17     p1.friends.push("miko");
    18     console.log( p1.friends );  //输出:(3) ["mona", "nana", "miko"]
    19     console.log( p2.friends );  //输出:(2) ["mona", "nana"]
    20     console.log( p1.friends == p2.friends );    //输出:false
    21     //方法 sayName() 是用原型模式定义的,所以实例之间共享方法 sayName()
    22     console.log( p1.sayName == p2.sayName );    //输出:true
    23 </script>

    动态原型模式

    动态原型模式把所有信息都封装在构造函数中,通过在构造函数中初始化原型保持了同时使用构造函数和原型的优点。

     1 <script>
     2     function Person(name, age){
     3         this.name = name;
     4         this.age = age;
     5         //下面这段代码仅仅在初次调用构造函数时才会执行,此后原型已经完成初始化。
     6         if( typeof this.sayName != "function" ){
     7             //如果对原型进行修改,能够立即在所有实例中得到反映
     8             //注意:不能使用对象字面量重写原型,否则会切断现有实例与新原型之间的联系。
     9             Person.prototype.sayName = function(){
    10                 console.log( this.name );
    11             };
    12         }
    13     }
    14     var friend = new Person("xiaoxu", 22);
    15     //第一次调用 sayName() 时会初始化构造函数里的原型
    16     friend.sayName();   //输出:xiaoxu
    17 </script>

    寄生构造函数模式

    该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。返回的对象与构造函数外部创建的对象没有什么不同,所以在可以使用其他模式的情况下,不要使用这种模式。

     1 <script>
     2     function Person(name, age){
     3         //创建一个函数,该函数的作用仅仅是封装创建对象的代码
     4         var o = new Object();
     5         o.name = name;
     6         o.age = age;
     7         o.sayName = function(){
     8             console.log( this.name );
     9         };
    10         //返回新创建的对象
    11         return o;
    12     }
    13     var p1 = new Person( "xiaoxu", 22 );
    14     p1.sayName();   //输出:xiaoxu
    15 
    16     //这个模式可以在特殊的情况下为对象创建构造函数,如果想创建一个具有额外方法的特殊数组,由于不能直接修改 Array 构造函数,因此可以使用这个模式。
    17     //创建一个名为 NameArray 的构造函数
    18     function NameArray(){
    19         //创建数组
    20         var values = new Array();
    21         //添加值。用push()方法(用构造函数接收到的所有参数)初始化数组的值
    22         values.push.apply(values, arguments);
    23         //添加方法
    24         values.toPipedString = function(){
    25             return values.join('|');
    26         }
    27         //返回数组。以函数的形式返回数组
    28         return values;
    29     }
    30     var names = new NameArray('xiaoxu', 'mona');    
    31     console.log( names.toPipedString());    //输出:xiaoxu|mona
    32 </script>

    稳妥构造函数模式

     稳妥对象指的是没有公共属性,其方法也不引用 this 的对象。稳妥对象最适合放在一些安全的环境中(这些环境禁用 this 跟 new), 或者在防止数据被其他应用程序改动时使用。

     1 <script>
     2     function Person(name, age){
     3         //创建要返回的对象
     4         var o = new Object();
     5         
     6         //可以在这里定义私有变量和函数
     7         //添加方法
     8         o.sayName = function(){
     9             console.log( name );
    10         };
    11 
    12         //返回对象
    13         return o;
    14     }
    15 
    16     //变量 friend 中保存的是一个稳妥对象,除了调用 sayName() 方法外,没有别的方法可以访问传入够早函数中的原始数据。
    17     //稳妥构造函数与寄生构造函数有2点不同:
    18     //1. 新创建对象的实例方法不引用 this
    19     //2. 不使用 new 操作符调用构造函数
    20     var friend = Person( "xiaoxu", 22 );
    21     friend.sayName();   //输出:xiaoxu
    22 </script>
  • 相关阅读:
    缓存清理
    机器学习在电商领域三大应用,推荐,搜索,广告中商品排序
    并发和并行
    拷贝控制
    gitk
    git GUI Clients
    new delete
    Windows 安装 gcc
    C++ 运算符优先级
    iostream 操作符
  • 原文地址:https://www.cnblogs.com/xiaoxuStudy/p/12416106.html
Copyright © 2020-2023  润新知