一起来搞对象吧

540 阅读10分钟

什么是对象?

无序属性的集合, 其属性可以包含基本值、 对象或者函数

对象的属性类型

ECMA-262定义了一些为实现JavaScript引擎用的属性,因此不能直接访问,为了表示其是特殊的内部值,把他们放在两对儿方括号中。例如[[[Enumerable]]。对象的属性分为数据属性和访问器属性

数据属性

一般数据属性的值都为基础数据类型。

  • [[Configurable]] 表示能否通过delete删除属性从而从新定义…(默认为true PS:一旦修改,不能反悔)
  • [[Enumerable]] 表示能否通过for-in循环返回属性…(自定义属性默认为true,默认属性的默认值为false,例如:constructor )
  • [[Writable]] 表示能否修改属性值…(默认为true))
  • [[Value]] 包含属性的数据值…(就是上例中的‘Jiangwen’)
访问器属性
  • [[Configurable]] 表示能否通过delete删除属性从而从新定义…默认为true)
  • [[Enumerable]] 表示能否通过for-in循环返回属性…(默认为true)
  • [[get]]: 在读取属性时,调用的函数,默认undefined。
  • [[set]]: 在写入属性时,调用的函数,默认undefeated。

修改默认属性

ECMAScript给我提供了一个方法,Object.defineProperty()方法。这个方法接受三个参数:属性所在对象、属性的名字和一个描述对象。

var person = {};
Object.defineProperty(person,'name',{
    Writable:false,
    value:'jiangwen'
})
console.log(person.name) //'jiangwen'
person.name = 'xiaoming';
console.log(person.name) // 'jiangwen'

修改默认属性为不允许修改,因此重新进行赋值修改无效

定义多个对象属性

由于为对象定义多个属性的可能性很大,ECMAScript 5 又定义了一个 Object.definePro- perties()方法。利用这个方法可以通过描述符一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。

var people = {};
Object.defineProperties(book,{ //定义多个对象属性,用到Object.defineProperties();
    _year:{
        value:18
    },
    name:{
        value:'jiangwen'
    }
})

获取描述属性

ECMAScript也给我们提供了一个方法:Object.getOwnPropertyDescriptor()这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称;返回值是一个对象

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;             
      }        
    }     
  } 
}); 
var descriptor = Object.getOwnPropertyDescriptor(book,"_year");
console.log(descriptor.value) // 18
console.log(descriptor.enumerable);   //false

创建对象

1.基本方法

var person = new Object(); 
person.name = "JiangWen"; 
person.age = 29; 
person.job = "Software Engineer";  
person.sayName = function(){
  	alert(this.name);  
};

2.对象字面量方法

var person = {     
  name: "JiangWen",      
  age: 25,     
  job: "Software Engineer",      
  sayName: function(){         
    alert(this.name);     
  } 
}; 

虽然上面两种创建对象的方式,都可以用来创建单个对象,但是有个明显的缺点,使用同一接口创建很多对象,会产生大量的重复代码,因此就产生了不同模式满足不同情况下的需求

3.工厂模式

工厂模式是软件工程领域一种广为人知的设计模式,用函数来封装以特定接口创建对象的细节

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("Jiangwen", 29, "Software Engineer"); 
var person2 = createPerson("Owen", 27, "Doctor");

4.构造函数模式

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

调用构造函数实际上会经历以下 4 个步骤:

  1. 创建一个新对象
  2. 将构造函数的作用域赋值给新对象(因此 this 就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性方法)
  4. 返回新对象 (注:默认返回新创建的对象; 不过如果在构造函数中显式返回一个对象数据类型,那么将来new的对象就是该显式return的对象)

构造函数模式和工厂模式有以下几个不同之处

  • 没有显式地创建对象
  • 直接将属性方法赋值给this
  • 没有return语句

5.原型模式

我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

function Person(){ }  
Person.prototype.name = "Jiangwen"; 
Person.prototype.age = 25; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function(){     
  alert(this.name); 
};  
var person1 = new Person(); 
person1.sayName();   //"Jiangwen"  
var person2 = new Person();
person2.sayName();   //"Jiangwen"  
alert(person1.sayName == person2.sayName);  //true

与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。也就是说,person1 和 person2 访问的都是同一组属性和同一个 sayName()函数,如下图所示:

img

一些关于原型的方法:

// 确定实例对象和构造函数原型之间是否存在关系
Person.prototype.isPrototypeOf(person1) // true
person1 instanceof Person)       //true 

// 获取实例的原型对象
Object.getPrototypeOf(person1) == Person.prototype  // true
Object.getPrototypeOf(person1).name; //"Jiangwen"

// 检测一个属性是存在于实例中,还是存在于原型中
Person.prototype.name = "Owen"
var person1 = new Person()
person1.hasOwnProperty("name")  // false 来自原型 
person1.name = 'jiangwen'
person1.hasOwnProperty("name")  // true 来自实例

// 检测一个属性是存在于实例或者原型中,即该属性存在即可
"name" in person1  //true

