#JavaScript学习#第六章:面向对象的程序设计

133 阅读12分钟

Tips: 内容为知识梳理


目录

  1. 理解对象

  2. 创建对象

  3. 继承


1. 理解对象

对象中创建的属性都带有一些特征值,描述了属性的各种特征,用[[]]括起。

1.1 属性类型

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

  1. 数据属性 数据属性包含一个数据值的位置,这个位置可以读取和写入值,拥有4个特性(特征值)
  • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,能否把属性改为访问器属性,默认值为true
  • [[Enumerable]]:表示能否通过 for-in 循环返回属性,默认为true
  • [[Writable]]:表示能否修改属性的值,默认为true
  • [[Value]]:其值为这个属性的数据值,读取和写入数据值都是从这个位置开始,默认为undefined,假如name:"john",那么 [[Value]]特性将被设置为"john"

若要修改属性默认特性,如下操作即可:

Object.defineProperty(person, "name",{    
  writable: false,    
  value: "Nicholas"  
  }); 

这个方法接受三个参数,属性所在对象,属性名,和一个描述对象,即最后用花括号括起的特征值,如此操作后,name就会被改为Nicholas,并且不可被修改(writable被设置成false)。用这个方法也可以更改其他的几个特性

  1. 访问器属性 访问器属性无数据值,但有一对函数getter和setter(不是必须的),在读取访问器属性时先调用getter,返回有效值,在写入访问器属性时,调用setter,决定如何处理数据,访问器属性有如下四个特性
  • [[Configurable]]:与数据属性的一样
  • [[Enumerable]]:与数据属性一样
  • [[Get]]:在读取属性时调用的函数。默认值为 undefined。
  • [[Set]]:在写入属性时调用的函数。默认值为 undefined。 修改这些特征值也是和数据属性的方式一样,用Object.defineProperty方法,传入参数一样
var book = {    
    _year: 2004,    
    edition: 1 
  }; 
Object.defineProperty(book, "year", {     
get: function(){        
 return this._year;    
  },     
  set: function(newValue){ 
        if (newValue > 2004) {            
         this._year = newValue;             
         this.edition += newValue - 2004;        
          }     
          } 
          }); 
book.year = 2005; alert(book.edition);  //2 

year前面的下划线是一种特殊记号,表示只能通过对象方法访问属性,而year则是一个访问器属性。

1.2 定义多个属性

用Object.defineProperties()方法一次定义多个属性

	var book = {}; 
Object.defineProperties(book, {    
 _year: {        
  value: 2004     
  },          
  edition: {      
     value: 1    
      }, 
    year: {         
    get: function(){ 
     return this._year;        
      }, 
        set: function(newValue){            
         if (newValue > 2004) {               
           this._year = newValue;          
                  this.edition += newValue - 2004;            
                   }       
                     }     } }); 

如上所示,方法接受两个参数,第一个是要添加属性的对象,第二个参数是包含要修改属性的特征值的对象

1.3 读取属性的特性

Object.getOwnPropertyDescriptor()方法可以取得给定属性的描述符对象,该方法返回一个对象,接受两个参数,属性所在对象和属性名,如下所示

var descriptor = Object.getOwnPropertyDescriptor(book, "_year");

这样就可以用descriptor去访问这个属性的特征值了如descriptor.value等

2. 创建对象

2.1 工厂模式

用函数来封装以特定接口创建对象的细节,如下所示

function createPerson(name, age, job){     
var o = new Object();    
 o.name = name;     
 o.age = age;     
 o.job = job;     
 o.sayName = function(){        
  alert(this.name);    
   };         
   return o;
    } 
var person1 = createPerson("Nicholas", 29, "Software Engineer");
 var person2 = createPerson("Greg", 27, "Doctor");

函数createPerson返回一个对象,这样做可以解决创建多个相似对象的问题。

2.2 构造函数模式

可以创建自定义的构造函数像object,array这样。下面是创建构造函数的一个例子:

function Person(name, age, job){    
 this.name = name;    
  this.age = age;   
    this.job = job;    
     this.sayName = function(){        
      alert(this.name);     
      };     
      } 
var person1 = new Person("Nicholas", 29, "Software Engineer"); 
var person2 = new Person("Greg", 27, "Doctor"); 

构造函数与之前工厂模式的函数有所不同

  • 没有显示创建对象
  • 属性和方法赋值给this
  • 没return

此外按照惯例构造函数始终以一个大写字母开头,而非构造函数以小写字母开头。 用new操作符创建新实例会经过以下几个过程:

  • 创建一个新对象
  • 将构造函数作用域赋给新对象
  • 执行构造函数代码
  • 返回新对象

注意:

  • 新的实例同时会是自定义构造函数实例和object实例
  • 构造函数也可以当做普通函数来使用,只要不通过new来调用
  • 创建两个完成相同任务的Function实例没有必要,可以把其中的方法如(sayname)移到构造函数外部,但如果方法太多都移到全局环境下,就完全无封装性可言了

