学习第三天(2019-11-16)

第六章 面向对象的程序设计

ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”可以把这里的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。 

一·、对象的属性类型

    ECMAScript中有两种属性:数据属性和访问器属性。

    a:数据属性

        数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性。

         [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性,默认值为 true。

         [[Enumerable]]:表示能否通过 for-in 循环返回属性,默认值为 true。

         [[Writable]]:表示能否修改属性的值,默认值为 true。 

         [[Value]]:包含这个属性的数据值,默认值为 undefined。 

     要修改属性默认的特性,可以使用 ECMAScript 5的Object.defineProperty()方法。这个方法 接收三个参数:属性所在的对象、属性的名字和一个描述符对象。

   b:访问器属性

        访问器属性不包含数据值,它们包含一对儿 getter和 setter函数(这两个函数不是必须的)。在读取访问器属性时,会调用 getter函数,这个函数负责返回有效的值;

        在写入访问器属性时,会调用 setter函数并传入新值,这个函数负责决定如何处理数据。 

        访问器属性有如下 4个特性:

        [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特 性,或者能否把属性修改为访问器属性,默认值为 true。

        [[Enumerable]]:表示能否通过 for-in 循环返回属性,默认值为 true。

        [[Get]]:在读取属性时调用的函数。默认值为 undefined。

        [[Set]]:在写入属性时调用的函数。默认值为 undefined。 

        访问器属性不能直接定义,必须使用 Object.defineProperty()来定义。如下:

 1 var book = { 
 2     _year: 2004,   
 3    edition: 1
 4 }; 
 5  
 6 Object.defineProperty(book, "year", {    
 7     get: function(){        
 8        return this._year;     
 9     }, 
10     set: function(newValue){ 
11        if (newValue > 2004) {    
12           this._year = newValue;   
13           this.edition += newValue - 2004;         
14        }     
15     } 
16 }); 
17  
18 book.year = 2005; 
19 alert(book.edition);  //2 

以上代码创建了一个 book 对象,并给它定义两个默认的属性:_year 和 edition。_year 前面的下划线是一种常用的记号,用于表示只能通过对象方法访问的属性。而访问器属性 year 则包含一个 getter函数和一个 setter函数。getter函数返回_year 的值,setter函数通过计算来确定正确的版本。因此,把 year 属性修改为 2005会导致_year 变成 2005,而 edition 变为 2。这是使用访问器属性的常见方式,即设置一个属性的值会导致其他属性发生变化。 

思考一下,在函数中defineProperty中为什么指定属性为year而不是_year,这里有一个坑,参考:https://blog.csdn.net/weixin_34050005/article/details/85936466

使用 ECMAScript 5的Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。

 

二、创建对象

     ECMAScript支持面向对象(OO)编程,但不使用类或者接口。对象可以在代码执行过程中创建和增强,因此具有动态性而非严格定义的实体。在没有类的情况下,可以采用下列模式创建对象。

  a:工厂模式,使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。这个模式后来被构造函数模式所取代。

  b:构造函数模式,可以创建自定义引用类型,可以像创建内置对象实例一样使用 new 操作符。不过,构造函数模式也有缺点,即它的每个成员都无法得到复用,包括函数。       由于函数可以不局限于任何对象(即与对象具有松散耦合的特点),因此没有理由不在多个对象间共享函数。

  c:原型模式,使用构造函数的 prototype 属性来指定那些应该共享的属性和方法。(重点理解原型的概念,实例与原型的关系)

  d:组合使用构造函数模式和原型模式时,使用构造函数定义实例属性,而使用原型定义共享的属性和方法,结合了两者的优点。

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

 1 function Person(name, age, job){     
 2   this.name = name;     
 3   this.age = age;     
 4   this.job = job;     
 5   this.friends = ["Shelby", "Court"]; 
 6 } 
 7  
 8 Person.prototype = {     
 9   constructor : Person,
10   sayName : function(){         
11     alert(this.name);     
12   } 
13 } 
14  
15 var person1 = new Person("Nicholas", 29, "Software Engineer"); 
16 var person2 = new Person("Greg", 27, "Doctor"); 
17  
18 person1.friends.push("Van");
19 alert(person1.friends);    //"Shelby,Count,Van" 
20 alert(person2.friends);    //"Shelby,Count" 
21 alert(person1.friends === person2.friends);    //false 
22 alert(person1.sayName === person2.sayName);    //true 
//在这个例子中,实例属性都是在构造函数中定义的,
//而由所有实例共享的属性 constructor 和方法 sayName()则是在原型中定义的。
//而修改了 person1.friends(向其中添加一个新字符串),
//并不会影响到 person2.friends,因为它们分别引用了不同的数组。

这种构造函数与原型混成的模式,是目前在 ECMAScript中使用广泛、认同度高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。

 e:动态原型模式:它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。

  代码:

 1 function Person(name, age, job){ 
 2     //属性     
 3     this.name = name;     
 4     this.age = age;     
 5     this.job = job;   
 6 
 7     //方法     
 8     if (typeof this.sayName != "function"){ 
 9         //该函数不存在时,添加到原型中  
//这里对原型所做的修改,能够立即在所有实例中得到反映
10 Person.prototype.sayName = function(){ 11 alert(this.name); 12 }; 13 } 14 } 15

这种方法也算是非常完美,其中,if 语句检查的可以是初始化之后应该存在的任何属性或方法——不必用一大堆 if 语句检查每个属性和每个方法;只要检查其中一个即可。对于采用这种模式创建的对象,还可以使用 instanceof 操作符确定它的类型。 

 注意:使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。(这里需要理解原型的原理、以及原型和对象的关系)

 f:其他方法还有:寄生构造函数模式 、稳妥构造函数模式 

 

三、继承

JavaScript 主要通过原型链实现继承,重点理解原型链的原理

1、原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样,子类型就能够访问超类型的所有属性和方法,这一点与基于类的继承很相似。

原型链的问题:  对于包含引用值的原型,所有继承的是一个引用,所以实例操作的是同一个对象。(主要问题) 

代码:

 1 function SuperType(){     
 2     this.colors = ["red", "blue", "green"]; 
 3 } 
 4  
 5 function SubType(){  } 
 6 //继承了 SuperType 
 7 SubType.prototype = new SuperType(); 
 8  
 9 var instance1 = new SubType(); 
10 instance1.colors.push("black"); 
11 alert(instance1.colors);    //"red,blue,green,black" 
12  
13 var instance2 = new SubType(); 
14 alert(instance2.colors);    //"red,blue,green,black" 

          还有一个问题就是,在创建子类型的实例时,不能像超类型的构造函数中传递函数。

                     

2、解决这个问题的技术是借用构造函数来实现继承,即在子类型构造函数的内部调用超类型构造函数。这样就可以做到每个实例都具有自己的属性,同时还能保证只使用构造函数模式来定义类型。

      存在的问题:在超类中定义的方法,对于子类型而言是不可见的。(如果借用的构造函数中定义了方法,方法就会重复定义)

 

3、使用较多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。

  组合继承大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

    能解决这一问题的是寄生组合式继承 : 

   代码:

 1 function object(o){ 
 2   function F(){}
 3   F.prototype = o;
 4   return new F();
 5 }
 6 //object()对传入其中的对象执行了一次浅复制
 7 
 8 function inheritPrototype(subType, superType){
 9     var prototype = object(superType.prototype);//创建对象    
10     prototype.constructor = subType;//增强对象
11     subType.prototype = prototype; //指定对象 
12 } 
13 
14 function SuperType(name){
15     this.name = name;
16     this.colors = ["red", "blue", "green"]; 
17 } 
18  
19 SuperType.prototype.sayName = function(){
20     alert(this.name); 
21 }; 
22  
23 function SubType(name, age){
24      SuperType.call(this, name);
25      this.age = age; 
26 } 
27  
28 inheritPrototype(SubType, SuperType); 
29  
30 SubType.prototype.sayAge = function(){
31      alert(this.age); 
32 }; 
33 
34 var s1 = new SubType("xiao",12);
35 s1.sayAge(); //12
36 s1.sayName(); //xiao
37 var s2 = new SubType("lan",11);
38 s2.sayAge(); //11
39 s2.sayName(); //lan

相比组合继承这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType. prototype上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf()。开发人员普遍认为寄生组合式继承是引用类型理想的继承范式。

4、

还有以下继承方式:

 原型式继承,可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制。而复制得到的副本还可以得到进一步改造。

 寄生式继承,与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,后返回对象。

 

第七章 函数表达式

1、函数表达式不同于函数声明。函数声明要求有名字,但函数表达式不需要。没有名字的函数表达式也叫做匿名函数。

2、 递归函数应该始终使用 arguments.callee 来递归地调用自身,不要使用函数名——函数名可能会发生变化。

3、当在函数内部定义了其他函数时,就创建了闭包。闭包有权访问包含函数内部的所有变量,

       在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域。
       通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。

4、使用闭包可以在JavaScript中模仿块级作用域

       代码:

 1 (function(){
 2      //这里是块级作用域 
 3 })(); 
 4 
 5 //注意如果写成下面这样会出错
 6 function(){
 7      //这里是块级作用域 
 8 }();    //出错!
 9 
10 //因为JavaScript将function关键字当作一个函数声明的开始,而函数声明后面不能跟圆括号。
//然而,函数表达式的后面可以跟圆括号。要将函数声明转换成函数表达式, 给它加上一对圆括号即可

 

5、私有变量

     JavaScript中没有正式的私有对象属性的概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问在包含作用域中定义的变量。

     有权访问私有变量的公有方法叫做特权方法。

     可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。

   使用构造函数实现特权方法:

 1 function MyObject(){ 
 2     //私有变量和私有函数     
 3     var privateVariable = 10; 
 4     function privateFunction(){         return false;     } 
 5  
 6     //特权方法 
 7     this.publicMethod = function (){ 
 8          privateVariable++;   
 9          alert(privateVariable);   //11 
10          return privateFunction(); 
11     }; 
12 } 
13 var ob = new MyObject();
14 alert(ob.privateVariable); //undefined  利用私有和特权成员可以,隐藏那些不应该被直接修改的数据
15 ob.publicMethod(); // 11

  静态私有变量:

   上述代码中每一个实例都会创建自己的特权方法和私有变量和函数,静态私有变量会被所有属性共享

 1 (function(){          
 2    //私有变量和私有函数 
 3    var name = "";
 4    //初始化未声明的变量,总是会创建一个全局变量
 5    Person = function(value){
 6        name = value;
 7    };
 8    Person.prototype.getName = function(){
 9        return name;
10    };
11    Person.prototype.setName = function(value){
12        name = value;
13    };
14 })();
15 var person1 = new Person("Nicholas");
16 alert(person1.getName());  //"Nicholas" 
17 person1.setName("Greg"); 
18 alert(person1.getName());  //"Greg" 
19  
20 var person2 = new Person("Michael"); 
21 alert(person1.getName()); //"Michael" 
22 alert(person2.getName()); //"Michael" 共享了私有变量name

模块模式、增强的模块模式:为单例创建私有变量和特权方法,单例是指只有一个实例的对象,通常都作为全局对象存在。

 

JavaScript 中的函数表达式和闭包都是极其有用的特性,利用它们可以实现很多功能。不过,因为创建闭包必须维护额外的作用域,过度使用它们可能会占用大量内存,建议少使用闭包。

posted @ 2019-11-16 22:40  xiongbing  阅读(164)  评论(0)    收藏  举报