《JavaScript高级程序设计》读书笔记-第六章(面向对象的程序设计)

理解对象

属性类型

有两种类型:数据属性和访问器属性

  • 数据属性
    • [[Configurable]]
      表示能否通过delete删除属性
    • [[Enumerable]]
      表示能否通过for-in循环返回属性
    • [[Writable]]
      表示能否修改属性的值
    • [[Value]]
      包含这个属性的数据值

例如

var person = {
  name: 'Nicholas'
}

这个对象中[[Configurable]]、[[Enumerable]]、[[Writable]]都设置成true,[[Value]]设置为指定的值

要修改属性默认的值,则要用ES5的Objcet.defineProperty()方法
这个方法接受三个参数:属性所在的对象属性的名字和一个描述符对象,可以修改对应的特征值

var person = {}
Object.defineProperty(person,'name',{
  writable: false,
  value: 'Nicholes'
})

这个例子创建了一个name属性,他的值是只读,不可修改的

类似的设置configurable:false,表示不能从对象中删除属性
一旦把属性定义成不可配置的,就不能再把它变回可配置的了
此时,再调用Object.defineProperty修改除writable之外的属性,就会导致错误

  • 访问器属性
    访问器属性不包含数据值,他们包含一堆getter和setter函数,在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值。在写入访问器属性时,会调用setter函数并传入新值
    • [[Configurable]]
      表示能否通过delete删除属性从而重新定义属性
    • [[Enumerable]]
      表示能否通过for-in循环返回属性
    • [[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(this._year > 2004){
      this._year = newValue
      this.edition += newValue - 2004
    }
  }
})

book.year = 2005
console.log(book.edition)    //2

支持Object.defineProperty()这个方法只有IE9+,Firefox4+,Safari5+,Opera12+和Chrome

有两个遗留的方法,同样也能实现上面的功能
__defineGetter__()__defineSetter__()
这两个方法最初是由Firefox引进的,后来Safari3、Chrome 1 和Opera9.5也给出了相同的实现

var book = {
  _year: 2004,
  edition: 1
}
book.__defineGetter__('year'){
    return this._year
}

book.__defineSetter__('year',function(newValue){
    if(this._year > 2004){
      this._year = newValue
      this.edition += newValue - 2004
    }
})

book.year = 2005
console.log(book.edition)    //2
定义多个属性

ES5Object.defineProperties()
接收两个参数,第一个对象是添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。

用法:

var book = {}
Object.defineProperty(book,{
  _year: { 
    value: 2014
  },
  edition: {
    value: 1
  },
  year: {
    get: function(){
      return this._year
    },
    set: function(newValue){
      if(this._year > 2004){
        this._year = newValue
        this.edition += newValue - 2004
      }
    }
  }
})
读取属性的特性

ES5的Object.getOwnPropertyDescriptor()方法

创建对象

工厂模式
function createPerson(name, age, job){
  var o = new Object()
  o.name = name
  o. age = age
  o.job = job
  o.sayName = function(){
    console.log(this.name)
  }
  return o
}

var people1 = createPerson('Nicholas', 29, 'Software Engineer')
var people2 = createPerson('Greg', 27, 'Doctor')

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题,即怎样知道一个对象的类型,随着JavaScript的发展,又一个新模式出现了

构造函数模式
function Person(name, age, job){
  this.name = name
  this.age = age
  this.job = job
  this.sayName = function(){
    console.log(this.name)
  }
}
var person1 = new Person('Nicholas', 29, 'Software Engineer')
var person2 = new Person('Greg', 27, 'Doctor')

要创建Person的新实例,必须用new操作符,以这种方式调用构造函数的对象会经历四个步骤
1、创建一个新对象
2、将构造函数的作用域赋给新对象( 因此this就指向了这个新对象 )
3、执行构造函数中的代码(为这个新对象添加属性)
4、返回新对象

person1和person2分别保存着Person的一个不同的实例,这两个对象都有一个constructor(构造函数)属性,这个属性指向Person

console.log(person1.constructor === Person)  //true
console.log(person2.constructor === Person)  //true

