知识很多很杂,需要掌握原型链,其余可选;
创建对象:工厂模式;构造函数模式;原型模式;动态原型模式……
继承:原型链;借用构造函数;寄生式继承……

书摘&心得

1、面向对象的程序设计

使用Object.defineProperties()方法可以通过描述符一次定义多个属性
ECMAScript中有两种属性:数据属性、访问器属性
1、数据属性
要修改属性默认的特性,必须使用ES5的Object.defineProperty()方法
有4个描述其行为的特性

  • [[Configurable]]
    • 设为false则不能从对象中删除属性
    • 直接在对象上定义的属性,该特性默认值为true
    • 一旦把属性定义为不可配置,就不能把它变回可配置了
    • 可多次调用Object.defineProperty()修改同一属性,但在把configurable设置为false后就会有限制了
  • [[Enumerable]]
    • 表示能否通过for-in循环返回属性
    • 直接在对象上定义的属性,该特性默认值为true
  • [[Writable]]
    • 表示能否修改属性的值
    • 直接在对象上定义的属性,该特性默认值为true
    • 设为false则属性只读
  • [[Value]]
    • 包含这个属性的数据值
    • 默认值未undefined

2、访问器属性

  • 不包含数据值,包含一对getter和setter函数
    • getter负责返回有效的值
    • setter负责决定如何处理数据
  • 特性:[[Configurable]]、[[Enumerable]]、[[Get]]、[[Set]]
  • 访问器属性不能直接定义,必须使用Object.defineProperty()定义
  • 常见使用场景:
    • image.png
    • 上例通过修改year属性会导致_year和edition改变
    • 这是使用访问器属性的常见方式:设置一个属性的值导致其他属性发生变化
  • 不一定非要同时指定getter和setter

    • 只指定getter意味属性不能写
    • 只指定setter的属性不能读

      2、创建对象

      2.1 工厂模式

      抽象了创建具体对象的过程。
      用函数封装以特定接口创建对象的细节。
      image.png
      虽然解决了创建多个相似对象的问题,但却没用解决对象识别的问题(即怎样知道一个对象的类型)

      2.2 构造函数模式

      1、特点
  • ESMAScript中的构造函数可以用来创建特定类型的对象。

  • 像Object和Array这样的原生构造函数在运行时会自动出现在执行环境中。
  • 可以创建自定义的构造函数定义对象类型的属性和方法。
  • 构造函数始终都应该以大写字母开头,非构造函数则应该以一个小写字母开头。
  • 创建自定义的构造函数意味着将来可将它的实例标识为一种特定的类型,也是构造函数模式胜过工厂模式的地方
  • 任何函数只要通过new操作符来调用就可以作为构造函数
  • 构造函数模式和工厂模式的不同:
    • 没有显示地创建对象
    • 直接将属性和方法赋给了this对象
    • 没有return语句

2、原理
使用构造函数模式将前面的例子重写如下:
image.png
这种方式调用构造函数会经历以下4个步骤:

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

3、构造函数的问题

  • 每个实例都会包含一个不同的Function实例,因此不同实例上的同名函数是不相等的
  • 创建两个完成同样任务的function实例没有必要
  • 根本不用在执行代码前就把函数绑定到特定对象上面
  • 解决方案:将函数定义转移到构造函数外部

image.png

然而,将函数定义转移到构造函数外部会导致新的问题:若对象需要定义很多方法那就要定义多个全局函数,会导致自定义的引用类型毫无封装性可言。好在,这些问题可以通过使用原型模式来解决。

2.3 原型模式

1、特性

  • 每个函数都有一个prototype属性(一个指针),指向一个包含可由特定类型的所有实例共享的属性和方法。
  • 不必在构造函数中定义对象实例的信息,而是将这些信息直接添加到原型对象中
  • 可用包含所有属性和方法的对象字面量来重写整个原型对象image.png
    • 问题:这种方式重设consturctor属性会导致它的[[Enumerable]]特性被设置为true。默认情况下原生的constructor属性是不可枚举的。
    • 在ES5中可以通过Object.defineProperty()解决这个问题
      • image.png
    • 还是不要重写原型的好

