# 理解对象

# 属性类型

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

# 数据属性

包含一个数据值的位置,在这个位置可以读取和写入值。

  • [[Configurable]]: 表示能否通过 delete 删除属性,修改属性的特性,或者能否把属性修改为访问器属性,默认为 true;
  • [[Enmuerable]]: 表示能否通过 for-in 循环返回属性,默认为 true;
  • [[Writable]]: 表示能否修改属性的值,默认为 true;
  • [[Value]]:表示这个属性的值。比如定义一个 perosn 对象,他的 name 属性为 jie,那么他的 [[Value]] 存放的是 jie,默认为 undefined;

要修改属性默认的特性,必须使用 ecma5 中的 object.definePropery() 方法;该方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中描述符对象的属性必须是 configurable、enumerable、writable 和 value。设置其中的一个或者多个值,可以修改对应的属性值。

  let person = {}
  // 表示 name 属性是只读的
  Object.defineProperty(person, 'name', {
    writable: false, // 表示不能修改属性的值
    value: 'jiegiser', // 赋值属性值为 jiegiser
    configurable: false // 表示不能通过 delete 删除属性
  })
  console.log(person)
  person.name = 'jieouba' // 在严格模式下会报错
  console.log(person.name) // 修改不成功,还是 jiegiser
  delete person.name
  console.log(person) // 删除不成功

这里需要注意的是,如果一但通过 defineProperty 改变了 configurable 为不可配置,那么就不能再把它变回可配置;调用 Object.defineProperty 方法修改除了 writable之外的特性都会报错,抛出异常 Cannot redefine property: name

  var person = {}
  Object.defineProperty(person, 'name', {
    configurable: false,
    value: 'jiegiser'
  })
  Object.defineProperty(person, 'name', {
    configurable: true,
    value: 'jieouba'
  })

也就是说可以多次调用 Object.defineProperty 方法修改同一个属性,但是在把 configurable 特性设置为 false 之后,就会有限制;如果调用 Object.defineProperty 方法只是修改已定义的属性的特性,则无此限制。建议不要在 IE8 中使用,会有问题-不完善;

使用 Object.defineProperty 方法创建一个新的属性时,如果不指定 configurable、writable、enumerable 值,默认是 false。

# 访问器属性

不包含数据值,他们包含一对 getter 跟 setter 函数(不过这两个函数都不是必须的)在读取访问器属性时,就会调用 getter 方法;这个函数赋值返回有效的值,在写入访问器属性时,会调用 setter 函数并传入新值,这个函数负责决定如何处理数据。

  • [[Configurable]]:表示能否通过 delete 删除属性,修改属性的特性,或者能否把属性修改为数据属性;对于直接在对象上定义的属性,这个特性的默认值为 true;
  • [[Enmuerable]]:表示能否通过 for-in 循环返回属性;在对象上定义的属性,该特性默认值为 true;
  • [[Get]]:在读取属性时调用该函数;默认值为 undefined;
  • [[Set]]:在写入属性时调用该函数;默认值为 undefined;

访问器属性不能直接定义,必须使用 object.definePropery() 方法:

  let book= {
      _year: 2019, // 下划线表示只能通过对象方法访问
      edition:1
  }
  // 访问器
  Object.defineProperty(book, 'year', {
    // 不一定非要同时指定 getter 与 setter,只指定 getter 意味着属性是不能写的,尝试写入属性会被忽略,在严格模式会报错
    // 只指定 setter 函数的属性也不能读,在非严格模式会返回 undefined,在严格模式会抛出异常
    get() {
      return this._year
    },
    set(newValue) {
      if(newValue > 2019) {
        this._year = newValue
        this.edition += newValue - 2019
      }
    }
  })
  book.year = 2020
  console.log(book)

# 定义多个属性

使用 Object.defineProperties() 方法,可以利用这个方法通过描述符一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应:

    // 为对象定义多个属性
    let book={}
    Object.defineProperties(book, {
      _year: {
        writable: true,
        value: 2019
      },
      edition: {
        writable: true,
        value: 1
      },
      // 访问器属性
      year: {
        get() {
          return this._year;
        },
        set(newValue) {
          if(newValue > 2019) {
            this._year = newValue
            this.edition += newValue - 2019
          }
        }
      }
    })
    book.year = 2020
    console.log(book)

# 读取属性的特性

