JavaScript学习(3) - 聊聊原型链- 2. 对象与原型

992 阅读16分钟

《JavaScript高级程序设计(第三版)》学习笔记

相关内容:

JavaScript学习(3)- 聊聊原型链- 1. 变量

JavaScript学习(3)- 聊聊原型链- 2. 对象与原型

JavaScript学习(3)- 聊聊原型链- 3. 原型链与继承


关键词:对象的prototype属性;实例属性;创建对象的方式:原型模式、构造函数模式、寄生模式

本章节学习路线:

  1. 什么是对象?对象的属性?

  2. 创建对象的模式:工厂模式、构造模式、 原型模式、动态原型模式 、寄生构造函数模式、稳妥构造函数模式

  3. 原型模式深入理解:

  • 原型模式的优点与问题 - 共享
  • 问题的解决方法:构造函数与原型模式的使用 - 构造函数存实例属性,原型对象存共享属性

1. 关于对象的理解

// 创建对象
var person = {
  name: 'kenny',
  age: 29,
  sayName: function () {
    alert(this.name) // Object中可直接创建对象
  }
}

1.1 属性类型

1.1.1 数据属性

数据属性:包含一个数据值的位置。该位置可进行读取和写入

属性值定义默认值
[[Configurable]]能否修改属性的特性,一旦修改为false,则再也无法恢复true
[[Enumerable]]能否通过for-in循环返回属性true
[[Writable]]能否修改属性的值true
[[Value]]该属性的数据值undefined

Object.defineProperty()可以对属性进行修改:(但是慎重使用)

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

1.1.2 访问器属性

访问器属性:不包含数据值,包含getter和setter

属性值定义默认值
[[Configurable]]能否修改属性的特性,一旦修改为false,则再也无法恢复true
[[Enumerable]]能否通过for-in循环返回属性True
[[Get]]读取属性时调用undefined
[[Set]]写入属性时调用undefined

1.2 定义多个属性 - Object.defineProperties()

var book = {}

Object.defineProperties(book, {
  _year: {
    writable: true,
    value: 2004
  },
  
  edition: {
    writable: true,
    value: 1
  },
  
  year: {
    get: function() {
      return this._year
    }
  }
})

1.3 读取属性的特性 - Object.getOwnPropertyDescriptior()

任何DOM和BOM对象均可使用该方法,以获取对象属性

var descriptor = Object.getOwnPropertyDescriptior(book,  '_year')

alert(descriptor.value) // 2004
alert(descriptor.configurable) // false

2. 创建对象的方式

关键词:工厂模式、构造函数、原型模式、组合使用、动态原型、稳妥构造函数、寄生构造函数

2.1 工厂模式

  • 工厂模式:用函数封装,构造整个对象,并在末尾return这个对象
  • 存在的问题:无法使用instanceof操作符判断对象类型(因该方法并非构造函数模式) - 用构造函数解决该问题
function createPerson (name, age, job) {
  // 1. 创建新对象
  var o = new Object()  
  
  // 2. 为对象添加属性
  o.name = name
  o.age = age
  o.job = job
  o.sayName = function () {
    alert(this.name)
  }
  
  // 3. 需要在末尾return这个对象
  return o 
}

// 调用函数构造实例 - 无需使用new操作符
var person1 = createPerson('Greg', 27, 'Doctor')

// 存在的问题 - 无法使用instanceof判断person1是createPerson类型的对象
alert(person1 instanceof createPerson) // false
alert(person1 instanceof Object)       // true

2.2 构造函数模式

  1. 构造函数:将属性和方法直接赋值给this对象,无需return

  2. 与工厂模式的区别

    1. 没有显式的创造对象
    2. 直接将属性和方法给了this对象
    3. 没有return语句
  3. 解决了工厂模式的问题:用构造函数创建的对象,可以通过instanceof,获取到对象的类型

  4. 构造函数的缺点:功能效果完全相同的属性无法共享,重复占据内存空间 - 用原型模式解决该问题

// 构造函数Person(一般为首字母大写)
function Person (name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.sayName = function() {
    alert(this.name)
  }
}