2、原型对象&构造函数&实例对象

  • 函数拥有prototype属性指向函数的原型对象
  • 原型对象有constructor属性指向prototype属性所在函数的指针
    • 举例:Person.prototype.constructor指向Person
  • 调用构造函数创建实例后,实例内部包含一个指针指向构造函数的原型对象
    • 在ECMA-262第5版中这个指针交[[Prototype]]
    • Firefox Safari Chrome在每个对象上都支持属性proto
    • 这个连接存在于实例与构造函数的原型对象之间,不存在于实例与构造函数之间
    • 实例中的指针仅指向原型,而不指向构造函数。

image.png

  • 虽然无法访问到[[Prototype]],但是可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。
  • 使用Object.getPrototypeOf()方法可以方便地取得一个对象的原型,这在利用原型实现继承的情况下是非常重要的。
  • 虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。
    • 实例中的同名属性只会屏蔽原型中的属性
    • 使用delete操作符可以完全删除实例属性
  • 使用hasOwnProperty()方法可以检测一个属性是存在于实例中还是原型中。只有当对象实例重写了属性后该函数才会返回true

3、原型与in

  • 在单独使用in时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。image.png
  • 在使用for-in循环时,返回所有可通过对象访问的、可枚举的属性,无论该属性存在于实例中还是原型中。(该方法在IE中存在Bug)
  • 要取得对象上所有可枚举的实例属性,可以使用ES5的Object.keys()方法,该方法可用于代替for-in循环

4、原型的动态性

  • 对原型对象所做的任何修改都能立即从实例上反映出来。
  • 但是重写整个原型对象,情况就不一样了。
    • 调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。
    • 同时也会切断原型对象和构造函数的联系,但可以通过Object.defineProperty()解决
    • 总之别重写原型对象!!!

5、原生对象的原型

  • 原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型(Object Array String),都是采用这种模式创建的
    • Array.prototype中可以找到sort()方法
    • String.prototype中可以找到substring()方法
  • 也可以为原生对象的原型定义新方法,但是不推荐在产品化的程序中修改原生对象的原型,可能会出现命名冲突或意外重写原生方法。