Object.getOwnPropertyDescriptor() 方法可以取得给定属性的描述符,他包含两个参数为:属性所在的对象和要读取其描述符的属性名称,返回一个对象,如果是访问器属性,这个对象的属性有 configurable、enumerable、get 和 set ;如果是数据属性,这个对象属性有 configurable、enumberable、writable、和 value:

  const book = {}
  Object.defineProperties(book, {
    _year: {
      value: 2019
    },
    edition: {
      value: 1
    },
    year: {
      get() {
        return this._year;
      },
      set(newValue) {
        if (newValue > 2019) {
          this._year = newValue;
          this.edition += newValue - 2019;
        }
      }
    }
  })
  const decriptor = Object.getOwnPropertyDescriptor(book, '_year')
  console.log(decriptor) // {value: 2019, writable: false, enumerable: false, configurable: false}
  console.log(typeof decriptor.get) // undefined
  const descriptorYear = Object.getOwnPropertyDescriptor(book, 'year')
  console.log(descriptorYear); // {get: ƒ, set: ƒ, enumerable: false, configurable: false}
  console.log(descriptorYear.value); // undefined
  console.log(typeof descriptorYear.get) // function
  // 在 javascript 中,对于 DOM 以及 BOM 都可以使用 getOwnPropertyDescriptor() 方法

# 创建对象

# 工厂模式

  • 产生的原因:使用同一个接口创建很多对象,会产生大量的重复的代码,为了解决这个问题,
  • 工厂模式:该模式抽象了创建具体对象的过程,考虑到在 ECMAscript 中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节;
  function createPerson(name, age, job) {
    let o = {name, age, job}
    return o
  }
  const jiegiser = createPerson('jiegiser', 18, 'giser')
  const jieouba = createPerson('jieouba', 18, 'junren')
  console.log(jiegiser)
  console.log(jieouba)

工厂对象虽然解决了创建多个相同对象的问题,但是没有解决对象识别的问题,也就是怎么知道一个对象的类型

# 构造函数模式

上面的例子可以这样写:

  function Person(name, age, job){
    this.name = name
    this.age = age
    this.job = job
    this.sayName = function() {
      console.log(this.name)
    }
  }
  const person1 = new Person('jiegiser', 18, 'IT')
  const person2 = new Person('jieouba', 18, 'giser')
  console.log(person1) // Person {name: "jiegiser", age: 18, job: "IT", sayName: ƒ}
  console.log(person2) // Person {name: "jieouba", age: 18, job: "giser", sayName: ƒ}
  console.log(person1 === person2) // false

可以发现跟上面不同的是,没有显式的创建对象,直接将属性和方法赋值给了对象,没有 return 语句;还有就是 Person 的 P 是大写,按照规定构造函数始终都应该以一个大写字母开头,而非构造函数的那么使用小写字母进行书写;这里注意,构造函数本身也是构造函数,只不过可以用来创建对象而已;要创建 person 实例,必须要使用 new 关键字进行创建,创建一个实例,其实是以下四个步骤:

使用 new 关键字创建一个实例,经历以下步骤:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象;
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象

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

console.log(person1.constructor === Person) // true
console.log(person2.constructor === Person) // true
// 对象的 constructor 属性是用来标识对象类型的,我们创建的所有对象,既是 Object 的实例,同时也是 Person 的实例:
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型,而这个正是构造函数模式胜过工厂模式的地方。上面的例子中 person1 和 peroson2 之所以同时是Object 的实例,是因为所有对象均继承 object; 构造函数与其他函数的唯一区别就是需要用 new,但是任何函数,只要通过 new 操作符来调用,那他就可以作为构造函数,如果任何函数不使用 new 来调用,那他跟普通函数没什么区别; 例如前面的例子

// 当做构造函数使用
const person1 = new Person('jiegiser', 18, 'IT') // Person {name: "jiegiser", age: 18, job: "IT", sayName: ƒ}
console.log(person1)
// 当做普通函数使用
Person('jieouba', 18, 'giser')
window.sayName()
// 在另一个对象的作用域中调用
let o = new Object()
Person.call(o, 'jie', 18, 'giser') // 执行了 new 操作符,没有将属性挂在到全局,this 执行 o 对象。
o.sayName()

构造函数的问题:就是每一个方法都要在每个实例上重新创建一遍,比如说我们前面的例子,sayName 方法,其实在每次实例化对象的时候,都会创建一次; 他实际执行的如下的代码:

function Person(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.sayName = new Function('console.log(this.name)')  
}
const person1 = new Person('jiegiser', 18, 'IT') // Person {name: "jiegiser", age: 18, job: "IT", sayName: ƒ}
const person2 = new Person('jieouba', 18, 'giser') // Person {name: "jieouba", age: 18, job: "giser", sayName: ƒ}
console.log(person1)
console.log(person2)

但是如果我们将 Person 的 sayName 方法转移到构造函数外,像下面的代码:

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

这样一来,由于 sayName 包含的是一个指向函数的指针,因此 person1 与 perosn2 就共享了在全局作用域中定义的同一个 sayName 函数,这样确实解决了 两个函数做了同一件是的问题,但是,在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实,而且,如果对象需要定义很多 方法,那么就要定义很多个全局函数,那么我们这个自定义的引用类型就丝毫没有封装性可言了。可以使用原型模式来解决这个问题;

# 原型模式

我们创建的每一个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途就是包含可以由特定类型的所有实例共享的属性和方法。按照字面理解 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象,使用原型对象的好处是可以让所有对象共享它所包含的属性和方法,换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,下面的例子:

  function Person() {}
  Person.prototype.name = 'jiegiser'
  Person.prototype.age = 18
  Person.prototype.job = 'giser'
  Person.prototype.sayName = function() {
    console.log(this.name)
  }
  console.log(Person.prototype.constructor)
  
  const person1 = new Person()
  person1.name = 'jieouba'
  console.log(person1)
  person1.sayName()

  const perosn2 = new Person()
  perosn2.sayName()
  console.log(person1.sayName === perosn2.sayName) // true

可以看到构造函数变成了变成看空对象;

# 原型对象

无论什么时候,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。在默认的情况下,所有原型对象都会自动获取一个 constructor(构造函数)属性,这个属性是一个指向 prototype 属性所在函数的指针。比如前面的例子 Person.prototype.constructor 指向 Person,而通过这个构造函数,我们还可以继续为原型对象添加其他属性和方法。创建了自定义的构造函数之后,其原型对象默认只会取得 constructor 属性,至于其他方法,都是从 Object 继承而来,当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。

需要注意的是,当调用构造函数创建一个新实例后,该实例的内部将包含一个指针__prpto__ ,指向构造函数的原型对象;这个连接存在与实例与构造函数的原型对象之间,而不是实例与构造函数之间;

下图是上面代码为例,展示各个对象之间的关系:

在这里插入图片描述

理一遍思路:首先构造函数有一个 prototype 指向的是原型对象,而原型对象的 constructor 指向的是 prototype 属性所在的函数,也就是构造函数;比如我们的 Person,我们后来为他的原型对象又加了 name,age 等属性,其实 prototype 就是构造函数的原型对象;然后实例化后的对象有一个__proto__ 属性,指向的是构造函数的原型,而构造函数原型对象的 constructor 属性指向的是构造函数本身;构造函数所创建的实例有一个 constructor 属性,指向的是构造函数。

这里需要记住一句话:与构造函数没有直接的关系,他们都是构造函数原型对象之间的关系。

如果不存在__proto__ 属性可以通过 isProtoypeOf() 方法来确定对象之间是够存在这种关系;如果__proto__ 属性指向调用 isPrototypeOf 方法的对象(Person.prototype),那么这个方法就返回true;

  console.log(Person.prototype.isPrototypeOf(person1)) // true, 也就是说,看 person1 这个对象是否调用了 Person.prototype。
  // 对象有一个方法 getPrototypeOf 方法,会返回对象的 __proto__ 属性;
  console.log(Object.getPrototypeOf(person1)) // {name: "jiegiser", age: 18, job: "giser", sayName: ƒ, constructor: ƒ}
  console.log(Object.getPrototypeOf(person1) === Person.prototype) // true
  console.log(person1)
  console.log(Object.getPrototypeOf(person1) === person1.__proto__) // true

这里需要注意的是,每当代码读取某个对象的属性时,都会执行一次搜索,目标是具有给定名字的属性,首先从实例本身开始,如果在实例中找到了具有给定名字的属性,那么返回该属性值,如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回,例如上面的 person1.sayName(),会先后执行两次搜索,首先会从 person1 的实例进行搜索,如果没有就从 person1 的原型进行搜索。

