红宝书第六章——面向对象的程序设计(读书笔记)

1.对象的属性类型
ES中有两种属性:数据属性访问器属性

1.1 数据属性
数据属性包含一个数据值的位置,数据属性有4个行为描述其行为的特性。

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

如果直接在一个对象上定义属性,他的[[Configurable]]、[[Enumerable]]、[[Writable]]都将被置为true,而[[Value]]将被设置为特定的值
如果要修改属性默认的特性,必须使用object.defineProperty()方法;这个方法接受三个参数:属性所在的对象、属性的名字和一个描述符对象

var person = {
  name: 'jack'
}

Object.defineProperty(person, 'name', {
  // writable: false,
  configurable: false
})
delete person.name
console.log(person.name) // jack

这里需要注意的是,一旦把属性定义为不可配置的,就不能再将它便会可配置的了。(会抛出错误)
其次在调用object.defineProperty()方法创建一个新属性的时候,如果不指定Configurable、Enumerable、Writable特性的默认值都是false。
多数情况下可能都没有必要利用object.defineProperty()方法提供的这些高级功能,但是对理解javascript对象确非常有用。

1.2访问器属性

[[Configurable]]:表示能否通过delete删除属性从而重新定义属性
[[Enumerable]]:表示能否通过for-in循环返回属性
[[Get]]:在读取属性的时候调用的函数,默认值为undefined
[[Set]]:在写入属性的时候默认调用的函数,默认为undefined
需要注意的是,get和set函数都不是必需的

下面有一个例子

let date = {
  _year: 2019, // 下划线表示只能通过对象方法访问的属性???
  version: 0
}

Object.defineProperty(date, 'year', {
  // 只指定getter意味着属性是不能写的,尝试写入属性会被忽略,严格模式下则会报错
  get: function () {
    return this._year
  },
  // 只指定setter以为着属性是不能读的,非严格模式下返回undefined,严格模式下则会报错
  set: function (newValue) {
    this._year = newValue
    this.version = newValue - 2019
  }
})

date.year = 2020
console.log(date.version) // 1
console.log(date.year) // 2020

tips:可以使用object.defineProperties()方法一次性地定义多个属性

var obj = {};
Object.defineProperties(obj, {
  'name': {
    value: jack,
    writable: true
  },
  'age': {
    value: 18,
    writable: false
  }
});

tips:使用object.getOwnPropertyDescriptor方法可以获取给定属性的描述符

const object1 = {
  age: 42
}

const descriptor1 = Object.getOwnPropertyDescriptor(object1, 'age');
console.log(descriptor1.configurable); //true
console.log(descriptor1.value); //42

2.创建对象

2.1工厂模式,用函数来封装以特定接口创建对象的细节

function createPerson(name, age){
  let o = new Object()
  o.name = name
  o.age = age
  o.sayName = function(){
    console.log(this.name)
  }
  return o
}
// 工厂模式虽然解决了创建多个对象的问题,但是却没有解决对象识别的问题,即怎样知道一个对象的类型

2.2构造函数模式

// 默认构造函数的首字母大写
function Person(name, age){
  this.name = name
  this.age = age
  this.sayName = function(){
    console.log(this.name)
  }
}
let per = new Person('jack', 18)
// 没有显式地创建对象、直接将属性和方法复制给this对象,没有return语句
console.log(per.constructor === Person) // true
// 对象的constructor属性最初式用来表示对象类型的 ,但是更可靠的是使用instanceof来检测对象类型

就可以使用new来创建一个对象,其实使用new创建对象的过程可以分为以下四个步骤:创建一个新对象、将构造函数的作用域赋值给新对象、执行构造函数中的代码、返回新对象
创建自定义构造函数意味着可以将它的实例标识为一种特定的类型,而这也是构造函数模式胜过工厂模式的地方。

tips:使用构造函数模式的问题:每个方法都要在每个实例上重新创建一遍,而创建两个完成同样任务的方法的确是没有必要的。

let per1 = new Prson()
let per2 = new Person()
// 这样sayName方法就被创建了两遍

2.3原型模式
我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它包含的属性和方法。

function Person(){}
Person.prototype.name = 'jack'
Person.prototype.age = 18
Person.sayName = function(){
  console.log(this.sayName)
}
let per = new Person()
per.sayName() // jack

无论什么时候,只要创建了一个函数,就会根据一组特定的规则来为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性是一个指向prototype属性所在函数的指针。
ES5中新增了一个方法,object.getPrototypeOf()方法,使用object.getPrototypeOf()方法可以很简单的获得一个对象的原型