2.3 原型模式

每个函数都会有一个属性: prototype,它指向一个原型对象,这个对象拥有所有实例共享的属性和方法,即通过设置这个对象的属性,那么所有通过函数创建的实例都会有相同的这个属性,如下代码所示:

	function Person(){ } 
Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){     alert(this.name); }; 
var person1 = new Person(); 
person1.sayName();   //"Nicholas" 
var person2 = new Person();
person2.sayName();   //"Nicholas" 
  1. 理解原型对象
  • 原型对象有一个 constructor 属性,这个属性指向所在的构造函数
  • 原型对象有一个 isPrototypeOf()方法,来确定对象之间是否存在这种关系

alert(Person.prototype.isPrototypeOf(person1)); //true

  • 用Object.getPrototypeOf()返回实例的原型对象

alert(Object.getPrototypeOf(person1) == Person.prototype); //true

  • 代码读取每个实例对象属性时,会进行搜索,先从对象本身搜索,没有的话再从原型对象搜索,即如果实例对象有跟原型对象重名的属性,那么原型对象的属性就会被屏蔽
  • 用实例对象的hasOwnProperty()方法检测一个属性是实例属性还是原型对象的属性,当且仅当它存在于实例属性则返回true
  1. 原型与in操作符 有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用
  • 单独使用时,in会在通过对象能访问给定属性时返回true,如

alert("name" in person1); //true

  • 同时使用hasOwnProperty()方法和in操作符来判断该属性存于对象中还是原型对象中

return !object.hasOwnProperty(name) && (name in object); 当属性存于原型对象时返回true

  • 用for-in循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中 既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将 [[Enumerable]]标记为 false 的属性)的实例属性也会在 for-in 循环中返回
  • 用Object.keys()方法返回一个对象上所有可枚举属性,接受一个对象参数,返回一个包含这些属性的数组
  • 用 Object.getOwnPropertyNames() 方法返回一个对象的所有属性
  1. 更简单的原型语法
  • 用对象字面量法写原型对象的属性和方法 Person.prototype = { name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } 不过这样做后,constructor 属性不再指向Person,而是指向object构造函数,若在上面的例子中修改constructor的值使其指向Person,那么它的[[Enumerable]]特性将会被设置成true即可枚举
  1. 原型的动态性
  • 我们对原型对象所作的任何修改都会立刻在实例上反应出来,但如果重写原型对象即用对象字面量法来写原型对象那么就不会在实例中反映出来因为重写原型对象会切断新原型对象与实例的所有联系
  1. 原生对象的原型
  • 所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法
  • 可以像修改自定义函数的原型对象一样修改原生引用类型的原型对象,添加新方法等等
  1. 原型对象的问题
  • 省略了为构造函数传递初始化参数这一环节
  • 修改一个原型对象的属性会影响到所有实例

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

创建自定义类型最常见的方法就是组合使用构造函数和原型模式,构造函数用于定义实例属性,原型对象用于定义方法和共享属性

2.5 动态原型模式

在构造函数中初始化原型而非独立的修改原型,如下所示:

function Person(name, age, job){ 
    //属性     
    this.name = name;    
     this.age = age;    
      this.job = job; 
     //方法     
     if (typeof this.sayName != "function"){              
     Person.prototype.sayName = function(){             
     alert(this.name);         
     };             
      }
       } 
var friend = new Person("Nicholas", 29, "Software Engineer"); 
friend.sayName(); 

其中if语句检查sayName方法是否存在,若不存在则添加

####

2.6 寄生构造函数模式
创建一个函数,该函数作用仅仅是封装创建对象的代码,返回新创建的对象,下面是一个例子:

	function Person(name, age, job){    
	 var o = new Object();    
	  o.name = name;     
	  o.age = age;     
	  o.job = job;     
	  o.sayName = function(){        
	   alert(this.name);     
	   };         
	   return o; 
	   } 
var friend = new Person("Nicholas", 29, "Software Engineer"); 
friend.sayName();  //"Nicholas" 

看起来和工厂模式一模一样,有两点不同:名字用构造函数的名字和使用new操作符。 在可以使用其他模式的·情况下不要用这种模式

2.7 稳妥构造函数模式

稳妥对象无公共属性,不引用其方法this的对象,适合在一些安全的环境中,它与寄生构造函数模式类似,两点不同:新创建对象的实例方法不引用this,不用new操作符调用构造函数,重写如下:

	function Person(name, age, job){ 
	 var o = new Object(); 
	 o.sayName = function(){         
	 alert(name);     
	 }; 
	  return o;
	   }
	  var friend = Person("Nicholas", 29, "Software Engineer");
	   friend.sayName();  //"Nicholas" 
   这样,变量 friend 中保存的是一个稳妥对象,而除了调用 sayName()方法外,没有别的方式可 以访问其数据成员

3. 继承

