JS高程:读书摘要(三)面向对象

对象

属性类型

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 操作符来调用,那它跟普通函数也不会有什么两样。

构造函数的问题:

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在创建多个实例的时候创建多个同样任务的的Function实例的确没有必要;况且有this 对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此,大可像下面这样,通过把函数定义转移到全局中来解决这个问题。

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

// 但是这样一来 不仅污染了全局作用域
// 对于这个Person类来说,毫无封装性可言

原型模式

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

原型对象:

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

2、每一个原型对象,都有两个默认的属性,constructor属性和__proto__属性,constructor属性指向原构造函数,__proto__属性指向该原型对象的原型(每一个对象都有原型,原型对象也有原型),__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

当我们在使用一个对象的属性和方法时:

1、会先在自身中寻找,如果自身中有,则直接使用,如果没有则去原型对象中找。

2、如果原型对象中有则使用,如果原型对象中也没有,则去原型对象的原型中找,直到找到Object对象的原型。找到就用,找不到就是undefined或方法报错。

3、找到Object对象的原型就会停止,因为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)

使用字面量定义原型对象的问题:

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.prototye.__proto__上去查找。如果真的需要constructor属性,可以像下面这样,重新设置回来。

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

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

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

继承

原型链继承

function Animal () {}
function Dog() {
  this.name = "wangcai";
  this.sayHi = function(){
    console.log("wangwang")
  }
}

Dog.prototype = new Animal();
var d1 = new Dog();

// d1的constructor属性是指向 Animal的
// 因为这里也相当于重写了默认的原型对象,原型对象是Animal的实例,
// 所以原型对象的constructor指向 Animal 所以 d1的constructor指向Animal

原型链继承的问题

在通过原型来实现继承时,子类的原型实际上变成了父类型的实例。原先父类的实例属性也就顺理成章地变成了子类的原型属性了。此时,原型对象的缺点一样存在,即子类的一个实例对原型属性做出了改动,则会影响到其他所有实例。

原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

有鉴于这两个问题,实践中很少会单独使用原型链。

组合继承

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript 中最常用的继承模式。而且,instanceofisPrototypeOf()也能够用于识别基于组合继承创建的对象。

function SuperType(name){
  this.colors = ["red", "blue", "green"];
  this.name = name;
}
SuperType.prototype.sayHello = function () {
  console.log('hello')
}
function SubType(){
  //构造函数继承属性,同时还传递了参数
  SuperType.call(this, "Nicholas");
  //实例属性
  this.age = 29;
}
// 原型链继承方法 所有的实例将访问同一个方法 达到复用的效果 
// 而且也可以在实例中重写这个方法,而不影响到其他实例
SubType.prototype = new SuperType();
var instance = new SubType();
instance.sayHello()  // hello

原型式继承

var person = {
  friends: ["Shelby", "Court", "Van"]
};

var p1 = Object.create(person);
// p1.__proto__ = person
p1.friends.push("Rob");

var p2 = Object.create(person);
p2.friends.push("Barbie");

alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"

Object.create()方法实现的就是原型式的继承,借助一个中转构造函数来实现继承。

不需要创建构造函数,就可以让一个对象与另一个对象保持类似,但依然拥有原型链继承的弊病,共享引用类型的值。

寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增加对象,在返回这个对象。

function createAnother(original){
  var clone = Object.create(original);
  clone.sayHi = function(){ //以某种方式来增强这个对象
      alert("hi");
  };
  return clone; //返回这个对象
}

var person = {
  friends: ["Shelby", "Court", "Van"]
};

var p1 = createAnother(person);
p1.sayHi() // hi

返回的新对象不仅具有person的所有属性和方法,而且还有 “自己”的sayHi()方法。

在主要考虑为增强对象(为对象添加属性和方法)时,寄生式继承也是一种有用的模式。但是,会由于不能做到函数复用而降低效率。

本质上还是原型继承,虽然返回的是新对象,但是还是会有原型继承的弊病,在访问自身没有而原对象有的属性时,共享原对象的属性。

寄生组合式继承

组合继承是JavaScript 最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

SubType.prototype = new SuperType(); //第一次调用SuperType()
SuperType.call(this, name); //第二次调用SuperType()

解决这个问题的方法就是寄生组合式继承。

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

1.第一步是创建超类型原型的一个副本。

2.第二步是为创建的副本添加constructor 属性,从而弥补因重写原型而失去的默认的constructor属性。

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;
}
inheritPrototype(SubType, SuperType); // 不再使用new来做原型链继承

SubType.prototype.sayAge = function(){
  alert(this.age);
};

这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用instanceof 和isPrototypeOf()

小结

原型式继承,即封装函数对超类型对象进行浅拷贝,过程是用一个中转对象的原型指向超类型,返回这个中转对象的实例。实例的原型对象相等于超类型(弊病与原型链继承一样,引用数据类型被共享)

寄生式继承,即封装函数对超类型对象进行浅拷贝,并且在函数内部加强这个副本对象,并返回这个对象。

寄生组合式继承,通过封装函数通过寄生式继承去继承方法,不再使用new操作符(减少超类型的调用次数),在子函数中对超类型调用call() 或者apply()继承属性,是实现基于类型继承的最有效方式。

你可能感兴趣的:(JS高程:读书摘要(三)面向对象)