console.log(Object.getPrototypeOf(per) === Person.prototype) // true

tips:每当代码读取某个对象的属性时,都会执行一次搜索,目标是具有给定名字的属性,搜索首先从实例本身开始,如果实例中找到了给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象查找具有给定名字的属性,如果找到则返回该属性的值。
虽然可以通过对象访问到保存在原型中的值,但是却不能通过对象实例重写对象中的值。(使用delete可以删除实例中的属性)

tips:使用hasOwnPrototype()方法可以检测一个属性是存在于实例还是存在于原型中。(如果存在于实例中则返回true)

tips:有两种方法使用in操作符,单独使用和在for-in循环中使用,单独使用in操作符的时候会在通过对象能够访问到给定属性的时候返回true,无论该对象是存在于实例中还是存在于原型中;在使用for-in循环时,返回的是所有能够通过对象访问的,可枚举的属性,其中既包括了存在于实例中的属性,也包括了存在于原型中的属性。

tips:要取得对象上所有可枚举的实例属性,可以使用ES5的Object.keys方法,接受一个对象作为一个参数,返回一个包含所有可枚举属性的字符串数组。返回的顺序也是使用for-in循环中出现的顺序。

function Person () { }
Person.prototype.name = 'jack'
Person.prototype.age = 18
console.log(Object.keys(Person.prototype)) //['name','age']
let per = new Person()
console.log(Object.keys(per)) //[]
per.age = 19
console.log(Object.keys(per)) //['age']

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

function Person () { }
Person.prototype.name = 'jack'
Person.prototype.age = 18
console.log(Object.keys(Person.prototype)) //['constructor','name','age']

tips:更简单的原型语法

function Person(){}
Person.prototype = {
  name: 'jack',
  age: 18,
  sayName: function(){
    console.log(this.name)
  }
}
// 这里将Person的原型对象设置为一个对象字面量形式创建的一个新对象,最终的结果是相同的,但是有个例外,就是constructor属性不在指向Person了。可以重新定义一个constructor属性
function Person(){}
Person.prototype = {
  constructor: Person,
  name: 'jack',
  age: 18,
  sayName: function(){
    console.log(this.name)
  }
}
// 以这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true,默认情况下,原生的constructor属性是不可枚举的。当然你也可以使用Object.defineProperty来定义此属性是不可枚举的

tips:原型的动态性;由于在原型中查找值的过程是一次搜索,因此我们对对象所作的任何修改都能够立即从实例上反应出来,即使是先创建了实例后修改原型也是如此

function Person(){}
let per = new Person()
Person.prototype.age = 18
console.log(per.age) // 18

但是如果是重写了整个原型对象就不是如此了。使用对象字面量的形式修改原型,相当于切断了构造函数与最初原型之间的联系,而实例中的指针仅指向原型,而不是指向构造函数

function Person () { }
let per = new Person()
Person.prototype = {
  age: 18,
  sayName: function () {
    console.log(this.name)
  }
}
console.log(per.name) // error,非严格模式下返回undefined

tips:原生对象的原型

// 此方法给基本包装类型String添加了一个名为startsWith的方法
String.prototype.startsWith = function(text){
  return this.indexOf(text) === 0
}

tips:原型对象的问题:省略了构造函数传递初始化参数的这一环节,结果所的实例都会在默认情况下取得相同的属性值。

function Person(){}
Person.prototype = {
  name: 'jack',
  age: 18,
  friends: ['tony', 'mike']
}
let per1 = new Person()
let per2 = new Person()
per1.friends.push('bobo')
console.log(per2.friends) // tony, mike, bobo

2.4组合使用构造函数模式和原型模式
创建自定义类型最常见的方式,就是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性,这样每个实例都会有自己的一份实例属性,但同时又共享着方法的引用,极大限度的节省了内存。而且这种混成模式还支持向构造函数传递参数,是目前在ES中使用最广泛、认同度最高的一种创建自定义类型的方法。例子如下

function Person(name, age){
  this.name = name
  this.age = age
  this.friends = ['tony', 'mike']
}
Person.prototype = {
  constructor: Person,
  sayName: function(){
    console.log(this.name)
  }
}

let per1 = new Person()
let per2 = new Person()
console.log(per1.friends === per2.friends) // false
console.log(per1.sayName === per2.sayName) // true

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