检测对象类型,instanceof操作符更为可靠

console.log(person1 instanceof Object)  //true
console.log(person1 instanceof Person)  //true
console.log(person2 instanceof Object)  //true
console.log(person2 instanceof Person)  //true
  • 将构造函数当做函数
    任何函数,只要通过new操作符来调用,他都可以作为构造函数。
    同样的,如果不通过new操作符来调用,那它跟普通函数也没什么区别。
//当做构造函数来使用
var person1 = new Person('Nicholas', 29, 'Software Engineer')
person.sayName()

//当做普通函数来使用
Person('Greg',27,'Doctor')      //添加到window
window.sayName()               //'Greg'

//在另一个对象的作用域中调用
var o = new Object()
Person.call(o,'Nicholas', 29, 'Software Engineer')
o.sayName()                     //'Nicholas'
  • 构造函数的问题

缺点:每个方法在每个实例上要重新创建一遍
从逻辑角度上来讲,此时的构造函数也可以这样定义:

function Person(name, age, job){
  this.name = name
  this.age = age
  this.job = job
  this.sayName = new Function(console.log(this.name))      //与声明函数等价
}

以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建Function新实例的机制仍然是相同的。因此,不同实例上的同名函数是不同的。

这样创建没必要,况且有this对象在,可以将函数定义转移到构造函数之外,大可这么创建

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

function sayName() {
  console.log(this.name)
}

这样做确实是解决了两个函数做同一件事的问题。可是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实,如果对象需要很多方法,那就要定义很多个全局函数,这样我们定义的引用类型就丝毫没有封装性可言

原型模式
  • prototype属性,这个属性是一个指针,指向一个对象
  • prototype就是通过调用构造函数而创建的那个对象实例的原型对象
function Person() {
}
Person.prototype.name = 'Nicholas'
Person.prototype.age = 29
Person.prototype.job = 'Software Engineer'
Person.prototype.sayName = function() {
  console.log(this.name)
}
var person1 = new Person()
person1.sayNmae()                                        // 'Nicholas'
var person2 = new Person()
person2.sayName()                                        // 'Nicholas'

console.log(person1.sayNmae() === person2.sayName())     //true
  • 与构造函数不同的是,新对象的这些属性和方法是由所有实例共享的。

  • 理解原型对象


  • 如上图 Person.prototype 指向了原型对象 Person Prototype,而Person.prototype.constructor又指回 Person 。

  • 原型对象中除了包含 constructor 属性之外,还包括后来添加的其他属性。

  • Person的每个实例,person都包含一个内部属性,该属性指向了Person.prototype

  • 如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性会屏蔽原型中对应的属性

function Person() {

}
Person.prototype.name = 'Nicholas'
Person.prototype.age = 29
Person.prototype.job = 'Software Engineer'
Person.prototype.sayName = function() {
  console.log(this.name)
}
var person1 = new Person()
var person2 = new Person()

preson1.name = 'Greg'
console.log(person1.name)          //'Greg'
console.log(perison2.name)         //'Nicholas'

此时person1.name是一个实例属性

in
  • in操作符会在通过对象能访问给定属性时返回true,无论该属性存在于实例中还是原型中
  • 在使用for - in 循环时,返回的是所有能够通过对象访问的、可枚举(Enumerated)属性
Object.keys()

要取得对象上所有可枚举的属性,Object.keys()方法,返回一个包含所有可枚举属性的字符串数组

Object.getOwnPropertyName()

取得对象上的所有实例属性

原型模式的另一种写法
function Person(){
}

Person.prototype = {
  name: 'Nicholas',
  age: 28,
  job: 'Software Engineer',
  sayName: function(){
    console.log(this.name)
  }
}
  • 跟之前写法的不同之处:
    设置为等于一个以对象字面量形式创建的新对象。但是constructor属性不再指向Person了
  • 原因:
    每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。我们在这里的写法,重写了默认的prototype对象,因此constructor也变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数
  • 如果constructor重要,可以这么写
function Person(){
}

