面向对象(二)

2018-09-25  本文已影响0人  小小的白菜

内容承接 面向对象(一)

原型对象

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向 prototype属性所在函数的指针。就拿前面的例子来说,Person.prototype. constructor指向 Person。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。

可以通过 isPrototypeOf()方法来确定对象之间是否存在这种关系。

alert(Person.prototype.isPrototypeOf(person1)) // true

ECMAScript 5 增加了一个新方法,叫Object.getPrototypeOf(),在所支持的实现中,这个方法返回[[Prototype]]的值。例如:

alert(Object.getPrototypeOf(person1) === Person.prototype) // true

1)原型与 in 操作符

有两种方式使用in操作符:单独使用和在for-in循环中使用。在单独使用时,in操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中

function Person(name, age, job) {
    this.name = name
    this.age = age
    this.job = job
  }
  Person.prototype.sayName = function(){
    alert(this.name)
  }
  Person.prototype.name = 'superYY'
  const yy = new Person('yy', '22', 'web')
  console.log('name' in yy) // 来自实例
function Person(age, job) {
    this.age = age
    this.job = job
  }
  Person.prototype.sayName = function(){
    alert(this.name)
  }
  Person.prototype.name = 'superYY'
  const yy = new Person('yy', '22', 'web')
  console.log('name' in yy) // 来自原型

name属性要么是直接在对象上访问到的,要么是通过原型访问到的。因此,调用 'name' in yy始终都返回 true,无论该属性存在于实例中还是存在于原型中。

同时使用hasOwnProperty()方法和in操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中,如下所示。

function hasPrototypeProperty(object, name) {
  return !object.hasOwnProperty(name) && (name in object)
}

由于in操作符只要通过对象能够访问到属性就返回truehasOwnProperty()只在属性存在于实例中时才返回 true ,因此只要in操作符返回truehasOwnProperty()返回false,就可以确定属性是原型中的属性。

在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]] 标记为 false 的属性)的实例属性也会在for-in循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的——只有在IE8 及更早版本中例外。详见《javavscript高级程序设计3》P154

扩展一:Object.keys()

要取得对象上所有可枚举的实例属性,可以使用 ECMAScript 5Object.keys()方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。

function Person(age, job) {
    this.age = age
    this.job = job
  }
  Person.prototype.sayName = function(){
    alert(this.name)
  }
  Person.prototype.name = 'superYY'
  const yy = new Person('yy', '22', 'web')
  console.log(Object.keys(yy)) // Array(2) 0: "age" 1: "job" length: 2
扩展二:Object.getOwnPropertyNames()

如果你想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法。

function Person(age, job) {
    this.age = age
    this.job = job
  }
  Person.prototype.sayName = function(){
    alert(this.name)
  }
  Person.prototype.name = 'superYY'
  const yy = new Person('yy', '22', 'web')
  console.log(Object.getOwnPropertyNames(Person.prototype)) // Array(3) 0: "constructor" 1: "sayName" 2: "name" length: 3

2)更简单的原型语法

读者大概注意到了,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,

function Person() {
  }

  Person.prototype = {
    name: 'yy',
    age: 22,
    job: 'Software Engineer',
    sayName: function () {
      alert(this.name)
    }
  }

constructor属性不再指向 Person 了。前面曾经介绍过,每创建一个函数,就会同时创建它的 prototype对象,这个对象也会自动获得constructor 属性。而我们在这里使用的语法,本质上完全重写了默认的 prototype对象,因此constructor属性也就变成了新对象的constructor 属性(指向Object 构造函数),不再指向 Person函数。

此时,尽管instanceof操作符还能返回正确的结果,但通过constructor已经无法确定对象的类型了,如下所示。

  const friend = new Person() // 与 Person.prototype 绑定
  alert(friend instanceof Object) // true
  alert(friend instanceof Person) // true
  alert(friend.constructor === Person) // false
  alert(friend.constructor === Object) // true

在此,用 instanceof 操作符测试ObjectPerson 仍然返回true,但constructor属性则等于Object而不等于Person了。如果 constructor的值真的很重要,可以像下面这样特意将它设置回适当的值。

  function Person() {
  }

  Person.prototype = {
    constructor : Person,
    name: 'yy',
    age: 22,
    job: 'Software Engineer',
    sayName: function () {
      alert(this.name)
    }
  }
  const friend = new Person() // 与 Person.prototype 绑定
  alert(friend instanceof Object) // true
  alert(friend instanceof Person) // true
  alert(friend.constructor === Person) // true
  alert(friend.constructor === Object) // true