// 这里只有在sayName方法不存在时候才会将它添加到原型中
function Person(name, age){
  this.name = name
  this.age = age
  
  if(typeof this.sayName != function){
    Person.prototype.sayName = function(){
      console.log(this.name)
    }
  }
}

这里需要注意的是不能用对象字面量的方法重写原型,因为使用对象字面量,就会切断实例与新原型之间的联系。

2.6寄生构造函数模式(这种模式跟工厂模式其实是一模一样的)
寄生构造函数模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。这里就不做过多的解释,下面是代码

function Person(name, age){
  let o = new Object()
  o.name = name
  o.age = age
  o.sayName = function(){
    console.log(this.name)
  }
  return o
}

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

function SpecialArray(){
  // 创建数组
  let values = new Array()
  // 添加值
  values.push.apply(values, arguments)
  //添加方法
  values.toPipedString = function(){
    return this.join('|')
  }
  // 返回数组
  return values
}
let friends = new SpecialArray('jack', 'mike', 'tony')
console.log(friends.toPipedString()) //'jack|mike|tony'

2.7稳妥构造函数模式
所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this对象。稳妥构造函数最适合在一些安全的环境中(这些环境中会禁止使用this和new),或者防止数据被其他应用程序(如Mashhup程序改动时使用),下面是一个例子

function Person (name, age) {
  let o = new Object()

  //可以在这里定义私有变量和函数

  o.sayName = function () {
    return name
  }
  o.sayAge = function () {
    return age
  }
  // 不要将变量挂载到对象o中
  return o
}

//创建对象的时候可以不用new
let person1 = new Person('jack', 10)
console.log(person1.name) // undefined
console.log(person1.sayName()) // jack

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

3.继承
ES的实现继承主要时依靠原型链来实现的
3.1原型链继承
定义:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。假如我们让原型对象指向另一个类型的实例,,此时原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含一个指向另一个构造函数的指针,如果上述关系成立,如此层层递进,就构成了实例与原型之间的链条。这就是所谓原型链的基本概念。
概念比较抽象,我们可以看一下实际的代码+图来进行理解;如下是实现原型链的一种基本模式

function SuperType(){
  this.prototype = true
}
SuperType.prototype.getSuperValue = function(){
  return this.prototype
}

function SubType(){
  this.subPrototype = false
}
// 继承了SuperType
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function(){
  return this.subPrototype
}
let instance = new SubType()
console.log(instance.getSuperValue()) // true

在这里插入图片描述
这里的实现原型链的本质其实就是重写了原型对象,里面prototype属性是位于Subprototype.prototype中的,这是因为prototype是一个实例属性,而getSuperValue是一个原型方法。

tips:所有函数的默认原型都是object的实例
tips:可以使用instanceof方法判断原型与实例之间的关系
tips:使用isPrototypeOf()方法也可以判断实例与原型之间的关系,只要原型链中出现过的原型,isPrototypeOf()方法都会返回true
tips:给原型添加方法的代码一定要放在替换原型之后,这是因为使用对象字面量的方式创建原型方法,相当于重写了原型链。可以看以下代码

function SuperType () {
  this.prototype = true
}
SuperType.prototype.getSuperValue = function () {
  return this.prototype
}

function SubType () {
  this.subPrototype = false
}

// 将定义原型方法提前
SubType.prototype.getSubValue = function () {
  return this.subPrototype
}
// 使用字面量,会导致上一行代码无效。
SubType.prototype = new SuperType()

let instance = new SubType()
console.log(instance.getSubValue()) // error

tips:原型链的问题
第一个问题是包含引用类型值得原型属性会被所有的实例共享,上面有解释过,这里就不再次贴代码了;第二个问题就是创建子类型的时候,不能向超类构造函数传递参数。以上原因导致实践中很少会单独使用原型链。

3.2借用构造函数实现继承
在解决原型中包含引用类型值所带来的问题的过程中,一开始使用一种叫做借用构造函数的技术。这种技术也可以解决向父类传递参数的问题,这里将代码合并在一起。代码如下

function SuperType (name) {
  this.name = name
  this.colors = ['red', 'yellow']
}
function SubType () {
  // 调用SuperType构造函数,实例的colors互相不影响
  SuperType.call(this, 'jack')
}
let instance1 = new SubType()
instance1.colors.push('blue')
console.log(instance1.colors) // red,yellow,blue
console.log(instance1.name) // jack
let instance2 = new SubType()
console.log(instance2.colors) // red, yellow 

借用构造函数的问题:方法都在构造函数中定义,因此函数的复用就无从谈起。所以单独使用构造函数的技术也是很少见的。