// 取得当前对象上所有可枚举的实例属性
Object.keys(Person.prototype)  // "name,age,job,sayName"

// 取得当前对象上所有实例属性
Object.getOwnPropertyNames(Person.prototype); //"constructor,name,age,job,sayName"

6.组合构造函数模式和原型模式

该模式是创建自定义类型的最常见方式: 构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性

function Person(name, age, job){     
  this.name = name;     
  this.age = age;     
  this.job = job;     
  this.friends = ["Shelby", "Court"]; 
}  
Person.prototype = {     
  constructor : Person,     // 对象字面量的形式相当于重写了原型对象,所以需要重新指定
  sayName : function(){         
    alert(this.name);     
  } 
}

var person1 = new Person("Nicholas", 29, "Software Engineer"); 
var person2 = new Person("Greg", 27, "Doctor");  
person1.friends.push("Van"); 
alert(person1.friends);    //"Shelby,Count,Van" 
alert(person2.friends);    //"Shelby,Count" 
alert(person1.friends === person2.friends);    //false 
alert(person1.sayName === person2.sayName);    //true

7.动态原型模式

可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型,从而避免改写原型上原有的同名属性或方法

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);         
    };              
  }
}

该模式下只在 sayName()方法不存在于原型的情况下,才会将它添加到原型中

8.寄生构造函数模式(不建议使用)

该模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;

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"

构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。

9.稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用 this;二是不使用 new 操作符调用构造函数。

function Person(name, age, job){          
  //创建要返回的对象     
  var o = new Object();
  
  //可以在这里定义私有变量和函数      
  
  //添加方法     
  o.sayName = function(){         
    alert(name);     
  };              
  //返回对象     
  return o; 
}

注意, 在以这种模式创建的对象中, 除了使用 sayName()方法之外, 没有其他办法访问 name 的值

对象的继承

在Javascript中实现继承主要是依靠原型链来实现的;其基本思想是通过原型实现一个引用类型继承另一个引用类型的属性和方法。

1.原型链

function SuperType(){
  this.super = 'SuperType'
}
SuperType.prototype.sayType = function(){
  alert(this.super)
}
function SubType(){
  this.sub = 'SubType'
}
SubType.prototype = new SuperType()
let a1 = new SubType()

2.借用构造函数

子类型构造函数的内部调用超类型构造函数

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"

3.组合继承

将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

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; 
}  
//继承方法 
SubType.prototype = new SuperType(); 
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){     
  alert(this.age); 
}; 

// 创建实例对象
var instance1 = new SubType("Nicholas", 29); 
instance1.colors.push("black"); 
alert(instance1.colors);      //"red,blue,green,black" 
instance1.sayName();          //"Nicholas"; 
instance1.sayAge();      			//29

var instance2 = new SubType("Greg", 27); 
alert(instance2.colors);      //"red,blue,green" 
instance2.sayName();          //"Greg"; 
instance2.sayAge();           //27

所以组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。

4.原型式继承

将需要被继承的对象挂载到函数内部新创建的构造函数原型上并返回该构造函数的实例

/* 模拟Object.create()方法
*  o: 需要被继承的对象
*  o2: 自定义参数对象,默认为空
*/
function object(o,o2={}){
   // 创建一个构造函数F为接收自定义参数
  var F = function(){
    for (const key in o2) {
      for (const key2 in o2[key]) {
        this[key] = o2[key][key2]//跳坑指南: 不能使用this.key,因为点赋值会将变量key转为字符串“key” 
      }
    }
  };
  F.prototype = o   // 重写构造函数F原型对象为传入的对象o
  return new F()  //返回构造函数的实例
}

var person = {
  name: 'no-Name',
  friend:[1,2,3],
  sayName:function(){
    alert(this.name)
  }
}
let a1 = object(person)

ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象

// 在传入一个参数的情况下,Object.create()与 object()方法的行为相同
 var person = {     
   name: "Nicholas",     
   friends: ["Shelby", "Court", "Van"] 
 };  
var anotherPerson = Object.create(person); 
anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob");      
var yetAnotherPerson = Object.create(person); 
yetAnotherPerson.name = "Linda"; 
yetAnotherPerson.friends.push("Barbie");  
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"

5.寄生式继承

寄生式(parasitic)继承是与原型式继承紧密相关的一种思路 ,其与工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象

function createAnother(original){
  var clone = object(original)  //创建对象
  clone.sayHi = function(){			
    alert('hi!')
  }
  return clone
}
var person = {     
  name: "Jiangwen",     
  friends: ["A", "B", "C"] 
};  
var anotherPerson = createAnother(person); 
anotherPerson.sayHi(); //"hi"

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示范继承模式时使用的 object()函数不是必需的;任何能够返回新对象的函数都适用于此模式。

6.寄生组式继承

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法 ; 本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

function inheritPrototype(subType, superType){     
  var prototype = object(superType.prototype);     //创建对象     
	prototype.constructor = subType;                 //增强对象     
  subType.prototype = prototype;               //指定对象 
}