Person.prototype = {
  constructor: Person,
  name: 'Nicholas',
  age: 28,
  job: 'Software Engineer',
  sayName: function(){
    console.log(this.name)
  }
}

这种方式会让constructor的[[Enumerable]]特性设置为true
有另外一种方式,能还原constructor,用Object.defineProperty()

function Person(){
}

Person.prototype = {
  name: 'Nicholas',
  age: 28,
  job: 'Software Engineer',
  sayName: function(){
    console.log(this.name)
  }
}
Object.defineProperty(Person.prototype, 'constructor', {
  enumerable: false,
  value: Person
})
原型对象的问题
  • 原型对象的最大问题是由其共享的本性所导致的
    对于包含引用类型值的属性来说,问题就比较突出了
function Person(){
}

Person.prototype = {
  name: 'Nicholas',
  age: 28,
  job: 'Software Engineer',
  friends: ['Shelby', 'Court']
  sayName: function(){
    console.log(this.name)
  }
}

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

var person1 = new Person()
var person2 = new Person()

person1.friends.push('Van')

console.log(person1.friends)                        //'Shelby', 'Court','Van'
console.log(person2.friends)                        //'Shelby', 'Court','Van'

console.log(person1.friends === person1.friends)    //true
组合使用原型模式和构造函数
  • 构造函数模式用于定义实例属性
  • 原型模式用于定义方法和共享的属性
function Person(name, age, job){
  this.name = name
  this.age = age
  this.job = job
  this.friends = ['Shelby','Court']
}

Person.propotype = {
  sayName: function(){
    console.log(this.name)
  }
}

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

动态原型模式

可以通过检查某个应该存在的方法

function Person(name, age, job) {

  this.name = name
  this.age = age
  this.job = job

  if(typeof this.sayName != 'function') {
    Person.propotype.sayName = function() {
      console.log(this.name)
    }
  }
}
寄生构造函数模式

这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的数组。

function SpecialArray() {
  var values = new Array()
  values.push.apply(values, arguments)
  values.toPipedString = function(){
    return this.join('|')
  }
  return values
}
var colors = SpecialArray('red','green','blue')
console.log(colors.toPipedString())                      //'red|blue|green'

跟工厂模式的缺陷一样,不能依赖instanceof操作符来确定对象类型,所以不建议使用这种模式

稳妥构造函数

所谓稳妥对象,指的是没有公共属性,而其方法也不引用this的对象

function Person(name, age, job) {
  var o = new Object()
  o.sayname = function(){
    console.log(name)
  }
  return o
}
var friend = Person('Nicholas', 29, 'Softwate Engineer')
friend.sayName() 
继承

ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链

