blog-md icon indicating copy to clipboard operation
blog-md copied to clipboard

JS 面向对象的程序设计

Open jiangjiu opened this issue 8 years ago • 0 comments

面向对象的程序设计

理解对象

属性类型

  • 有两种:数据属性和访问器属性。

数据属性:

  1. [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性改为访问器属性。
  2. [[Enumerable]]:表示能否通过 for-in 循环枚举属性。
  3. [[Writable]]:表示能否修改属性的值。
  4. [[Value]]:表示属性数据值。默认为 undefined
  • 需要注意的是,前三个属性,直接在对象上定义属性时,默认都是 true,但是如果用 Object.defineProperty()方法,不写明默认为false.
  • Object.defineProperty()接受三个参数,对象名,属性,一个 json 对象(描述符对象)。其中描述符独享的属性必须是configurable、enumerable、writable、value
     //Object.defineProperty()默认configurable 等属性均为 false
      var person = {}
      Object.defineProperty(person,'name',{
        value:'Nicholas'
      })
      console.log(person.name)  //Nicholas
      person.name = 'James'
      console.log(person.name)  //Nicholas
  • 可以多次修改同一个属性通过Object.defineProperty()方法,但Configurable属性一旦设置为 false,就再也无法改回,而且除了Writable属性可以继续修改,其他的都没法改动了。

访问器属性:

  1. [[Configurable]]同上。
  2. [[Enumerable]]同上。
  3. [[Get]]读取属性时调用的函数,默认为undefined.
  4. [[Set]]写入属性时调用的函数,默认为undefined.
  • 访问器属性不包含数据值,取而代之的是一对儿 getter 和 setter 函数(非必须)。
  • _year前面的下划线约定俗成的表示只能通过对象方法访问的属性。
  • getter和 setter 不需要同时指定,代表只能读不能写或者只写不能读,只指定 getter 函数时,写入属性会被忽略,严格模式下报错。
   //Object.defineProperty()方法为访问器属性添加 getter/setter函数
    var book = {
      _year: 2004,
      edition: 1
    }

    Object.defineProperty(book, 'year', {
      get: function () {
        return this._year
      },
      set: function (value) {
        if (value > 2004) {
          this._year = value
          this.edition += value - 2004
        }
      }
    })
    book.year = 1999
    console.log(book.edition,book.year) //get方法了_year 的值给了 book.year
    book.year = 2005
    console.log(book.edition)

定义多个属性

  • Object.defineProperties()接受两个参数,第一个是要添加和修改属性的对象,第二个是一个 json 对象,包含一一对应的属性和值。
  • 和之前的单个方法基本类似的。

创建对象

工厂模式

  • 工厂模式抽象了创建具体对象的过程,用函数封装以特定接口创建对象的细节,在函数内部创建对象赋予属性和方法,最后返回这个对象。
  • 工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题。

构造函数模式

  • 构造函数模式和工厂模式极其类似,只不过函数名大写开头,没有显示的创建对象,把属性和方法直接付给了 this 对象,而且没有return语句。
  • 按照惯例,构造函数始终以大写字母开头,非构造函数应该以小写字母开头。
  • 实际上构造函数的调用经历了下面四个过程:
  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象,从而 this 就指向了新对象
  3. 执行构造函数的代码,为新对象添加新属性
  4. 返回新对象
function Person(name, age) {
    this.name = name
    this.age = age
    this.sayHi = function() {
        console.log('hi')
    }
}
var person1 = new Person('james', 29)
  • person1实例有一个constructor构造函数属性,指向的是构造函数 Person
  • 创建自定义的构造函数可以将他的实例标识为一种特定的类型,这正是胜过工厂模式的地方。

构造函数当做函数

Person('Nicholas', 29) 
console.log(window.sayHi()) //hi

Person.call(o, 'James', 33) //通过 call 和 apply 等进行特殊作用域调用
  • 构造函数与其他函数的唯一区别,就在于调用的方式不同。任何函数,只要通过new 操作符进行调用,那么他就可以作为构造函数,不通过 new 来调用,和其他函数无异。
  • 直接调用的话,不通过 new 操作符,会被添加到 window 对象。

构造函数的问题

  • 每个方法都要在实例上重新定义一遍,即使函数是完全一样的。如果把方法变成全局函数声明,全局作用域有点名不符实,而且太多的全局方法完全没有了封装性可言。

原型模式

  • 每一个函数都有一个prototype 属性,是一个指针,指向一个对象,这个对象包含了有特定类型所有实例共享的属性和方法。
  • 使用原型对象的好处是可以让对象实例共享它所包含的属性和方法。

理解原型对象

  • 默认情况下,所有原型对象都会获得一个 constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。
  • 当调用构造函数创建一个新实例时,实例内部讲包含一个指针[[prototype]],指向构造函数的原型对象。这个连接存在于实例与构造函数的原型对象之间,并非实例与构造函数之间
  • 搜索对象的属性时,先搜索实例属性再原型对象。实例属性如果同名,会覆盖掉原型属性。
  • 使用 delete操作符可以完全删除实例属性。
  • 使用hasOwnProperty()方法可以检测一个属性是存在于实例中还是原型中。只有给定属性存在于对象实例中,才会返回 true.
  • in操作符,单独使用或者for-in循环,只要能访问到给定属性就返回 true,无论存在于实例还是原型之中。
  • 所以,只要in返回 true 而hasOwnProperty()返回 false,就说明是原型属性,这个可以封装成函数来实现。
// /delete 操作符可以完全删除实例属性,从而访问同名原型属性
    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()

    person1.name = "James"
    console.log(person1.name) //James 来自实例
    console.log(person1.hasOwnProperty('name')) //true  来自对象实例的属性
    console.log('name' in person1) //true  in 操作符访问到了即返回true

    delete person1.name
    console.log(person1.name) //Nicholas 来自原型
    console.log(person2.hasOwnProperty('name'))  //false 来自对象原型的属性
    console.log('name' in person2) //true

    var keys = Object.keys(Person.prototype)
    console.log(keys)  //Object.keys()返回所有可枚举的实例属性

    var keys1 = Object.getOwnPropertyNames(Person.prototype)
    console.log(keys1)   //和上面不同的是 Object.getOwnPropertyNames()返回了所有实例属性,不管可不可以枚举

原型的动态性

  • 注意的是,原型中查找值是一次搜索,所以先创建实例,后修改原型对象也是会反应出来,因为实例与原型对象的连接是一个指针,并非副本。
  • 随时为原型添加属性和方法,并不会有问题,但如果重写整个原型对象,等于把原型对象的指针指向了另一个原型对象,但之前创建的实例的指针指向的还是最初的原型对象,这样就会出问题。所以先重写再构造

原型对象的问题

  • 原型的所有属性都会被实例共享,对函数复用来说再合适不过,对基本属性值也还说得过去,毕竟可以在实例上添加实例属性进行覆盖,但对引用类型属性来说就是灾难。
  • 例如一个数组属性,任意的实例或者原型属性修改数组,其他的实例同样会得到变化,因为他们指向的都是原型对象中存在的那个数组。所以原型模式也很少单独使用。

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

  • 既可以通过构造函数定义实例属性,支持传参,同时原型模式还可以对共享的方法和属性进行复用,最大限度节省了内存。这是用来定义引用类型的一种默认模式,使用最广泛,认可度最高。
  //组合构造函数和原型混成的方式,是使用最广,认可最高的自定义类型的方法,可称为定义引用类型的默认方式
  function Person(name, age, job) {
    this.name = name
    this.age = age
    this.job = job
    this.friends = ['Nicholas', 'James']
  }

  Person.prototype = {
    constructor: Person,
    sayName: function () {
      console.log(this.name)
    }
  }

  var name1 = new Person('haha', 29, 'software engineer')
  var name2 = new Person('wowo', 33, 'software engineer')

  name1.friends.push('huhu')
  console.log(name1.friends) //['Nicholas', 'James', 'huhu']
  console.log(name2.friends) //['Nicholas', 'James']
  console.log(name1.sayName === name2.sayName) //true

动态原型模式

  • 上面的混成模式中,构造函数和原型是分开的,其实可以在构造函数中对原型进行封装,通过检测原型属性(初次调用构造函数才会执行)进行动态原型初始化。
  • 不可以使用对象字面量重写原型,否则会切断现有实例和新原型之间的联系。

继承

  • 大部分的 OO 语言支持两种继承方式,接口继承实现继承。接口继承只继承方法签名,实现继承则继承实际的方法。但 JS 函数没有签名,所以只支持实现继承。主要是依靠原型链来实现。

原型链

  • 每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,实例都包含一个指向原型对象的内部指针。如果让原型对象等于另一个类型的实例,此时原型对象包含着一个指向另一个对象的指针,层层递进,构成了实例与原型的链条。
//原型链继承,引用类型值的原型属性会被所有实例共享
function SuperType() {
  this.colors = ['green', 'blue', 'yellow']
}
SuperType.prototype.getColors = function () {
  return this.colors
}

function SubType() {
}
SubType.prototype = new SuperType()

var ins1 = new SubType()
var ins2 = new SubType()

ins1.colors.push('black')

console.log(ins1.colors)  //['green', 'blue', 'yellow', 'black']
console.log(ins2.colors)  //['green', 'blue', 'yellow', 'black'] 引用类型的原型属性被共享
  • 属性搜索会经历三个搜索步骤,第一个是搜索实例,第二个搜索SubType.rototype,第三个搜索 Super.prototype,找不到属性的情况下,会向下一层一层寻找。
  • 所有的引用类型都是默认继承了 object,这也是所有自定义烈性都会继承 toString()和 valueOf().
  • **原型链的问题:**引用类型值的原型属性会被所有实例共享;无法向超类型的构造函数中传递函数(在不影响所有实例对象的情况下)。

借用构造函数

  • 有时候也叫伪造对象或经典继承,就是在子类构造函数中调用超类型构造函数。优势是可以传参。但却无法复用函数等。
  //构造函数继承
    function SuperType(name) {
      this.name = name
    }

    function SubType() {
      SuperType.call(this, 'Nicholas') //构造函数可以传参,却无法继承原型方法,无法函数复用
      this.age = 29
    }

    var ins1 = new SubType()
    console.log(ins1.name)  //Nicholas
    console.log(ins1.age)  //29

组合继承

  • 借用构造函数和原型链组合,原型链实现对原型属性方法继承,通过构造函数来实现对实例属性的继承。
  • 这也是最常用的继承模式,instanceOf和 isPrototypeOf()也可以识别相应对象。
    //组合继承,最常用的继承模式
    function SuperType(name) {
      this.name = name
      this.colors = ['green', '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.constructor = SubType
    SubType.prototype.sayAge = function () {
      console.log(this.age)
    }

    var ins1 = new SubType('Nicholas', 33)
    ins1.colors.push('black')
    console.log(ins1.colors)  //['green', 'blue', 'yellow', 'black']
    ins1.sayName()  //Nicholas
    ins1.sayAge()  //33

    var ins2 = new SubType('haha', 29)
    console.log(ins2.colors)  //['green', 'blue', 'yellow']
    ins2.sayName()  //haha
    ins2.sayAge()  //29

原型式继承

  • 要求必须有一个对象可以作为另一个对象的基础,在没有必要兴师动众的创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。
  • 别忘了引用类型值的属性会被所有实例共享,和之前的原型模式是一样的。
function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}  //对传进来的 o 进行一次浅复制

object()和 Object.create()是相似的

Object.create(obj, {
    value: 'xxx'
})  //传两个参数   后一个为 value 值的对象

寄生式继承

  • 与寄生构造函数和工厂模式相似,创建一个仅用于封装继承过程的函数,并在内部添加方法增强对象,最后返回这个对象。
  • 寄生式继承为对象添加函数,无法做到函数复用而降低效率,这一点与构造函数模式类似。
   //寄生继承, 主要考虑对象而不是自定义类型和构造函数
    function createAnother(original) {
      var clone = Object.create(original)
      clone.sayHi = function () {
        console.log('Hi')
      }
      return clone
    }
    var person = {
      name: 'Nicholas',
      age: 29
    }
    var anotherPerson = createAnother(person)
    anotherPerson.sayHi()  //hi

寄生组合式继承

  • 组合继承十分常用,但也有缺点:子类调用两次父类型,第二次在实例上重写了属性来覆盖原型中的属性。
  • 所谓寄生式继承,就是通过借用构造函数来继承属性,通过原型链的混成模式继承方法。不必为了指定子类型的原型而调用超类型的构造函数,所需要的无非是超类型的原型的一个副本而已。
  • 本质上,就是通过寄生式继承来继承超类型的原型,再将结果指定给子类型的原型。
  //  组合寄生式继承
  function inherit(subType, superType) {
    var prototype = Object.create(superType)
    prototype.constructor = subType
    subType.prototype = prototype
  }  //

jiangjiu avatar May 27 '16 03:05 jiangjiu