以上代码特意包含了一个 constructor属性,并将它的值设置为 Person,从而确保了通过该属性能够访问到适当的值。

以这种方式重设constructor属性会导致它的 [[Enumerable]]特性被设置为true。默认情况下,原生的constructor属性是不可枚举的,因此如果你使用兼容 ECMAScript 5JavaScript引擎,可以试一试Object.defineProperty()

// 重设构造函数,只适用于 ECMAScript 5  兼容的浏览器
Object.defineProperty(Person.prototype, 'constructor', {
  enumerable: false,
  value: Person
})

3)原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。请看下面的例子。

  const friend = new Person()
  Person.prototype.sayHi = function(){
    alert("hi")
  }
  friend.sayHi() //"hi"(没有问题!)

即使person 实例是在添加新方法之前创建的,但它仍然可以访问这个新方法。其原因可以归结为实例与原型之间的松散连接关系。当我们调用person.sayHi()时,首先会在实例中搜索名为sayHi的属性,在没找到的情况下,会继续搜索原型。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的 sayHi属性并返回保存在那里的函数。

  function Person(){
  }
  const yy = new Person()
  Person.prototype = {
    constructor: Person,
    name : 'yy',
    age : 18,
    job : 'student',
    sayName : function () {
      alert(this.name)
    }
  }
  yy.sayName() //error
重写后的原型对象

重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。

构造函数模式

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

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。

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)
    }
  }
  const person1 = new Person('yy', 22, 'web')
  const 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

在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor和方法 sayName()则是在原型中定义的。而修改了person1.friends(向其中添加一个新字符串),并不会影响到person2.friends,因为它们分别引用了不同的数组。

这种构造函数与原型混成的模式,是目前在 ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。

动态原型模式

有其他OO语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到非常困惑。动态原型模式正是致力于解决这个问题的一个方案,它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。

换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。来看一个例子。

function Person(name, age, job) {
    this.name = name
    this.age = age
    this.job = job
    if(typeof sayName !== 'function') {  // 动态创建共享的函数 sayName
      Person.prototype.sayName = function() {
        console.log(this.name)
      }
    }
  }
  const person = new Person('yy', 22, 'web')
  person.sayName() // yy

注意构造函数代码中加粗的部分。这里只在 sayName()方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可以说非常完美。

寄生构造函数模式

通常,在前述的几种模式都不适用的情况下,可以使用寄生(parasitic)构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数(工厂模式)。下面是一个例子。

这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改 Array 构造函数,因此可以使用这个模式。

function SpecialArray() {
    // 创建数组
    let values = new Array()

    //添加值
    values.push.apply(values, arguments)

    //添加方法
    values.toPipedString = function(){
      return this.join("|")
    }

    //返回数组
    return values
  }
  const colors = new SpecialArray("red", "blue", "green")
  alert(colors.toPipedString()) //"red|blue|green"

在这个例子中,我们创建了一个名叫SpecialArray 的构造函数。在这个函数内部,首先创建了一个数组,然后push()方法(用构造函数接收到的所有参数)初始化了数组的值。

随后,又给数组实例添加了一个toPipedString()方法,该方法返回以竖线分割的数组值。最后,将数组以函数值的形式返回。接着,我们调用了 SpecialArray构造函数,向其中传入了用于初始化数组的值,此后又调用了 toPipedString() 方法。

关于寄生构造函数模式,有一点需要说明:返回的对象与构造函数或者与构造函数的原型属性之间没有关系。

console.log(colors)

也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。

稳妥构造函数模式

道格拉斯·克罗克福德(Douglas Crockford)发明了JavaScript中的稳妥对象(durable objects)这个概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用thisnew),或者在防止数据被其他应用程序(如Mashup程序)改动时使用。

稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不使用 new 操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的Person构造函数重写如下。

function Person(name, age, job){
  //创建要返回的对象
  
  //可以在这里定义私有变量和函数

    //添加方法
    o.sayName = function(){
      alert(name)
    }

    //返回对象
    return o
  }

  const friend = Person('yy', 22, 'web')
  friend.sayName(); //yy

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

与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此instanceof操作符对这种对象也没有意义。

下一篇: 面向对象(三)

参考书籍

《JavaScript高级程序设计(第3版)》

上一篇下一篇

猜你喜欢

热点阅读