需要注意的是,我们不能够通过对象的实例来重写原型中的值,如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性就会屏蔽原型中的那个属性。即使是一个 null,也会屏蔽;比如下面的例子:

  function Person(){}
  Person.prototype.name = 'jiegiser'
  Person.prototype.age = 18
  Person.prototype.job = 'giser'
  Person.prototype.sayName = function() {
    console.log(this.name)
  }
  const perosn1 = new Person()
  perosn1.name = 'jieouba' // Person {name: "jieouba"}
  console.log(perosn1)

不过使用 delete 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性。 例如下面的代码:

  function Person() {}
  Person.prototype.name = 'jiegiser'
  Person.prototype.age = 18
  Person.prototype.job = 'giser'
  Person.prototype.sayName = function() {
    console.log(this.name)
  }
  const perosn1 = new Person()
  perosn1.name = 'jieouba' // Person {name: "jieouba"}
  console.log(perosn1) // Person {name: "jieouba"}
  delete perosn1.name
  console.log(perosn1.name) // jiegiser 读取原型上的属性

使用 hasOwnProperty 方法可以检测一个属性是存在于实例中,还是存在与原型中。需要注意的是这个方法是从 Object 中继承来的,只在给定属性存在于对象实例中时,才会返回 true,例子如下:

  function Person() {}
  Person.prototype.name = 'jiegiser'
  Person.prototype.age = 18
  Person.prototype.job = 'giser'
  Person.prototype.sayName = function() {
    console.log(this.name)
  }
  const perosn1 = new Person()
  // perosn1.name='jieouba' // Person {name: "jieouba"}
  console.log(perosn1)
  console.log(perosn1.hasOwnProperty('name')) // false,不是来自实例属性

通过 hasOwnProperty 方法可以检测什么时候访问的是实例属性什么时候访问原型属性就很清楚了。

这里需要注意的是,Object.getOwnPropertyDescriptor() 方法只能用于实例属性。要取得原型属性的描述符,必须直接在原型对象上调用 Object.getOwnPropertyDescriptor() 方法

  console.log(Object.getOwnPropertyDescriptor(perosn1, 'name')) // undefined
  console.log(Object.getOwnPropertyDescriptor(Person, 'name')) // { value: "Person", writable: false, enumerable: false, configurable: true }

# 原型与 in 操作符

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

  function Person(){}
  Person.prototype.name = 'jiegiser'
  Person.prototype.age = 18
  Person.prototype.job = 'giser'
  Person.prototype.sayName=function(){
    console.log(this.name)
  }
  const perosn1 = new Person()
  console.log(perosn1.hasOwnProperty('name')) // false--不是实例属性
  console.log('name' in perosn1) // true----只要能够访问到 name,就是 true,不管该属性是在原型中还是在实例中
  console.log(perosn1)

同时使用 hasOwnProperty 以及 in 方法就可以进行判断该属性到底是在对象中还是在原型中: hasOwnProperty 方法如果是实例属性的就返回 true,不是就返回 false;如果是实例属性定义的,那么返回 true

  function hasPrototypeProperty(object,name) {
    // 属性不是对象中的且属性存在与对象中,也就是判断一个属性;如果是实例属性返回 false,如果是原型对象属性,返回 true
    return !object.hasOwnProperty(name) && (name in object)
  }   
  console.log(hasPrototypeProperty(perosn1, 'name')) // true

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

  function Person() {}
  Person.prototype.name = 'jiegiser'
  Person.prototype.age = 18
  Person.prototype.job = 'giser'
  Person.prototype.sayName = function() {
    console.log(this.name)
  }
  const keys = Object.keys(Person.prototype)
  console.log(keys) // ["name", "age", "job", "sayName"]
  const pi = new Person()
  pi.name = 'Rob'
  pi.age = 23
  const pik = Object.keys(pi) // 获取到可枚举的实例属性
  console.log(pik) // ["name", "age"]

  // 如果想要得到所有实例属性,无论他是否可枚举,都可以使用 Object.getOwnPropertyNames() 方法,例子如下:
  console.log(Object.getOwnPropertyNames(Person.prototype)) // ["constructor", "name", "age", "job", "sayName"]
  console.log(Object.getOwnPropertyNames(pi)) // ["name", "age"]
  console.log(Object.getOwnPropertyNames(pi.__proto__)) // ["constructor", "name", "age", "job", "sayName"]

