面向对象

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-07-29

属性类型

ECMAScript的对象中有两种属性:数据属性访问器属性

数据属性

数据属性有 4 个描述其行为的特性。

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

  • [[Enumerable]]:表示能否通过for-in 循环返回属性,即是否可枚举,默认值为true

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

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

要修改属性默认的特性,必须使用Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符对象的属性必须是:configurableenumerablewritablevalue。设置其中的一或多个值,可以修改对应的特性值。

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

// 设置writable: false则不可写
person.name = "Greg";
alert(person.name); //"Nicholas" 

需要注意的是: writable默认值都是true,但是在调用Object.defineProperty()方法时,这个特性的值却默认为**false**

访问器属性

在读取访问器属性时,会调用getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter 函数并传入新值,这个函数负责决定如何处理数据。

不一定非要同时指定gettersetter。只指定getter 意味着属性是不能写,只指定setter 函数的属性不能读

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

  • [[Configurable]]:与数据属性一样,默认值为true

  • [[Enumerable]]:与数据属性一样,默认值为true

  • [[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

读取属性的特性

Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有configurableenumerablegetset;如果是数据属性,这个对象的属性有configurableenumerablewritablevalue。例如:

var book = { _year:2004 }
var descriptor =Object.getOwnPropertyDescriptor(book, "_year");
alert(descriptor.value); //2004

遍历对象属性的方法

  • for...in循环遍历对象自身的和继承的可枚举属性。对象原型的toString方法,以及数组的length属性,就通过“可枚举性”,从而避免被for...in遍历到。

  • Object.keys()返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in循环,而用Object.keys()代替。

  • 如果你想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法。注:在ES6中不包含Symbol属性。

创建对象

工厂模式

对象的封装,避免构建对象时大量的重复代码。

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

每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。即person1和person2都是Object构造出来的。

构造函数模式

构造函数意味着将来可以将它的实例标识为一种特定的类型;

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

// person1 和 person2 分别保存着Person 的一个不同的实例。
// 这两个对象都有一个constructor(构造函数)属性,该属性指向Person。
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true

要创建Person 的新实例,必须使用new 操作符。以这种方式调用构造函数实际上会经历以下4个步骤:

(1) 创建一个新对象;

(2) 将构造函数的作用域赋给新对象(因此this 就指向了这个新对象);

(3) 执行构造函数中的代码(为这个新对象添加属性);

(4) 返回新对象。

与工厂模式的不同之处:

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

任何函数,只要通过new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new 操作符来调用,那它跟普通函数也不会有什么两样。

优化构造函数(针对不被new调用的时候)

var Book = function (title, price) {
  if (this instanceof Book) {
    this.title = title;
    this.price = price;
  } else {
    return new Book(title, price)
  }
}

var book = Book('JavaScript', 40);
var book2 = new Book('JavaScript', 40);
// 效果一样

构造函数的问题

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在创建多个实例的时候创建多个同样任务的的Function 实例的确没有必要;因此,大可像下面这样,把函数定义转移到原型对象中来解决这个问题。

function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
}

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

原型对象

使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。

关于原型对象:

1、每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象(即原型对象)。

2、每一个原型对象,都有两个默认的属性,constructor属性和__proto__属性,constructor属性指向原构造函数,__proto__属性指向该原型对象的原型(每一个对象都有原型,原型对象也有原型)

3、__proto__属性指向的是构造该对象的构造函数的原型对象。__proto__属性即是查找原型链的指针或者说是方式

4、实例的constructor 属性是指向构造函数的,但不是直接关系,该属性是从原型上继承而来的。 __proto__这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。换句话说,实例与构造函数没有直接的关系。

function Person (name,age) {
  this.name = name;
  this.age = age;
  this.sayName= function(){
    alert(this.name)
  }
}
var p1 = new Person();
console.log(p1.constructor === Person) // true
console.log(p1.__proto__ === Person.prototype) // true
console.log(Person.prototype.constructor === Person) // true
console.log(p1.constructor === Person.prototype.constructor)  // true

console.log(Person.prototype.__proto__ === Object.prototype)  // true
console.log( Object.prototype == Object)  // false
console.log(Person.__proto__ === Function.prototype) // true
console.log(Function.prototype.__proto__ === Object.prototype) // true

值得注意的是:

  • 原型对象.__proto__ === Object.prototype 。原型对象也是Object对象构造出来的。

  • Object.prototype是它自身Object.prototype

  • Object.prototype.__proto__ === null

  • 构造函数的__proto__属性指向Function的原型对象,而构造函数.__proto__ === Fuction.prototypeFuction.prototype.__proto__ === Object.prototype

原型链查找

当我们在使用一个对象的属性和方法时,查找的顺序如下:

自身 -> 原型对象 -> 原型对象的原型对象 -> Object.Prototype(停止,因为Object的原型对象的原型是null

对象属性和方法的检查

1、使用in 操作符,检查对象的原型链中有没有某个属性,返回一个布尔值,console.log("name" in Person)

2、使用hasOwnProperty(),检查对象自身有没有某个属性,返回一个布尔值,Person.hasOwnProperty("name")

结合前两个方法就能写出封装方法,确定该属性到底是存在于对象中,还是存在于原型中。

function hasPrototypeProperty(object, name){
  return !object.hasOwnProperty(name) && (name in object); 
  // 只有自身没有,原型链上有才会返回true。
}

检查原型链的方法

  • isPrototypeOf()用来检查传入的对象的__proto__指针是否指向该方法的调用者,返回布尔值。例如:Person.prototype.isPrototypeOf(person1)

  • Object.getPrototypeOf()这个方法可以用来获取到传入对象的__proto__所指向的那个原型对象,因此也可以用来做检查,例如:alert(Object.getPrototypeOf(person1) == Person.prototype)

  • instanceof,这是一个操作符,用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上,a instanceof b 即检查b.prototype 是否能被a通过__proto__(a.__proto__ / a.__proto__.__proto__ / ...)向上查找到。

使用字面量定义原型对象

我们常使用字面量定义原型对象,但是这种方法存在以下一些问题。

1、constructor不再指向原构造函数:

function Person(){}
Person.prototype = {
  name : "Nicholas",
  age : 29,
};
var p1 = new Person();

alert(p1.constructor == Person); //false
alert(p1.constructor == Object); //true

上面这种写法,本质上完全重写了默认的prototype 对象,p1本没有constructor属性,需要从原型上获取,但是此时原型对象是字面量定义的对象,没有constructor属性,所以到Person.prototype.__proto__上去查找。如果真的需要constructor属性,可以像下面这样,重新设置回来。

Object.defineProperty(Person.prototype, "constructor", {
  enumerable: false,
  value: Person
});

注意,不要直接赋值Person.prototype.constructor = Person;,因为这样会使得constructor属性能被遍历到,要使用Object.defineProperty()

2、先构造实例,再重写原型对象,实例中将没有后添加的哪些属性和方法:

function Person(){}
var friend = new Person();
Person.prototype = {
  sayName : function () {
    alert(this.name);
  }
};
friend.sayName(); //error 报错

与第一个问题一样,使用字面量的方式定义对象,相当于重写了原型对象,当使用new构造实例的时候,friend的__proto__属性指向了默认的原型对象,之后重写了原型对象,改变了Person.prototype内存地址,但是friend本身没有sayName()方法,而它的__proto__指向还是之前那个默认的原型对象,也是没有sayName()方法的,所以报错。

所以应该在修改原型对象之后再进行new操作。

原型对象的缺点

当一个实例访问一个自身没有而原型对象有的属性并修改之后,别的实例在之后访问到的就是被修改过的,而不是最初那个被共享的了。

function Person(){}
Person.prototype = {
  friends : ["Shelby", "Court"]
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");

alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van" 访问的是被修改过的
alert(person1.friends === person2.friends); //true

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

针对原型对象的缺点,需要组合使用构造函数模式与原型模式,构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。

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();
var person2 = new Person();

person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false