6、原型对象的问题

  • 省略了为构造函数传递初始化参数的环境,因此所有实例在默认情况下都将取得相同的属性值
  • 原型中所有属性是被很多实例共享的,这种共享对于函数非常适合,对于包含基本值的属性也说得过去,然而对于包含引用类型值的属性问题突出。image.png
  • 这个问题正是我们很少看到有人单独使用原型模式的原因所在。

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

  • 构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性。

  • 每个实例都有自己的一份实例属性的副本,但同时又共享着对方法的引用。
  • 这种混成模式支持向构造函数传递参数,集两种模式之长。image.png

    2.5 动态原型模式

  • 将构造函数模式和原型模式割裂着写比较奇怪。

  • 动态原型模式把所有信息封装在了构造函数中
  • 使用动态原型模式时,不能使用对象字面量重写原型
  • 可以通过检查某个应该存在的方法是否有效,来决定是否要初始化原型,如下:

    1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2841570/1612173101815-3f2ccb6c-98a7-496d-82c1-a28ed540d01e.png#align=left&display=inline&height=137&margin=%5Bobject%20Object%5D&name=image.png&originHeight=184&originWidth=496&size=28825&status=done&style=none&width=368)
    • 只在sayName()方法不存在的情况下(未在构造函数中添加),才会将它添加到原型中。
    • 这段代码只会在除此调用构造函数时执行,此后原型已经完成初始化不需要再做什么修改。
    • 其中if语句检查的可以是初始化之后应该存在的任何属性或方法(只要检查其中一个即可)

      2.6 寄生构造函数模式

  • 基本思想:创建一个函数,其作用仅仅是封装创建对象的代码,然后再返回新创建的对象。image.png

  • 除了使用new操作符,并把使用的包装函数叫做构造函数之外,这个模式根工厂模式其实是一模一样的。
  • 没有返回值的构造函数默认返回新对象实例,添加返回值可以重写构造函数的默认返回值。
  • 使用场景:假设我们想创建一个具有额外方法的特殊数组,但不能直接修改Array构造函数,则可以使用这个模式。
    • image.png
  • 缺点:返回的对象与构造函数和构造函数的原型属性之间没有关系,因此不能依赖instanceof操作符来确定对象类型。

    2.7 稳妥构造函数模式

  • 稳妥对象指没有公共属性,其方法也不引用this的对象。

  • 稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:
    • 新创建对象的实例方法不引用this
    • 不使用new操作符调用构造函数
  • 结构如下:
    • image.png
  • 在这种模式创建的对象中,除了使用sayName()方法之外没有其他方法访问其数据成员
  • 缺点:与寄生构造函数模式一样

    3、继承

    3.1原型链

  • 基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。

  • 实现方法:让原型对象等于另一个类型的实例。
    • image.png
  • 所有函数的默认原型都是Object,这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因
  • 谨慎地定义方法
    • 子类型有时需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎么样,给原型添加方法的代码一定要放在替换原型的语句之后。
    • 说白了就是先继承后重写
  • 问题

    • 所有的实例都会共享引用类型属性
    • 在创建子类型的实例时,不能向超类型的构造函数中传递参数
    • 实践中很少会单独使用原型链

      3.2 借用构造函数

  • 可以解决上述原型链的问题

  • 基本思想:在子类型构造函数的内部调用超类型构造函数
    • image.png
  • 可以在子类型构造函数中向超类型构造函数传递参数
    • image.png
    • 在SubType构造函数内部调用SuperType构造函数时,实际上是为SubType的实例设置了name属性。
    • 为了确保SuperType构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。
  • 存在问题:

    • 无法避免构造函数模式存在的问题。
    • 超类型的原型中定义的方法对子类型而言不可见。
    • 同样很少单独使用

      3.3 组合继承

  • 也叫伪经典继承,将原型链和借用构造函数组合到一起。

  • 基本思想:使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承。
  • 既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性。image.png
  • 成为JavaScript中最常用的继承模式。

    3.4 原型式继承

  • 思想:借助原型基于已有的对象创建新对象。

  • 在object()函数内部先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。
    • image.png
  • ES5通过新增Object.create()方法规范化了原型式继承

    • image.png

      3.5 寄生式继承

      思路:创建一个仅用于封装继承过程的函数

      3.6 寄生组合式继承

  • 组合继承是javascript最常用的继承模式,不过也有不足:无论什么情况,都会调用两次超类型构造函数

    • 一次是在创建子类型原型的时候。
    • 一次在子类型构造函数内部。
      • image.png

        4 总结

        1、创建对象。ECMAScript支持面向对象OO编程,在没有类的情况下(ES6新增了类),可以采用下列模式创建对象:
  • 工厂模式:使用简单函数创建对象,为对象添加属性和方法,然后返回对象,这个模式后来被构造函数模式所取代。

  • 构造函数模式:可以创建自定义引用类型,可以像创建内置对象实例一样使用new操作符。缺点在于每个成员都无法得到复用,包括函数。
  • 原型模式:使用构造函数的prototype属性来指定那些应该共享的属性和方法。组合使用构造函数模式和原型模式时,使用构造函数定义实例属性,使用原型定义共享的属性和方法。

2、对象的继承。JavaScript主要通过原型链实现继承,原型链的构建通过将一个类型的实例赋值给另一个构造函数的原型实现。原型链的问题在于对象实例共享所有继承的属性和方法,因此不适合单独使用。

  • 借用构造函数
    • 在子类型构造函数内部调用超类型构造函数
    • 每个实例都有自己的属性,同时还能保住只使用构造函数模式来定义类型。
  • 组合继承是使用最多的继承模式
    • 使用原型链继承贡献的属性和方法,而通过借用构造函数继承实例属性。
  • 此外还存在下列可供选择的继承模式:
    • 原型式继承:可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制。而复制得到的副本还可以得到进一步改造。
    • 寄生式继承:与原型式继承相似,为解决组合继承模式由于多次调用超类型构造函数而导致的低效率问题,可以将这个模式与组合继承一起使用。
    • 寄生组合式继承:集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效方式。