# 更简单的原型语法

从前面的例子可以看到,每次给对象的原型对象定义属性时,都需要输入 .prototype,很麻烦,可以使用对象字面量的方法,来重写整个原型对象;如下面的代码:

function Person() {}
Person.prototype = {
  name: 'jiegiser',
  age: 18,
  job: 'giser',
  sayName() {
    console.log(this.name)
  }
}

上面的写法有一个例外就是,constructor 构造函数属性不在指向 Person 了,前面曾介绍过,每创建一个函数,就会同时创建它的 prototype,这个对象也会自动获取 constructor 属性;而我们在这里使用的语法,本质上完全重写了默认的 prototype 对象,因此他的原型对象的 constructor 属性也就变成了新对象的 constructor 属性(指向了 Object 构造函数),不再指向 Person 函数,尽管 instanceof 操作符还能返回正确的结果,但通过 constructor 已经无法确定对象的类型了;如下代码:

  const friend = new Person()
  console.log(friend instanceof Object) // true
  console.log(friend instanceof Person) // true
  console.log(friend.constructor === Object) // true
  console.log(friend.constructor === Person) // false
  console.log(friend)

如果在我们的开发中,constructor 的值真的很重要,可以通过下面这样特意将它设置回适当的值:

function Person() {}
Person.prototype = {
  constructor: Person,
  name: 'jiegiser',
  age: 18,
  job: 'giser',
  sayName() {
    console.log(this.name)
  }
}
console.log(Object.getOwnPropertyNames(Person.prototype)) // ["constructor", "name", "age", "job", "sayName"]
const friend = new Person()
console.log(friend instanceof Object) // true
console.log(friend instanceof Person) // true
console.log(friend.constructor instanceof Object) //true
console.log(friend.constructor === Person) // true
console.log(friend.constructor === Object) // false

但是需要注意的是,以这种方式重设 constructor 属性,会导致他的 [[Enumberable]] 特性设置为 true,默认情况下,原生的 constructor 属性是不可以枚举的; 因此也可以使用 defineProperty 方法进行设置 constructor 属性,如下面的例子:

  function Person() {}
  Person.prototype = {
    name: 'jiegiser',
    age: 18,
    job: 'giser',
    sayName() {
      console.log(this.name)
    }
  }
  // 重置构造函数
  Object.defineProperty(Person.prototype, 'constructor', {
    enumerbale: false,
    value: Person
  })

# 原型的动态性

也就是说在原型中查找值的过程是一次搜索,如果我们对原型对象做任何修改都能够立即从实例上反映出来,即使先创建了实例后修改原型也照样如此:

  const friend = new Person()
  Person.prototype.sayHi = function() {
    console.log('hi')
  }
  friend.sayHi() // hi(没有问题)

尽管可以修改原型添加属性以及方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了,我们知道,调用构造函数时,会为实例添加一个指向最初原型__proto__ 指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系;这里需要注意的是实例中的指针指向的是原型,而不是指向构造函数:

  function Person() {}
  const friend = new Person()
  Person.prototype = {
    constructor: Person,
    name: 'jiegiser',
    age: 18,
    job: 'giser',
    sayName() {
      console.log(this.name)
    }
  }
  friend.sayName() // friend.sayName is not a function,因为 Friend 指向的原型中不包含以改名字命名的属性。

重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系,他们引用的仍然是最初的原型。这里的 Person 的 prototype 指向了 new Person prototype;而 friend 的 prototype 指向的是原来的 Person prototype。

# 原型对象的问题

省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值,虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题,原型模式的最大问题是由其共享的本性所导致的。原型中所有属性是被很多实例共享的,这种共享对于函数非常合适,对于那些包含基本值的属性倒也说的过去,毕竟(去前面的例子),通过在实例上添加一个同名属性,可以隐藏原型中的对应属性,然而,对于包含引用类型值的属性来说,问题就比较突出了。比如下面的例子:

  function Person() {}
  const friend = new Person()
  Person.prototype = {
    constructor: Person,
    name: 'jiegiser',
    age: 18,
    friend: ['gao','qi','zhang '],
    job: 'giser',
    sayName() {
      console.log(this.name)
    }
  }
  const person1 = new Person()
  const perosn2 = new Person()
  person1.friend.push('huang')
  console.log(person1.friend) // ["gao", "qi", "zhang ", "huang"]
  console.log(perosn2.friend) // ["gao", "qi", "zhang ", "huang"]
  console.log(person1.friend === perosn2.friend) // true