// 调用构造函数的方式:
// 1. 用new方法,使用构造函数,创建person对象
var person1 = new Person('Greg', 29, 'Doctor')
alert(person1 instanceof Person) // true - 可使用instanceof获得对象类型

// 2. 直接作为普通函数使用,则为给window创建创建对象 - 此时Person()中的this指向window
Person('Greg', 29, 'Doctor')
window.sayName() // 'Greg'


// 存在的问题:构造函数创建的属性为实例属性,其中功能效果完全相同的属性无法共享,重复占据内存空间
alert(person1.sayName == person2.sayName) // false

/*
 * 例如:通过Person构造的两个对象的sayName()是不等价的,但两者执行的功能是一致的
 * 普通解决方法:将共享属性从构造函数中移出,作为全局属性
 * 产生的新的问题:如果一个对象需要有多个function,就要全局声明多个function,违反了封装性
 * 最佳解决方案:原型模式
 */
function Person (name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.sayName = sayName
}
function sayName() { // 全局function - 缺乏封装性
  alert(this.name)
}

2.3 原型模式 - prototype

2.3.1 原型模式的创建方法与基本概念

  1. 原型模式:每一个创建的函数、Object都有一个prototype属性
  2. prototype属性是一个指针,指向一个对象,包含由特定类型的所有实例共享的属性核方法
  3. 优点:让所有对象实例均可共享其所包含的属性和方法,而无需在构造函数中定义对象实例的信息
  4. 注意:实例中创建的同名属性,会覆盖掉原型中的属性
function Person() {} // 构造函数Person
  
// 在Person的原型中添加属性,而非在构造函数中添加
Person.prototype.name = 'Nicholas'
Person.prototype.name = 29
Person.prototype.sayName = function () { alert(this.name) }
  
// 创建对象 - 使用Person构造函数创建Person对象,该对象可以直接使用Person中的原型属性
var person1 = new Person()
person1.sayName() // 'Nicholas'
  
var person2 = new Person()
person2.sayName() // 'Nicholas'
  
alert(person1.sayName == person2.sayName) // true - 创建不同对象时,均指向同一个原型属性(同一个指针)
  1. 另一种创建原型的方法:对象字面量法
// 对象字面量法
Person.prototype = {
  name: 'Nicholas',
  age: 29,
  job: 'Doctor',
  sayName: function() {
    alert(this.name)
  }
}

对象字面量法存在问题:prototype对象的constructor属性,不再指向Person

原因:通过上述方法,完全重写了默认的prototype对象,所以constructor属性变成了新对象的constructor属性,指向Object构造函数,而非Person函数

var person1 = new Person()
alert(person1 instanceof Object) // true
alert(person1 instanceof Person) // true
alert(person1.constructor == Person ) // false - person1对象的构造函数因为重写prototype对象,所以直接指向了Object
alert(person1.constructor == Object) // true

// 处理上述问题的方法:在重写的对象中也把constructor进行赋值返回
Person.prototype = {
  constructor: Person, // 在重写的原型对象中,把Person设置为constructor的值
  name: 'Nicholas',
  age: 29,
  job: 'Doctor',
  sayName: function() {
    alert(this.name)
  }
}
// 但上述方法会导致constructor变成可枚举的值,实际上constructor是默认不可枚举的,所以需要用defineProperty方法进行设置
Object.defineProperty(Person.prototype, 'constructor', {
  enumerable: false, // 重置为false
  value: Person
}) 

2.3.2 原型对象的理解 - 共享属性

  1. 图解上述代码样例: 图解说明如下:

    1. Person Prototype - 原型对象:包含构造函数constructor和属性值:name、age、sayName
    2. Person 构造函数:原型Prototype指向整个Person原型对象;原型对象中的构造函数指回了Person构造函数
    3. 利用Person构造函数创建的Person1和Person2对象:包含一个[[Prototype]]属性,该属性指向的就是Person原型对象 所以,Person1和Person2对象,均指向的是同一个原型对象中的sayName属性

image.png

A. 原型对象解析(根据上图总结):

1. 创建一个新函数function,就会为该函数创建一个prototype属性,该属性指向函数的原型对象
2. 所有的原型对象都会自动获得一个constructor属性,该属性是一个指向prototype属性所在函数的指针(注意:指针)
3. 通过构造函数创建一个对象后,该对象内部将包含一个`[[prototype]]`指针,指向构造函数的原型对象