原型链
function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function() {
  return this.property
}
function SubType() {
  this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function() {
  return this.subproperty
}
var instance =  new SubType()
console.log(instance.getSuperValue())  //true
console.log(instance.getSubValue())   //fasle

此时打印instance会得到



书上的图

在上面的代码中,我们没有使用SubType默认提供的原型,而是给它换了一个新原型,这个新原型就是SuperType的实例。新原型不仅具有作为一个SuperType的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了SuperType的原型

  • 别忘了默认的原型object
image.png
  • 谨慎地定义方法

给原型添加方法的代码一定要放在替换原型的语句之后

function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function() {
  return this.property
}
function SubType() {
  this.subproperty = false
}
SubType.prototype = new SuperType()
//添加了新方法
SubType.prototype.getSubValue = function() {
  return this.subproperty
}
//重写SuperType中的方法
SubType.prototype.getSuperValue = function() {
  return false
}
var instance =  new SubType()
console.log(instance.getSuperValue())  //false
console.log(instance.getSubValue())   //fasle
  • 不能使用对象字面量创建原型方法

这样做会重写原型链

function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function() {
  return this.property
}
function SubType() {
  this.subproperty = false
}
SubType.prototype = new SuperType()

//使用了字面量来添加新方法,会导致上一行代码失效,覆盖掉上面的
SubType.prototype = {
  getSubValue: function() {
    return this.subproperty
  },
  someOtherMethod: function(){
    return false
  }
}

var instance =  new SubType()
console.log(instance.getSuperValue())  //undefined
原型链的问题

包含引用类型的原型属性会被所有的实例共享

function SuperType(){
  this.colors = ['red', 'blue']
}

function SubType(){
}

SubType.prototype = new SuperType()

var instance1 = new SubType()
var instance2 = new SubType()

instance1.colors.push('black')

console.log(instance1.colors)        //["red", "blue", "black"]
console.log(instance2.colors)        //["red", "blue", "black"]
  • 原型链的第二个问题是,在创建子类型的实例的时候,没有办法在不影响所有对象实例的情况下向父类型的构造函数中传递参数。
借用构造函数

也被称为伪造对象或经典继承。
这种技术的基本思想相当简单,即在子类型构造函数的内部调用父类型的构造函数

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

var instance2 = new SubType()
console.log(instance2.colors)          //["red", "blue", "green"]

通过调用call()方法或者apply()方法,我们实际上是在(未来将要)新创建的SubType实例的环境下调用了SuperType构造函数。这样一来,就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。

  • 借用构造函数的问题
    函数复用无从谈起
组合继承

也叫做伪经典继承,思路是使用原型链实现了对原型属性和方法的继承,而通过构造函数来实现对实例属性的继承

function SuperType(name) {
  this.name = name
  this.colors = ['red','blue','yellow']
}

SuperType.prototype.sayName = function() {
  console.log(this.name)
}

function SubType(name,age) {
  SuperType.call(this,name)
  this.age = age
}

SubType.prototype = new SuperType()

SubType.prototype.sayAge = function() {
  console.log(this.age)
}

var instance1 = new SubType('Nicholas', 29)
instance1.colors.push('black')
console.log(instance1.colors)                 // ["red", "blue", "yellow", "black"]
instance1.sayName()                          //Nicholas
instance1.sayAge()                          //29

var instance2 = new SubType('Greg', 27)
console.log(instance2.colors)              //["red", "blue", "yellow"]
instance2.sayName()                        //Greg
instance2.sayAge()                        //27

以上是比较常用的继承方式

原型式继承

这种继承模式封装了原型链继承,是原型链继承的升级版,原型链继承+工厂模式
首先我们得创建一个object函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。

缺点:会调用两次父类构造函数,第一次是创建子类型原型的时候

也就是ES5的Object.creat()简陋版的原理

const object = function(o){
  function F(){}
  F.prototype = o
  return new F()
}
const person = {
  name: 'Nicholas',
  friends: ['Shelby', 'Court', 'Van']
}
const anotherPerson = object(person)
anotherPerson.name = 'Greg'
anotherPerson.friends.push('Rob')

const yetAnotherPerson = object(person)
yetAnotherPerson.name = 'Linda'
yetAnotherPerson.friends.push('Barbie')

console.log(person.friends)
寄生式继承

增强原型式继承的功能的一种模式
缺点:使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点与构造函数模式类似。

const createAnother = function(original){
  const clone = object(original)
  clone.sayHi = function(){
    console.log('hi')
  }
  return clone
}
const person = {
  name: 'Nicholas',
  friends: ['Shelby', 'Court', 'Van']
}
const anotherPerson = createAnother(person)
anotherPerson.sayHi() //hi
寄生组合式继承

所谓寄生组合式继承,就是借用构造函数来继承属性

const object = function(o){
  function F(){}
  F.prototype = o
  return new F()
}
const inheritPrototype = function(Child, Parent){
  const prototype = object(Parent.prototype)
  prototype.constructor = Child
  Child.prototype = prototype
  Child.__proto__ = Parent
}
function Parent(name) {
  this.name = name
}

Parent.sayHello = function (){
    console.log('hello')
}

Parent.prototype.sayName = function() {
    console.log('my name is ' + this.name)
    return this.name
}


function Child(name, age) {
    Parent.call(this, name)
    this.age = age
}

inheritPrototype(Child, Parent)

Child.prototype.sayAge = function () {
    console.log('my age is ' + this.age)
    return this.age
}

你可能感兴趣的:(《JavaScript高级程序设计》读书笔记-第六章(面向对象的程序设计))