许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承继承实际的方法。在ECMAscript中只支持实现继承,其主要依靠原型链来实现。

3.1 原型链

让一个构造函数的原型对象充当另一个类型的实例,这样就可以构成原型链,代码如下: SubType.prototype = new SuperType();

这样,再用SubType构造函数创建的实例对象,就可以继承SuperType的所有属性和方法了,就实现了继承。具体如图:

这样做也会扩展搜索模式,一层一层向上搜索。

  1. 别忘记默认的原型 所有引用类型都继承了Object,这个继承也是通过原型链实现的。
  2. 确定原型和实例的关系 有两种方式确定实例和原型的方式
  • instanceOf:只要是在原型链中出现过的构造函数就会返回true
  • isPrototypeOf():Object.prototype.isPrototypeOf(instance)也是返回true
  1. 谨慎地定义方法
  • 添加方法的代码必须放在父本的实例替换原型之后
  • 当用子类的实例调用重写的方法时,会调用重新定义的方法
  • 当用父类实例调用重写的方法时,会用原本的方法
  • 不要用字面量创建原型方法,因为这样会破坏原型链
  1. 原型链的问题
  • 所有实例都共享原型对象的属性
  • 在创建子类型实例时不能向超类型的构造函数中传递参数

3.2 借用构造函数

基本思想:在子类中调用超类构造函数,比如用call和apply方法,代码如下:

	function SuperType(){    
	 this.colors = ["red", "blue", "green"];
	  } 
function SubType(){      
 //继承了 SuperType    
  SuperType.call(this); 
  } 
 var instance1 = new SubType(); 
 instance1.colors.push("black"); 
 alert(instance1.colors);    //"red,blue,green,black" 
 var instance2 = new SubType(); 
 alert(instance2.colors);    //"red,blue,green" 

子类subtype用call方法将supertype放在了subtype中解析,结果每个subtype实例都有自己的colors属性了

  1. 传递参数 借用构造函数可以向超类中传递参数

SuperType.call(this, "Nicholas");

  1. 借用构造函数的问题
  • 方法都在构造函数中定义,函数复用无从谈起
  • 在超类原型定义的方法对子类也不可见
  • 借用构造函数模式很少见

3.3 组合继承

指的是将原型链和构造函数技术组合到一块,思路是用原型链实现对原型的属性和方法的继承而用构造函数实现对实例属性的继承,这样能保证函数的复用又能保证每个实例有自己的属性,此处不举代码(其实就是将前两节代码融合为一体),这种方法是js中最常见的继承模式

3.4 原型式继承

借助原型基于已有的对象创建新对象

	function object(o){    
	 function F(){}    
	  F.prototype = o;    
	   return new F();
	    }//以上是原理
	    //以下是例子
	    var person = {    
	     name: "Nicholas",    
	      friends: ["Shelby", "Court", "Van"]
	       }; 
 var anotherPerson = object(person); 
 anotherPerson.name = "Greg"; 
 anotherPerson.friends.push("Rob"); 
 var yetAnotherPerson = object(person); 
 yetAnotherPerson.name = "Linda"; 
 yetAnotherPerson.friends.push("Barbie"); 
 alert(person.friends);   //"Shelby,Court,Van,Rob,Barbie" 

person作为新对象的原型,其属性被另外两个实例共享

用object.create()方法也可以达到这个效果,这个方法接受两个参数,原有对象(person)和描述符对象,代码如下

	var anotherPerson = Object.create(person, {     
	name: {         
	value: "Greg"    
	 }
	  }
	  );

这里的name会覆盖掉person原有的name

3.5 寄生式继承

寄生式继承思路是创建一个仅用于封装继承过程的函数,该函数内部以某种方式增强对象,然后返回对象,代码如下:

	function createAnother(original){    
	 var clone = object(original);  //通过调用函数创建一个新对象    
	  clone.sayHi = function(){     
	   //以某种方式来增强这个对象        
	    alert("hi");    
	     };    
	      return clone;         //返回这个对象
	       }

3.6 寄生组合式继承

组合式继承有一个缺点即无论在什么情况下都会调用两次超类构造函数,造成两组同名属性,用寄生组合式继承来解决这个问题。 通过借用构造函数来继承属性,通过原型链的混成形式来继承方法,其背后思路是:就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型 的原型。代码如下:

	function inheritPrototype(subType, superType){     
	var prototype = object(superType.prototype);     //创建对象     prototype.constructor = subType;              //增强对象    
	 subType.prototype = prototype;               //指定对象
	  } 
	  function SuperType(name){     
	  this.name = name;    
	   this.colors = ["red", "blue", "green"]; 
	   } 
 SuperType.prototype.sayName = function(){    
  alert(this.name); 
  }; 
 function SubType(name, age){       
 SuperType.call(this, name);         
  this.age = age; 
  } 
 inheritPrototype(SubType, SuperType); 
 SubType.prototype.sayAge = function(){     
 alert(this.age); 
 };