B. 可以通过isPrototype()Object.getPrototypeOf()方法确定对象是否指向构造函数(因为在常规实现中无法访问):

// 1. 用isPrototype()方法确定对象和构造函数之间关于prototype的关系
Person.prototype.isPrototypeOf(person1) // true

// 2. 用es5提出的Object.getPrototypeOf()来返回[[Prototype]]的值
Object.getPrototypeOf(person1).name // 'Nicholas'
Object.getPrototypeOf(person1) == Person.prototype // true

C. 无法通过对象实例重写原型中的值,只能通过在实例中创建同名的该属性,以屏蔽原型中的属性

// 1. 创建构造函数Person
function Person() {}

// 2. 创建Person的原型对象
Person.prototype.name = 'Nicholas'
Person.prototype.age = 29
Person.prototype.job = 'Doctor'
Person.prototype.sayName = function() { alert(this.name) }

// 3. 创建Person的对象person1
var person1 = new Person()

// 4. 在实例(constructor)中创建name属性值
person1.name = 'Greg'

// 5. 实例中的name属性覆盖掉prototype中的name属性
alert(person1.name) // 'Greg'

// 6. 通过delete删除实例中的属性值
delete person1.name

// 7. 由于删除了实例中的属性,所以调用时候会展示原型中的属性值
alert(person1.name) // 'Nicholas'

2.3.3 判断属性在原型还是在实例中的方法

A. 判断属性是否来自于实例:hasOwnProperty() - 来自于实例中则返回true

var person1 = new Person()

// 1. name属性来自于原型中,故返回false
alert(person1.hasOwnProperty('name')) // false

// 2. 创建实例中的属性,则检测后返回true
person1.name = 'Greg'
alert(person1.hasOwnProperty('name')) // true

B. 判断属性是否来自于实例或对象:hasOwnProperty()、单独使用in操作符

  1. in操作符:可单独使用或在for-in循环中使用,单独使用时可以判断属性是否存在于实例或原型
// 用in方法确认name属性是否存在于实例或原型中
var person1 = new Person()
alert('name' in person1) // true --- name属性存在于实例或原型中
  1. Object.hasOwnProperty()为Object方法,可用于判断属性是否存在于实例
// 用Object.hasOwnProperty()方法确认属性是否存在于实例中
var person1 = new Person()
alert(person1.hasOwnProperty('name')) // false --- name属性不存在于实例中
  1. 另外:可以将上述两个方法进行自定义封装,从而实现判断属性是否存在于原型
// 利用Object.hasOwnProperty()和in操作符,封装一个自定义的方法,判断属性是否存在于原型中
function hasPrototypeProperty(object, name) {
  return !object.hasOwnProperty(name) && (name in object)
}
alert(hasPrototypeProperty(person, 'name')) // true --- name属性存在于原型中

C. 通过for-in方法可访问Enumerated为true的属性值,但会屏蔽Enumerated为false的值

// 通过Object.keys()方法,返回包含所有可枚举属性的字符串数组
var keys = Object.keys(Person.prototype)
alert(keys) // 'name, age, job, sayName'

// 通过Object.getOwnPropertyNames()方法,返回所有实例属性(无论是否可枚举)
var keys2 = Object.getOwnPropertyNames(Person.prototype)
alert(keys2) // 'constructor, name, age, job, sayName' --- constructor不可枚举,但可以通过该方法获取

2.3.4 原型的动态性 - 动态修改原型

A. 动态修改原型:对原型对象所做的任何修改可立即在实例中反映出来

// 1. 直接创建对象friend
var friend = new Person()

// 2. 然后为Person添加原型对象属性sayHi
Person.prototype.sayHi = function() {
  alert('Hi')
}

// 3. 对象friend可以使用新添加的属性值
friend.sayHi() //'Hi'

B. 注意:如果使用对象字面量法重写原型对象,会切断构造函数和最初原型对象的关系

原因:实例中的指针是指向原型,而非构造函数

// 1. 直接创建对象friend
var friend = new Person()