3.3组合继承(是Javascript中最常用的继承模式)
组合继承有时候也叫伪经典继承,指的是将原型链和借用构造函数的技术组合到一块。通俗讲便是使用原型链实现对原型属性和方法的继承,而借用构造函数实现对实例属性的继承;这样,既通过在原型上定义方法实现了函数的复用,又能保证每个实例都有自己的属性。
可以来看实现的一个例子

function SuperType (name) {
  this.name = name
  this.colors = ['red', 'yellow']
}
SuperType.prototype.sayName = function(){
  console.log(this.name)
}
function SubType (name, age) {
  // 继承属性
  SuperType.call(this, name)
  this.age = age
}
// 继承方法
SubType.prototype = new SuperType()
// 重写constructor指针的指向
SubType.prototype.constructor = SubType

SubType.prototype.sayAge = function(){
  console.log(this.age)
}
// 测试用例就不写了,大家可以自行测试

3.4原型式继承
直接贴代码

function object (o) {
  function F () { }
  F.prototype = o
  return new F()
}

let person = {
  name: 'jack',
  friends: ['tony', 'mike']
}
let anotherPerson = object(person)
anotherPerson.name = 'xiaoming'
anotherPerson.friends.push('bobo')
console.log(person.friends) // tony, mike, bobo

这种原型式继承要求你必须有一个对象可以作为另一个对象的基础,以上代码相当于创建了Person对象的副本。
tips:ES5通过新增了一个Object.create方法规范了原型式继承,方法接收两个参数,一个是用作新对象的原型对象,另一个是(可选的)为新对象定义额外属性的对象;具体使用就是将上述的自定义object()方法换成Object.create(),大家可以自行尝试。这里不做过多的说明。

3.5寄生式继承
寄生式继承的思路与寄生构造函数和工厂模式是很类似的,即创建一个用于封装继承过程的函数,改函数在内部以某种方式来增强对象,最后进行返回。代码如下

function createAnother (original) {
  // 调用函数创建一个新对象,object在之前定义过
  let flag = object(original)
  // 定义自己的方法
  flag.say = function () {
    console.log('hello')
  }
  return flag
}
let person = {
  name: 'jack',
  friends: ['mike', 'tony']
}
let anotherPerson = createAnother(person)
anotherPerson.say() // hello

前面实例的object函数不是必须的,任何能够返回新对象的函数都适用与此模式

3.6寄生组合式继承
前面说组合继承是javascript最常用的继承模式,不过它也有自己的不足,就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。可以看看之前组合继承的例子

function SuperType (name) {
  this.name = name
  this.colors = ['red', 'yellow']
}
SuperType.prototype.sayName = function(){
  console.log(this.name)
}
function SubType (name, age) {
  // 继承属性
  SuperType.call(this, name) // 第二次调用
  this.age = age
}
// 继承方法
SubType.prototype = new SuperType() //第一次调用
// 重写constructor指针的指向
SubType.prototype.constructor = SubType

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

说完组合继承我们重新回到寄生组合式继承,所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
本质上就是通过适用寄生式继承来继承超类型的原型,然后再将结果只当给子类型的原型。代码会更直观一点

// 这是实现寄生组合式继承的最简单的形式
function inheritPrototype(subtype, supertype){
  let flag = object(supertype.prototype) //创建对象
  flag.constructor.subtype //增强对象
  subtype.prototype = flag // 指定对象
}

这样我们就可以通过inheritPrototype()函数其替换前面例子中;使用如下

function inheritPrototype(subtype, supertype){
  let flag = object(supertype) //创建对象
  flag.constructor.subtype //增强对象
  subtype.prototype = flag // 指定对象
}

function SuperType(name){
  this.name = name
  this.colors = ['red', 'yellow']
}
SuperType.prototype.sayName = function(){
  console.log(this.name)
}
function SubType(name, age){
  SuperType.call(this, name)
  this.age = age
}
inheritPrototype(SuperType, SubType)
SubType.prototype.sayAge = function(){
  console.log(this.age)
}

这个例子的高效率就体现在只调用了一次SuperType()构造函数,并且因此避免了在SubType.prototype上面创建不必要的、多余的属性。与此同时,原型链还能保持不变,;因此还能够正常地使用instance方法和inPrototypeOf()方法。开发人员普遍认为寄生组合式继承是引用类型最理想的继承方式。

到此就结束了,希望这个笔记也能够帮助到大家。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值