# 组合使用构造函数模式和原型模式(最常用的模式)

也正是由于上面的问题,所以很少有人单独使用原型模式,而大部分使用组合的模式;创建自定义类型的最常见的方式,就是组合使用构造函数模式与原型模式,构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。这样,每个实例都会用自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数,可谓是集两种模式之长,如下面的代码,重写了上面的例子:

function Person(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.friend = ['jie','huang','gao']
}
Person.prototype = {
  constructor: Person,
  sayName() {
    console.log(this.name)
  }
}
const person1 = new Person('jiegiser', 18, 'giser', 'zhang')
const perosn2 = new Person('jieouba', 18, 'gou')
person1.friend.push('qi')
console.log(person1.friend) // ["jie", "huang", "gao", "qi"]
console.log(perosn2.friend) // ["jie", "huang", "gao"]
console.log(person1.friend === perosn2.friend) // false
console.log(person1.sayName === perosn2.sayName) // true

上面的例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor 和方法 sayName 则是在原型中定义的,而修改了 person1.friend ,并不会影响到 person2.friend,因为他们分别引用了不同的数组。

# 动态原型模式

动态原型模式是把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点;换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型,比如下面的例子:

  function Person(name, age, job) {
    // 属性
    this.name = name
    this.age = age
    this.job = job
    // 方法
    // 如果 sayName 方法不存在,就添加到原型中,这段代码只会在初次调用构造函数才会执行,此后,原型已经完成初始化,不需要在做什么修改了
    // 这里需要注意的是,这里对原型所做的修改,能够立即在所有实例中得到反映。需要注意的是,if 预计检查的可以是初始化之后应该存在的任何属性或方法
    // 不必用一大堆 if 语句检查每个属性和每个方法,只要检查其中一个即可。对于采用这种模式创建的对象,还可以使用 instanceof 操作符确定他的类型
    if(typeof this.sayName !== 'function'){
      Person.prototype.sayName = function() {
        console.log(this.name)
      }
    }
  }
  const person1 = new Person('jiegiser', 18, 'giser')
  person1.sayName() // jiegiser

# 寄生构造函数模式

这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象,但从表面上看,这个函数又很像是典型的构造函数。如下面的例子:

function Person(name, age, job) {
  let o = new Object()
  o.name = name
  o.age = age
  o.job = job
  o.sayName = function(){
    console.log(this.name)
  }
   return o
}
const friend = new Person('jiegiser', 18, 'giser')
friend.sayName() // jiegiser

除了使用 new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的,构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。

这个模式可以在特殊的情况下用来为对象创建构造函数;假设我们想创建一个具有额外方法的特殊数组,由于不能直接修改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')
console.log(colors.toPipedString()) // red|blue|green

寄生构造函数模式:返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么区别。因此不能依赖 instanceof 操作符来确定对象类型。

# 稳妥构造函数模式

所谓稳妥指的是没有公共属性,而且其方法也不引用 this 的对象,稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this 和 new );或者在防止数据被其他应用程序(如 Mashup 程序)改动时使用,稳妥构造函数遵循与寄生构造函数类似的模式,但是有两点:一是新创建对象的实例方法不引用 this,二是不使用 new 操作符调用构造函数,按照稳妥构造函数的要求,可以将前面的 Person 构造函数重写如下:

function Person(name, age, job){
  // 创建要返回的对象
  let o = new Object()
  // 可以在这里定义私有变量和函数
  // 添加方法
  o.sayName = function() {
    console.log(name)
  }
  // 返回对象
  return o
}

这里需要注意的是,以这种模式创建的对象中,除了使用 sayName 方法之外,没有其他方法访问 name 的值,可以像下面使用稳妥的 Person 构造函数

const friend = Person('jiegiser', 18, 'giser')
friend.sayName()

这样变量 friend 中保存的是一个稳妥对象,而除了调用 sayName 方法外,没有别的方式可以访问其数据成员,即使有其他代码会给这个对象添加方法或数据成员;但也不可能有别的方法访问传入到构造函数中的原始数据,稳妥构造函数模式提供的这种安全性,使得他非常适合在某些安全秩序环境。

评 论:

更新: 12/27/2020, 4:59:16 PM