// 2. 然后为Person重写原型对象,包含属性sayHi
Person.prototype = {
  constructor: Person, // 在重写的原型对象中,把Person设置为constructor的值
  name: 'Nicholas',
  age: 29,
  job: 'Doctor',
  sayName: function() {
    alert(this.name)
  }
}

// 3. 对象friend不能使用通过重写原型对象而新添加的属性值,
// 原因:Person本身指向了一个全新的prototype对象,而非friend指向的原有的原型对象
friend.sayHi() //error

2.3.5 原生对象也具备原型模式

  1. 原生对象:例如:Array、String等
  2. 所有的原生的引用类型,都是上述模式创建的,具备原型模式。例如:Array.prototype中可以找到sort()方法;String.prototype中可以找到substring()方法
  3. 可以随时给原生对象添加新的原型方法,但是不推荐使用

2.3.6 成也原型,败也原型 - 浅析原型对象的问题:共享属性

  1. prototype最优秀的特性,就是具备共享能力,可以把一些公有的方法、属性作为原型属性,让所有实例共享使用
  2. 但,prototype的缺点也来自于这种共享的特性:如果prototype中存在引用类型,则会使不同实例的该属性值相互干扰
  3. 解决方案:共享属性放在原型中,非共享属性(需避免干扰的)放在构造函数中 ------------ 即为:构造函数模式和原型模式的组合使用(详见2.4节)
// 创建构造函数Person
function Person() {}

// 创建原型对象,其中包含引用类型friends(Array)
Person.prototype = {
  constructor: Person, // 在重写的原型对象中,把Person设置为constructor的值
  name: 'Nicholas',
  age: 29,
  job: 'Doctor',
  friends: ['Sheldon', 'Raj']
  sayName: function() {
    alert(this.name)
  }
}

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

// person1对象中,为引用对象属性friends赋值
person1.friends.push('Penny')

// 由于原型对象的共享性,所以所有通过Person()构造函数创建的对象,都具备相同的原型对象属性,原型对象中的引用对象会指向同一个值
// 问题:两个实例的共享属性互相干扰
alert(person1.friends) // 'Sheldon, Raj, Penny'
alert(person1.friends) // 'Sheldon, Raj, Penny'
alert(person1.fiends === person2.friends) // true

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

  1. 用以解决上述原型模式共享属性带来的互相干扰的问题,并最大限度节省内存空间
  2. 充分结合利用构造函数模式与原型模式的特点:
    1. 构造函数模式:定义实例属性
    2. 原型模式:定义方法和共享的属性
// 创建构造函数Person - 非共享属性,尤其是引用类型,作为实例属性
function Person(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.friends = ['Sheldon', 'Raj']
}

// 创建Person的原型对象 - 将共享的属性,例如function,作为原型属性
Person.prototype = {
  constructor: Person, 
  sayName: function() {
    alert(this.name)
  }
}

var person1 = new Person('Nicholas', 29, 'Doctor')
var person2 = new Person('Cindy', 28, 'Developer')
person1.friends.push('Van')

// 实例属性互不相同,但共享属性指向同一指针
alert(person1.friends === person2.friends) // false
alert(person1.sayName === person2.sayName) // true

2.5 动态原型模式

  1. 动态原型模式:通过检查某个应该存在的方法是否有效,来决定是否需初始化原型
  2. 优点:可以更进一步优化内存空间
  3. 注意:使用动态原型模式时,不能使用对象字面量重写原型。因为已经创建了实例的情况下重写原型,会切换现有实例和新原型之间的关系
function Person(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.friends = ['Sheldon', 'Raj']
  
  // 仅在sayName不存在的情况下,才将其添加到原型中
  // 添加方法不可使用对象字面量方法,
  if (typeof this.sayName != 'function') {
    Person.prototype.sayName = function() {
      alert(this.sayName)
    }
  }
}

2.6 寄生构造函数模式

  1. 寄生构造函数模式:创建一个函数,该函数的作用仅为封装创建对象的代码,再返回新创建的对象
  2. 用途:用于将原生对象(例如:Array)添加自定义属性方法而不修改原生对象的构造函数(对原生对象的增强)
  3. 特点
    1. 创建方法类似工厂模式,但工厂模式的函数不是构造函数,本模式的包装函数可以称为构造函数
    2. 可以使用new操作符
    3. 构造函数中的返回对象可以重写
    4. 构造函数返回的对象,与在构造函数外部创建的对象相同
    5. 无法通过instanceof来确定对象的类型
  4. 不推荐使用该模式!
// 样例1:
// 以类似工厂模式的方法,创建寄生构造函数模式
function Person(name, age, job) {
  var o = new Object() // 创建对象
  o.name = name
  o.age = age
  o.job = job
  o.sayName = function () {
    alert(this.name)
  }
  return o // 返回对象
  // 如果不设置返回值,则默认会返回新对象实例;故可以在寄生模式中重写返回值
}

// 可以使用new操作符创建,因为上述方法可以被认为是构造函数
var friend = new Person('Nicholas', 29, 'doctor')
friend.sayName() // 'Nicholas'



// 样例2:重写并为Array方法添加属性
// 不能修改Array构造函数,但是可以使用寄生模式为对象添加构造函数
function SpecialArray () {
  var values = new Array()              // 创建数组对象
  values.push.apply(values, arguments)  // 添加值
  values.toPipedString = function () {  // 添加方法
    return this.join('|')
  }
  return values                         // 返回数组
}

var colors = new SpecialArray('red', 'blue', 'green')
alert(colors.toPipedString())          // 'red|blue|green'

2.7 稳妥构造函数模式

  1. 稳妥对象:没有公共属性,方法不引用this的对象,适合在安全环境中使用(安全模式下禁止this)
  2. 与寄生模式创建方法相似,不同点如下
    1. 新创建对象的实例方法不引用this
    2. 不使用new操作符调用构造函数
  3. 注意:稳妥模式下不能使用对象字面量重写原型,原因:对象字面量方法会切断与最初构造函数之间的关系
function Person(name, age, job) {
  // 创建对象
  var o = new Object() 
  o.name = name
  o.age = age
  o.job = job
  
  // 不使用this对象
  o.sayName = function () {
    alert(name) 
  }
  
  // 返回对象
  return o 
}

var friend = Person('Nicholas', 29, 'doctor')

// 除了使用Person中的sayName,不能通过其他方法获取name,故适合安全环境
friend.sayName() // 'Nicholas'

3. 小结

  1. 对象的基本属性:数据属性、访问器属性

  2. 创建对象的方式: | 创建对象方式 | 简要说明 | | ---------------- | ------------------------------------------------------------ | | 工场模式 | 用函数封装,构造整个对象,并在末尾return这个对象
    缺点:无法使用instanceof操作符判断对象类型(因该方法并非构造函数模式)
    解决方案:用构造函数解决该问题 | | 原型模式 | 直接使用prototype,利用prototype的共享特性;
    缺点:引用对象存于prototype时,会导致创建的不同实例的该原型属性值互相干扰
    解决方案:构造+原型组合模式 | | 动态原型 | 通过检查某个应该存在的方法是否有效,来决定是否需初始化原型,以更进一步优化存储空间 | | 构造+原型 | 构造部分存自身实例属性
    原型部分存共享的属性(例如function) | | 寄生构造函数函数 | 可用于给原生对象(例如Array)增强添加一些自定义方法或属性而不破坏原有对象构造函数
    缺点:不可用instanceof确定对象类型 | | 稳妥构造函数 | 新创建对象的实例方法不引用this,在安全模式下稳妥运行
    不使用new操作符调用构造函数 |

  3. 原型的问题:引用对象放在prototype中,会因为共享导致一处改变、处处改变

  4. 寄生的本质:增强 - 在原有基础上添加自定义新属性


笔记目录:

JavaScript学习(1) - JavaScript历史回顾

JavaScript学习(2) - 基础语法知识

JavaScript学习(3)- 聊聊原型链- 1. 变量

JavaScript学习(3)- 聊聊原型链- 2. 对象与原型

JavaScript学习(3)- 聊聊原型链- 3. 原型链与继承

JavaScript学习(4)- 聊聊闭包那些事

Github:

Github笔记链接(持续更新中,欢迎star,转载请标注来源)